Browse Source
Excel Import/Export/Report (#1522)
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
pull/1530/head
Kitti U
6 years ago
committed by
Eric @ Elico Corp
52 changed files with 3945 additions and 1 deletions
-
153excel_import_export/README.rst
-
5excel_import_export/__init__.py
-
29excel_import_export/__manifest__.py
-
8excel_import_export/models/__init__.py
-
335excel_import_export/models/common.py
-
48excel_import_export/models/styles.py
-
273excel_import_export/models/xlsx_export.py
-
259excel_import_export/models/xlsx_import.py
-
69excel_import_export/models/xlsx_report.py
-
452excel_import_export/models/xlsx_template.py
-
1excel_import_export/readme/CONTRIBUTORS.rst
-
8excel_import_export/readme/DESCRIPTION.rst
-
4excel_import_export/readme/HISTORY.rst
-
5excel_import_export/readme/INSTALL.rst
-
2excel_import_export/readme/ROADMAP.rst
-
41excel_import_export/readme/USAGE.rst
-
4excel_import_export/security/ir.model.access.csv
-
496excel_import_export/static/description/index.html
-
51excel_import_export/views/xlsx_report.xml
-
230excel_import_export/views/xlsx_template_view.xml
-
2excel_import_export/wizard/__init__.py
-
82excel_import_export/wizard/export_xlsx_wizard.py
-
39excel_import_export/wizard/export_xlsx_wizard.xml
-
146excel_import_export/wizard/import_xlsx_wizard.py
-
44excel_import_export/wizard/import_xlsx_wizard.xml
-
112excel_import_export_demo/README.rst
-
5excel_import_export_demo/__init__.py
-
22excel_import_export_demo/__manifest__.py
-
32excel_import_export_demo/import_export_sale_order/actions.xml
-
BINexcel_import_export_demo/import_export_sale_order/sale_order.xlsx
-
52excel_import_export_demo/import_export_sale_order/templates.xml
-
BINexcel_import_export_demo/import_sale_orders/import_sale_order.xlsx
-
25excel_import_export_demo/import_sale_orders/menu_action.xml
-
38excel_import_export_demo/import_sale_orders/templates.xml
-
1excel_import_export_demo/readme/CONTRIBUTORS.rst
-
5excel_import_export_demo/readme/DESCRIPTION.rst
-
4excel_import_export_demo/readme/HISTORY.rst
-
3excel_import_export_demo/readme/INSTALL.rst
-
11excel_import_export_demo/readme/USAGE.rst
-
4excel_import_export_demo/report_sale_order/__init__.py
-
35excel_import_export_demo/report_sale_order/report_sale_order.py
-
BINexcel_import_export_demo/report_sale_order/report_sale_order.xlsx
-
41excel_import_export_demo/report_sale_order/report_sale_order.xml
-
36excel_import_export_demo/report_sale_order/templates.xml
-
455excel_import_export_demo/static/description/index.html
-
5excel_import_export_demo/tests/__init__.py
-
BINexcel_import_export_demo/tests/sale_order.xlsx
-
130excel_import_export_demo/tests/test_common.py
-
48excel_import_export_demo/tests/test_xlsx_import_export.py
-
29excel_import_export_demo/tests/test_xlsx_report.py
-
62excel_import_export_demo/tests/test_xlsx_template.py
-
5requirements.txt
@ -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, <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. 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 |
|||
3. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import -- <file>.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 -- <import file>.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 -- <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 -- <report.xml> |
|||
3. Create report model as models.Transient, then define search criteria fields, and get reporing data into ``results`` field -- <report>.py |
|||
4. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results -- <report_file>.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 <https://github.com/OCA/server-tools/issues>`_. |
|||
In case of trouble, please check there if your issue has already been reported. |
|||
If you spotted it first, help us smashing it by providing a detailed and welcomed |
|||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_export%0Aversion:%2012-add-excel_import_export%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|||
|
|||
Do not contact contributors directly about support or help with technical issues. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Authors |
|||
~~~~~~~ |
|||
|
|||
* Ecosoft |
|||
|
|||
Contributors |
|||
~~~~~~~~~~~~ |
|||
|
|||
* Kitti Upariphutthiphong. <kittiu@gmail.com> (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 <https://odoo-community.org/page/maintainer-role>`__: |
|||
|
|||
|maintainer-kittiu| |
|||
|
|||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export>`_ project on GitHub. |
|||
|
|||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
@ -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 |
@ -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'], |
|||
} |
@ -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 |
@ -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 |
@ -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%', |
|||
}, |
|||
} |
@ -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)) |
@ -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 |
@ -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', |
|||
} |
@ -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 |
@ -0,0 +1 @@ |
|||
* Kitti Upariphutthiphong. <kittiu@gmail.com> (http://ecosoft.co.th) |
@ -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. |
@ -0,0 +1,4 @@ |
|||
12.0.1.0.0 (2019-02-24) |
|||
~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
* Start of the history |
@ -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**. |
@ -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. |
@ -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, <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. 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 |
|||
3. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import -- <file>.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 -- <import file>.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 -- <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 -- <report.xml> |
|||
3. Create report model as models.Transient, then define search criteria fields, and get reporing data into ``results`` field -- <report>.py |
|||
4. Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results -- <report_file>.xlsx |
|||
5. Create instruction dictionary for report in xlsx.template model -- templates.xml |
@ -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 |
@ -0,0 +1,496 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
|||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> |
|||
<head> |
|||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> |
|||
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" /> |
|||
<title>Excel Import/Export</title> |
|||
<style type="text/css"> |
|||
|
|||
/* |
|||
:Author: David Goodger (goodger@python.org) |
|||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $ |
|||
:Copyright: This stylesheet has been placed in the public domain. |
|||
|
|||
Default cascading style sheet for the HTML output of Docutils. |
|||
|
|||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to |
|||
customize this style sheet. |
|||
*/ |
|||
|
|||
/* used to remove borders from tables and images */ |
|||
.borderless, table.borderless td, table.borderless th { |
|||
border: 0 } |
|||
|
|||
table.borderless td, table.borderless th { |
|||
/* Override padding for "table.docutils td" with "! important". |
|||
The right padding separates the table cells. */ |
|||
padding: 0 0.5em 0 0 ! important } |
|||
|
|||
.first { |
|||
/* Override more specific margin styles with "! important". */ |
|||
margin-top: 0 ! important } |
|||
|
|||
.last, .with-subtitle { |
|||
margin-bottom: 0 ! important } |
|||
|
|||
.hidden { |
|||
display: none } |
|||
|
|||
.subscript { |
|||
vertical-align: sub; |
|||
font-size: smaller } |
|||
|
|||
.superscript { |
|||
vertical-align: super; |
|||
font-size: smaller } |
|||
|
|||
a.toc-backref { |
|||
text-decoration: none ; |
|||
color: black } |
|||
|
|||
blockquote.epigraph { |
|||
margin: 2em 5em ; } |
|||
|
|||
dl.docutils dd { |
|||
margin-bottom: 0.5em } |
|||
|
|||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* Uncomment (and remove this text!) to get bold-faced definition list terms |
|||
dl.docutils dt { |
|||
font-weight: bold } |
|||
*/ |
|||
|
|||
div.abstract { |
|||
margin: 2em 5em } |
|||
|
|||
div.abstract p.topic-title { |
|||
font-weight: bold ; |
|||
text-align: center } |
|||
|
|||
div.admonition, div.attention, div.caution, div.danger, div.error, |
|||
div.hint, div.important, div.note, div.tip, div.warning { |
|||
margin: 2em ; |
|||
border: medium outset ; |
|||
padding: 1em } |
|||
|
|||
div.admonition p.admonition-title, div.hint p.admonition-title, |
|||
div.important p.admonition-title, div.note p.admonition-title, |
|||
div.tip p.admonition-title { |
|||
font-weight: bold ; |
|||
font-family: sans-serif } |
|||
|
|||
div.attention p.admonition-title, div.caution p.admonition-title, |
|||
div.danger p.admonition-title, div.error p.admonition-title, |
|||
div.warning p.admonition-title, .code .error { |
|||
color: red ; |
|||
font-weight: bold ; |
|||
font-family: sans-serif } |
|||
|
|||
/* Uncomment (and remove this text!) to get reduced vertical space in |
|||
compound paragraphs. |
|||
div.compound .compound-first, div.compound .compound-middle { |
|||
margin-bottom: 0.5em } |
|||
|
|||
div.compound .compound-last, div.compound .compound-middle { |
|||
margin-top: 0.5em } |
|||
*/ |
|||
|
|||
div.dedication { |
|||
margin: 2em 5em ; |
|||
text-align: center ; |
|||
font-style: italic } |
|||
|
|||
div.dedication p.topic-title { |
|||
font-weight: bold ; |
|||
font-style: normal } |
|||
|
|||
div.figure { |
|||
margin-left: 2em ; |
|||
margin-right: 2em } |
|||
|
|||
div.footer, div.header { |
|||
clear: both; |
|||
font-size: smaller } |
|||
|
|||
div.line-block { |
|||
display: block ; |
|||
margin-top: 1em ; |
|||
margin-bottom: 1em } |
|||
|
|||
div.line-block div.line-block { |
|||
margin-top: 0 ; |
|||
margin-bottom: 0 ; |
|||
margin-left: 1.5em } |
|||
|
|||
div.sidebar { |
|||
margin: 0 0 0.5em 1em ; |
|||
border: medium outset ; |
|||
padding: 1em ; |
|||
background-color: #ffffee ; |
|||
width: 40% ; |
|||
float: right ; |
|||
clear: right } |
|||
|
|||
div.sidebar p.rubric { |
|||
font-family: sans-serif ; |
|||
font-size: medium } |
|||
|
|||
div.system-messages { |
|||
margin: 5em } |
|||
|
|||
div.system-messages h1 { |
|||
color: red } |
|||
|
|||
div.system-message { |
|||
border: medium outset ; |
|||
padding: 1em } |
|||
|
|||
div.system-message p.system-message-title { |
|||
color: red ; |
|||
font-weight: bold } |
|||
|
|||
div.topic { |
|||
margin: 2em } |
|||
|
|||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, |
|||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { |
|||
margin-top: 0.4em } |
|||
|
|||
h1.title { |
|||
text-align: center } |
|||
|
|||
h2.subtitle { |
|||
text-align: center } |
|||
|
|||
hr.docutils { |
|||
width: 75% } |
|||
|
|||
img.align-left, .figure.align-left, object.align-left, table.align-left { |
|||
clear: left ; |
|||
float: left ; |
|||
margin-right: 1em } |
|||
|
|||
img.align-right, .figure.align-right, object.align-right, table.align-right { |
|||
clear: right ; |
|||
float: right ; |
|||
margin-left: 1em } |
|||
|
|||
img.align-center, .figure.align-center, object.align-center { |
|||
display: block; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
table.align-center { |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
.align-left { |
|||
text-align: left } |
|||
|
|||
.align-center { |
|||
clear: both ; |
|||
text-align: center } |
|||
|
|||
.align-right { |
|||
text-align: right } |
|||
|
|||
/* reset inner alignment in figures */ |
|||
div.align-right { |
|||
text-align: inherit } |
|||
|
|||
/* div.align-center * { */ |
|||
/* text-align: left } */ |
|||
|
|||
.align-top { |
|||
vertical-align: top } |
|||
|
|||
.align-middle { |
|||
vertical-align: middle } |
|||
|
|||
.align-bottom { |
|||
vertical-align: bottom } |
|||
|
|||
ol.simple, ul.simple { |
|||
margin-bottom: 1em } |
|||
|
|||
ol.arabic { |
|||
list-style: decimal } |
|||
|
|||
ol.loweralpha { |
|||
list-style: lower-alpha } |
|||
|
|||
ol.upperalpha { |
|||
list-style: upper-alpha } |
|||
|
|||
ol.lowerroman { |
|||
list-style: lower-roman } |
|||
|
|||
ol.upperroman { |
|||
list-style: upper-roman } |
|||
|
|||
p.attribution { |
|||
text-align: right ; |
|||
margin-left: 50% } |
|||
|
|||
p.caption { |
|||
font-style: italic } |
|||
|
|||
p.credits { |
|||
font-style: italic ; |
|||
font-size: smaller } |
|||
|
|||
p.label { |
|||
white-space: nowrap } |
|||
|
|||
p.rubric { |
|||
font-weight: bold ; |
|||
font-size: larger ; |
|||
color: maroon ; |
|||
text-align: center } |
|||
|
|||
p.sidebar-title { |
|||
font-family: sans-serif ; |
|||
font-weight: bold ; |
|||
font-size: larger } |
|||
|
|||
p.sidebar-subtitle { |
|||
font-family: sans-serif ; |
|||
font-weight: bold } |
|||
|
|||
p.topic-title { |
|||
font-weight: bold } |
|||
|
|||
pre.address { |
|||
margin-bottom: 0 ; |
|||
margin-top: 0 ; |
|||
font: inherit } |
|||
|
|||
pre.literal-block, pre.doctest-block, pre.math, pre.code { |
|||
margin-left: 2em ; |
|||
margin-right: 2em } |
|||
|
|||
pre.code .ln { color: grey; } /* line numbers */ |
|||
pre.code, code { background-color: #eeeeee } |
|||
pre.code .comment, code .comment { color: #5C6576 } |
|||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } |
|||
pre.code .literal.string, code .literal.string { color: #0C5404 } |
|||
pre.code .name.builtin, code .name.builtin { color: #352B84 } |
|||
pre.code .deleted, code .deleted { background-color: #DEB0A1} |
|||
pre.code .inserted, code .inserted { background-color: #A3D289} |
|||
|
|||
span.classifier { |
|||
font-family: sans-serif ; |
|||
font-style: oblique } |
|||
|
|||
span.classifier-delimiter { |
|||
font-family: sans-serif ; |
|||
font-weight: bold } |
|||
|
|||
span.interpreted { |
|||
font-family: sans-serif } |
|||
|
|||
span.option { |
|||
white-space: nowrap } |
|||
|
|||
span.pre { |
|||
white-space: pre } |
|||
|
|||
span.problematic { |
|||
color: red } |
|||
|
|||
span.section-subtitle { |
|||
/* font-size relative to parent (h1..h6 element) */ |
|||
font-size: 80% } |
|||
|
|||
table.citation { |
|||
border-left: solid 1px gray; |
|||
margin-left: 1px } |
|||
|
|||
table.docinfo { |
|||
margin: 2em 4em } |
|||
|
|||
table.docutils { |
|||
margin-top: 0.5em ; |
|||
margin-bottom: 0.5em } |
|||
|
|||
table.footnote { |
|||
border-left: solid 1px black; |
|||
margin-left: 1px } |
|||
|
|||
table.docutils td, table.docutils th, |
|||
table.docinfo td, table.docinfo th { |
|||
padding-left: 0.5em ; |
|||
padding-right: 0.5em ; |
|||
vertical-align: top } |
|||
|
|||
table.docutils th.field-name, table.docinfo th.docinfo-name { |
|||
font-weight: bold ; |
|||
text-align: left ; |
|||
white-space: nowrap ; |
|||
padding-left: 0 } |
|||
|
|||
/* "booktabs" style (no vertical lines) */ |
|||
table.docutils.booktabs { |
|||
border: 0px; |
|||
border-top: 2px solid; |
|||
border-bottom: 2px solid; |
|||
border-collapse: collapse; |
|||
} |
|||
table.docutils.booktabs * { |
|||
border: 0px; |
|||
} |
|||
table.docutils.booktabs th { |
|||
border-bottom: thin solid; |
|||
text-align: left; |
|||
} |
|||
|
|||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, |
|||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { |
|||
font-size: 100% } |
|||
|
|||
ul.auto-toc { |
|||
list-style-type: none } |
|||
|
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="document" id="excel-import-export"> |
|||
<h1 class="title">Excel Import/Export</h1> |
|||
|
|||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|||
!! This file is generated by oca-gen-addon-readme !! |
|||
!! changes will be overwritten. !! |
|||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> |
|||
<p><a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-tools-12-add-excel_import_export/server-tools-12-add-excel_import_export-excel_import_export"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/149/12-add-excel_import_export"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p> |
|||
<p>The module provide pre-built functions and wizards for developer to build excel import / export / report with ease.</p> |
|||
<p>Without having to code to create excel file, developer do,</p> |
|||
<ul class="simple"> |
|||
<li>Create menu, action, wizard, model, view a normal Odoo development.</li> |
|||
<li>Design excel template using standard Excel application, e.g., colors, fonts, formulas, etc.</li> |
|||
<li>Instruct how the data will be located in Excel with simple dictionary instruction or from Odoo UI.</li> |
|||
<li>Odoo will combine instruction with excel template, and result in final excel file.</li> |
|||
</ul> |
|||
<p><strong>Table of contents</strong></p> |
|||
<div class="contents local topic" id="contents"> |
|||
<ul class="simple"> |
|||
<li><a class="reference internal" href="#installation" id="id2">Installation</a></li> |
|||
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li> |
|||
<li><a class="reference internal" href="#known-issues-roadmap" id="id4">Known issues / Roadmap</a></li> |
|||
<li><a class="reference internal" href="#changelog" id="id5">Changelog</a><ul> |
|||
<li><a class="reference internal" href="#id1" id="id6">12.0.1.0.0 (2019-02-24)</a></li> |
|||
</ul> |
|||
</li> |
|||
<li><a class="reference internal" href="#bug-tracker" id="id7">Bug Tracker</a></li> |
|||
<li><a class="reference internal" href="#credits" id="id8">Credits</a><ul> |
|||
<li><a class="reference internal" href="#authors" id="id9">Authors</a></li> |
|||
<li><a class="reference internal" href="#contributors" id="id10">Contributors</a></li> |
|||
<li><a class="reference internal" href="#maintainers" id="id11">Maintainers</a></li> |
|||
</ul> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="installation"> |
|||
<h1><a class="toc-backref" href="#id2">Installation</a></h1> |
|||
<p>To install this module, you need to install following python library, <strong>xlrd, xlwt, openpyxl</strong>.</p> |
|||
<p>Then, simply install <strong>excel_import_export</strong>.</p> |
|||
<p>For samples, install <strong>excel_import_export_sample</strong>.</p> |
|||
</div> |
|||
<div class="section" id="usage"> |
|||
<h1><a class="toc-backref" href="#id3">Usage</a></h1> |
|||
<p>This module contain pre-defined function and wizards to make exporting, importing and reporting easy.</p> |
|||
<p>At the heart of this module, there are 2 <cite>main methods</cite></p> |
|||
<ul class="simple"> |
|||
<li><tt class="docutils literal"><span class="pre">self.env['xlsx.export'].export_xlsx(...)</span></tt></li> |
|||
<li><tt class="docutils literal"><span class="pre">self.env['xlsx.import'].import_xlsx(...)</span></tt></li> |
|||
</ul> |
|||
<p>For reporting, also call <cite>export_xlsx(…)</cite> but through following method</p> |
|||
<ul class="simple"> |
|||
<li><tt class="docutils literal"><span class="pre">self.env['xslx.report'].report_xlsx(...)</span></tt></li> |
|||
</ul> |
|||
<p>After install this module, go to Settings > Excel Import/Export > XLSX Templates, this is where the key component located.</p> |
|||
<p>As this module provide tools, it is best to explain as use cases. For example use cases, please install <strong>excel_import_export_sample</strong></p> |
|||
<p><strong>Use Case 1:</strong> Export/Import Excel on existing document</p> |
|||
<p>This add export/import action menus in existing document (example - excel_import_export_sample/import_export_sale_order)</p> |
|||
<ol class="arabic simple"> |
|||
<li>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</li> |
|||
<li>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</li> |
|||
<li>Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for export/import – <file>.xlsx</li> |
|||
<li>Create instruction dictionary for export/import in xlsx.template model – templates.xml</li> |
|||
</ol> |
|||
<p><strong>Use Case 2:</strong> Import Excel Files</p> |
|||
<p>With menu wizard to create new documents (example - excel_import_export_sample/import_sale_orders)</p> |
|||
<ol class="arabic simple"> |
|||
<li>Create report menu with search wizard, res_model=”import.xlsx.wizard” and context[‘template_domain’] to locate the right template – menu_action.xml</li> |
|||
<li>Create Excel Template File (.xlsx), in the template, name the underlining tab used for import – <import file>.xlsx</li> |
|||
<li>Create instruction dictionary for import in xlsx.template model – templates.xml</li> |
|||
</ol> |
|||
<p><strong>Use Case 3:</strong> Create Excel Report</p> |
|||
<p>This create report menu with criteria wizard. (example - excel_import_export_sample/report_sale_order)</p> |
|||
<ol class="arabic simple"> |
|||
<li>Create report’s menu, action, and add context[‘template_domain’] to locate the right template for this report – <report>.xml</li> |
|||
<li>Create report’s wizard for search criteria. The view inherits <tt class="docutils literal">excel_import_export.xlsx_report_view</tt> and mode=”primary”. In this view, you only need to add criteria fields, the rest will reuse from interited view – <report.xml></li> |
|||
<li>Create report model as models.Transient, then define search criteria fields, and get reporing data into <tt class="docutils literal">results</tt> field – <report>.py</li> |
|||
<li>Create/Design Excel Template File (.xlsx), in the template, name the underlining tab used for report results – <report_file>.xlsx</li> |
|||
<li>Create instruction dictionary for report in xlsx.template model – templates.xml</li> |
|||
</ol> |
|||
</div> |
|||
<div class="section" id="known-issues-roadmap"> |
|||
<h1><a class="toc-backref" href="#id4">Known issues / Roadmap</a></h1> |
|||
<ul class="simple"> |
|||
<li>Module extension e.g., excel_import_export_async, that add ability to execute as async process.</li> |
|||
<li>Ability to add contextual action in XLSX Tempalte, e.g., Add import action, Add export action. In similar manner as in Server Action.</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="changelog"> |
|||
<h1><a class="toc-backref" href="#id5">Changelog</a></h1> |
|||
<div class="section" id="id1"> |
|||
<h2><a class="toc-backref" href="#id6">12.0.1.0.0 (2019-02-24)</a></h2> |
|||
<ul class="simple"> |
|||
<li>Start of the history</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<div class="section" id="bug-tracker"> |
|||
<h1><a class="toc-backref" href="#id7">Bug Tracker</a></h1> |
|||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>. |
|||
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 |
|||
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_export%0Aversion:%2012-add-excel_import_export%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p> |
|||
<p>Do not contact contributors directly about support or help with technical issues.</p> |
|||
</div> |
|||
<div class="section" id="credits"> |
|||
<h1><a class="toc-backref" href="#id8">Credits</a></h1> |
|||
<div class="section" id="authors"> |
|||
<h2><a class="toc-backref" href="#id9">Authors</a></h2> |
|||
<ul class="simple"> |
|||
<li>Ecosoft</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="contributors"> |
|||
<h2><a class="toc-backref" href="#id10">Contributors</a></h2> |
|||
<ul class="simple"> |
|||
<li>Kitti Upariphutthiphong. <<a class="reference external" href="mailto:kittiu@gmail.com">kittiu@gmail.com</a>> (<a class="reference external" href="http://ecosoft.co.th">http://ecosoft.co.th</a>)</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="maintainers"> |
|||
<h2><a class="toc-backref" href="#id11">Maintainers</a></h2> |
|||
<p>This module is maintained by the OCA.</p> |
|||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a> |
|||
<p>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.</p> |
|||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p> |
|||
<p><a class="reference external" href="https://github.com/kittiu"><img alt="kittiu" src="https://github.com/kittiu.png?size=40px" /></a></p> |
|||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export">OCA/server-tools</a> project on GitHub.</p> |
|||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
</html> |
@ -0,0 +1,51 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="xlsx_report_view" model="ir.ui.view"> |
|||
<field name="name">xlsx.report.view</field> |
|||
<field name="model">xlsx.report</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Excel Report"> |
|||
|
|||
<!-- search criteria --> |
|||
<group name="criteria" states="choose"> |
|||
</group> |
|||
|
|||
<!-- xlsx.report common field --> |
|||
<div name="xlsx.report"> |
|||
<field name="state" invisible="1"/> |
|||
<field name="name" invisible="1"/> |
|||
<field name="choose_template" invisible="1"/> |
|||
<div states="choose"> |
|||
<label string="Choose Template: " for="template_id" |
|||
attrs="{'invisible': [('choose_template', '=', False)]}"/> |
|||
<field name="template_id" |
|||
attrs="{'invisible': [('choose_template', '=', False)]}"/> |
|||
</div> |
|||
<div states="get"> |
|||
<h2> |
|||
Complete Prepare Report (.xlsx) |
|||
</h2> |
|||
<p colspan="4"> |
|||
Here is the report file: |
|||
<field name="data" filename="name" class="oe_inline"/> |
|||
</p> |
|||
</div> |
|||
<footer states="choose"> |
|||
<button name="report_xlsx" string="Execute Report" type="object" class="oe_highlight"/> |
|||
or |
|||
<button special="cancel" string="Cancel" type="object" class="oe_link"/> |
|||
</footer> |
|||
<footer states="get"> |
|||
<button special="cancel" string="Close" type="object"/> |
|||
</footer> |
|||
</div> |
|||
|
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -0,0 +1,230 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="view_xlsx_template_tree" model="ir.ui.view"> |
|||
<field name="model">xlsx.template</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="XLSX Template"> |
|||
<field name="name"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="view_xlsx_template_form" model="ir.ui.view"> |
|||
<field name="model">xlsx.template</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="XLSX Template"> |
|||
<sheet> |
|||
<h1> |
|||
<field name="name" colspan="3"/> |
|||
</h1> |
|||
<group> |
|||
<group> |
|||
<field name="description"/> |
|||
<field name="to_csv"/> |
|||
<field name="csv_delimiter" attrs="{'invisible': [('to_csv', '=', False)]}"/> |
|||
<field name="csv_extension" attrs="{'invisible': [('to_csv', '=', False)]}"/> |
|||
<field name="csv_quote" attrs="{'invisible': [('to_csv', '=', False)]}"/> |
|||
</group> |
|||
<group> |
|||
<field name="fname" invisible="1"/> |
|||
<field name="datas" filename="fname"/> |
|||
<field name="gname"/> |
|||
<field name="res_model"/> |
|||
<field name="redirect_action"/> |
|||
</group> |
|||
</group> |
|||
<notebook> |
|||
<page string="Export"> |
|||
<field name="export_ids"> |
|||
<tree name="export_instruction" editable="bottom"> |
|||
<control> |
|||
<create string="Add sheet section" context="{'default_section_type': 'sheet'}"/> |
|||
<create string="Add header section" context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"/> |
|||
<create string="Add row section" context="{'default_section_type': 'row'}"/> |
|||
<create string="Add data column" context="{'default_section_type': 'data'}"/> |
|||
</control> |
|||
<field name="sequence" widget="handle"/> |
|||
<field name="section_type" invisible="1"/> |
|||
<field name="sheet" attrs="{'required': [('section_type', '=', 'sheet')], |
|||
'invisible': [('section_type', '!=', 'sheet')]}"/> |
|||
<field name="row_field" attrs="{'required': [('section_type', 'in', ('head', 'row'))], |
|||
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> |
|||
<field name="is_cont" attrs="{'required': [('section_type', 'in', ('head', 'row'))], |
|||
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> |
|||
<field name="excel_cell" attrs="{'required': [('section_type', '=', 'data')], |
|||
'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="field_name" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="field_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="is_sum" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="style" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="style_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
</tree> |
|||
</field> |
|||
<div style="margin-top: 4px;"> |
|||
<h3>Help with Export Instruction</h3> |
|||
<p> |
|||
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). |
|||
</p> |
|||
<ul> |
|||
<li>In header section part, map data fields (e.g., number, partner_id.name) into cells (e.g., B1, B2).</li> |
|||
<li>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).</li> |
|||
</ul> |
|||
<p>Following are more explaination on each column:</p> |
|||
<ul> |
|||
<li><b>Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet to export data to</li> |
|||
<li><b>Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li> |
|||
<li><b>Continue</b>: If not selected, start rolling with specified first row cells. If selected, continue from previous one2many field</li> |
|||
<li><b>Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li> |
|||
<li><b>Field</b>: Field of the record, e.g., product_id.uom_id.name. They are orm compliant.</li> |
|||
<li><b>Field Cond.</b>: Python code in <code>${...}</code> to manipulate field value, e.g., if field = product_id, <code>value</code> will represent product object, e.g., <code>${value and value.uom_id.name or ""}</code></li> |
|||
<li><b>Sum</b>: Add sum value on last row, <code>@{sum}</code></li> |
|||
<li><b>Style</b>: Default style in <code>#{...}</code> that apply to each cell, e.g., <code>#{align=left;style=text}</code>. See module's <b>style.py</b> for available styles.</li> |
|||
<li><b>Style w/Cond.</b>: Conditional style by python code in <code>#?...?</code>, e.g., apply style for specific product, <code>#?value.name == "ABC" and #{font=bold;fill=red} or None?</code></li> |
|||
</ul> |
|||
<p><b>Note:</b></p> |
|||
For code block <code>${...}</code> and <code>#?...?</code>, following object are available, |
|||
<ul> |
|||
<li><code>value</code>: value from <b>Field</b></li> |
|||
<li><code>object</code>: record object or line object depends on <b>Row Field</b></li> |
|||
<li><code>model</code>: active model, e.g., self.env['my.model']</li> |
|||
<li><code>date, datetime, time</code>: some useful python classes</li> |
|||
</ul> |
|||
</div> |
|||
</page> |
|||
<page string="Import"> |
|||
<field name="import_ids"> |
|||
<tree name="import_instruction" editable="bottom"> |
|||
<control> |
|||
<create string="Add sheet section" context="{'default_section_type': 'sheet'}"/> |
|||
<create string="Add header section" context="{'default_section_type': 'head', 'default_row_field': '_HEAD_'}"/> |
|||
<create string="Add row section" context="{'default_section_type': 'row'}"/> |
|||
<create string="Add data column" context="{'default_section_type': 'data'}"/> |
|||
</control> |
|||
<field name="sequence" widget="handle"/> |
|||
<field name="section_type" invisible="1"/> |
|||
<field name="sheet" attrs="{'required': [('section_type', '=', 'sheet')], |
|||
'invisible': [('section_type', '!=', 'sheet')]}"/> |
|||
<field name="row_field" attrs="{'required': [('section_type', 'in', ('head', 'row'))], |
|||
'invisible': [('section_type', 'not in', ('head', 'row'))]}"/> |
|||
<field name="no_delete" attrs="{'invisible': [('section_type', '!=', 'row')]}"/> |
|||
<field name="excel_cell" attrs="{'required': [('section_type', '=', 'data')], |
|||
'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="field_name" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
<field name="field_cond" attrs="{'invisible': [('section_type', '!=', 'data')]}"/> |
|||
</tree> |
|||
</field> |
|||
<group string="Post Import Hook"> |
|||
<field name="post_import_hook" placeholder="${object.post_import_do_something()}"/> |
|||
</group> |
|||
<hr/> |
|||
<div style="margin-top: 4px;"> |
|||
<h3>Help with Import Instruction</h3> |
|||
<p> |
|||
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. |
|||
</p> |
|||
<ul> |
|||
<li>In header section, map cells (e.g., B1, B2) into data fields (e.g., number, partner_id).</li> |
|||
<li>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) </li> |
|||
</ul> |
|||
<p>Following are more explaination on each column:</p> |
|||
<ul> |
|||
<li><b>Sheet</b>: Name (e.g., Sheet 1) or index (e.g., 1) of excel sheet</li> |
|||
<li><b>Row Field</b>: Use _HEAD_ for the record itself, and one2many field (e.g., line_ids) for row data</li> |
|||
<li><b>No Delete</b>: By default, all one2many lines will be deleted before import. Select this, to avoid deletion</li> |
|||
<li><b>Cell</b>: Location of data in excel sheet (e.g., A1, B1, ...)</li> |
|||
<li><b>Field</b>: Field of the record to be imported to, e.g., product_id</li> |
|||
<li><b>Field Cond.</b>: Python code in <code>${...}</code> value will represent data from excel cell, e.g., if A1 = 'ABC', <code>value</code> will represent 'ABC', e.g., <code>${value == "ABC" and "X" or "Y"}</code> thus can change from cell value to other value for import.</li> |
|||
</ul> |
|||
<p><b>Note:</b></p> |
|||
For code block <code>${...}</code>, following object are available, |
|||
<ul> |
|||
<li><code>value</code>: value from <b>Cell</b></li> |
|||
<li><code>model</code>: active model, e.g., self.env['my.model']</li> |
|||
<li><code>date, datetime, time</code>: some useful python classes</li> |
|||
</ul> |
|||
</div> |
|||
</page> |
|||
<page string="Input Instruction (Dict.)"> |
|||
<field name="input_instruction"/> |
|||
<field name="show_instruction"/><label for="show_instruction"/> |
|||
<field name="instruction" attrs="{'invisible': [('show_instruction', '=', False)]}"/> |
|||
<hr/> |
|||
<div style="margin-top: 4px;"> |
|||
<h3>Sample Input Instruction as Dictionary</h3> |
|||
<p> |
|||
Following show very simple example of the dictionary construct. |
|||
Normally, this will be within templates.xml file within addons. |
|||
</p> |
|||
<pre> |
|||
<code class="oe_grey"> |
|||
{ |
|||
'__EXPORT__': { |
|||
'sale_order': { # sheet can be name (string) or index (integer) |
|||
'_HEAD_': { |
|||
'B2': 'partner_id.display_name${value or ""}#{align=left;style=text}', |
|||
'B3': 'name${value or ""}#{align=left;style=text}', |
|||
}, |
|||
'line_ids': { # prefix with _CONT_ to continue rows from previous row field |
|||
'A6': 'product_id.display_name${value or ""}#{style=text}', |
|||
'C6': 'product_uom_qty${value or 0}#{style=number}', |
|||
'E6': 'price_unit${value or 0}#{style=number}', |
|||
'G6': 'price_subtotal${value or 0}#{style=number}', |
|||
}, |
|||
}, |
|||
}, |
|||
'__IMPORT__': { |
|||
'sale_order': { # sheet can be name (string) or index (integer) |
|||
'order_line': { # prefix with _NODEL_ to not delete rows before import |
|||
'A6': 'product_id', |
|||
'C6': 'product_uom_qty', |
|||
'E6': 'price_unit${value > 0 and value or 0}', |
|||
}, |
|||
}, |
|||
}, |
|||
'__POST_IMPORT__': '${object.post_import_do_something()}', |
|||
} |
|||
|
|||
</code> |
|||
</pre> |
|||
</div> |
|||
</page> |
|||
</notebook> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="action_xlsx_template" model="ir.actions.act_window"> |
|||
<field name="name">XLSX Templates</field> |
|||
<field name="type">ir.actions.act_window</field> |
|||
<field name="res_model">xlsx.template</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">tree,form</field> |
|||
<field name="help" type="html"> |
|||
<p class="oe_view_nocontent_create"> |
|||
Click to create a XLSX Template Object. |
|||
</p> |
|||
</field> |
|||
</record> |
|||
|
|||
<menuitem id="menu_excel_import_export" |
|||
name="Excel Import/Export" |
|||
parent="base.menu_custom" |
|||
sequence="130"/> |
|||
|
|||
<menuitem id="menu_xlsx_template" |
|||
parent="menu_excel_import_export" |
|||
action="action_xlsx_template" |
|||
sequence="10"/> |
|||
|
|||
</odoo> |
@ -0,0 +1,2 @@ |
|||
from . import export_xlsx_wizard |
|||
from . import import_xlsx_wizard |
@ -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', |
|||
} |
@ -0,0 +1,39 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="export_xlsx_wizard" model="ir.ui.view"> |
|||
<field name="name">export.xlsx.wizard</field> |
|||
<field name="model">export.xlsx.wizard</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Get Import Template"> |
|||
<field invisible="1" name="state"/> |
|||
<field name="name" invisible="1"/> |
|||
<group states="choose"> |
|||
<group> |
|||
<field name="template_id" widget="selection"/> |
|||
</group> |
|||
<group> |
|||
<field name="res_model" invisible="1"/> |
|||
<field name="res_id" invisible="1"/> |
|||
</group> |
|||
</group> |
|||
<div states="get"> |
|||
<h2>Complete Prepare File (.xlsx)</h2> |
|||
<p>Here is the exported file: <field name="data" readonly="1" filename="name"/></p> |
|||
</div> |
|||
<footer states="choose"> |
|||
<button name="action_export" string="Export" type="object" class="oe_highlight"/> or |
|||
<button special="cancel" string="Cancel" type="object" class="oe_link"/> |
|||
</footer> |
|||
<footer states="get"> |
|||
<button special="cancel" string="Close" type="object"/> |
|||
</footer> |
|||
</form> |
|||
|
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -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', |
|||
} |
@ -0,0 +1,44 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="import_xlsx_wizard" model="ir.ui.view"> |
|||
<field name="name">import.xlsx.wizard</field> |
|||
<field name="model">import.xlsx.wizard</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Import File Template"> |
|||
<field name="state" invisible="1"/> |
|||
<group states="choose"> |
|||
<group> |
|||
<field name="import_file" attrs="{'invisible': [('res_id', '=', False)]}"/> |
|||
<field name="attachment_ids" widget="many2many_binary" nolabel="1" |
|||
attrs="{'invisible': [('res_id', '!=', False)]}"/> |
|||
</group> |
|||
<group> |
|||
<field name="template_id" widget="selection"/> |
|||
<field name="fname" invisible="1"/> |
|||
<field name="datas" filename="fname"/> |
|||
<field name="res_model" invisible="1"/> |
|||
<field name="res_id" invisible="1"/> |
|||
</group> |
|||
</group> |
|||
<group states="get"> |
|||
<p> |
|||
Import Successful! |
|||
</p> |
|||
</group> |
|||
<footer states="choose"> |
|||
<button name="action_import" string="Import" type="object" class="oe_highlight"/> |
|||
or |
|||
<button string="Cancel" class="oe_link" special="cancel"/> |
|||
</footer> |
|||
<footer states="get"> |
|||
<button string="Close" class="oe_link" special="cancel"/> |
|||
</footer> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
</odoo> |
@ -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 <https://github.com/OCA/server-tools/issues>`_. |
|||
In case of trouble, please check there if your issue has already been reported. |
|||
If you spotted it first, help us smashing it by providing a detailed and welcomed |
|||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_export_demo%0Aversion:%2012-add-excel_import_export%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|||
|
|||
Do not contact contributors directly about support or help with technical issues. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Authors |
|||
~~~~~~~ |
|||
|
|||
* Ecosoft |
|||
|
|||
Contributors |
|||
~~~~~~~~~~~~ |
|||
|
|||
* Kitti Upariphutthiphong. <kittiu@gmail.com> (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 <https://odoo-community.org/page/maintainer-role>`__: |
|||
|
|||
|maintainer-kittiu| |
|||
|
|||
This module is part of the `OCA/server-tools <https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export_demo>`_ project on GitHub. |
|||
|
|||
You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. |
@ -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 |
@ -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'], |
|||
} |
@ -0,0 +1,32 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<act_window id="action_sale_oder_export_xlsx" |
|||
name="Export Excel" |
|||
res_model="export.xlsx.wizard" |
|||
src_model="sale.order" |
|||
view_mode="form" |
|||
target="new" |
|||
context="{ |
|||
'template_domain': [('res_model', '=', 'sale.order'), |
|||
('fname', '=', 'sale_order.xlsx'), |
|||
('gname', '=', False)], |
|||
}"/> |
|||
<act_window id="action_sale_oder_import_xlsx" |
|||
name="Import Excel" |
|||
res_model="import.xlsx.wizard" |
|||
src_model="sale.order" |
|||
view_mode="form" |
|||
target="new" |
|||
context="{ |
|||
'template_domain': [('res_model', '=', 'sale.order'), |
|||
('fname', '=', 'sale_order.xlsx'), |
|||
('gname', '=', False)], |
|||
'template_context': {}, |
|||
'template_import_states': [], |
|||
}"/> |
|||
|
|||
</odoo> |
@ -0,0 +1,52 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="sale_order_xlsx_template" model="xlsx.template"> |
|||
<field name="res_model">sale.order</field> |
|||
<field name="fname">sale_order.xlsx</field> |
|||
<field name="name">Sale Order Template</field> |
|||
<field name="description">Sample Sales Order Tempalte for testing</field> |
|||
<field name="input_instruction"> |
|||
{ |
|||
'__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()}', |
|||
} |
|||
</field> |
|||
</record> |
|||
|
|||
<function model="xlsx.template" name="load_xlsx_template"> |
|||
<value eval="[ref('sale_order_xlsx_template')]"/> |
|||
</function> |
|||
|
|||
</odoo> |
@ -0,0 +1,25 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="action_import_sale_order" model="ir.actions.act_window"> |
|||
<field name="name">Sample Import Sale Order</field> |
|||
<field name="res_model">import.xlsx.wizard</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">form</field> |
|||
<field name="target">new</field> |
|||
<field name="context">{ |
|||
'template_domain': [('res_model', '=', 'sale.order'), |
|||
('fname', '=', 'import_sale_order.xlsx'), |
|||
('gname', '=', False)], } |
|||
</field> |
|||
</record> |
|||
|
|||
<menuitem id="menu_import_sale_order" |
|||
parent="excel_import_export.menu_excel_import_export" |
|||
action="action_import_sale_order" |
|||
sequence="30"/> |
|||
|
|||
</odoo> |
@ -0,0 +1,38 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="import_sale_order_xlsx_template" model="xlsx.template"> |
|||
<field name="res_model">sale.order</field> |
|||
<field name="fname">import_sale_order.xlsx</field> |
|||
<field name="name">Import Sale Order Template</field> |
|||
<field name="description">Sample Import Sales Order Tempalte for testing</field> |
|||
<field name="redirect_action" ref="sale.action_orders"/> |
|||
<field name="input_instruction"> |
|||
{ |
|||
'__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', |
|||
} |
|||
} |
|||
}, |
|||
} |
|||
</field> |
|||
</record> |
|||
|
|||
<function model="xlsx.template" name="load_xlsx_template"> |
|||
<value eval="[ref('import_sale_order_xlsx_template')]"/> |
|||
</function> |
|||
|
|||
</odoo> |
@ -0,0 +1 @@ |
|||
* Kitti Upariphutthiphong. <kittiu@gmail.com> (http://ecosoft.co.th) |
@ -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) |
@ -0,0 +1,4 @@ |
|||
12.0.1.0.0 (2019-02-24) |
|||
~~~~~~~~~~~~~~~~~~~~~~~ |
|||
|
|||
* Start of the history |
@ -0,0 +1,3 @@ |
|||
To install this module, you need to install **excel_import_export** |
|||
|
|||
Then, simply install **excel_import_export_demo**. |
@ -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 |
@ -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 |
@ -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) |
@ -0,0 +1,41 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="report_sale_order" model="ir.ui.view"> |
|||
<field name="name">report.sale.order</field> |
|||
<field name="model">report.sale.order</field> |
|||
<field name="inherit_id" ref="excel_import_export.xlsx_report_view"/> |
|||
<field name="mode">primary</field> |
|||
<field name="arch" type="xml"> |
|||
<xpath expr="//group[@name='criteria']" position="inside"> |
|||
<group> |
|||
<field name="partner_id"/> |
|||
</group> |
|||
<group> |
|||
</group> |
|||
</xpath> |
|||
</field> |
|||
</record> |
|||
|
|||
<record id="action_report_sale_order" model="ir.actions.act_window"> |
|||
<field name="name">Sample Sales Report</field> |
|||
<field name="res_model">report.sale.order</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">form</field> |
|||
<field name="target">new</field> |
|||
<field name="context"> |
|||
{'template_domain': [('res_model', '=', 'report.sale.order'), |
|||
('fname', '=', 'report_sale_order.xlsx'), |
|||
('gname', '=', False)]} |
|||
</field> |
|||
</record> |
|||
|
|||
<menuitem id="menu_report_sale_order" |
|||
parent="excel_import_export.menu_excel_import_export" |
|||
action="action_report_sale_order" |
|||
sequence="20"/> |
|||
|
|||
</odoo> |
@ -0,0 +1,36 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<!-- |
|||
Copyright 2019 Ecosoft Co., Ltd. |
|||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).--> |
|||
<odoo> |
|||
|
|||
<record id="report_sale_order_template" model="xlsx.template"> |
|||
<field name="res_model">report.sale.order</field> |
|||
<field name="fname">report_sale_order.xlsx</field> |
|||
<field name="name">Report Sale Order Template</field> |
|||
<field name="description">Sample Report Sales Order Tempalte for testing</field> |
|||
<field name="input_instruction"> |
|||
{ |
|||
'__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}', |
|||
}, |
|||
}, |
|||
}, |
|||
} |
|||
</field> |
|||
</record> |
|||
|
|||
<function model="xlsx.template" name="load_xlsx_template"> |
|||
<value eval="[ref('report_sale_order_template')]"/> |
|||
</function> |
|||
|
|||
</odoo> |
@ -0,0 +1,455 @@ |
|||
<?xml version="1.0" encoding="utf-8" ?> |
|||
<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> |
|||
<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en"> |
|||
<head> |
|||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8" /> |
|||
<meta name="generator" content="Docutils 0.14: http://docutils.sourceforge.net/" /> |
|||
<title>Excel Import/Export Demo</title> |
|||
<style type="text/css"> |
|||
|
|||
/* |
|||
:Author: David Goodger (goodger@python.org) |
|||
:Id: $Id: html4css1.css 7952 2016-07-26 18:15:59Z milde $ |
|||
:Copyright: This stylesheet has been placed in the public domain. |
|||
|
|||
Default cascading style sheet for the HTML output of Docutils. |
|||
|
|||
See http://docutils.sf.net/docs/howto/html-stylesheets.html for how to |
|||
customize this style sheet. |
|||
*/ |
|||
|
|||
/* used to remove borders from tables and images */ |
|||
.borderless, table.borderless td, table.borderless th { |
|||
border: 0 } |
|||
|
|||
table.borderless td, table.borderless th { |
|||
/* Override padding for "table.docutils td" with "! important". |
|||
The right padding separates the table cells. */ |
|||
padding: 0 0.5em 0 0 ! important } |
|||
|
|||
.first { |
|||
/* Override more specific margin styles with "! important". */ |
|||
margin-top: 0 ! important } |
|||
|
|||
.last, .with-subtitle { |
|||
margin-bottom: 0 ! important } |
|||
|
|||
.hidden { |
|||
display: none } |
|||
|
|||
.subscript { |
|||
vertical-align: sub; |
|||
font-size: smaller } |
|||
|
|||
.superscript { |
|||
vertical-align: super; |
|||
font-size: smaller } |
|||
|
|||
a.toc-backref { |
|||
text-decoration: none ; |
|||
color: black } |
|||
|
|||
blockquote.epigraph { |
|||
margin: 2em 5em ; } |
|||
|
|||
dl.docutils dd { |
|||
margin-bottom: 0.5em } |
|||
|
|||
object[type="image/svg+xml"], object[type="application/x-shockwave-flash"] { |
|||
overflow: hidden; |
|||
} |
|||
|
|||
/* Uncomment (and remove this text!) to get bold-faced definition list terms |
|||
dl.docutils dt { |
|||
font-weight: bold } |
|||
*/ |
|||
|
|||
div.abstract { |
|||
margin: 2em 5em } |
|||
|
|||
div.abstract p.topic-title { |
|||
font-weight: bold ; |
|||
text-align: center } |
|||
|
|||
div.admonition, div.attention, div.caution, div.danger, div.error, |
|||
div.hint, div.important, div.note, div.tip, div.warning { |
|||
margin: 2em ; |
|||
border: medium outset ; |
|||
padding: 1em } |
|||
|
|||
div.admonition p.admonition-title, div.hint p.admonition-title, |
|||
div.important p.admonition-title, div.note p.admonition-title, |
|||
div.tip p.admonition-title { |
|||
font-weight: bold ; |
|||
font-family: sans-serif } |
|||
|
|||
div.attention p.admonition-title, div.caution p.admonition-title, |
|||
div.danger p.admonition-title, div.error p.admonition-title, |
|||
div.warning p.admonition-title, .code .error { |
|||
color: red ; |
|||
font-weight: bold ; |
|||
font-family: sans-serif } |
|||
|
|||
/* Uncomment (and remove this text!) to get reduced vertical space in |
|||
compound paragraphs. |
|||
div.compound .compound-first, div.compound .compound-middle { |
|||
margin-bottom: 0.5em } |
|||
|
|||
div.compound .compound-last, div.compound .compound-middle { |
|||
margin-top: 0.5em } |
|||
*/ |
|||
|
|||
div.dedication { |
|||
margin: 2em 5em ; |
|||
text-align: center ; |
|||
font-style: italic } |
|||
|
|||
div.dedication p.topic-title { |
|||
font-weight: bold ; |
|||
font-style: normal } |
|||
|
|||
div.figure { |
|||
margin-left: 2em ; |
|||
margin-right: 2em } |
|||
|
|||
div.footer, div.header { |
|||
clear: both; |
|||
font-size: smaller } |
|||
|
|||
div.line-block { |
|||
display: block ; |
|||
margin-top: 1em ; |
|||
margin-bottom: 1em } |
|||
|
|||
div.line-block div.line-block { |
|||
margin-top: 0 ; |
|||
margin-bottom: 0 ; |
|||
margin-left: 1.5em } |
|||
|
|||
div.sidebar { |
|||
margin: 0 0 0.5em 1em ; |
|||
border: medium outset ; |
|||
padding: 1em ; |
|||
background-color: #ffffee ; |
|||
width: 40% ; |
|||
float: right ; |
|||
clear: right } |
|||
|
|||
div.sidebar p.rubric { |
|||
font-family: sans-serif ; |
|||
font-size: medium } |
|||
|
|||
div.system-messages { |
|||
margin: 5em } |
|||
|
|||
div.system-messages h1 { |
|||
color: red } |
|||
|
|||
div.system-message { |
|||
border: medium outset ; |
|||
padding: 1em } |
|||
|
|||
div.system-message p.system-message-title { |
|||
color: red ; |
|||
font-weight: bold } |
|||
|
|||
div.topic { |
|||
margin: 2em } |
|||
|
|||
h1.section-subtitle, h2.section-subtitle, h3.section-subtitle, |
|||
h4.section-subtitle, h5.section-subtitle, h6.section-subtitle { |
|||
margin-top: 0.4em } |
|||
|
|||
h1.title { |
|||
text-align: center } |
|||
|
|||
h2.subtitle { |
|||
text-align: center } |
|||
|
|||
hr.docutils { |
|||
width: 75% } |
|||
|
|||
img.align-left, .figure.align-left, object.align-left, table.align-left { |
|||
clear: left ; |
|||
float: left ; |
|||
margin-right: 1em } |
|||
|
|||
img.align-right, .figure.align-right, object.align-right, table.align-right { |
|||
clear: right ; |
|||
float: right ; |
|||
margin-left: 1em } |
|||
|
|||
img.align-center, .figure.align-center, object.align-center { |
|||
display: block; |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
table.align-center { |
|||
margin-left: auto; |
|||
margin-right: auto; |
|||
} |
|||
|
|||
.align-left { |
|||
text-align: left } |
|||
|
|||
.align-center { |
|||
clear: both ; |
|||
text-align: center } |
|||
|
|||
.align-right { |
|||
text-align: right } |
|||
|
|||
/* reset inner alignment in figures */ |
|||
div.align-right { |
|||
text-align: inherit } |
|||
|
|||
/* div.align-center * { */ |
|||
/* text-align: left } */ |
|||
|
|||
.align-top { |
|||
vertical-align: top } |
|||
|
|||
.align-middle { |
|||
vertical-align: middle } |
|||
|
|||
.align-bottom { |
|||
vertical-align: bottom } |
|||
|
|||
ol.simple, ul.simple { |
|||
margin-bottom: 1em } |
|||
|
|||
ol.arabic { |
|||
list-style: decimal } |
|||
|
|||
ol.loweralpha { |
|||
list-style: lower-alpha } |
|||
|
|||
ol.upperalpha { |
|||
list-style: upper-alpha } |
|||
|
|||
ol.lowerroman { |
|||
list-style: lower-roman } |
|||
|
|||
ol.upperroman { |
|||
list-style: upper-roman } |
|||
|
|||
p.attribution { |
|||
text-align: right ; |
|||
margin-left: 50% } |
|||
|
|||
p.caption { |
|||
font-style: italic } |
|||
|
|||
p.credits { |
|||
font-style: italic ; |
|||
font-size: smaller } |
|||
|
|||
p.label { |
|||
white-space: nowrap } |
|||
|
|||
p.rubric { |
|||
font-weight: bold ; |
|||
font-size: larger ; |
|||
color: maroon ; |
|||
text-align: center } |
|||
|
|||
p.sidebar-title { |
|||
font-family: sans-serif ; |
|||
font-weight: bold ; |
|||
font-size: larger } |
|||
|
|||
p.sidebar-subtitle { |
|||
font-family: sans-serif ; |
|||
font-weight: bold } |
|||
|
|||
p.topic-title { |
|||
font-weight: bold } |
|||
|
|||
pre.address { |
|||
margin-bottom: 0 ; |
|||
margin-top: 0 ; |
|||
font: inherit } |
|||
|
|||
pre.literal-block, pre.doctest-block, pre.math, pre.code { |
|||
margin-left: 2em ; |
|||
margin-right: 2em } |
|||
|
|||
pre.code .ln { color: grey; } /* line numbers */ |
|||
pre.code, code { background-color: #eeeeee } |
|||
pre.code .comment, code .comment { color: #5C6576 } |
|||
pre.code .keyword, code .keyword { color: #3B0D06; font-weight: bold } |
|||
pre.code .literal.string, code .literal.string { color: #0C5404 } |
|||
pre.code .name.builtin, code .name.builtin { color: #352B84 } |
|||
pre.code .deleted, code .deleted { background-color: #DEB0A1} |
|||
pre.code .inserted, code .inserted { background-color: #A3D289} |
|||
|
|||
span.classifier { |
|||
font-family: sans-serif ; |
|||
font-style: oblique } |
|||
|
|||
span.classifier-delimiter { |
|||
font-family: sans-serif ; |
|||
font-weight: bold } |
|||
|
|||
span.interpreted { |
|||
font-family: sans-serif } |
|||
|
|||
span.option { |
|||
white-space: nowrap } |
|||
|
|||
span.pre { |
|||
white-space: pre } |
|||
|
|||
span.problematic { |
|||
color: red } |
|||
|
|||
span.section-subtitle { |
|||
/* font-size relative to parent (h1..h6 element) */ |
|||
font-size: 80% } |
|||
|
|||
table.citation { |
|||
border-left: solid 1px gray; |
|||
margin-left: 1px } |
|||
|
|||
table.docinfo { |
|||
margin: 2em 4em } |
|||
|
|||
table.docutils { |
|||
margin-top: 0.5em ; |
|||
margin-bottom: 0.5em } |
|||
|
|||
table.footnote { |
|||
border-left: solid 1px black; |
|||
margin-left: 1px } |
|||
|
|||
table.docutils td, table.docutils th, |
|||
table.docinfo td, table.docinfo th { |
|||
padding-left: 0.5em ; |
|||
padding-right: 0.5em ; |
|||
vertical-align: top } |
|||
|
|||
table.docutils th.field-name, table.docinfo th.docinfo-name { |
|||
font-weight: bold ; |
|||
text-align: left ; |
|||
white-space: nowrap ; |
|||
padding-left: 0 } |
|||
|
|||
/* "booktabs" style (no vertical lines) */ |
|||
table.docutils.booktabs { |
|||
border: 0px; |
|||
border-top: 2px solid; |
|||
border-bottom: 2px solid; |
|||
border-collapse: collapse; |
|||
} |
|||
table.docutils.booktabs * { |
|||
border: 0px; |
|||
} |
|||
table.docutils.booktabs th { |
|||
border-bottom: thin solid; |
|||
text-align: left; |
|||
} |
|||
|
|||
h1 tt.docutils, h2 tt.docutils, h3 tt.docutils, |
|||
h4 tt.docutils, h5 tt.docutils, h6 tt.docutils { |
|||
font-size: 100% } |
|||
|
|||
ul.auto-toc { |
|||
list-style-type: none } |
|||
|
|||
</style> |
|||
</head> |
|||
<body> |
|||
<div class="document" id="excel-import-export-demo"> |
|||
<h1 class="title">Excel Import/Export Demo</h1> |
|||
|
|||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! |
|||
!! This file is generated by oca-gen-addon-readme !! |
|||
!! changes will be overwritten. !! |
|||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! --> |
|||
<p><a class="reference external" href="http://www.gnu.org/licenses/agpl-3.0-standalone.html"><img alt="License: AGPL-3" src="https://img.shields.io/badge/licence-AGPL--3-blue.png" /></a> <a class="reference external" href="https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export_demo"><img alt="OCA/server-tools" src="https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github" /></a> <a class="reference external" href="https://translation.odoo-community.org/projects/server-tools-12-add-excel_import_export/server-tools-12-add-excel_import_export-excel_import_export_demo"><img alt="Translate me on Weblate" src="https://img.shields.io/badge/weblate-Translate%20me-F47D42.png" /></a> <a class="reference external" href="https://runbot.odoo-community.org/runbot/149/12-add-excel_import_export"><img alt="Try me on Runbot" src="https://img.shields.io/badge/runbot-Try%20me-875A7B.png" /></a></p> |
|||
<p>This module provide some example use case for excel_import_export</p> |
|||
<ol class="arabic simple"> |
|||
<li>Import/Export Sales Order (import_export_sale_order)</li> |
|||
<li>Import New Sales Orders (import_sale_orders)</li> |
|||
<li>Sales Orders Report (report_sale_order)</li> |
|||
</ol> |
|||
<p><strong>Table of contents</strong></p> |
|||
<div class="contents local topic" id="contents"> |
|||
<ul class="simple"> |
|||
<li><a class="reference internal" href="#installation" id="id2">Installation</a></li> |
|||
<li><a class="reference internal" href="#usage" id="id3">Usage</a></li> |
|||
<li><a class="reference internal" href="#changelog" id="id4">Changelog</a><ul> |
|||
<li><a class="reference internal" href="#id1" id="id5">12.0.1.0.0 (2019-02-24)</a></li> |
|||
</ul> |
|||
</li> |
|||
<li><a class="reference internal" href="#bug-tracker" id="id6">Bug Tracker</a></li> |
|||
<li><a class="reference internal" href="#credits" id="id7">Credits</a><ul> |
|||
<li><a class="reference internal" href="#authors" id="id8">Authors</a></li> |
|||
<li><a class="reference internal" href="#contributors" id="id9">Contributors</a></li> |
|||
<li><a class="reference internal" href="#maintainers" id="id10">Maintainers</a></li> |
|||
</ul> |
|||
</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="installation"> |
|||
<h1><a class="toc-backref" href="#id2">Installation</a></h1> |
|||
<p>To install this module, you need to install <strong>excel_import_export</strong></p> |
|||
<p>Then, simply install <strong>excel_import_export_demo</strong>.</p> |
|||
</div> |
|||
<div class="section" id="usage"> |
|||
<h1><a class="toc-backref" href="#id3">Usage</a></h1> |
|||
<p><strong>Use Case 1:</strong> Export/Import Excel on existing document</p> |
|||
<p>To test this use case, go to any Sales Order and use Export Excel or Import Excel in action menu.</p> |
|||
<p><strong>Use Case 2:</strong> Import Excel Files</p> |
|||
<p>To test this use case, go to Settings > Excel Import/Export > Sample Import Sales Order</p> |
|||
<p><strong>Use Case 3:</strong> Create Excel Report</p> |
|||
<p>To test this use case, go to Settings > Excel Import/Export > Sample Sales Report</p> |
|||
</div> |
|||
<div class="section" id="changelog"> |
|||
<h1><a class="toc-backref" href="#id4">Changelog</a></h1> |
|||
<div class="section" id="id1"> |
|||
<h2><a class="toc-backref" href="#id5">12.0.1.0.0 (2019-02-24)</a></h2> |
|||
<ul class="simple"> |
|||
<li>Start of the history</li> |
|||
</ul> |
|||
</div> |
|||
</div> |
|||
<div class="section" id="bug-tracker"> |
|||
<h1><a class="toc-backref" href="#id6">Bug Tracker</a></h1> |
|||
<p>Bugs are tracked on <a class="reference external" href="https://github.com/OCA/server-tools/issues">GitHub Issues</a>. |
|||
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 |
|||
<a class="reference external" href="https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_export_demo%0Aversion:%2012-add-excel_import_export%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">feedback</a>.</p> |
|||
<p>Do not contact contributors directly about support or help with technical issues.</p> |
|||
</div> |
|||
<div class="section" id="credits"> |
|||
<h1><a class="toc-backref" href="#id7">Credits</a></h1> |
|||
<div class="section" id="authors"> |
|||
<h2><a class="toc-backref" href="#id8">Authors</a></h2> |
|||
<ul class="simple"> |
|||
<li>Ecosoft</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="contributors"> |
|||
<h2><a class="toc-backref" href="#id9">Contributors</a></h2> |
|||
<ul class="simple"> |
|||
<li>Kitti Upariphutthiphong. <<a class="reference external" href="mailto:kittiu@gmail.com">kittiu@gmail.com</a>> (<a class="reference external" href="http://ecosoft.co.th">http://ecosoft.co.th</a>)</li> |
|||
</ul> |
|||
</div> |
|||
<div class="section" id="maintainers"> |
|||
<h2><a class="toc-backref" href="#id10">Maintainers</a></h2> |
|||
<p>This module is maintained by the OCA.</p> |
|||
<a class="reference external image-reference" href="https://odoo-community.org"><img alt="Odoo Community Association" src="https://odoo-community.org/logo.png" /></a> |
|||
<p>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.</p> |
|||
<p>Current <a class="reference external" href="https://odoo-community.org/page/maintainer-role">maintainer</a>:</p> |
|||
<p><a class="reference external" href="https://github.com/kittiu"><img alt="kittiu" src="https://github.com/kittiu.png?size=40px" /></a></p> |
|||
<p>This module is part of the <a class="reference external" href="https://github.com/OCA/server-tools/tree/12-add-excel_import_export/excel_import_export_demo">OCA/server-tools</a> project on GitHub.</p> |
|||
<p>You are welcome to contribute. To learn how please visit <a class="reference external" href="https://odoo-community.org/page/Contribute">https://odoo-community.org/page/Contribute</a>.</p> |
|||
</div> |
|||
</div> |
|||
</div> |
|||
</body> |
|||
</html> |
@ -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 |
@ -0,0 +1,130 @@ |
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) |
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) |
|||
from odoo.tests.common import SingleTransactionCase |
|||
|
|||
|
|||
class TestExcelImportExport(SingleTransactionCase): |
|||
|
|||
@classmethod |
|||
def setUpClass(cls): |
|||
super(TestExcelImportExport, cls).setUpClass() |
|||
|
|||
@classmethod |
|||
def setUpXLSXTemplate(cls): |
|||
cls.template_obj = cls.env['xlsx.template'] |
|||
# Create xlsx.template using input_instruction |
|||
input_instruction = { |
|||
'__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': { |
|||
'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()}', |
|||
} |
|||
vals = { |
|||
'res_model': 'sale.order', |
|||
'fname': 'sale_order.xlsx', |
|||
'name': 'Sale Order Template', |
|||
'description': 'Sample Sales Order Tempalte for testing', |
|||
'input_instruction': str(input_instruction), |
|||
} |
|||
cls.sample_template = cls.template_obj.create(vals) |
|||
|
|||
@classmethod |
|||
def setUpSaleOrder(cls): |
|||
cls.setUpPrepSaleOrder() |
|||
# Create a Sales Order |
|||
product_line = { |
|||
'name': cls.product_order.name, |
|||
'product_id': cls.product_order.id, |
|||
'product_uom_qty': 2, |
|||
'product_uom': cls.product_order.uom_id.id, |
|||
'price_unit': cls.product_order.list_price, |
|||
'tax_id': False, |
|||
} |
|||
cls.sale_order = cls.env['sale.order'].create({ |
|||
'partner_id': cls.partner.id, |
|||
'order_line': [(0, 0, product_line), (0, 0, product_line)], |
|||
}) |
|||
|
|||
@classmethod |
|||
def setUpManySaleOrder(cls): |
|||
cls.setUpPrepSaleOrder() |
|||
# Create a Sales Order |
|||
product_line = { |
|||
'name': cls.product_order.name, |
|||
'product_id': cls.product_order.id, |
|||
'product_uom_qty': 2, |
|||
'product_uom': cls.product_order.uom_id.id, |
|||
'price_unit': cls.product_order.list_price, |
|||
'tax_id': False, |
|||
} |
|||
for i in range(10): |
|||
cls.env['sale.order'].create({ |
|||
'partner_id': cls.partner.id, |
|||
'order_line': [(0, 0, product_line), (0, 0, product_line)], |
|||
}) |
|||
|
|||
@classmethod |
|||
def setUpPrepSaleOrder(cls): |
|||
categ_ids = cls.env['res.partner.category'].search([]).ids |
|||
cls.partner = cls.env['res.partner'].create({ |
|||
'name': 'Test Partner', |
|||
'category_id': [(6, 0, categ_ids)], |
|||
}) |
|||
# Create a Product |
|||
user_type_income = \ |
|||
cls.env.ref('account.data_account_type_direct_costs') |
|||
cls.account_income_product = cls.env['account.account'].create({ |
|||
'code': 'INCOME_PROD111', |
|||
'name': 'Icome - Test Account', |
|||
'user_type_id': user_type_income.id, |
|||
}) |
|||
# Create category |
|||
cls.product_category = cls.env['product.category'].create({ |
|||
'name': 'Product Category with Income account', |
|||
'property_account_income_categ_id': cls.account_income_product.id |
|||
}) |
|||
# Products |
|||
uom_unit = cls.env.ref('uom.product_uom_unit') |
|||
cls.product_order = cls.env['product.product'].create({ |
|||
'name': "Test Product", |
|||
'standard_price': 235.0, |
|||
'list_price': 280.0, |
|||
'type': 'consu', |
|||
'uom_id': uom_unit.id, |
|||
'uom_po_id': uom_unit.id, |
|||
'invoice_policy': 'order', |
|||
'expense_policy': 'no', |
|||
'default_code': 'PROD_ORDER', |
|||
'service_type': 'manual', |
|||
'taxes_id': False, |
|||
'categ_id': cls.product_category.id, |
|||
}) |
@ -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 .test_common import TestExcelImportExport |
|||
from odoo.tests.common import Form |
|||
|
|||
|
|||
class TestXLSXImportExport(TestExcelImportExport): |
|||
|
|||
@classmethod |
|||
def setUpClass(cls): |
|||
super(TestExcelImportExport, cls).setUpClass() |
|||
|
|||
def test_xlsx_export_import(self): |
|||
""" Test Export Excel from Sales Order """ |
|||
# Create Sales Order |
|||
self.setUpSaleOrder() |
|||
# ----------- EXPORT --------------- |
|||
ctx = {'active_model': 'sale.order', |
|||
'active_id': self.sale_order.id, |
|||
'template_domain': [('res_model', '=', 'sale.order'), |
|||
('fname', '=', 'sale_order.xlsx'), |
|||
('gname', '=', False)], } |
|||
f = Form(self.env['export.xlsx.wizard'].with_context(ctx)) |
|||
export_wizard = f.save() |
|||
# Test whether it loads correct template |
|||
self.assertEqual(export_wizard.template_id, |
|||
self.env.ref('excel_import_export_demo.' |
|||
'sale_order_xlsx_template')) |
|||
# Export excel |
|||
export_wizard.action_export() |
|||
self.assertTrue(export_wizard.data) |
|||
self.export_file = export_wizard.data |
|||
|
|||
# ----------- IMPORT --------------- |
|||
ctx = {'active_model': 'sale.order', |
|||
'active_id': self.sale_order.id, |
|||
'template_domain': [('res_model', '=', 'sale.order'), |
|||
('fname', '=', 'sale_order.xlsx'), |
|||
('gname', '=', False)], } |
|||
with Form(self.env['import.xlsx.wizard'].with_context(ctx)) as f: |
|||
f.import_file = self.export_file |
|||
import_wizard = f.save() |
|||
# Test whether it loads correct template |
|||
self.assertEqual(import_wizard.template_id, |
|||
self.env.ref('excel_import_export_demo.' |
|||
'sale_order_xlsx_template')) |
|||
# Import Excel |
|||
import_wizard.action_import() |
@ -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) |
|||
from .test_common import TestExcelImportExport |
|||
from odoo.tests.common import Form |
|||
|
|||
|
|||
class TestXLSXReport(TestExcelImportExport): |
|||
|
|||
@classmethod |
|||
def setUpClass(cls): |
|||
super(TestXLSXReport, cls).setUpClass() |
|||
|
|||
def test_xlsx_report(self): |
|||
""" Test Report from Sales Order """ |
|||
# Create Many Sales Orders |
|||
self.setUpManySaleOrder() |
|||
ctx = {'template_domain': [('res_model', '=', 'report.sale.order'), |
|||
('fname', '=', 'report_sale_order.xlsx'), |
|||
('gname', '=', False)], } |
|||
with Form(self.env['report.sale.order'].with_context(ctx)) as f: |
|||
f.partner_id = self.partner |
|||
report_wizard = f.save() |
|||
# Test whether it loads correct template |
|||
self.assertEqual(report_wizard.template_id, |
|||
self.env.ref('excel_import_export_demo.' |
|||
'report_sale_order_template')) |
|||
# Report excel |
|||
report_wizard.report_xlsx() |
|||
self.assertTrue(report_wizard.data) |
@ -0,0 +1,62 @@ |
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/) |
|||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) |
|||
from ast import literal_eval |
|||
from .test_common import TestExcelImportExport |
|||
|
|||
|
|||
class TestXLSXTemplate(TestExcelImportExport): |
|||
|
|||
@classmethod |
|||
def setUpClass(cls): |
|||
super(TestExcelImportExport, cls).setUpClass() |
|||
|
|||
def test_xlsx_tempalte(self): |
|||
""" Test XLSX Tempalte input and output instruction """ |
|||
self.setUpXLSXTemplate() |
|||
instruction_dict = literal_eval(self.sample_template.instruction) |
|||
self.assertDictEqual( |
|||
instruction_dict, |
|||
{ |
|||
'__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': { |
|||
'order_line': { |
|||
'A6': 'product_id', |
|||
'B6': 'name', |
|||
'C6': 'product_uom_qty', |
|||
'D6': 'product_uom', |
|||
'E6': 'price_unit', |
|||
'F6': 'tax_id', |
|||
} |
|||
} |
|||
}, |
|||
'__POST_IMPORT__': False |
|||
} |
|||
) |
|||
# Finally load excel file into this new template |
|||
self.assertFalse(self.sample_template.datas) # Not yet loaded |
|||
self.template_obj.load_xlsx_template([self.sample_template.id], |
|||
addon='excel_import_export_demo') |
|||
self.assertTrue(self.sample_template.datas) # Loaded successfully |
@ -1 +1,4 @@ |
|||
raven |
|||
raven |
|||
openpyxl |
|||
xlrd |
|||
xlwt |
Write
Preview
Loading…
Cancel
Save
Reference in new issue