Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
b95fb37
Add file import/export
Jun 17, 2015
2e47b90
POC external_file_location integration in connector_flow
Jun 24, 2015
136a29d
[FIX] encode to utf8 user/password
bealdav Feb 23, 2016
4075ae2
[FIX] prevent not installed class been evaluated
bealdav Feb 23, 2016
85ff3cc
[FIX] python header in akretion modules
bealdav Feb 23, 2016
4247a7f
[FIX] check if class is in installed module, README
bealdav Feb 24, 2016
c9a0449
[FIX] travis for dependency on a PR
bealdav Feb 26, 2016
6ea558f
[WIP] Use pattern in file name & add move and rename file option
mourad-ehm Mar 1, 2016
3a453b5
[WIP] use jinja to render new file name based on simple template
mourad-ehm Mar 3, 2016
fcd3cff
[FIX] bug move and rename file option
mourad-ehm Mar 4, 2016
763c8db
[ADD] add file type on task
mourad-ehm Mar 4, 2016
d7d3702
[FIX] add file type in attachemnt create method
mourad-ehm Mar 7, 2016
3425043
[WIP] test rename file
mourad-ehm Mar 7, 2016
e00298e
[WIP] inherit view from attachment_metadata
mourad-ehm Mar 9, 2016
c4d4ce7
[FIX] bug of inherit view and menu from attachment_metadata
mourad-ehm Mar 10, 2016
9fcf565
[IMP] add move & rename test to test_sftp
mourad-ehm Mar 10, 2016
b216db2
[IMP] move file location menu inside automation menu and rename it
mourad-ehm Mar 10, 2016
272e566
[IMP] add ir.model.access manager rule
mourad-ehm Mar 11, 2016
61e2727
[FIX] typing mistake
mourad-ehm Mar 11, 2016
21d9136
[FIX] access file store without login/password
mourad-ehm Mar 14, 2016
46ec646
[IMP] reorganize task form view
mourad-ehm Mar 14, 2016
db6dfd0
[FIX] api.multi in task run method
mourad-ehm Mar 15, 2016
c7668b9
[FIX] OCA guidelines
mourad-ehm Mar 17, 2016
20fee96
[FIX] add authors and contributors
mourad-ehm Mar 21, 2016
4d9c30d
[FIX] fix pylint
mourad-ehm May 25, 2016
489fe45
[FIX] add oca_dependencies & fixe oca version format
mourad-ehm May 25, 2016
6c29cec
[FIX] fix pylint & oca_dependencies
mourad-ehm May 26, 2016
58ba998
[FIX] python fs lib to travis
mourad-ehm May 26, 2016
ef7c158
[FIX] fix views path
mourad-ehm May 26, 2016
0bea727
[FIX] set application to False
mourad-ehm May 31, 2016
d88b06a
[FIX] Move fs lib from travis.yml to requirements.txt
mourad-ehm May 31, 2016
f99e8c6
[FIX] delete akretion branch from travis.yml
mourad-ehm May 31, 2016
9890697
[FIX] Remove dependency to Akretion repository in oca_dependencies.txt
mourad-ehm Jun 2, 2016
54a4a4f
[FIX] fs lib name
mourad-ehm Jun 3, 2016
1a57029
[FIX] fs lib name
mourad-ehm Jun 3, 2016
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
60 changes: 60 additions & 0 deletions external_file_location/README.rst
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
.. 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 <http://initos.com>
* Valentin CHEMIERE <valentin.chemiere@akretion.com>
* Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>


Contributors
------------

* Sebastien BEAU <sebastian.beau@akretion.com>
* David BEAL <david.beal@akretion.com>

Maintainer
----------

.. 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.
3 changes: 3 additions & 0 deletions external_file_location/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import models
from . import tasks
from . import tests
33 changes: 33 additions & 0 deletions external_file_location/__openerp__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,33 @@
# coding: utf-8
# @ 2015 Valentin CHEMIERE @ Akretion
# © 2016 @author Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

{
'name': 'external_file_location',
'version': '8.0.1.0.0',
'author': 'Akretion,Odoo Community Association (OCA)',
'website': 'www.akretion.com',
'license': 'AGPL-3',
'category': 'Generic Modules',
'depends': [
'attachment_metadata',
],
'external_dependencies': {
'python': [
'fs',
'paramiko',

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should we add paramiko to requirements.txt too?

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't see it being used in the code actually. Looks like the depends are now ftplib and pysftp

],
},
'data': [
'views/menu.xml',
'views/attachment_view.xml',
'views/location_view.xml',
'views/task_view.xml',
'data/cron.xml',
'security/ir.model.access.csv',
],
'installable': True,
'application': False,
'images': [],
}
18 changes: 18 additions & 0 deletions external_file_location/data/cron.xml
Original file line number Diff line number Diff line change
@@ -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>
3 changes: 3 additions & 0 deletions external_file_location/models/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from . import attachment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

license?

from . import location
from . import task
21 changes: 21 additions & 0 deletions external_file_location/models/attachment.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# coding: utf-8
# @ 2015 Valentin CHEMIERE @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

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)
84 changes: 84 additions & 0 deletions external_file_location/models/helper.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,84 @@
# coding: utf-8
# Author: Joel Grand-Guillaume
# Copyright 2011-2012 Camptocamp SA
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).


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


def _get_erp_module_name(module_path):
# see this PR for v9 https://github.com/odoo/odoo/pull/11084
""" Extract the name of the Odoo module from the path of the
Python module.

Taken from Odoo server: ``openerp.models.MetaModel``

The (Odoo) module name can be in the ``openerp.addons`` namespace
or not. For instance module ``sale`` can be imported as
``openerp.addons.sale`` (the good way) or ``sale`` (for backward
compatibility).
"""
module_parts = module_path.split('.')
if len(module_parts) > 2 and module_parts[:2] == ['openerp', 'addons']:
module_name = module_parts[2]
else:
module_name = module_parts[0]
return module_name


def is_module_installed(env, module_name):
""" Check if an Odoo addon is installed.

:param module_name: name of the addon
"""
# the registry maintains a set of fully loaded modules so we can
# lookup for our module there
return module_name in env.registry._init_modules


def get_erp_module(cls_or_func):
""" For a top level function or class, returns the
name of the Odoo module where it lives.

So we will be able to filter them according to the modules
installation state.
"""
return _get_erp_module_name(cls_or_func.__module__)
51 changes: 51 additions & 0 deletions external_file_location/models/location.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
# coding: utf-8
# @ 2015 Valentin CHEMIERE @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from openerp import models, fields, api
from ..tasks.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
115 changes: 115 additions & 0 deletions external_file_location/models/task.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
# coding: utf-8
# @ 2015 Valentin CHEMIERE @ Akretion
# © @author Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).

from openerp import models, fields, api
from .helper import itersubclasses, get_erp_module, is_module_installed
from ..tasks.abstract_task import AbstractTask


class Task(models.Model):
_name = 'external.file.task'
_description = 'External file task'

name = fields.Char(required=True)
method = fields.Selection(selection='_get_method', required=True,
help='procotol and trasmitting info')
method_type = fields.Char()
filename = fields.Char(help='File name which is imported.'
'You can use file pattern like *.txt'
'to import all txt files')
filepath = fields.Char(help='Path to imported file')
location_id = fields.Many2one('external.file.location', string='Location',
required=True)
attachment_ids = fields.One2many('ir.attachment.metadata', 'task_id',
string='Attachment')
move_path = fields.Char(string='Move path',
help='Imported File will be moved to this path')
new_name = fields.Char(string='New name',
help='Imported File will be renamed to this name'
'Name can use mako template where obj is an '
'ir_attachement. template exemple : '
' ${obj.name}-${obj.create_date}.csv')
md5_check = fields.Boolean(help='Control file integrity after import with'
' a md5 file')
after_import = fields.Selection(selection='_get_action',
help='Action after import a file')
file_type = fields.Selection(
selection="_get_file_type",
string="File type",
help="The file type determines an import method to be used "
"to parse and transform data before their import in ERP")

def _get_action(self):
return [('rename', 'Rename'),
('move', 'Move'),
('move_rename', 'Move & Rename'),
('delete', 'Delete'),
]

def _get_file_type(self):
"""This is the method to be inherited for adding file types
The basic import do not apply any parsing or transform of the file.
The file is just added as an attachement
"""
return [('basic_import', 'Basic import')]

def _get_method(self):
res = []
for cls in itersubclasses(AbstractTask):
if not is_module_installed(self.env, get_erp_module(cls)):
continue
if cls._synchronize_type and (
'protocol' not in self._context or
cls._key == self._context['protocol']):
cls_info = (cls._key + '_' + cls._synchronize_type,
cls._name + ' ' + cls._synchronize_type)
res.append(cls_info)
return res

@api.onchange('method')
def onchange_method(self):
if self.method:
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.multi
def run(self):
for tsk in self:
for cls in itersubclasses(AbstractTask):
if not is_module_installed(self.env, get_erp_module(cls)):
continue
cls_build = '%s_%s' % (cls._key, cls._synchronize_type)
if cls._synchronize_type and cls_build == tsk.method:
method_class = cls
config = {
'host': tsk.location_id.address,
# ftplib does not support unicode
'user': tsk.location_id.login and\
tsk.location_id.login.encode('utf-8'),
'pwd': tsk.location_id.password and \
tsk.location_id.password.encode('utf-8'),
'port': tsk.location_id.port,
'allow_dir_creation': False,
'file_name': tsk.filename,
'path': tsk.filepath,
'attachment_ids': tsk.attachment_ids,
'task': tsk,
'move_path': tsk.move_path,
'new_name': tsk.new_name,
'after_import': tsk.after_import,
'file_type': tsk.file_type,
'md5_check': tsk.md5_check,
}
conn = method_class(self.env, config)
conn.run()
Loading