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 100644pull/2505/head96157ea3
3b1217e8 M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
5b4d1fb1 M excel_import_export/models/styles.py :100644 100644ace11a32
185a3330 M excel_import_export/models/xlsx_export.py :100644 100644881b814f
cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 10064458689ee5
80490ce8 M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
5b4d1fb1 M excel_import_export/models/styles.py :100644 100644ace11a32
185a3330 M excel_import_export/models/xlsx_export.py :100644 100644881b814f
cadfb0f2 M excel_import_export/models/xlsx_import.py :100644 10064458689ee5
80490ce8 M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
a7d6adc5 M excel_import_export/models/xlsx_export.py :100644 100644881b814f
12f9ca99 M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
578a1fd8 M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
c7db3f92
M excel_import_export/models/xlsx_export.py :100644 100644881b814f
12f9ca99 M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
578a1fd8 M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
c7db3f92
M excel_import_export/models/xlsx_export.py :100644 100644881b814f
12f9ca99 M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
e3826e08 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 0000000018618688
A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 c8481487 A excel_import_export/tests/test_xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
c7db3f92
M excel_import_export/models/xlsx_export.py :100644 100644881b814f
12f9ca99 M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
ed8c9fc7 M excel_import_export/models/xlsx_template.py :000000 100644 00000000 34aa53bf A excel_import_export/tests/__init__.py :000000 100644 0000000018618688
A excel_import_export/tests/sale_order.xlsx :000000 100644 00000000 69aa6ea0 A excel_import_export/tests/test_xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644475b5187
392fe6e5 M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
45ee33c6
M excel_import_export_demo/report_sale_order/templates.xml :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
c7db3f92
M excel_import_export/models/xlsx_export.py :100644 100644881b814f
933d8614
M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
1460473a
M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644a2d035ef
9463f279
M excel_import_export_demo/__manifest__.py :100644 100644475b5187
e7f1255b
M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
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 0000000018618688
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 00000000730605c1
A excel_import_export_demo/tests/test_xlsx_template.py :100644 10064496157ea3
fee958bc
M excel_import_export/__manifest__.py :100644 10064400ee3d9f
51c2572a
M excel_import_export/models/common.py :100644 100644a215d29b
9738a3c8
M excel_import_export/models/styles.py :100644 100644ace11a32
c7db3f92
M excel_import_export/models/xlsx_export.py :100644 100644881b814f
933d8614
M excel_import_export/models/xlsx_import.py :100644 10064470c37799
f123d2a6
M excel_import_export/models/xlsx_report.py :100644 10064458689ee5
1460473a
M excel_import_export/models/xlsx_template.py :100644 1006445c9c09a6
a363ad19
M excel_import_export/views/xlsx_template_view.xml :100644 100644800ea573
1807ea7e
M excel_import_export/wizard/export_xlsx_wizard.py :100644 100644febed8d0
750dc17e
M excel_import_export/wizard/import_xlsx_wizard.py :100644 100644a2d035ef
9463f279
M excel_import_export_demo/__manifest__.py :100644 100644475b5187
e7f1255b
M excel_import_export_demo/import_export_sale_order/templates.xml :100644 1006448e40a2d0
21574896
M excel_import_export_demo/report_sale_order/report_sale_order.py :100644 1006444af9c519
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 0000000018618688
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 00000000730605c1
A excel_import_export_demo/tests/test_xlsx_template.py
parent
7fdeff00ae
commit
e7ee17505a
|
@ -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>
|
Loading…
Reference in New Issue