From 02569c1f57f3969fb82507d3d53646f1ee6633c5 Mon Sep 17 00:00:00 2001 From: Kitti U Date: Fri, 15 Mar 2019 06:40:06 +0700 Subject: [PATCH] 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 ++ excel_import_export_demo/README.rst | 112 ++++ excel_import_export_demo/__init__.py | 5 + excel_import_export_demo/__manifest__.py | 22 + .../import_export_sale_order/actions.xml | 32 ++ .../import_export_sale_order/sale_order.xlsx | Bin 0 -> 5328 bytes .../import_export_sale_order/templates.xml | 52 ++ .../import_sale_orders/import_sale_order.xlsx | Bin 0 -> 5328 bytes .../import_sale_orders/menu_action.xml | 25 + .../import_sale_orders/templates.xml | 38 ++ .../readme/CONTRIBUTORS.rst | 1 + .../readme/DESCRIPTION.rst | 5 + excel_import_export_demo/readme/HISTORY.rst | 4 + excel_import_export_demo/readme/INSTALL.rst | 3 + excel_import_export_demo/readme/USAGE.rst | 11 + .../report_sale_order/__init__.py | 4 + .../report_sale_order/report_sale_order.py | 35 ++ .../report_sale_order/report_sale_order.xlsx | Bin 0 -> 5337 bytes .../report_sale_order/report_sale_order.xml | 41 ++ .../report_sale_order/templates.xml | 36 ++ .../static/description/index.html | 455 ++++++++++++++++ excel_import_export_demo/tests/__init__.py | 5 + .../tests/sale_order.xlsx | Bin 0 -> 5328 bytes excel_import_export_demo/tests/test_common.py | 130 +++++ .../tests/test_xlsx_import_export.py | 48 ++ .../tests/test_xlsx_report.py | 29 + .../tests/test_xlsx_template.py | 62 +++ requirements.txt | 5 +- 52 files changed, 3945 insertions(+), 1 deletion(-) 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 create mode 100644 excel_import_export_demo/README.rst create mode 100644 excel_import_export_demo/__init__.py create mode 100644 excel_import_export_demo/__manifest__.py create mode 100644 excel_import_export_demo/import_export_sale_order/actions.xml create mode 100644 excel_import_export_demo/import_export_sale_order/sale_order.xlsx create mode 100644 excel_import_export_demo/import_export_sale_order/templates.xml create mode 100644 excel_import_export_demo/import_sale_orders/import_sale_order.xlsx create mode 100644 excel_import_export_demo/import_sale_orders/menu_action.xml create mode 100644 excel_import_export_demo/import_sale_orders/templates.xml create mode 100644 excel_import_export_demo/readme/CONTRIBUTORS.rst create mode 100644 excel_import_export_demo/readme/DESCRIPTION.rst create mode 100644 excel_import_export_demo/readme/HISTORY.rst create mode 100644 excel_import_export_demo/readme/INSTALL.rst create mode 100644 excel_import_export_demo/readme/USAGE.rst create mode 100644 excel_import_export_demo/report_sale_order/__init__.py create mode 100644 excel_import_export_demo/report_sale_order/report_sale_order.py create mode 100644 excel_import_export_demo/report_sale_order/report_sale_order.xlsx create mode 100644 excel_import_export_demo/report_sale_order/report_sale_order.xml create mode 100644 excel_import_export_demo/report_sale_order/templates.xml create mode 100644 excel_import_export_demo/static/description/index.html create mode 100644 excel_import_export_demo/tests/__init__.py create mode 100644 excel_import_export_demo/tests/sale_order.xlsx create mode 100644 excel_import_export_demo/tests/test_common.py create mode 100644 excel_import_export_demo/tests/test_xlsx_import_export.py create mode 100644 excel_import_export_demo/tests/test_xlsx_report.py create mode 100644 excel_import_export_demo/tests/test_xlsx_template.py 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! +

+
+
+
+
+
+ +
+
+ +
diff --git a/excel_import_export_demo/README.rst b/excel_import_export_demo/README.rst new file mode 100644 index 000000000..17d98a14a --- /dev/null +++ b/excel_import_export_demo/README.rst @@ -0,0 +1,112 @@ +======================== +Excel Import/Export Demo +======================== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! 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_demo + :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_demo + :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| + +This module provide some example use case for excel_import_export + +1. Import/Export Sales Order (import_export_sale_order) +2. Import New Sales Orders (import_sale_orders) +3. Sales Orders Report (report_sale_order) + +**Table of contents** + +.. contents:: + :local: + +Installation +============ + +To install this module, you need to install **excel_import_export** + +Then, simply install **excel_import_export_demo**. + +Usage +===== + +**Use Case 1:** Export/Import Excel on existing document + +To test this use case, go to any Sales Order and use Export Excel or Import Excel in action menu. + +**Use Case 2:** Import Excel Files + +To test this use case, go to Settings > Excel Import/Export > Sample Import Sales Order + +**Use Case 3:** Create Excel Report + +To test this use case, go to Settings > Excel Import/Export > Sample Sales Report + +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_demo/__init__.py b/excel_import_export_demo/__init__.py new file mode 100644 index 000000000..1334da2f4 --- /dev/null +++ b/excel_import_export_demo/__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 import_export_sale_order +from . import report_sale_order diff --git a/excel_import_export_demo/__manifest__.py b/excel_import_export_demo/__manifest__.py new file mode 100644 index 000000000..9463f279f --- /dev/null +++ b/excel_import_export_demo/__manifest__.py @@ -0,0 +1,22 @@ +# 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 Demo', + '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': ['excel_import_export', + 'sale_management'], + 'data': ['import_export_sale_order/actions.xml', + 'import_export_sale_order/templates.xml', + 'report_sale_order/report_sale_order.xml', + 'report_sale_order/templates.xml', + 'import_sale_orders/menu_action.xml', + 'import_sale_orders/templates.xml', + ], + 'installable': True, + 'development_status': 'alpha', + 'maintainers': ['kittiu'], + } diff --git a/excel_import_export_demo/import_export_sale_order/actions.xml b/excel_import_export_demo/import_export_sale_order/actions.xml new file mode 100644 index 000000000..bbff83122 --- /dev/null +++ b/excel_import_export_demo/import_export_sale_order/actions.xml @@ -0,0 +1,32 @@ + + + + + + + + diff --git a/excel_import_export_demo/import_export_sale_order/sale_order.xlsx b/excel_import_export_demo/import_export_sale_order/sale_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..18618688be4f5aeade17e3f3911e81ef305688b6 GIT binary patch literal 5328 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 + + + + + sale.order + sale_order.xlsx + Sale Order Template + Sample Sales Order Tempalte for testing + + { + '__EXPORT__': { + 'sale_order': { + '_HEAD_': { + 'B2': 'partner_id.display_name${value or ""}#{align=left;style=text}', + 'B3': 'name${value or ""}#{align=left;style=text}', + }, + 'order_line': { + 'A6': 'product_id.display_name${value or ""}#{style=text}', + 'B6': 'name${value or ""}#{style=text}', + 'C6': 'product_uom_qty${value or 0}#{style=number}', + 'D6': 'product_uom.name${value or ""}#{style=text}', + 'E6': 'price_unit${value or 0}#{style=number}', + 'F6': 'tax_id${value and ",".join([x.display_name for x in value]) or ""}', + 'G6': 'price_subtotal${value or 0}#{style=number}', + } + } + }, + '__IMPORT__': { + 'sale_order': { + '_NODEL_order_line': { + 'A6': 'product_id', + 'B6': 'name', + 'C6': 'product_uom_qty', + 'D6': 'product_uom', + 'E6': 'price_unit', + 'F6': 'tax_id', + } + } + }, + # '__POST_IMPORT__': '${object.post_import_do_something()}', + } + + + + + + + + diff --git a/excel_import_export_demo/import_sale_orders/import_sale_order.xlsx b/excel_import_export_demo/import_sale_orders/import_sale_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..18618688be4f5aeade17e3f3911e81ef305688b6 GIT binary patch literal 5328 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 + + + + + Sample Import Sale Order + import.xlsx.wizard + form + form + new + { + 'template_domain': [('res_model', '=', 'sale.order'), + ('fname', '=', 'import_sale_order.xlsx'), + ('gname', '=', False)], } + + + + + + diff --git a/excel_import_export_demo/import_sale_orders/templates.xml b/excel_import_export_demo/import_sale_orders/templates.xml new file mode 100644 index 000000000..b5fad027c --- /dev/null +++ b/excel_import_export_demo/import_sale_orders/templates.xml @@ -0,0 +1,38 @@ + + + + + + sale.order + import_sale_order.xlsx + Import Sale Order Template + Sample Import Sales Order Tempalte for testing + + + { + '__IMPORT__': { + 'sale_order': { + '_HEAD_': { + 'B2': 'partner_id', + }, + 'order_line': { + 'A6': 'product_id', + 'B6': 'name', + 'C6': 'product_uom_qty', + 'D6': 'product_uom', + 'E6': 'price_unit', + 'F6': 'tax_id', + } + } + }, + } + + + + + + + + diff --git a/excel_import_export_demo/readme/CONTRIBUTORS.rst b/excel_import_export_demo/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..9c7a518b1 --- /dev/null +++ b/excel_import_export_demo/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Kitti Upariphutthiphong. (http://ecosoft.co.th) diff --git a/excel_import_export_demo/readme/DESCRIPTION.rst b/excel_import_export_demo/readme/DESCRIPTION.rst new file mode 100644 index 000000000..e1c00d558 --- /dev/null +++ b/excel_import_export_demo/readme/DESCRIPTION.rst @@ -0,0 +1,5 @@ +This module provide some example use case for excel_import_export + +1. Import/Export Sales Order (import_export_sale_order) +2. Import New Sales Orders (import_sale_orders) +3. Sales Orders Report (report_sale_order) diff --git a/excel_import_export_demo/readme/HISTORY.rst b/excel_import_export_demo/readme/HISTORY.rst new file mode 100644 index 000000000..b8d1b41b6 --- /dev/null +++ b/excel_import_export_demo/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_demo/readme/INSTALL.rst b/excel_import_export_demo/readme/INSTALL.rst new file mode 100644 index 000000000..d428d7850 --- /dev/null +++ b/excel_import_export_demo/readme/INSTALL.rst @@ -0,0 +1,3 @@ +To install this module, you need to install **excel_import_export** + +Then, simply install **excel_import_export_demo**. diff --git a/excel_import_export_demo/readme/USAGE.rst b/excel_import_export_demo/readme/USAGE.rst new file mode 100644 index 000000000..cdf6bdeff --- /dev/null +++ b/excel_import_export_demo/readme/USAGE.rst @@ -0,0 +1,11 @@ +**Use Case 1:** Export/Import Excel on existing document + +To test this use case, go to any Sales Order and use Export Excel or Import Excel in action menu. + +**Use Case 2:** Import Excel Files + +To test this use case, go to Settings > Excel Import/Export > Sample Import Sales Order + +**Use Case 3:** Create Excel Report + +To test this use case, go to Settings > Excel Import/Export > Sample Sales Report diff --git a/excel_import_export_demo/report_sale_order/__init__.py b/excel_import_export_demo/report_sale_order/__init__.py new file mode 100644 index 000000000..03a1b8b20 --- /dev/null +++ b/excel_import_export_demo/report_sale_order/__init__.py @@ -0,0 +1,4 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from . import report_sale_order diff --git a/excel_import_export_demo/report_sale_order/report_sale_order.py b/excel_import_export_demo/report_sale_order/report_sale_order.py new file mode 100644 index 000000000..215748968 --- /dev/null +++ b/excel_import_export_demo/report_sale_order/report_sale_order.py @@ -0,0 +1,35 @@ +# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) + +from openerp import models, fields, api + + +class ReportSaleOrder(models.TransientModel): + _name = 'report.sale.order' + _description = 'Wizard for report.sale.order' + _inherit = 'xlsx.report' + + # Search Criteria + partner_id = fields.Many2one( + 'res.partner', + string='Partner', + ) + # Report Result, sale.order + results = fields.Many2many( + 'sale.order', + string='Results', + compute='_compute_results', + help='Use compute fields, so there is nothing stored in database', + ) + + @api.multi + def _compute_results(self): + """ On the wizard, result will be computed and added to results line + before export to excel, by using xlsx.export + """ + self.ensure_one() + Result = self.env['sale.order'] + domain = [] + if self.partner_id: + domain += [('partner_id', '=', self.partner_id.id)] + self.results = Result.search(domain) diff --git a/excel_import_export_demo/report_sale_order/report_sale_order.xlsx b/excel_import_export_demo/report_sale_order/report_sale_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..6e5cf8fa6128c91cc0d16cbe0958002e08e18327 GIT binary patch literal 5337 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 XgDMK>2pW-)un~_q;#h)+Z$AAW_5?93 literal 0 HcmV?d00001 diff --git a/excel_import_export_demo/report_sale_order/report_sale_order.xml b/excel_import_export_demo/report_sale_order/report_sale_order.xml new file mode 100644 index 000000000..92b1a147d --- /dev/null +++ b/excel_import_export_demo/report_sale_order/report_sale_order.xml @@ -0,0 +1,41 @@ + + + + + + report.sale.order + report.sale.order + + primary + + + + + + + + + + + + + Sample Sales Report + report.sale.order + form + form + new + + {'template_domain': [('res_model', '=', 'report.sale.order'), + ('fname', '=', 'report_sale_order.xlsx'), + ('gname', '=', False)]} + + + + + + diff --git a/excel_import_export_demo/report_sale_order/templates.xml b/excel_import_export_demo/report_sale_order/templates.xml new file mode 100644 index 000000000..45ee33c6d --- /dev/null +++ b/excel_import_export_demo/report_sale_order/templates.xml @@ -0,0 +1,36 @@ + + + + + + report.sale.order + report_sale_order.xlsx + Report Sale Order Template + Sample Report Sales Order Tempalte for testing + + { + '__EXPORT__': { + 1: { + '_HEAD_': { + 'B2': 'partner_id.display_name${value or ""}#{align=left;style=text}', + }, + 'results': { + 'A5': 'name${value or ""}#{style=text}', + 'B5': 'confirmation_date${value or ""}#{style=date}', + 'C5': 'amount_untaxed${value or 0}#{style=number}@{sum}', + 'D5': 'amount_tax${value or 0}#{style=number}@{sum}', + 'E5': 'amount_total${value or 0}#{style=number}@{sum}', + }, + }, + }, + } + + + + + + + + diff --git a/excel_import_export_demo/static/description/index.html b/excel_import_export_demo/static/description/index.html new file mode 100644 index 000000000..5ac32b4e4 --- /dev/null +++ b/excel_import_export_demo/static/description/index.html @@ -0,0 +1,455 @@ + + + + + + +Excel Import/Export Demo + + + +
+

Excel Import/Export Demo

+ + +

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

+

This module provide some example use case for excel_import_export

+
    +
  1. Import/Export Sales Order (import_export_sale_order)
  2. +
  3. Import New Sales Orders (import_sale_orders)
  4. +
  5. Sales Orders Report (report_sale_order)
  6. +
+

Table of contents

+ +
+

Installation

+

To install this module, you need to install excel_import_export

+

Then, simply install excel_import_export_demo.

+
+
+

Usage

+

Use Case 1: Export/Import Excel on existing document

+

To test this use case, go to any Sales Order and use Export Excel or Import Excel in action menu.

+

Use Case 2: Import Excel Files

+

To test this use case, go to Settings > Excel Import/Export > Sample Import Sales Order

+

Use Case 3: Create Excel Report

+

To test this use case, go to Settings > Excel Import/Export > Sample Sales Report

+
+
+

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_demo/tests/__init__.py b/excel_import_export_demo/tests/__init__.py new file mode 100644 index 000000000..44c33f918 --- /dev/null +++ b/excel_import_export_demo/tests/__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 test_xlsx_template +from . import test_xlsx_import_export +from . import test_xlsx_report diff --git a/excel_import_export_demo/tests/sale_order.xlsx b/excel_import_export_demo/tests/sale_order.xlsx new file mode 100644 index 0000000000000000000000000000000000000000..18618688be4f5aeade17e3f3911e81ef305688b6 GIT binary patch literal 5328 zcmaJ_cRbte*S3k()OZk7%~GwPwQ5z37)4OjrdEgwV%FZFHf>RRv_-{;QWT}Am6{I~ zHHzAMQ{#>2d7r0!`~H4C_aEORpYJ*O+}C~X>zwOAHHb+V2q-8h2yR^x)gt)GK={wz zPGAc+TPGyg3*iQTfIz^7ydOFVo&Qprs0sfdOdYX)!l<&}rbccZBiRt6P=LR~k(#VHk0iXg&6A=b_+)MMD~ka(Z(<6@ z8(+>rSMrYja}bjn*ch`dX(aTZB|+M(pi^C|Xr!|)bX5o3y`?soL5OA4D>Wgmau$Tv zBU{eOtw(5RE%JS{g*#{fShq`wH~MwsA4~~?Mag#_9)6SkCeOwGnY*hjG+S5P3-G6f z-KF(MR0zOWDKuc^m}h&dCQZ4tY+~Gx2&zF&p}leVXaK)&ULpbl=zm3q;csM+Fe^7( z8$EY72WNZaIWVyU6w?$|x8N(ZY0*U!OF0%}7&*HsR0Cjn8 zjH}%DXV=Dd#k7_}(iHQ|!AzM3s&X{JpBm&=EY`BC9uR^o)x#PZIhWtmV|wx&1zpP^ zi9WW}-!F1+aA8$<=TqEBihBLTW$eR(AxcrnJ_~gadd>s`ImRTAi5^vWZ^-k>5DZ(! z3BYKRy3uY#Am%Uxw`zH3H;cSy7=@X8e)>TooVQKo*kHOM(HRMfe#ZAbW(ApAxK`U4 z$Z#3p25%v!%o|W^;Bx8wJb}SB*KEp~CO)4ZA#sun{*xA8Hx;uzD@2%Kjtfw_B+rhIJc}>JB&@pc z(;K<*vZNm$H6+D~pcV@<#nS*P5W{^oc7GT4hO;a^``B=vjM#DKk6!FO53+#Op!)3f z`Lh^P$kkRlC8-bA`v8soli4!ZhqnA4VFOwoOScs}b;c`MS7xZ^RXx3fS#}CcXH^yS zUdtF!#L(T~e`!ATofr%WtJRO7-%MhY(Nv4k_A#AHExH=@ojyRNTY1}=G-U8Usl(mqTYoOTf-#;ztwxQE0GTSGAK$Q{+R+QdFJR zYis#CrwWrESyH|050si;px0|~hIR*>#WZF)SIj{sOXdwGPc!5lRp)niMbnI6p;QH4 z2Bj??z#O<~31Dp1@fCNIz6RZz!QCs)fs;bmFDvZ#S8#xh6C${w!u`JBh7;-lL?X`0 z+^F_F{4VDNv|7ZaGCjBkA`59ccn6Wq-I!y~({GeiM_c z$9fBsTT7EU))!9WUzMwU{S^*rzI;`H#A>Ndxb0Q4f-AZH4w~6?m9klpM18<6&`lvr zN)PoZr==~=L+VZ%TGh$RTi?t#sx_KGMC@DMBuiX`m6_sNI@C5zRGvBl%aIYzYgm35 zziabCWt>-4%CNM0WN65iFx)h_jol}rWdHiX&{dEgSHVExSN$}wtH*fyQGMf54fcg3 zl09?Jn0YM>@!3COR7{eSwoDrXsQYTI5mNp3ife!8OIQ+s7Qu?Q^ z#p-oBj|l@C_612M9$q(hWPM6(7?(gW4t&{JkyDV^md-`$_U_F>lb4c_L-bx>D9@)B z34~qNQ;B)wM-X8H7$e4oS@^;|G*zyr$oH*OTDu-hhkkRR0T4rV-xKo)BjbSSt(x$4 zw-`;;M`2s`ADSy*SD_L){JM^MNS?tJzcJ>i@KS>ErvVHkdQEe8Drl{&PRSLY6?RzV zZ!6MJN&|_LxGQR&z^<8G*WmnG1n-#9+)f-(lKv`cf4UUDXFp>rGs%&jQfY1A<`4`j z-C5Mzqj4*Udz;cDD0xTnLc4z#2M{EX+?9awkO4@JPHqmnqr7mabz{zFV_#qb))8|q zr7B7~vuDvxV+yicJ$s+FG0 zkM^ovauE7S%YAocHp-V;akqDkAA`0mJZ3$I17*LFIo5`i;TSB9>t>t4uj&RVbr1Mo zckon*Qz;N4EtF{AMCLt3;(~7rr(`dLa)GS6yKf&VFp*U7Rl$#K6%2ie>7*{QlPP>| zXdkaMjKfXT@K+cxq9(byzTaC^6)~6+ z@ysd7=P0@08hu!~i#Az})e+`rIdTsJ3PG?J?#8=ubpe*`N@Ok2hRs#-?2?+?? zh<}$jH2+)t-F=*F|El_{MmmTwabUn?8StP{z1pb_WWyLsMdQm7Ek;U4?VH1E%%`46 zq+H_}c;cORO+zS+*F5M}C?g5o`i%enrgsW6pHF>R>V9}f1ZAUAvIOB4MX$1gPpwk+ zrOIrk4miUcsa;4i@2v!~2F*ZqkcZ{$%v0bu2eOH4{RTxxOp}PM>qRcxDnns#8*ZEM zC(d012uC%S{vf@!V+Wv8K)3+Jp~Xwzps2mOEl%)yzJ$-*o=I+OKebPUGcV=!Z-y;V zt<;AO(_SPFl^2g&Z$R1;_^=Y|RCeh+L2GP=tyPn1j?Ch3^HwZl^n_N?*hqV!$n`-_6z#;1h#9HkA5I_U6gI|D3F zOGSK0+X|;r{fnQ$LX8xmh)3YYt$Yzp=23ojl>s*@FJ4C|cM9E;0Lh~u;D;~r3mZ~a zE8MR?$V)UtIOM49KVKiAL9jmZ3Ef-k7xthr9a%{LTy~)Pz`Qml#62jjqG6thiQ_1M z@))kd3&dUmUu0* z-kx4saJ(Jb0lTn#^UntA_^9I>0&!e5Rek8ttkVGG)1s-Amz{+^OYT`$9ATI2mhO|4 zL3(!h+BxT@-+0g?|5;I~M&6nhU8A$bLp^A9>`wYntNi^K`7e#nvB#Di@=flnO_P3Q zey`@`)dL^;9_irhdNsznH4n~s;e#f_0p7!Gfk^qRDpOXJpR$zi=1# ztO(WjeyMqLobH5nzQ8G=dN)o~*=MB7EA~=HLj}!)%)!@6`d_)bZRGL**=jQ65CywRT2gB1>tOx(bX7b9pKx~rnvVX80WngPIgS|?Z?bG<41zwS{x<7 z5!LZ03!ozDk)Y4pWQJ^G_G646E%zkRIFVGE;PlGxVVph18qeXR^@)<6oY2R6pja-5 zC)YtmZ#?`-m=)XD#f=I<3PmQOlK$AY3_)Z}bP=o4Co3nb9FxnZi<2)F<UFpG!3 zZzzRvQ6YJ}TEI3|LNR@u&Tcn40}`!*C^i<1i92TY&CjOYUuJE98)CUX8L0DIu`K@WoLblpSn{gZ8u!m+A#Ah}Od#Iwl3E0HouRwa~fG|nT5JiPA#n)VGa*p+;wZ>|$ z1XZo}V0(LyMg#o(-K%fi?&3Sh`XU~4Z%V(fef`mv+`AzSUE36M4OW?*S(3EZZ8U)z zoOkXJ*Ip&b_N|1(2&!Xnp~wjo1+15huN5E971k5n+;>@WEb-jE^{rj~8>zHBwDBg<|B4Z?FO@gqI z@;S5v6YI?U%t7r+`&wfih`F1}A}2v~?(?*RxWUNvmm`u|;Myp}IZzP35SL{<41V}h z^4~S&#h-z)M!4CYBjvAah6oYHtD&;X{*k)wDy;8qO#IS2>BLO-hMx#}h7MHe?@X2j z<&5=Atkur%d?{<+wl?x6?^C%as=z~$NS|lMW zV3!~bxK1~xlwd}R+kxK@fHNxLZ;x2dvEg%}TCkZ7fg!0#n=AY7NElQ! z_v?aed((tOgqi7Ll@<7X8w)c!ymBpq{gC^sOS4U*7nQcc(Q|!47G10WmEFn!yT9ON zUrDKW*4oDWt&H!11!){fo&^bU_B(QQpdtyFAY2kPw_Z3?y+H6BjXj;Af4iyz3VK{x zPU^lWcf8Fp_}4M%)0&^c@JBTE+kB?SACrk9!r9%{+1=uvkBcqR>@V4x(yaAC7^t*< z65U~#^7dO0P#s512YIC=#chpX6^wegnx1qjyPRS`q@{&zcA%(zgVG{$l=Nx zIuL6d{EjWYtYA+l@L-dL+C z5Gt?~6LLu$%=+^{iscqmX}f28jP_2OIx3_?Faeb2kF|`vR%E7Mgkv~bEgzxYH3Xiy z&Pz_S`CsmA4@3iG1!+=c%6ToTB2CstaoicSlOCE&>#aHhksNJ1?B|!mv5X-`43D}Y zzCnj-5E3yE{1`s{JbXDHKK)<&@1v++BSFI z!)FUV!hf7){2Jxwe)3#y{IChKf1~^(K7MunxelGToIi{S@b@PEZ9so@{h6uH3)K%} z!$