From eb665583aad31f0f06326beb698fa34c26ba4c72 Mon Sep 17 00:00:00 2001 From: Valentin Chemiere Date: Wed, 17 Jun 2015 12:14:35 +0200 Subject: [PATCH 1/4] Add file import/export POC external_file_location integration in connector_flow --- external_file_location/README.rst | 59 ++++++++ external_file_location/__init__.py | 27 ++++ external_file_location/__openerp__.py | 49 ++++++ external_file_location/abstract_task.py | 26 ++++ external_file_location/attachment.py | 39 +++++ external_file_location/attachment_view.xml | 99 ++++++++++++ external_file_location/cron.xml | 18 +++ external_file_location/helper.py | 60 ++++++++ external_file_location/location.py | 69 +++++++++ external_file_location/location_view.xml | 69 +++++++++ external_file_location/menu.xml | 12 ++ .../security/ir.model.access.csv | 3 + external_file_location/task.py | 99 ++++++++++++ external_file_location/task_view.xml | 46 ++++++ external_file_location/tasks/__init__.py | 26 ++++ external_file_location/tasks/abstract_fs.py | 140 +++++++++++++++++ external_file_location/tasks/filestore.py | 69 +++++++++ external_file_location/tasks/ftp.py | 67 +++++++++ external_file_location/tasks/sftp.py | 73 +++++++++ external_file_location/tests/__init__.py | 24 +++ external_file_location/tests/mock_server.py | 75 ++++++++++ external_file_location/tests/test_sftp.py | 141 ++++++++++++++++++ 22 files changed, 1290 insertions(+) create mode 100644 external_file_location/README.rst create mode 100644 external_file_location/__init__.py create mode 100644 external_file_location/__openerp__.py create mode 100644 external_file_location/abstract_task.py create mode 100644 external_file_location/attachment.py create mode 100644 external_file_location/attachment_view.xml create mode 100644 external_file_location/cron.xml create mode 100644 external_file_location/helper.py create mode 100644 external_file_location/location.py create mode 100644 external_file_location/location_view.xml create mode 100644 external_file_location/menu.xml create mode 100644 external_file_location/security/ir.model.access.csv create mode 100644 external_file_location/task.py create mode 100644 external_file_location/task_view.xml create mode 100644 external_file_location/tasks/__init__.py create mode 100644 external_file_location/tasks/abstract_fs.py create mode 100644 external_file_location/tasks/filestore.py create mode 100644 external_file_location/tasks/ftp.py create mode 100644 external_file_location/tasks/sftp.py create mode 100644 external_file_location/tests/__init__.py create mode 100644 external_file_location/tests/mock_server.py create mode 100644 external_file_location/tests/test_sftp.py diff --git a/external_file_location/README.rst b/external_file_location/README.rst new file mode 100644 index 000000000..d245fa00a --- /dev/null +++ b/external_file_location/README.rst @@ -0,0 +1,59 @@ +.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg + :alt: License + +External File Location +====================== + +This module was written to extend the functionality of ir.attachment to support remote communication and allow you to import/export file to a remote server + +Installation +============ + +To install this module, you need to: + +* fs python module +* Paramiko python module + +Usage +===== + +To use this module, you need to: + +* Add a location with your server infos +* Create a task with your file info and remote communication method +* A cron task will trigger each task + +For further information, please visit: + +* https://www.odoo.com/forum/help-1 + +Known issues / Roadmap +====================== + + +Credits +======= + +* Joel Grand-Guillaume Camptocamp +* initOS +* Valentin CHEMIERE + +Contributors +------------ + +* Sebastien BEAU + +Maintainer +---------- + +* Valentin CHEMIERE + +.. image:: http://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: http://odoo-community.org + +This module is maintained by the OCA. + +OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use. + +To contribute to this module, please visit http://odoo-community.org. diff --git a/external_file_location/__init__.py b/external_file_location/__init__.py new file mode 100644 index 000000000..101a9f4ad --- /dev/null +++ b/external_file_location/__init__.py @@ -0,0 +1,27 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Module for OpenERP +# Copyright (C) 2014 Akretion (http://www.akretion.com). +# @author Sébastien BEAU +# +# 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 . +# +############################################################################### + +from . import attachment +from . import location +from . import task +from . import tasks +from . import tests diff --git a/external_file_location/__openerp__.py b/external_file_location/__openerp__.py new file mode 100644 index 000000000..8d286d4e8 --- /dev/null +++ b/external_file_location/__openerp__.py @@ -0,0 +1,49 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Module for OpenERP +# Copyright (C) 2015 Akretion (http://www.akretion.com). +# @author Valentin CHEMIERE +# +# 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 . +# +############################################################################### + +{ + 'name': 'external_file_location', + 'version': '0.0.1', + 'author': 'Akretion', + 'website': 'www.akretion.com', + 'license': 'AGPL-3', + 'category': 'Generic Modules', + 'depends': [ + 'attachment_metadata', + ], + 'external_dependencies': { + 'python': [ + 'fs', + 'paramiko', + ], + }, + 'data': [ + 'menu.xml', + 'attachment_view.xml', + 'location_view.xml', + 'task_view.xml', + 'cron.xml', + 'security/ir.model.access.csv', + ], + 'installable': True, + 'application': True, + } diff --git a/external_file_location/abstract_task.py b/external_file_location/abstract_task.py new file mode 100644 index 000000000..15e2323e9 --- /dev/null +++ b/external_file_location/abstract_task.py @@ -0,0 +1,26 @@ +#!/usr/bin/env python +# -*- coding: utf-8 -*- + +from base64 import b64encode + + +class AbstractTask(object): + + _name = None + _key = None + _synchronize_type = None + _default_port = None + _hide_login = False + _hide_password = False + _hide_port = False + + def create_file(self, filename, data): + ir_attachment_id = self.env['ir.attachment.metadata'].create({ + 'name': filename, + 'datas': b64encode(data), + 'datas_fname': filename, + 'task_id': self.task and self.task.id or False, + 'location_id': self.task and self.task.location_id.id or False, + 'external_hash': self.ext_hash + }) + return ir_attachment_id diff --git a/external_file_location/attachment.py b/external_file_location/attachment.py new file mode 100644 index 000000000..26925547b --- /dev/null +++ b/external_file_location/attachment.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Module for OpenERP +# Copyright (C) 2015 Akretion (http://www.akretion.com). +# @author Valentin CHEMIERE +# +# 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 . +# +############################################################################### + +from openerp import models, fields + + +class IrAttachmentMetadata(models.Model): + _inherit = 'ir.attachment.metadata' + + 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 + ) diff --git a/external_file_location/attachment_view.xml b/external_file_location/attachment_view.xml new file mode 100644 index 000000000..b11b6c4f6 --- /dev/null +++ b/external_file_location/attachment_view.xml @@ -0,0 +1,99 @@ + + + + + + ir.attachment.metadata + + + + + + + + + + + + + + ir.attachment.metadata + + + + + + + + + + + + + + + ir.attachment.metadata + + + + + + + + + + + + + + + + + + + + + + + + + + Attachments + ir.actions.act_window + ir.attachment.metadata + form + tree,form + + + + + + + + tree + + + + + + + form + + + + + + + + diff --git a/external_file_location/cron.xml b/external_file_location/cron.xml new file mode 100644 index 000000000..a18f5f6a2 --- /dev/null +++ b/external_file_location/cron.xml @@ -0,0 +1,18 @@ + + + + + + Run file exchange tasks + 30 + minutes + -1 + True + + external.file.task + _run + ([]) + + + + diff --git a/external_file_location/helper.py b/external_file_location/helper.py new file mode 100644 index 000000000..6bfe08dcb --- /dev/null +++ b/external_file_location/helper.py @@ -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 . +# +############################################################################## + + +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', ...] + """ + 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 diff --git a/external_file_location/location.py b/external_file_location/location.py new file mode 100644 index 000000000..91d4ad2f8 --- /dev/null +++ b/external_file_location/location.py @@ -0,0 +1,69 @@ +# -*- coding: utf-8 -*- +############################################################################### +# +# Module for OpenERP +# Copyright (C) 2015 Akretion (http://www.akretion.com). +# @author Valentin CHEMIERE +# +# 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 . +# +############################################################################### + +from openerp import models, fields, api +from .abstract_task import AbstractTask +from .helper import itersubclasses + + +class Location(models.Model): + _name = 'external.file.location' + _description = 'Description' + + name = fields.Char(string='Name', required=True) + protocol = fields.Selection(selection='_get_protocol', required=True) + address = fields.Char(string='Address', required=True) + port = fields.Integer() + login = fields.Char() + password = fields.Char() + task_ids = fields.One2many('external.file.task', 'location_id') + hide_login = fields.Boolean() + hide_password = fields.Boolean() + hide_port = fields.Boolean() + + 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) + elif not cls._synchronize_type and cls._key and cls._name: + pass + return res + + @api.onchange('protocol') + def onchange_protocol(self): + for cls in itersubclasses(AbstractTask): + if cls._key == self.protocol: + self.port = cls._default_port + if cls._hide_login: + self.hide_login = True + else: + self.hide_login = False + if cls._hide_password: + self.hide_password = True + else: + self.hide_password = False + if cls._hide_port: + self.hide_port = True + else: + self.hide_port = False diff --git a/external_file_location/location_view.xml b/external_file_location/location_view.xml new file mode 100644 index 000000000..5d2ee0a9c --- /dev/null +++ b/external_file_location/location_view.xml @@ -0,0 +1,69 @@ + + + + + + external.file.location + +
+ + +
+
+ + + + + + + + + + + + + + + +