From e7ee17505aaea89e55d29d7e8981fd784b56d56b Mon Sep 17 00:00:00 2001 From: Kitti U Date: Fri, 15 Mar 2019 06:40:06 +0700 Subject: [PATCH 01/88] Excel Import/Export/Report (#1522) * [ADD] v12 excel_import_export * Change from eval() to safe_evel() * Change variable to format to style, as fomat is a common python function :100644 100644 00ee3d9f... e9e48d87... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 01e5b9f5... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3... 17d3964d... M excel_import_export/__manifest__.py :100644 100644 00ee3d9f... 51c2572a... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 185a3330... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3... 933ce0dc... M excel_import_export/__manifest__.py :100644 100644 00ee3d9f... 51c2572a... M excel_import_export/models/common.py :100644 100644 a215d29b... 5b4d1fb1... M excel_import_export/models/styles.py :100644 100644 ace11a32... 185a3330... M excel_import_export/models/xlsx_export.py :100644 100644 881b814f... cadfb0f2... M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5... 80490ce8... M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6... a363ad19... M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187... 392fe6e5... M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519... 45ee33c6... M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 3b1217e8 M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 5b4d1fb1 M excel_import_export/models/styles.py :100644 100644 ace11a32 185a3330 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5 80490ce8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 5b4d1fb1 M excel_import_export/models/styles.py :100644 100644 ace11a32 185a3330 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 100644 58689ee5 80490ce8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 a7d6adc5 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 578a1fd8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 578a1fd8 M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 e3826e08 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 c8481487 A excel_import_export/tests/test_xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 12f9ca99 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 ed8c9fc7 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 69aa6ea0 A excel_import_export/tests/test_xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 475b5187 392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 933d8614 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 1460473a M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 a2d035ef 9463f279 M excel_import_export_demo/__manifest__.py :100644 100644 475b5187 e7f1255b M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :000000 100644 00000000 79db62f7 A excel_import_export_demo/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export_demo/tests/sale_order.xlsx :000000 100644 00000000 c9733b95 A excel_import_export_demo/tests/test_common.py :000000 100644 00000000 9c943768 A excel_import_export_demo/tests/test_xlsx_import_export.py :000000 100644 00000000 730605c1 A excel_import_export_demo/tests/test_xlsx_template.py :100644 100644 96157ea3 fee958bc M excel_import_export/__manifest__.py :100644 100644 00ee3d9f 51c2572a M excel_import_export/models/common.py :100644 100644 a215d29b 9738a3c8 M excel_import_export/models/styles.py :100644 100644 ace11a32 c7db3f92 M excel_import_export/models/xlsx_export.py :100644 100644 881b814f 933d8614 M excel_import_export/models/xlsx_import.py :100644 100644 70c37799 f123d2a6 M excel_import_export/models/xlsx_report.py :100644 100644 58689ee5 1460473a M excel_import_export/models/xlsx_template.py :100644 100644 5c9c09a6 a363ad19 M excel_import_export/views/xlsx_template_view.xml :100644 100644 800ea573 1807ea7e M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644 febed8d0 750dc17e M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644 a2d035ef 9463f279 M excel_import_export_demo/__manifest__.py :100644 100644 475b5187 e7f1255b M excel_import_export_demo/import_export_sale_order/templates.xml :100644 100644 8e40a2d0 21574896 M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 100644 4af9c519 45ee33c6 M excel_import_export_demo/report_sale_order/templates.xml :000000 100644 00000000 79db62f7 A excel_import_export_demo/tests/__init__.py :000000 100644 00000000 18618688 A excel_import_export_demo/tests/sale_order.xlsx :000000 100644 00000000 bb3ea32e A excel_import_export_demo/tests/test_common.py :000000 100644 00000000 9c943768 A excel_import_export_demo/tests/test_xlsx_import_export.py :000000 100644 00000000 730605c1 A excel_import_export_demo/tests/test_xlsx_template.py --- excel_import_export/README.rst | 153 ++++++ excel_import_export/__init__.py | 5 + excel_import_export/__manifest__.py | 29 + excel_import_export/models/__init__.py | 8 + excel_import_export/models/common.py | 335 ++++++++++++ excel_import_export/models/styles.py | 48 ++ excel_import_export/models/xlsx_export.py | 273 ++++++++++ excel_import_export/models/xlsx_import.py | 259 +++++++++ excel_import_export/models/xlsx_report.py | 69 +++ excel_import_export/models/xlsx_template.py | 452 ++++++++++++++++ excel_import_export/readme/CONTRIBUTORS.rst | 1 + excel_import_export/readme/DESCRIPTION.rst | 8 + excel_import_export/readme/HISTORY.rst | 4 + excel_import_export/readme/INSTALL.rst | 5 + excel_import_export/readme/ROADMAP.rst | 2 + excel_import_export/readme/USAGE.rst | 41 ++ .../security/ir.model.access.csv | 4 + .../static/description/index.html | 496 ++++++++++++++++++ excel_import_export/views/xlsx_report.xml | 51 ++ .../views/xlsx_template_view.xml | 230 ++++++++ excel_import_export/wizard/__init__.py | 2 + .../wizard/export_xlsx_wizard.py | 82 +++ .../wizard/export_xlsx_wizard.xml | 39 ++ .../wizard/import_xlsx_wizard.py | 146 ++++++ .../wizard/import_xlsx_wizard.xml | 44 ++ 25 files changed, 2786 insertions(+) create mode 100644 excel_import_export/README.rst create mode 100644 excel_import_export/__init__.py create mode 100644 excel_import_export/__manifest__.py create mode 100644 excel_import_export/models/__init__.py create mode 100644 excel_import_export/models/common.py create mode 100644 excel_import_export/models/styles.py create mode 100644 excel_import_export/models/xlsx_export.py create mode 100644 excel_import_export/models/xlsx_import.py create mode 100644 excel_import_export/models/xlsx_report.py create mode 100644 excel_import_export/models/xlsx_template.py create mode 100644 excel_import_export/readme/CONTRIBUTORS.rst create mode 100644 excel_import_export/readme/DESCRIPTION.rst create mode 100644 excel_import_export/readme/HISTORY.rst create mode 100644 excel_import_export/readme/INSTALL.rst create mode 100644 excel_import_export/readme/ROADMAP.rst create mode 100644 excel_import_export/readme/USAGE.rst create mode 100644 excel_import_export/security/ir.model.access.csv create mode 100644 excel_import_export/static/description/index.html create mode 100644 excel_import_export/views/xlsx_report.xml create mode 100644 excel_import_export/views/xlsx_template_view.xml create mode 100644 excel_import_export/wizard/__init__.py create mode 100644 excel_import_export/wizard/export_xlsx_wizard.py create mode 100644 excel_import_export/wizard/export_xlsx_wizard.xml create mode 100644 excel_import_export/wizard/import_xlsx_wizard.py create mode 100644 excel_import_export/wizard/import_xlsx_wizard.xml diff --git a/excel_import_export/README.rst b/excel_import_export/README.rst new file mode 100644 index 000000000..48d59e6ef --- /dev/null +++ b/excel_import_export/README.rst @@ -0,0 +1,153 @@ +=================== +Excel Import/Export +=================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |badge1| image:: https://img.shields.io/badge/licence-AGPL--3-blue.png + :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html + :alt: License: AGPL-3 +.. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github + :target: https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export + :alt: OCA/server-tools +.. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/server-tools-12-add-excel_import_export/server-tools-12-add-excel_import_export-excel_import_export + :alt: Translate me on Weblate +.. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/149/12-add-excel_import_export + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| + +The module provide pre-built functions and wizards for developer to build excel import / export / report with ease. + +Without having to code to create excel file, developer do, + +- Create menu, action, wizard, model, view a normal Odoo development. +- Design excel template using standard Excel application, e.g., colors, fonts, formulas, etc. +- Instruct how the data will be located in Excel with simple dictionary instruction or from Odoo UI. +- Odoo will combine instruction with excel template, and result in final excel file. + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install this module, you need to install following python library, **xlrd, xlwt, openpyxl**. + +Then, simply install **excel_import_export**. + +For samples, install **excel_import_export_sample**. + +Usage +===== + +This module contain pre-defined function and wizards to make exporting, importing and reporting easy. + +At the heart of this module, there are 2 `main methods` + +- ``self.env['xlsx.export'].export_xlsx(...)`` +- ``self.env['xlsx.import'].import_xlsx(...)`` + +For reporting, also call `export_xlsx(...)` but through following method + +- ``self.env['xslx.report'].report_xlsx(...)`` + +After install this module, go to Settings > Excel Import/Export > XLSX Templates, this is where the key component located. + +As this module provide tools, it is best to explain as use cases. For example use cases, please install **excel_import_export_sample** + +**Use Case 1:** Export/Import Excel on existing document + +This add export/import action menus in existing document (example - excel_import_export_sample/import_export_sale_order) + +1. Create export action menu on document, with res_model="export.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- actions.xml +2. Create import action menu on document, with res_model="import.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- action.xml +3. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import -- .xlsx +4. Create instruction dictionary for export/import in xlsx.template model -- templates.xml + +**Use Case 2:** Import Excel Files + +With menu wizard to create new documents (example - excel_import_export_sample/import_sale_orders) + +1. Create report menu with search wizard, res_model="import.xlsx.wizard" and context['template_domain'] to locate the right template -- menu_action.xml +2. Create Excel Template File (.xlsx), in the template, name the underlining tab used for import -- .xlsx +3. Create instruction dictionary for import in xlsx.template model -- templates.xml + +**Use Case 3:** Create Excel Report + +This create report menu with criteria wizard. (example - excel_import_export_sample/report_sale_order) + +1. Create report's menu, action, and add context['template_domain'] to locate the right template for this report -- .xml +2. Create report's wizard for search criteria. The view inherits ``excel_import_export.xlsx_report_view`` and mode="primary". In this view, you only need to add criteria fields, the rest will reuse from interited view -- +3. Create report model as models.Transient, then define search criteria fields, and get reporing data into ``results`` field -- .py +4. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results -- .xlsx +5. Create instruction dictionary for report in xlsx.template model -- templates.xml + +Known issues / Roadmap +====================== + +- Module extension e.g., excel_import_export_async, that add ability to execute as async process. +- Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action. + +Changelog +========= + +12.0.1.0.0 (2019-02-24) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +~~~~~~~ + +* Ecosoft + +Contributors +~~~~~~~~~~~~ + +* Kitti Upariphutthiphong. (http://ecosoft.co.th) + +Maintainers +~~~~~~~~~~~ + +This module is maintained by the OCA. + +.. image:: https://odoo-community.org/logo.png + :alt: Odoo Community Association + :target: https://odoo-community.org + +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. + +.. |maintainer-kittiu| image:: https://github.com/kittiu.png?size=40px + :target: https://github.com/kittiu + :alt: kittiu + +Current `maintainer `__: + +|maintainer-kittiu| + +This module is part of the `OCA/server-tools `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/excel_import_export/__init__.py b/excel_import_export/__init__.py new file mode 100644 index 000000000..673c49964 --- /dev/null +++ b/excel_import_export/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import wizard +from . import models diff --git a/excel_import_export/__manifest__.py b/excel_import_export/__manifest__.py new file mode 100644 index 000000000..fee958bc5 --- /dev/null +++ b/excel_import_export/__manifest__.py @@ -0,0 +1,29 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +{ + 'name': 'Excel Import/Export', + 'summary': 'Base module for easy way to develop Excel import/export', + 'version': '12.0.1.0.0', + 'author': 'Ecosoft,Odoo Community Association (OCA)', + 'license': 'AGPL-3', + 'website': 'https://github.com/OCA/server-tools/', + 'category': 'Tools', + 'depends': ['mail'], + 'external_dependencies': { + 'python': [ + 'xlrd', + 'xlwt', + 'openpyxl', + ], + }, + 'data': ['security/ir.model.access.csv', + 'wizard/export_xlsx_wizard.xml', + 'wizard/import_xlsx_wizard.xml', + 'views/xlsx_template_view.xml', + 'views/xlsx_report.xml', + ], + 'installable': True, + 'development_status': 'alpha', + 'maintainers': ['kittiu'], +} diff --git a/excel_import_export/models/__init__.py b/excel_import_export/models/__init__.py new file mode 100644 index 000000000..6f262fca5 --- /dev/null +++ b/excel_import_export/models/__init__.py @@ -0,0 +1,8 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) +from . import styles +from . import common +from . import xlsx_export +from . import xlsx_import +from . import xlsx_template +from . import xlsx_report diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py new file mode 100644 index 000000000..51c2572a3 --- /dev/null +++ b/excel_import_export/models/common.py @@ -0,0 +1,335 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +import re +import uuid +import csv +import base64 +import string +import itertools +import logging +from datetime import datetime as dt +from ast import literal_eval +from dateutil.parser import parse +from io import StringIO +from odoo.exceptions import ValidationError +from odoo import _ + + +_logger = logging.getLogger(__name__) +try: + import xlrd +except ImportError: + _logger.debug('Cannot import "xlrd". Please make sure it is installed.') + + +def adjust_cell_formula(value, k): + """ Cell formula, i.e., if i=5, val=?(A11)+?(B12) -> val=A16+B17 """ + if isinstance(value, str): + for i in range(value.count('?(')): + if value and '?(' in value and ')' in value: + i = value.index('?(') + j = value.index(')', i) + val = value[i + 2:j] + col, row = split_row_col(val) + new_val = '%s%s' % (col, row+k) + value = value.replace('?(%s)' % val, new_val) + return value + + +def get_field_aggregation(field): + """ i..e, 'field@{sum}' """ + if field and '@{' in field and '}' in field: + i = field.index('@{') + j = field.index('}', i) + cond = field[i + 2:j] + try: + if cond or cond == '': + return (field[:i], cond) + except Exception: + return (field.replace('@{%s}' % cond, ''), False) + return (field, False) + + +def get_field_condition(field): + """ i..e, 'field${value > 0 and value or False}' """ + if field and '${' in field and '}' in field: + i = field.index('${') + j = field.index('}', i) + cond = field[i + 2:j] + try: + if cond or cond == '': + return (field.replace('${%s}' % cond, ''), cond) + except Exception: + return (field, False) + return (field, False) + + +def get_field_style(field): + """ + Available styles + - font = bold, bold_red + - fill = red, blue, yellow, green, grey + - align = left, center, right + - number = true, false + i.e., 'field#{font=bold;fill=red;align=center;style=number}' + """ + if field and '#{' in field and '}' in field: + i = field.index('#{') + j = field.index('}', i) + cond = field[i + 2:j] + try: + if cond or cond == '': + return (field.replace('#{%s}' % cond, ''), cond) + except Exception: + return (field, False) + return (field, False) + + +def get_field_style_cond(field): + """ i..e, 'field#?object.partner_id and #{font=bold} or #{}?' """ + if field and '#?' in field and '?' in field: + i = field.index('#?') + j = field.index('?', i+2) + cond = field[i + 2:j] + try: + if cond or cond == '': + return (field.replace('#?%s?' % cond, ''), cond) + except Exception: + return (field, False) + return (field, False) + + +def fill_cell_style(field, field_style, styles): + field_styles = field_style.split(';') + for f in field_styles: + (key, value) = f.split('=') + if key not in styles.keys(): + raise ValidationError(_('Invalid style type %s' % key)) + if value.lower() not in styles[key].keys(): + raise ValidationError( + _('Invalid value %s for style type %s' % (value, key))) + cell_style = styles[key][value] + if key == 'font': + field.font = cell_style + if key == 'fill': + field.fill = cell_style + if key == 'align': + field.alignment = cell_style + if key == 'style': + if value == 'text': + try: + # In case value can't be encoded as utf, we do normal str() + field.value = field.value.encode('utf-8') + except Exception: + field.value = str(field.value) + field.number_format = cell_style + + +def get_line_max(line_field): + """ i.e., line_field = line_ids[100], max = 100 else 0 """ + if line_field and '[' in line_field and ']' in line_field: + i = line_field.index('[') + j = line_field.index(']') + max_str = line_field[i + 1:j] + try: + if len(max_str) > 0: + return (line_field[:i], int(max_str)) + else: + return (line_field, False) + except Exception: + return (line_field, False) + return (line_field, False) + + +def get_groupby(line_field): + """i.e., line_field = line_ids["a_id, b_id"], groupby = ["a_id", "b_id"]""" + if line_field and '[' in line_field and ']' in line_field: + i = line_field.index('[') + j = line_field.index(']') + groupby = literal_eval(line_field[i:j+1]) + return groupby + return False + + +def split_row_col(pos): + match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) + if not match: + raise ValidationError(_('Position %s is not valid') % pos) + col, row = match.groups() + return col, int(row) + + +def openpyxl_get_sheet_by_name(book, name): + """ Get sheet by name for openpyxl """ + i = 0 + for sheetname in book.sheetnames: + if sheetname == name: + return book.worksheets[i] + i += 1 + raise ValidationError(_("'%s' sheet not found") % (name,)) + + +def xlrd_get_sheet_by_name(book, name): + try: + for idx in itertools.count(): + sheet = book.sheet_by_index(idx) + if sheet.name == name: + return sheet + except IndexError: + raise ValidationError(_("'%s' sheet not found") % (name,)) + + +def isfloat(input): + try: + float(input) + return True + except ValueError: + return False + + +def isinteger(input): + try: + int(input) + return True + except ValueError: + return False + + +def isdatetime(input): + try: + if len(input) == 10: + dt.strptime(input, '%Y-%m-%d') + elif len(input) == 19: + dt.strptime(input, '%Y-%m-%d %H:%M:%S') + else: + return False + return True + except ValueError: + return False + + +def str_to_number(input): + if isinstance(input, str): + if ' ' not in input: + if isdatetime(input): + return parse(input) + elif isinteger(input): + if not (len(input) > 1 and input[:1] == '0'): + return int(input) + elif isfloat(input): + if not (input.find(".") > 2 and input[:1] == '0'): # 00.123 + return float(input) + return input + + +def csv_from_excel(excel_content, delimiter, quote): + decoded_data = base64.decodestring(excel_content) + wb = xlrd.open_workbook(file_contents=decoded_data) + sh = wb.sheet_by_index(0) + content = StringIO() + quoting = csv.QUOTE_ALL + if not quote: + quoting = csv.QUOTE_NONE + if delimiter == " " and quoting == csv.QUOTE_NONE: + quoting = csv.QUOTE_MINIMAL + wr = csv.writer(content, delimiter=delimiter, quoting=quoting) + for rownum in range(sh.nrows): + row = [] + for x in sh.row_values(rownum): + if quoting == csv.QUOTE_NONE and delimiter in x: + raise ValidationError( + _('Template with CSV Quoting = False, data must not ' + 'contain the same char as delimiter -> "%s"') % + delimiter) + row.append(x) + wr.writerow(row) + content.seek(0) # Set index to 0, and start reading + out_file = base64.b64encode(content.getvalue().encode('utf-8')) + return out_file + + +def pos2idx(pos): + match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) + if not match: + raise ValidationError(_('Position %s is not valid') % (pos, )) + col, row = match.groups() + col_num = 0 + for c in col: + if c in string.ascii_letters: + col_num = col_num * 26 + (ord(c.upper()) - ord('A')) + 1 + return (int(row) - 1, col_num - 1) + + +def _get_cell_value(cell, field_type=False): + """ If Odoo's field type is known, convert to valid string for import, + if not know, just get value as is """ + value = False + datemode = 0 # From book.datemode, but we fix it for simplicity + if field_type in ['date', 'datetime']: + ctype = xlrd.sheet.ctype_text.get(cell.ctype, 'unknown type') + if ctype == 'number': + time_tuple = xlrd.xldate_as_tuple(cell.value, datemode) + date = dt(*time_tuple) + if field_type == 'date': + value = date.strftime("%Y-%m-%d") + elif field_type == 'datetime': + value = date.strftime("%Y-%m-%d %H:%M:%S") + else: + value = cell.value + elif field_type in ['integer', 'float']: + value_str = str(cell.value).strip().replace(',', '') + if len(value_str) == 0: + value = '' + elif value_str.replace('.', '', 1).isdigit(): # Is number + if field_type == 'integer': + value = int(float(value_str)) + elif field_type == 'float': + value = float(value_str) + else: # Is string, no conversion + value = value_str + elif field_type in ['many2one']: + # If number, change to string + if isinstance(cell.value, (int, float, complex)): + value = str(cell.value) + else: + value = cell.value + else: # text, char + value = cell.value + # If string, cleanup + if isinstance(value, str): + if value[-2:] == '.0': + value = value[:-2] + # Except boolean, when no value, we should return as '' + if field_type not in ['boolean']: + if not value: + value = '' + return value + + +def _add_column(column_name, column_value, file_txt): + i = 0 + txt_lines = [] + for line in file_txt.split('\n'): + if line and i == 0: + line = '"' + str(column_name) + '",' + line + elif line: + line = '"' + str(column_value) + '",' + line + txt_lines.append(line) + i += 1 + file_txt = '\n'.join(txt_lines) + return file_txt + + +def _add_id_column(file_txt): + i = 0 + txt_lines = [] + for line in file_txt.split('\n'): + if line and i == 0: + line = '"id",' + line + elif line: + line = '%s.%s' % ('xls', uuid.uuid4()) + ',' + line + txt_lines.append(line) + i += 1 + file_txt = '\n'.join(txt_lines) + return file_txt diff --git a/excel_import_export/models/styles.py b/excel_import_export/models/styles.py new file mode 100644 index 000000000..9738a3c8a --- /dev/null +++ b/excel_import_export/models/styles.py @@ -0,0 +1,48 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models, api +import logging + +_logger = logging.getLogger(__name__) + +try: + from openpyxl.styles import colors, PatternFill, Alignment, Font +except ImportError: + _logger.debug( + 'Cannot import "openpyxl". Please make sure it is installed.') + + +class XLSXStyles(models.AbstractModel): + _name = 'xlsx.styles' + _description = 'Available styles for excel' + + @api.model + def get_openpyxl_styles(self): + """ List all syles that can be used with styleing directive #{...} """ + return { + 'font': { + 'bold': Font(name="Arial", size=10, bold=True), + 'bold_red': Font(name="Arial", size=10, + color=colors.RED, bold=True), + }, + 'fill': { + 'red': PatternFill("solid", fgColor="FF0000"), + 'grey': PatternFill("solid", fgColor="DDDDDD"), + 'yellow': PatternFill("solid", fgColor="FFFCB7"), + 'blue': PatternFill("solid", fgColor="9BF3FF"), + 'green': PatternFill("solid", fgColor="B0FF99"), + }, + 'align': { + 'left': Alignment(horizontal='left'), + 'center': Alignment(horizontal='center'), + 'right': Alignment(horizontal='right'), + }, + 'style': { + 'number': '#,##0.00', + 'date': 'dd/mm/yyyy', + 'datestamp': 'yyyy-mm-dd', + 'text': '@', + 'percent': '0.00%', + }, + } diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py new file mode 100644 index 000000000..c7db3f928 --- /dev/null +++ b/excel_import_export/models/xlsx_export.py @@ -0,0 +1,273 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +import os +import logging +import base64 +from io import BytesIO +import time +from datetime import date, datetime as dt +from odoo.tools.float_utils import float_compare +from odoo import models, fields, api, _ +from odoo.tools.safe_eval import safe_eval +from odoo.exceptions import ValidationError +from . import common as co + +_logger = logging.getLogger(__name__) +try: + from openpyxl import load_workbook + from openpyxl.utils.exceptions import IllegalCharacterError +except ImportError: + _logger.debug( + 'Cannot import "openpyxl". Please make sure it is installed.') + + +class XLSXExport(models.AbstractModel): + _name = 'xlsx.export' + _description = 'Excel Export AbstractModel' + + @api.model + def get_eval_context(self, model, record, value): + eval_context = {'float_compare': float_compare, + 'time': time, + 'datetime': dt, + 'date': date, + 'value': value, + 'object': record, + 'model': self.env[model], + 'env': self.env, + 'context': self._context, + } + return eval_context + + @api.model + def _get_line_vals(self, record, line_field, fields): + """ Get values of this field from record set and return as dict of vals + - record: main object + - line_field: rows object, i.e., line_ids + - fields: fields in line_ids, i.e., partner_id.display_name + """ + line_field, max_row = co.get_line_max(line_field) + line_field = line_field.replace('_CONT_', '') # Remove _CONT_ if any + lines = record[line_field] + if max_row > 0 and len(lines) > max_row: + raise Exception( + _('Records in %s exceed max records allowed') % line_field) + vals = dict([(field, []) for field in fields]) # value and do_style + # Get field condition & aggre function + field_cond_dict = {} + aggre_func_dict = {} + field_style_dict = {} + style_cond_dict = {} + pair_fields = [] # I.e., ('debit${value and . or .}@{sum}', 'debit') + for field in fields: + temp_field, eval_cond = co.get_field_condition(field) + eval_cond = eval_cond or 'value or ""' + temp_field, field_style = co.get_field_style(temp_field) + temp_field, style_cond = co.get_field_style_cond(temp_field) + raw_field, aggre_func = co.get_field_aggregation(temp_field) + # Dict of all special conditions + field_cond_dict.update({field: eval_cond}) + aggre_func_dict.update({field: aggre_func}) + field_style_dict.update({field: field_style}) + style_cond_dict.update({field: style_cond}) + # -- + pair_fields.append((field, raw_field)) + for line in lines: + for field in pair_fields: # (field, raw_field) + value = self._get_field_data(field[1], line) + eval_cond = field_cond_dict[field[0]] + eval_context = \ + self.get_eval_context(line._name, line, value) + if eval_cond: + value = safe_eval(eval_cond, eval_context) + # style w/Cond takes priority + style_cond = style_cond_dict[field[0]] + style = self._eval_style_cond(line._name, line, + value, style_cond) + if style is None: + style = False # No style + elif style is False: + style = field_style_dict[field[0]] # Use default style + vals[field[0]].append((value, style)) + return (vals, aggre_func_dict,) + + @api.model + def _eval_style_cond(self, model, record, value, style_cond): + eval_context = self.get_eval_context(model, record, value) + field = style_cond = style_cond or '#??' + styles = {} + for i in range(style_cond.count('#{')): + i += 1 + field, style = co.get_field_style(field) + styles.update({i: style}) + style_cond = style_cond.replace('#{%s}' % style, str(i)) + if not styles: + return False + res = safe_eval(style_cond, eval_context) + if res is None or res is False: + return res + return styles[res] + + @api.model + def _fill_workbook_data(self, workbook, record, data_dict): + """ Fill data from record with style in data_dict to workbook """ + if not record or not data_dict: + return + try: + for sheet_name in data_dict: + ws = data_dict[sheet_name] + st = False + if isinstance(sheet_name, str): + st = co.openpyxl_get_sheet_by_name(workbook, sheet_name) + elif isinstance(sheet_name, int): + if sheet_name > len(workbook.worksheets): + raise Exception(_('Not enough worksheets')) + st = workbook.worksheets[sheet_name - 1] + if not st: + raise ValidationError( + _('Sheet %s not found') % sheet_name) + # Fill data, header and rows + self._fill_head(ws, st, record) + self._fill_lines(ws, st, record) + except KeyError as e: + raise ValidationError(_('Key Error\n%s') % e) + except IllegalCharacterError as e: + raise ValidationError( + _('IllegalCharacterError\n' + 'Some exporting data contain special character\n%s') % e) + except Exception as e: + raise ValidationError( + _('Error filling data into Excel sheets\n%s') % e) + + @api.model + def _get_field_data(self, _field, _line): + """ Get field data, and convert data type if needed """ + if not _field: + return None + line_copy = _line + for f in _field.split('.'): + line_copy = line_copy[f] + if isinstance(line_copy, str): + line_copy = line_copy.encode('utf-8') + return line_copy + + @api.model + def _fill_head(self, ws, st, record): + for rc, field in ws.get('_HEAD_', {}).items(): + tmp_field, eval_cond = co.get_field_condition(field) + eval_cond = eval_cond or 'value or ""' + tmp_field, field_style = co.get_field_style(tmp_field) + tmp_field, style_cond = co.get_field_style_cond(tmp_field) + value = tmp_field and self._get_field_data(tmp_field, record) + # Eval + eval_context = self.get_eval_context(record._name, + record, value) + if eval_cond: + value = safe_eval(eval_cond, eval_context) + if value is not None: + st[rc] = value + fc = not style_cond and True or \ + safe_eval(style_cond, eval_context) + if field_style and fc: # has style and pass style_cond + styles = self.env['xlsx.styles'].get_openpyxl_styles() + co.fill_cell_style(st[rc], field_style, styles) + + @api.model + def _fill_lines(self, ws, st, record): + line_fields = list(ws) + if '_HEAD_' in line_fields: + line_fields.remove('_HEAD_') + cont_row = 0 # last data row to continue + for line_field in line_fields: + fields = ws.get(line_field, {}).values() + vals, func = self._get_line_vals(record, line_field, fields) + is_cont = '_CONT_' in line_field and True or False # continue row + cont_set = 0 + rows_inserted = False # flag to insert row + for rc, field in ws.get(line_field, {}).items(): + col, row = co.split_row_col(rc) # starting point + # Case continue, start from the last data row + if is_cont and not cont_set: # only once per line_field + cont_set = cont_row + 1 + if is_cont: + row = cont_set + rc = '%s%s' % (col, cont_set) + i = 0 + new_row = 0 + new_rc = False + row_count = len(vals[field]) + # Insert rows to preserve total line + if not rows_inserted: + rows_inserted = True + if row_count > 1: + for _x in range(row_count-1): + st.insert_rows(row+1) + # -- + for (row_val, style) in vals[field]: + new_row = row + i + new_rc = '%s%s' % (col, new_row) + row_val = co.adjust_cell_formula(row_val, i) + if row_val not in ('None', None): + st[new_rc] = co.str_to_number(row_val) + if style: + styles = self.env['xlsx.styles'].get_openpyxl_styles() + co.fill_cell_style(st[new_rc], style, styles) + i += 1 + # Add footer line if at least one field have sum + f = func.get(field, False) + if f and new_row > 0: + new_row += 1 + f_rc = '%s%s' % (col, new_row) + st[f_rc] = '=%s(%s:%s)' % (f, rc, new_rc) + cont_row = cont_row < new_row and new_row or cont_row + return + + @api.model + def export_xlsx(self, template, res_model, res_id): + if template.res_model != res_model: + raise ValidationError(_("Template's model mismatch")) + data_dict = co.literal_eval(template.instruction.strip()) + export_dict = data_dict.get('__EXPORT__', False) + out_name = template.name + if not export_dict: # If there is not __EXPORT__ formula, just export + out_name = template.fname + out_file = template.datas + return (out_file, out_name) + # Prepare temp file (from now, only xlsx file works for openpyxl) + decoded_data = base64.decodestring(template.datas) + ConfParam = self.env['ir.config_parameter'] + ptemp = ConfParam.get_param('path_temp_file') or '/tmp' + stamp = dt.utcnow().strftime('%H%M%S%f')[:-3] + ftemp = '%s/temp%s.xlsx' % (ptemp, stamp) + f = open(ftemp, 'wb') + f.write(decoded_data) + f.seek(0) + f.close() + # Workbook created, temp fie removed + wb = load_workbook(ftemp) + os.remove(ftemp) + # Start working with workbook + record = res_model and self.env[res_model].browse(res_id) or False + self._fill_workbook_data(wb, record, export_dict) + # Return file as .xlsx + content = BytesIO() + wb.save(content) + content.seek(0) # Set index to 0, and start reading + out_file = base64.encodestring(content.read()) + if record and 'name' in record and record.name: + out_name = record.name.replace(' ', '').replace('/', '') + else: + fname = out_name.replace(' ', '').replace('/', '') + ts = fields.Datetime.context_timestamp(self, dt.now()) + out_name = '%s_%s' % (fname, ts.strftime('%Y%m%d_%H%M%S')) + if not out_name or len(out_name) == 0: + out_name = 'noname' + out_ext = 'xlsx' + # CSV (convert only on 1st sheet) + if template.to_csv: + delimiter = template.csv_delimiter + out_file = co.csv_from_excel(out_file, delimiter, + template.csv_quote) + out_ext = template.csv_extension + return (out_file, '%s.%s' % (out_name, out_ext)) diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py new file mode 100644 index 000000000..933d86149 --- /dev/null +++ b/excel_import_export/models/xlsx_import.py @@ -0,0 +1,259 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +import base64 +import uuid +import xlrd +import xlwt +import time +from io import BytesIO +from . import common as co +from ast import literal_eval +from datetime import date, datetime as dt +from odoo.tools.float_utils import float_compare +from odoo import models, api, _ +from odoo.exceptions import ValidationError +from odoo.tools.safe_eval import safe_eval + + +class XLSXImport(models.AbstractModel): + _name = 'xlsx.import' + _description = 'Excel Import AbstractModel' + + @api.model + def get_eval_context(self, model=False, value=False): + eval_context = {'float_compare': float_compare, + 'time': time, + 'datetime': dt, + 'date': date, + 'env': self.env, + 'context': self._context, + 'value': False, + 'model': False, + } + if model: + eval_context.update({'model': self.env[model]}) + if value: + if isinstance(value, str): # Remove non Ord 128 character + value = ''.join([i if ord(i) < 128 else ' ' for i in value]) + eval_context.update({'value': value}) + return eval_context + + @api.model + def get_external_id(self, record): + """ Get external ID of the record, if not already exists create one """ + ModelData = self.env['ir.model.data'] + xml_id = record.get_external_id() + if not xml_id or (record.id in xml_id and xml_id[record.id] == ''): + ModelData.create({'name': '%s_%s' % (record._table, record.id), + 'module': 'excel_import_export', + 'model': record._name, + 'res_id': record.id, }) + xml_id = record.get_external_id() + return xml_id[record.id] + + @api.model + def _get_field_type(self, model, field): + try: + record = self.env[model].new() + for f in field.split('/'): + field_type = record._fields[f].type + if field_type in ('one2many', 'many2many'): + record = record[f] + else: + return field_type + except Exception: + raise ValidationError( + _('Invalid declaration, %s has no valid field type') % field) + + @api.model + def _delete_record_data(self, record, data_dict): + """ If no _NODEL_, delete existing lines before importing """ + if not record or not data_dict: + return + try: + for sheet_name in data_dict: + worksheet = data_dict[sheet_name] + line_fields = filter(lambda x: x != '_HEAD_', worksheet) + for line_field in line_fields: + if '_NODEL_' not in line_field: + if line_field in record and record[line_field]: + record[line_field].unlink() + # Remove _NODEL_ from dict + for s, sv in data_dict.items(): + for f, fv in data_dict[s].items(): + if '_NODEL_' in f: + new_fv = data_dict[s].pop(f) + data_dict[s][f.replace('_NODEL_', '')] = new_fv + except Exception as e: + raise ValidationError(_('Error deleting data\n%s') % e) + + @api.model + def _get_line_vals(self, st, worksheet, model, line_field): + """ Get values of this field from excel sheet """ + vals = {} + for rc, columns in worksheet.get(line_field, {}).items(): + if not isinstance(columns, list): + columns = [columns] + for field in columns: + rc, key_eval_cond = co.get_field_condition(rc) + x_field, val_eval_cond = co.get_field_condition(field) + row, col = co.pos2idx(rc) + out_field = '%s/%s' % (line_field, x_field) + field_type = self._get_field_type(model, out_field) + vals.update({out_field: []}) + for idx in range(row, st.nrows): + value = co._get_cell_value(st.cell(idx, col), + field_type=field_type) + eval_context = self.get_eval_context(model=model, + value=value) + if key_eval_cond: + value = safe_eval(key_eval_cond, eval_context) + if val_eval_cond: + value = safe_eval(val_eval_cond, eval_context) + vals[out_field].append(value) + if not filter(lambda x: x != '', vals[out_field]): + vals.pop(out_field) + return vals + + @api.model + def _import_record_data(self, import_file, record, data_dict): + """ From complex excel, create temp simple excel and do import """ + if not data_dict: + return + try: + header_fields = [] + decoded_data = base64.decodestring(import_file) + wb = xlrd.open_workbook(file_contents=decoded_data) + col_idx = 0 + out_wb = xlwt.Workbook() + out_st = out_wb.add_sheet("Sheet 1") + xml_id = record and self.get_external_id(record) or \ + '%s.%s' % ('xls', uuid.uuid4()) + out_st.write(0, 0, 'id') # id and xml_id on first column + out_st.write(1, 0, xml_id) + header_fields.append('id') + col_idx += 1 + model = record._name + for sheet_name in data_dict: # For each Sheet + worksheet = data_dict[sheet_name] + st = False + if isinstance(sheet_name, str): + st = co.xlrd_get_sheet_by_name(wb, sheet_name) + elif isinstance(sheet_name, int): + st = wb.sheet_by_index(sheet_name - 1) + if not st: + raise ValidationError( + _('Sheet %s not found') % sheet_name) + # HEAD updates + for rc, field in worksheet.get('_HEAD_', {}).items(): + rc, key_eval_cond = co.get_field_condition(rc) + field, val_eval_cond = co.get_field_condition(field) + field_type = self._get_field_type(model, field) + value = False + try: + row, col = co.pos2idx(rc) + value = co._get_cell_value(st.cell(row, col), + field_type=field_type) + except Exception: + pass + eval_context = self.get_eval_context(model=model, + value=value) + if key_eval_cond: + value = str(safe_eval(key_eval_cond, eval_context)) + if val_eval_cond: + value = str(safe_eval(val_eval_cond, eval_context)) + out_st.write(0, col_idx, field) # Next Column + out_st.write(1, col_idx, value) # Next Value + header_fields.append(field) + col_idx += 1 + # Line Items + line_fields = filter(lambda x: x != '_HEAD_', worksheet) + for line_field in line_fields: + vals = self._get_line_vals(st, worksheet, + model, line_field) + for field in vals: + # Columns, i.e., line_ids/field_id + out_st.write(0, col_idx, field) + header_fields.append(field) + # Data + i = 1 + for value in vals[field]: + out_st.write(i, col_idx, value) + i += 1 + col_idx += 1 + content = BytesIO() + out_wb.save(content) + content.seek(0) # Set index to 0, and start reading + xls_file = content.read() + # Do the import + Import = self.env['base_import.import'] + imp = Import.create({ + 'res_model': model, + 'file': xls_file, + 'file_type': 'application/vnd.ms-excel', + 'file_name': 'temp.xls', + }) + errors = imp.do( + header_fields, + header_fields, + {'headers': True, + 'advanced': True, + 'keep_matches': False, + 'encoding': '', + 'separator': '', + 'quoting': '"', + 'date_style': '', + 'datetime_style': '%Y-%m-%d %H:%M:%S', + 'float_thousand_separator': ',', + 'float_decimal_separator': '.', + 'fields': []}) + if errors.get('messages'): + message = errors['messages']['message'].encode('utf-8') + raise ValidationError(message) + return self.env.ref(xml_id) + except xlrd.XLRDError: + raise ValidationError( + _('Invalid file style, only .xls or .xlsx file allowed')) + except Exception as e: + raise ValidationError(_('Error importing data\n%s') % e) + + @api.model + def _post_import_operation(self, record, operation): + """ Run python code after import """ + if not record or not operation: + return + try: + if '${' in operation: + code = (operation.split('${'))[1].split('}')[0] + eval_context = {'object': record} + safe_eval(code, eval_context) + except Exception as e: + raise ValidationError(_('Post import operation error\n%s') % e) + + @api.model + def import_xlsx(self, import_file, template, + res_model=False, res_id=False): + """ + - If res_id = False, we want to create new document first + - Delete fields' data according to data_dict['__IMPORT__'] + - Import data from excel according to data_dict['__IMPORT__'] + """ + self = self.sudo() + if res_model and template.res_model != res_model: + raise ValidationError(_("Template's model mismatch")) + record = self.env[template.res_model].browse(res_id) + data_dict = literal_eval(template.instruction.strip()) + if not data_dict.get('__IMPORT__'): + raise ValidationError( + _("No data_dict['__IMPORT__'] in template %s") % template.name) + if record: + # Delete existing data first + self._delete_record_data(record, data_dict['__IMPORT__']) + # Fill up record with data from excel sheets + record = self._import_record_data(import_file, record, + data_dict['__IMPORT__']) + # Post Import Operation, i.e., cleanup some data + if data_dict.get('__POST_IMPORT__', False): + self._post_import_operation(record, data_dict['__POST_IMPORT__']) + return record diff --git a/excel_import_export/models/xlsx_report.py b/excel_import_export/models/xlsx_report.py new file mode 100644 index 000000000..f123d2a65 --- /dev/null +++ b/excel_import_export/models/xlsx_report.py @@ -0,0 +1,69 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class XLSXReport(models.AbstractModel): + """ Common class for xlsx reporting wizard """ + _name = 'xlsx.report' + _description = 'Excel Report AbstractModel' + + name = fields.Char( + string='File Name', + readonly=True, + size=500, + ) + data = fields.Binary( + string='File', + readonly=True, + ) + template_id = fields.Many2one( + 'xlsx.template', + string='Template', + required=True, + ondelete='cascade', + domain=lambda self: self._context.get('template_domain', []), + ) + choose_template = fields.Boolean( + string='Allow Choose Template', + default=False, + ) + state = fields.Selection( + [('choose', 'Choose'), + ('get', 'Get')], + default='choose', + help="* Choose: wizard show in user selection mode" + "\n* Get: wizard show results from user action", + ) + + @api.model + def default_get(self, fields): + template_domain = self._context.get('template_domain', []) + templates = self.env['xlsx.template'].search(template_domain) + if not templates: + raise ValidationError(_('No template found')) + defaults = super(XLSXReport, self).default_get(fields) + for template in templates: + if not template.datas: + raise ValidationError(_('No file in %s') % (template.name,)) + defaults['template_id'] = len(templates) == 1 and templates.id or False + return defaults + + @api.multi + def report_xlsx(self): + self.ensure_one() + Export = self.env['xlsx.export'] + out_file, out_name = \ + Export.export_xlsx(self.template_id, self._name, self.id) + self.write({'state': 'get', 'data': out_file, 'name': out_name}) + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.id, + 'views': [(False, 'form')], + 'target': 'new', + } diff --git a/excel_import_export/models/xlsx_template.py b/excel_import_export/models/xlsx_template.py new file mode 100644 index 000000000..1460473a8 --- /dev/null +++ b/excel_import_export/models/xlsx_template.py @@ -0,0 +1,452 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +import os +import base64 +from ast import literal_eval +from odoo import api, fields, models, _ +from odoo.modules.module import get_module_path +from os.path import join as opj +from . import common as co +from odoo.exceptions import ValidationError + + +class XLSXTemplate(models.Model): + """ Master Data for XLSX Templates + - Excel Template + - Import/Export Meta Data (dict text) + - Default values, etc. + """ + _name = 'xlsx.template' + _description = 'Excel template file and instruction' + _order = 'name' + + name = fields.Char( + string='Template Name', + required=True, + ) + res_model = fields.Char( + string='Resource Model', + help="The database object this attachment will be attached to.", + ) + fname = fields.Char( + string='File Name', + ) + gname = fields.Char( + string='Group Name', + help="Multiple template of same model, can belong to same group,\n" + "result in multiple template selection", + ) + description = fields.Char( + string='Description', + ) + input_instruction = fields.Text( + string='Instruction (Input)', + help="This is used to construct instruction in tab Import/Export", + ) + instruction = fields.Text( + string='Instruction', + compute='_compute_output_instruction', + help="Instruction on how to import/export, prepared by system." + ) + datas = fields.Binary( + string='File Content', + ) + to_csv = fields.Boolean( + string='Convert to CSV?', + default=False, + ) + csv_delimiter = fields.Char( + string='CSV Delimiter', + size=1, + default=',', + required=True, + help="Optional for CSV, default is comma.", + ) + csv_extension = fields.Char( + string='CSV File Extension', + size=5, + default='csv', + required=True, + help="Optional for CSV, default is .csv" + ) + csv_quote = fields.Boolean( + string='CSV Quoting', + default=True, + help="Optional for CSV, default is full quoting." + ) + export_ids = fields.One2many( + comodel_name='xlsx.template.export', + inverse_name='template_id', + ) + import_ids = fields.One2many( + comodel_name='xlsx.template.import', + inverse_name='template_id', + ) + post_import_hook = fields.Char( + string='Post Import Function Hook', + help="Call a function after successful import, i.e.,\n" + "${object.post_import_do_something()}", + ) + show_instruction = fields.Boolean( + string='Show Output', + default=False, + help="This is the computed instruction based on tab Import/Export,\n" + "to be used by xlsx import/export engine", + ) + redirect_action = fields.Many2one( + comodel_name='ir.actions.act_window', + string='Return Action', + domain=[('type', '=', 'ir.actions.act_window')], + help="Optional action, redirection after finish import operation", + ) + + @api.multi + @api.constrains('redirect_action', 'res_model') + def _check_action_model(self): + for rec in self: + if rec.res_model and rec.redirect_action and \ + rec.res_model != rec.redirect_action.res_model: + raise ValidationError(_('The selected redirect action is ' + 'not for model %s') % rec.res_model) + + @api.model + def load_xlsx_template(self, tempalte_ids, addon=False): + for template in self.browse(tempalte_ids): + if not addon: + addon = list(template.get_external_id(). + values())[0].split('.')[0] + addon_path = get_module_path(addon) + file_path = False + for root, dirs, files in os.walk(addon_path): + for name in files: + if name == template.fname: + file_path = os.path.abspath(opj(root, name)) + if file_path: + template.datas = base64.b64encode(open(file_path, 'rb').read()) + return True + + @api.model + def create(self, vals): + rec = super().create(vals) + if vals.get('input_instruction'): + rec._compute_input_export_instruction() + rec._compute_input_import_instruction() + rec._compute_input_post_import_hook() + return rec + + @api.multi + def write(self, vals): + res = super().write(vals) + if vals.get('input_instruction'): + for rec in self: + rec._compute_input_export_instruction() + rec._compute_input_import_instruction() + rec._compute_input_post_import_hook() + return res + + @api.multi + def _compute_input_export_instruction(self): + self = self.with_context(compute_from_input=True) + for rec in self: + # Export Instruction + input_dict = literal_eval(rec.input_instruction.strip()) + rec.export_ids.unlink() + export_dict = input_dict.get('__EXPORT__') + if not export_dict: + continue + export_lines = [] + sequence = 0 + # Sheet + for sheet, rows in export_dict.items(): + sequence += 1 + vals = {'sequence': sequence, + 'section_type': 'sheet', + 'sheet': str(sheet), + } + export_lines.append((0, 0, vals)) + # Rows + for row_field, lines in rows.items(): + sequence += 1 + is_cont = False + if '_CONT_' in row_field: + is_cont = True + row_field = row_field.replace('_CONT_', '') + vals = {'sequence': sequence, + 'section_type': (row_field == '_HEAD_' and + 'head' or 'row'), + 'row_field': row_field, + 'is_cont': is_cont, + } + export_lines.append((0, 0, vals)) + for excel_cell, field_name in lines.items(): + sequence += 1 + vals = {'sequence': sequence, + 'section_type': 'data', + 'excel_cell': excel_cell, + 'field_name': field_name, + } + export_lines.append((0, 0, vals)) + rec.write({'export_ids': export_lines}) + + @api.multi + def _compute_input_import_instruction(self): + self = self.with_context(compute_from_input=True) + for rec in self: + # Import Instruction + input_dict = literal_eval(rec.input_instruction.strip()) + rec.import_ids.unlink() + import_dict = input_dict.get('__IMPORT__') + if not import_dict: + continue + import_lines = [] + sequence = 0 + # Sheet + for sheet, rows in import_dict.items(): + sequence += 1 + vals = {'sequence': sequence, + 'section_type': 'sheet', + 'sheet': str(sheet), + } + import_lines.append((0, 0, vals)) + # Rows + for row_field, lines in rows.items(): + sequence += 1 + no_delete = False + if '_NODEL_' in row_field: + no_delete = True + row_field = row_field.replace('_NODEL_', '') + vals = {'sequence': sequence, + 'section_type': (row_field == '_HEAD_' and + 'head' or 'row'), + 'row_field': row_field, + 'no_delete': no_delete, + } + import_lines.append((0, 0, vals)) + for excel_cell, field_name in lines.items(): + sequence += 1 + vals = {'sequence': sequence, + 'section_type': 'data', + 'excel_cell': excel_cell, + 'field_name': field_name, + } + import_lines.append((0, 0, vals)) + rec.write({'import_ids': import_lines}) + + @api.multi + def _compute_input_post_import_hook(self): + self = self.with_context(compute_from_input=True) + for rec in self: + # Import Instruction + input_dict = literal_eval(rec.input_instruction.strip()) + rec.post_import_hook = input_dict.get('__POST_IMPORT__') + + @api.multi + def _compute_output_instruction(self): + """ From database, compute back to dictionary """ + for rec in self: + inst_dict = {} + prev_sheet = False + prev_row = False + # Export Instruction + itype = '__EXPORT__' + inst_dict[itype] = {} + for line in rec.export_ids: + if line.section_type == 'sheet': + sheet = co.isinteger(line.sheet) and \ + int(line.sheet) or line.sheet + sheet_dict = {sheet: {}} + inst_dict[itype].update(sheet_dict) + prev_sheet = sheet + continue + if line.section_type in ('head', 'row'): + row_field = line.row_field + if line.section_type == 'row' and line.is_cont: + row_field = '_CONT_%s' % row_field + row_dict = {row_field: {}} + inst_dict[itype][prev_sheet].update(row_dict) + prev_row = row_field + continue + if line.section_type == 'data': + excel_cell = line.excel_cell + field_name = line.field_name or '' + field_name += line.field_cond or '' + field_name += line.style or '' + field_name += line.style_cond or '' + if line.is_sum: + field_name += '@{sum}' + cell_dict = {excel_cell: field_name} + inst_dict[itype][prev_sheet][prev_row].update(cell_dict) + continue + # Import Instruction + itype = '__IMPORT__' + inst_dict[itype] = {} + for line in rec.import_ids: + if line.section_type == 'sheet': + sheet = co.isinteger(line.sheet) and \ + int(line.sheet) or line.sheet + sheet_dict = {sheet: {}} + inst_dict[itype].update(sheet_dict) + prev_sheet = sheet + continue + if line.section_type in ('head', 'row'): + row_field = line.row_field + if line.section_type == 'row' and line.no_delete: + row_field = '_NODEL_%s' % row_field + row_dict = {row_field: {}} + inst_dict[itype][prev_sheet].update(row_dict) + prev_row = row_field + continue + if line.section_type == 'data': + excel_cell = line.excel_cell + field_name = line.field_name or '' + field_name += line.field_cond or '' + cell_dict = {excel_cell: field_name} + inst_dict[itype][prev_sheet][prev_row].update(cell_dict) + continue + itype = '__POST_IMPORT__' + inst_dict[itype] = False + if rec.post_import_hook: + inst_dict[itype] = rec.post_import_hook + rec.instruction = inst_dict + + +class XLSXTemplateImport(models.Model): + _name = 'xlsx.template.import' + _description = 'Detailed of how excel data will be imported' + _order = 'sequence' + + template_id = fields.Many2one( + comodel_name='xlsx.template', + string='XLSX Template', + index=True, + ondelete='cascade', + readonly=True, + ) + sequence = fields.Integer( + string='Sequence', + default=10, + ) + sheet = fields.Char( + string='Sheet', + ) + section_type = fields.Selection( + [('sheet', 'Sheet'), + ('head', 'Head'), + ('row', 'Row'), + ('data', 'Data')], + string='Section Type', + required=True, + ) + row_field = fields.Char( + string='Row Field', + help="If section type is row, this field is required", + ) + no_delete = fields.Boolean( + string='No Delete', + default=False, + help="By default, all rows will be deleted before import.\n" + "Select No Delete, otherwise" + ) + excel_cell = fields.Char( + string='Cell', + ) + field_name = fields.Char( + string='Field', + ) + field_cond = fields.Char( + string='Field Cond.', + ) + + @api.model + def create(self, vals): + new_vals = self._extract_field_name(vals) + return super().create(new_vals) + + @api.model + def _extract_field_name(self, vals): + if self._context.get('compute_from_input') and vals.get('field_name'): + field_name, field_cond = co.get_field_condition(vals['field_name']) + field_cond = field_cond and '${%s}' % (field_cond or '') or False + vals.update({'field_name': field_name, + 'field_cond': field_cond, + }) + return vals + + +class XLSXTemplateExport(models.Model): + _name = 'xlsx.template.export' + _description = 'Detailed of how excel data will be exported' + _order = 'sequence' + + template_id = fields.Many2one( + comodel_name='xlsx.template', + string='XLSX Template', + index=True, + ondelete='cascade', + readonly=True, + ) + sequence = fields.Integer( + string='Sequence', + default=10, + ) + sheet = fields.Char( + string='Sheet', + ) + section_type = fields.Selection( + [('sheet', 'Sheet'), + ('head', 'Head'), + ('row', 'Row'), + ('data', 'Data')], + string='Section Type', + required=True, + ) + row_field = fields.Char( + string='Row Field', + help="If section type is row, this field is required", + ) + is_cont = fields.Boolean( + string='Continue', + default=False, + help="Continue data rows after last data row", + ) + excel_cell = fields.Char( + string='Cell', + ) + field_name = fields.Char( + string='Field', + ) + field_cond = fields.Char( + string='Field Cond.', + ) + is_sum = fields.Boolean( + string='Sum', + default=False, + ) + style = fields.Char( + string='Default Style', + ) + style_cond = fields.Char( + string='Style w/Cond.', + ) + + @api.model + def create(self, vals): + new_vals = self._extract_field_name(vals) + return super().create(new_vals) + + @api.model + def _extract_field_name(self, vals): + if self._context.get('compute_from_input') and vals.get('field_name'): + field_name, field_cond = co.get_field_condition(vals['field_name']) + field_cond = field_cond or 'value or ""' + field_name, style = co.get_field_style(field_name) + field_name, style_cond = co.get_field_style_cond(field_name) + field_name, func = co.get_field_aggregation(field_name) + vals.update({'field_name': field_name, + 'field_cond': '${%s}' % (field_cond or ''), + 'style': '#{%s}' % (style or ''), + 'style_cond': '#?%s?' % (style_cond or ''), + 'is_sum': func == 'sum' and True or False, + }) + return vals diff --git a/excel_import_export/readme/CONTRIBUTORS.rst b/excel_import_export/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..9c7a518b1 --- /dev/null +++ b/excel_import_export/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Kitti Upariphutthiphong. (http://ecosoft.co.th) diff --git a/excel_import_export/readme/DESCRIPTION.rst b/excel_import_export/readme/DESCRIPTION.rst new file mode 100644 index 000000000..408b485fc --- /dev/null +++ b/excel_import_export/readme/DESCRIPTION.rst @@ -0,0 +1,8 @@ +The module provide pre-built functions and wizards for developer to build excel import / export / report with ease. + +Without having to code to create excel file, developer do, + +- Create menu, action, wizard, model, view a normal Odoo development. +- Design excel template using standard Excel application, e.g., colors, fonts, formulas, etc. +- Instruct how the data will be located in Excel with simple dictionary instruction or from Odoo UI. +- Odoo will combine instruction with excel template, and result in final excel file. diff --git a/excel_import_export/readme/HISTORY.rst b/excel_import_export/readme/HISTORY.rst new file mode 100644 index 000000000..b8d1b41b6 --- /dev/null +++ b/excel_import_export/readme/HISTORY.rst @@ -0,0 +1,4 @@ +12.0.1.0.0 (2019-02-24) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Start of the history diff --git a/excel_import_export/readme/INSTALL.rst b/excel_import_export/readme/INSTALL.rst new file mode 100644 index 000000000..f35fa5a09 --- /dev/null +++ b/excel_import_export/readme/INSTALL.rst @@ -0,0 +1,5 @@ +To install this module, you need to install following python library, **xlrd, xlwt, openpyxl**. + +Then, simply install **excel_import_export**. + +For samples, install **excel_import_export_sample**. diff --git a/excel_import_export/readme/ROADMAP.rst b/excel_import_export/readme/ROADMAP.rst new file mode 100644 index 000000000..21040a2d2 --- /dev/null +++ b/excel_import_export/readme/ROADMAP.rst @@ -0,0 +1,2 @@ +- Module extension e.g., excel_import_export_async, that add ability to execute as async process. +- Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action. diff --git a/excel_import_export/readme/USAGE.rst b/excel_import_export/readme/USAGE.rst new file mode 100644 index 000000000..372fda63b --- /dev/null +++ b/excel_import_export/readme/USAGE.rst @@ -0,0 +1,41 @@ +This module contain pre-defined function and wizards to make exporting, importing and reporting easy. + +At the heart of this module, there are 2 `main methods` + +- ``self.env['xlsx.export'].export_xlsx(...)`` +- ``self.env['xlsx.import'].import_xlsx(...)`` + +For reporting, also call `export_xlsx(...)` but through following method + +- ``self.env['xslx.report'].report_xlsx(...)`` + +After install this module, go to Settings > Excel Import/Export > XLSX Templates, this is where the key component located. + +As this module provide tools, it is best to explain as use cases. For example use cases, please install **excel_import_export_sample** + +**Use Case 1:** Export/Import Excel on existing document + +This add export/import action menus in existing document (example - excel_import_export_sample/import_export_sale_order) + +1. Create export action menu on document, with res_model="export.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- actions.xml +2. Create import action menu on document, with res_model="import.xlsx.wizard" and src_model="", and context['template_domain'] to locate the right template -- action.xml +3. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import -- .xlsx +4. Create instruction dictionary for export/import in xlsx.template model -- templates.xml + +**Use Case 2:** Import Excel Files + +With menu wizard to create new documents (example - excel_import_export_sample/import_sale_orders) + +1. Create report menu with search wizard, res_model="import.xlsx.wizard" and context['template_domain'] to locate the right template -- menu_action.xml +2. Create Excel Template File (.xlsx), in the template, name the underlining tab used for import -- .xlsx +3. Create instruction dictionary for import in xlsx.template model -- templates.xml + +**Use Case 3:** Create Excel Report + +This create report menu with criteria wizard. (example - excel_import_export_sample/report_sale_order) + +1. Create report's menu, action, and add context['template_domain'] to locate the right template for this report -- .xml +2. Create report's wizard for search criteria. The view inherits ``excel_import_export.xlsx_report_view`` and mode="primary". In this view, you only need to add criteria fields, the rest will reuse from interited view -- +3. Create report model as models.Transient, then define search criteria fields, and get reporing data into ``results`` field -- .py +4. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results -- .xlsx +5. Create instruction dictionary for report in xlsx.template model -- templates.xml diff --git a/excel_import_export/security/ir.model.access.csv b/excel_import_export/security/ir.model.access.csv new file mode 100644 index 000000000..e6a1652ff --- /dev/null +++ b/excel_import_export/security/ir.model.access.csv @@ -0,0 +1,4 @@ +"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" +xlsx_template_user,xlsx_template_user,model_xlsx_template,,1,1,1,1 +xlsx_template_export_user,xlsx_template_export_user,model_xlsx_template_export,,1,1,1,1 +xlsx_template_import_user,xlsx_template_import_user,model_xlsx_template_import,,1,1,1,1 diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html new file mode 100644 index 000000000..4a8b0b106 --- /dev/null +++ b/excel_import_export/static/description/index.html @@ -0,0 +1,496 @@ + + + + + + +Excel Import/Export + + + +
+

Excel Import/Export

+ + +

License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

The module provide pre-built functions and wizards for developer to build excel import / export / report with ease.

+

Without having to code to create excel file, developer do,

+
    +
  • Create menu, action, wizard, model, view a normal Odoo development.
  • +
  • Design excel template using standard Excel application, e.g., colors, fonts, formulas, etc.
  • +
  • Instruct how the data will be located in Excel with simple dictionary instruction or from Odoo UI.
  • +
  • Odoo will combine instruction with excel template, and result in final excel file.
  • +
+

Table of contents

+ +
+

Installation

+

To install this module, you need to install following python library, xlrd, xlwt, openpyxl.

+

Then, simply install excel_import_export.

+

For samples, install excel_import_export_sample.

+
+
+

Usage

+

This module contain pre-defined function and wizards to make exporting, importing and reporting easy.

+

At the heart of this module, there are 2 main methods

+
    +
  • self.env['xlsx.export'].export_xlsx(...)
  • +
  • self.env['xlsx.import'].import_xlsx(...)
  • +
+

For reporting, also call export_xlsx(…) but through following method

+
    +
  • self.env['xslx.report'].report_xlsx(...)
  • +
+

After install this module, go to Settings > Excel Import/Export > XLSX Templates, this is where the key component located.

+

As this module provide tools, it is best to explain as use cases. For example use cases, please install excel_import_export_sample

+

Use Case 1: Export/Import Excel on existing document

+

This add export/import action menus in existing document (example - excel_import_export_sample/import_export_sale_order)

+
    +
  1. Create export action menu on document, <act_window> with res_model=”export.xlsx.wizard” and src_model=”<document_model>”, and context[‘template_domain’] to locate the right template – actions.xml
  2. +
  3. Create import action menu on document, <act_window> with res_model=”import.xlsx.wizard” and src_model=”<document_model>”, and context[‘template_domain’] to locate the right template – action.xml
  4. +
  5. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import – <file>.xlsx
  6. +
  7. Create instruction dictionary for export/import in xlsx.template model – templates.xml
  8. +
+

Use Case 2: Import Excel Files

+

With menu wizard to create new documents (example - excel_import_export_sample/import_sale_orders)

+
    +
  1. Create report menu with search wizard, res_model=”import.xlsx.wizard” and context[‘template_domain’] to locate the right template – menu_action.xml
  2. +
  3. Create Excel Template File (.xlsx), in the template, name the underlining tab used for import – <import file>.xlsx
  4. +
  5. Create instruction dictionary for import in xlsx.template model – templates.xml
  6. +
+

Use Case 3: Create Excel Report

+

This create report menu with criteria wizard. (example - excel_import_export_sample/report_sale_order)

+
    +
  1. Create report’s menu, action, and add context[‘template_domain’] to locate the right template for this report – <report>.xml
  2. +
  3. Create report’s wizard for search criteria. The view inherits excel_import_export.xlsx_report_view and mode=”primary”. In this view, you only need to add criteria fields, the rest will reuse from interited view – <report.xml>
  4. +
  5. Create report model as models.Transient, then define search criteria fields, and get reporing data into results field – <report>.py
  6. +
  7. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results – <report_file>.xlsx
  8. +
  9. Create instruction dictionary for report in xlsx.template model – templates.xml
  10. +
+
+
+

Known issues / Roadmap

+
    +
  • Module extension e.g., excel_import_export_async, that add ability to execute as async process.
  • +
  • Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action.
  • +
+
+
+

Changelog

+
+

12.0.1.0.0 (2019-02-24)

+
    +
  • Start of the history
  • +
+
+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Ecosoft
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

Current maintainer:

+

kittiu

+

This module is part of the OCA/server-tools project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/excel_import_export/views/xlsx_report.xml b/excel_import_export/views/xlsx_report.xml new file mode 100644 index 000000000..96853ea4a --- /dev/null +++ b/excel_import_export/views/xlsx_report.xml @@ -0,0 +1,51 @@ + + + + + + xlsx.report.view + xlsx.report + +
+ + + + + + +
+ + + +
+
+
+

+ Complete Prepare Report (.xlsx) +

+

+ Here is the report file: + +

+
+
+
+
+
+
+ +
+
+
+ +
diff --git a/excel_import_export/views/xlsx_template_view.xml b/excel_import_export/views/xlsx_template_view.xml new file mode 100644 index 000000000..a363ad194 --- /dev/null +++ b/excel_import_export/views/xlsx_template_view.xml @@ -0,0 +1,230 @@ + + + + + + xlsx.template + + + + + + + + + xlsx.template + +
+ +

+ +

+ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+

Help with Export Instruction

+

+ Export Instruction is how to write data from an active data record to specified cells in excel sheet. + For example, an active record can be a sale order that user want to export. + The record itself will be mapped to the header part of excel sheet. The record can contain multiple one2many fields, which will be written as data lines. + You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields). +

+
    +
  • In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2).
  • +
  • In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6).
  • +
+

Following are more explaination on each column:

+
    +
  • Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to
  • +
  • Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data
  • +
  • Continue: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field
  • +
  • Cell: Location of data in excel sheet (e.g., A1, B1, ...)
  • +
  • Field: Field of the record, e.g., product_id.uom_id.name. They are orm compliant.
  • +
  • Field Cond.: Python code in ${...} to manipulate field value, e.g., if field = product_id, value will represent product object, e.g., ${value and value.uom_id.name or ""}
  • +
  • Sum: Add sum value on last row, @{sum}
  • +
  • Style: Default style in #{...} that apply to each cell, e.g., #{align=left;style=text}. See module's style.py for available styles.
  • +
  • Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == "ABC" and #{font=bold;fill=red} or None?
  • +
+

Note:

+ For code block ${...} and #?...?, following object are available, +
    +
  • value: value from Field
  • +
  • object: record object or line object depends on Row Field
  • +
  • model: active model, e.g., self.env['my.model']
  • +
  • date, datetime, time: some useful python classes
  • +
+
+
+ + + + + + + + + + + + + + + + + + + + + + +
+
+

Help with Import Instruction

+

+ Import Instruction is how to get data from excel sheet and write them to an active record. + For example, user create a sales order document, and want to import order lines from excel. + In reverse direction to exporting, data from excel's cells will be mapped to record fields during import. + Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells. +

+
    +
  • In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id).
  • +
  • In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty)
  • +
+

Following are more explaination on each column:

+
    +
  • Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet
  • +
  • Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data
  • +
  • No Delete: By default, all one2many lines will be deleted before import. Select this, to avoid deletion
  • +
  • Cell: Location of data in excel sheet (e.g., A1, B1, ...)
  • +
  • Field: Field of the record to be imported to, e.g., product_id
  • +
  • Field Cond.: Python code in ${...} value will represent data from excel cell, e.g., if A1 = 'ABC', value will represent 'ABC', e.g., ${value == "ABC" and "X" or "Y"} thus can change from cell value to other value for import.
  • +
+

Note:

+ For code block ${...}, following object are available, +
    +
  • value: value from Cell
  • +
  • model: active model, e.g., self.env['my.model']
  • +
  • date, datetime, time: some useful python classes
  • +
+
+
+ + + +
+
+
+
+
+ + + XLSX Templates + ir.actions.act_window + xlsx.template + form + tree,form + +

+ Click to create a XLSX Template Object. +

+
+
+ + + + + +
diff --git a/excel_import_export/wizard/__init__.py b/excel_import_export/wizard/__init__.py new file mode 100644 index 000000000..136b4ff0c --- /dev/null +++ b/excel_import_export/wizard/__init__.py @@ -0,0 +1,2 @@ +from . import export_xlsx_wizard +from . import import_xlsx_wizard diff --git a/excel_import_export/wizard/export_xlsx_wizard.py b/excel_import_export/wizard/export_xlsx_wizard.py new file mode 100644 index 000000000..1807ea7e9 --- /dev/null +++ b/excel_import_export/wizard/export_xlsx_wizard.py @@ -0,0 +1,82 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError + + +class ExportXLSXWizard(models.TransientModel): + """ This wizard is used with the template (xlsx.template) to export + xlsx template filled with data form the active record """ + _name = 'export.xlsx.wizard' + _description = 'Wizard for exporting excel' + + name = fields.Char( + string='File Name', + readonly=True, + size=500, + ) + data = fields.Binary( + string='File', + readonly=True, + ) + template_id = fields.Many2one( + 'xlsx.template', + string='Template', + required=True, + ondelete='cascade', + domain=lambda self: self._context.get('template_domain', []), + ) + res_id = fields.Integer( + string='Resource ID', + readonly=True, + required=True, + ) + res_model = fields.Char( + string='Resource Model', + readonly=True, + required=True, + size=500, + ) + state = fields.Selection( + [('choose', 'Choose'), + ('get', 'Get')], + default='choose', + help="* Choose: wizard show in user selection mode" + "\n* Get: wizard show results from user action", + ) + + @api.model + def default_get(self, fields): + res_model = self._context.get('active_model', False) + res_id = self._context.get('active_id', False) + template_domain = self._context.get('template_domain', []) + templates = self.env['xlsx.template'].search(template_domain) + if not templates: + raise ValidationError(_('No template found')) + defaults = super(ExportXLSXWizard, self).default_get(fields) + for template in templates: + if not template.datas: + raise ValidationError(_('No file in %s') % (template.name,)) + defaults['template_id'] = len(templates) == 1 and templates.id or False + defaults['res_id'] = res_id + defaults['res_model'] = res_model + return defaults + + @api.multi + def action_export(self): + self.ensure_one() + Export = self.env['xlsx.export'] + out_file, out_name = Export.export_xlsx(self.template_id, + self.res_model, + self.res_id) + self.write({'state': 'get', 'data': out_file, 'name': out_name}) + return { + 'type': 'ir.actions.act_window', + 'res_model': 'export.xlsx.wizard', + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.id, + 'views': [(False, 'form')], + 'target': 'new', + } diff --git a/excel_import_export/wizard/export_xlsx_wizard.xml b/excel_import_export/wizard/export_xlsx_wizard.xml new file mode 100644 index 000000000..b39ec97c7 --- /dev/null +++ b/excel_import_export/wizard/export_xlsx_wizard.xml @@ -0,0 +1,39 @@ + + + + + + export.xlsx.wizard + export.xlsx.wizard + +
+ + + + + + + + + + + +
+

Complete Prepare File (.xlsx)

+

Here is the exported file:

+
+
+
+
+
+ + +
+
+ +
diff --git a/excel_import_export/wizard/import_xlsx_wizard.py b/excel_import_export/wizard/import_xlsx_wizard.py new file mode 100644 index 000000000..750dc17e1 --- /dev/null +++ b/excel_import_export/wizard/import_xlsx_wizard.py @@ -0,0 +1,146 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from odoo import models, fields, api, _ +from odoo.exceptions import ValidationError, RedirectWarning + + +class ImportXLSXWizard(models.TransientModel): + """ This wizard is used with the template (xlsx.template) to import + xlsx template back to active record """ + _name = 'import.xlsx.wizard' + _description = 'Wizard for importing excel' + + import_file = fields.Binary( + string='Import File (*.xlsx)', + ) + template_id = fields.Many2one( + 'xlsx.template', + string='Template', + required=True, + ondelete='set null', + domain=lambda self: self._context.get('template_domain', []), + ) + res_id = fields.Integer( + string='Resource ID', + readonly=True, + ) + res_model = fields.Char( + string='Resource Model', + readonly=True, + size=500, + ) + datas = fields.Binary( + string='Sample', + related='template_id.datas', + readonly=True, + ) + fname = fields.Char( + string='Template Name', + related='template_id.fname', + readonly=True, + ) + attachment_ids = fields.Many2many( + 'ir.attachment', + string='Import File(s) (*.xlsx)', + required=True, + help="You can select multiple files to import.", + ) + state = fields.Selection( + [('choose', 'Choose'), + ('get', 'Get')], + default='choose', + help="* Choose: wizard show in user selection mode" + "\n* Get: wizard show results from user action", + ) + + @api.model + def view_init(self, fields_list): + """ This template only works on some context of active record """ + res = super(ImportXLSXWizard, self).view_init(fields_list) + res_model = self._context.get('active_model', False) + res_id = self._context.get('active_id', False) + if not res_model or not res_id: + return res + record = self.env[res_model].browse(res_id) + messages = [] + valid = True + # For all import, only allow import in draft state (for documents) + import_states = self._context.get('template_import_states', []) + if import_states: # states specified in context, test this + if 'state' in record and \ + record['state'] not in import_states: + messages.append( + _('Document must be in %s states') % import_states) + valid = False + else: # no specific state specified, test with draft + if 'state' in record and 'draft' not in record['state']: # not in + messages.append(_('Document must be in draft state')) + valid = False + # Context testing + if self._context.get('template_context', False): + template_context = self._context['template_context'] + for key, value in template_context.iteritems(): + if key not in record or \ + (record._fields[key].type == 'many2one' and + record[key].id or record[key]) != value: + valid = False + messages.append( + _('This import action is not usable ' + 'in this document context')) + break + if not valid: + raise ValidationError('\n'.join(messages)) + return res + + @api.model + def default_get(self, fields): + res_model = self._context.get('active_model', False) + res_id = self._context.get('active_id', False) + template_domain = self._context.get('template_domain', []) + templates = self.env['xlsx.template'].search(template_domain) + if not templates: + raise ValidationError(_('No template found')) + defaults = super(ImportXLSXWizard, self).default_get(fields) + for template in templates: + if not template.datas: + act = self.env.ref('excel_import_export.action_xlsx_template') + raise RedirectWarning( + _('File "%s" not found in template, %s.') % + (template.fname, template.name), + act.id, _('Set Templates')) + defaults['template_id'] = len(templates) == 1 and template.id or False + defaults['res_id'] = res_id + defaults['res_model'] = res_model + return defaults + + @api.multi + def action_import(self): + self.ensure_one() + Import = self.env['xlsx.import'] + res_ids = [] + if self.import_file: + record = Import.import_xlsx(self.import_file, self.template_id, + self.res_model, self.res_id) + res_ids = [record.id] + elif self.attachment_ids: + for attach in self.attachment_ids: + record = Import.import_xlsx(attach.datas, self.template_id) + res_ids.append(record.id) + else: + raise ValidationError(_('Please select Excel file to import')) + # If redirect_action is specified, do redirection + if self.template_id.redirect_action: + vals = self.template_id.redirect_action.read()[0] + vals['domain'] = [('id', 'in', res_ids)] + return vals + self.write({'state': 'get'}) + return { + 'type': 'ir.actions.act_window', + 'res_model': self._name, + 'view_mode': 'form', + 'view_type': 'form', + 'res_id': self.id, + 'views': [(False, 'form')], + 'target': 'new', + } diff --git a/excel_import_export/wizard/import_xlsx_wizard.xml b/excel_import_export/wizard/import_xlsx_wizard.xml new file mode 100644 index 000000000..b1abc2144 --- /dev/null +++ b/excel_import_export/wizard/import_xlsx_wizard.xml @@ -0,0 +1,44 @@ + + + + + + import.xlsx.wizard + import.xlsx.wizard + +
+ + + + + + + + + + + + + + + +

+ Import Successful! +

+
+
+
+
+
+ +
+
+ +
From 41317e6e77a8ac7c6de522df3aa350500d4ad5b0 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 14 Mar 2019 23:40:29 +0000 Subject: [PATCH 02/88] [UPD] README.rst --- excel_import_export/README.rst | 10 +++++----- excel_import_export/static/description/index.html | 6 +++--- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/excel_import_export/README.rst b/excel_import_export/README.rst index 48d59e6ef..ae02e1653 100644 --- a/excel_import_export/README.rst +++ b/excel_import_export/README.rst @@ -11,13 +11,13 @@ Excel Import/Export :target: http://www.gnu.org/licenses/agpl-3.0-standalone.html :alt: License: AGPL-3 .. |badge2| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github - :target: https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export + :target: https://github.com/OCA/server-tools/tree/12.0/excel_import_export :alt: OCA/server-tools .. |badge3| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png - :target: https://translation.odoo-community.org/projects/server-tools-12-add-excel_import_export/server-tools-12-add-excel_import_export-excel_import_export + :target: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export :alt: Translate me on Weblate .. |badge4| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png - :target: https://runbot.odoo-community.org/runbot/149/12-add-excel_import_export + :target: https://runbot.odoo-community.org/runbot/149/12.0 :alt: Try me on Runbot |badge1| |badge2| |badge3| |badge4| @@ -110,7 +110,7 @@ Bug Tracker Bugs are tracked on `GitHub 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 `_. +`feedback `_. Do not contact contributors directly about support or help with technical issues. @@ -148,6 +148,6 @@ Current `maintainer `__: |maintainer-kittiu| -This module is part of the `OCA/server-tools `_ project on GitHub. +This module is part of the `OCA/server-tools `_ project on GitHub. You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html index 4a8b0b106..ed10f9556 100644 --- a/excel_import_export/static/description/index.html +++ b/excel_import_export/static/description/index.html @@ -367,7 +367,7 @@ ul.auto-toc { !! This file is generated by oca-gen-addon-readme !! !! changes will be overwritten. !! !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> -

License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

+

License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

The module provide pre-built functions and wizards for developer to build excel import / export / report with ease.

Without having to code to create excel file, developer do,

    @@ -461,7 +461,7 @@ ul.auto-toc {

    Bugs are tracked on GitHub 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.

    +feedback.

    Do not contact contributors directly about support or help with technical issues.

    @@ -487,7 +487,7 @@ mission is to support the collaborative development of Odoo features and promote its widespread use.

    Current maintainer:

    kittiu

    -

    This module is part of the OCA/server-tools project on GitHub.

    +

    This module is part of the OCA/server-tools project on GitHub.

    You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

    From 7010b152580a385ffe7c147bab003b25b646fd29 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Fri, 15 Mar 2019 00:10:21 +0000 Subject: [PATCH 03/88] [UPD] Update excel_import_export.pot --- .../i18n/excel_import_export.pot | 965 ++++++++++++++++++ 1 file changed, 965 insertions(+) create mode 100644 excel_import_export/i18n/excel_import_export.pot diff --git a/excel_import_export/i18n/excel_import_export.pot b/excel_import_export/i18n/excel_import_export.pot new file mode 100644 index 000000000..2623c200e --- /dev/null +++ b/excel_import_export/i18n/excel_import_export.pot @@ -0,0 +1,965 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * excel_import_export +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 12.0\n" +"Report-Msgid-Bugs-To: \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: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "${object.post_import_do_something()}" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/common.py:170 +#: code:addons/excel_import_export/models/common.py:180 +#, python-format +msgid "'%s' sheet not found" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_export_xlsx_wizard__state +#: model:ir.model.fields,help:excel_import_export.field_import_xlsx_wizard__state +#: model:ir.model.fields,help:excel_import_export.field_xlsx_report__state +msgid "* Choose: wizard show in user selection mode\n" +"* Get: wizard show results from user action" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Cell: Location of data in excel sheet (e.g., A1, B1, ...)" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Continue: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Field Cond.: Python code in ${...} to manipulate field value, e.g., if field = product_id, value will represent product object, e.g., ${value and value.uom_id.name or \"\"}" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Field Cond.: Python code in ${...} value will represent data from excel cell, e.g., if A1 = 'ABC', value will represent 'ABC', e.g., ${value == \"ABC\" and \"X\" or \"Y\"} thus can change from cell value to other value for import." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Field: Field of the record to be imported to, e.g., product_id" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Field: Field of the record, e.g., product_id.uom_id.name. They are orm compliant." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "No Delete: By default, all one2many lines will be deleted before import. Select this, to avoid deletion" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Note:" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == \"ABC\" and #{font=bold;fill=red} or None?" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Style: Default style in #{...} that apply to each cell, e.g., #{align=left;style=text}. See module's style.py for available styles." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Sum: Add sum value on last row, @{sum}" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "\n" +"{\n" +" '__EXPORT__': {\n" +" 'sale_order': { # sheet can be name (string) or index (integer)\n" +" '_HEAD_': {\n" +" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;style=text}',\n" +" 'B3': 'name${value or \"\"}#{align=left;style=text}',\n" +" },\n" +" 'line_ids': { # prefix with _CONT_ to continue rows from previous row field\n" +" 'A6': 'product_id.display_name${value or \"\"}#{style=text}',\n" +" 'C6': 'product_uom_qty${value or 0}#{style=number}',\n" +" 'E6': 'price_unit${value or 0}#{style=number}',\n" +" 'G6': 'price_subtotal${value or 0}#{style=number}',\n" +" },\n" +" },\n" +" },\n" +" '__IMPORT__': {\n" +" 'sale_order': { # sheet can be name (string) or index (integer)\n" +" 'order_line': { # prefix with _NODEL_ to not delete rows before import\n" +" 'A6': 'product_id',\n" +" 'C6': 'product_uom_qty',\n" +" 'E6': 'price_unit${value > 0 and value or 0}',\n" +" },\n" +" },\n" +" },\n" +" '__POST_IMPORT__': '${object.post_import_do_something()}',\n" +"}\n" +"\n" +" " +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "date, datetime, time: some useful python classes" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "model: active model, e.g., self.env['my.model']" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "object: record object or line object depends on Row Field" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "value: value from Cell" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "value: value from Field" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Add data column" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Add header section" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Add row section" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Add sheet section" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__choose_template +msgid "Allow Choose Template" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_styles +msgid "Available styles for excel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template_import__no_delete +msgid "By default, all rows will be deleted before import.\n" +"Select No Delete, otherwise" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__csv_delimiter +msgid "CSV Delimiter" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__csv_extension +msgid "CSV File Extension" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__csv_quote +msgid "CSV Quoting" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__post_import_hook +msgid "Call a function after successful import, i.e.,\n" +"${object.post_import_do_something()}" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Cancel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__excel_cell +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__excel_cell +msgid "Cell" +msgstr "" + +#. module: excel_import_export +#: selection:export.xlsx.wizard,state:0 +#: selection:import.xlsx.wizard,state:0 +#: selection:xlsx.report,state:0 +msgid "Choose" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Choose Template:" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.actions.act_window,help:excel_import_export.action_xlsx_template +msgid "Click to create a XLSX Template Object." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Close" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +msgid "Complete Prepare File (.xlsx)" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Complete Prepare Report (.xlsx)" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__is_cont +msgid "Continue" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template_export__is_cont +msgid "Continue data rows after last data row" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__to_csv +msgid "Convert to CSV?" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__create_uid +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__create_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__create_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__create_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__create_uid +msgid "Created by" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__create_date +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__create_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__create_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__create_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__create_date +msgid "Created on" +msgstr "" + +#. module: excel_import_export +#: selection:xlsx.template.export,section_type:0 +#: selection:xlsx.template.import,section_type:0 +msgid "Data" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__style +msgid "Default Style" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__description +msgid "Description" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_template_export +msgid "Detailed of how excel data will be exported" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_template_import +msgid "Detailed of how excel data will be imported" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__display_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__display_name +msgid "Display Name" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:74 +#, python-format +msgid "Document must be in %s states" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:78 +#, python-format +msgid "Document must be in draft state" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:89 +#, python-format +msgid "Error deleting data\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:141 +#, python-format +msgid "Error filling data into Excel sheets\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:219 +#, python-format +msgid "Error importing data\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_export +msgid "Excel Export AbstractModel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_import +msgid "Excel Import AbstractModel" +msgstr "" + +#. module: excel_import_export +#: model:ir.ui.menu,name:excel_import_export.menu_excel_import_export +msgid "Excel Import/Export" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Excel Report" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_report +msgid "Excel Report AbstractModel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_xlsx_template +msgid "Excel template file and instruction" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Execute Report" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__export_ids +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Export" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Export Instruction is how to write data from an active data record to specified cells in excel sheet.\n" +" For example, an active record can be a sale order that user want to export.\n" +" The record itself will be mapped to the header part of excel sheet. The record can contain multiple one2many fields, which will be written as data lines.\n" +" You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields)." +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__field_name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__field_name +msgid "Field" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__field_cond +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__field_cond +msgid "Field Cond." +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__data +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__data +msgid "File" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:109 +#, python-format +msgid "File \"%s\" not found in template, %s." +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__datas +msgid "File Content" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__name +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__fname +msgid "File Name" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Following are more explaination on each column:" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Following show very simple example of the dictionary construct.\n" +" Normally, this will be within templates.xml file within addons." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "For code block ${...} and #?...?, following object are available," +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "For code block ${...}, following object are available," +msgstr "" + +#. module: excel_import_export +#: selection:export.xlsx.wizard,state:0 +#: selection:import.xlsx.wizard,state:0 +#: selection:xlsx.report,state:0 +msgid "Get" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +msgid "Get Import Template" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__gname +msgid "Group Name" +msgstr "" + +#. module: excel_import_export +#: selection:xlsx.template.export,section_type:0 +#: selection:xlsx.template.import,section_type:0 +msgid "Head" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Help with Export Instruction" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Help with Import Instruction" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +msgid "Here is the exported file:" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "Here is the report file:" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__id +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__id +msgid "ID" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template_export__row_field +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template_import__row_field +msgid "If section type is row, this field is required" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:137 +#, python-format +msgid "IllegalCharacterError\n" +"Some exporting data contain special character\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__import_ids +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Import" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__import_file +msgid "Import File (*.xlsx)" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +msgid "Import File Template" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__attachment_ids +msgid "Import File(s) (*.xlsx)" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Import Instruction is how to get data from excel sheet and write them to an active record.\n" +" For example, user create a sales order document, and want to import order lines from excel.\n" +" In reverse direction to exporting, data from excel's cells will be mapped to record fields during import.\n" +" Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +msgid "Import Successful!" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2)." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id)." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6)." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty)" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Input Instruction (Dict.)" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__instruction +msgid "Instruction" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__input_instruction +msgid "Instruction (Input)" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__instruction +msgid "Instruction on how to import/export, prepared by system." +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:67 +#, python-format +msgid "Invalid declaration, %s has no valid field type" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:217 +#, python-format +msgid "Invalid file style, only .xls or .xlsx file allowed" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/common.py:108 +#, python-format +msgid "Invalid style type %s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/common.py:111 +#, python-format +msgid "Invalid value %s for style type %s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:134 +#, python-format +msgid "Key Error\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export____last_update +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import____last_update +msgid "Last Modified on" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__write_uid +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__write_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__write_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__write_uid +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__write_uid +msgid "Last Updated by" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__write_date +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__write_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__write_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__write_date +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__write_date +msgid "Last Updated on" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__gname +msgid "Multiple template of same model, can belong to same group,\n" +"result in multiple template selection" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__no_delete +msgid "No Delete" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:249 +#, python-format +msgid "No data_dict['__IMPORT__'] in template %s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_report.py:50 +#: code:addons/excel_import_export/wizard/export_xlsx_wizard.py:60 +#, python-format +msgid "No file in %s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_report.py:46 +#: code:addons/excel_import_export/wizard/export_xlsx_wizard.py:56 +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:103 +#, python-format +msgid "No template found" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:125 +#, python-format +msgid "Not enough worksheets" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__redirect_action +msgid "Optional action, redirection after finish import operation" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__csv_extension +msgid "Optional for CSV, default is .csv" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__csv_delimiter +msgid "Optional for CSV, default is comma." +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__csv_quote +msgid "Optional for CSV, default is full quoting." +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:131 +#, python-format +msgid "Please select Excel file to import" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/common.py:158 +#: code:addons/excel_import_export/models/common.py:255 +#, python-format +msgid "Position %s is not valid" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__post_import_hook +msgid "Post Import Function Hook" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Post Import Hook" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:232 +#, python-format +msgid "Post import operation error\n" +"%s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:55 +#, python-format +msgid "Records in %s exceed max records allowed" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__res_id +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__res_id +msgid "Resource ID" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__res_model +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__res_model +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__res_model +msgid "Resource Model" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__redirect_action +msgid "Return Action" +msgstr "" + +#. module: excel_import_export +#: selection:xlsx.template.export,section_type:0 +#: selection:xlsx.template.import,section_type:0 +msgid "Row" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__row_field +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__row_field +msgid "Row Field" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__datas +msgid "Sample" +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "Sample Input Instruction as Dictionary" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__section_type +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__section_type +msgid "Section Type" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__sequence +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__sequence +msgid "Sequence" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:111 +#, python-format +msgid "Set Templates" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__sheet +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__sheet +#: selection:xlsx.template.export,section_type:0 +#: selection:xlsx.template.import,section_type:0 +msgid "Sheet" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:129 +#: code:addons/excel_import_export/models/xlsx_import.py:147 +#, python-format +msgid "Sheet %s not found" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__show_instruction +msgid "Show Output" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__state +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__state +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__state +msgid "State" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__style_cond +msgid "Style w/Cond." +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__is_sum +msgid "Sum" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__template_id +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__template_id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__template_id +msgid "Template" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_import_xlsx_wizard__fname +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__name +msgid "Template Name" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/common.py:242 +#, python-format +msgid "Template with CSV Quoting = False, data must not contain the same char as delimiter -> \"%s\"" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_export.py:229 +#: code:addons/excel_import_export/models/xlsx_import.py:244 +#, python-format +msgid "Template's model mismatch" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__res_model +msgid "The database object this attachment will be attached to." +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_template.py:110 +#, python-format +msgid "The selected redirect action is not for model %s" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:89 +#, python-format +msgid "This import action is not usable in this document context" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__show_instruction +msgid "This is the computed instruction based on tab Import/Export,\n" +"to be used by xlsx import/export engine" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_xlsx_template__input_instruction +msgid "This is used to construct instruction in tab Import/Export" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_export_xlsx_wizard +msgid "Wizard for exporting excel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model,name:excel_import_export.model_import_xlsx_wizard +msgid "Wizard for importing excel" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__template_id +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__template_id +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_tree +msgid "XLSX Template" +msgstr "" + +#. module: excel_import_export +#: model:ir.actions.act_window,name:excel_import_export.action_xlsx_template +#: model:ir.ui.menu,name:excel_import_export.menu_xlsx_template +msgid "XLSX Templates" +msgstr "" + +#. module: excel_import_export +#: model:ir.model.fields,help:excel_import_export.field_import_xlsx_wizard__attachment_ids +msgid "You can select multiple files to import." +msgstr "" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.export_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard +#: model_terms:ir.ui.view,arch_db:excel_import_export.xlsx_report_view +msgid "or" +msgstr "" + From 6bce70e2af074f91aecc31eaf4c045071440d677 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Tue, 2 Apr 2019 10:36:13 +0700 Subject: [PATCH 04/88] [12.0][FIX] excel_import_export wrong use of dict.iteritems() for python3 --- excel_import_export/models/xlsx_export.py | 2 +- excel_import_export/wizard/import_xlsx_wizard.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index c7db3f928..78b9eb41f 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -244,7 +244,7 @@ class XLSXExport(models.AbstractModel): f.write(decoded_data) f.seek(0) f.close() - # Workbook created, temp fie removed + # Workbook created, temp file removed wb = load_workbook(ftemp) os.remove(ftemp) # Start working with workbook diff --git a/excel_import_export/wizard/import_xlsx_wizard.py b/excel_import_export/wizard/import_xlsx_wizard.py index 750dc17e1..49be5c07a 100644 --- a/excel_import_export/wizard/import_xlsx_wizard.py +++ b/excel_import_export/wizard/import_xlsx_wizard.py @@ -80,7 +80,7 @@ class ImportXLSXWizard(models.TransientModel): # Context testing if self._context.get('template_context', False): template_context = self._context['template_context'] - for key, value in template_context.iteritems(): + for key, value in template_context.items(): if key not in record or \ (record._fields[key].type == 'many2one' and record[key].id or record[key]) != value: From 5efe8fb03ca908d2914645dbb40b08a24b04867c Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Wed, 3 Apr 2019 03:22:21 +0000 Subject: [PATCH 05/88] [ADD] icon.png --- excel_import_export/static/description/icon.png | Bin 0 -> 9455 bytes 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 excel_import_export/static/description/icon.png diff --git a/excel_import_export/static/description/icon.png b/excel_import_export/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..3a0328b516c4980e8e44cdb63fd945757ddd132d GIT binary patch literal 9455 zcmW++2RxMjAAjx~&dlBk9S+%}OXg)AGE&Cb*&}d0jUxM@u(PQx^-s)697TX`ehR4?GS^qbkof1cslKgkU)h65qZ9Oc=ml_0temigYLJfnz{IDzUf>bGs4N!v3=Z3jMq&A#7%rM5eQ#dc?k~! zVpnB`o+K7|Al`Q_U;eD$B zfJtP*jH`siUq~{KE)`jP2|#TUEFGRryE2`i0**z#*^6~AI|YzIWy$Cu#CSLW3q=GA z6`?GZymC;dCPk~rBS%eCb`5OLr;RUZ;D`}um=H)BfVIq%7VhiMr)_#G0N#zrNH|__ zc+blN2UAB0=617@>_u;MPHN;P;N#YoE=)R#i$k_`UAA>WWCcEVMh~L_ zj--gtp&|K1#58Yz*AHCTMziU1Jzt_jG0I@qAOHsk$2}yTmVkBp_eHuY$A9)>P6o~I z%aQ?!(GqeQ-Y+b0I(m9pwgi(IIZZzsbMv+9w{PFtd_<_(LA~0H(xz{=FhLB@(1&qHA5EJw1>>=%q2f&^X>IQ{!GJ4e9U z&KlB)z(84HmNgm2hg2C0>WM{E(DdPr+EeU_N@57;PC2&DmGFW_9kP&%?X4}+xWi)( z;)z%wI5>D4a*5XwD)P--sPkoY(a~WBw;E~AW`Yue4kFa^LM3X`8x|}ZUeMnqr}>kH zG%WWW>3ml$Yez?i%)2pbKPI7?5o?hydokgQyZsNEr{a|mLdt;X2TX(#B1j35xPnPW z*bMSSOauW>o;*=kO8ojw91VX!qoOQb)zHJ!odWB}d+*K?#sY_jqPdg{Sm2HdYzdEx zOGVPhVRTGPtv0o}RfVP;Nd(|CB)I;*t&QO8h zFfekr30S!-LHmV_Su-W+rEwYXJ^;6&3|L$mMC8*bQptyOo9;>Qb9Q9`ySe3%V$A*9 zeKEe+b0{#KWGp$F+tga)0RtI)nhMa-K@JS}2krK~n8vJ=Ngm?R!9G<~RyuU0d?nz# z-5EK$o(!F?hmX*2Yt6+coY`6jGbb7tF#6nHA zuKk=GGJ;ZwON1iAfG$E#Y7MnZVmrY|j0eVI(DN_MNFJmyZ|;w4tf@=CCDZ#5N_0K= z$;R~bbk?}TpfDjfB&aiQ$VA}s?P}xPERJG{kxk5~R`iRS(SK5d+Xs9swCozZISbnS zk!)I0>t=A<-^z(cmSFz3=jZ23u13X><0b)P)^1T_))Kr`e!-pb#q&J*Q`p+B6la%C zuVl&0duN<;uOsB3%T9Fp8t{ED108<+W(nOZd?gDnfNBC3>M8WE61$So|P zVvqH0SNtDTcsUdzaMDpT=Ty0pDHHNL@Z0w$Y`XO z2M-_r1S+GaH%pz#Uy0*w$Vdl=X=rQXEzO}d6J^R6zjM1u&c9vYLvLp?W7w(?np9x1 zE_0JSAJCPB%i7p*Wvg)pn5T`8k3-uR?*NT|J`eS#_#54p>!p(mLDvmc-3o0mX*mp_ zN*AeS<>#^-{S%W<*mz^!X$w_2dHWpcJ6^j64qFBft-o}o_Vx80o0>}Du;>kLts;$8 zC`7q$QI(dKYG`Wa8#wl@V4jVWBRGQ@1dr-hstpQL)Tl+aqVpGpbSfN>5i&QMXfiZ> zaA?T1VGe?rpQ@;+pkrVdd{klI&jVS@I5_iz!=UMpTsa~mBga?1r}aRBm1WS;TT*s0f0lY=JBl66Upy)-k4J}lh=P^8(SXk~0xW=T9v*B|gzIhN z>qsO7dFd~mgxAy4V?&)=5ieYq?zi?ZEoj)&2o)RLy=@hbCRcfT5jigwtQGE{L*8<@Yd{zg;CsL5mvzfDY}P-wos_6PfprFVaeqNE%h zKZhLtcQld;ZD+>=nqN~>GvROfueSzJD&BE*}XfU|H&(FssBqY=hPCt`d zH?@s2>I(|;fcW&YM6#V#!kUIP8$Nkdh0A(bEVj``-AAyYgwY~jB zT|I7Bf@%;7aL7Wf4dZ%VqF$eiaC38OV6oy3Z#TER2G+fOCd9Iaoy6aLYbPTN{XRPz z;U!V|vBf%H!}52L2gH_+j;`bTcQRXB+y9onc^wLm5wi3-Be}U>k_u>2Eg$=k!(l@I zcCg+flakT2Nej3i0yn+g+}%NYb?ta;R?(g5SnwsQ49U8Wng8d|{B+lyRcEDvR3+`O{zfmrmvFrL6acVP%yG98X zo&+VBg@px@i)%o?dG(`T;n*$S5*rnyiR#=wW}}GsAcfyQpE|>a{=$Hjg=-*_K;UtD z#z-)AXwSRY?OPefw^iI+ z)AXz#PfEjlwTes|_{sB?4(O@fg0AJ^g8gP}ex9Ucf*@_^J(s_5jJV}c)s$`Myn|Kd z$6>}#q^n{4vN@+Os$m7KV+`}c%4)4pv@06af4-x5#wj!KKb%caK{A&Y#Rfs z-po?Dcb1({W=6FKIUirH&(yg=*6aLCekcKwyfK^JN5{wcA3nhO(o}SK#!CINhI`-I z1)6&n7O&ZmyFMuNwvEic#IiOAwNkR=u5it{B9n2sAJV5pNhar=j5`*N!Na;c7g!l$ z3aYBqUkqqTJ=Re-;)s!EOeij=7SQZ3Hq}ZRds%IM*PtM$wV z@;rlc*NRK7i3y5BETSKuumEN`Xu_8GP1Ri=OKQ$@I^ko8>H6)4rjiG5{VBM>B|%`&&s^)jS|-_95&yc=GqjNo{zFkw%%HHhS~e=s zD#sfS+-?*t|J!+ozP6KvtOl!R)@@-z24}`9{QaVLD^9VCSR2b`b!KC#o;Ki<+wXB6 zx3&O0LOWcg4&rv4QG0)4yb}7BFSEg~=IR5#ZRj8kg}dS7_V&^%#Do==#`u zpy6{ox?jWuR(;pg+f@mT>#HGWHAJRRDDDv~@(IDw&R>9643kK#HN`!1vBJHnC+RM&yIh8{gG2q zA%e*U3|N0XSRa~oX-3EAneep)@{h2vvd3Xvy$7og(sayr@95+e6~Xvi1tUqnIxoIH zVWo*OwYElb#uyW{Imam6f2rGbjR!Y3`#gPqkv57dB6K^wRGxc9B(t|aYDGS=m$&S!NmCtrMMaUg(c zc2qC=2Z`EEFMW-me5B)24AqF*bV5Dr-M5ig(l-WPS%CgaPzs6p_gnCIvTJ=Y<6!gT zVt@AfYCzjjsMEGi=rDQHo0yc;HqoRNnNFeWZgcm?f;cp(6CNylj36DoL(?TS7eU#+ z7&mfr#y))+CJOXQKUMZ7QIdS9@#-}7y2K1{8)cCt0~-X0O!O?Qx#E4Og+;A2SjalQ zs7r?qn0H044=sDN$SRG$arw~n=+T_DNdSrarmu)V6@|?1-ZB#hRn`uilTGPJ@fqEy zGt(f0B+^JDP&f=r{#Y_wi#AVDf-y!RIXU^0jXsFpf>=Ji*TeqSY!H~AMbJdCGLhC) zn7Rx+sXw6uYj;WRYrLd^5IZq@6JI1C^YkgnedZEYy<&4(z%Q$5yv#Boo{AH8n$a zhb4Y3PWdr269&?V%uI$xMcUrMzl=;w<_nm*qr=c3Rl@i5wWB;e-`t7D&c-mcQl7x! zZWB`UGcw=Y2=}~wzrfLx=uet<;m3~=8I~ZRuzvMQUQdr+yTV|ATf1Uuomr__nDf=X zZ3WYJtHp_ri(}SQAPjv+Y+0=fH4krOP@S&=zZ-t1jW1o@}z;xk8 z(Nz1co&El^HK^NrhVHa-_;&88vTU>_J33=%{if;BEY*J#1n59=07jrGQ#IP>@u#3A z;!q+E1Rj3ZJ+!4bq9F8PXJ@yMgZL;>&gYA0%_Kbi8?S=XGM~dnQZQ!yBSgcZhY96H zrWnU;k)qy`rX&&xlDyA%(a1Hhi5CWkmg(`Gb%m(HKi-7Z!LKGRP_B8@`7&hdDy5n= z`OIxqxiVfX@OX1p(mQu>0Ai*v_cTMiw4qRt3~NBvr9oBy0)r>w3p~V0SCm=An6@3n)>@z!|o-$HvDK z|3D2ZMJkLE5loMKl6R^ez@Zz%S$&mbeoqH5`Bb){Ei21q&VP)hWS2tjShfFtGE+$z zzCR$P#uktu+#!w)cX!lWN1XU%K-r=s{|j?)Akf@q#3b#{6cZCuJ~gCxuMXRmI$nGtnH+-h z+GEi!*X=AP<|fG`1>MBdTb?28JYc=fGvAi2I<$B(rs$;eoJCyR6_bc~p!XR@O-+sD z=eH`-ye})I5ic1eL~TDmtfJ|8`0VJ*Yr=hNCd)G1p2MMz4C3^Mj?7;!w|Ly%JqmuW zlIEW^Ft%z?*|fpXda>Jr^1noFZEwFgVV%|*XhH@acv8rdGxeEX{M$(vG{Zw+x(ei@ zmfXb22}8-?Fi`vo-YVrTH*C?a8%M=Hv9MqVH7H^J$KsD?>!SFZ;ZsvnHr_gn=7acz z#W?0eCdVhVMWN12VV^$>WlQ?f;P^{(&pYTops|btm6aj>_Uz+hqpGwB)vWp0Cf5y< zft8-je~nn?W11plq}N)4A{l8I7$!ks_x$PXW-2XaRFswX_BnF{R#6YIwMhAgd5F9X zGmwdadS6(a^fjHtXg8=l?Rc0Sm%hk6E9!5cLVloEy4eh(=FwgP`)~I^5~pBEWo+F6 zSf2ncyMurJN91#cJTy_u8Y}@%!bq1RkGC~-bV@SXRd4F{R-*V`bS+6;W5vZ(&+I<9$;-V|eNfLa5n-6% z2(}&uGRF;p92eS*sE*oR$@pexaqr*meB)VhmIg@h{uzkk$9~qh#cHhw#>O%)b@+(| z^IQgqzuj~Sk(J;swEM-3TrJAPCq9k^^^`q{IItKBRXYe}e0Tdr=Huf7da3$l4PdpwWDop%^}n;dD#K4s#DYA8SHZ z&1!riV4W4R7R#C))JH1~axJ)RYnM$$lIR%6fIVA@zV{XVyx}C+a-Dt8Y9M)^KU0+H zR4IUb2CJ{Hg>CuaXtD50jB(_Tcx=Z$^WYu2u5kubqmwp%drJ6 z?Fo40g!Qd<-l=TQxqHEOuPX0;^z7iX?Ke^a%XT<13TA^5`4Xcw6D@Ur&VT&CUe0d} z1GjOVF1^L@>O)l@?bD~$wzgf(nxX1OGD8fEV?TdJcZc2KoUe|oP1#=$$7ee|xbY)A zDZq+cuTpc(fFdj^=!;{k03C69lMQ(|>uhRfRu%+!k&YOi-3|1QKB z z?n?eq1XP>p-IM$Z^C;2L3itnbJZAip*Zo0aw2bs8@(s^~*8T9go!%dHcAz2lM;`yp zD=7&xjFV$S&5uDaiScyD?B-i1ze`+CoRtz`Wn+Zl&#s4&}MO{@N!ufrzjG$B79)Y2d3tBk&)TxUTw@QS0TEL_?njX|@vq?Uz(nBFK5Pq7*xj#u*R&i|?7+6# z+|r_n#SW&LXhtheZdah{ZVoqwyT{D>MC3nkFF#N)xLi{p7J1jXlmVeb;cP5?e(=f# zuT7fvjSbjS781v?7{)-X3*?>tq?)Yd)~|1{BDS(pqC zC}~H#WXlkUW*H5CDOo<)#x7%RY)A;ShGhI5s*#cRDA8YgqG(HeKDx+#(ZQ?386dv! zlXCO)w91~Vw4AmOcATuV653fa9R$fyK8ul%rG z-wfS zihugoZyr38Im?Zuh6@RcF~t1anQu7>#lPpb#}4cOA!EM11`%f*07RqOVkmX{p~KJ9 z^zP;K#|)$`^Rb{rnHGH{~>1(fawV0*Z#)}M`m8-?ZJV<+e}s9wE# z)l&az?w^5{)`S(%MRzxdNqrs1n*-=jS^_jqE*5XDrA0+VE`5^*p3CuM<&dZEeCjoz zR;uu_H9ZPZV|fQq`Cyw4nscrVwi!fE6ciMmX$!_hN7uF;jjKG)d2@aC4ropY)8etW=xJvni)8eHi`H$%#zn^WJ5NLc-rqk|u&&4Z6fD_m&JfSI1Bvb?b<*n&sfl0^t z=HnmRl`XrFvMKB%9}>PaA`m-fK6a0(8=qPkWS5bb4=v?XcWi&hRY?O5HdulRi4?fN zlsJ*N-0Qw+Yic@s0(2uy%F@ib;GjXt01Fmx5XbRo6+n|pP(&nodMoap^z{~q ziEeaUT@Mxe3vJSfI6?uLND(CNr=#^W<1b}jzW58bIfyWTDle$mmS(|x-0|2UlX+9k zQ^EX7Nw}?EzVoBfT(-LT|=9N@^hcn-_p&sqG z&*oVs2JSU+N4ZD`FhCAWaS;>|wH2G*Id|?pa#@>tyxX`+4HyIArWDvVrX)2WAOQff z0qyHu&-S@i^MS-+j--!pr4fPBj~_8({~e1bfcl0wI1kaoN>mJL6KUPQm5N7lB(ui1 zE-o%kq)&djzWJ}ob<-GfDlkB;F31j-VHKvQUGQ3sp`CwyGJk_i!y^sD0fqC@$9|jO zOqN!r!8-p==F@ZVP=U$qSpY(gQ0)59P1&t@y?5rvg<}E+GB}26NYPp4f2YFQrQtot5mn3wu_qprZ=>Ig-$ zbW26Ws~IgY>}^5w`vTB(G`PTZaDiGBo5o(tp)qli|NeV( z@H_=R8V39rt5J5YB2Ky?4eJJ#b`_iBe2ot~6%7mLt5t8Vwi^Jy7|jWXqa3amOIoRb zOr}WVFP--DsS`1WpN%~)t3R!arKF^Q$e12KEqU36AWwnCBICpH4XCsfnyrHr>$I$4 z!DpKX$OKLWarN7nv@!uIA+~RNO)l$$w}p(;b>mx8pwYvu;dD_unryX_NhT8*Tj>BTrTTL&!?O+%Rv;b?B??gSzdp?6Uug9{ zd@V08Z$BdI?fpoCS$)t4mg4rT8Q_I}h`0d-vYZ^|dOB*Q^S|xqTV*vIg?@fVFSmMpaw0qtTRbx} z({Pg?#{2`sc9)M5N$*N|4;^t$+QP?#mov zGVC@I*lBVrOU-%2y!7%)fAKjpEFsgQc4{amtiHb95KQEwvf<(3T<9-Zm$xIew#P22 zc2Ix|App^>v6(3L_MCU0d3W##AB0M~3D00EWoKZqsJYT(#@w$Y_H7G22M~ApVFTRHMI_3be)Lkn#0F*V8Pq zc}`Cjy$bE;FJ6H7p=0y#R>`}-m4(0F>%@P|?7fx{=R^uFdISRnZ2W_xQhD{YuR3t< z{6yxu=4~JkeA;|(J6_nv#>Nvs&FuLA&PW^he@t(UwFFE8)|a!R{`E`K`i^ZnyE4$k z;(749Ix|oi$c3QbEJ3b~D_kQsPz~fIUKym($a_7dJ?o+40*OLl^{=&oq$<#Q(yyrp z{J-FAniyAw9tPbe&IhQ|a`DqFTVQGQ&Gq3!C2==4x{6EJwiPZ8zub-iXoUtkJiG{} zPaR&}_fn8_z~(=;5lD-aPWD3z8PZS@AaUiomF!G8I}Mf>e~0g#BelA-5#`cj;O5>N Xviia!U7SGha1wx#SCgwmn*{w2TRX*I literal 0 HcmV?d00001 From e9b1e57f762059c982a105a2a0ddd3e498c92069 Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Mon, 29 Jul 2019 03:39:11 +0000 Subject: [PATCH 06/88] [UPD] README.rst --- excel_import_export/static/description/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html index ed10f9556..56f33669e 100644 --- a/excel_import_export/static/description/index.html +++ b/excel_import_export/static/description/index.html @@ -3,7 +3,7 @@ - + Excel Import/Export -
    -

    Excel Import/Export

    +
    +

    Excel Import/Export/Report

    -

    License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

    +

    Beta License: AGPL-3 OCA/server-tools Translate me on Weblate Try me on Runbot

    The module provide pre-built functions and wizards for developer to build excel import / export / report with ease.

    Without having to code to create excel file, developer do,

      From b4a13186057f8daabe2a85fd96aa268c1475e46b Mon Sep 17 00:00:00 2001 From: kittiu Date: Tue, 19 Nov 2019 13:10:42 +0700 Subject: [PATCH 25/88] [12.0][ENH] excel_import_export, faster export Previously export is slow on large number of rows. This fix ensure that sheet.insert_rows is called only once for all rows, instead of insert 1 row for every rows --- excel_import_export/models/xlsx_export.py | 4 +--- excel_import_export/readme/HISTORY.rst | 5 +++++ 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index 95749b869..fcc2512a5 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -200,9 +200,7 @@ class XLSXExport(models.AbstractModel): # Insert rows to preserve total line if not rows_inserted: rows_inserted = True - if row_count > 1: - for _x in range(row_count-1): - st.insert_rows(row+1) + st.insert_rows(row+1, amount=row_count-1) # -- for (row_val, style) in vals[field]: new_row = row + i diff --git a/excel_import_export/readme/HISTORY.rst b/excel_import_export/readme/HISTORY.rst index 58745034f..c6e2a27cf 100644 --- a/excel_import_export/readme/HISTORY.rst +++ b/excel_import_export/readme/HISTORY.rst @@ -1,3 +1,8 @@ +12.0.1.0.5 (2019-12-19) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Speed up export when dealing with many rows + 12.0.1.0.4 (2019-08-28) ~~~~~~~~~~~~~~~~~~~~~~~ From a528f611ea5a4ef4d2339aaaaa29d1bd79520f3d Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 21 Nov 2019 06:42:05 +0000 Subject: [PATCH 26/88] [UPD] Update excel_import_export.pot --- excel_import_export/i18n/excel_import_export.pot | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/excel_import_export/i18n/excel_import_export.pot b/excel_import_export/i18n/excel_import_export.pot index f6d2eaf9f..7c00bb8de 100644 --- a/excel_import_export/i18n/excel_import_export.pot +++ b/excel_import_export/i18n/excel_import_export.pot @@ -943,7 +943,7 @@ msgid "Template with CSV Quoting = False, data must not contain the same char as msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_export.py:230 +#: code:addons/excel_import_export/models/xlsx_export.py:228 #: code:addons/excel_import_export/models/xlsx_import.py:244 #, python-format msgid "Template's model mismatch" From cc9c121072f2b3c5e19a7645fe3404ef3446bfce Mon Sep 17 00:00:00 2001 From: OCA-git-bot Date: Thu, 21 Nov 2019 06:51:37 +0000 Subject: [PATCH 27/88] [UPD] README.rst --- excel_import_export/README.rst | 5 ++ .../static/description/index.html | 71 ++++++++++--------- 2 files changed, 44 insertions(+), 32 deletions(-) diff --git a/excel_import_export/README.rst b/excel_import_export/README.rst index a856a6a4a..bb2b29a1c 100644 --- a/excel_import_export/README.rst +++ b/excel_import_export/README.rst @@ -122,6 +122,11 @@ Known issues / Roadmap Changelog ========= +12.0.1.0.5 (2019-12-19) +~~~~~~~~~~~~~~~~~~~~~~~ + +* Speed up export when dealing with many rows + 12.0.1.0.4 (2019-08-28) ~~~~~~~~~~~~~~~~~~~~~~~ diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html index 17eebfbfb..e0b8816d6 100644 --- a/excel_import_export/static/description/index.html +++ b/excel_import_export/static/description/index.html @@ -379,34 +379,35 @@ ul.auto-toc {

      Table of contents

      -

      Installation

      +

      Installation

      To install this module, you need to install following python library, xlrd, xlwt, openpyxl.

      Then, simply install excel_import_export.

      For demo, install excel_import_export_demo.

      -

      Usage

      +

      Usage

      This module contain pre-defined function and wizards to make exporting, importing and reporting easy.

      At the heart of this module, there are 2 main methods

        @@ -462,47 +463,53 @@ Please see example in excel_import_export_demo/report_action, which shows,

      -

      Known issues / Roadmap

      +

      Known issues / Roadmap

      • Module extension e.g., excel_import_export_async, that add ability to execute as async process.
      • Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action.
      -

      Changelog

      +

      Changelog

      -

      12.0.1.0.4 (2019-08-28)

      +

      12.0.1.0.5 (2019-12-19)

      +
        +
      • Speed up export when dealing with many rows
      • +
      +
      +
      +

      12.0.1.0.4 (2019-08-28)

      • Fix style sum in footer
      -
      -

      12.0.1.0.3 (2019-08-09)

      +
      +

      12.0.1.0.3 (2019-08-09)

      • Add report action for report_type = ‘excel’
      -
      -

      12.0.1.0.2 (2019-08-07)

      +
      +

      12.0.1.0.2 (2019-08-07)

      • Small fix, to ensure that system parameter ‘path_temp_file’ (ir.config_parameter) is readable
      -
      -

      12.0.1.0.1 (2019-06-24)

      +
      +

      12.0.1.0.1 (2019-06-24)

      -
      -

      Bug Tracker

      +

      Bug Tracker

      Bugs are tracked on GitHub 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 @@ -510,22 +517,22 @@ If you spotted it first, help us smashing it by providing a detailed and welcome

      Do not contact contributors directly about support or help with technical issues.

      -

      Credits

      +

      Credits

      -

      Authors

      +

      Authors

      • Ecosoft
      -

      Maintainers

      +

      Maintainers

      This module is maintained by the OCA.

      Odoo Community Association

      OCA, or the Odoo Community Association, is a nonprofit organization whose From 04a178b40b92ca4eace4845c6e12b4bef7002742 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Thu, 21 Nov 2019 06:51:55 +0000 Subject: [PATCH 28/88] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-12.0/server-tools-12.0-excel_import_export Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export/ --- excel_import_export/i18n/zh_CN.po | 267 ++++++++++++++++++++---------- 1 file changed, 180 insertions(+), 87 deletions(-) diff --git a/excel_import_export/i18n/zh_CN.po b/excel_import_export/i18n/zh_CN.po index f8f0aa3fb..523fba3b8 100644 --- a/excel_import_export/i18n/zh_CN.po +++ b/excel_import_export/i18n/zh_CN.po @@ -1,6 +1,6 @@ # Translation of Odoo Server. # This file contains the translation of the following modules: -# * excel_import_export +# * excel_import_export # msgid "" msgstr "" @@ -32,7 +32,8 @@ msgstr "'%s' 找不到工作表" #: model:ir.model.fields,help:excel_import_export.field_export_xlsx_wizard__state #: model:ir.model.fields,help:excel_import_export.field_import_xlsx_wizard__state #: model:ir.model.fields,help:excel_import_export.field_xlsx_report__state -msgid "* Choose: wizard show in user selection mode\n" +msgid "" +"* Choose: wizard show in user selection mode\n" "* Get: wizard show results from user action" msgstr "" "* 选择:用户选择模式下的向导显示\n" @@ -45,24 +46,35 @@ msgstr "单元格:Excel工作表中的数据位置(例如:A1,B1,... #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Continue: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field" -msgstr "继续: 如果未选中,则使用指定的第一行单元格开始滚动。 如果选中,则从之前的one2many字段继续" +msgid "" +"Continue: If not selected, start rolling with specified first row " +"cells. If selected, continue from previous one2many field" +msgstr "" +"继续: 如果未选中,则使用指定的第一行单元格开始滚动。 如果选中,则从之" +"前的one2many字段继续" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Field Cond.: Python code in ${...} to manipulate field value, e.g., if field = product_id, value will represent product object, e.g., ${value and value.uom_id.name or \"\"}" +msgid "" +"Field Cond.: Python code in ${...} to manipulate field " +"value, e.g., if field = product_id, value will represent " +"product object, e.g., ${value and value.uom_id.name or \"\"}" msgstr "" -"字段条件: Python代码${...} 操纵字段值, 例如, if field = product_id, " -"value 将代表产品对象, 例如, ${value and value.uom_id.name or \"\"" -"}" +"字段条件: Python代码${...} 操纵字段值, 例如, if field = " +"product_id, value 将代表产品对象, 例如, ${value and value." +"uom_id.name or \"\"}" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Field Cond.: Python code in ${...} value will represent data from excel cell, e.g., if A1 = 'ABC', value will represent 'ABC', e.g., ${value == \"ABC\" and \"X\" or \"Y\"} thus can change from cell value to other value for import." +msgid "" +"Field Cond.: Python code in ${...} value will represent " +"data from excel cell, e.g., if A1 = 'ABC', value will represent " +"'ABC', e.g., ${value == \"ABC\" and \"X\" or \"Y\"} thus can " +"change from cell value to other value for import." msgstr "" -"字段条件: Python代码 ${...} 值将代表来自excel 单元格的数据, 例如, if A1 = " -"'ABC', value 代表'ABC',例如, ${value == \"ABC\" and \"X\" or " -"\"Y\"} 因此可以从单元格值更改为其他值以进行导入." +"字段条件: Python代码 ${...} 值将代表来自excel 单元格的数" +"据, 例如, if A1 = 'ABC', value 代表'ABC',例如, ${value == " +"\"ABC\" and \"X\" or \"Y\"} 因此可以从单元格值更改为其他值以进行导入." #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -71,13 +83,18 @@ msgstr "字段: 要导入的记录的字段,例如product_id" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Field: Field of the record, e.g., product_id.uom_id.name. They are orm compliant." +msgid "" +"Field: Field of the record, e.g., product_id.uom_id.name. They are " +"orm compliant." msgstr "字段: 记录的字段,例如product_id.uom_id.name。他们是合规的。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "No Delete: By default, all one2many lines will be deleted before import. Select this, to avoid deletion" -msgstr "不删除: 默认情况下,导入前将删除所有one2many行。选择此项,以避免删除" +msgid "" +"No Delete: By default, all one2many lines will be deleted before " +"import. Select this, to avoid deletion" +msgstr "" +"不删除: 默认情况下,导入前将删除所有one2many行。选择此项,以避免删除" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -86,8 +103,12 @@ msgstr "备注:" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Row Field: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data" -msgstr "行字段: 使用_HEAD_表示记录本身,使用one2many字段(例如line_ids)表示行数据" +msgid "" +"Row Field: Use _HEAD_ for the record itself, and one2many field (e." +"g., line_ids) for row data" +msgstr "" +"行字段: 使用_HEAD_表示记录本身,使用one2many字段(例如line_ids)表示行" +"数据" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -96,22 +117,34 @@ msgstr "工作表: excel工作表的名称(例如,工作表1)或索 #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to" -msgstr "工作表: 要将数据导出到的Excel工作表的名称(例如,工作表1)或索引(例如,1)" - -#. module: excel_import_export -#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == \"ABC\" and #{font=bold;fill=red} or None?" +msgid "" +"Sheet: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to " +"export data to" msgstr "" -"样式条件: python代码中的条件样式 #?...?, 例如,为特定产品应用样式, #" -"?value.name == \"ABC\" and #{font=bold;fill=red} or None?" +"工作表: 要将数据导出到的Excel工作表的名称(例如,工作表1)或索引(例" +"如,1)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Style: Default style in #{...} that apply to each cell, e.g., #{align=left;style=text}. See module's style.py for available styles." +msgid "" +"Style w/Cond.: Conditional style by python code in #?...?, e.g., apply style for specific product, #?value.name == \"ABC\" " +"and #{font=bold;fill=red} or None?" +msgstr "" +"样式条件: python代码中的条件样式 #?...?, 例如,为特定产品" +"应用样式, #?value.name == \"ABC\" and #{font=bold;fill=red} or None?" + +#. module: excel_import_export +#: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form +msgid "" +"Style: Default style in #{...} that apply to each cell, " +"e.g., #{align=left;style=text}. See module's style.py " +"for available styles." msgstr "" "样式: 默认样式#{...} 适用于每个单元格, 例如, " -"#{align=left;style=text}. 请参阅模块的style.py 可用的样式。" +"#{align=left;style=text}. 请参阅模块的style.py 可用的样" +"式。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -120,16 +153,20 @@ msgstr "总和: 在最后一行添加总和值, @{sum}" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "\n" +msgid "" +"\n" "{\n" " '__EXPORT__': {\n" " 'sale_order': { # sheet can be name (string) or index (integer)\n" " '_HEAD_': {\n" -" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;style=text}',\n" +" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;" +"style=text}',\n" " 'B3': 'name${value or \"\"}#{align=left;style=text}',\n" " },\n" -" 'line_ids': { # prefix with _CONT_ to continue rows from previous row field\n" -" 'A6': 'product_id.display_name${value or \"\"}#{style=text}',\n" +" 'line_ids': { # prefix with _CONT_ to continue rows from " +"previous row field\n" +" 'A6': 'product_id.display_name${value or \"\"}" +"#{style=text}',\n" " 'C6': 'product_uom_qty${value or 0}#{style=number}',\n" " 'E6': 'price_unit${value or 0}#{style=number}',\n" " 'G6': 'price_subtotal${value or 0}#{style=number}',\n" @@ -138,7 +175,8 @@ msgid "\n" " },\n" " '__IMPORT__': {\n" " 'sale_order': { # sheet can be name (string) or index (integer)\n" -" 'order_line': { # prefix with _NODEL_ to not delete rows before import\n" +" 'order_line': { # prefix with _NODEL_ to not delete rows before " +"import\n" " 'A6': 'product_id',\n" " 'C6': 'product_uom_qty',\n" " 'E6': 'price_unit${value > 0 and value or 0}',\n" @@ -155,14 +193,14 @@ msgstr "" " '__EXPORT__': {\n" " 'sale_order': { # 工作表名称可以是(字符串)或索引(整数)\n" " '_HEAD_': {\n" -" 'B2': 'partner_id.display_name${value or \"\"" -"}#{align=left;style=text}',\n" +" 'B2': 'partner_id.display_name${value or \"\"}#{align=left;" +"style=text}',\n" " 'B3': 'name${value or \"\"}#{align=left;style=text}',\n" " },\n" " 'line_ids': { # prefix with _CONT_ to continue rows from " "previous row field\n" -" 'A6': 'product_id.display_name${value or \"\"}#{style=text}'," -"\n" +" 'A6': 'product_id.display_name${value or \"\"}" +"#{style=text}',\n" " 'C6': 'product_uom_qty${value or 0}#{style=number}',\n" " 'E6': 'price_unit${value or 0}#{style=number}',\n" " 'G6': 'price_subtotal${value or 0}#{style=number}',\n" @@ -196,7 +234,8 @@ msgstr "model: 活动模型,例如., self.env['my.model']" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "object: record object or line object depends on Row Field" +msgid "" +"object: record object or line object depends on Row Field" msgstr "object: 记录对象或行对象依赖于 行字段" #. module: excel_import_export @@ -213,8 +252,11 @@ msgstr "value: 值来自 字段" #. openerp-web #: code:addons/excel_import_export/static/src/js/report/action_manager_report.js:49 #, python-format -msgid "A popup window with your report was blocked. You may need to change your browser settings to allow popup windows for this page." -msgstr "您的报告的弹出窗口被阻止。您可能需要更改浏览器设置以允许此页面的弹出窗口。" +msgid "" +"A popup window with your report was blocked. You may need to change your " +"browser settings to allow popup windows for this page." +msgstr "" +"您的报告的弹出窗口被阻止。您可能需要更改浏览器设置以允许此页面的弹出窗口。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -248,7 +290,8 @@ msgstr "excel的可用样式" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template_import__no_delete -msgid "By default, all rows will be deleted before import.\n" +msgid "" +"By default, all rows will be deleted before import.\n" "Select No Delete, otherwise" msgstr "" "默认情况下,导入前将删除所有行。\n" @@ -271,7 +314,8 @@ msgstr "CSV引用" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__post_import_hook -msgid "Call a function after successful import, i.e.,\n" +msgid "" +"Call a function after successful import, i.e.,\n" "${object.post_import_do_something()}" msgstr "" "成功导入后调用函数,即,\n" @@ -291,8 +335,7 @@ msgid "Cell" msgstr "单元格" #. module: excel_import_export -#: selection:export.xlsx.wizard,state:0 -#: selection:import.xlsx.wizard,state:0 +#: selection:export.xlsx.wizard,state:0 selection:import.xlsx.wizard,state:0 #: selection:xlsx.report,state:0 msgid "Choose" msgstr "选择" @@ -411,7 +454,8 @@ msgstr "文件必须处于草稿状态" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_import.py:89 #, python-format -msgid "Error deleting data\n" +msgid "" +"Error deleting data\n" "%s" msgstr "" "删除数据时出错\n" @@ -420,7 +464,8 @@ msgstr "" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:141 #, python-format -msgid "Error filling data into Excel sheets\n" +msgid "" +"Error filling data into Excel sheets\n" "%s" msgstr "" "将数据填充到Excel工作表时出错\n" @@ -429,7 +474,8 @@ msgstr "" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_import.py:219 #, python-format -msgid "Error importing data\n" +msgid "" +"Error importing data\n" "%s" msgstr "" "导入数据时出错\n" @@ -484,17 +530,25 @@ msgstr "导出" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Export Instruction is how to write data from an active data record to specified cells in excel sheet.\n" -" For example, an active record can be a sale order that user want to export.\n" -" The record itself will be mapped to the header part of excel sheet. The record can contain multiple one2many fields, which will be written as data lines.\n" -" You can look at following instruction as Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row sections (one2many fields)." +msgid "" +"Export Instruction is how to write data from an active data record to " +"specified cells in excel sheet.\n" +" For example, an active record can be a " +"sale order that user want to export.\n" +" The record itself will be mapped to the " +"header part of excel sheet. The record can contain multiple one2many fields, " +"which will be written as data lines.\n" +" You can look at following instruction as " +"Excel Sheet(s), each with 1 header section (_HEAD_) and multiple row " +"sections (one2many fields)." msgstr "" "导出指令是如何将数据从活动数据记录写入Excel工作表中的指定单元格。\n" -" 例如,活动记录可以是用户想要导出的销售订单。\n" -" " -"记录本身将映射到Excel工作表的标题部分。该记录可以包含多个one2many字段,这些字段将写为数据行。\n" -" " -"您可以将以下指令看作Excel工作表,每个工作表有1个标题部分(_HEAD_)和多个行部分(one2many字段)。" +" 例如,活动记录可以是用户想要导出的销售订" +"单。\n" +" 记录本身将映射到Excel工作表的标题部分。该记" +"录可以包含多个one2many字段,这些字段将写为数据行。\n" +" 您可以将以下指令看作Excel工作表,每个工作表" +"有1个标题部分(_HEAD_)和多个行部分(one2many字段)。" #. module: excel_import_export #: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__field_name @@ -539,15 +593,20 @@ msgstr "以下是每列的详细说明:" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Following show very simple example of the dictionary construct.\n" -" Normally, this will be within templates.xml file within addons." +msgid "" +"Following show very simple example of the dictionary construct.\n" +" Normally, this will be within templates." +"xml file within addons." msgstr "" "下面显示了字典构造的非常简单的示例。\n" -" 通常,这将位于插件中的templates.xml文件中。" +" 通常,这将位于插件中的templates.xml文件" +"中。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "For code block ${...} and #?...?, following object are available," +msgid "" +"For code block ${...} and #?...?, following object " +"are available," msgstr "对于代码块 ${...}#?...?, 以下对象可用," #. module: excel_import_export @@ -556,8 +615,7 @@ msgid "For code block ${...}, following object are available," msgstr "对于代码块${...}, 以下对象可用," #. module: excel_import_export -#: selection:export.xlsx.wizard,state:0 -#: selection:import.xlsx.wizard,state:0 +#: selection:export.xlsx.wizard,state:0 selection:import.xlsx.wizard,state:0 #: selection:xlsx.report,state:0 msgid "Get" msgstr "获取" @@ -625,7 +683,8 @@ msgstr "如果部分类型是行,则此字段是必需的" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:137 #, python-format -msgid "IllegalCharacterError\n" +msgid "" +"IllegalCharacterError\n" "Some exporting data contain special character\n" "%s" msgstr "" @@ -663,16 +722,24 @@ msgstr "导入文件(*.xlsx)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "Import Instruction is how to get data from excel sheet and write them to an active record.\n" -" For example, user create a sales order document, and want to import order lines from excel.\n" -" In reverse direction to exporting, data from excel's cells will be mapped to record fields during import.\n" -" Cells can be mapped to record in header section (_HEAD_) and data table can be mapped to row section (one2many field, begins from specifed cells." +msgid "" +"Import Instruction is how to get data from excel sheet and write them to an " +"active record.\n" +" For example, user create a sales order " +"document, and want to import order lines from excel.\n" +" In reverse direction to exporting, data " +"from excel's cells will be mapped to record fields during import.\n" +" Cells can be mapped to record in header " +"section (_HEAD_) and data table can be mapped to row section (one2many " +"field, begins from specifed cells." msgstr "" "导入指令是如何从Excel工作表中获取数据并将其写入活动记录。\n" -" 例如,用户创建销售订单文档,并希望从excel导入订单行。\n" -" 与导出相反,excel单元格中的数据将在导入期间映射到记录字段。\n" -" " -"单元格可以映射到标题部分(_HEAD_)中的记录,数据表可以映射到行部分(one2many字段,从指定的单元格开始)。" +" 例如,用户创建销售订单文档,并希望从excel导" +"入订单行。\n" +" 与导出相反,excel单元格中的数据将在导入期间" +"映射到记录字段。\n" +" 单元格可以映射到标题部分(_HEAD_)中的记" +"录,数据表可以映射到行部分(one2many字段,从指定的单元格开始)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.import_xlsx_wizard @@ -681,27 +748,41 @@ msgstr "导入成功!" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2)." -msgstr "在头部分,将数据字段(例如,number, partner id.name)映射到单元格(例如,B1, B2)。" +msgid "" +"In header section part, map data fields (e.g., number, partner_id.name) into " +"cells (e.g., B1, B2)." +msgstr "" +"在头部分,将数据字段(例如,number, partner id.name)映射到单元格(例如,B1, " +"B2)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id)." +msgid "" +"In header section, map cells (e.g., B1, B2) into data fields (e.g., number, " +"partner_id)." msgstr "在头部分,将单元格(如B1、B2)映射到数据字段(如number、合作伙伴id)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "In row section, data list will be rolled out from one2many row field (e.g., order_line), and map data field (i.e., product_id.name, uom_id.name, qty) into the first row cells to start rolling (e.g., A6, B6, C6)." +msgid "" +"In row section, data list will be rolled out from one2many row field (e.g., " +"order_line), and map data field (i.e., product_id.name, uom_id.name, qty) " +"into the first row cells to start rolling (e.g., A6, B6, C6)." msgstr "" -"在行部分中,数据列表将从one2many行字段(例如,order_line)推出,并将数据字段(即product_id.name,uom_id.name," -"qty)映射到第一行单元格以开始滚动(例如,A6,B6,C6)。" +"在行部分中,数据列表将从one2many行字段(例如,order_line)推出,并将数据字段" +"(即product_id.name,uom_id.name,qty)映射到第一行单元格以开始滚动(例如," +"A6,B6,C6)。" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form -msgid "In row section, data table from excel can be imported to one2many row field (e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) to fields (e.g., product_id, uom_id, qty)" +msgid "" +"In row section, data table from excel can be imported to one2many row field " +"(e.g., order_line) by mapping cells on first row onwards (e.g., A6, B6, C6) " +"to fields (e.g., product_id, uom_id, qty)" msgstr "" -"在行部分中,可以通过将第一行上的单元格(例如,A6,B6,C6)映射到字段(例如,product_id,uom_id,qty),将来自excel的数据表导" -"入到one2many行字段(例如,order_line)" +"在行部分中,可以通过将第一行上的单元格(例如,A6,B6,C6)映射到字段(例如," +"product_id,uom_id,qty),将来自excel的数据表导入到one2many行字段(例如," +"order_line)" #. module: excel_import_export #: model_terms:ir.ui.view,arch_db:excel_import_export.view_xlsx_template_form @@ -750,7 +831,8 @@ msgstr "样式类型%s的值%s无效" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:134 #, python-format -msgid "Key Error\n" +msgid "" +"Key Error\n" "%s" msgstr "" "关键字错误\n" @@ -789,7 +871,8 @@ msgstr "最后更新时间" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__gname -msgid "Multiple template of same model, can belong to same group,\n" +msgid "" +"Multiple template of same model, can belong to same group,\n" "result in multiple template selection" msgstr "" "同一模型的多个模板,可以属于同一组,\n" @@ -884,7 +967,8 @@ msgstr "导入后挂钩" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_import.py:232 #, python-format -msgid "Post import operation error\n" +msgid "" +"Post import operation error\n" "%s" msgstr "" "发布导入操作错误\n" @@ -1023,11 +1107,13 @@ msgstr "模板名称" #. module: excel_import_export #: code:addons/excel_import_export/models/common.py:242 #, python-format -msgid "Template with CSV Quoting = False, data must not contain the same char as delimiter -> \"%s\"" +msgid "" +"Template with CSV Quoting = False, data must not contain the same char as " +"delimiter -> \"%s\"" msgstr "CSV Quoting =False的模板,数据不能包含与分隔符相同的字符->\"%s\"" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_export.py:230 +#: code:addons/excel_import_export/models/xlsx_export.py:228 #: code:addons/excel_import_export/models/xlsx_import.py:244 #, python-format msgid "Template's model mismatch" @@ -1051,8 +1137,14 @@ msgstr "选定的重定向操作不适用于模型%s" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_ir_actions_report__report_type -msgid "The type of the report that will be rendered, each one having its own rendering method. HTML means the report will be opened directly in your browser PDF means the report will be rendered using Wkhtmltopdf and downloaded by the user." -msgstr "将呈现的报告类型,每个报告都有自己的呈现方法。HTML表示报告将直接在浏览器中打开PDF表示报告将使用Wkhtmltopdf呈现并由用户下载。" +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" +"将呈现的报告类型,每个报告都有自己的呈现方法。HTML表示报告将直接在浏览器中打" +"开PDF表示报告将使用Wkhtmltopdf呈现并由用户下载。" #. module: excel_import_export #: code:addons/excel_import_export/wizard/import_xlsx_wizard.py:89 @@ -1062,7 +1154,8 @@ msgstr "此导入操作在此文档上下文中不可用" #. module: excel_import_export #: model:ir.model.fields,help:excel_import_export.field_xlsx_template__show_instruction -msgid "This is the computed instruction based on tab Import/Export,\n" +msgid "" +"This is the computed instruction based on tab Import/Export,\n" "to be used by xlsx import/export engine" msgstr "" "这是基于选项卡导入/导出的计算指令,\n" From 5bc47b33b6c29728a40be2f44ab39b80c6979398 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 20 Feb 2020 16:56:27 +0000 Subject: [PATCH 29/88] [UPD] Update excel_import_export.pot --- excel_import_export/i18n/excel_import_export.pot | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/excel_import_export/i18n/excel_import_export.pot b/excel_import_export/i18n/excel_import_export.pot index 7c00bb8de..a39d82775 100644 --- a/excel_import_export/i18n/excel_import_export.pot +++ b/excel_import_export/i18n/excel_import_export.pot @@ -193,6 +193,18 @@ msgstr "" msgid "Allow Choose Template" msgstr "" +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__attachment_ids +msgid "Attachments" +msgstr "" + #. module: excel_import_export #: model:ir.model,name:excel_import_export.model_xlsx_styles msgid "Available styles for excel" From deb6229a186f3e221c73dfbaa6e1db3238edbe68 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Thu, 20 Feb 2020 17:16:18 +0000 Subject: [PATCH 30/88] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-12.0/server-tools-12.0-excel_import_export Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export/ --- excel_import_export/i18n/zh_CN.po | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/excel_import_export/i18n/zh_CN.po b/excel_import_export/i18n/zh_CN.po index 523fba3b8..976f61844 100644 --- a/excel_import_export/i18n/zh_CN.po +++ b/excel_import_export/i18n/zh_CN.po @@ -283,6 +283,18 @@ msgstr "添加工作表部分" msgid "Allow Choose Template" msgstr "添加表单部分" +#. module: excel_import_export +#: model:ir.model.fields,field_description:excel_import_export.field_export_xlsx_wizard__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_export__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_import__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_report__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_styles__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_export__attachment_ids +#: model:ir.model.fields,field_description:excel_import_export.field_xlsx_template_import__attachment_ids +msgid "Attachments" +msgstr "" + #. module: excel_import_export #: model:ir.model,name:excel_import_export.model_xlsx_styles msgid "Available styles for excel" From 492d477c390979ccd5a83427a10238d94dc6a4c1 Mon Sep 17 00:00:00 2001 From: kittiu Date: Sun, 15 Mar 2020 15:44:39 +0700 Subject: [PATCH 31/88] [12.0][FIX] excel_import_export, problem on import with datetime field Can't import if the import file has date time field --- excel_import_export/models/common.py | 9 ++++----- excel_import_export/models/xlsx_import.py | 13 +++++++++---- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py index 3f5fc50fe..5e70f0d95 100644 --- a/excel_import_export/models/common.py +++ b/excel_import_export/models/common.py @@ -268,13 +268,12 @@ def _get_cell_value(cell, field_type=False): datemode = 0 # From book.datemode, but we fix it for simplicity if field_type in ['date', 'datetime']: ctype = xlrd.sheet.ctype_text.get(cell.ctype, 'unknown type') - if ctype == 'number': + if ctype in ('xldate', 'number'): + is_datetime = cell.value % 1 != 0.0 time_tuple = xlrd.xldate_as_tuple(cell.value, datemode) date = dt(*time_tuple) - if field_type == 'date': - value = date.strftime("%Y-%m-%d") - elif field_type == 'datetime': - value = date.strftime("%Y-%m-%d %H:%M:%S") + value = (date.strftime("%Y-%m-%d %H:%M:%S") + if is_datetime else date.strftime("%Y-%m-%d")) else: value = cell.value elif field_type in ['integer', 'float']: diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py index 933d86149..389a0a544 100644 --- a/excel_import_export/models/xlsx_import.py +++ b/excel_import_export/models/xlsx_import.py @@ -203,14 +203,19 @@ class XLSXImport(models.AbstractModel): 'encoding': '', 'separator': '', 'quoting': '"', - 'date_style': '', - 'datetime_style': '%Y-%m-%d %H:%M:%S', + 'date_format': '%Y-%m-%d', + 'datetime_format': '%Y-%m-%d %H:%M:%S', 'float_thousand_separator': ',', 'float_decimal_separator': '.', 'fields': []}) if errors.get('messages'): - message = errors['messages']['message'].encode('utf-8') - raise ValidationError(message) + message = _('Error importing data') + messages = errors['messages'] + if isinstance(messages, dict): + message = messages['message'] + if isinstance(messages, list): + message = ', '.join([x['message'] for x in messages]) + raise ValidationError(message.encode('utf-8')) return self.env.ref(xml_id) except xlrd.XLRDError: raise ValidationError( From 66e6fc0f735177d0d78d3be4e4a0403bf06a37e0 Mon Sep 17 00:00:00 2001 From: oca-travis Date: Thu, 2 Apr 2020 11:10:31 +0000 Subject: [PATCH 32/88] [UPD] Update excel_import_export.pot --- excel_import_export/i18n/excel_import_export.pot | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/excel_import_export/i18n/excel_import_export.pot b/excel_import_export/i18n/excel_import_export.pot index a39d82775..d2843ba6e 100644 --- a/excel_import_export/i18n/excel_import_export.pot +++ b/excel_import_export/i18n/excel_import_export.pot @@ -383,7 +383,13 @@ msgid "Error filling data into Excel sheets\n" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:219 +#: code:addons/excel_import_export/models/xlsx_import.py:212 +#, python-format +msgid "Error importing data" +msgstr "" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:224 #, python-format msgid "Error importing data\n" "%s" @@ -664,7 +670,7 @@ msgid "Invalid declaration, %s has no valid field type" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:217 +#: code:addons/excel_import_export/models/xlsx_import.py:222 #, python-format msgid "Invalid file style, only .xls or .xlsx file allowed" msgstr "" @@ -731,7 +737,7 @@ msgid "No Delete" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:249 +#: code:addons/excel_import_export/models/xlsx_import.py:254 #, python-format msgid "No data_dict['__IMPORT__'] in template %s" msgstr "" @@ -812,7 +818,7 @@ msgid "Post Import Hook" msgstr "" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:232 +#: code:addons/excel_import_export/models/xlsx_import.py:237 #, python-format msgid "Post import operation error\n" "%s" @@ -956,7 +962,7 @@ msgstr "" #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:228 -#: code:addons/excel_import_export/models/xlsx_import.py:244 +#: code:addons/excel_import_export/models/xlsx_import.py:249 #, python-format msgid "Template's model mismatch" msgstr "" From f8166fafd2933ef35fcf6ebb08e9fee252e0d7c0 Mon Sep 17 00:00:00 2001 From: OCA Transbot Date: Thu, 2 Apr 2020 11:21:59 +0000 Subject: [PATCH 33/88] Update translation files Updated by "Update PO files to match POT (msgmerge)" hook in Weblate. Translation: server-tools-12.0/server-tools-12.0-excel_import_export Translate-URL: https://translation.odoo-community.org/projects/server-tools-12-0/server-tools-12-0-excel_import_export/ --- excel_import_export/i18n/zh_CN.po | 21 ++++++++++++++++----- 1 file changed, 16 insertions(+), 5 deletions(-) diff --git a/excel_import_export/i18n/zh_CN.po b/excel_import_export/i18n/zh_CN.po index 976f61844..7fec0fdef 100644 --- a/excel_import_export/i18n/zh_CN.po +++ b/excel_import_export/i18n/zh_CN.po @@ -484,7 +484,18 @@ msgstr "" "%s" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:219 +#: code:addons/excel_import_export/models/xlsx_import.py:212 +#, fuzzy, python-format +#| msgid "" +#| "Error importing data\n" +#| "%s" +msgid "Error importing data" +msgstr "" +"导入数据时出错\n" +"%s" + +#. module: excel_import_export +#: code:addons/excel_import_export/models/xlsx_import.py:224 #, python-format msgid "" "Error importing data\n" @@ -823,7 +834,7 @@ msgid "Invalid declaration, %s has no valid field type" msgstr "声明无效,%s没有有效的字段类型" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:217 +#: code:addons/excel_import_export/models/xlsx_import.py:222 #, python-format msgid "Invalid file style, only .xls or .xlsx file allowed" msgstr "文件样式无效,仅允许.xls或.xlsx文件" @@ -896,7 +907,7 @@ msgid "No Delete" msgstr "不删除" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:249 +#: code:addons/excel_import_export/models/xlsx_import.py:254 #, python-format msgid "No data_dict['__IMPORT__'] in template %s" msgstr "模版%s中没有 data_dict['__IMPORT__']" @@ -977,7 +988,7 @@ msgid "Post Import Hook" msgstr "导入后挂钩" #. module: excel_import_export -#: code:addons/excel_import_export/models/xlsx_import.py:232 +#: code:addons/excel_import_export/models/xlsx_import.py:237 #, python-format msgid "" "Post import operation error\n" @@ -1126,7 +1137,7 @@ msgstr "CSV Quoting =False的模板,数据不能包含与分隔符相同的字 #. module: excel_import_export #: code:addons/excel_import_export/models/xlsx_export.py:228 -#: code:addons/excel_import_export/models/xlsx_import.py:244 +#: code:addons/excel_import_export/models/xlsx_import.py:249 #, python-format msgid "Template's model mismatch" msgstr "模板的模型不匹配" From cf30d5a77fa94a54d0d7a28220c91590e0dc4272 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Mon, 24 Aug 2020 17:57:03 +0700 Subject: [PATCH 34/88] [IMP] : black, isort, prettier --- excel_import_export/__manifest__.py | 45 +-- excel_import_export/controllers/main.py | 53 +-- excel_import_export/models/common.py | 186 ++++----- excel_import_export/models/ir_report.py | 29 +- excel_import_export/models/styles.py | 49 ++- excel_import_export/models/xlsx_export.py | 147 +++---- excel_import_export/models/xlsx_import.py | 201 +++++----- excel_import_export/models/xlsx_report.py | 69 ++-- excel_import_export/models/xlsx_template.py | 371 ++++++++---------- .../src/js/report/action_manager_report.js | 65 +-- .../views/webclient_templates.xml | 7 +- excel_import_export/views/xlsx_report.xml | 44 ++- .../views/xlsx_template_view.xml | 314 ++++++++++----- .../wizard/export_xlsx_wizard.py | 86 ++-- .../wizard/export_xlsx_wizard.xml | 65 +-- .../wizard/import_xlsx_wizard.py | 165 ++++---- .../wizard/import_xlsx_wizard.xml | 55 ++- 17 files changed, 1037 insertions(+), 914 deletions(-) diff --git a/excel_import_export/__manifest__.py b/excel_import_export/__manifest__.py index 5a942f070..212408875 100644 --- a/excel_import_export/__manifest__.py +++ b/excel_import_export/__manifest__.py @@ -2,29 +2,24 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) { - 'name': 'Excel Import/Export/Report', - 'summary': 'Base module for developing Excel import/export/report', - 'version': '12.0.1.0.4', - 'author': 'Ecosoft,Odoo Community Association (OCA)', - 'license': 'AGPL-3', - 'website': 'https://github.com/OCA/server-tools/', - 'category': 'Tools', - 'depends': ['mail'], - 'external_dependencies': { - 'python': [ - 'xlrd', - 'xlwt', - 'openpyxl', - ], - }, - 'data': ['security/ir.model.access.csv', - 'wizard/export_xlsx_wizard.xml', - 'wizard/import_xlsx_wizard.xml', - 'views/xlsx_template_view.xml', - 'views/xlsx_report.xml', - 'views/webclient_templates.xml', - ], - 'installable': True, - 'development_status': 'beta', - 'maintainers': ['kittiu'], + "name": "Excel Import/Export/Report", + "summary": "Base module for developing Excel import/export/report", + "version": "12.0.1.0.4", + "author": "Ecosoft,Odoo Community Association (OCA)", + "license": "AGPL-3", + "website": "https://github.com/OCA/server-tools/", + "category": "Tools", + "depends": ["mail"], + "external_dependencies": {"python": ["xlrd", "xlwt", "openpyxl",],}, + "data": [ + "security/ir.model.access.csv", + "wizard/export_xlsx_wizard.xml", + "wizard/import_xlsx_wizard.xml", + "views/xlsx_template_view.xml", + "views/xlsx_report.xml", + "views/webclient_templates.xml", + ], + "installable": True, + "development_status": "beta", + "maintainers": ["kittiu"], } diff --git a/excel_import_export/controllers/main.py b/excel_import_export/controllers/main.py index ea9effefe..2382c071c 100644 --- a/excel_import_export/controllers/main.py +++ b/excel_import_export/controllers/main.py @@ -1,52 +1,53 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import json import base64 +import json import time -from odoo.addons.web.controllers import main as report -from odoo.http import content_disposition, route, request + +from odoo.http import content_disposition, request, route from odoo.tools.safe_eval import safe_eval +from odoo.addons.web.controllers import main as report + class ReportController(report.ReportController): - @route() def report_routes(self, reportname, docids=None, converter=None, **data): - if converter == 'excel': - report = request.env['ir.actions.report']._get_report_from_name( - reportname) + if converter == "excel": + report = request.env["ir.actions.report"]._get_report_from_name(reportname) context = dict(request.env.context) if docids: - docids = [int(i) for i in docids.split(',')] - if data.get('options'): - data.update(json.loads(data.pop('options'))) - if data.get('context'): + docids = [int(i) for i in docids.split(",")] + if data.get("options"): + data.update(json.loads(data.pop("options"))) + if data.get("context"): # Ignore 'lang' here, because the context in data is the one # from the webclient *but* if the user explicitely wants to # change the lang, this mechanism overwrites it. - data['context'] = json.loads(data['context']) - if data['context'].get('lang'): - del data['context']['lang'] - context.update(data['context']) + data["context"] = json.loads(data["context"]) + if data["context"].get("lang"): + del data["context"]["lang"] + context.update(data["context"]) excel, report_name = report.with_context(context).render_excel( docids, data=data ) excel = base64.decodestring(excel) if report.print_report_name and not len(docids) > 1: obj = request.env[report.model].browse(docids[0]) - file_ext = report_name.split('.')[-1:].pop() - report_name = safe_eval(report.print_report_name, - {'object': obj, 'time': time}) - report_name = '%s.%s' % (report_name, file_ext) - excelhttpheaders = [ - ('Content-Type', 'application/vnd.openxmlformats-' - 'officedocument.spreadsheetml.sheet'), - ('Content-Length', len(excel)), - ( - 'Content-Disposition', - content_disposition(report_name) + file_ext = report_name.split(".")[-1:].pop() + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} ) + report_name = "{}.{}".format(report_name, file_ext) + excelhttpheaders = [ + ( + "Content-Type", + "application/vnd.openxmlformats-" + "officedocument.spreadsheetml.sheet", + ), + ("Content-Length", len(excel)), + ("Content-Disposition", content_disposition(report_name)), ] return request.make_response(excel, headers=excelhttpheaders) return super(ReportController, self).report_routes( diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py index 5e70f0d95..e0314904f 100644 --- a/excel_import_export/models/common.py +++ b/excel_import_export/models/common.py @@ -1,20 +1,21 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import re -import uuid -import csv import base64 -import string +import csv import itertools import logging -from datetime import datetime as dt +import re +import string +import uuid from ast import literal_eval -from dateutil.parser import parse +from datetime import datetime as dt from io import StringIO -from odoo.exceptions import ValidationError -from odoo import _ +from dateutil.parser import parse + +from odoo import _ +from odoo.exceptions import ValidationError _logger = logging.getLogger(__name__) try: @@ -26,40 +27,40 @@ except ImportError: def adjust_cell_formula(value, k): """ Cell formula, i.e., if i=5, val=?(A11)+?(B12) -> val=A16+B17 """ if isinstance(value, str): - for i in range(value.count('?(')): - if value and '?(' in value and ')' in value: - i = value.index('?(') - j = value.index(')', i) - val = value[i + 2:j] + for i in range(value.count("?(")): + if value and "?(" in value and ")" in value: + i = value.index("?(") + j = value.index(")", i) + val = value[i + 2 : j] col, row = split_row_col(val) - new_val = '%s%s' % (col, row+k) - value = value.replace('?(%s)' % val, new_val) + new_val = "{}{}".format(col, row + k) + value = value.replace("?(%s)" % val, new_val) return value def get_field_aggregation(field): """ i..e, 'field@{sum}' """ - if field and '@{' in field and '}' in field: - i = field.index('@{') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "@{" in field and "}" in field: + i = field.index("@{") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': + if cond or cond == "": return (field[:i], cond) except Exception: - return (field.replace('@{%s}' % cond, ''), False) + return (field.replace("@{%s}" % cond, ""), False) return (field, False) def get_field_condition(field): """ i..e, 'field${value > 0 and value or False}' """ - if field and '${' in field and '}' in field: - i = field.index('${') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "${" in field and "}" in field: + i = field.index("${") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('${%s}' % cond, ''), cond) + if cond or cond == "": + return (field.replace("${%s}" % cond, ""), cond) except Exception: return (field, False) return (field, False) @@ -74,13 +75,13 @@ def get_field_style(field): - number = true, false i.e., 'field#{font=bold;fill=red;align=center;style=number}' """ - if field and '#{' in field and '}' in field: - i = field.index('#{') - j = field.index('}', i) - cond = field[i + 2:j] + if field and "#{" in field and "}" in field: + i = field.index("#{") + j = field.index("}", i) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('#{%s}' % cond, ''), cond) + if cond or cond == "": + return (field.replace("#{%s}" % cond, ""), cond) except Exception: return (field, False) return (field, False) @@ -88,39 +89,40 @@ def get_field_style(field): def get_field_style_cond(field): """ i..e, 'field#?object.partner_id and #{font=bold} or #{}?' """ - if field and '#?' in field and '?' in field: - i = field.index('#?') - j = field.index('?', i+2) - cond = field[i + 2:j] + if field and "#?" in field and "?" in field: + i = field.index("#?") + j = field.index("?", i + 2) + cond = field[i + 2 : j] try: - if cond or cond == '': - return (field.replace('#?%s?' % cond, ''), cond) + if cond or cond == "": + return (field.replace("#?%s?" % cond, ""), cond) except Exception: return (field, False) return (field, False) def fill_cell_style(field, field_style, styles): - field_styles = field_style.split(';') + field_styles = field_style.split(";") for f in field_styles: - (key, value) = f.split('=') + (key, value) = f.split("=") if key not in styles.keys(): - raise ValidationError(_('Invalid style type %s' % key)) + raise ValidationError(_("Invalid style type %s" % key)) if value.lower() not in styles[key].keys(): raise ValidationError( - _('Invalid value %s for style type %s' % (value, key))) + _("Invalid value {} for style type {}".format(value, key)) + ) cell_style = styles[key][value] - if key == 'font': + if key == "font": field.font = cell_style - if key == 'fill': + if key == "fill": field.fill = cell_style - if key == 'align': + if key == "align": field.alignment = cell_style - if key == 'style': - if value == 'text': + if key == "style": + if value == "text": try: # In case value can't be encoded as utf, we do normal str() - field.value = field.value.encode('utf-8') + field.value = field.value.encode("utf-8") except Exception: field.value = str(field.value) field.number_format = cell_style @@ -128,10 +130,10 @@ def fill_cell_style(field, field_style, styles): def get_line_max(line_field): """ i.e., line_field = line_ids[100], max = 100 else 0 """ - if line_field and '[' in line_field and ']' in line_field: - i = line_field.index('[') - j = line_field.index(']') - max_str = line_field[i + 1:j] + if line_field and "[" in line_field and "]" in line_field: + i = line_field.index("[") + j = line_field.index("]") + max_str = line_field[i + 1 : j] try: if len(max_str) > 0: return (line_field[:i], int(max_str)) @@ -144,10 +146,10 @@ def get_line_max(line_field): def get_groupby(line_field): """i.e., line_field = line_ids["a_id, b_id"], groupby = ["a_id", "b_id"]""" - if line_field and '[' in line_field and ']' in line_field: - i = line_field.index('[') - j = line_field.index(']') - groupby = literal_eval(line_field[i:j+1]) + if line_field and "[" in line_field and "]" in line_field: + i = line_field.index("[") + j = line_field.index("]") + groupby = literal_eval(line_field[i : j + 1]) return groupby return False @@ -155,7 +157,7 @@ def get_groupby(line_field): def split_row_col(pos): match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) if not match: - raise ValidationError(_('Position %s is not valid') % pos) + raise ValidationError(_("Position %s is not valid") % pos) col, row = match.groups() return col, int(row) @@ -199,9 +201,9 @@ def isinteger(input_val): def isdatetime(input_val): try: if len(input_val) == 10: - dt.strptime(input_val, '%Y-%m-%d') + dt.strptime(input_val, "%Y-%m-%d") elif len(input_val) == 19: - dt.strptime(input_val, '%Y-%m-%d %H:%M:%S') + dt.strptime(input_val, "%Y-%m-%d %H:%M:%S") else: return False return True @@ -211,14 +213,14 @@ def isdatetime(input_val): def str_to_number(input_val): if isinstance(input_val, str): - if ' ' not in input_val: + if " " not in input_val: if isdatetime(input_val): return parse(input_val) elif isinteger(input_val): - if not (len(input_val) > 1 and input_val[:1] == '0'): + if not (len(input_val) > 1 and input_val[:1] == "0"): return int(input_val) elif isfloat(input_val): - if not (input_val.find(".") > 2 and input_val[:1] == '0'): + if not (input_val.find(".") > 2 and input_val[:1] == "0"): return float(input_val) return input_val @@ -239,25 +241,28 @@ def csv_from_excel(excel_content, delimiter, quote): for x in sh.row_values(rownum): if quoting == csv.QUOTE_NONE and delimiter in x: raise ValidationError( - _('Template with CSV Quoting = False, data must not ' - 'contain the same char as delimiter -> "%s"') % - delimiter) + _( + "Template with CSV Quoting = False, data must not " + 'contain the same char as delimiter -> "%s"' + ) + % delimiter + ) row.append(x) wr.writerow(row) content.seek(0) # Set index to 0, and start reading - out_file = base64.b64encode(content.getvalue().encode('utf-8')) + out_file = base64.b64encode(content.getvalue().encode("utf-8")) return out_file def pos2idx(pos): match = re.match(r"([a-z]+)([0-9]+)", pos, re.I) if not match: - raise ValidationError(_('Position %s is not valid') % (pos, )) + raise ValidationError(_("Position %s is not valid") % (pos,)) col, row = match.groups() col_num = 0 for c in col: if c in string.ascii_letters: - col_num = col_num * 26 + (ord(c.upper()) - ord('A')) + 1 + col_num = col_num * 26 + (ord(c.upper()) - ord("A")) + 1 return (int(row) - 1, col_num - 1) @@ -266,28 +271,31 @@ def _get_cell_value(cell, field_type=False): if not know, just get value as is """ value = False datemode = 0 # From book.datemode, but we fix it for simplicity - if field_type in ['date', 'datetime']: - ctype = xlrd.sheet.ctype_text.get(cell.ctype, 'unknown type') - if ctype in ('xldate', 'number'): + if field_type in ["date", "datetime"]: + ctype = xlrd.sheet.ctype_text.get(cell.ctype, "unknown type") + if ctype in ("xldate", "number"): is_datetime = cell.value % 1 != 0.0 time_tuple = xlrd.xldate_as_tuple(cell.value, datemode) date = dt(*time_tuple) - value = (date.strftime("%Y-%m-%d %H:%M:%S") - if is_datetime else date.strftime("%Y-%m-%d")) + value = ( + date.strftime("%Y-%m-%d %H:%M:%S") + if is_datetime + else date.strftime("%Y-%m-%d") + ) else: value = cell.value - elif field_type in ['integer', 'float']: - value_str = str(cell.value).strip().replace(',', '') + elif field_type in ["integer", "float"]: + value_str = str(cell.value).strip().replace(",", "") if len(value_str) == 0: - value = '' - elif value_str.replace('.', '', 1).isdigit(): # Is number - if field_type == 'integer': + value = "" + elif value_str.replace(".", "", 1).isdigit(): # Is number + if field_type == "integer": value = int(float(value_str)) - elif field_type == 'float': + elif field_type == "float": value = float(value_str) else: # Is string, no conversion value = value_str - elif field_type in ['many2one']: + elif field_type in ["many2one"]: # If number, change to string if isinstance(cell.value, (int, float, complex)): value = str(cell.value) @@ -297,38 +305,38 @@ def _get_cell_value(cell, field_type=False): value = cell.value # If string, cleanup if isinstance(value, str): - if value[-2:] == '.0': + if value[-2:] == ".0": value = value[:-2] # Except boolean, when no value, we should return as '' - if field_type not in ['boolean']: + if field_type not in ["boolean"]: if not value: - value = '' + value = "" return value def _add_column(column_name, column_value, file_txt): i = 0 txt_lines = [] - for line in file_txt.split('\n'): + for line in file_txt.split("\n"): if line and i == 0: line = '"' + str(column_name) + '",' + line elif line: line = '"' + str(column_value) + '",' + line txt_lines.append(line) i += 1 - file_txt = '\n'.join(txt_lines) + file_txt = "\n".join(txt_lines) return file_txt def _add_id_column(file_txt): i = 0 txt_lines = [] - for line in file_txt.split('\n'): + for line in file_txt.split("\n"): if line and i == 0: line = '"id",' + line elif line: - line = '%s.%s' % ('xls', uuid.uuid4()) + ',' + line + line = "{}.{}".format("xls", uuid.uuid4()) + "," + line txt_lines.append(line) i += 1 - file_txt = '\n'.join(txt_lines) + file_txt = "\n".join(txt_lines) return file_txt diff --git a/excel_import_export/models/ir_report.py b/excel_import_export/models/ir_report.py index 656096f9c..8b5bf26cd 100644 --- a/excel_import_export/models/ir_report.py +++ b/excel_import_export/models/ir_report.py @@ -1,7 +1,7 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import api, fields, models, _ +from odoo import _, api, fields, models from odoo.exceptions import UserError @@ -13,15 +13,18 @@ class ReportAction(models.Model): @api.model def render_excel(self, docids, data): if len(docids) != 1: - raise UserError( - _('Only one id is allowed for excel_import_export')) - xlsx_template = self.env['xlsx.template'].search( - [('fname', '=', self.report_name), ('res_model', '=', self.model)]) + raise UserError(_("Only one id is allowed for excel_import_export")) + xlsx_template = self.env["xlsx.template"].search( + [("fname", "=", self.report_name), ("res_model", "=", self.model)] + ) if not xlsx_template or len(xlsx_template) != 1: raise UserError( - _("Template %s on model %s is not unique!" % - (self.report_name, self.model))) - Export = self.env['xlsx.export'] + _( + "Template %s on model %s is not unique!" + % (self.report_name, self.model) + ) + ) + Export = self.env["xlsx.export"] return Export.export_xlsx(xlsx_template, self.model, docids[0]) @api.model @@ -29,11 +32,11 @@ class ReportAction(models.Model): res = super(ReportAction, self)._get_report_from_name(report_name) if res: return res - report_obj = self.env['ir.actions.report'] - qwebtypes = ['excel'] + report_obj = self.env["ir.actions.report"] + qwebtypes = ["excel"] conditions = [ - ('report_type', 'in', qwebtypes), - ('report_name', '=', report_name), + ("report_type", "in", qwebtypes), + ("report_name", "=", report_name), ] - context = self.env['res.users'].context_get() + context = self.env["res.users"].context_get() return report_obj.with_context(context).search(conditions, limit=1) diff --git a/excel_import_export/models/styles.py b/excel_import_export/models/styles.py index 9738a3c8a..2c753f345 100644 --- a/excel_import_export/models/styles.py +++ b/excel_import_export/models/styles.py @@ -1,48 +1,47 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import models, api import logging +from odoo import api, models + _logger = logging.getLogger(__name__) try: from openpyxl.styles import colors, PatternFill, Alignment, Font except ImportError: - _logger.debug( - 'Cannot import "openpyxl". Please make sure it is installed.') + _logger.debug('Cannot import "openpyxl". Please make sure it is installed.') class XLSXStyles(models.AbstractModel): - _name = 'xlsx.styles' - _description = 'Available styles for excel' + _name = "xlsx.styles" + _description = "Available styles for excel" @api.model def get_openpyxl_styles(self): """ List all syles that can be used with styleing directive #{...} """ return { - 'font': { - 'bold': Font(name="Arial", size=10, bold=True), - 'bold_red': Font(name="Arial", size=10, - color=colors.RED, bold=True), + "font": { + "bold": Font(name="Arial", size=10, bold=True), + "bold_red": Font(name="Arial", size=10, color=colors.RED, bold=True), }, - 'fill': { - 'red': PatternFill("solid", fgColor="FF0000"), - 'grey': PatternFill("solid", fgColor="DDDDDD"), - 'yellow': PatternFill("solid", fgColor="FFFCB7"), - 'blue': PatternFill("solid", fgColor="9BF3FF"), - 'green': PatternFill("solid", fgColor="B0FF99"), + "fill": { + "red": PatternFill("solid", fgColor="FF0000"), + "grey": PatternFill("solid", fgColor="DDDDDD"), + "yellow": PatternFill("solid", fgColor="FFFCB7"), + "blue": PatternFill("solid", fgColor="9BF3FF"), + "green": PatternFill("solid", fgColor="B0FF99"), }, - 'align': { - 'left': Alignment(horizontal='left'), - 'center': Alignment(horizontal='center'), - 'right': Alignment(horizontal='right'), + "align": { + "left": Alignment(horizontal="left"), + "center": Alignment(horizontal="center"), + "right": Alignment(horizontal="right"), }, - 'style': { - 'number': '#,##0.00', - 'date': 'dd/mm/yyyy', - 'datestamp': 'yyyy-mm-dd', - 'text': '@', - 'percent': '0.00%', + "style": { + "number": "#,##0.00", + "date": "dd/mm/yyyy", + "datestamp": "yyyy-mm-dd", + "text": "@", + "percent": "0.00%", }, } diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index fcc2512a5..2aac49b90 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -1,16 +1,18 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import os -import logging import base64 -from io import BytesIO +import logging +import os import time from datetime import date, datetime as dt -from odoo.tools.float_utils import float_compare -from odoo import models, fields, api, _ -from odoo.tools.safe_eval import safe_eval +from io import BytesIO + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare +from odoo.tools.safe_eval import safe_eval + from . import common as co _logger = logging.getLogger(__name__) @@ -18,26 +20,26 @@ try: from openpyxl import load_workbook from openpyxl.utils.exceptions import IllegalCharacterError except ImportError: - _logger.debug( - 'Cannot import "openpyxl". Please make sure it is installed.') + _logger.debug('Cannot import "openpyxl". Please make sure it is installed.') class XLSXExport(models.AbstractModel): - _name = 'xlsx.export' - _description = 'Excel Export AbstractModel' + _name = "xlsx.export" + _description = "Excel Export AbstractModel" @api.model def get_eval_context(self, model, record, value): - eval_context = {'float_compare': float_compare, - 'time': time, - 'datetime': dt, - 'date': date, - 'value': value, - 'object': record, - 'model': self.env[model], - 'env': self.env, - 'context': self._context, - } + eval_context = { + "float_compare": float_compare, + "time": time, + "datetime": dt, + "date": date, + "value": value, + "object": record, + "model": self.env[model], + "env": self.env, + "context": self._context, + } return eval_context @api.model @@ -48,12 +50,11 @@ class XLSXExport(models.AbstractModel): - fields: fields in line_ids, i.e., partner_id.display_name """ line_field, max_row = co.get_line_max(line_field) - line_field = line_field.replace('_CONT_', '') # Remove _CONT_ if any + line_field = line_field.replace("_CONT_", "") # Remove _CONT_ if any lines = record[line_field] if max_row > 0 and len(lines) > max_row: - raise Exception( - _('Records in %s exceed max records allowed') % line_field) - vals = dict([(field, []) for field in fields]) # value and do_style + raise Exception(_("Records in %s exceed max records allowed") % line_field) + vals = {field: [] for field in fields} # value and do_style # Get field condition & aggre function field_cond_dict = {} aggre_func_dict = {} @@ -77,31 +78,32 @@ class XLSXExport(models.AbstractModel): for field in pair_fields: # (field, raw_field) value = self._get_field_data(field[1], line) eval_cond = field_cond_dict[field[0]] - eval_context = \ - self.get_eval_context(line._name, line, value) + eval_context = self.get_eval_context(line._name, line, value) if eval_cond: value = safe_eval(eval_cond, eval_context) # style w/Cond takes priority style_cond = style_cond_dict[field[0]] - style = self._eval_style_cond(line._name, line, - value, style_cond) + style = self._eval_style_cond(line._name, line, value, style_cond) if style is None: style = False # No style elif style is False: style = field_style_dict[field[0]] # Use default style vals[field[0]].append((value, style)) - return (vals, aggre_func_dict,) + return ( + vals, + aggre_func_dict, + ) @api.model def _eval_style_cond(self, model, record, value, style_cond): eval_context = self.get_eval_context(model, record, value) - field = style_cond = style_cond or '#??' + field = style_cond = style_cond or "#??" styles = {} - for i in range(style_cond.count('#{')): + for i in range(style_cond.count("#{")): i += 1 field, style = co.get_field_style(field) styles.update({i: style}) - style_cond = style_cond.replace('#{%s}' % style, str(i)) + style_cond = style_cond.replace("#{%s}" % style, str(i)) if not styles: return False res = safe_eval(style_cond, eval_context) @@ -122,23 +124,25 @@ class XLSXExport(models.AbstractModel): st = co.openpyxl_get_sheet_by_name(workbook, sheet_name) elif isinstance(sheet_name, int): if sheet_name > len(workbook.worksheets): - raise Exception(_('Not enough worksheets')) + raise Exception(_("Not enough worksheets")) st = workbook.worksheets[sheet_name - 1] if not st: - raise ValidationError( - _('Sheet %s not found') % sheet_name) + raise ValidationError(_("Sheet %s not found") % sheet_name) # Fill data, header and rows self._fill_head(ws, st, record) self._fill_lines(ws, st, record) except KeyError as e: - raise ValidationError(_('Key Error\n%s') % e) + raise ValidationError(_("Key Error\n%s") % e) except IllegalCharacterError as e: raise ValidationError( - _('IllegalCharacterError\n' - 'Some exporting data contain special character\n%s') % e) + _( + "IllegalCharacterError\n" + "Some exporting data contain special character\n%s" + ) + % e + ) except Exception as e: - raise ValidationError( - _('Error filling data into Excel sheets\n%s') % e) + raise ValidationError(_("Error filling data into Excel sheets\n%s") % e) @api.model def _get_field_data(self, _field, _line): @@ -146,43 +150,41 @@ class XLSXExport(models.AbstractModel): if not _field: return None line_copy = _line - for f in _field.split('.'): + for f in _field.split("."): line_copy = line_copy[f] if isinstance(line_copy, str): - line_copy = line_copy.encode('utf-8') + line_copy = line_copy.encode("utf-8") return line_copy @api.model def _fill_head(self, ws, st, record): - for rc, field in ws.get('_HEAD_', {}).items(): + for rc, field in ws.get("_HEAD_", {}).items(): tmp_field, eval_cond = co.get_field_condition(field) eval_cond = eval_cond or 'value or ""' tmp_field, field_style = co.get_field_style(tmp_field) tmp_field, style_cond = co.get_field_style_cond(tmp_field) value = tmp_field and self._get_field_data(tmp_field, record) # Eval - eval_context = self.get_eval_context(record._name, - record, value) + eval_context = self.get_eval_context(record._name, record, value) if eval_cond: value = safe_eval(eval_cond, eval_context) if value is not None: st[rc] = value - fc = not style_cond and True or \ - safe_eval(style_cond, eval_context) + fc = not style_cond and True or safe_eval(style_cond, eval_context) if field_style and fc: # has style and pass style_cond - styles = self.env['xlsx.styles'].get_openpyxl_styles() + styles = self.env["xlsx.styles"].get_openpyxl_styles() co.fill_cell_style(st[rc], field_style, styles) @api.model def _fill_lines(self, ws, st, record): line_fields = list(ws) - if '_HEAD_' in line_fields: - line_fields.remove('_HEAD_') + if "_HEAD_" in line_fields: + line_fields.remove("_HEAD_") cont_row = 0 # last data row to continue for line_field in line_fields: fields = ws.get(line_field, {}).values() vals, func = self._get_line_vals(record, line_field, fields) - is_cont = '_CONT_' in line_field and True or False # continue row + is_cont = "_CONT_" in line_field and True or False # continue row cont_set = 0 rows_inserted = False # flag to insert row for rc, field in ws.get(line_field, {}).items(): @@ -192,7 +194,7 @@ class XLSXExport(models.AbstractModel): cont_set = cont_row + 1 if is_cont: row = cont_set - rc = '%s%s' % (col, cont_set) + rc = "{}{}".format(col, cont_set) i = 0 new_row = 0 new_rc = False @@ -200,24 +202,24 @@ class XLSXExport(models.AbstractModel): # Insert rows to preserve total line if not rows_inserted: rows_inserted = True - st.insert_rows(row+1, amount=row_count-1) + st.insert_rows(row + 1, amount=row_count - 1) # -- for (row_val, style) in vals[field]: new_row = row + i - new_rc = '%s%s' % (col, new_row) + new_rc = "{}{}".format(col, new_row) row_val = co.adjust_cell_formula(row_val, i) - if row_val not in ('None', None): + if row_val not in ("None", None): st[new_rc] = co.str_to_number(row_val) if style: - styles = self.env['xlsx.styles'].get_openpyxl_styles() + styles = self.env["xlsx.styles"].get_openpyxl_styles() co.fill_cell_style(st[new_rc], style, styles) i += 1 # Add footer line if at least one field have sum f = func.get(field, False) if f and new_row > 0: new_row += 1 - f_rc = '%s%s' % (col, new_row) - st[f_rc] = '=%s(%s:%s)' % (f, rc, new_rc) + f_rc = "{}{}".format(col, new_row) + st[f_rc] = "={}({}:{})".format(f, rc, new_rc) co.fill_cell_style(st[f_rc], style, styles) cont_row = cont_row < new_row and new_row or cont_row return @@ -227,7 +229,7 @@ class XLSXExport(models.AbstractModel): if template.res_model != res_model: raise ValidationError(_("Template's model mismatch")) data_dict = co.literal_eval(template.instruction.strip()) - export_dict = data_dict.get('__EXPORT__', False) + export_dict = data_dict.get("__EXPORT__", False) out_name = template.name if not export_dict: # If there is not __EXPORT__ formula, just export out_name = template.fname @@ -235,11 +237,11 @@ class XLSXExport(models.AbstractModel): return (out_file, out_name) # Prepare temp file (from now, only xlsx file works for openpyxl) decoded_data = base64.decodestring(template.datas) - ConfParam = self.env['ir.config_parameter'].sudo() - ptemp = ConfParam.get_param('path_temp_file') or '/tmp' - stamp = dt.utcnow().strftime('%H%M%S%f')[:-3] - ftemp = '%s/temp%s.xlsx' % (ptemp, stamp) - f = open(ftemp, 'wb') + ConfParam = self.env["ir.config_parameter"].sudo() + ptemp = ConfParam.get_param("path_temp_file") or "/tmp" + stamp = dt.utcnow().strftime("%H%M%S%f")[:-3] + ftemp = "{}/temp{}.xlsx".format(ptemp, stamp) + f = open(ftemp, "wb") f.write(decoded_data) f.seek(0) f.close() @@ -254,19 +256,18 @@ class XLSXExport(models.AbstractModel): wb.save(content) content.seek(0) # Set index to 0, and start reading out_file = base64.encodestring(content.read()) - if record and 'name' in record and record.name: - out_name = record.name.replace(' ', '').replace('/', '') + if record and "name" in record and record.name: + out_name = record.name.replace(" ", "").replace("/", "") else: - fname = out_name.replace(' ', '').replace('/', '') + fname = out_name.replace(" ", "").replace("/", "") ts = fields.Datetime.context_timestamp(self, dt.now()) - out_name = '%s_%s' % (fname, ts.strftime('%Y%m%d_%H%M%S')) + out_name = "{}_{}".format(fname, ts.strftime("%Y%m%d_%H%M%S")) if not out_name or len(out_name) == 0: - out_name = 'noname' - out_ext = 'xlsx' + out_name = "noname" + out_ext = "xlsx" # CSV (convert only on 1st sheet) if template.to_csv: delimiter = template.csv_delimiter - out_file = co.csv_from_excel(out_file, delimiter, - template.csv_quote) + out_file = co.csv_from_excel(out_file, delimiter, template.csv_quote) out_ext = template.csv_extension - return (out_file, '%s.%s' % (out_name, out_ext)) + return (out_file, "{}.{}".format(out_name, out_ext)) diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py index 389a0a544..9af7b4b4f 100644 --- a/excel_import_export/models/xlsx_import.py +++ b/excel_import_export/models/xlsx_import.py @@ -2,53 +2,61 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) import base64 -import uuid -import xlrd -import xlwt import time -from io import BytesIO -from . import common as co +import uuid from ast import literal_eval from datetime import date, datetime as dt -from odoo.tools.float_utils import float_compare -from odoo import models, api, _ +from io import BytesIO + +import xlrd +import xlwt + +from odoo import _, api, models from odoo.exceptions import ValidationError +from odoo.tools.float_utils import float_compare from odoo.tools.safe_eval import safe_eval +from . import common as co + class XLSXImport(models.AbstractModel): - _name = 'xlsx.import' - _description = 'Excel Import AbstractModel' + _name = "xlsx.import" + _description = "Excel Import AbstractModel" @api.model def get_eval_context(self, model=False, value=False): - eval_context = {'float_compare': float_compare, - 'time': time, - 'datetime': dt, - 'date': date, - 'env': self.env, - 'context': self._context, - 'value': False, - 'model': False, - } + eval_context = { + "float_compare": float_compare, + "time": time, + "datetime": dt, + "date": date, + "env": self.env, + "context": self._context, + "value": False, + "model": False, + } if model: - eval_context.update({'model': self.env[model]}) + eval_context.update({"model": self.env[model]}) if value: if isinstance(value, str): # Remove non Ord 128 character - value = ''.join([i if ord(i) < 128 else ' ' for i in value]) - eval_context.update({'value': value}) + value = "".join([i if ord(i) < 128 else " " for i in value]) + eval_context.update({"value": value}) return eval_context @api.model def get_external_id(self, record): """ Get external ID of the record, if not already exists create one """ - ModelData = self.env['ir.model.data'] + ModelData = self.env["ir.model.data"] xml_id = record.get_external_id() - if not xml_id or (record.id in xml_id and xml_id[record.id] == ''): - ModelData.create({'name': '%s_%s' % (record._table, record.id), - 'module': 'excel_import_export', - 'model': record._name, - 'res_id': record.id, }) + if not xml_id or (record.id in xml_id and xml_id[record.id] == ""): + ModelData.create( + { + "name": "{}_{}".format(record._table, record.id), + "module": "excel_import_export", + "model": record._name, + "res_id": record.id, + } + ) xml_id = record.get_external_id() return xml_id[record.id] @@ -56,15 +64,16 @@ class XLSXImport(models.AbstractModel): def _get_field_type(self, model, field): try: record = self.env[model].new() - for f in field.split('/'): + for f in field.split("/"): field_type = record._fields[f].type - if field_type in ('one2many', 'many2many'): + if field_type in ("one2many", "many2many"): record = record[f] else: return field_type except Exception: raise ValidationError( - _('Invalid declaration, %s has no valid field type') % field) + _("Invalid declaration, %s has no valid field type") % field + ) @api.model def _delete_record_data(self, record, data_dict): @@ -74,19 +83,19 @@ class XLSXImport(models.AbstractModel): try: for sheet_name in data_dict: worksheet = data_dict[sheet_name] - line_fields = filter(lambda x: x != '_HEAD_', worksheet) + line_fields = filter(lambda x: x != "_HEAD_", worksheet) for line_field in line_fields: - if '_NODEL_' not in line_field: + if "_NODEL_" not in line_field: if line_field in record and record[line_field]: record[line_field].unlink() # Remove _NODEL_ from dict for s, sv in data_dict.items(): for f, fv in data_dict[s].items(): - if '_NODEL_' in f: + if "_NODEL_" in f: new_fv = data_dict[s].pop(f) - data_dict[s][f.replace('_NODEL_', '')] = new_fv + data_dict[s][f.replace("_NODEL_", "")] = new_fv except Exception as e: - raise ValidationError(_('Error deleting data\n%s') % e) + raise ValidationError(_("Error deleting data\n%s") % e) @api.model def _get_line_vals(self, st, worksheet, model, line_field): @@ -99,20 +108,18 @@ class XLSXImport(models.AbstractModel): rc, key_eval_cond = co.get_field_condition(rc) x_field, val_eval_cond = co.get_field_condition(field) row, col = co.pos2idx(rc) - out_field = '%s/%s' % (line_field, x_field) + out_field = "{}/{}".format(line_field, x_field) field_type = self._get_field_type(model, out_field) vals.update({out_field: []}) for idx in range(row, st.nrows): - value = co._get_cell_value(st.cell(idx, col), - field_type=field_type) - eval_context = self.get_eval_context(model=model, - value=value) + value = co._get_cell_value(st.cell(idx, col), field_type=field_type) + eval_context = self.get_eval_context(model=model, value=value) if key_eval_cond: value = safe_eval(key_eval_cond, eval_context) if val_eval_cond: value = safe_eval(val_eval_cond, eval_context) vals[out_field].append(value) - if not filter(lambda x: x != '', vals[out_field]): + if not filter(lambda x: x != "", vals[out_field]): vals.pop(out_field) return vals @@ -128,11 +135,14 @@ class XLSXImport(models.AbstractModel): col_idx = 0 out_wb = xlwt.Workbook() out_st = out_wb.add_sheet("Sheet 1") - xml_id = record and self.get_external_id(record) or \ - '%s.%s' % ('xls', uuid.uuid4()) - out_st.write(0, 0, 'id') # id and xml_id on first column + xml_id = ( + record + and self.get_external_id(record) + or "{}.{}".format("xls", uuid.uuid4()) + ) + out_st.write(0, 0, "id") # id and xml_id on first column out_st.write(1, 0, xml_id) - header_fields.append('id') + header_fields.append("id") col_idx += 1 model = record._name for sheet_name in data_dict: # For each Sheet @@ -143,22 +153,21 @@ class XLSXImport(models.AbstractModel): elif isinstance(sheet_name, int): st = wb.sheet_by_index(sheet_name - 1) if not st: - raise ValidationError( - _('Sheet %s not found') % sheet_name) + raise ValidationError(_("Sheet %s not found") % sheet_name) # HEAD updates - for rc, field in worksheet.get('_HEAD_', {}).items(): + for rc, field in worksheet.get("_HEAD_", {}).items(): rc, key_eval_cond = co.get_field_condition(rc) field, val_eval_cond = co.get_field_condition(field) field_type = self._get_field_type(model, field) value = False try: row, col = co.pos2idx(rc) - value = co._get_cell_value(st.cell(row, col), - field_type=field_type) + value = co._get_cell_value( + st.cell(row, col), field_type=field_type + ) except Exception: pass - eval_context = self.get_eval_context(model=model, - value=value) + eval_context = self.get_eval_context(model=model, value=value) if key_eval_cond: value = str(safe_eval(key_eval_cond, eval_context)) if val_eval_cond: @@ -168,10 +177,9 @@ class XLSXImport(models.AbstractModel): header_fields.append(field) col_idx += 1 # Line Items - line_fields = filter(lambda x: x != '_HEAD_', worksheet) + line_fields = filter(lambda x: x != "_HEAD_", worksheet) for line_field in line_fields: - vals = self._get_line_vals(st, worksheet, - model, line_field) + vals = self._get_line_vals(st, worksheet, model, line_field) for field in vals: # Columns, i.e., line_ids/field_id out_st.write(0, col_idx, field) @@ -187,41 +195,47 @@ class XLSXImport(models.AbstractModel): content.seek(0) # Set index to 0, and start reading xls_file = content.read() # Do the import - Import = self.env['base_import.import'] - imp = Import.create({ - 'res_model': model, - 'file': xls_file, - 'file_type': 'application/vnd.ms-excel', - 'file_name': 'temp.xls', - }) + Import = self.env["base_import.import"] + imp = Import.create( + { + "res_model": model, + "file": xls_file, + "file_type": "application/vnd.ms-excel", + "file_name": "temp.xls", + } + ) errors = imp.do( header_fields, header_fields, - {'headers': True, - 'advanced': True, - 'keep_matches': False, - 'encoding': '', - 'separator': '', - 'quoting': '"', - 'date_format': '%Y-%m-%d', - 'datetime_format': '%Y-%m-%d %H:%M:%S', - 'float_thousand_separator': ',', - 'float_decimal_separator': '.', - 'fields': []}) - if errors.get('messages'): - message = _('Error importing data') - messages = errors['messages'] + { + "headers": True, + "advanced": True, + "keep_matches": False, + "encoding": "", + "separator": "", + "quoting": '"', + "date_format": "%Y-%m-%d", + "datetime_format": "%Y-%m-%d %H:%M:%S", + "float_thousand_separator": ",", + "float_decimal_separator": ".", + "fields": [], + }, + ) + if errors.get("messages"): + message = _("Error importing data") + messages = errors["messages"] if isinstance(messages, dict): - message = messages['message'] + message = messages["message"] if isinstance(messages, list): - message = ', '.join([x['message'] for x in messages]) - raise ValidationError(message.encode('utf-8')) + message = ", ".join([x["message"] for x in messages]) + raise ValidationError(message.encode("utf-8")) return self.env.ref(xml_id) except xlrd.XLRDError: raise ValidationError( - _('Invalid file style, only .xls or .xlsx file allowed')) + _("Invalid file style, only .xls or .xlsx file allowed") + ) except Exception as e: - raise ValidationError(_('Error importing data\n%s') % e) + raise ValidationError(_("Error importing data\n%s") % e) @api.model def _post_import_operation(self, record, operation): @@ -229,16 +243,15 @@ class XLSXImport(models.AbstractModel): if not record or not operation: return try: - if '${' in operation: - code = (operation.split('${'))[1].split('}')[0] - eval_context = {'object': record} + if "${" in operation: + code = (operation.split("${"))[1].split("}")[0] + eval_context = {"object": record} safe_eval(code, eval_context) except Exception as e: - raise ValidationError(_('Post import operation error\n%s') % e) + raise ValidationError(_("Post import operation error\n%s") % e) @api.model - def import_xlsx(self, import_file, template, - res_model=False, res_id=False): + def import_xlsx(self, import_file, template, res_model=False, res_id=False): """ - If res_id = False, we want to create new document first - Delete fields' data according to data_dict['__IMPORT__'] @@ -249,16 +262,16 @@ class XLSXImport(models.AbstractModel): raise ValidationError(_("Template's model mismatch")) record = self.env[template.res_model].browse(res_id) data_dict = literal_eval(template.instruction.strip()) - if not data_dict.get('__IMPORT__'): + if not data_dict.get("__IMPORT__"): raise ValidationError( - _("No data_dict['__IMPORT__'] in template %s") % template.name) + _("No data_dict['__IMPORT__'] in template %s") % template.name + ) if record: # Delete existing data first - self._delete_record_data(record, data_dict['__IMPORT__']) + self._delete_record_data(record, data_dict["__IMPORT__"]) # Fill up record with data from excel sheets - record = self._import_record_data(import_file, record, - data_dict['__IMPORT__']) + record = self._import_record_data(import_file, record, data_dict["__IMPORT__"]) # Post Import Operation, i.e., cleanup some data - if data_dict.get('__POST_IMPORT__', False): - self._post_import_operation(record, data_dict['__POST_IMPORT__']) + if data_dict.get("__POST_IMPORT__", False): + self._post_import_operation(record, data_dict["__POST_IMPORT__"]) return record diff --git a/excel_import_export/models/xlsx_report.py b/excel_import_export/models/xlsx_report.py index f123d2a65..fdc741458 100644 --- a/excel_import_export/models/xlsx_report.py +++ b/excel_import_export/models/xlsx_report.py @@ -1,69 +1,58 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import models, fields, api, _ +from odoo import _, api, fields, models from odoo.exceptions import ValidationError class XLSXReport(models.AbstractModel): """ Common class for xlsx reporting wizard """ - _name = 'xlsx.report' - _description = 'Excel Report AbstractModel' - name = fields.Char( - string='File Name', - readonly=True, - size=500, - ) - data = fields.Binary( - string='File', - readonly=True, - ) + _name = "xlsx.report" + _description = "Excel Report AbstractModel" + + name = fields.Char(string="File Name", readonly=True, size=500,) + data = fields.Binary(string="File", readonly=True,) template_id = fields.Many2one( - 'xlsx.template', - string='Template', + "xlsx.template", + string="Template", required=True, - ondelete='cascade', - domain=lambda self: self._context.get('template_domain', []), - ) - choose_template = fields.Boolean( - string='Allow Choose Template', - default=False, + ondelete="cascade", + domain=lambda self: self._context.get("template_domain", []), ) + choose_template = fields.Boolean(string="Allow Choose Template", default=False,) state = fields.Selection( - [('choose', 'Choose'), - ('get', 'Get')], - default='choose', + [("choose", "Choose"), ("get", "Get")], + default="choose", help="* Choose: wizard show in user selection mode" - "\n* Get: wizard show results from user action", + "\n* Get: wizard show results from user action", ) @api.model def default_get(self, fields): - template_domain = self._context.get('template_domain', []) - templates = self.env['xlsx.template'].search(template_domain) + template_domain = self._context.get("template_domain", []) + templates = self.env["xlsx.template"].search(template_domain) if not templates: - raise ValidationError(_('No template found')) + raise ValidationError(_("No template found")) defaults = super(XLSXReport, self).default_get(fields) for template in templates: if not template.datas: - raise ValidationError(_('No file in %s') % (template.name,)) - defaults['template_id'] = len(templates) == 1 and templates.id or False + raise ValidationError(_("No file in %s") % (template.name,)) + defaults["template_id"] = len(templates) == 1 and templates.id or False return defaults @api.multi def report_xlsx(self): self.ensure_one() - Export = self.env['xlsx.export'] - out_file, out_name = \ - Export.export_xlsx(self.template_id, self._name, self.id) - self.write({'state': 'get', 'data': out_file, 'name': out_name}) + Export = self.env["xlsx.export"] + out_file, out_name = Export.export_xlsx(self.template_id, self._name, self.id) + self.write({"state": "get", "data": out_file, "name": out_name}) return { - 'type': 'ir.actions.act_window', - 'res_model': self._name, - 'view_mode': 'form', - 'view_type': 'form', - 'res_id': self.id, - 'views': [(False, 'form')], - 'target': 'new', + "type": "ir.actions.act_window", + "res_model": self._name, + "view_mode": "form", + "view_type": "form", + "res_id": self.id, + "views": [(False, "form")], + "target": "new", } diff --git a/excel_import_export/models/xlsx_template.py b/excel_import_export/models/xlsx_template.py index 1460473a8..e34528042 100644 --- a/excel_import_export/models/xlsx_template.py +++ b/excel_import_export/models/xlsx_template.py @@ -1,14 +1,16 @@ # Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -import os import base64 +import os from ast import literal_eval -from odoo import api, fields, models, _ -from odoo.modules.module import get_module_path from os.path import join as opj -from . import common as co + +from odoo import _, api, fields, models from odoo.exceptions import ValidationError +from odoo.modules.module import get_module_path + +from . import common as co class XLSXTemplate(models.Model): @@ -17,105 +19,96 @@ class XLSXTemplate(models.Model): - Import/Export Meta Data (dict text) - Default values, etc. """ - _name = 'xlsx.template' - _description = 'Excel template file and instruction' - _order = 'name' - name = fields.Char( - string='Template Name', - required=True, - ) + _name = "xlsx.template" + _description = "Excel template file and instruction" + _order = "name" + + name = fields.Char(string="Template Name", required=True,) res_model = fields.Char( - string='Resource Model', + string="Resource Model", help="The database object this attachment will be attached to.", ) - fname = fields.Char( - string='File Name', - ) + fname = fields.Char(string="File Name",) gname = fields.Char( - string='Group Name', + string="Group Name", help="Multiple template of same model, can belong to same group,\n" "result in multiple template selection", ) - description = fields.Char( - string='Description', - ) + description = fields.Char(string="Description",) input_instruction = fields.Text( - string='Instruction (Input)', + string="Instruction (Input)", help="This is used to construct instruction in tab Import/Export", ) instruction = fields.Text( - string='Instruction', - compute='_compute_output_instruction', - help="Instruction on how to import/export, prepared by system." - ) - datas = fields.Binary( - string='File Content', - ) - to_csv = fields.Boolean( - string='Convert to CSV?', - default=False, + string="Instruction", + compute="_compute_output_instruction", + help="Instruction on how to import/export, prepared by system.", ) + datas = fields.Binary(string="File Content",) + to_csv = fields.Boolean(string="Convert to CSV?", default=False,) csv_delimiter = fields.Char( - string='CSV Delimiter', + string="CSV Delimiter", size=1, - default=',', + default=",", required=True, help="Optional for CSV, default is comma.", ) csv_extension = fields.Char( - string='CSV File Extension', + string="CSV File Extension", size=5, - default='csv', + default="csv", required=True, - help="Optional for CSV, default is .csv" + help="Optional for CSV, default is .csv", ) csv_quote = fields.Boolean( - string='CSV Quoting', + string="CSV Quoting", default=True, - help="Optional for CSV, default is full quoting." + help="Optional for CSV, default is full quoting.", ) export_ids = fields.One2many( - comodel_name='xlsx.template.export', - inverse_name='template_id', + comodel_name="xlsx.template.export", inverse_name="template_id", ) import_ids = fields.One2many( - comodel_name='xlsx.template.import', - inverse_name='template_id', + comodel_name="xlsx.template.import", inverse_name="template_id", ) post_import_hook = fields.Char( - string='Post Import Function Hook', + string="Post Import Function Hook", help="Call a function after successful import, i.e.,\n" "${object.post_import_do_something()}", ) show_instruction = fields.Boolean( - string='Show Output', + string="Show Output", default=False, help="This is the computed instruction based on tab Import/Export,\n" "to be used by xlsx import/export engine", ) redirect_action = fields.Many2one( - comodel_name='ir.actions.act_window', - string='Return Action', - domain=[('type', '=', 'ir.actions.act_window')], + comodel_name="ir.actions.act_window", + string="Return Action", + domain=[("type", "=", "ir.actions.act_window")], help="Optional action, redirection after finish import operation", ) @api.multi - @api.constrains('redirect_action', 'res_model') + @api.constrains("redirect_action", "res_model") def _check_action_model(self): for rec in self: - if rec.res_model and rec.redirect_action and \ - rec.res_model != rec.redirect_action.res_model: - raise ValidationError(_('The selected redirect action is ' - 'not for model %s') % rec.res_model) + if ( + rec.res_model + and rec.redirect_action + and rec.res_model != rec.redirect_action.res_model + ): + raise ValidationError( + _("The selected redirect action is " "not for model %s") + % rec.res_model + ) @api.model def load_xlsx_template(self, tempalte_ids, addon=False): for template in self.browse(tempalte_ids): if not addon: - addon = list(template.get_external_id(). - values())[0].split('.')[0] + addon = list(template.get_external_id().values())[0].split(".")[0] addon_path = get_module_path(addon) file_path = False for root, dirs, files in os.walk(addon_path): @@ -123,13 +116,13 @@ class XLSXTemplate(models.Model): if name == template.fname: file_path = os.path.abspath(opj(root, name)) if file_path: - template.datas = base64.b64encode(open(file_path, 'rb').read()) + template.datas = base64.b64encode(open(file_path, "rb").read()) return True @api.model def create(self, vals): rec = super().create(vals) - if vals.get('input_instruction'): + if vals.get("input_instruction"): rec._compute_input_export_instruction() rec._compute_input_import_instruction() rec._compute_input_post_import_hook() @@ -138,7 +131,7 @@ class XLSXTemplate(models.Model): @api.multi def write(self, vals): res = super().write(vals) - if vals.get('input_instruction'): + if vals.get("input_instruction"): for rec in self: rec._compute_input_export_instruction() rec._compute_input_import_instruction() @@ -152,7 +145,7 @@ class XLSXTemplate(models.Model): # Export Instruction input_dict = literal_eval(rec.input_instruction.strip()) rec.export_ids.unlink() - export_dict = input_dict.get('__EXPORT__') + export_dict = input_dict.get("__EXPORT__") if not export_dict: continue export_lines = [] @@ -160,34 +153,36 @@ class XLSXTemplate(models.Model): # Sheet for sheet, rows in export_dict.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'sheet', - 'sheet': str(sheet), - } + vals = { + "sequence": sequence, + "section_type": "sheet", + "sheet": str(sheet), + } export_lines.append((0, 0, vals)) # Rows for row_field, lines in rows.items(): sequence += 1 is_cont = False - if '_CONT_' in row_field: + if "_CONT_" in row_field: is_cont = True - row_field = row_field.replace('_CONT_', '') - vals = {'sequence': sequence, - 'section_type': (row_field == '_HEAD_' and - 'head' or 'row'), - 'row_field': row_field, - 'is_cont': is_cont, - } + row_field = row_field.replace("_CONT_", "") + vals = { + "sequence": sequence, + "section_type": (row_field == "_HEAD_" and "head" or "row"), + "row_field": row_field, + "is_cont": is_cont, + } export_lines.append((0, 0, vals)) for excel_cell, field_name in lines.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'data', - 'excel_cell': excel_cell, - 'field_name': field_name, - } + vals = { + "sequence": sequence, + "section_type": "data", + "excel_cell": excel_cell, + "field_name": field_name, + } export_lines.append((0, 0, vals)) - rec.write({'export_ids': export_lines}) + rec.write({"export_ids": export_lines}) @api.multi def _compute_input_import_instruction(self): @@ -196,7 +191,7 @@ class XLSXTemplate(models.Model): # Import Instruction input_dict = literal_eval(rec.input_instruction.strip()) rec.import_ids.unlink() - import_dict = input_dict.get('__IMPORT__') + import_dict = input_dict.get("__IMPORT__") if not import_dict: continue import_lines = [] @@ -204,34 +199,36 @@ class XLSXTemplate(models.Model): # Sheet for sheet, rows in import_dict.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'sheet', - 'sheet': str(sheet), - } + vals = { + "sequence": sequence, + "section_type": "sheet", + "sheet": str(sheet), + } import_lines.append((0, 0, vals)) # Rows for row_field, lines in rows.items(): sequence += 1 no_delete = False - if '_NODEL_' in row_field: + if "_NODEL_" in row_field: no_delete = True - row_field = row_field.replace('_NODEL_', '') - vals = {'sequence': sequence, - 'section_type': (row_field == '_HEAD_' and - 'head' or 'row'), - 'row_field': row_field, - 'no_delete': no_delete, - } + row_field = row_field.replace("_NODEL_", "") + vals = { + "sequence": sequence, + "section_type": (row_field == "_HEAD_" and "head" or "row"), + "row_field": row_field, + "no_delete": no_delete, + } import_lines.append((0, 0, vals)) for excel_cell, field_name in lines.items(): sequence += 1 - vals = {'sequence': sequence, - 'section_type': 'data', - 'excel_cell': excel_cell, - 'field_name': field_name, - } + vals = { + "sequence": sequence, + "section_type": "data", + "excel_cell": excel_cell, + "field_name": field_name, + } import_lines.append((0, 0, vals)) - rec.write({'import_ids': import_lines}) + rec.write({"import_ids": import_lines}) @api.multi def _compute_input_post_import_hook(self): @@ -239,7 +236,7 @@ class XLSXTemplate(models.Model): for rec in self: # Import Instruction input_dict = literal_eval(rec.input_instruction.strip()) - rec.post_import_hook = input_dict.get('__POST_IMPORT__') + rec.post_import_hook = input_dict.get("__POST_IMPORT__") @api.multi def _compute_output_instruction(self): @@ -249,62 +246,60 @@ class XLSXTemplate(models.Model): prev_sheet = False prev_row = False # Export Instruction - itype = '__EXPORT__' + itype = "__EXPORT__" inst_dict[itype] = {} for line in rec.export_ids: - if line.section_type == 'sheet': - sheet = co.isinteger(line.sheet) and \ - int(line.sheet) or line.sheet + if line.section_type == "sheet": + sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet sheet_dict = {sheet: {}} inst_dict[itype].update(sheet_dict) prev_sheet = sheet continue - if line.section_type in ('head', 'row'): + if line.section_type in ("head", "row"): row_field = line.row_field - if line.section_type == 'row' and line.is_cont: - row_field = '_CONT_%s' % row_field + if line.section_type == "row" and line.is_cont: + row_field = "_CONT_%s" % row_field row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field continue - if line.section_type == 'data': + if line.section_type == "data": excel_cell = line.excel_cell - field_name = line.field_name or '' - field_name += line.field_cond or '' - field_name += line.style or '' - field_name += line.style_cond or '' + field_name = line.field_name or "" + field_name += line.field_cond or "" + field_name += line.style or "" + field_name += line.style_cond or "" if line.is_sum: - field_name += '@{sum}' + field_name += "@{sum}" cell_dict = {excel_cell: field_name} inst_dict[itype][prev_sheet][prev_row].update(cell_dict) continue # Import Instruction - itype = '__IMPORT__' + itype = "__IMPORT__" inst_dict[itype] = {} for line in rec.import_ids: - if line.section_type == 'sheet': - sheet = co.isinteger(line.sheet) and \ - int(line.sheet) or line.sheet + if line.section_type == "sheet": + sheet = co.isinteger(line.sheet) and int(line.sheet) or line.sheet sheet_dict = {sheet: {}} inst_dict[itype].update(sheet_dict) prev_sheet = sheet continue - if line.section_type in ('head', 'row'): + if line.section_type in ("head", "row"): row_field = line.row_field - if line.section_type == 'row' and line.no_delete: - row_field = '_NODEL_%s' % row_field + if line.section_type == "row" and line.no_delete: + row_field = "_NODEL_%s" % row_field row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field continue - if line.section_type == 'data': + if line.section_type == "data": excel_cell = line.excel_cell - field_name = line.field_name or '' - field_name += line.field_cond or '' + field_name = line.field_name or "" + field_name += line.field_cond or "" cell_dict = {excel_cell: field_name} inst_dict[itype][prev_sheet][prev_row].update(cell_dict) continue - itype = '__POST_IMPORT__' + itype = "__POST_IMPORT__" inst_dict[itype] = False if rec.post_import_hook: inst_dict[itype] = rec.post_import_hook @@ -312,51 +307,36 @@ class XLSXTemplate(models.Model): class XLSXTemplateImport(models.Model): - _name = 'xlsx.template.import' - _description = 'Detailed of how excel data will be imported' - _order = 'sequence' + _name = "xlsx.template.import" + _description = "Detailed of how excel data will be imported" + _order = "sequence" template_id = fields.Many2one( - comodel_name='xlsx.template', - string='XLSX Template', + comodel_name="xlsx.template", + string="XLSX Template", index=True, - ondelete='cascade', + ondelete="cascade", readonly=True, ) - sequence = fields.Integer( - string='Sequence', - default=10, - ) - sheet = fields.Char( - string='Sheet', - ) + sequence = fields.Integer(string="Sequence", default=10,) + sheet = fields.Char(string="Sheet",) section_type = fields.Selection( - [('sheet', 'Sheet'), - ('head', 'Head'), - ('row', 'Row'), - ('data', 'Data')], - string='Section Type', + [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], + string="Section Type", required=True, ) row_field = fields.Char( - string='Row Field', - help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required", ) no_delete = fields.Boolean( - string='No Delete', + string="No Delete", default=False, help="By default, all rows will be deleted before import.\n" - "Select No Delete, otherwise" - ) - excel_cell = fields.Char( - string='Cell', - ) - field_name = fields.Char( - string='Field', - ) - field_cond = fields.Char( - string='Field Cond.', + "Select No Delete, otherwise", ) + excel_cell = fields.Char(string="Cell",) + field_name = fields.Char(string="Field",) + field_cond = fields.Char(string="Field Cond.",) @api.model def create(self, vals): @@ -365,70 +345,46 @@ class XLSXTemplateImport(models.Model): @api.model def _extract_field_name(self, vals): - if self._context.get('compute_from_input') and vals.get('field_name'): - field_name, field_cond = co.get_field_condition(vals['field_name']) - field_cond = field_cond and '${%s}' % (field_cond or '') or False - vals.update({'field_name': field_name, - 'field_cond': field_cond, - }) + if self._context.get("compute_from_input") and vals.get("field_name"): + field_name, field_cond = co.get_field_condition(vals["field_name"]) + field_cond = field_cond and "${%s}" % (field_cond or "") or False + vals.update( + {"field_name": field_name, "field_cond": field_cond,} + ) return vals class XLSXTemplateExport(models.Model): - _name = 'xlsx.template.export' - _description = 'Detailed of how excel data will be exported' - _order = 'sequence' + _name = "xlsx.template.export" + _description = "Detailed of how excel data will be exported" + _order = "sequence" template_id = fields.Many2one( - comodel_name='xlsx.template', - string='XLSX Template', + comodel_name="xlsx.template", + string="XLSX Template", index=True, - ondelete='cascade', + ondelete="cascade", readonly=True, ) - sequence = fields.Integer( - string='Sequence', - default=10, - ) - sheet = fields.Char( - string='Sheet', - ) + sequence = fields.Integer(string="Sequence", default=10,) + sheet = fields.Char(string="Sheet",) section_type = fields.Selection( - [('sheet', 'Sheet'), - ('head', 'Head'), - ('row', 'Row'), - ('data', 'Data')], - string='Section Type', + [("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")], + string="Section Type", required=True, ) row_field = fields.Char( - string='Row Field', - help="If section type is row, this field is required", + string="Row Field", help="If section type is row, this field is required", ) is_cont = fields.Boolean( - string='Continue', - default=False, - help="Continue data rows after last data row", - ) - excel_cell = fields.Char( - string='Cell', - ) - field_name = fields.Char( - string='Field', - ) - field_cond = fields.Char( - string='Field Cond.', - ) - is_sum = fields.Boolean( - string='Sum', - default=False, - ) - style = fields.Char( - string='Default Style', - ) - style_cond = fields.Char( - string='Style w/Cond.', + string="Continue", default=False, help="Continue data rows after last data row", ) + excel_cell = fields.Char(string="Cell",) + field_name = fields.Char(string="Field",) + field_cond = fields.Char(string="Field Cond.",) + is_sum = fields.Boolean(string="Sum", default=False,) + style = fields.Char(string="Default Style",) + style_cond = fields.Char(string="Style w/Cond.",) @api.model def create(self, vals): @@ -437,16 +393,19 @@ class XLSXTemplateExport(models.Model): @api.model def _extract_field_name(self, vals): - if self._context.get('compute_from_input') and vals.get('field_name'): - field_name, field_cond = co.get_field_condition(vals['field_name']) + if self._context.get("compute_from_input") and vals.get("field_name"): + field_name, field_cond = co.get_field_condition(vals["field_name"]) field_cond = field_cond or 'value or ""' field_name, style = co.get_field_style(field_name) field_name, style_cond = co.get_field_style_cond(field_name) field_name, func = co.get_field_aggregation(field_name) - vals.update({'field_name': field_name, - 'field_cond': '${%s}' % (field_cond or ''), - 'style': '#{%s}' % (style or ''), - 'style_cond': '#?%s?' % (style_cond or ''), - 'is_sum': func == 'sum' and True or False, - }) + vals.update( + { + "field_name": field_name, + "field_cond": "${%s}" % (field_cond or ""), + "style": "#{%s}" % (style or ""), + "style_cond": "#?%s?" % (style_cond or ""), + "is_sum": func == "sum" and True or False, + } + ) return vals diff --git a/excel_import_export/static/src/js/report/action_manager_report.js b/excel_import_export/static/src/js/report/action_manager_report.js index 303df50a0..6b1330705 100644 --- a/excel_import_export/static/src/js/report/action_manager_report.js +++ b/excel_import_export/static/src/js/report/action_manager_report.js @@ -1,6 +1,6 @@ // Copyright 2019 Ecosoft Co., Ltd. // License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). -odoo.define("excel_import_export.report", function (require) { +odoo.define("excel_import_export.report", function(require) { "use strict"; var core = require("web.core"); @@ -11,23 +11,27 @@ odoo.define("excel_import_export.report", function (require) { var _t = core._t; ActionManager.include({ - - _downloadReportExcel: function (url, actions) { + _downloadReportExcel: function(url, actions) { framework.blockUI(); var def = $.Deferred(); var type = "excel"; var cloned_action = _.clone(actions); - if (_.isUndefined(cloned_action.data) || + if ( + _.isUndefined(cloned_action.data) || _.isNull(cloned_action.data) || - (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data))) - { + (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data)) + ) { if (cloned_action.context.active_ids) { - url += "/" + cloned_action.context.active_ids.join(','); + url += "/" + cloned_action.context.active_ids.join(","); } } else { - url += "?options=" + encodeURIComponent(JSON.stringify(cloned_action.data)); - url += "&context=" + encodeURIComponent(JSON.stringify(cloned_action.context)); + url += + "?options=" + + encodeURIComponent(JSON.stringify(cloned_action.data)); + url += + "&context=" + + encodeURIComponent(JSON.stringify(cloned_action.context)); } var blocked = !session.get_file({ @@ -36,7 +40,7 @@ odoo.define("excel_import_export.report", function (require) { data: JSON.stringify([url, type]), }, success: def.resolve.bind(def), - error: function () { + error: function() { crash_manager.rpc_error.apply(crash_manager, arguments); def.reject(); }, @@ -46,43 +50,48 @@ odoo.define("excel_import_export.report", function (require) { // AAB: this check should be done in get_file service directly, // should not be the concern of the caller (and that way, get_file // could return a deferred) - var message = _t('A popup window with your report was blocked. You ' + - 'may need to change your browser settings to allow ' + - 'popup windows for this page.'); - this.do_warn(_t('Warning'), message, true); + var message = _t( + "A popup window with your report was blocked. You " + + "may need to change your browser settings to allow " + + "popup windows for this page." + ); + this.do_warn(_t("Warning"), message, true); } return def; }, - _triggerDownload: function (action, options, type) { + _triggerDownload: function(action, options, type) { var self = this; var reportUrls = this._makeReportUrls(action); if (type === "excel") { - return this._downloadReportExcel(reportUrls[type], action).then(function () { - if (action.close_on_report_download) { - var closeAction = {type: 'ir.actions.act_window_close'}; - return self.doAction(closeAction, _.pick(options, 'on_close')); - } else { + return this._downloadReportExcel(reportUrls[type], action).then( + function() { + if (action.close_on_report_download) { + var closeAction = {type: "ir.actions.act_window_close"}; + return self.doAction( + closeAction, + _.pick(options, "on_close") + ); + } return options.on_close(); } - }); + ); } return this._super.apply(this, arguments); }, - _makeReportUrls: function (action) { + _makeReportUrls: function(action) { var reportUrls = this._super.apply(this, arguments); - reportUrls.excel = '/report/excel/' + action.report_name; + reportUrls.excel = "/report/excel/" + action.report_name; return reportUrls; }, - _executeReportAction: function (action, options) { + _executeReportAction: function(action, options) { var self = this; - if (action.report_type === 'excel') { - return self._triggerDownload(action, options, 'excel'); + if (action.report_type === "excel") { + return self._triggerDownload(action, options, "excel"); } return this._super.apply(this, arguments); - } + }, }); - }); diff --git a/excel_import_export/views/webclient_templates.xml b/excel_import_export/views/webclient_templates.xml index 96cdbdb22..db11d018c 100644 --- a/excel_import_export/views/webclient_templates.xml +++ b/excel_import_export/views/webclient_templates.xml @@ -1,11 +1,14 @@