mirror of
https://github.com/OCA/knowledge.git
synced 2025-07-25 18:08:42 -06:00
Merge remote-tracking branch 'ir_attachment_exchange/master' into 8.0-external_file_sync
This commit is contained in:
commit
3fc73f7fd1
27
external_file_location/__init__.py
Normal file
27
external_file_location/__init__.py
Normal file
@ -0,0 +1,27 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2014 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from . import attachment
|
||||
from . import location
|
||||
from . import task
|
||||
from . import tasks
|
||||
|
46
external_file_location/__openerp__.py
Normal file
46
external_file_location/__openerp__.py
Normal file
@ -0,0 +1,46 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
{
|
||||
'name': 'external_file_location',
|
||||
'version': '0.0.1',
|
||||
'author': 'Akretion',
|
||||
'website': 'www.akretion.com',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Generic Modules',
|
||||
'description': """
|
||||
File exchange system with multiple protocol (SFTP, FTP, Filestore)
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
'ir_attachment_metadata',
|
||||
],
|
||||
'data': [
|
||||
'attachment_view.xml',
|
||||
'menu.xml',
|
||||
'location_view.xml',
|
||||
'task_view.xml',
|
||||
'cron.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
}
|
20
external_file_location/abstract_task.py
Executable file
20
external_file_location/abstract_task.py
Executable file
@ -0,0 +1,20 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from base64 import b64encode
|
||||
|
||||
|
||||
class AbstractTask(object):
|
||||
|
||||
def create_file(self, filename, data):
|
||||
ir_attachment_id = self.env['ir.attachment'].create(
|
||||
{
|
||||
'name': filename,
|
||||
'datas': b64encode(data),
|
||||
'datas_fname': filename,
|
||||
'task_id': self.task.id,
|
||||
'location_id': self.task.location_id.id,
|
||||
'external_hash': self.ext_hash
|
||||
}
|
||||
)
|
||||
return ir_attachment_id
|
39
external_file_location/attachment.py
Normal file
39
external_file_location/attachment.py
Normal file
@ -0,0 +1,39 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2014 Akretion (http://www.akretion.com).
|
||||
# @author Sébastien BEAU <sebastien.beau@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from openerp import models, fields
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
sync_date = fields.Datetime()
|
||||
state = fields.Selection([
|
||||
('pending', 'Pending'),
|
||||
('failed', 'Failed'),
|
||||
('done', 'Done'),
|
||||
], readonly=False, required=True, default='pending')
|
||||
state_message = fields.Text()
|
||||
task_id = fields.Many2one('external.file.task', string='Task')
|
||||
location_id = fields.Many2one('external.file.location', string='Location',
|
||||
related='task_id.location_id', store=True
|
||||
)
|
99
external_file_location/attachment_view.xml
Normal file
99
external_file_location/attachment_view.xml
Normal file
@ -0,0 +1,99 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_attachment_improved_form" model="ir.ui.view">
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="inherit_id" ref="base.view_attachment_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="url" position="after">
|
||||
<field name="sync_date"/>
|
||||
<field name="state"/>
|
||||
<field name="state_message"/>
|
||||
<field name="task_id"/>
|
||||
<field name="location_id"/>
|
||||
</field>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_external_attachment_tree" model="ir.ui.view">
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Attachments" >
|
||||
<field name="name"/>
|
||||
<field name="datas_fname"/>
|
||||
<field name="task_id"/>
|
||||
<field name="location_id"/>
|
||||
<field name="type"/>
|
||||
<field name="create_date"/>
|
||||
<field name="state"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_external_attachment_search" model="ir.ui.view">
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="Attachments">
|
||||
<field name="name" filter_domain="['|', ('name','ilike',self), ('datas_fname','ilike',self)]" string="Attachment"/>
|
||||
<field name="create_date"/>
|
||||
<filter icon="terp-stage"
|
||||
string="URL"
|
||||
domain="[('type','=','url')]"/>
|
||||
<filter icon="terp-stock_align_left_24"
|
||||
string="Binary"
|
||||
domain="[('type','=','binary')]"/>
|
||||
<separator/>
|
||||
<filter name="my_documents_filter"
|
||||
string="My Document(s)"
|
||||
icon="terp-personal"
|
||||
domain="[('create_uid','=',uid)]"
|
||||
help="Filter on my documents"/>
|
||||
<field name="create_uid"/>
|
||||
<field name="type"/>
|
||||
<filter string="Pending" domain="[('state', '=', 'pending')]"/>
|
||||
<filter string="Failed" domain="[('state', '=', 'failed')]"/>
|
||||
<filter string="Done" domain="[('state', '=', 'done')]"/>
|
||||
<group expand="0" string="Group By">
|
||||
<filter string="Owner" icon="terp-personal" domain="[]" context="{'group_by':'create_uid'}"/>
|
||||
<filter string="Type" icon="terp-stock_symbol-selection" domain="[]" context="{'group_by':'type'}" groups="base.group_no_one"/>
|
||||
<filter string="Company" icon="terp-gtk-home" domain="[]" context="{'group_by':'company_id'}" groups="base.group_multi_company"/>
|
||||
<filter string="Creation Month" icon="terp-go-month" domain="[]" context="{'group_by':'create_date'}"/>
|
||||
<filter string="State" domain="[]" context="{'group_by': 'state'}"/>
|
||||
</group>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_attachment" model="ir.actions.act_window">
|
||||
<field name="name">Attachments</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">ir.attachment</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">kanban,tree,form</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
<field name="domain">[('task_id', '!=', False)]</field>
|
||||
<field name="search_view_id" ref="view_external_attachment_search"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_attachment_view1" model="ir.actions.act_window.view">
|
||||
<field eval="20" name="sequence"/>
|
||||
<field name="view_mode">kanban</field>
|
||||
<field name="view_id" ref="mail.view_document_file_kanban"/>
|
||||
<field name="act_window_id" ref="action_attachment"/>
|
||||
</record>
|
||||
|
||||
<record id="ir_attachment_view2" model="ir.actions.act_window.view">
|
||||
<field eval="10" name="sequence"/>
|
||||
<field name="view_mode">tree</field>
|
||||
<field name="view_id" ref="view_external_attachment_tree"/>
|
||||
<field name="act_window_id" ref="action_attachment"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_ir_attachment"
|
||||
parent="menu_file_exchange"
|
||||
sequence="20"
|
||||
action="action_attachment"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
18
external_file_location/cron.xml
Normal file
18
external_file_location/cron.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record model="ir.cron" id="cronjob_run_exchange_tasks">
|
||||
<field name='name'>Run file exchange tasks</field>
|
||||
<field name='interval_number'>30</field>
|
||||
<field name='interval_type'>minutes</field>
|
||||
<field name="numbercall">-1</field>
|
||||
<field name="active">True</field>
|
||||
<field name="doall" eval="False" />
|
||||
<field name="model">external.file.task</field>
|
||||
<field name="function">_run</field>
|
||||
<field name="args">([])</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
60
external_file_location/helper.py
Normal file
60
external_file_location/helper.py
Normal file
@ -0,0 +1,60 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Author: Joel Grand-Guillaume
|
||||
# Copyright 2011-2012 Camptocamp SA
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
def itersubclasses(cls, _seen=None):
|
||||
"""
|
||||
itersubclasses(cls)
|
||||
Generator over all subclasses of a given class, in depth first order.
|
||||
>>> list(itersubclasses(int)) == [bool]
|
||||
True
|
||||
>>> class A(object): pass
|
||||
>>> class B(A): pass
|
||||
>>> class C(A): pass
|
||||
>>> class D(B,C): pass
|
||||
>>> class E(D): pass
|
||||
>>>
|
||||
>>> for cls in itersubclasses(A):
|
||||
... print(cls.__name__)
|
||||
B
|
||||
D
|
||||
E
|
||||
C
|
||||
>>> # get ALL (new-style) classes currently defined
|
||||
>>> [cls.__name__ for cls in itersubclasses(object)] #doctest: +ELLIPSIS
|
||||
['type', ...'tuple', ...]
|
||||
"""
|
||||
#import pdb; pdb.set_trace()
|
||||
if not isinstance(cls, type):
|
||||
raise TypeError('itersubclasses must be called with '
|
||||
'new-style classes, not %.100r' % cls
|
||||
)
|
||||
if _seen is None:
|
||||
_seen = set()
|
||||
try:
|
||||
subs = cls.__subclasses__()
|
||||
except TypeError: # fails only when cls is type
|
||||
subs = cls.__subclasses__(cls)
|
||||
for sub in subs:
|
||||
if sub not in _seen:
|
||||
_seen.add(sub)
|
||||
yield sub
|
||||
for sub in itersubclasses(sub, _seen):
|
||||
yield sub
|
47
external_file_location/location.py
Normal file
47
external_file_location/location.py
Normal file
@ -0,0 +1,47 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from openerp import models, fields
|
||||
from abstract_task import AbstractTask
|
||||
from helper import itersubclasses
|
||||
|
||||
class Location(models.Model):
|
||||
_name = 'external.file.location'
|
||||
_description = 'Description'
|
||||
|
||||
name = fields.Char(string='Name')
|
||||
protocol = fields.Selection(selection='_get_protocol')
|
||||
address = fields.Char(string='Address')
|
||||
port = fields.Integer()
|
||||
login = fields.Char()
|
||||
password = fields.Char()
|
||||
task_ids = fields.One2many('external.file.task', 'location_id')
|
||||
|
||||
|
||||
def _get_protocol(self):
|
||||
res = []
|
||||
for cls in itersubclasses(AbstractTask):
|
||||
if not cls._synchronize_type:
|
||||
cls_info = (cls._key, cls._name)
|
||||
res.append(cls_info)
|
||||
return res
|
||||
|
64
external_file_location/location_view.xml
Normal file
64
external_file_location/location_view.xml
Normal file
@ -0,0 +1,64 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_location_form" model="ir.ui.view">
|
||||
<field name="model">external.file.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Location" version="7.0">
|
||||
<sheet>
|
||||
<group col="4">
|
||||
<div class="oe_title" style="width: 390px;" colspan="4">
|
||||
<label class="oe_edit_only" for="name" string="Location Name"/>
|
||||
<h1><field name="name" class="oe_inline"/></h1>
|
||||
</div>
|
||||
<newline/>
|
||||
<field name="protocol" colspan="2"/>
|
||||
<newline/>
|
||||
<field name="address" colspan="2"/>
|
||||
<field name="port" colspan="2"/>
|
||||
<field name="login" colspan="2"/>
|
||||
<field name="password" password="1" colspan="2"/>
|
||||
<separator string="Tasks" colspan="4"/>
|
||||
<field name="task_ids" colspan="4" nolabel="1">
|
||||
<tree>
|
||||
<field name="name"/>
|
||||
<field name="method"/>
|
||||
<field name="filename"/>
|
||||
<field name="filepath"/>
|
||||
<button name="run" type="object" string="Run" icon="gtk-execute"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_location_tree" model="ir.ui.view">
|
||||
<field name="model">external.file.location</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Location">
|
||||
<field name="name" select="1"/>
|
||||
<field name="protocol"/>
|
||||
<field name="address"/>
|
||||
<field name="login"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_location" model="ir.actions.act_window">
|
||||
<field name="name">Locations</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">external.file.location</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_ir_location"
|
||||
parent="menu_file_exchange"
|
||||
sequence="20"
|
||||
action="action_location"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
12
external_file_location/menu.xml
Normal file
12
external_file_location/menu.xml
Normal file
@ -0,0 +1,12 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<menuitem id="menu_file_exchange"
|
||||
parent="base.menu_administration"
|
||||
sequence="20"
|
||||
name="File exchange"
|
||||
/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
88
external_file_location/task.py
Normal file
88
external_file_location/task.py
Normal file
@ -0,0 +1,88 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from openerp import models, fields, api
|
||||
from helper import itersubclasses
|
||||
from abstract_task import AbstractTask
|
||||
|
||||
|
||||
class Task(models.Model):
|
||||
_name = 'external.file.task'
|
||||
_description = 'Description'
|
||||
|
||||
name = fields.Char()
|
||||
method = fields.Selection(selection='_get_method')
|
||||
method_type = fields.Char()
|
||||
filename = fields.Char()
|
||||
filepath = fields.Char()
|
||||
location_id = fields.Many2one('external.file.location', string='Location')
|
||||
attachment_ids = fields.One2many('ir.attachment', 'task_id',
|
||||
string='Attachment')
|
||||
delete_file = fields.Boolean(string='Delete file')
|
||||
move_file = fields.Boolean(string='Move file')
|
||||
move_path = fields.Char(string='Move path')
|
||||
|
||||
def _get_method(self):
|
||||
res = []
|
||||
for cls in itersubclasses(AbstractTask):
|
||||
if cls._synchronize_type:
|
||||
cls_info = (cls._key + '_' + cls._synchronize_type,
|
||||
cls._name + ' ' + cls._synchronize_type)
|
||||
res.append(cls_info)
|
||||
return res
|
||||
|
||||
@api.onchange('method')
|
||||
def onchage_method(self):
|
||||
if 'import' in self.method:
|
||||
self.method_type = 'import'
|
||||
elif 'export' in self.method:
|
||||
self.method_type = 'export'
|
||||
|
||||
@api.model
|
||||
def _run(self, domain=None):
|
||||
if not domain:
|
||||
domain = []
|
||||
tasks = self.env['external.file.task'].search(domain)
|
||||
tasks.run()
|
||||
|
||||
@api.one
|
||||
def run(self):
|
||||
for cls in itersubclasses(AbstractTask):
|
||||
if cls._synchronize_type and \
|
||||
cls._key + '_' + cls._synchronize_type == self.method:
|
||||
method_class = cls
|
||||
config = {
|
||||
'host': self.location_id.address,
|
||||
'user': self.location_id.login,
|
||||
'pwd': self.location_id.password,
|
||||
'port': self.location_id.port,
|
||||
'allow_dir_creation': False,
|
||||
'file_name': self.filename,
|
||||
'path': self.filepath,
|
||||
'attachment_ids': self.attachment_ids,
|
||||
'task': self,
|
||||
'move_path': self.move_path,
|
||||
'delete_file': self.delete_file,
|
||||
'move_file': self.move_file,
|
||||
}
|
||||
conn = method_class(self.env, config)
|
||||
conn.run()
|
61
external_file_location/task_view.xml
Normal file
61
external_file_location/task_view.xml
Normal file
@ -0,0 +1,61 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_task_form" model="ir.ui.view">
|
||||
<field name="model">external.file.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Tasks" version="7.0">
|
||||
<sheet>
|
||||
<field name="method_type" invisible="1"/>
|
||||
<group col="4">
|
||||
<div class="oe_title" style="width: 390px;" colspan="4">
|
||||
<label class="oe_edit_only" for="name" string="Product Name"/>
|
||||
<h1><field name="name" class="oe_inline"/></h1>
|
||||
</div>
|
||||
<field name="method" colspan="2"/>
|
||||
<field name="location_id" colspan="2"/>
|
||||
<field name="filename" colspan="4" attrs="{'invisible':[('method_type','!=','import')]}"/>
|
||||
<field name="filepath" colspan="4" />
|
||||
<field name="move_file" colspan="2" attrs="{'invisible':[('method_type','!=','import')]}"/>
|
||||
<field name="move_path" colspan="2" attrs="{'invisible':['|', ('move_file','=',False), ('method_type','!=','import')]}"/>
|
||||
<field name="delete_file" colspan="2" attrs="{'invisible':[('method_type','!=','import')]}"/>
|
||||
<group attrs="{'invisible':[('method_type','!=','export')]}" colspan="4">
|
||||
<separator string="Attachments" colspan="4" />
|
||||
<field name="attachment_ids" colspan="4" nolabel="1"/>
|
||||
</group>
|
||||
<button name="run" type="object" string="Run" icon="gtk-execute"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_task_tree" model="ir.ui.view">
|
||||
<field name="model">external.file.task</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="Tasks" >
|
||||
<field name="name" select="1"/>
|
||||
<field name="method"/>
|
||||
<field name="filename"/>
|
||||
<field name="filepath"/>
|
||||
<button name="run" type="object" string="Run" icon="gtk-execute"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="action_task" model="ir.actions.act_window">
|
||||
<field name="name">Tasks</field>
|
||||
<field name="type">ir.actions.act_window</field>
|
||||
<field name="res_model">external.file.task</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_id" eval="False"/>
|
||||
</record>
|
||||
|
||||
<menuitem id="menu_external_file_task"
|
||||
parent="menu_file_exchange"
|
||||
sequence="20"
|
||||
action="action_task"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
23
external_file_location/tasks/__init__.py
Normal file
23
external_file_location/tasks/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from . import ftp
|
73
external_file_location/tasks/filestore.py
Executable file
73
external_file_location/tasks/filestore.py
Executable file
@ -0,0 +1,73 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from ..backend import AbstractTask
|
||||
import sys
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
|
||||
class FileStore(AbstractTask):
|
||||
_key = "filestore"
|
||||
_name = "Filestore"
|
||||
_synchronize_type = None
|
||||
|
||||
def __init__(self, config):
|
||||
# super(FilestoreConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
|
||||
self.host = config.get('host', '')
|
||||
self.user = config.get('user', '')
|
||||
self.pwd = config.get('pwd', '')
|
||||
self.port = config.get('port', '')
|
||||
self.allow_dir_creation = config.get('allow_dir_creation', '')
|
||||
self.filename = config.get('filename', '')
|
||||
self.path = config.get('path', '')
|
||||
|
||||
def connect(self):
|
||||
return NotImplemented
|
||||
|
||||
def close(self):
|
||||
return NotImplemented
|
||||
|
||||
def get(self):
|
||||
if self.path:
|
||||
filepath = "{}/{}".format(self.path, self.filename)
|
||||
else:
|
||||
filepath = self.filename
|
||||
return open(filepath, 'r+b')
|
||||
|
||||
def put(self):
|
||||
if self.path:
|
||||
filepath = "{}/{}".format(self.path, self.filename)
|
||||
else:
|
||||
filepath = self.filename
|
||||
output = open(filepath, 'w+b')
|
||||
return True
|
||||
|
||||
def search(self):
|
||||
if self.path:
|
||||
filepath = "{}/{}".format(self.path, self.filename)
|
||||
else:
|
||||
filepath = self.filename
|
||||
connection_list_result = os.listdir(filepath)
|
||||
return [x for x in connection_list_result if filename in x]
|
||||
|
||||
|
||||
class ImportFileStore(FileStore):
|
||||
_synchronize_type = "import"
|
||||
|
||||
|
||||
def run():
|
||||
self.connect()
|
||||
file = self.get(self.filename)
|
||||
self.close()
|
||||
return file
|
||||
|
||||
class ExportFileStore(FileStore):
|
||||
_synchronize_type = "export"
|
||||
|
||||
|
||||
def run():
|
||||
self.connect()
|
||||
self.put(self.filename)
|
||||
self.close()
|
||||
|
182
external_file_location/tasks/ftp.py
Normal file
182
external_file_location/tasks/ftp.py
Normal file
@ -0,0 +1,182 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from ..abstract_task import AbstractTask
|
||||
from base64 import b64decode
|
||||
import ftputil
|
||||
import ftputil.session
|
||||
from ftputil.error import FTPIOError
|
||||
import logging
|
||||
from base64 import b64encode
|
||||
import hashlib
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class FtpTask(AbstractTask):
|
||||
|
||||
_key = 'ftp'
|
||||
_name = 'FTP'
|
||||
_synchronize_type = None
|
||||
|
||||
def __init__(self, env, config):
|
||||
self.env = env
|
||||
self.host = config.get('host', '')
|
||||
self.user = config.get('user', '')
|
||||
self.pwd = config.get('pwd', '')
|
||||
self.port = config.get('port', '')
|
||||
self.allow_dir_creation = config.get('allow_dir_creation', '')
|
||||
self.file_name = config.get('file_name', '')
|
||||
self.path = config.get('path', '.')
|
||||
self.move_path = config.get('move_path', '')
|
||||
self.move_file = config.get('move_file', False)
|
||||
self.delete_file = config.get('delete_file', False)
|
||||
self.attachment_ids = config.get('attachment_ids', False)
|
||||
self.task = config.get('task', False)
|
||||
self.ext_hash = False
|
||||
|
||||
|
||||
class FtpImportTask(FtpTask):
|
||||
"""FTP Configuration options:
|
||||
- host, user, password, port
|
||||
- download_directory: directory on the FTP server where files are
|
||||
downloaded from
|
||||
- move_directory: If present, files will be moved to this directory
|
||||
on the FTP server after download.
|
||||
- delete_files: If true, files will be deleted on the FTP server
|
||||
after download.
|
||||
"""
|
||||
|
||||
_synchronize_type = 'import'
|
||||
|
||||
def _handle_new_source(self, ftp_conn, download_directory, file_name,
|
||||
move_directory):
|
||||
"""open and read given file into create_file method,
|
||||
move file if move_directory is given"""
|
||||
with ftp_conn.open(self._source_name(download_directory, file_name),
|
||||
"rb") as fileobj:
|
||||
data = fileobj.read()
|
||||
return self.create_file(file_name, data)
|
||||
|
||||
def _source_name(self, download_directory, file_name):
|
||||
"""helper to get the full name"""
|
||||
return download_directory + '/' + file_name
|
||||
|
||||
def _move_file(self, ftp_conn, source, target):
|
||||
"""Moves a file on the FTP server"""
|
||||
_logger.info('Moving file %s %s' % (source, target))
|
||||
ftp_conn.rename(source, target)
|
||||
|
||||
def _delete_file(self, ftp_conn, source):
|
||||
"""Deletes a file from the FTP server"""
|
||||
_logger.info('Deleting file %s' % source)
|
||||
ftp_conn.remove(source)
|
||||
|
||||
@staticmethod
|
||||
def _get_hash(file_name, ftp_conn):
|
||||
with ftp_conn.open(file_name, 'rb') as f:
|
||||
return hashlib.md5(f.read()).hexdigest()
|
||||
|
||||
def run(self):
|
||||
port_session_factory = ftputil.session.session_factory(
|
||||
port=self.port)
|
||||
with ftputil.FTPHost(self.host, self.user,
|
||||
self.pwd,
|
||||
session_factory=port_session_factory) as ftp_conn:
|
||||
|
||||
path = self.path or '.'
|
||||
file_list = ftp_conn.listdir(path)
|
||||
downloaded_files = []
|
||||
for ftpfile in file_list:
|
||||
source_name = self._source_name(path, self.file_name)
|
||||
if ftp_conn.path.isfile(source_name) and \
|
||||
ftpfile == self.file_name:
|
||||
self.ext_hash = self._get_hash(source_name, ftp_conn)
|
||||
self._handle_new_source(
|
||||
ftp_conn,
|
||||
path,
|
||||
self.file_name,
|
||||
self.move_path)
|
||||
downloaded_files.append(self.file_name)
|
||||
|
||||
# Move/delete files only after all files have been processed.
|
||||
if self.delete_file:
|
||||
for ftpfile in downloaded_files:
|
||||
self._delete_file(ftp_conn,
|
||||
self._source_name(path,
|
||||
ftpfile))
|
||||
elif self.move_path:
|
||||
if not ftp_conn.path.exists(self.move_path):
|
||||
ftp_conn.mkdir(self.move_path)
|
||||
for ftpfile in downloaded_files:
|
||||
self._move_file(
|
||||
ftp_conn,
|
||||
self._source_name(path, ftpfile),
|
||||
self._source_name(self.move_path, ftpfile))
|
||||
|
||||
|
||||
class FtpExportTask(FtpTask):
|
||||
"""FTP Configuration options:
|
||||
- host, user, password, port
|
||||
- upload_directory: directory on the FTP server where files are
|
||||
uploaded to
|
||||
"""
|
||||
|
||||
_synchronize_type = 'export'
|
||||
|
||||
def _handle_existing_target(self, ftp_conn, target_name, filedata):
|
||||
raise Exception("%s already exists" % target_name)
|
||||
|
||||
def _handle_new_target(self, ftp_conn, target_name, filedata):
|
||||
try:
|
||||
with ftp_conn.open(target_name, mode='wb') as fileobj:
|
||||
fileobj.write(filedata)
|
||||
_logger.info('wrote %s, size %d', target_name, len(filedata))
|
||||
self.attachment_id.state = 'done'
|
||||
self.attachment_id.state_message = ''
|
||||
except FTPIOError:
|
||||
self.attachment_id.state = 'failed'
|
||||
self.attachment_id.state_message = (
|
||||
'The directory doesn\'t exist or had insufficient rights')
|
||||
|
||||
def _target_name(self, ftp_conn, upload_directory, filename):
|
||||
return upload_directory + '/' + filename
|
||||
|
||||
def _upload_file(self, host, port, user, pwd, path, filename, filedata):
|
||||
upload_directory = path or '.'
|
||||
port_session_factory = ftputil.session.session_factory(port=port)
|
||||
with ftputil.FTPHost(host, user, pwd,
|
||||
session_factory=port_session_factory) as ftp_conn:
|
||||
target_name = self._target_name(ftp_conn,
|
||||
upload_directory,
|
||||
filename)
|
||||
if ftp_conn.path.isfile(target_name):
|
||||
self._handle_existing_target(ftp_conn, target_name, filedata)
|
||||
else:
|
||||
self._handle_new_target(ftp_conn, target_name, filedata)
|
||||
|
||||
def run(self, async=True):
|
||||
for attachment in self.attachment_ids:
|
||||
if attachment.state in ('pending', 'failed'):
|
||||
self.attachment_id = attachment
|
||||
self._upload_file(self.host, self.port, self.user, self.pwd,
|
||||
self.path,
|
||||
attachment.datas_fname,
|
||||
b64decode(attachment.datas))
|
58
external_file_location/tasks/ftp_backup.py
Executable file
58
external_file_location/tasks/ftp_backup.py
Executable file
@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
|
||||
import sys
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
from ftplib import FTP
|
||||
|
||||
class FTPConnection(object):
|
||||
|
||||
def __init__(self, host, user, pwd, port=None, allow_dir_creation=False):
|
||||
super(FTPConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
|
||||
if not port:
|
||||
self.port = 21
|
||||
self.protocol = "FTP"
|
||||
|
||||
def connect(self):
|
||||
self.connection = FTP(self.location, self.port)
|
||||
self.connection.login(self.user, self.pwd)
|
||||
|
||||
def close(self):
|
||||
self.connection.close()
|
||||
|
||||
def get(self, filename, path=None):
|
||||
if path:
|
||||
filepath = "{}/{}".format(path, filename)
|
||||
else:
|
||||
filepath = filename
|
||||
outfile = TemporaryFile('w+b')
|
||||
self.connection.retrbinary('RETR ' + filepath, outfile.write)
|
||||
return outfile
|
||||
|
||||
def put(self, fileobject, filename, path=None):
|
||||
if path:
|
||||
filepath = "{}/{}".format(path, filename)
|
||||
else:
|
||||
filepath = filename
|
||||
self.connection.storbinary('STOR ' + filepath, fileobject)
|
||||
return True
|
||||
|
||||
def search(self, filename, path=None):
|
||||
if path:
|
||||
filepath = "{}/{}".format(path, filename)
|
||||
else:
|
||||
filepath = filename
|
||||
connection_list_result = self.connection.nlst()
|
||||
return [x for x in connection_list_result if filename in x]
|
||||
|
||||
|
||||
def move(self, filename, oldpath, newpath):
|
||||
self.connection.rename(
|
||||
os.path.join(oldpath, filename),
|
||||
os.path.join(newpath, filename)
|
||||
)
|
||||
|
||||
def rename(self, oldfilename, newfilename, path=None):
|
||||
return NotImplemented
|
59
external_file_location/tasks/sftp.py
Executable file
59
external_file_location/tasks/sftp.py
Executable file
@ -0,0 +1,59 @@
|
||||
#!/usr/bin/env python
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
import sys
|
||||
import paramiko
|
||||
import os
|
||||
from tempfile import TemporaryFile
|
||||
|
||||
|
||||
class SFTPConnection(AbstractConnection):
|
||||
|
||||
def __init__(self, host, user, pwd, port=None, allow_dir_creation=False):
|
||||
super(SFTPConnection, self).__init__(host, user, pwd, port, allow_dir_creation)
|
||||
if not port:
|
||||
self.port = 22
|
||||
self.protocol = "STFP"
|
||||
|
||||
def connect(self):
|
||||
self.ssh = paramiko.SSHClient()
|
||||
self.ssh.set_missing_host_key_policy(paramiko.AutoAddPolicy())
|
||||
self.ssh.connect(self.host, self.port, self.user, self.pwd, compress=True)
|
||||
self.connection = self.ssh.open_sftp()
|
||||
|
||||
def close(self):
|
||||
self.connection.close()
|
||||
|
||||
def get(self, filename, path=None):
|
||||
if path:
|
||||
remotefile = "{}/{}".format(path, filename)
|
||||
else:
|
||||
remotefile = filename
|
||||
localfile = filename
|
||||
newfile = open(filename, 'w')
|
||||
self.connection.getfo(remotefile, newfile)
|
||||
return newfile
|
||||
|
||||
def put(self, fileobject, filename, path=None):
|
||||
if path:
|
||||
remotefile = "{}/{}".format(path, filename)
|
||||
else:
|
||||
remotefile = filename
|
||||
if self.allow_dir_creation:
|
||||
self.connection.mkdirs(path)
|
||||
oldfile = open(fileobj, 'r')
|
||||
self.connection.putfo(oldfile, remotefile)
|
||||
|
||||
def search(self, filename, path=None):
|
||||
if path:
|
||||
self.connection.chdir(path)
|
||||
file_list = self.connection.listdir()
|
||||
return [x for x in file_list if filename in x]
|
||||
|
||||
def move(self, filename, oldpath, newpath):
|
||||
self.connection.rename(os.path.join(oldpath, filename), os.path.join(newpath, filename))
|
||||
|
||||
def rename(self, oldfilename, newfilename, path=None):
|
||||
if not path:
|
||||
path = ''
|
||||
self.connection.rename(os.path.join(path, oldfilename), os.path.join(path, newfilename))
|
23
ir_attachment_metadata/__init__.py
Normal file
23
ir_attachment_metadata/__init__.py
Normal file
@ -0,0 +1,23 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from . import attachment
|
45
ir_attachment_metadata/__openerp__.py
Normal file
45
ir_attachment_metadata/__openerp__.py
Normal file
@ -0,0 +1,45 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
{'name': 'ir_attachment_metadata',
|
||||
'version': '0.0.1',
|
||||
'author': 'Akretion',
|
||||
'website': 'www.akretion.com',
|
||||
'license': 'AGPL-3',
|
||||
'category': 'Generic Modules',
|
||||
'description': """
|
||||
Add some useful field to ir.attachment object like:
|
||||
internal and external hash for coherence verification
|
||||
""",
|
||||
'depends': [
|
||||
'base',
|
||||
],
|
||||
'data': [
|
||||
'attachment_view.xml',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
42
ir_attachment_metadata/attachment.py
Normal file
42
ir_attachment_metadata/attachment.py
Normal file
@ -0,0 +1,42 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
###############################################################################
|
||||
#
|
||||
# Module for OpenERP
|
||||
# Copyright (C) 2015 Akretion (http://www.akretion.com).
|
||||
# @author Valentin CHEMIERE <valentin.chemiere@akretion.com>
|
||||
#
|
||||
# This program is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License as
|
||||
# published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# This program is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
###############################################################################
|
||||
|
||||
from openerp import models, fields, api, _
|
||||
from openerp.exceptions import Warning
|
||||
import hashlib
|
||||
from base64 import b64decode
|
||||
|
||||
|
||||
class IrAttachment(models.Model):
|
||||
_inherit = 'ir.attachment'
|
||||
|
||||
internal_hash = fields.Char(store=True, compute='_compute_hash')
|
||||
external_hash = fields.Char()
|
||||
|
||||
@api.depends('datas', 'external_hash')
|
||||
def _compute_hash(self):
|
||||
if self.datas:
|
||||
self.internal_hash = hashlib.md5(b64decode(self.datas)).hexdigest()
|
||||
if self.external_hash and self.internal_hash != self.external_hash:
|
||||
raise Warning(_('File corrupted'),
|
||||
_("Something was wrong with the retreived file, "
|
||||
"please relaunch the task."))
|
18
ir_attachment_metadata/attachment_view.xml
Normal file
18
ir_attachment_metadata/attachment_view.xml
Normal file
@ -0,0 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record id="view_attachment_improved_form" model="ir.ui.view">
|
||||
<field name="model">ir.attachment</field>
|
||||
<field name="inherit_id" ref="base.view_attachment_form" />
|
||||
<field name="arch" type="xml">
|
||||
<field name="url" position="after">
|
||||
<field name="internal_hash"/>
|
||||
<field name="external_hash"/>
|
||||
</field>
|
||||
|
||||
</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
Loading…
Reference in New Issue
Block a user