mirror of
https://github.com/bringout/oca-technical.git
synced 2026-04-18 06:32:00 +02:00
Initial commit: OCA Technical packages (595 packages)
This commit is contained in:
commit
2cc02aac6e
24950 changed files with 2318079 additions and 0 deletions
46
odoo-bringout-oca-server-tools-excel_import_export/README.md
Normal file
46
odoo-bringout-oca-server-tools-excel_import_export/README.md
Normal file
|
|
@ -0,0 +1,46 @@
|
|||
# Excel Import/Export/Report
|
||||
|
||||
Odoo addon: excel_import_export
|
||||
|
||||
## Installation
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-server-tools-excel_import_export
|
||||
```
|
||||
|
||||
## Dependencies
|
||||
|
||||
This addon depends on:
|
||||
- mail
|
||||
|
||||
## Manifest Information
|
||||
|
||||
- **Name**: Excel Import/Export/Report
|
||||
- **Version**: 16.0.1.3.1
|
||||
- **Category**: Tools
|
||||
- **License**: AGPL-3
|
||||
- **Installable**: True
|
||||
|
||||
## Source
|
||||
|
||||
Based on [OCA/server-tools](https://github.com/OCA/server-tools) branch 16.0, addon `excel_import_export`.
|
||||
|
||||
## License
|
||||
|
||||
This package maintains the original AGPL-3 license from the upstream Odoo project.
|
||||
|
||||
## Documentation
|
||||
|
||||
- Overview: doc/OVERVIEW.md
|
||||
- Architecture: doc/ARCHITECTURE.md
|
||||
- Models: doc/MODELS.md
|
||||
- Controllers: doc/CONTROLLERS.md
|
||||
- Wizards: doc/WIZARDS.md
|
||||
- Reports: doc/REPORTS.md
|
||||
- Security: doc/SECURITY.md
|
||||
- Install: doc/INSTALL.md
|
||||
- Usage: doc/USAGE.md
|
||||
- Configuration: doc/CONFIGURATION.md
|
||||
- Dependencies: doc/DEPENDENCIES.md
|
||||
- Troubleshooting: doc/TROUBLESHOOTING.md
|
||||
- FAQ: doc/FAQ.md
|
||||
|
|
@ -0,0 +1,32 @@
|
|||
# Architecture
|
||||
|
||||
```mermaid
|
||||
flowchart TD
|
||||
U[Users] -->|HTTP| V[Views and QWeb Templates]
|
||||
V --> C[Controllers]
|
||||
V --> W[Wizards – Transient Models]
|
||||
C --> M[Models and ORM]
|
||||
W --> M
|
||||
M --> R[Reports]
|
||||
DX[Data XML] --> M
|
||||
S[Security – ACLs and Groups] -. enforces .-> M
|
||||
|
||||
subgraph Excel_import_export Module - excel_import_export
|
||||
direction LR
|
||||
M:::layer
|
||||
W:::layer
|
||||
C:::layer
|
||||
V:::layer
|
||||
R:::layer
|
||||
S:::layer
|
||||
DX:::layer
|
||||
end
|
||||
|
||||
classDef layer fill:#eef8ff,stroke:#6ea8fe,stroke-width:1px
|
||||
```
|
||||
|
||||
Notes
|
||||
- Views include tree/form/kanban templates and report templates.
|
||||
- Controllers provide website/portal routes when present.
|
||||
- Wizards are UI flows implemented with `models.TransientModel`.
|
||||
- Data XML loads data/demo records; Security defines groups and access.
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Configuration
|
||||
|
||||
Refer to Odoo settings for excel_import_export. Configure related models, access rights, and options as needed.
|
||||
|
|
@ -0,0 +1,17 @@
|
|||
# Controllers
|
||||
|
||||
HTTP routes provided by this module.
|
||||
|
||||
```mermaid
|
||||
sequenceDiagram
|
||||
participant U as User/Client
|
||||
participant C as Module Controllers
|
||||
participant O as ORM/Views
|
||||
|
||||
U->>C: HTTP GET/POST (routes)
|
||||
C->>O: ORM operations, render templates
|
||||
O-->>U: HTML/JSON/PDF
|
||||
```
|
||||
|
||||
Notes
|
||||
- See files in controllers/ for route definitions.
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Dependencies
|
||||
|
||||
This addon depends on:
|
||||
|
||||
- [mail](../../odoo-bringout-oca-ocb-mail)
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# FAQ
|
||||
|
||||
- Q: Which Odoo version? A: 16.0 (OCA/OCB packaged).
|
||||
- Q: How to enable? A: Start server with --addon excel_import_export or install in UI.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Install
|
||||
|
||||
```bash
|
||||
pip install odoo-bringout-oca-server-tools-excel_import_export"
|
||||
# or
|
||||
uv pip install odoo-bringout-oca-server-tools-excel_import_export"
|
||||
```
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
# Models
|
||||
|
||||
Detected core models and extensions in excel_import_export.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class xlsx_export
|
||||
class xlsx_import
|
||||
class xlsx_report
|
||||
class xlsx_styles
|
||||
class xlsx_template
|
||||
class xlsx_template_export
|
||||
class xlsx_template_import
|
||||
class ir_actions_report
|
||||
```
|
||||
|
||||
Notes
|
||||
- Classes show model technical names; fields omitted for brevity.
|
||||
- Items listed under _inherit are extensions of existing models.
|
||||
|
|
@ -0,0 +1,6 @@
|
|||
# Overview
|
||||
|
||||
Packaged Odoo addon: excel_import_export. Provides features documented in upstream Odoo 16 under this addon.
|
||||
|
||||
- Source: OCA/OCB 16.0, addon excel_import_export
|
||||
- License: LGPL-3
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
# Reports
|
||||
|
||||
This module does not define custom reports.
|
||||
|
|
@ -0,0 +1,34 @@
|
|||
# Security
|
||||
|
||||
Access control and security definitions in excel_import_export.
|
||||
|
||||
## Access Control Lists (ACLs)
|
||||
|
||||
Model access permissions defined in:
|
||||
- **[ir.model.access.csv](../excel_import_export/security/ir.model.access.csv)**
|
||||
- 6 model access rules
|
||||
|
||||
## Record Rules
|
||||
|
||||
Row-level security rules defined in:
|
||||
|
||||
```mermaid
|
||||
graph TB
|
||||
subgraph "Security Layers"
|
||||
A[Users] --> B[Groups]
|
||||
B --> C[Access Control Lists]
|
||||
C --> D[Models]
|
||||
B --> E[Record Rules]
|
||||
E --> F[Individual Records]
|
||||
end
|
||||
```
|
||||
|
||||
Security files overview:
|
||||
- **[ir.model.access.csv](../excel_import_export/security/ir.model.access.csv)**
|
||||
- Model access permissions (CRUD rights)
|
||||
|
||||
Notes
|
||||
- Access Control Lists define which groups can access which models
|
||||
- Record Rules provide row-level security (filter records by user/group)
|
||||
- Security groups organize users and define permission sets
|
||||
- All security is enforced at the ORM level by Odoo
|
||||
|
|
@ -0,0 +1,5 @@
|
|||
# Troubleshooting
|
||||
|
||||
- Ensure Python and Odoo environment matches repo guidance.
|
||||
- Check database connectivity and logs if startup fails.
|
||||
- Validate that dependent addons listed in DEPENDENCIES.md are installed.
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
# Usage
|
||||
|
||||
Start Odoo including this addon (from repo root):
|
||||
|
||||
```bash
|
||||
python3 scripts/nix_odoo_web_server.py --db-name mydb --addon excel_import_export
|
||||
```
|
||||
|
|
@ -0,0 +1,10 @@
|
|||
# Wizards
|
||||
|
||||
Transient models exposed as UI wizards in excel_import_export.
|
||||
|
||||
```mermaid
|
||||
classDiagram
|
||||
class ExportXLSXWizard
|
||||
class ImportXLSXWizard
|
||||
class ReportXLSXWizard
|
||||
```
|
||||
|
|
@ -0,0 +1,204 @@
|
|||
==========================
|
||||
Excel Import/Export/Report
|
||||
==========================
|
||||
|
||||
..
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:40c58b91ccaaaa2b5ead94e8975df9d77a9ae3d08e29f5647422c2a3e1dafd6b
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
|
||||
.. |badge1| image:: https://img.shields.io/badge/maturity-Beta-yellow.png
|
||||
:target: https://odoo-community.org/page/development-status
|
||||
:alt: Beta
|
||||
.. |badge2| 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
|
||||
.. |badge3| image:: https://img.shields.io/badge/github-OCA%2Fserver--tools-lightgray.png?logo=github
|
||||
:target: https://github.com/OCA/server-tools/tree/16.0/excel_import_export
|
||||
:alt: OCA/server-tools
|
||||
.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png
|
||||
:target: https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-excel_import_export
|
||||
:alt: Translate me on Weblate
|
||||
.. |badge5| image:: https://img.shields.io/badge/runboat-Try%20me-875A7B.png
|
||||
:target: https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0
|
||||
:alt: Try me on Runboat
|
||||
|
||||
|badge1| |badge2| |badge3| |badge4| |badge5|
|
||||
|
||||
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 demo, install **excel_import_export_demo**
|
||||
|
||||
Configuration
|
||||
=============
|
||||
|
||||
If you have existing templates from the version 16.0.1.2.0 or earlier, you need to click 'REMOVE EXPORT ACTION'
|
||||
and then click 'ADD EXPORT ACTION' in these templates for export actions to work as expected.
|
||||
|
||||
Usage
|
||||
=====
|
||||
|
||||
Concepts
|
||||
~~~~~~~~
|
||||
|
||||
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_demo**
|
||||
|
||||
Use Cases
|
||||
~~~~~~~~~
|
||||
|
||||
**Use Case 1:** Export/Import Excel on existing document
|
||||
|
||||
This add export/import action menus in existing document (example - excel_import_export_demo/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_demo/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_demo/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
|
||||
|
||||
**Note:**
|
||||
|
||||
Another option for reporting is to use report action (report_type='excel'), I.e.,
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<report id='action_report_saleorder_excel'
|
||||
string='Quotation / Order (.xlsx)'
|
||||
model='sale.order'
|
||||
name='sale_order.xlsx'
|
||||
file='sale_order'
|
||||
report_type='excel'
|
||||
/>
|
||||
|
||||
By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record.
|
||||
Please see example in excel_import_export_demo/report_action, which shows,
|
||||
|
||||
1. Print excel from an active sale.order
|
||||
2. Run partner list report based on search criteria.
|
||||
|
||||
Easy Reporting Option
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Technically, this option is the same as "Create Excel Report" use case. But instead of having to write XML / Python code like normally do,
|
||||
this option allow user to create a report based on a model or view, all by configuration only.
|
||||
|
||||
1. Goto > Technical> Excel Import/Export > XLSX Templates, and create a new template for a report.
|
||||
2. On the new template, select "Easy Reporting" option, then select followings
|
||||
- Report Model, this can be data model or data view we want to get the results from.
|
||||
- Click upload your file and add the excel template (.xlsx)
|
||||
- Click Save, system will create sample export line, user can add more fields according to results model.
|
||||
3. Click Add Report Menu, the report menu will be created, user can change its location. Now the report is ready to use.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/xlsx_template.png
|
||||
:width: 800 px
|
||||
|
||||
Note: Using easy reporting mode, system will used a common criteria wizard.
|
||||
|
||||
.. figure:: https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/common_wizard.png
|
||||
:width: 800 px
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
- Module extension e.g., excel_import_export_async, that add ability to execute as async process.
|
||||
|
||||
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 to smash it by providing a detailed and welcomed
|
||||
`feedback <https://github.com/OCA/server-tools/issues/new?body=module:%20excel_import_export%0Aversion:%2016.0%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)
|
||||
* Saran Lim. <saranl@ecosoft.co.th> (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/16.0/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,6 @@
|
|||
# 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
|
||||
from . import controllers
|
||||
|
|
@ -0,0 +1,30 @@
|
|||
# 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/Report",
|
||||
"summary": "Base module for developing Excel import/export/report",
|
||||
"version": "16.0.1.3.1",
|
||||
"author": "Ecosoft,Odoo Community Association (OCA)",
|
||||
"license": "AGPL-3",
|
||||
"website": "https://github.com/OCA/server-tools",
|
||||
"category": "Tools",
|
||||
"depends": ["mail"],
|
||||
"external_dependencies": {"python": ["openpyxl"]},
|
||||
"data": [
|
||||
"security/ir.model.access.csv",
|
||||
"wizard/export_xlsx_wizard.xml",
|
||||
"wizard/import_xlsx_wizard.xml",
|
||||
"wizard/report_xlsx_wizard.xml",
|
||||
"views/xlsx_template_view.xml",
|
||||
"views/xlsx_report.xml",
|
||||
],
|
||||
"installable": True,
|
||||
"development_status": "Beta",
|
||||
"maintainers": ["kittiu"],
|
||||
"assets": {
|
||||
"web.assets_backend": [
|
||||
"/excel_import_export/static/src/js/report/action_manager_report.esm.js"
|
||||
]
|
||||
},
|
||||
}
|
||||
|
|
@ -0,0 +1,4 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from . import report
|
||||
|
|
@ -0,0 +1,94 @@
|
|||
# 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 json
|
||||
import logging
|
||||
|
||||
from werkzeug.urls import url_decode
|
||||
|
||||
from odoo import http
|
||||
from odoo.http import content_disposition, request, route, serialize_exception
|
||||
from odoo.tools import html_escape
|
||||
from odoo.tools.safe_eval import safe_eval, time
|
||||
|
||||
from odoo.addons.web.controllers import report
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportController(report.ReportController):
|
||||
@route()
|
||||
def report_routes(self, reportname, docids=None, converter=None, **data):
|
||||
if converter == "excel":
|
||||
report = request.env["ir.actions.report"]._get_report_from_name(reportname)
|
||||
context = dict(request.env.context)
|
||||
if docids:
|
||||
docids = [int(i) for i in docids.split(",")]
|
||||
if data.get("options"):
|
||||
data.update(json.loads(data.pop("options")))
|
||||
if data.get("context"):
|
||||
# Ignore 'lang' here, because the context in data is the one
|
||||
# from the webclient *but* if the user explicitely wants to
|
||||
# change the lang, this mechanism overwrites it.
|
||||
data["context"] = json.loads(data["context"])
|
||||
if data["context"].get("lang"):
|
||||
del data["context"]["lang"]
|
||||
context.update(data["context"])
|
||||
|
||||
excel, report_name = report.with_context(**context)._render_excel(
|
||||
docids, data=data
|
||||
)
|
||||
excel = base64.decodebytes(excel)
|
||||
if docids:
|
||||
records = request.env[report.model].browse(docids)
|
||||
if report.print_report_name and not len(records) > 1:
|
||||
# this is a bad idea, this should only be .xlsx
|
||||
extension = report_name.split(".")[-1:].pop()
|
||||
report_name = safe_eval(
|
||||
report.print_report_name, {"object": records, "time": time}
|
||||
)
|
||||
report_name = f"{report_name}.{extension}"
|
||||
excelhttpheaders = [
|
||||
(
|
||||
"Content-Type",
|
||||
"application/vnd.openxmlformats-"
|
||||
"officedocument.spreadsheetml.sheet",
|
||||
),
|
||||
("Content-Length", len(excel)),
|
||||
("Content-Disposition", content_disposition(report_name)),
|
||||
]
|
||||
return request.make_response(excel, headers=excelhttpheaders)
|
||||
return super().report_routes(reportname, docids, converter, **data)
|
||||
|
||||
@http.route()
|
||||
def report_download(self, data, context=None):
|
||||
requestcontent = json.loads(data)
|
||||
url, report_type = requestcontent[0], requestcontent[1]
|
||||
if report_type != "excel":
|
||||
return super().report_download(data, context)
|
||||
reportname = "???"
|
||||
try:
|
||||
pattern = "/report/excel/"
|
||||
reportname = url.split(pattern)[1].split("?")[0]
|
||||
docids = None
|
||||
if "/" in reportname:
|
||||
reportname, docids = reportname.split("/")
|
||||
if docids:
|
||||
return self.report_routes(
|
||||
reportname, docids=docids, converter="excel", context=context
|
||||
)
|
||||
data = dict(url_decode(url.split("?")[1]).items())
|
||||
if "context" in data:
|
||||
context, data_context = json.loads(context or "{}"), json.loads(
|
||||
data.pop("context")
|
||||
)
|
||||
context = json.dumps({**context, **data_context})
|
||||
return self.report_routes(
|
||||
reportname, converter="excel", context=context, **data
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.exception("Error while generating report %s", reportname)
|
||||
se = serialize_exception(e)
|
||||
error = {"code": 200, "message": "Odoo Server Error", "data": se}
|
||||
return request.make_response(html_escape(json.dumps(error)))
|
||||
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
File diff suppressed because it is too large
Load diff
|
|
@ -0,0 +1,23 @@
|
|||
# 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
|
||||
from . import ir_report
|
||||
|
||||
#
|
||||
#
|
||||
# INSERT INTO "purchase_order_line" (
|
||||
# "id", "create_uid", "create_date",
|
||||
# "write_uid", "write_date", "date_planned",
|
||||
# "display_type", "name", "order_id",
|
||||
# "price_unit", "product_qty", "product_uom",
|
||||
# "sequence") VALUES (
|
||||
# nextval('purchase_order_line_id_seq'), 2, (now() at time zone 'UTC'),
|
||||
# 2, (now() at time zone 'UTC'), '2020-10-05 09:39:28',
|
||||
# NULL, '[FURN_0269] Office Chair Black', 8,
|
||||
# '11111.00', '5.000', 1,
|
||||
# 10)
|
||||
|
|
@ -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 csv
|
||||
import itertools
|
||||
import re
|
||||
import string
|
||||
import uuid
|
||||
from ast import literal_eval
|
||||
from datetime import datetime as dt
|
||||
from io import StringIO
|
||||
|
||||
import xlrd
|
||||
from dateutil.parser import parse
|
||||
|
||||
from odoo import _
|
||||
from odoo.exceptions import ValidationError
|
||||
|
||||
|
||||
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 = "{}{}".format(col, row + k)
|
||||
value = value.replace("?(%s)" % val, new_val)
|
||||
return value
|
||||
|
||||
|
||||
def get_field_aggregation(field):
|
||||
"""i..e, 'field@{sum}'"""
|
||||
if field and "@{" in field and "}" in field:
|
||||
i = field.index("@{")
|
||||
j = field.index("}", i)
|
||||
cond = field[i + 2 : j]
|
||||
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(";") if field_style else []
|
||||
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 %(value)s for style type %(key)s")
|
||||
% {"value": value, "key": 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 as exc:
|
||||
raise ValidationError(_("'%s' sheet not found") % (name,)) from exc
|
||||
|
||||
|
||||
def isfloat(input_val):
|
||||
try:
|
||||
float(input_val)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def isinteger(input_val):
|
||||
try:
|
||||
int(input_val)
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def isdatetime(input_val):
|
||||
try:
|
||||
if len(input_val) == 10:
|
||||
dt.strptime(input_val, "%Y-%m-%d")
|
||||
elif len(input_val) == 19:
|
||||
dt.strptime(input_val, "%Y-%m-%d %H:%M:%S")
|
||||
else:
|
||||
return False
|
||||
return True
|
||||
except ValueError:
|
||||
return False
|
||||
|
||||
|
||||
def str_to_number(input_val):
|
||||
if isinstance(input_val, str):
|
||||
if " " not in input_val:
|
||||
if isdatetime(input_val):
|
||||
return parse(input_val)
|
||||
elif isinteger(input_val):
|
||||
if not (len(input_val) > 1 and input_val[:1] == "0"):
|
||||
return int(input_val)
|
||||
elif isfloat(input_val):
|
||||
if not (input_val.find(".") > 2 and input_val[:1] == "0"):
|
||||
return float(input_val)
|
||||
return input_val
|
||||
|
||||
|
||||
def csv_from_excel(excel_content, delimiter, quote):
|
||||
wb = xlrd.open_workbook(file_contents=excel_content)
|
||||
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 = 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 in ("xldate", "number"):
|
||||
is_datetime = cell.value % 1 != 0.0
|
||||
time_tuple = xlrd.xldate_as_tuple(cell.value, datemode)
|
||||
date = dt(*time_tuple)
|
||||
value = (
|
||||
date.strftime("%Y-%m-%d %H:%M:%S")
|
||||
if is_datetime
|
||||
else date.strftime("%Y-%m-%d")
|
||||
)
|
||||
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 = f"__excel_import_export__.{uuid.uuid4()},{line}"
|
||||
txt_lines.append(line)
|
||||
i += 1
|
||||
file_txt = "\n".join(txt_lines)
|
||||
return file_txt
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError
|
||||
|
||||
|
||||
class ReportAction(models.Model):
|
||||
_inherit = "ir.actions.report"
|
||||
|
||||
report_type = fields.Selection(
|
||||
selection_add=[("excel", "Excel")], ondelete={"excel": "cascade"}
|
||||
)
|
||||
|
||||
@api.model
|
||||
def _render_excel(self, docids, data):
|
||||
if len(docids) != 1:
|
||||
raise UserError(_("Only one id is allowed for excel_import_export"))
|
||||
xlsx_template = self.env["xlsx.template"].search(
|
||||
[("fname", "=", self.report_name), ("res_model", "=", self.model)]
|
||||
)
|
||||
if not xlsx_template or len(xlsx_template) != 1:
|
||||
raise UserError(
|
||||
_("Template %(report_name)s on model %(model)s is not unique!")
|
||||
% {"report_name": self.report_name, "model": self.model}
|
||||
)
|
||||
Export = self.env["xlsx.export"]
|
||||
return Export.export_xlsx(xlsx_template, self.model, docids[0])
|
||||
|
||||
@api.model
|
||||
def _get_report_from_name(self, report_name):
|
||||
res = super(ReportAction, self)._get_report_from_name(report_name)
|
||||
if res:
|
||||
return res
|
||||
report_obj = self.env["ir.actions.report"]
|
||||
qwebtypes = ["excel"]
|
||||
conditions = [
|
||||
("report_type", "in", qwebtypes),
|
||||
("report_name", "=", report_name),
|
||||
]
|
||||
context = self.env["res.users"].context_get()
|
||||
return report_obj.with_context(**context).search(conditions, limit=1)
|
||||
|
|
@ -0,0 +1,47 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
import logging
|
||||
|
||||
from odoo import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
try:
|
||||
from openpyxl.styles import Alignment, Font, PatternFill
|
||||
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="FF0000", 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,313 @@
|
|||
# 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 logging
|
||||
import os
|
||||
import zipfile
|
||||
from datetime import date, datetime as dt
|
||||
from io import BytesIO
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from . import common as co
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
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,
|
||||
"datetime": dt,
|
||||
"date": date,
|
||||
"value": value,
|
||||
"object": record,
|
||||
"model": self.env[model],
|
||||
"env": self.env,
|
||||
"context": self._context,
|
||||
}
|
||||
return eval_context
|
||||
|
||||
def _get_conditions_dict(self):
|
||||
return {
|
||||
"field_cond_dict": {},
|
||||
"field_style_dict": {},
|
||||
"style_cond_dict": {},
|
||||
"aggre_func_dict": {},
|
||||
}
|
||||
|
||||
def run_field_cond_dict(self, field):
|
||||
temp_field, eval_cond = co.get_field_condition(field)
|
||||
eval_cond = eval_cond or 'value or ""'
|
||||
return temp_field, eval_cond
|
||||
|
||||
def run_field_style_dict(self, field):
|
||||
return co.get_field_style(field)
|
||||
|
||||
def run_style_cond_dict(self, field):
|
||||
return co.get_field_style_cond(field)
|
||||
|
||||
def run_aggre_func_dict(self, field):
|
||||
return co.get_field_aggregation(field)
|
||||
|
||||
def apply_extra_conditions_to_value(self, field, value, conditions_dict):
|
||||
return value
|
||||
|
||||
@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
|
||||
line_field = line_field.replace("_EXTEND_", "") # Remove _EXTEND_ 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 = {field: [] for field in fields} # value and do_style
|
||||
# Get field condition & aggre function
|
||||
conditions_dict = self._get_conditions_dict()
|
||||
|
||||
pair_fields = [] # I.e., ('debit${value and . or .}@{sum}', 'debit')
|
||||
for field in fields:
|
||||
raw_field = field
|
||||
for key, condition_dict in conditions_dict.items():
|
||||
run_func_name = "run_" + key
|
||||
raw_field, get_result = getattr(self, run_func_name, None)(raw_field)
|
||||
condition_dict.update({field: get_result})
|
||||
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 = conditions_dict["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)
|
||||
value = self.apply_extra_conditions_to_value(
|
||||
field, value, conditions_dict
|
||||
)
|
||||
# style w/Cond takes priority
|
||||
style_cond = conditions_dict["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 = conditions_dict["field_style_dict"][
|
||||
field[0]
|
||||
] # Use default style
|
||||
vals[field[0]].append((value, style))
|
||||
return (vals, conditions_dict["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) from e
|
||||
except IllegalCharacterError as e:
|
||||
raise ValidationError(
|
||||
_(
|
||||
"IllegalCharacterError\n"
|
||||
"Some exporting data contain special character\n%s"
|
||||
)
|
||||
% e
|
||||
) from e
|
||||
except Exception as e:
|
||||
raise ValidationError(
|
||||
_("Error filling data into Excel sheets\n%s") % e
|
||||
) from 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
|
||||
is_extend = "_EXTEND_" in line_field and True or False # extend 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 = "{}{}".format(col, cont_set)
|
||||
i = 0
|
||||
new_row = 0
|
||||
new_rc = False
|
||||
row_count = len(vals[field])
|
||||
# Insert rows to preserve total line
|
||||
if is_extend and not rows_inserted:
|
||||
rows_inserted = True
|
||||
st.insert_rows(row + 1, row_count - 1)
|
||||
# --
|
||||
for (row_val, style) in vals[field]:
|
||||
new_row = row + i
|
||||
new_rc = "{}{}".format(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 = "{}{}".format(col, new_row)
|
||||
st[f_rc] = "={}({}:{})".format(f, rc, new_rc)
|
||||
styles = self.env["xlsx.styles"].get_openpyxl_styles()
|
||||
co.fill_cell_style(st[f_rc], style, styles)
|
||||
cont_row = cont_row < new_row and new_row or cont_row
|
||||
return
|
||||
|
||||
@api.model
|
||||
def export_xlsx(self, template, res_model, res_ids):
|
||||
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.decodebytes(template.datas)
|
||||
ConfParam = self.env["ir.config_parameter"].sudo()
|
||||
ptemp = ConfParam.get_param("path_temp_file") or "/tmp"
|
||||
stamp = dt.utcnow().strftime("%H%M%S%f")[:-3]
|
||||
ftemp = "{}/temp{}.xlsx".format(ptemp, stamp)
|
||||
# Start working with workbook
|
||||
records = res_model and self.env[res_model].browse(res_ids) or False
|
||||
outputs = []
|
||||
for record in records:
|
||||
f = open(ftemp, "wb")
|
||||
f.write(decoded_data)
|
||||
f.seek(0)
|
||||
f.close()
|
||||
# Workbook created, temp file removed
|
||||
wb = load_workbook(ftemp)
|
||||
os.remove(ftemp)
|
||||
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 = 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 = "{}_{}".format(fname, ts.strftime("%Y%m%d_%H%M%S"))
|
||||
if not out_name or len(out_name) == 0:
|
||||
out_name = "noname"
|
||||
out_ext = "xlsx"
|
||||
# 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
|
||||
outputs.append((out_file, "{}.{}".format(out_name, out_ext)))
|
||||
# If outputs > 1 files, zip it
|
||||
if len(outputs) > 1:
|
||||
zip_buffer = BytesIO()
|
||||
with zipfile.ZipFile(
|
||||
zip_buffer, "a", zipfile.ZIP_DEFLATED, False
|
||||
) as zip_file:
|
||||
for data, file_name in outputs:
|
||||
zip_file.writestr(file_name, data)
|
||||
zip_buffer.seek(0)
|
||||
out_file = base64.encodebytes(zip_buffer.read())
|
||||
out_name = "files.zip"
|
||||
return (out_file, out_name)
|
||||
else:
|
||||
(out_file, out_name) = outputs[0]
|
||||
return (base64.encodebytes(out_file), out_name)
|
||||
|
|
@ -0,0 +1,309 @@
|
|||
# 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
|
||||
from ast import literal_eval
|
||||
from datetime import date, datetime as dt
|
||||
from io import BytesIO
|
||||
|
||||
import xlrd
|
||||
import xlwt
|
||||
|
||||
from odoo import _, api, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.tools.float_utils import float_compare
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
from . import common as co
|
||||
|
||||
|
||||
class XLSXImport(models.AbstractModel):
|
||||
_name = "xlsx.import"
|
||||
_description = "Excel Import AbstractModel"
|
||||
|
||||
@api.model
|
||||
def get_eval_context(self, model=False, value=False):
|
||||
eval_context = {
|
||||
"float_compare": float_compare,
|
||||
"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.sudo().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": "{}_{}".format(record._table, record.id),
|
||||
"module": "__excel_import_export__",
|
||||
"model": record._name,
|
||||
"res_id": record.id,
|
||||
}
|
||||
)
|
||||
xml_id = record.get_external_id()
|
||||
return xml_id[record.id]
|
||||
|
||||
@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 as exc:
|
||||
raise ValidationError(
|
||||
_("Invalid declaration, %s has no valid field type") % field
|
||||
) from exc
|
||||
|
||||
@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.copy().items():
|
||||
for f, _fv in data_dict[s].copy().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) from e
|
||||
|
||||
@api.model
|
||||
def _get_end_row(self, st, worksheet, line_field):
|
||||
"""Get max row or next empty row as the ending row"""
|
||||
_x, max_row = co.get_line_max(line_field)
|
||||
test_rows = {}
|
||||
max_end_row = 0
|
||||
for rc, _col in worksheet.get(line_field, {}).items():
|
||||
rc, key_eval_cond = co.get_field_condition(rc)
|
||||
row, col = co.pos2idx(rc)
|
||||
# Use max_row, i.e., order_line[5], use it. Otherwise, use st.nrows
|
||||
max_end_row = st.nrows if max_row is False else (row + max_row)
|
||||
for idx in range(row, max_row and max_end_row or st.nrows):
|
||||
try:
|
||||
cell_type = st.cell_type(idx, col) # empty type = 0
|
||||
except Exception as e:
|
||||
raise UserError(
|
||||
_(
|
||||
"The value for the '%(field)s' field is expected to be "
|
||||
"in cell %(cell_position)s, but no column exists for that "
|
||||
"cell in the Excel sheet. Please check your Excel file."
|
||||
)
|
||||
% {"field": _col, "cell_position": rc}
|
||||
) from e
|
||||
r_types = test_rows.get(idx, [])
|
||||
r_types.append(cell_type)
|
||||
test_rows[idx] = r_types
|
||||
empty_list = filter(lambda y: all(i == 0 for i in y[1]), test_rows.items())
|
||||
empty_rows = list(map(lambda z: z[0], empty_list))
|
||||
next_empty_row = empty_rows and min(empty_rows) or max_end_row
|
||||
return next_empty_row
|
||||
|
||||
@api.model
|
||||
def _get_line_vals(self, st, worksheet, model, line_field):
|
||||
"""Get values of this field from excel sheet"""
|
||||
vals = {}
|
||||
end_row = self._get_end_row(st, worksheet, line_field)
|
||||
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)
|
||||
new_line_field, _x = co.get_line_max(line_field)
|
||||
out_field = "{}/{}".format(new_line_field, x_field)
|
||||
field_type = self._get_field_type(model, out_field)
|
||||
vals.update({out_field: []})
|
||||
for idx in range(row, end_row):
|
||||
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 _process_worksheet(self, wb, out_wb, out_st, model, data_dict, header_fields):
|
||||
col_idx = 1
|
||||
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)
|
||||
try:
|
||||
row, col = co.pos2idx(rc)
|
||||
value = co._get_cell_value(st.cell(row, col), field_type=field_type)
|
||||
except Exception:
|
||||
value = False
|
||||
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
|
||||
|
||||
@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 = []
|
||||
model = record._name
|
||||
decoded_data = base64.decodebytes(import_file)
|
||||
wb = xlrd.open_workbook(file_contents=decoded_data)
|
||||
out_wb = xlwt.Workbook()
|
||||
out_st = out_wb.add_sheet("Sheet 1")
|
||||
xml_id = (
|
||||
record
|
||||
and self.get_external_id(record)
|
||||
or "{}.{}".format("__excel_import_export__", 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")
|
||||
# Process on all worksheets
|
||||
self._process_worksheet(wb, out_wb, out_st, model, data_dict, header_fields)
|
||||
# --
|
||||
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.execute_import(
|
||||
header_fields,
|
||||
header_fields,
|
||||
{
|
||||
"has_headers": True,
|
||||
"advanced": True,
|
||||
"keep_matches": False,
|
||||
"encoding": "",
|
||||
"separator": "",
|
||||
"quoting": '"',
|
||||
"date_format": "%Y-%m-%d",
|
||||
"datetime_format": "%Y-%m-%d %H:%M:%S",
|
||||
"float_thousand_separator": ",",
|
||||
"float_decimal_separator": ".",
|
||||
"fields": [],
|
||||
},
|
||||
)
|
||||
if errors.get("messages"):
|
||||
message = _("Error importing data")
|
||||
messages = errors["messages"]
|
||||
if isinstance(messages, dict):
|
||||
message = messages["message"]
|
||||
if isinstance(messages, list):
|
||||
message = ", ".join([x["message"] for x in messages])
|
||||
raise ValidationError(message.encode("utf-8"))
|
||||
return self.env.ref(xml_id)
|
||||
except xlrd.XLRDError as exc:
|
||||
raise ValidationError(
|
||||
_("Invalid file style, only .xls or .xlsx file allowed")
|
||||
) from exc
|
||||
except Exception as e:
|
||||
raise 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) from 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__']
|
||||
"""
|
||||
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,56 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.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
|
||||
|
||||
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",
|
||||
"res_id": self.id,
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
}
|
||||
|
|
@ -0,0 +1,655 @@
|
|||
# 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 os
|
||||
from ast import literal_eval
|
||||
from os.path import join as opj
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import UserError, ValidationError
|
||||
from odoo.modules.module import get_module_path
|
||||
|
||||
from . import common as co
|
||||
|
||||
|
||||
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()
|
||||
input_instruction = fields.Text(
|
||||
string="Instruction (Input)",
|
||||
help="This is used to construct instruction in tab Import/Export",
|
||||
)
|
||||
instruction = fields.Text(
|
||||
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,
|
||||
help="Convert file into CSV format on export",
|
||||
)
|
||||
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",
|
||||
)
|
||||
# Utilities
|
||||
export_action_id = fields.Many2one(
|
||||
comodel_name="ir.actions.act_window",
|
||||
ondelete="set null",
|
||||
)
|
||||
import_action_id = fields.Many2one(
|
||||
comodel_name="ir.actions.act_window",
|
||||
ondelete="set null",
|
||||
)
|
||||
use_report_wizard = fields.Boolean(
|
||||
string="Easy Reporting",
|
||||
help="Use common report wizard model, instead of create specific model",
|
||||
)
|
||||
result_model_id = fields.Many2one(
|
||||
comodel_name="ir.model",
|
||||
string="Report Model",
|
||||
help="When use commone wizard, choose the result model",
|
||||
)
|
||||
result_field = fields.Char(
|
||||
compute="_compute_result_field",
|
||||
)
|
||||
report_menu_id = fields.Many2one(
|
||||
comodel_name="ir.ui.menu",
|
||||
string="Report Menu",
|
||||
readonly=True,
|
||||
)
|
||||
report_action_id = fields.Many2one(
|
||||
comodel_name="ir.actions.report",
|
||||
string="Report Action",
|
||||
)
|
||||
|
||||
def _compute_result_field(self):
|
||||
for rec in self:
|
||||
rec.result_field = (
|
||||
("x_%s_results" % rec.id) if rec.result_model_id else False
|
||||
)
|
||||
|
||||
@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, template_ids, addon=False):
|
||||
for template in self.browse(template_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_create_multi
|
||||
def create(self, vals_list):
|
||||
res = super().create(vals_list)
|
||||
for rec in res:
|
||||
if rec.input_instruction:
|
||||
rec._compute_input_export_instruction()
|
||||
rec._compute_input_import_instruction()
|
||||
rec._compute_input_post_import_hook()
|
||||
if rec.result_model_id:
|
||||
rec._update_result_field_common_wizard()
|
||||
rec._update_result_export_ids()
|
||||
return res
|
||||
|
||||
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()
|
||||
if vals.get("result_model_id"):
|
||||
for rec in self:
|
||||
rec._update_result_field_common_wizard()
|
||||
rec._update_result_export_ids()
|
||||
return res
|
||||
|
||||
def unlink(self):
|
||||
self.env["ir.model.fields"].search(
|
||||
[
|
||||
("model", "=", "report.xlsx.wizard"),
|
||||
("name", "=", self.mapped("result_field")),
|
||||
]
|
||||
).unlink()
|
||||
return super().unlink()
|
||||
|
||||
def _update_result_field_common_wizard(self):
|
||||
self.ensure_one()
|
||||
_model = self.env["ir.model"].search([("model", "=", "report.xlsx.wizard")])
|
||||
_model.ensure_one()
|
||||
_field = self.env["ir.model.fields"].search(
|
||||
[("model", "=", "report.xlsx.wizard"), ("name", "=", self.result_field)]
|
||||
)
|
||||
if not _field:
|
||||
_field = self.env["ir.model.fields"].create(
|
||||
{
|
||||
"model_id": _model.id,
|
||||
"name": self.result_field,
|
||||
"field_description": "Results",
|
||||
"ttype": "many2many",
|
||||
"relation": self.result_model_id.model,
|
||||
"store": False,
|
||||
"depends": "res_model",
|
||||
}
|
||||
)
|
||||
else:
|
||||
_field.ensure_one()
|
||||
_field.write({"relation": self.result_model_id.model})
|
||||
_field.compute = """
|
||||
self['{}'] = self.env['{}'].search(self.safe_domain(self.domain))
|
||||
""".format(
|
||||
self.result_field,
|
||||
self.result_model_id.model,
|
||||
)
|
||||
|
||||
def _update_result_export_ids(self):
|
||||
self.ensure_one()
|
||||
results = self.env["xlsx.template.export"].search(
|
||||
[("template_id", "=", self.id), ("row_field", "=", self.result_field)]
|
||||
)
|
||||
if not results:
|
||||
self.export_ids.unlink()
|
||||
self.write(
|
||||
{
|
||||
"export_ids": [
|
||||
(0, 0, {"sequence": 10, "section_type": "sheet", "sheet": 1}),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"sequence": 20,
|
||||
"section_type": "row",
|
||||
"row_field": self.result_field,
|
||||
},
|
||||
),
|
||||
(
|
||||
0,
|
||||
0,
|
||||
{
|
||||
"sequence": 30,
|
||||
"section_type": "data",
|
||||
"excel_cell": "A1",
|
||||
"field_name": "id",
|
||||
},
|
||||
),
|
||||
],
|
||||
}
|
||||
)
|
||||
|
||||
@api.onchange("use_report_wizard")
|
||||
def _onchange_use_report_wizard(self):
|
||||
self.res_model = "report.xlsx.wizard" if self.use_report_wizard else False
|
||||
self.redirect_action = False
|
||||
|
||||
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_", "")
|
||||
is_extend = False
|
||||
if "_EXTEND_" in row_field:
|
||||
is_extend = True
|
||||
row_field = row_field.replace("_EXTEND_", "")
|
||||
vals = {
|
||||
"sequence": sequence,
|
||||
"section_type": (row_field == "_HEAD_" and "head" or "row"),
|
||||
"row_field": row_field,
|
||||
"is_cont": is_cont,
|
||||
"is_extend": is_extend,
|
||||
}
|
||||
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})
|
||||
|
||||
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})
|
||||
|
||||
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__")
|
||||
|
||||
def _compose_field_name(self, line):
|
||||
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}"
|
||||
return field_name
|
||||
|
||||
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
|
||||
if line.section_type == "row" and line.is_extend:
|
||||
row_field = "_EXTEND_%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 = self._compose_field_name(line)
|
||||
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
|
||||
|
||||
def _get_export_action_domain(self, model):
|
||||
return [
|
||||
("binding_model_id", "=", model.id),
|
||||
("res_model", "=", "export.xlsx.wizard"),
|
||||
("name", "=", "Export Excel"),
|
||||
]
|
||||
|
||||
def _get_export_action(self, model):
|
||||
export_action_domain = self._get_export_action_domain(model)
|
||||
return self.env["ir.actions.act_window"].search(export_action_domain, limit=1)
|
||||
|
||||
def _create_export_action(self, model):
|
||||
vals = {
|
||||
"name": "Export Excel",
|
||||
"res_model": "export.xlsx.wizard",
|
||||
"binding_model_id": model.id,
|
||||
"binding_type": "action",
|
||||
"target": "new",
|
||||
"view_mode": "form",
|
||||
"context": """
|
||||
{'template_domain': [('res_model', '=', '%s'),
|
||||
('export_action_id', '!=', False),
|
||||
('gname', '=', False)]}
|
||||
"""
|
||||
% (self.res_model),
|
||||
}
|
||||
return self.env["ir.actions.act_window"].create(vals)
|
||||
|
||||
def add_export_action(self):
|
||||
self.ensure_one()
|
||||
model = self.env["ir.model"].search([("model", "=", self.res_model)], limit=1)
|
||||
export_action = self._get_export_action(model)
|
||||
if not export_action:
|
||||
export_action = self._create_export_action(model)
|
||||
self.export_action_id = export_action
|
||||
|
||||
def remove_export_action(self):
|
||||
self.ensure_one()
|
||||
export_action = self.export_action_id
|
||||
self.export_action_id = False
|
||||
if not self.search(
|
||||
[
|
||||
("res_model", "=", self.res_model),
|
||||
("export_action_id", "=", export_action.id),
|
||||
]
|
||||
):
|
||||
export_action.unlink()
|
||||
|
||||
def add_import_action(self):
|
||||
self.ensure_one()
|
||||
vals = {
|
||||
"name": "Import Excel",
|
||||
"res_model": "import.xlsx.wizard",
|
||||
"binding_model_id": self.env["ir.model"]
|
||||
.search([("model", "=", self.res_model)])
|
||||
.id,
|
||||
"binding_type": "action",
|
||||
"target": "new",
|
||||
"view_mode": "form",
|
||||
"context": """
|
||||
{'template_domain': [('res_model', '=', '%s'),
|
||||
('fname', '=', '%s'),
|
||||
('gname', '=', False)]}
|
||||
"""
|
||||
% (self.res_model, self.fname),
|
||||
}
|
||||
action = self.env["ir.actions.act_window"].create(vals)
|
||||
self.import_action_id = action
|
||||
|
||||
def remove_import_action(self):
|
||||
self.ensure_one()
|
||||
if self.import_action_id:
|
||||
self.import_action_id.unlink()
|
||||
|
||||
def add_report_menu(self):
|
||||
self.ensure_one()
|
||||
if not self.fname:
|
||||
raise UserError(_("No file content!"))
|
||||
# Create report action
|
||||
vals = {
|
||||
"name": self.name,
|
||||
"report_type": "excel",
|
||||
"model": "report.xlsx.wizard",
|
||||
"report_name": self.fname,
|
||||
"report_file": self.fname,
|
||||
}
|
||||
report_action = self.env["ir.actions.report"].create(vals)
|
||||
self.report_action_id = report_action
|
||||
# Create window action
|
||||
vals = {
|
||||
"name": self.name,
|
||||
"res_model": "report.xlsx.wizard",
|
||||
"binding_type": "action",
|
||||
"target": "new",
|
||||
"view_mode": "form",
|
||||
"context": {
|
||||
"report_action_id": report_action.id,
|
||||
"default_res_model": self.result_model_id.model,
|
||||
},
|
||||
}
|
||||
action = self.env["ir.actions.act_window"].create(vals)
|
||||
# Create menu
|
||||
vals = {
|
||||
"name": self.name,
|
||||
"action": "{},{}".format(action._name, action.id),
|
||||
}
|
||||
menu = self.env["ir.ui.menu"].create(vals)
|
||||
self.report_menu_id = menu
|
||||
|
||||
def remove_report_menu(self):
|
||||
self.ensure_one()
|
||||
if self.report_action_id:
|
||||
self.report_action_id.unlink()
|
||||
if self.report_menu_id:
|
||||
self.report_menu_id.action.unlink()
|
||||
self.report_menu_id.unlink()
|
||||
|
||||
|
||||
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(default=10)
|
||||
sheet = fields.Char()
|
||||
section_type = fields.Selection(
|
||||
[("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")],
|
||||
required=True,
|
||||
)
|
||||
row_field = fields.Char(help="If section type is row, this field is required")
|
||||
no_delete = fields.Boolean(
|
||||
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_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
vals = self._extract_field_name(vals)
|
||||
return super().create(vals_list)
|
||||
|
||||
@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(default=10)
|
||||
sheet = fields.Char()
|
||||
section_type = fields.Selection(
|
||||
[("sheet", "Sheet"), ("head", "Head"), ("row", "Row"), ("data", "Data")],
|
||||
required=True,
|
||||
)
|
||||
row_field = fields.Char(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"
|
||||
)
|
||||
is_extend = fields.Boolean(
|
||||
string="Extend",
|
||||
default=False,
|
||||
help="Extend a blank row after filling each record, to extend the footer",
|
||||
)
|
||||
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_create_multi
|
||||
def create(self, vals_list):
|
||||
for vals in vals_list:
|
||||
vals = self._extract_field_name(vals)
|
||||
return super().create(vals_list)
|
||||
|
||||
@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,2 @@
|
|||
If you have existing templates from the version 16.0.1.2.0 or earlier, you need to click 'REMOVE EXPORT ACTION'
|
||||
and then click 'ADD EXPORT ACTION' in these templates for export actions to work as expected.
|
||||
|
|
@ -0,0 +1,2 @@
|
|||
* Kitti Upariphutthiphong. <kittiu@gmail.com> (http://ecosoft.co.th)
|
||||
* Saran Lim. <saranl@ecosoft.co.th> (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,5 @@
|
|||
To install this module, you need to install following python library, **xlrd, xlwt, openpyxl**.
|
||||
|
||||
Then, simply install **excel_import_export**.
|
||||
|
||||
For demo, install **excel_import_export_demo**
|
||||
|
|
@ -0,0 +1 @@
|
|||
- Module extension e.g., excel_import_export_async, that add ability to execute as async process.
|
||||
|
|
@ -0,0 +1,88 @@
|
|||
Concepts
|
||||
~~~~~~~~
|
||||
|
||||
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_demo**
|
||||
|
||||
Use Cases
|
||||
~~~~~~~~~
|
||||
|
||||
**Use Case 1:** Export/Import Excel on existing document
|
||||
|
||||
This add export/import action menus in existing document (example - excel_import_export_demo/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_demo/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_demo/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
|
||||
|
||||
**Note:**
|
||||
|
||||
Another option for reporting is to use report action (report_type='excel'), I.e.,
|
||||
|
||||
.. code-block:: xml
|
||||
|
||||
<report id='action_report_saleorder_excel'
|
||||
string='Quotation / Order (.xlsx)'
|
||||
model='sale.order'
|
||||
name='sale_order.xlsx'
|
||||
file='sale_order'
|
||||
report_type='excel'
|
||||
/>
|
||||
|
||||
By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record.
|
||||
Please see example in excel_import_export_demo/report_action, which shows,
|
||||
|
||||
1. Print excel from an active sale.order
|
||||
2. Run partner list report based on search criteria.
|
||||
|
||||
Easy Reporting Option
|
||||
~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
Technically, this option is the same as "Create Excel Report" use case. But instead of having to write XML / Python code like normally do,
|
||||
this option allow user to create a report based on a model or view, all by configuration only.
|
||||
|
||||
1. Goto > Technical> Excel Import/Export > XLSX Templates, and create a new template for a report.
|
||||
2. On the new template, select "Easy Reporting" option, then select followings
|
||||
- Report Model, this can be data model or data view we want to get the results from.
|
||||
- Click upload your file and add the excel template (.xlsx)
|
||||
- Click Save, system will create sample export line, user can add more fields according to results model.
|
||||
3. Click Add Report Menu, the report menu will be created, user can change its location. Now the report is ready to use.
|
||||
|
||||
.. figure:: ../static/description/xlsx_template.png
|
||||
:width: 800 px
|
||||
|
||||
Note: Using easy reporting mode, system will used a common criteria wizard.
|
||||
|
||||
.. figure:: ../static/description/common_wizard.png
|
||||
:width: 800 px
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
"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
|
||||
access_export_xlsx_wizard,access_export_xlsx_wizard,model_export_xlsx_wizard,base.group_user,1,1,1,1
|
||||
access_import_xlsx_wizard,access_import_xlsx_wizard,model_import_xlsx_wizard,base.group_user,1,1,1,1
|
||||
access_report_xlsx_wizard,access_report_xlsx_wizard,model_report_xlsx_wizard,base.group_user,1,1,1,1
|
||||
|
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 9.2 KiB |
|
|
@ -0,0 +1,545 @@
|
|||
<!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: https://docutils.sourceforge.io/" />
|
||||
<title>Excel Import/Export/Report</title>
|
||||
<style type="text/css">
|
||||
|
||||
/*
|
||||
:Author: David Goodger (goodger@python.org)
|
||||
:Id: $Id: html4css1.css 9511 2024-01-13 09:50:07Z milde $
|
||||
:Copyright: This stylesheet has been placed in the public domain.
|
||||
|
||||
Default cascading style sheet for the HTML output of Docutils.
|
||||
Despite the name, some widely supported CSS2 features are used.
|
||||
|
||||
See https://docutils.sourceforge.io/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: gray; } /* 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, pre.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-report">
|
||||
<h1 class="title">Excel Import/Export/Report</h1>
|
||||
|
||||
<!-- !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! This file is generated by oca-gen-addon-readme !!
|
||||
!! changes will be overwritten. !!
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!
|
||||
!! source digest: sha256:40c58b91ccaaaa2b5ead94e8975df9d77a9ae3d08e29f5647422c2a3e1dafd6b
|
||||
!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! -->
|
||||
<p><a class="reference external image-reference" href="https://odoo-community.org/page/development-status"><img alt="Beta" src="https://img.shields.io/badge/maturity-Beta-yellow.png" /></a> <a class="reference external image-reference" 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 image-reference" href="https://github.com/OCA/server-tools/tree/16.0/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 image-reference" href="https://translation.odoo-community.org/projects/server-tools-16-0/server-tools-16-0-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 image-reference" href="https://runboat.odoo-community.org/builds?repo=OCA/server-tools&target_branch=16.0"><img alt="Try me on Runboat" src="https://img.shields.io/badge/runboat-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="toc-entry-1">Installation</a></li>
|
||||
<li><a class="reference internal" href="#configuration" id="toc-entry-2">Configuration</a></li>
|
||||
<li><a class="reference internal" href="#usage" id="toc-entry-3">Usage</a><ul>
|
||||
<li><a class="reference internal" href="#concepts" id="toc-entry-4">Concepts</a></li>
|
||||
<li><a class="reference internal" href="#use-cases" id="toc-entry-5">Use Cases</a></li>
|
||||
<li><a class="reference internal" href="#easy-reporting-option" id="toc-entry-6">Easy Reporting Option</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
<li><a class="reference internal" href="#known-issues-roadmap" id="toc-entry-7">Known issues / Roadmap</a></li>
|
||||
<li><a class="reference internal" href="#bug-tracker" id="toc-entry-8">Bug Tracker</a></li>
|
||||
<li><a class="reference internal" href="#credits" id="toc-entry-9">Credits</a><ul>
|
||||
<li><a class="reference internal" href="#authors" id="toc-entry-10">Authors</a></li>
|
||||
<li><a class="reference internal" href="#contributors" id="toc-entry-11">Contributors</a></li>
|
||||
<li><a class="reference internal" href="#maintainers" id="toc-entry-12">Maintainers</a></li>
|
||||
</ul>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="installation">
|
||||
<h1><a class="toc-backref" href="#toc-entry-1">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 demo, install <strong>excel_import_export_demo</strong></p>
|
||||
</div>
|
||||
<div class="section" id="configuration">
|
||||
<h1><a class="toc-backref" href="#toc-entry-2">Configuration</a></h1>
|
||||
<p>If you have existing templates from the version 16.0.1.2.0 or earlier, you need to click ‘REMOVE EXPORT ACTION’
|
||||
and then click ‘ADD EXPORT ACTION’ in these templates for export actions to work as expected.</p>
|
||||
</div>
|
||||
<div class="section" id="usage">
|
||||
<h1><a class="toc-backref" href="#toc-entry-3">Usage</a></h1>
|
||||
<div class="section" id="concepts">
|
||||
<h2><a class="toc-backref" href="#toc-entry-4">Concepts</a></h2>
|
||||
<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_demo</strong></p>
|
||||
</div>
|
||||
<div class="section" id="use-cases">
|
||||
<h2><a class="toc-backref" href="#toc-entry-5">Use Cases</a></h2>
|
||||
<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_demo/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_demo/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_demo/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>
|
||||
<p><strong>Note:</strong></p>
|
||||
<p>Another option for reporting is to use report action (report_type=’excel’), I.e.,</p>
|
||||
<pre class="code xml literal-block">
|
||||
<span class="nt"><report</span><span class="w"> </span><span class="na">id=</span><span class="s">'action_report_saleorder_excel'</span><span class="w">
|
||||
</span><span class="na">string=</span><span class="s">'Quotation / Order (.xlsx)'</span><span class="w">
|
||||
</span><span class="na">model=</span><span class="s">'sale.order'</span><span class="w">
|
||||
</span><span class="na">name=</span><span class="s">'sale_order.xlsx'</span><span class="w">
|
||||
</span><span class="na">file=</span><span class="s">'sale_order'</span><span class="w">
|
||||
</span><span class="na">report_type=</span><span class="s">'excel'</span><span class="w">
|
||||
</span><span class="nt">/></span>
|
||||
</pre>
|
||||
<p>By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record.
|
||||
Please see example in excel_import_export_demo/report_action, which shows,</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Print excel from an active sale.order</li>
|
||||
<li>Run partner list report based on search criteria.</li>
|
||||
</ol>
|
||||
</div>
|
||||
<div class="section" id="easy-reporting-option">
|
||||
<h2><a class="toc-backref" href="#toc-entry-6">Easy Reporting Option</a></h2>
|
||||
<p>Technically, this option is the same as “Create Excel Report” use case. But instead of having to write XML / Python code like normally do,
|
||||
this option allow user to create a report based on a model or view, all by configuration only.</p>
|
||||
<ol class="arabic simple">
|
||||
<li>Goto > Technical> Excel Import/Export > XLSX Templates, and create a new template for a report.</li>
|
||||
<li>On the new template, select “Easy Reporting” option, then select followings
|
||||
- Report Model, this can be data model or data view we want to get the results from.
|
||||
- Click upload your file and add the excel template (.xlsx)
|
||||
- Click Save, system will create sample export line, user can add more fields according to results model.</li>
|
||||
<li>Click Add Report Menu, the report menu will be created, user can change its location. Now the report is ready to use.</li>
|
||||
</ol>
|
||||
<blockquote>
|
||||
<div class="figure">
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/xlsx_template.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/xlsx_template.png" style="width: 800px;" />
|
||||
</div>
|
||||
</blockquote>
|
||||
<p>Note: Using easy reporting mode, system will used a common criteria wizard.</p>
|
||||
<blockquote>
|
||||
<div class="figure">
|
||||
<img alt="https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/common_wizard.png" src="https://raw.githubusercontent.com/OCA/server-tools/16.0/excel_import_export/static/description/common_wizard.png" style="width: 800px;" />
|
||||
</div>
|
||||
</blockquote>
|
||||
</div>
|
||||
</div>
|
||||
<div class="section" id="known-issues-roadmap">
|
||||
<h1><a class="toc-backref" href="#toc-entry-7">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>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="bug-tracker">
|
||||
<h1><a class="toc-backref" href="#toc-entry-8">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 to smash 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:%2016.0%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="#toc-entry-9">Credits</a></h1>
|
||||
<div class="section" id="authors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-10">Authors</a></h2>
|
||||
<ul class="simple">
|
||||
<li>Ecosoft</li>
|
||||
</ul>
|
||||
</div>
|
||||
<div class="section" id="contributors">
|
||||
<h2><a class="toc-backref" href="#toc-entry-11">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>
|
||||
<li>Saran Lim. <<a class="reference external" href="mailto:saranl@ecosoft.co.th">saranl@ecosoft.co.th</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="#toc-entry-12">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 image-reference" 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/16.0/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>
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 80 KiB |
|
|
@ -0,0 +1,55 @@
|
|||
/** @odoo-module **/
|
||||
|
||||
import {download} from "@web/core/network/download";
|
||||
import {registry} from "@web/core/registry";
|
||||
|
||||
function getReportUrl({report_name, context, data}, env) {
|
||||
// Rough copy of action_service.js _getReportUrl method.
|
||||
let url = `/report/excel/${report_name}`;
|
||||
const actionContext = context || {};
|
||||
if (data && JSON.stringify(data) !== "{}") {
|
||||
const encodedOptions = encodeURIComponent(JSON.stringify(data));
|
||||
const encodedContext = encodeURIComponent(JSON.stringify(actionContext));
|
||||
return `${url}?options=${encodedOptions}&context=${encodedContext}`;
|
||||
}
|
||||
if (actionContext.active_ids) {
|
||||
url += `/${actionContext.active_ids.join(",")}`;
|
||||
}
|
||||
const userContext = encodeURIComponent(JSON.stringify(env.services.user.context));
|
||||
return `${url}?context=${userContext}`;
|
||||
}
|
||||
|
||||
async function triggerDownload(action, {onClose}, env) {
|
||||
// Rough copy of action_service.js _triggerDownload method.
|
||||
env.services.ui.block();
|
||||
try {
|
||||
await download({
|
||||
url: "/report/download",
|
||||
data: {
|
||||
data: JSON.stringify([getReportUrl(action, env), "excel"]),
|
||||
context: JSON.stringify(env.services.user.context),
|
||||
},
|
||||
});
|
||||
} finally {
|
||||
env.services.ui.unblock();
|
||||
}
|
||||
if (action.close_on_report_download) {
|
||||
return env.services.action.doAction(
|
||||
{type: "ir.actions.act_window_close"},
|
||||
{onClose}
|
||||
);
|
||||
}
|
||||
if (onClose) {
|
||||
onClose();
|
||||
}
|
||||
}
|
||||
|
||||
registry
|
||||
.category("ir.actions.report handlers")
|
||||
.add("excel_handler", async function (action, options, env) {
|
||||
if (action.report_type === "excel") {
|
||||
await triggerDownload(action, options, env);
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
});
|
||||
|
|
@ -0,0 +1,61 @@
|
|||
<?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,439 @@
|
|||
<?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>
|
||||
<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>
|
||||
<div class="oe_button_box" name="button_box">
|
||||
<button
|
||||
name="add_report_menu"
|
||||
string="Add Report Menu"
|
||||
type="object"
|
||||
attrs="{'invisible': ['|', ('use_report_wizard', '=', False), ('report_menu_id', '!=', False)]}"
|
||||
icon="fa-plus-square"
|
||||
help="Add new report menu at root level"
|
||||
class="oe_stat_button"
|
||||
/>
|
||||
<button
|
||||
name="remove_report_menu"
|
||||
string="Remove Report Menu"
|
||||
type="object"
|
||||
attrs="{'invisible': [('report_menu_id', '=', False)]}"
|
||||
icon="fa-minus-square"
|
||||
class="oe_stat_button"
|
||||
/>
|
||||
</div>
|
||||
<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)]}"
|
||||
/>
|
||||
<field name="use_report_wizard" />
|
||||
<field
|
||||
name="report_menu_id"
|
||||
attrs="{'invisible': [('report_menu_id', '=', False)]}"
|
||||
/>
|
||||
<field
|
||||
name="result_model_id"
|
||||
attrs="{'invisible': [('use_report_wizard', '=', False)],
|
||||
'required': [('use_report_wizard', '=', True)]}"
|
||||
/>
|
||||
<field
|
||||
name="result_field"
|
||||
attrs="{'invisible': [('use_report_wizard', '=', 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">
|
||||
<div
|
||||
name="export_actions"
|
||||
attrs="{'invisible': [('use_report_wizard', '=', True)]}"
|
||||
>
|
||||
<button
|
||||
name="add_export_action"
|
||||
class="oe_highlight"
|
||||
type="object"
|
||||
string="Add Export Action"
|
||||
attrs="{'invisible': [('export_action_id', '!=', False)]}"
|
||||
/>
|
||||
<button
|
||||
name="remove_export_action"
|
||||
type="object"
|
||||
string="Remove Export Action"
|
||||
attrs="{'invisible': [('export_action_id', '=', False)]}"
|
||||
/>
|
||||
<field name="export_action_id" invisible="1" />
|
||||
</div>
|
||||
<separator />
|
||||
<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="is_extend"
|
||||
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
|
||||
>Extend</b>: If selected, extend one row after one data row in order to preserve the sum line</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><code
|
||||
>{...}</code> to manipulate field value, e.g., if field = product_id, <code
|
||||
>value</code> will represent product object, e.g., <code
|
||||
>$</code><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><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"
|
||||
attrs="{'invisible': [('use_report_wizard', '=', True)]}"
|
||||
>
|
||||
<div name="import_actions">
|
||||
<button
|
||||
name="add_import_action"
|
||||
class="oe_highlight"
|
||||
type="object"
|
||||
string="Add Import Action"
|
||||
attrs="{'invisible': [('import_action_id', '!=', False)]}"
|
||||
/>
|
||||
<button
|
||||
name="remove_import_action"
|
||||
type="object"
|
||||
string="Remove Import Action"
|
||||
attrs="{'invisible': [('import_action_id', '=', False)]}"
|
||||
/>
|
||||
<field name="import_action_id" invisible="1" />
|
||||
</div>
|
||||
<separator />
|
||||
<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 for row data, e.g., order_line, line_ids<code
|
||||
>[max_row]</code> where <code
|
||||
>[max_row]</code> is optional number of rows to import</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><code
|
||||
>{...}</code> value will represent data from excel cell, e.g., if A1 = 'ABC', <code
|
||||
>value</code> will represent 'ABC', e.g., <code
|
||||
>$</code><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><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<span
|
||||
>$</span>{value or ""}#{align=left;style=text}',
|
||||
'B3': 'name<span>$</span>{value or ""}#{align=left;style=text}',
|
||||
},
|
||||
'line_ids': { # prefix with _CONT_ to continue rows from previous row field
|
||||
'A6': 'product_id.display_name<span>$</span>{value or ""}#{style=text}',
|
||||
'C6': 'product_uom_qty<span>$</span>{value or 0}#{style=number}',
|
||||
'E6': 'price_unit<span>$</span>{value or 0}#{style=number}',
|
||||
'G6': 'price_subtotal<span>$</span>{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<span>$</span>{value > 0 and value or 0}',
|
||||
},
|
||||
},
|
||||
},
|
||||
'__POST_IMPORT__': '<span>$</span>{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_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,3 @@
|
|||
from . import export_xlsx_wizard
|
||||
from . import import_xlsx_wizard
|
||||
from . import report_xlsx_wizard
|
||||
|
|
@ -0,0 +1,67 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import ValidationError
|
||||
from odoo.tools.safe_eval import safe_eval
|
||||
|
||||
|
||||
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_ids = fields.Char(string="Resource IDs", 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_ids = self._context.get("active_ids", 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_ids"] = ",".join([str(x) for x in res_ids])
|
||||
defaults["res_model"] = res_model
|
||||
return defaults
|
||||
|
||||
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, safe_eval(self.res_ids)
|
||||
)
|
||||
self.write({"state": "get", "data": out_file, "name": out_name})
|
||||
return {
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "export.xlsx.wizard",
|
||||
"view_mode": "form",
|
||||
"res_id": self.id,
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
}
|
||||
|
|
@ -0,0 +1,50 @@
|
|||
<?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_ids" 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,157 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
|
||||
|
||||
from odoo import _, api, fields, models
|
||||
from odoo.exceptions import RedirectWarning, ValidationError
|
||||
|
||||
|
||||
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)")
|
||||
filename = fields.Char("Import File Name")
|
||||
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)
|
||||
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.items():
|
||||
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 "%(fname)s" not found in template, %(name)s.',
|
||||
fname=template.fname,
|
||||
name=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
|
||||
|
||||
def get_import_sample(self):
|
||||
self.ensure_one()
|
||||
return {
|
||||
"name": _("Import Excel"),
|
||||
"type": "ir.actions.act_window",
|
||||
"res_model": "import.xlsx.wizard",
|
||||
"view_mode": "form",
|
||||
"res_id": self.id,
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
"context": self._context.copy(),
|
||||
}
|
||||
|
||||
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",
|
||||
"res_id": self.id,
|
||||
"views": [(False, "form")],
|
||||
"target": "new",
|
||||
}
|
||||
|
|
@ -0,0 +1,70 @@
|
|||
<?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="id" invisible="1" />
|
||||
<field name="state" invisible="1" />
|
||||
<field name="fname" invisible="1" />
|
||||
<field name="filename" invisible="1" />
|
||||
<field name="res_model" invisible="1" />
|
||||
<field name="res_id" invisible="1" />
|
||||
<group states="choose">
|
||||
<group>
|
||||
<field
|
||||
name="import_file"
|
||||
attrs="{'invisible': [('res_id', '=', False)]}"
|
||||
filename="filename"
|
||||
/>
|
||||
<field
|
||||
name="attachment_ids"
|
||||
widget="many2many_binary"
|
||||
nolabel="1"
|
||||
attrs="{'invisible': [('res_id', '!=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="template_id" widget="selection" />
|
||||
<div colspan="2">
|
||||
<button
|
||||
name="get_import_sample"
|
||||
string="⇒ Get Sample Import Template"
|
||||
type="object"
|
||||
class="oe_link"
|
||||
attrs="{'invisible': [('id', '!=', False)]}"
|
||||
/>
|
||||
</div>
|
||||
<field
|
||||
name="datas"
|
||||
filename="fname"
|
||||
attrs="{'invisible': [('id', '=', False)]}"
|
||||
/>
|
||||
</group>
|
||||
</group>
|
||||
<group states="get">
|
||||
<p>
|
||||
Import Successful!
|
||||
</p>
|
||||
</group>
|
||||
<footer states="choose">
|
||||
<button
|
||||
name="action_import"
|
||||
string="Import"
|
||||
type="object"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
<footer states="get">
|
||||
<button string="Close" class="oe_link" special="cancel" />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,22 @@
|
|||
# Copyright 2019 Ecosoft Co., Ltd (http://ecosoft.co.th/)
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html)
|
||||
import ast
|
||||
|
||||
from odoo import fields, models
|
||||
|
||||
|
||||
class ReportXLSXWizard(models.TransientModel):
|
||||
_name = "report.xlsx.wizard"
|
||||
_description = "Generic Report Wizard, used with template reporting option"
|
||||
|
||||
res_model = fields.Char()
|
||||
domain = fields.Char(string="Search Criterias")
|
||||
|
||||
def action_report(self):
|
||||
action_id = self._context.get("report_action_id")
|
||||
action = self.env["ir.actions.report"].browse(action_id)
|
||||
res = action.read()[0]
|
||||
return res
|
||||
|
||||
def safe_domain(self, str_domain):
|
||||
return ast.literal_eval(str_domain or "[]")
|
||||
|
|
@ -0,0 +1,31 @@
|
|||
<?xml version="1.0" encoding="utf-8" ?>
|
||||
<!--
|
||||
Copyright 2019 Ecosoft Co., Ltd.
|
||||
License LGPL-3.0 or later (https://www.gnu.org/licenses/lgpl.html).-->
|
||||
<odoo>
|
||||
<record id="report_xlsx_wizard" model="ir.ui.view">
|
||||
<field name="name">report.xlsx.wizard</field>
|
||||
<field name="model">report.xlsx.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form>
|
||||
<group>
|
||||
<field name="res_model" invisible="1" />
|
||||
<field
|
||||
name="domain"
|
||||
widget="domain"
|
||||
options="{'model': 'res_model', 'in_dialog': True}"
|
||||
/>
|
||||
</group>
|
||||
<footer>
|
||||
<button
|
||||
name="action_report"
|
||||
type="object"
|
||||
string="Execute"
|
||||
class="oe_highlight"
|
||||
/>
|
||||
<button special='cancel' string='Cancel' />
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
</odoo>
|
||||
|
|
@ -0,0 +1,42 @@
|
|||
[project]
|
||||
name = "odoo-bringout-oca-server-tools-excel_import_export"
|
||||
version = "16.0.0"
|
||||
description = "Excel Import/Export/Report - Base module for developing Excel import/export/report"
|
||||
authors = [
|
||||
{ name = "Ernad Husremovic", email = "hernad@bring.out.ba" }
|
||||
]
|
||||
dependencies = [
|
||||
"odoo-bringout-oca-ocb-mail>=16.0.0",
|
||||
"requests>=2.25.1"
|
||||
]
|
||||
readme = "README.md"
|
||||
requires-python = ">= 3.11"
|
||||
classifiers = [
|
||||
"Development Status :: 5 - Production/Stable",
|
||||
"Intended Audience :: Developers",
|
||||
"License :: OSI Approved :: GNU Lesser General Public License v3 (LGPLv3)",
|
||||
"Programming Language :: Python :: 3",
|
||||
"Programming Language :: Python :: 3.11",
|
||||
"Programming Language :: Python :: 3.12",
|
||||
"Topic :: Office/Business",
|
||||
]
|
||||
|
||||
[project.urls]
|
||||
homepage = "https://github.com/bringout/0"
|
||||
repository = "https://github.com/bringout/0"
|
||||
|
||||
[build-system]
|
||||
requires = ["hatchling"]
|
||||
build-backend = "hatchling.build"
|
||||
|
||||
[tool.hatch.metadata]
|
||||
allow-direct-references = true
|
||||
|
||||
[tool.hatch.build.targets.wheel]
|
||||
packages = ["excel_import_export"]
|
||||
|
||||
[tool.rye]
|
||||
managed = true
|
||||
dev-dependencies = [
|
||||
"pytest>=8.4.1",
|
||||
]
|
||||
Loading…
Add table
Add a link
Reference in a new issue