Browse Source

Merge pull request #516 from akretion/9-external-file-location

9 external file location
pull/799/head
Dave Lasley 8 years ago
committed by GitHub
parent
commit
c8c7c4efaf
  1. 69
      external_file_location/README.rst
  2. 3
      external_file_location/__init__.py
  3. 35
      external_file_location/__openerp__.py
  4. 18
      external_file_location/data/cron.xml
  5. 99
      external_file_location/demo/task_demo.xml
  6. 347
      external_file_location/i18n/external_file_location.pot
  7. 347
      external_file_location/i18n/fr.po
  8. 3
      external_file_location/models/__init__.py
  9. 33
      external_file_location/models/attachment.py
  10. 68
      external_file_location/models/location.py
  11. 212
      external_file_location/models/task.py
  12. 5
      external_file_location/security/ir.model.access.csv
  13. 3
      external_file_location/tasks/__init__.py
  14. 27
      external_file_location/tasks/filestore.py
  15. 30
      external_file_location/tasks/ftp.py
  16. 30
      external_file_location/tasks/sftp.py
  17. 4
      external_file_location/tests/__init__.py
  18. 32
      external_file_location/tests/common.py
  19. 74
      external_file_location/tests/mock_server.py
  20. 50
      external_file_location/tests/test_filestore.py
  21. 86
      external_file_location/tests/test_ftp.py
  22. 85
      external_file_location/tests/test_sftp.py
  23. 28
      external_file_location/views/attachment_view.xml
  24. 70
      external_file_location/views/location_view.xml
  25. 12
      external_file_location/views/menu.xml
  26. 70
      external_file_location/views/task_view.xml
  27. 1
      requirements.txt

69
external_file_location/README.rst

@ -0,0 +1,69 @@
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html
:alt: License: AGPL-3
======================
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.
For now, FTP, SFTP and local filestore are handled by the module.
Installation
============
To install this module, you need to:
* fs python module at version 0.5.4 or under
* 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
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas
:alt: Try me on Runbot
:target: https://runbot.odoo-community.org/runbot/149/9.0
Bug Tracker
===========
Bugs are tracked on `GitHub Issues
<https://github.com/OCA/server-tools/issues>`_. In case of trouble, please
check there if your issue has already been reported. If you spotted it first,
help us smashing it by providing a detailed and welcomed feedback.
Credits
=======
Images
------
* Odoo Community Association: `Icon <https://github.com/OCA/maintainer-tools/blob/master/template/module/static/description/icon.svg>`_.
Contributors
------------
* Valentin CHEMIERE <valentin.chemiere@akretion.com>
* Mourad EL HADJ MIMOUNE <mourad.elhadj.mimoune@akretion.com>
* Florian DA COSTA <florian.dacosta@akretion.com>
Maintainer
----------
.. image:: https://odoo-community.org/logo.png
:alt: Odoo Community Association
:target: https://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 https://odoo-community.org.

3
external_file_location/__init__.py

@ -0,0 +1,3 @@
from . import models
from . import tasks
from . import tests

35
external_file_location/__openerp__.py

@ -0,0 +1,35 @@
# coding: utf-8
# @ 2016 florian DA COSTA @ 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': '9.0.1.0.0',
'author': 'Akretion,Odoo Community Association (OCA)',
'website': 'www.akretion.com',
'license': 'AGPL-3',
'category': 'Generic Modules',
'depends': [
'attachment_base_synchronize',
],
'external_dependencies': {
'python': [
'fs',
'paramiko',
],
},
'data': [
'views/menu.xml',
'views/attachment_view.xml',
'views/location_view.xml',
'views/task_view.xml',
'data/cron.xml',
'security/ir.model.access.csv',
],
'demo': [
'demo/task_demo.xml',
],
'installable': True,
'application': False,
}

18
external_file_location/data/cron.xml

@ -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_task_scheduler</field>
<field name="args">([[('method_type', '=', 'import')]])</field>
</record>
</data>
</openerp>

99
external_file_location/demo/task_demo.xml

@ -0,0 +1,99 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="location_ftp" model="external.file.location">
<field name="name">TEST FTP</field>
<field name="protocol">ftp</field>
<field name="address">my-ftp-address</field>
<field name="login">my-ftp-user</field>
<field name="password">my-ftp-password</field>
<field name="port">21</field>
</record>
<record id="location_sftp" model="external.file.location">
<field name="name">TEST SFTP</field>
<field name="protocol">sftp</field>
<field name="address">my-sftp-address</field>
<field name="login">my-sftp-user</field>
<field name="password">my-sftp-password</field>
<field name="port">22</field>
</record>
<record id="location_filestore" model="external.file.location">
<field name="name">TEST File Store</field>
<field name="protocol">file_store</field>
<field name="filestore_rootpath">/</field>
</record>
<record id="ftp_import_task" model="external.file.task">
<field name="method_type">import</field>
<field name="location_id" eval="ref('external_file_location.location_ftp')"/>
<field name="filename">test-import-ftp.txt</field>
<field name="filepath">/home/user/test</field>
<field name="name">Import FTP Task</field>
</record>
<record id="ftp_export_task" model="external.file.task">
<field name="method_type">export</field>
<field name="location_id" eval="ref('external_file_location.location_ftp')"/>
<field name="filepath">/home/user/test</field>
<field name="name">Export FTP Task</field>
</record>
<record id="sftp_import_task" model="external.file.task">
<field name="method_type">import</field>
<field name="location_id" eval="ref('external_file_location.location_sftp')"/>
<field name="filename">test-import-sftp.txt</field>
<field name="filepath">/home/user/test</field>
<field name="name">Import SFTP Task</field>
</record>
<record id="sftp_export_task" model="external.file.task">
<field name="method_type">export</field>
<field name="location_id" eval="ref('external_file_location.location_sftp')"/>
<field name="filepath">/home/user/test</field>
<field name="name">Export SFTP Task</field>
</record>
<record id="filestore_import_task" model="external.file.task">
<field name="method_type">import</field>
<field name="location_id" eval="ref('external_file_location.location_filestore')"/>
<field name="filename">test-import-filestore.txt</field>
<field name="filepath">/home/user/test</field>
<field name="name">Import filestore Task</field>
</record>
<record id="filestore_export_task" model="external.file.task">
<field name="method_type">export</field>
<field name="location_id" eval="ref('external_file_location.location_filestore')"/>
<field name="filepath">/home/user/test</field>
<field name="name">Export filestore Task</field>
</record>
<record id="ir_attachment_export_file_sftp" model="ir.attachment.metadata">
<field name="name">Sftp text export file</field>
<field name="datas">dGVzdCBzZnRwIGZpbGUgZXhwb3J0</field>
<field name="datas_fname">sftp_test_export.txt</field>
<field name="task_id" eval="ref('external_file_location.sftp_export_task')"/>
<field name="file_type">export_external_location</field>
</record>
<record id="ir_attachment_export_file_ftp" model="ir.attachment.metadata">
<field name="name">ftp text export file</field>
<field name="datas">dGVzdCBmdHAgZmlsZSBleHBvcnQ=</field>
<field name="datas_fname">ftp_test_export.txt</field>
<field name="task_id" eval="ref('external_file_location.ftp_export_task')"/>
<field name="file_type">export_external_location</field>
</record>
<record id="ir_attachment_export_file_filestore" model="ir.attachment.metadata">
<field name="name">filestore text export file</field>
<field name="datas">dGVzdCBmaWxlc3RvcmUgZmlsZSBleHBvcnQ=</field>
<field name="datas_fname">filestore_test_export.txt</field>
<field name="task_id" eval="ref('external_file_location.filestore_export_task')"/>
<field name="file_type">export_external_location</field>
</record>
</data>
</openerp>

347
external_file_location/i18n/external_file_location.pot

@ -0,0 +1,347 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * external_file_location
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-10 16:51+0000\n"
"PO-Revision-Date: 2016-08-10 16:51+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_needaction
msgid "Action Needed"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_after_import
msgid "Action after import a file"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_address
msgid "Address"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_after_import
msgid "After import"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_attachment_ids
msgid "Attachment"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_md5_check
msgid "Control file integrity after import with a md5 file"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_create_uid
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_create_uid
msgid "Created by"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_create_date
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_create_date
msgid "Created on"
msgstr ""
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Data importation setting"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_last_post
msgid "Date of the last message posted on the record."
msgstr ""
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_external_file_location
msgid "Description"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_display_name
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_display_name
msgid "Display Name"
msgstr ""
#. module: external_file_location
#: selection:external.file.task,method_type:0
msgid "Export"
msgstr ""
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_external_file_task
msgid "External file task"
msgstr ""
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_location_tree
msgid "File Location"
msgstr ""
#. module: external_file_location
#: model:ir.actions.act_window,name:external_file_location.action_location
#: model:ir.ui.menu,name:external_file_location.menu_ir_location
msgid "File Locations"
msgstr ""
#. module: external_file_location
#: model:ir.ui.menu,name:external_file_location.menu_file_exchange
msgid "File exchange"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_filename
msgid "File name which is imported.You can use file pattern like *.txtto import all txt files"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_file_type
msgid "File type"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_filestore_rootpath
msgid "FileStore Root Path"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_filename
msgid "Filename"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_filepath
msgid "Filepath"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_follower_ids
msgid "Followers"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_channel_ids
msgid "Followers (Channels)"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_partner_ids
msgid "Followers (Partners)"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_login
msgid "Hide login"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_password
msgid "Hide password"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_port
msgid "Hide port"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_id
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_id
msgid "ID"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_unread
msgid "If checked new messages require your attention."
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_needaction
msgid "If checked, new messages require your attention."
msgstr ""
#. module: external_file_location
#: selection:external.file.task,method_type:0
msgid "Import"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_move_path
msgid "Imported File will be moved to this path"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_new_name
msgid "Imported File will be renamed to this nameName can use mako template where obj is an ir_attachement. template exemple : ${obj.name}-${obj.create_date}.csv"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_is_follower
msgid "Is Follower"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_last_post
msgid "Last Message Date"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location___last_update
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task___last_update
msgid "Last Modified on"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_write_uid
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_write_uid
msgid "Last Updated by"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_write_date
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_write_date
msgid "Last Updated on"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_location_id
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_location_id
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
msgid "Location"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_login
msgid "Login"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_md5_check
msgid "Md5 check"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_ids
msgid "Messages"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_method_type
msgid "Method type"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_move_path
msgid "Move path"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_name
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_name
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Name"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_new_name
msgid "New name"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_needaction_counter
msgid "Number of Actions"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_needaction_counter
msgid "Number of messages which requires an action"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_unread_counter
msgid "Number of unread messages"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_password
msgid "Password"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_filepath
msgid "Path to imported/exported file"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_port
msgid "Port"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_protocol
msgid "Protocol"
msgstr ""
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Run"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_location_filestore_rootpath
msgid "Server's root path"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_task_id
msgid "Task"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_task_ids
msgid "Task ids"
msgstr ""
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_tree
msgid "Tasks"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_file_type
msgid "The file type determines an import method to be used to parse and transform data before their import in ERP"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_unread
msgid "Unread Messages"
msgstr ""
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_unread_counter
msgid "Unread Messages Counter"
msgstr ""
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_ir_attachment_metadata
msgid "ir.attachment.metadata"
msgstr ""

347
external_file_location/i18n/fr.po

@ -0,0 +1,347 @@
# Translation of Odoo Server.
# This file contains the translation of the following modules:
# * external_file_location
#
msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 9.0c\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2016-08-10 16:51+0000\n"
"PO-Revision-Date: 2016-08-10 16:51+0000\n"
"Last-Translator: <>\n"
"Language-Team: \n"
"MIME-Version: 1.0\n"
"Content-Type: text/plain; charset=UTF-8\n"
"Content-Transfer-Encoding: \n"
"Plural-Forms: \n"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_needaction
msgid "Action Needed"
msgstr "A besoin d'une action"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_after_import
msgid "Action after import a file"
msgstr "Action après l'import du fichier"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_address
msgid "Address"
msgstr "Addresse"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_after_import
msgid "After import"
msgstr "Après import"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_attachment_ids
msgid "Attachment"
msgstr "Pièce jointe"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_md5_check
msgid "Control file integrity after import with a md5 file"
msgstr "Contrôle l'intégrité du fichier après l'import avec un fichier md5"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_create_uid
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_create_uid
msgid "Created by"
msgstr "Créé par"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_create_date
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_create_date
msgid "Created on"
msgstr "Créé le"
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Data importation setting"
msgstr "Data importation setting"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_last_post
msgid "Date of the last message posted on the record."
msgstr "Date du dernier message publié sur cet enregistrement"
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_external_file_location
msgid "Description"
msgstr "Description"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_display_name
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_display_name
msgid "Display Name"
msgstr "Afficher le nom"
#. module: external_file_location
#: selection:external.file.task,method_type:0
msgid "Export"
msgstr "Export"
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_external_file_task
msgid "External file task"
msgstr "Tache"
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_location_tree
msgid "File Location"
msgstr "Emplacement fichier"
#. module: external_file_location
#: model:ir.actions.act_window,name:external_file_location.action_location
#: model:ir.ui.menu,name:external_file_location.menu_ir_location
msgid "File Locations"
msgstr "Emplacements fichiers"
#. module: external_file_location
#: model:ir.ui.menu,name:external_file_location.menu_file_exchange
msgid "File exchange"
msgstr "Echange de fichier"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_filename
msgid "File name which is imported.You can use file pattern like *.txtto import all txt files"
msgstr "Nom du fichier importé. Vous pouvez utiliser une expression comme *.txt pour importer tous les fichiers txt"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_file_type
msgid "File type"
msgstr "Type de fichier"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_filestore_rootpath
msgid "FileStore Root Path"
msgstr "Emplacement racine"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_filename
msgid "Filename"
msgstr "Nom du fichier"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_filepath
msgid "Filepath"
msgstr "Chemin"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_follower_ids
msgid "Followers"
msgstr "Abonnés"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_channel_ids
msgid "Followers (Channels)"
msgstr "Abonnés (Canaux)"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_partner_ids
msgid "Followers (Partners)"
msgstr "Abonnés (Partenaires)"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_login
msgid "Hide login"
msgstr "Cacher le login"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_password
msgid "Hide password"
msgstr "Cacher le mot de passe"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_hide_port
msgid "Hide port"
msgstr "Cacher le port"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_id
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_id
msgid "ID"
msgstr "Identifiant"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_unread
msgid "If checked new messages require your attention."
msgstr "Si coché, de nouveaux messages demandent votre attention."
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_needaction
msgid "If checked, new messages require your attention."
msgstr "si elle est cochée, de nouveaux messages requièrent votre attention."
#. module: external_file_location
#: selection:external.file.task,method_type:0
msgid "Import"
msgstr "Import"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_move_path
msgid "Imported File will be moved to this path"
msgstr "Le fichier importé sera déplacé dans cet emplacement"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_new_name
msgid "Imported File will be renamed to this nameName can use mako template where obj is an ir_attachement. template exemple : ${obj.name}-${obj.create_date}.csv"
msgstr "Imported File will be renamed to this nameName can use mako template where obj is an ir_attachement. template exemple : ${obj.name}-${obj.create_date}.csv"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_is_follower
msgid "Is Follower"
msgstr "Est un abonné"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_last_post
msgid "Last Message Date"
msgstr "Date du dernier message"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location___last_update
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task___last_update
msgid "Last Modified on"
msgstr "Dernière modification le"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_write_uid
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_write_uid
msgid "Last Updated by"
msgstr "Dernière mise à jour par"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_write_date
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_write_date
msgid "Last Updated on"
msgstr "Dernière mise à jour le"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_location_id
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_location_id
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
msgid "Location"
msgstr "Emplacement"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_login
msgid "Login"
msgstr "Identifiant"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_md5_check
msgid "Md5 check"
msgstr "Md5 check"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_ids
msgid "Messages"
msgstr "Messages"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_method_type
msgid "Method type"
msgstr "Type de méthode"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_move_path
msgid "Move path"
msgstr "chemin des archives"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_name
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_name
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Name"
msgstr "Nom"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_task_new_name
msgid "New name"
msgstr "Nouveau nom"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_needaction_counter
msgid "Number of Actions"
msgstr "Nombre d'Actions"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_needaction_counter
msgid "Number of messages which requires an action"
msgstr "Nombre de messages demandant une action"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_ir_attachment_metadata_message_unread_counter
msgid "Number of unread messages"
msgstr "Nombre de messages non lus"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_password
msgid "Password"
msgstr "Mot de passe"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_filepath
msgid "Path to imported/exported file"
msgstr "Path to imported/exported file"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_port
msgid "Port"
msgstr "Port"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_protocol
msgid "Protocol"
msgstr "Protocole"
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
msgid "Run"
msgstr "Run"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_location_filestore_rootpath
msgid "Server's root path"
msgstr "Chemin racine du serveur"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_task_id
msgid "Task"
msgstr "Tache"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_external_file_location_task_ids
msgid "Task ids"
msgstr "Task ids"
#. module: external_file_location
#: model:ir.ui.view,arch_db:external_file_location.view_location_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_form
#: model:ir.ui.view,arch_db:external_file_location.view_task_tree
msgid "Tasks"
msgstr "Taches"
#. module: external_file_location
#: model:ir.model.fields,help:external_file_location.field_external_file_task_file_type
msgid "The file type determines an import method to be used to parse and transform data before their import in ERP"
msgstr "Le type de fichier détermine la méthode d'import utilisée pour parser le fichier et transformer les données avant l'import dans l'ERP"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_unread
msgid "Unread Messages"
msgstr "Messages non lus"
#. module: external_file_location
#: model:ir.model.fields,field_description:external_file_location.field_ir_attachment_metadata_message_unread_counter
msgid "Unread Messages Counter"
msgstr "Compteur de messages non lus"
#. module: external_file_location
#: model:ir.model,name:external_file_location.model_ir_attachment_metadata
msgid "ir.attachment.metadata"
msgstr "ir.attachment.metadata"

3
external_file_location/models/__init__.py

@ -0,0 +1,3 @@
from . import attachment
from . import location
from . import task

33
external_file_location/models/attachment.py

@ -0,0 +1,33 @@
# coding: utf-8
# @ 2016 Florian DA COSTA @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
from openerp import models, fields, api
import base64
import os
class IrAttachmentMetadata(models.Model):
_inherit = 'ir.attachment.metadata'
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)
file_type = fields.Selection(
selection_add=[
('export_external_location',
'Export File (External location)')
])
@api.multi
def _run(self):
super(IrAttachmentMetadata, self)._run()
if self.file_type == 'export_external_location':
protocols = self.env['external.file.location']._get_classes()
location = self.location_id
cls = protocols.get(location.protocol)[1]
path = os.path.join(self.task_id.filepath, self.datas_fname)
with cls.connect(location) as conn:
datas = base64.decodestring(self.datas)
conn.setcontents(path, data=datas)

68
external_file_location/models/location.py

@ -0,0 +1,68 @@
# 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.filestore import FileStoreTask
from ..tasks.ftp import FtpTask
from ..tasks.sftp import SftpTask
class Location(models.Model):
_name = 'external.file.location'
_description = 'Location'
name = fields.Char(string='Name', required=True)
protocol = fields.Selection(selection='_get_protocol', required=True)
address = fields.Char(
string='Address')
filestore_rootpath = fields.Char(
string='FileStore Root Path',
help="Server's root path")
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()
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get(
'external.file.location'))
@api.model
def _get_classes(self):
"surcharge this method to add new protocols"
return {
'ftp': ('FTP', FtpTask),
'sftp': ('SFTP', SftpTask),
'file_store': ('File Store', FileStoreTask),
}
@api.model
def _get_protocol(self):
protocols = self._get_classes()
selection = []
for key, val in protocols.iteritems():
selection.append((key, val[0]))
return selection
@api.onchange('protocol')
def onchange_protocol(self):
protocols = self._get_classes()
if self.protocol:
cls = protocols.get(self.protocol)[1]
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

212
external_file_location/models/task.py

@ -0,0 +1,212 @@
# 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
import openerp
from openerp import tools
from base64 import b64encode
import os
import datetime
import logging
_logger = logging.getLogger(__name__)
try:
# We use a jinja2 sandboxed environment to render mako templates.
# Note that the rendering does not cover all the mako syntax, in particular
# arbitrary Python statements are not accepted, and not all expressions are
# allowed: only "public" attributes (not starting with '_') of objects may
# be accessed.
# This is done on purpose: it prevents incidental or malicious execution of
# Python code that may break the security of the server.
from jinja2.sandbox import SandboxedEnvironment
mako_template_env = SandboxedEnvironment(
variable_start_string="${",
variable_end_string="}",
line_statement_prefix="%",
trim_blocks=True, # do not output newline after blocks
)
mako_template_env.globals.update({
'str': str,
'datetime': datetime,
'len': len,
'abs': abs,
'min': min,
'max': max,
'sum': sum,
'filter': filter,
'reduce': reduce,
'map': map,
'round': round,
})
except ImportError:
_logger.warning("jinja2 not available, templating features will not work!")
class Task(models.Model):
_name = 'external.file.task'
_description = 'External file task'
name = fields.Char(required=True)
method_type = fields.Selection(
[('import', 'Import'), ('export', 'Export')],
required=True)
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/exported 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')
company_id = fields.Many2one(
'res.company', 'Company',
default=lambda self: self.env['res.company']._company_default_get(
'external.file.task'))
file_type = fields.Selection(
selection=[],
string="File Type",
help="The file type determines an import method to be used "
"to parse and transform data before their import in ERP")
active = fields.Boolean(default=True)
def _get_action(self):
return [('rename', 'Rename'),
('move', 'Move'),
('move_rename', 'Move & Rename'),
('delete', 'Delete'),
]
@api.multi
def _prepare_attachment_vals(self, datas, filename, md5_datas):
self.ensure_one()
vals = {
'name': filename,
'datas': b64encode(datas),
'datas_fname': filename,
'task_id': self.id,
'external_hash': md5_datas,
'file_type': self.file_type or False,
}
return vals
@api.model
def _template_render(self, template, record):
try:
template = mako_template_env.from_string(tools.ustr(template))
except Exception:
_logger.exception("Failed to load template %r", template)
variables = {'obj': record}
try:
render_result = template.render(variables)
except Exception:
_logger.exception(
"Failed to render template %r using values %r" %
(template, variables))
render_result = u""
if render_result == u"False":
render_result = u""
return render_result
@api.model
def run_task_scheduler(self, domain=None):
if domain is None:
domain = []
tasks = self.env['external.file.task'].search(domain)
for task in tasks:
if task.method_type == 'import':
task.run_import()
elif task.method_type == 'export':
task.run_export()
@api.multi
def run_import(self):
self.ensure_one()
protocols = self.env['external.file.location']._get_classes()
cls = protocols.get(self.location_id.protocol)[1]
attach_obj = self.env['ir.attachment.metadata']
with cls.connect(self.location_id) as conn:
md5_datas = ''
for file_name in conn.listdir(path=self.filepath,
wildcard=self.filename or '',
files_only=True):
with api.Environment.manage():
with openerp.registry(
self.env.cr.dbname).cursor() as new_cr:
new_env = api.Environment(new_cr, self.env.uid,
self.env.context)
try:
full_path = os.path.join(self.filepath, file_name)
file_data = conn.open(full_path, 'rb')
datas = file_data.read()
if self.md5_check:
md5_file = conn.open(full_path + '.md5', 'rb')
md5_datas = md5_file.read().rstrip('\r\n')
attach_vals = self._prepare_attachment_vals(
datas, file_name, md5_datas)
attachment = attach_obj.with_env(new_env).create(
attach_vals)
new_full_path = False
if self.after_import == 'rename':
new_name = self._template_render(
self.new_name, attachment)
new_full_path = os.path.join(
self.filepath, new_name)
elif self.after_import == 'move':
new_full_path = os.path.join(
self.move_path, file_name)
elif self.after_import == 'move_rename':
new_name = self._template_render(
self.new_name, attachment)
new_full_path = os.path.join(
self.move_path, new_name)
if new_full_path:
conn.rename(full_path, new_full_path)
if self.md5_check:
conn.rename(
full_path + '.md5',
new_full_path + '/md5')
if self.after_import == 'delete':
conn.remove(full_path)
if self.md5_check:
conn.remove(full_path + '.md5')
except Exception, e:
new_env.cr.rollback()
raise e
else:
new_env.cr.commit()
@api.multi
def run_export(self):
self.ensure_one()
attachment_obj = self.env['ir.attachment.metadata']
attachments = attachment_obj.search(
[('task_id', '=', self.id), ('state', '!=', 'done')])
for attachment in attachments:
attachment.run()

5
external_file_location/security/ir.model.access.csv

@ -0,0 +1,5 @@
id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
access_external_file_location_manager,external.file.location.manager,model_external_file_location,base.group_system,1,1,1,1
access_external_file_location_user,external.file.location.user,model_external_file_location,base.group_user,1,0,0,0
access_external_file_task_manager,external.file.task.manager,model_external_file_task,base.group_system,1,1,1,1
access_external_file_task_user,external.file.task.user,model_external_file_task,base.group_user,1,0,0,0

3
external_file_location/tasks/__init__.py

@ -0,0 +1,3 @@
from . import ftp
from . import sftp
from . import filestore

27
external_file_location/tasks/filestore.py

@ -0,0 +1,27 @@
# coding: utf-8
# @ 2016 Florian DA COSTA @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
_logger = logging.getLogger(__name__)
try:
from fs import osfs
except ImportError:
_logger.debug('Cannot `import fs`.')
class FileStoreTask(osfs.OSFS):
_key = 'filestore'
_name = 'File Store'
_default_port = None
_hide_login = True
_hide_password = True
_hide_port = True
@staticmethod
def connect(location):
rootpath = location.filestore_rootpath or '/'
conn = FileStoreTask(rootpath)
return conn

30
external_file_location/tasks/ftp.py

@ -0,0 +1,30 @@
# coding: utf-8
# @ 2016 Florian DA COSTA @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
_logger = logging.getLogger(__name__)
try:
from fs import ftpfs
except ImportError:
_logger.debug('Cannot `import fs`.')
class FtpTask(ftpfs.FTPFS):
_key = 'sftp'
_name = 'SFTP'
_synchronize_type = None
_default_port = 22
_hide_login = False
_hide_password = False
_hide_port = False
@staticmethod
def connect(location):
conn = FtpTask(location.address,
location.login,
location.password,
location.port)
return conn

30
external_file_location/tasks/sftp.py

@ -0,0 +1,30 @@
# coding: utf-8
# @ 2016 Florian DA COSTA @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
_logger = logging.getLogger(__name__)
try:
from fs import sftpfs
except ImportError:
_logger.debug('Cannot `import fs`.')
class SftpTask(sftpfs.SFTPFS):
_key = 'sftp'
_name = 'SFTP'
_synchronize_type = None
_default_port = 22
_hide_login = False
_hide_password = False
_hide_port = False
@staticmethod
def connect(location):
connection_string = "{}:{}".format(location.address, location.port)
conn = SftpTask(connection=connection_string,
username=location.login,
password=location.password)
return conn

4
external_file_location/tests/__init__.py

@ -0,0 +1,4 @@
from . import mock_server
from . import test_ftp
from . import test_sftp
from . import test_filestore

32
external_file_location/tests/common.py

@ -0,0 +1,32 @@
# coding: utf-8
# @ 2016 Florian da Costa @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import openerp.tests.common as common
from openerp import api
from StringIO import StringIO
class ContextualStringIO(StringIO):
"""
snippet from http://bit.ly/1HfH6uW (stackoverflow)
"""
def __enter__(self):
return self
def __exit__(self, *args):
self.close()
return False
class TestConnection(common.TransactionCase):
def setUp(self):
super(TestConnection, self).setUp()
self.registry.enter_test_mode()
self.env = api.Environment(self.registry.test_cr, self.env.uid,
self.env.context)
def tearDown(self):
self.registry.leave_test_mode()
super(TestConnection, self).tearDown()

74
external_file_location/tests/mock_server.py

@ -0,0 +1,74 @@
# coding: utf-8
# Copyright (C) 2014 initOS GmbH & Co. KG (<http://www.initos.com>).
# @ 2015 Valentin CHEMIERE @ Akretion
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import mock
from contextlib import contextmanager
from collections import defaultdict
class MultiResponse(dict):
pass
class ConnMock(object):
def __init__(self, response):
self.response = response
self._calls = []
self.call_count = defaultdict(int)
def __getattribute__(self, method):
if method not in ('_calls', 'response', 'call_count'):
def callable(*args, **kwargs):
self._calls.append({
'method': method,
'args': args,
'kwargs': kwargs,
})
call = self.response[method]
if isinstance(call, MultiResponse):
call = call[self.call_count[method]]
self.call_count[method] += 1
return call
return callable
else:
return super(ConnMock, self).__getattribute__(method)
def __call__(self, *args, **kwargs):
return self
def __enter__(self, *args, **kwargs):
return self
def __exit__(self, *args, **kwargs):
pass
def __repr__(self, *args, **kwargs):
return self
def __getitem__(self, key):
return
@contextmanager
def server_mock_sftp(response):
with mock.patch('openerp.addons.external_file_location.tasks.sftp.'
'SftpTask', ConnMock(response)) as SFTPFS:
yield SFTPFS._calls
@contextmanager
def server_mock_ftp(response):
with mock.patch('openerp.addons.external_file_location.tasks.ftp.'
'FtpTask', ConnMock(response)) as FTPFS:
yield FTPFS._calls
@contextmanager
def server_mock_filestore(response):
with mock.patch('openerp.addons.external_file_location.tasks.filestore.'
'FileStoreTask', ConnMock(response)) as FTPFS:
yield FTPFS._calls

50
external_file_location/tests/test_filestore.py

@ -0,0 +1,50 @@
# 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).
import logging
from base64 import b64decode
from .common import TestConnection, ContextualStringIO
from .mock_server import server_mock_filestore
_logger = logging.getLogger(__name__)
class TestfilestoreConnection(TestConnection):
def setUp(self):
super(TestfilestoreConnection, self).setUp()
self.test_file_filestore = ContextualStringIO()
self.test_file_filestore.write('import filestore')
self.test_file_filestore.seek(0)
def test_00_filestore_import(self):
self.task = self.env.ref(
'external_file_location.filestore_import_task')
with server_mock_filestore(
{'open': self.test_file_filestore,
'listdir': ['test-import-filestore.txt']}):
self.task.run_import()
search_file = self.env['ir.attachment.metadata'].search(
[('name', '=', 'test-import-filestore.txt')])
self.assertEqual(len(search_file), 1)
self.assertEqual(b64decode(search_file[0].datas), 'import filestore')
def test_01_filestore_export(self):
self.task = self.env.ref(
'external_file_location.filestore_export_task')
self.filestore_attachment = self.env.ref(
'external_file_location.ir_attachment_export_file_filestore')
with server_mock_filestore(
{'setcontents': ''}) as Fakefilestore:
self.task.run_export()
if Fakefilestore:
self.assertEqual('setcontents', Fakefilestore[-1]['method'])
self.assertEqual('done', self.filestore_attachment.state)
self.assertEqual(
'/home/user/test/filestore_test_export.txt',
Fakefilestore[-1]['args'][0])
self.assertEqual(
'test filestore file export',
Fakefilestore[-1]['kwargs']['data'])

86
external_file_location/tests/test_ftp.py

@ -0,0 +1,86 @@
# 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).
import logging
from base64 import b64decode
import hashlib
from .common import TestConnection, ContextualStringIO
from .mock_server import server_mock_ftp
from .mock_server import MultiResponse
from openerp.exceptions import UserError
_logger = logging.getLogger(__name__)
class TestFtpConnection(TestConnection):
def setUp(self):
super(TestFtpConnection, self).setUp()
self.test_file_ftp = ContextualStringIO()
self.test_file_ftp.write('import ftp')
self.test_file_ftp.seek(0)
def test_00_ftp_import(self):
self.task = self.env.ref('external_file_location.ftp_import_task')
with server_mock_ftp(
{'open': self.test_file_ftp,
'listdir': ['test-import-ftp.txt']}):
self.task.run_import()
search_file = self.env['ir.attachment.metadata'].search(
[('name', '=', 'test-import-ftp.txt')])
self.assertEqual(len(search_file), 1)
self.assertEqual(b64decode(search_file[0].datas), 'import ftp')
def test_01_ftp_export(self):
self.task = self.env.ref('external_file_location.ftp_export_task')
self.ftp_attachment = self.env.ref(
'external_file_location.ir_attachment_export_file_ftp')
with server_mock_ftp(
{'setcontents': ''}) as FakeFTP:
self.task.run_export()
if FakeFTP:
self.assertEqual('setcontents', FakeFTP[-1]['method'])
self.assertEqual('done', self.ftp_attachment.state)
self.assertEqual(
'/home/user/test/ftp_test_export.txt',
FakeFTP[-1]['args'][0])
self.assertEqual(
'test ftp file export',
FakeFTP[-1]['kwargs']['data'])
def test_02_ftp_import_md5(self):
md5_file = ContextualStringIO()
md5_file.write(hashlib.md5('import ftp').hexdigest())
md5_file.seek(0)
task = self.env.ref('external_file_location.ftp_import_task')
task.md5_check = True
with server_mock_ftp(
{'open': MultiResponse({
1: md5_file,
0: self.test_file_ftp}),
'listdir': [task.filename]}) as Fakeftp:
task.run_import()
search_file = self.env['ir.attachment.metadata'].search(
(('name', '=', task.filename),))
self.assertEqual(len(search_file), 1)
self.assertEqual(b64decode(search_file[0].datas),
'import ftp')
self.assertEqual('open', Fakeftp[-1]['method'])
self.assertEqual(hashlib.md5('import ftp').hexdigest(),
search_file.external_hash)
def test_03_ftp_import_md5_corrupt_file(self):
md5_file = ContextualStringIO()
md5_file.write(hashlib.md5('import test ftp corrupted').hexdigest())
md5_file.seek(0)
task = self.env.ref('external_file_location.ftp_import_task')
task.md5_check = True
with server_mock_ftp(
{'open': MultiResponse({
1: md5_file,
0: self.test_file_ftp}),
'listdir': [task.filename]}):
with self.assertRaises(UserError):
task.run_import()

85
external_file_location/tests/test_sftp.py

@ -0,0 +1,85 @@
# 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).
import logging
from base64 import b64decode
import hashlib
from .common import TestConnection, ContextualStringIO
from .mock_server import server_mock_sftp
from .mock_server import MultiResponse
from openerp.exceptions import UserError
_logger = logging.getLogger(__name__)
class TestSftpConnection(TestConnection):
def setUp(self):
super(TestSftpConnection, self).setUp()
self.test_file_sftp = ContextualStringIO()
self.test_file_sftp.write('import sftp')
self.test_file_sftp.seek(0)
def test_00_sftp_import(self):
task = self.env.ref('external_file_location.sftp_import_task')
with server_mock_sftp(
{'open': self.test_file_sftp,
'listdir': [task.filename]}):
task.run_import()
search_file = self.env['ir.attachment.metadata'].search(
[('name', '=', task.filename)])
self.assertEqual(len(search_file), 1)
self.assertEqual(b64decode(search_file[0].datas), 'import sftp')
def test_01_sftp_export(self):
self.task = self.env.ref('external_file_location.sftp_export_task')
self.sftp_attachment = self.env.ref(
'external_file_location.ir_attachment_export_file_sftp')
with server_mock_sftp(
{'setcontents': ''}) as FakeSFTP:
self.task.run_export()
if FakeSFTP:
self.assertEqual('setcontents', FakeSFTP[-1]['method'])
self.assertEqual(
'/home/user/test/sftp_test_export.txt',
FakeSFTP[-1]['args'][0])
self.assertEqual(
'test sftp file export',
FakeSFTP[-1]['kwargs']['data'])
def test_02_sftp_import_md5(self):
md5_file = ContextualStringIO()
md5_file.write(hashlib.md5('import sftp').hexdigest())
md5_file.seek(0)
task = self.env.ref('external_file_location.sftp_import_task')
task.md5_check = True
with server_mock_sftp(
{'open': MultiResponse({
1: md5_file,
0: self.test_file_sftp}),
'listdir': [task.filename]}) as FakeSFTP:
task.run_import()
search_file = self.env['ir.attachment.metadata'].search(
(('name', '=', task.filename),))
self.assertEqual(len(search_file), 1)
self.assertEqual(b64decode(search_file[0].datas),
'import sftp')
self.assertEqual('open', FakeSFTP[-1]['method'])
self.assertEqual(hashlib.md5('import sftp').hexdigest(),
search_file.external_hash)
def test_03_sftp_import_md5_corrupt_file(self):
md5_file = ContextualStringIO()
md5_file.write(hashlib.md5('import test sftp corrupted').hexdigest())
md5_file.seek(0)
task = self.env.ref('external_file_location.sftp_import_task')
task.md5_check = True
with server_mock_sftp(
{'open': MultiResponse({
1: md5_file,
0: self.test_file_sftp}),
'listdir': [task.filename]}):
with self.assertRaises(UserError):
task.run_import()

28
external_file_location/views/attachment_view.xml

@ -0,0 +1,28 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<record id="view_attachment_improved_form" model="ir.ui.view">
<field name="model">ir.attachment.metadata</field>
<field name="inherit_id" ref="attachment_base_synchronize.view_attachment_improved_form" />
<field name="arch" type="xml">
<field name="url" position="after">
<field name="task_id" attrs="{'required': [('file_type', '=', 'export_external_location')]}"/>
<field name="location_id"/>
</field>
</field>
</record>
<record id="view_external_attachment_tree" model="ir.ui.view">
<field name="model">ir.attachment.metadata</field>
<field name="inherit_id" ref="attachment_base_synchronize.view_external_attachment_tree" />
<field name="arch" type="xml">
<field name="file_type" position="after">
<field name="task_id"/>
<field name="location_id"/>
</field>
</field>
</record>
</data>
</openerp>

70
external_file_location/views/location_view.xml

@ -0,0 +1,70 @@
<?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="Name"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<newline/>
<field name="protocol" colspan="2"/>
<newline/>
<field name="address" colspan="2" attrs="{'required': [('protocol', '!=', 'file_store')], 'invisible': [('protocol', '=', 'file_store')]}"/>
<field name="filestore_rootpath" colspan="2" attrs="{'required': [('protocol', '=', 'file_store')], 'invisible': [('protocol', '!=', 'file_store')]}"/>
<field name="port" colspan="2" attrs="{'invisible': [('hide_port', '=', True)], 'required': [('hide_port', '=', False)]}"/>
<field name="company_id" colspan="2"/>
<field name="login" colspan="2" attrs="{'invisible': [('hide_login', '=', True)], 'required': [('hide_login', '=', False)]}"/>
<field name="password" password="1" colspan="2" attrs="{'invisible': [('hide_password', '=', True)]}"/>
<separator string="Tasks" colspan="4"/>
<field name="task_ids" colspan="4" nolabel="1" context="{'hide_location': True, 'protocol': protocol}">
<tree>
<field name="name"/>
<field name="name"/>
<field name="method_type"/>
<field name="filename"/>
<field name="filepath"/>
</tree>
</field>
<field name="hide_login" invisible="1"/>
<field name="hide_password" invisible="1"/>
<field name="hide_port" invisible="1"/>
</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="File 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">File 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="base.menu_automation"
sequence="20"
action="action_location"/>
</data>
</openerp>

12
external_file_location/views/menu.xml

@ -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>

70
external_file_location/views/task_view.xml

@ -0,0 +1,70 @@
<?xml version="1.0" encoding="UTF-8"?>
<openerp>
<data>
<!-- © 2014-2016 Akretion (http://www.akretion.com)
# @ 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). -->
<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">
<header>
<button name="run_import" type="object" string="Run" icon="gtk-execute" attrs="{'invisible': [('method_type', '!=', 'import')]}"/>
<button name="run_export" type="object" string="Run" icon="gtk-execute" attrs="{'invisible': [('method_type', '!=', 'export')]}"/>
</header>
<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="Name"/>
<h1><field name="name" class="oe_inline"/></h1>
</div>
<field name="method_type" colspan="2"/>
<span colspan="2"/>
<field name="filename" colspan="4" attrs="{'invisible':[('method_type','!=','import')], 'required':[('method_type', '=', 'import')]}"/>
<field name="filepath" colspan="4" />
<field name="company_id" colspan="4"/>
</group>
<group col="6">
<field name="after_import" attrs="{'invisible':[('method_type','!=','import')]}"/>
<group col="4" colspan="4" >
<field name="move_path" colspan="4"
attrs="{'invisible':['|', '&amp;',
('after_import','!=','move'),
('after_import','!=','move_rename'),
('method_type','!=','import')]}"/>
<field name="new_name" colspan="4"
attrs="{'invisible': ['|', '&amp;',
('after_import','!=','rename'),
('after_import','!=','move_rename'),
('method_type','!=','import')]}"/>
</group>
<field name="md5_check" colspan="2"
attrs="{'invisible':
[('method_type','!=','import')]}"
/>
<field name="active" colspan="2"/>
</group>
<group string="Data importation setting">
<field name="file_type" attrs="{'invisible':[('method_type','!=','import')]}"/>
</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_type"/>
<field name="filename"/>
<field name="filepath"/>
</tree>
</field>
</record>
</data>
</openerp>

1
requirements.txt

@ -5,3 +5,4 @@ IPy
validate_email
pysftp
pyotp
fs==0.5.4
Loading…
Cancel
Save