From 23111d69af3eb0220b041cfe1f58fb86dbd5472b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Duy=20=28=C4=90=E1=BB=97=20Anh=29?= Date: Wed, 19 Feb 2025 16:25:56 +0700 Subject: [PATCH] [MIG] excel_import_export: Migration to 18.0 --- excel_import_export/README.rst | 25 ++-- excel_import_export/__manifest__.py | 2 +- excel_import_export/controllers/report.py | 9 +- excel_import_export/models/__init__.py | 14 --- excel_import_export/models/common.py | 33 ++--- excel_import_export/models/ir_report.py | 23 ++-- excel_import_export/models/xlsx_export.py | 24 ++-- excel_import_export/models/xlsx_import.py | 27 ++-- excel_import_export/models/xlsx_report.py | 7 +- excel_import_export/models/xlsx_template.py | 63 +++++----- excel_import_export/readme/CONTRIBUTORS.md | 1 + excel_import_export/readme/USAGE.md | 20 +-- .../security/ir.model.access.csv | 6 +- .../static/description/index.html | 25 ++-- .../js/report/action_manager_report.esm.js | 11 +- excel_import_export/views/xlsx_report.xml | 20 ++- .../views/xlsx_template_view.xml | 115 ++++++++---------- .../wizard/export_xlsx_wizard.py | 18 ++- .../wizard/export_xlsx_wizard.xml | 40 +++--- .../wizard/import_xlsx_wizard.py | 71 ++++++----- .../wizard/import_xlsx_wizard.xml | 36 ++---- .../wizard/report_xlsx_wizard.py | 5 +- .../wizard/report_xlsx_wizard.xml | 2 +- 23 files changed, 295 insertions(+), 302 deletions(-) diff --git a/excel_import_export/README.rst b/excel_import_export/README.rst index ab61bd4c19e..5ee079ebf75 100644 --- a/excel_import_export/README.rst +++ b/excel_import_export/README.rst @@ -81,8 +81,8 @@ 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. +After install this module, go to Settings > Technical > 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** @@ -145,13 +145,15 @@ Another option for reporting is to use report action .. code:: xml - + + Quotation / Order (.xlsx) + ir.model + 'sale.order' + 'sale.order' + + report + excel + By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record. Please @@ -168,8 +170,8 @@ 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. +1. Go to Settings > 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 @@ -223,6 +225,7 @@ Contributors - Kitti Upariphutthiphong. (http://ecosoft.co.th) - Saran Lim. (http://ecosoft.co.th) +- Do Anh Duy Maintainers ----------- diff --git a/excel_import_export/__manifest__.py b/excel_import_export/__manifest__.py index 0c6170048dc..afcdfe450dc 100644 --- a/excel_import_export/__manifest__.py +++ b/excel_import_export/__manifest__.py @@ -4,7 +4,7 @@ { "name": "Excel Import/Export/Report", "summary": "Base module for developing Excel import/export/report", - "version": "16.0.1.3.1", + "version": "18.0.1.0.0", "author": "Ecosoft,Odoo Community Association (OCA)", "license": "AGPL-3", "website": "https://github.com/OCA/server-tools", diff --git a/excel_import_export/controllers/report.py b/excel_import_export/controllers/report.py index e779247b348..edd59530fa4 100644 --- a/excel_import_export/controllers/report.py +++ b/excel_import_export/controllers/report.py @@ -12,19 +12,19 @@ from odoo.tools import html_escape from odoo.tools.safe_eval import safe_eval, time -from odoo.addons.web.controllers import report +from odoo.addons.web.controllers.report import ReportController _logger = logging.getLogger(__name__) -class ReportController(report.ReportController): +class ReportExcelController(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(",")] + docids = [int(i) for i in docids.split(",") if i.isdigit()] if data.get("options"): data.update(json.loads(data.pop("options"))) if data.get("context"): @@ -32,10 +32,7 @@ def report_routes(self, reportname, docids=None, converter=None, **data): # 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 ) diff --git a/excel_import_export/models/__init__.py b/excel_import_export/models/__init__.py index 04a47be5979..52501c936b5 100644 --- a/excel_import_export/models/__init__.py +++ b/excel_import_export/models/__init__.py @@ -7,17 +7,3 @@ 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) diff --git a/excel_import_export/models/common.py b/excel_import_export/models/common.py index b18eb6552f0..e5462cf8504 100644 --- a/excel_import_export/models/common.py +++ b/excel_import_export/models/common.py @@ -27,7 +27,7 @@ def adjust_cell_formula(value, k): val = value[i + 2 : j] col, row = split_row_col(val) new_val = f"{col}{row + k}" - value = value.replace("?(%s)" % val, new_val) + value = value.replace(f"?({val})", new_val) return value @@ -41,7 +41,7 @@ def get_field_aggregation(field): if cond or cond == "": return (field[:i], cond) except Exception: - return (field.replace("@{%s}" % cond, ""), False) + return (field.replace(f"@{{{cond}}}", ""), False) return (field, False) @@ -53,7 +53,7 @@ def get_field_condition(field): cond = field[i + 2 : j] try: if cond or cond == "": - return (field.replace("${%s}" % cond, ""), cond) + return (field.replace(f"${{{cond}}}", ""), cond) except Exception: return (field, False) return (field, False) @@ -74,7 +74,7 @@ def get_field_style(field): cond = field[i + 2 : j] try: if cond or cond == "": - return (field.replace("#{%s}" % cond, ""), cond) + return (field.replace(f"#{{{cond}}}", ""), cond) except Exception: return (field, False) return (field, False) @@ -88,7 +88,7 @@ def get_field_style_cond(field): cond = field[i + 2 : j] try: if cond or cond == "": - return (field.replace("#?%s?" % cond, ""), cond) + return (field.replace(f"#?{cond}?", ""), cond) except Exception: return (field, False) return (field, False) @@ -99,11 +99,14 @@ def fill_cell_style(field, field_style, styles): for f in field_styles: (key, value) = f.split("=") if key not in styles.keys(): - raise ValidationError(_("Invalid style type %s") % key) + 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} + _( + "Invalid value %(value)s for style type %(key)s", + value=value, + key=key, + ) ) cell_style = styles[key][value] if key == "font": @@ -151,7 +154,7 @@ def get_groupby(line_field): 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) + raise ValidationError(_("Position %s is not valid", pos)) col, row = match.groups() return col, int(row) @@ -163,7 +166,7 @@ def openpyxl_get_sheet_by_name(book, name): if sheetname == name: return book.worksheets[i] i += 1 - raise ValidationError(_("'%s' sheet not found") % (name,)) + raise ValidationError(_("'%s' sheet not found", name)) def xlrd_get_sheet_by_name(book, name): @@ -173,7 +176,7 @@ def xlrd_get_sheet_by_name(book, name): if sheet.name == name: return sheet except IndexError as exc: - raise ValidationError(_("'%s' sheet not found") % (name,)) from exc + raise ValidationError(_("'%s' sheet not found", name)) from exc def isfloat(input_val): @@ -236,9 +239,9 @@ def csv_from_excel(excel_content, delimiter, quote): raise ValidationError( _( "Template with CSV Quoting = False, data must not " - 'contain the same char as delimiter -> "%s"' + 'contain the same char as delimiter -> "%s"', + delimiter, ) - % delimiter ) row.append(x) wr.writerow(row) @@ -250,7 +253,7 @@ def csv_from_excel(excel_content, delimiter, quote): 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,)) + raise ValidationError(_("Position %s is not valid", pos)) col, row = match.groups() col_num = 0 for c in col: @@ -290,7 +293,7 @@ def _get_cell_value(cell, field_type=False): value = value_str elif field_type in ["many2one"]: # If number, change to string - if isinstance(cell.value, (int, float, complex)): + if isinstance(cell.value, int | float | complex): value = str(cell.value) else: value = cell.value diff --git a/excel_import_export/models/ir_report.py b/excel_import_export/models/ir_report.py index 68e8029e8ea..40da6e5fcb2 100644 --- a/excel_import_export/models/ir_report.py +++ b/excel_import_export/models/ir_report.py @@ -1,7 +1,7 @@ # 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 import api, fields, models from odoo.exceptions import UserError @@ -15,17 +15,21 @@ class ReportAction(models.Model): @api.model def _render_excel(self, docids, data): if len(docids) != 1: - raise UserError(_("Only one id is allowed for excel_import_export")) + raise UserError( + self.env._("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} + self.env._( + "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]) + return self.env["xlsx.export"].export_xlsx(xlsx_template, self.model, docids[0]) @api.model def _get_report_from_name(self, report_name): @@ -33,10 +37,9 @@ def _get_report_from_name(self, report_name): if res: return res report_obj = self.env["ir.actions.report"] - qwebtypes = ["excel"] - conditions = [ - ("report_type", "in", qwebtypes), + domain = [ + ("report_type", "=", "excel"), ("report_name", "=", report_name), ] context = self.env["res.users"].context_get() - return report_obj.with_context(**context).search(conditions, limit=1) + return report_obj.with_context(**context).search(domain, limit=1) diff --git a/excel_import_export/models/xlsx_export.py b/excel_import_export/models/xlsx_export.py index 8a1fb9ed77c..3cbe9a3030e 100644 --- a/excel_import_export/models/xlsx_export.py +++ b/excel_import_export/models/xlsx_export.py @@ -9,7 +9,7 @@ from datetime import datetime as dt from io import BytesIO -from odoo import _, api, fields, models +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 @@ -79,7 +79,9 @@ def _get_line_vals(self, record, line_field, fields): 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) + raise Exception( + self.env._("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() @@ -123,7 +125,7 @@ def _eval_style_cond(self, model, record, value, style_cond): i += 1 field, style = co.get_field_style(field) styles.update({i: style}) - style_cond = style_cond.replace("#{%s}" % style, str(i)) + style_cond = style_cond.replace(f"#{{{style}}}", str(i)) if not styles: return False res = safe_eval(style_cond, eval_context) @@ -144,26 +146,26 @@ def _fill_workbook_data(self, workbook, record, data_dict): 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")) + raise Exception(self.env._("Not enough worksheets")) st = workbook.worksheets[sheet_name - 1] if not st: - raise ValidationError(_("Sheet %s not found") % sheet_name) + raise ValidationError(self.env._("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 + raise ValidationError(self.env._("Key Error\n%s", e)) from e except IllegalCharacterError as e: raise ValidationError( - _( + self.env._( "IllegalCharacterError\n" - "Some exporting data contain special character\n%s" + "Some exporting data contain special character\n%s", + e, ) - % e ) from e except Exception as e: raise ValidationError( - _("Error filling data into Excel sheets\n%s") % e + self.env._("Error filling data into Excel sheets\n%s", e) ) from e @api.model @@ -251,7 +253,7 @@ def _fill_lines(self, ws, st, record): @api.model def export_xlsx(self, template, res_model, res_ids): if template.res_model != res_model: - raise ValidationError(_("Template's model mismatch")) + raise ValidationError(self.env._("Template's model mismatch")) data_dict = co.literal_eval(template.instruction.strip()) export_dict = data_dict.get("__EXPORT__", False) out_name = template.name diff --git a/excel_import_export/models/xlsx_import.py b/excel_import_export/models/xlsx_import.py index 829edc7c9db..f1d1333f1d0 100644 --- a/excel_import_export/models/xlsx_import.py +++ b/excel_import_export/models/xlsx_import.py @@ -11,7 +11,7 @@ import xlrd import xlwt -from odoo import _, api, models +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 @@ -71,7 +71,7 @@ def _get_field_type(self, model, field): return field_type except Exception as exc: raise ValidationError( - _("Invalid declaration, %s has no valid field type") % field + self.env._("Invalid declaration, %s has no valid field type", field) ) from exc @api.model @@ -94,7 +94,7 @@ def _delete_record_data(self, record, data_dict): 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 + raise ValidationError(self.env._("Error deleting data\n%s", e)) from e @api.model def _get_end_row(self, st, worksheet, line_field): @@ -112,12 +112,13 @@ def _get_end_row(self, st, worksheet, line_field): cell_type = st.cell_type(idx, col) # empty type = 0 except Exception as e: raise UserError( - _( + self.env._( "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." + "cell in the Excel sheet. Please check your Excel file.", + field=_col, + cell_position=rc, ) - % {"field": _col, "cell_position": rc} ) from e r_types = test_rows.get(idx, []) r_types.append(cell_type) @@ -166,7 +167,7 @@ def _process_worksheet(self, wb, out_wb, out_st, model, data_dict, header_fields elif isinstance(sheet_name, int): st = wb.sheet_by_index(sheet_name - 1) if not st: - raise ValidationError(_("Sheet %s not found") % sheet_name) + raise ValidationError(self.env._("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) @@ -256,7 +257,7 @@ def _import_record_data(self, import_file, record, data_dict): }, ) if errors.get("messages"): - message = _("Error importing data") + message = self.env._("Error importing data") messages = errors["messages"] if isinstance(messages, dict): message = messages["message"] @@ -266,7 +267,7 @@ def _import_record_data(self, import_file, record, data_dict): return self.env.ref(xml_id) except xlrd.XLRDError as exc: raise ValidationError( - _("Invalid file style, only .xls or .xlsx file allowed") + self.env._("Invalid file style, only .xls or .xlsx file allowed") ) from exc except Exception as e: raise e @@ -282,7 +283,9 @@ def _post_import_operation(self, record, operation): eval_context = {"object": record} safe_eval(code, eval_context) except Exception as e: - raise ValidationError(_("Post import operation error\n%s") % e) from e + raise ValidationError( + self.env._("Post import operation error\n%s", e) + ) from e @api.model def import_xlsx(self, import_file, template, res_model=False, res_id=False): @@ -292,12 +295,12 @@ def import_xlsx(self, import_file, template, res_model=False, res_id=False): - Import data from excel according to data_dict['__IMPORT__'] """ if res_model and template.res_model != res_model: - raise ValidationError(_("Template's model mismatch")) + raise ValidationError(self.env._("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 + self.env._("No data_dict['__IMPORT__'] in template %s", template.name) ) if record: # Delete existing data first diff --git a/excel_import_export/models/xlsx_report.py b/excel_import_export/models/xlsx_report.py index f7e5be04304..0c1bf9a4081 100644 --- a/excel_import_export/models/xlsx_report.py +++ b/excel_import_export/models/xlsx_report.py @@ -1,7 +1,7 @@ # 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 import api, fields, models from odoo.exceptions import ValidationError @@ -33,11 +33,11 @@ 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")) + raise ValidationError(self.env._("No template found")) defaults = super().default_get(fields) for template in templates: if not template.datas: - raise ValidationError(_("No file in %s") % (template.name,)) + raise ValidationError(self.env._("No file in %s", template.name)) defaults["template_id"] = len(templates) == 1 and templates.id or False return defaults @@ -47,6 +47,7 @@ def report_xlsx(self): 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 { + "name": self.env._("Excel Report"), "type": "ir.actions.act_window", "res_model": self._name, "view_mode": "form", diff --git a/excel_import_export/models/xlsx_template.py b/excel_import_export/models/xlsx_template.py index f22439f33eb..934b3e64299 100644 --- a/excel_import_export/models/xlsx_template.py +++ b/excel_import_export/models/xlsx_template.py @@ -6,7 +6,7 @@ from ast import literal_eval from os.path import join as opj -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import UserError, ValidationError from odoo.modules.module import get_module_path @@ -125,9 +125,7 @@ class XLSXTemplate(models.Model): def _compute_result_field(self): for rec in self: - rec.result_field = ( - ("x_%s_results" % rec.id) if rec.result_model_id else False - ) + rec.result_field = (f"x_{rec.id}_results") if rec.result_model_id else False @api.constrains("redirect_action", "res_model") def _check_action_model(self): @@ -138,8 +136,10 @@ def _check_action_model(self): and rec.res_model != rec.redirect_action.res_model ): raise ValidationError( - _("The selected redirect action is " "not for model %s") - % rec.res_model + self.env._( + "The selected redirect action is " "not for model %s", + rec.res_model, + ) ) @api.model @@ -196,6 +196,7 @@ def _update_result_field_common_wizard(self): self.ensure_one() _model = self.env["ir.model"].search([("model", "=", "report.xlsx.wizard")]) _model.ensure_one() + result_model = self.result_model_id.model _field = self.env["ir.model.fields"].search( [("model", "=", "report.xlsx.wizard"), ("name", "=", self.result_field)] ) @@ -206,16 +207,18 @@ def _update_result_field_common_wizard(self): "name": self.result_field, "field_description": "Results", "ttype": "many2many", - "relation": self.result_model_id.model, + "relation": result_model, "store": False, "depends": "res_model", } ) else: _field.ensure_one() - _field.write({"relation": self.result_model_id.model}) + _field.write({"relation": result_model}) _field.compute = f""" -self['{self.result_field}'] = self.env['{self.result_model_id.model}'].search(self.safe_domain(self.domain)) +self[ +'{self.result_field}' +] = self.env['{result_model}'].search(self.safe_domain(self.domain)) """ def _update_result_export_ids(self): @@ -387,9 +390,9 @@ def _compute_output_instruction(self): if line.section_type in ("head", "row"): row_field = line.row_field if line.section_type == "row" and line.is_cont: - row_field = "_CONT_%s" % row_field + row_field = f"_CONT_{row_field}" if line.section_type == "row" and line.is_extend: - row_field = "_EXTEND_%s" % row_field + row_field = f"_EXTEND_{row_field}" row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field @@ -413,7 +416,7 @@ def _compute_output_instruction(self): 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_field = f"_NODEL_{row_field}" row_dict = {row_field: {}} inst_dict[itype][prev_sheet].update(row_dict) prev_row = row_field @@ -450,12 +453,13 @@ def _create_export_action(self, model): "binding_type": "action", "target": "new", "view_mode": "form", - "context": """ - {'template_domain': [('res_model', '=', '%s'), - ('export_action_id', '!=', False), - ('gname', '=', False)]} - """ - % (self.res_model), + "context": { + "template_domain": [ + ("res_model", "=", self.res_model), + ("export_action_id", "!=", False), + ("gname", "=", False), + ] + }, } return self.env["ir.actions.act_window"].create(vals) @@ -490,12 +494,13 @@ def add_import_action(self): "binding_type": "action", "target": "new", "view_mode": "form", - "context": """ - {'template_domain': [('res_model', '=', '%s'), - ('fname', '=', '%s'), - ('gname', '=', False)]} - """ - % (self.res_model, self.fname), + "context": { + "template_domain": [ + ("res_model", "=", self.res_model), + ("fname", "=", self.fname), + ("gname", "=", False), + ], + }, } action = self.env["ir.actions.act_window"].create(vals) self.import_action_id = action @@ -508,7 +513,7 @@ def remove_import_action(self): def add_report_menu(self): self.ensure_one() if not self.fname: - raise UserError(_("No file content!")) + raise UserError(self.env._("No file content!")) # Create report action vals = { "name": self.name, @@ -587,7 +592,7 @@ def create(self, vals_list): 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 + field_cond = field_cond and f"${{{field_cond or ''}}}" or False vals.update({"field_name": field_name, "field_cond": field_cond}) return vals @@ -643,9 +648,9 @@ def _extract_field_name(self, vals): vals.update( { "field_name": field_name, - "field_cond": "${%s}" % (field_cond or ""), - "style": "#{%s}" % (style or ""), - "style_cond": "#?%s?" % (style_cond or ""), + "field_cond": f"${{{field_cond or ''}}}", + "style": f"#{{{style or ''}}}", + "style_cond": f"#?{style_cond or ''}?", "is_sum": func == "sum" and True or False, } ) diff --git a/excel_import_export/readme/CONTRIBUTORS.md b/excel_import_export/readme/CONTRIBUTORS.md index 566b4bf9f87..f5c065bfe39 100644 --- a/excel_import_export/readme/CONTRIBUTORS.md +++ b/excel_import_export/readme/CONTRIBUTORS.md @@ -1,3 +1,4 @@ - Kitti Upariphutthiphong. \<\> () - Saran Lim. \<\> () +- Do Anh Duy \<\> diff --git a/excel_import_export/readme/USAGE.md b/excel_import_export/readme/USAGE.md index 47db3f0d736..34ffa3a1005 100644 --- a/excel_import_export/readme/USAGE.md +++ b/excel_import_export/readme/USAGE.md @@ -12,7 +12,7 @@ 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 +After install this module, go to Settings \> Technical \> 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 @@ -75,13 +75,15 @@ Another option for reporting is to use report action (report_type='excel'), I.e., ``` xml - + + Quotation / Order (.xlsx) + ir.model + 'sale.order' + 'sale.order' + + report + excel + ``` By using report action, Odoo will find template using combination of @@ -98,7 +100,7 @@ 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 +1. Go to Settings \> 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 diff --git a/excel_import_export/security/ir.model.access.csv b/excel_import_export/security/ir.model.access.csv index 5dad48a6941..c71433f5bc0 100644 --- a/excel_import_export/security/ir.model.access.csv +++ b/excel_import_export/security/ir.model.access.csv @@ -1,7 +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 +xlsx_template_user,xlsx_template_user,model_xlsx_template,base.group_user,1,1,1,1 +xlsx_template_export_user,xlsx_template_export_user,model_xlsx_template_export,base.group_user,1,1,1,1 +xlsx_template_import_user,xlsx_template_import_user,model_xlsx_template_import,base.group_user,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 diff --git a/excel_import_export/static/description/index.html b/excel_import_export/static/description/index.html index c67490b3e69..e500ba84cdd 100644 --- a/excel_import_export/static/description/index.html +++ b/excel_import_export/static/description/index.html @@ -431,8 +431,8 @@

Concepts

  • 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.

+

After install this module, go to Settings > Technical > 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

@@ -487,13 +487,15 @@

Use Cases

Another option for reporting is to use report action (report_type=’excel’), I.e.,

-<report id='action_report_saleorder_excel'
-        string='Quotation / Order (.xlsx)'
-        model='sale.order'
-        name='sale_order.xlsx'
-        file='sale_order'
-        report_type='excel'
-/>
+<record id="action_report_saleorder_excel" model="ir.actions.report">
+        <field name="name">Quotation / Order (.xlsx)</field>
+        <field name="model">ir.model</field>
+        <field name="report_name">'sale.order'</field>
+        <field name="report_file">'sale.order'</field>
+        <field name="binding_model_id" ref="sale.model_sale_order"/>
+        <field name="binding_type">report</field>
+        <field name="report_type">excel</field>
+</record>
 

By using report action, Odoo will find template using combination of model and name, then do the export for the underlining record. Please @@ -510,8 +512,8 @@

Easy Reporting Option

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. +
  3. Go to Settings > Technical> Excel Import/Export > XLSX Templates, and +create a new template for a report.
  4. 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 @@ -561,6 +563,7 @@

      Contributors

      diff --git a/excel_import_export/static/src/js/report/action_manager_report.esm.js b/excel_import_export/static/src/js/report/action_manager_report.esm.js index 0b67e026285..20b073be419 100644 --- a/excel_import_export/static/src/js/report/action_manager_report.esm.js +++ b/excel_import_export/static/src/js/report/action_manager_report.esm.js @@ -1,9 +1,8 @@ -/** @odoo-module **/ - import {download} from "@web/core/network/download"; import {registry} from "@web/core/registry"; +import {user} from "@web/core/user"; -function getReportUrl({report_name, context, data}, env) { +function getReportUrl({report_name, context, data}) { // Rough copy of action_service.js _getReportUrl method. let url = `/report/excel/${report_name}`; const actionContext = context || {}; @@ -15,7 +14,7 @@ function getReportUrl({report_name, context, data}, env) { if (actionContext.active_ids) { url += `/${actionContext.active_ids.join(",")}`; } - const userContext = encodeURIComponent(JSON.stringify(env.services.user.context)); + const userContext = encodeURIComponent(JSON.stringify(user.context)); return `${url}?context=${userContext}`; } @@ -26,8 +25,8 @@ async function triggerDownload(action, {onClose}, env) { await download({ url: "/report/download", data: { - data: JSON.stringify([getReportUrl(action, env), "excel"]), - context: JSON.stringify(env.services.user.context), + data: JSON.stringify([getReportUrl(action), "excel"]), + context: JSON.stringify(user.context), }, }); } finally { diff --git a/excel_import_export/views/xlsx_report.xml b/excel_import_export/views/xlsx_report.xml index 5f9272bcab9..e412a351c34 100644 --- a/excel_import_export/views/xlsx_report.xml +++ b/excel_import_export/views/xlsx_report.xml @@ -9,24 +9,18 @@
      - +
      - - - -
      +
      -
      +

      Complete Prepare Report (.xlsx)

      @@ -35,7 +29,7 @@

      -
      +
      -
      +
      diff --git a/excel_import_export/views/xlsx_template_view.xml b/excel_import_export/views/xlsx_template_view.xml index e7fa9ef64fc..bc3da2fcbcb 100644 --- a/excel_import_export/views/xlsx_template_view.xml +++ b/excel_import_export/views/xlsx_template_view.xml @@ -6,9 +6,9 @@ xlsx.template - + - + @@ -21,7 +21,7 @@ name="add_report_menu" string="Add Report Menu" type="object" - attrs="{'invisible': ['|', ('use_report_wizard', '=', False), ('report_menu_id', '!=', False)]}" + invisible="not use_report_wizard or report_menu_id" icon="fa-plus-square" help="Add new report menu at root level" class="oe_stat_button" @@ -30,7 +30,7 @@ name="remove_report_menu" string="Remove Report Menu" type="object" - attrs="{'invisible': [('report_menu_id', '=', False)]}" + invisible="not report_menu_id" icon="fa-minus-square" class="oe_stat_button" /> @@ -42,31 +42,22 @@ - - - + + + @@ -79,28 +70,24 @@ -
      +
      - + - + - +

      Help with Export Instruction

      @@ -231,29 +218,25 @@
    - +
    - + - + - +
    @@ -417,7 +400,7 @@ XLSX Templates ir.actions.act_window xlsx.template - tree,form + list,form

    Click to create a XLSX Template Object. diff --git a/excel_import_export/wizard/export_xlsx_wizard.py b/excel_import_export/wizard/export_xlsx_wizard.py index b7d5a755122..3a03d33a7b6 100644 --- a/excel_import_export/wizard/export_xlsx_wizard.py +++ b/excel_import_export/wizard/export_xlsx_wizard.py @@ -1,7 +1,7 @@ # 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 import api, fields, models from odoo.exceptions import ValidationError from odoo.tools.safe_eval import safe_eval @@ -13,6 +13,15 @@ class ExportXLSXWizard(models.TransientModel): _name = "export.xlsx.wizard" _description = "Wizard for exporting excel" + def _domain_template_id(self): + return ( + "[" + "('res_model', '=', res_model), " + "('export_action_id', '!=', False), " + "('gname', '=', False)" + "]" + ) + name = fields.Char(string="File Name", readonly=True, size=500) data = fields.Binary(string="File", readonly=True) template_id = fields.Many2one( @@ -20,7 +29,7 @@ class ExportXLSXWizard(models.TransientModel): string="Template", required=True, ondelete="cascade", - domain=lambda self: self._context.get("template_domain", []), + domain=lambda self: self._domain_template_id(), ) res_ids = fields.Char(string="Resource IDs", readonly=True, required=True) res_model = fields.Char( @@ -40,11 +49,11 @@ 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")) + raise ValidationError(self.env._("No template found")) defaults = super().default_get(fields) for template in templates: if not template.datas: - raise ValidationError(_("No file in %s") % (template.name,)) + raise ValidationError(self.env._("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 @@ -58,6 +67,7 @@ def action_export(self): ) self.write({"state": "get", "data": out_file, "name": out_name}) return { + "name": self.env._("Excel Report"), "type": "ir.actions.act_window", "res_model": "export.xlsx.wizard", "view_mode": "form", diff --git a/excel_import_export/wizard/export_xlsx_wizard.xml b/excel_import_export/wizard/export_xlsx_wizard.xml index 61874161110..5248c7e4374 100644 --- a/excel_import_export/wizard/export_xlsx_wizard.xml +++ b/excel_import_export/wizard/export_xlsx_wizard.xml @@ -8,18 +8,14 @@ export.xlsx.wizard - - + - - - - + -

    +

    Complete Prepare File (.xlsx)

    Here is the exported file:

    -
    -
    -
    +
    +
    +
    diff --git a/excel_import_export/wizard/import_xlsx_wizard.py b/excel_import_export/wizard/import_xlsx_wizard.py index 6ef7bfdf806..7899b949f0c 100644 --- a/excel_import_export/wizard/import_xlsx_wizard.py +++ b/excel_import_export/wizard/import_xlsx_wizard.py @@ -2,7 +2,7 @@ # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html) -from odoo import _, api, fields, models +from odoo import api, fields, models from odoo.exceptions import RedirectWarning, ValidationError @@ -13,6 +13,15 @@ class ImportXLSXWizard(models.TransientModel): _name = "import.xlsx.wizard" _description = "Wizard for importing excel" + def _domain_template_id(self): + return ( + "[" + "('res_model', '=', res_model), " + "('fname', '=', fname), " + "('gname', '=', False)" + "]" + ) + import_file = fields.Binary(string="Import File (*.xlsx)") filename = fields.Char("Import File Name") template_id = fields.Many2one( @@ -20,7 +29,7 @@ class ImportXLSXWizard(models.TransientModel): string="Template", required=True, ondelete="cascade", - domain=lambda self: self._context.get("template_domain", []), + domain=lambda self: self._domain_template_id(), ) res_id = fields.Integer(string="Resource ID", readonly=True) res_model = fields.Char(string="Resource Model", readonly=True, size=500) @@ -41,30 +50,30 @@ class ImportXLSXWizard(models.TransientModel): "\n* Get: wizard show results from user action", ) - @api.model - def view_init(self, fields_list): + def check_view_init(self, context): """This template only works on some context of active record""" - res = super().view_init(fields_list) - res_model = self._context.get("active_model", False) - res_id = self._context.get("active_id", False) + res_model = context.get("active_model", False) + res_id = context.get("active_id", False) if not res_model or not res_id: - return res + return 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", []) + import_states = 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) + messages.append( + self.env._("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")) + messages.append(self.env._("Document must be in draft state")) valid = False # Context testing - if self._context.get("template_context", False): - template_context = self._context["template_context"] + if context.get("template_context", False): + template_context = context["template_context"] for key, value in template_context.items(): if ( key not in record @@ -77,7 +86,7 @@ def view_init(self, fields_list): ): valid = False messages.append( - _( + self.env._( "This import action is not usable " "in this document context" ) @@ -85,45 +94,47 @@ def view_init(self, fields_list): break if not valid: raise ValidationError("\n".join(messages)) - return res + return @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", []) + def default_get(self, fields_list): + context = self.env.context + res_model = context.get("active_model", False) + res_id = context.get("active_id", False) + template_domain = context.get("template_domain", []) templates = self.env["xlsx.template"].search(template_domain) if not templates: - raise ValidationError(_("No template found")) - defaults = super().default_get(fields) + raise ValidationError(self.env._("No template found")) + res = super().default_get(fields_list) for template in templates: if not template.datas: act = self.env.ref("excel_import_export.action_xlsx_template") raise RedirectWarning( - _( + self.env._( 'File "%(fname)s" not found in template, %(name)s.', fname=template.fname, name=template.name, ), act.id, - _("Set Templates"), + self.env._("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 + self.check_view_init(context) + res["template_id"] = len(templates) == 1 and template.id or False + res["res_id"] = res_id + res["res_model"] = res_model + return res def get_import_sample(self): self.ensure_one() return { - "name": _("Import Excel"), + "name": self.env._("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(), + "context": self.env.context.copy(), } def action_import(self): @@ -140,7 +151,7 @@ def action_import(self): record = Import.import_xlsx(attach.datas, self.template_id) res_ids.append(record.id) else: - raise ValidationError(_("Please select Excel file to import")) + raise ValidationError(self.env._("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] diff --git a/excel_import_export/wizard/import_xlsx_wizard.xml b/excel_import_export/wizard/import_xlsx_wizard.xml index f361275e535..f02ff103f26 100644 --- a/excel_import_export/wizard/import_xlsx_wizard.xml +++ b/excel_import_export/wizard/import_xlsx_wizard.xml @@ -8,24 +8,19 @@ import.xlsx.wizard
    - - - - - - + @@ -36,32 +31,27 @@ string="⇒ Get Sample Import Template" type="object" class="oe_link" - attrs="{'invisible': [('id', '!=', False)]}" + invisible="id" />
    - + - +

    Import Successful!

    -