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