diff --git a/report_csv/README.rst b/report_csv/README.rst new file mode 100644 index 0000000000..d6194d25cf --- /dev/null +++ b/report_csv/README.rst @@ -0,0 +1,146 @@ +=============== +Base report csv +=============== + +.. + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! source digest: sha256:3d970690227b2dd444c5359798ce3ea5be684d6fbc82504e90694e193e738938 + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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%2Freporting--engine-lightgray.png?logo=github + :target: https://github.com/OCA/reporting-engine/tree/17.0/report_csv + :alt: OCA/reporting-engine +.. |badge4| image:: https://img.shields.io/badge/weblate-Translate%20me-F47D42.png + :target: https://translation.odoo-community.org/projects/reporting-engine-17-0/reporting-engine-17-0-report_csv + :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/reporting-engine&target_branch=17.0 + :alt: Try me on Runboat + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides a basic report class to generate csv report. + +**Table of contents** + +.. contents:: + :local: + +Configuration +============= + +In case the exported CSV report should be encoded in another system than +UTF-8, following fields of the report record (*Settings > Technical > +Reports*) should be populated accordingly. + +- Encoding: set an encoding system (such as cp932) +- Encode Error Handling: select 'Ignore' or 'Replace' as necessary. + + - 'Ignore': in case of an encoding error, the problematic character + will be removed from the exported file. + - 'Replace': in case of an encoding error, the problematic character + will be replaced with '?' symbol. + - Leaving the field blank: in case of an encoding error, the report + generation fails with an error message. + +Usage +===== + +An example of CSV report for partners on a module called +\`module_name\`: + +A python class : + +:: + + from odoo import models + + class PartnerCSV(models.AbstractModel): + _name = 'report.report_csv.partner_csv' + _inherit = 'report.report_csv.abstract' + + def generate_csv_report(self, writer, data, partners): + writer.writeheader() + for obj in partners: + writer.writerow({ + 'name': obj.name, + 'email': obj.email, + }) + + def csv_report_options(self): + res = super().csv_report_options() + res['fieldnames'].append('name') + res['fieldnames'].append('email') + res['delimiter'] = ';' + res['quoting'] = csv.QUOTE_ALL + return res + +A report XML record : + +:: + + + +Update encoding with an appropriate value (e.g. cp932) as necessary. + +Bug Tracker +=========== + +Bugs are tracked on `GitHub 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 `_. + +Do not contact contributors directly about support or help with technical issues. + +Credits +======= + +Authors +------- + +* Creu Blanca + +Contributors +------------ + +- Enric Tobella +- Jaime Arroyo +- Rattapong Chokmasermkul +- `Quartile `__: + + - Aung Ko Ko Lin + +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. + +This module is part of the `OCA/reporting-engine `_ project on GitHub. + +You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute. diff --git a/report_csv/__init__.py b/report_csv/__init__.py new file mode 100644 index 0000000000..9b6fa04eed --- /dev/null +++ b/report_csv/__init__.py @@ -0,0 +1,3 @@ +from . import controllers +from . import models +from . import report diff --git a/report_csv/__manifest__.py b/report_csv/__manifest__.py new file mode 100644 index 0000000000..81d839b2f9 --- /dev/null +++ b/report_csv/__manifest__.py @@ -0,0 +1,20 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +{ + "name": "Base report csv", + "summary": "Base module to create csv report", + "author": "Creu Blanca, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/reporting-engine", + "category": "Reporting", + "version": "17.0.1.0.0", + "license": "AGPL-3", + "depends": ["base", "web"], + "demo": ["demo/report.xml"], + "data": ["views/ir_actions_views.xml"], + "assets": { + "web.assets_backend": [ + "report_csv/static/src/js/report/qwebactionmanager.esm.js" + ] + }, + "installable": True, +} diff --git a/report_csv/controllers/__init__.py b/report_csv/controllers/__init__.py new file mode 100644 index 0000000000..12a7e529b6 --- /dev/null +++ b/report_csv/controllers/__init__.py @@ -0,0 +1 @@ +from . import main diff --git a/report_csv/controllers/main.py b/report_csv/controllers/main.py new file mode 100644 index 0000000000..0cd1686e59 --- /dev/null +++ b/report_csv/controllers/main.py @@ -0,0 +1,108 @@ +# Copyright (C) 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json +import logging + +from werkzeug.exceptions import InternalServerError +from werkzeug.urls import url_decode + +from odoo.http import ( + content_disposition, + request, + route, +) +from odoo.http import ( + serialize_exception as _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 == "csv": + 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"]) + csv = report.with_context(**context)._render_csv( + reportname, docids, data=data + )[0] + csvhttpheaders = [ + ("Content-Type", "text/csv"), + ("Content-Length", len(csv)), + ] + return request.make_response(csv, headers=csvhttpheaders) + return super().report_routes(reportname, docids, converter, **data) + + @route() + def report_download(self, data, context=None): + requestcontent = json.loads(data) + url, report_type = requestcontent[0], requestcontent[1] + reportname = "" + try: + if report_type == "csv": + reportname = url.split("/report/csv/")[1].split("?")[0] + docids = None + if "/" in reportname: + reportname, docids = reportname.split("/") + if docids: + # Generic report: + response = self.report_routes( + reportname, docids=docids, converter="csv", context=context + ) + else: + # Particular report: + data = dict( + url_decode(url.split("?")[1]).items() + ) # decoding the args represented in JSON + if "context" in data: + context, data_context = ( + json.loads(context or "{}"), + json.loads(data.pop("context")), + ) + context = json.dumps({**context, **data_context}) + response = self.report_routes( + reportname, converter="csv", context=context, **data + ) + + report = request.env["ir.actions.report"]._get_report_from_name( + reportname + ) + filename = f"{report.name}.{report_type}" + if docids: + ids = [int(x) for x in docids.split(",")] + obj = request.env[report.model].browse(ids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, {"object": obj, "time": time} + ) + filename = f"{report_name}.{report_type}" + response.headers.add( + "Content-Disposition", content_disposition(filename) + ) + return response + else: + return super().report_download(data, context) + 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} + res = request.make_response(html_escape(json.dumps(error))) + raise InternalServerError(response=res) from e diff --git a/report_csv/demo/report.xml b/report_csv/demo/report.xml new file mode 100644 index 0000000000..1bab560ac8 --- /dev/null +++ b/report_csv/demo/report.xml @@ -0,0 +1,14 @@ + + + + + Print to CSV + res.partner + csv + report_csv.partner_csv + res_partner + + diff --git a/report_csv/i18n/it.po b/report_csv/i18n/it.po new file mode 100644 index 0000000000..08b6fe05ec --- /dev/null +++ b/report_csv/i18n/it.po @@ -0,0 +1,101 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_csv +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"PO-Revision-Date: 2024-01-18 09:34+0000\n" +"Last-Translator: mymage \n" +"Language-Team: none\n" +"Language: it\n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: nplurals=2; plural=n != 1;\n" +"X-Generator: Weblate 4.17\n" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_report_report_csv_abstract +msgid "Abstract Model for CSV reports" +msgstr "Modello astratto per resoconto CSV" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__encode_error_handling +msgid "Encode Error Handling" +msgstr "Gestione errore codifica" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__encoding +msgid "Encoding" +msgstr "Codifica" + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__encoding +msgid "Encoding to be applied to the generated CSV file. e.g. cp932" +msgstr "Codifica da applicare al file CSV generato. Es. cp932" + +#. module: report_csv +#. odoo-python +#: code:addons/report_csv/report/report_csv.py:0 +#, python-format +msgid "Failed to encode the data with the encoding set in the report." +msgstr "Codifica dati fallita con la codifica impostata nel resoconto." + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__encode_error_handling +msgid "" +"If nothing is selected, CSV export will fail with an error message when " +"there is a character that fail to be encoded." +msgstr "" +"Se non è selezionato nulla, l'esportazione del CSV fallirà con un messaggio " +"di errore qando c'è un carattere che non può essere codificato." + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__encode_error_handling__ignore +msgid "Ignore" +msgstr "Ignora" + +#. module: report_csv +#: model:ir.actions.report,name:report_csv.partner_csv +msgid "Print to CSV" +msgstr "Stampa su CSV" + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__encode_error_handling__replace +msgid "Replace" +msgstr "Sostituzione" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_ir_actions_report +msgid "Report Action" +msgstr "Azione resoconto" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_report_report_csv_partner_csv +msgid "Report Partner to CSV" +msgstr "Indica il partner al CSV" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__report_type +msgid "Report Type" +msgstr "Tipo resoconto" + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__report_type +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" +"Il tipo di resoconto che verrà generato, ognuno avente il suo metodo di " +"generazione. HTML vuol dire che il resoconto sarà aperto direttamente nel " +"tuo browser mentre PDF vuol dire che il resoconto sarà generato usando " +"Wkhtmltopdf e scaricato dall'utente." + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__report_type__csv +msgid "csv" +msgstr "csv" diff --git a/report_csv/i18n/report_csv.pot b/report_csv/i18n/report_csv.pot new file mode 100644 index 0000000000..32092a2132 --- /dev/null +++ b/report_csv/i18n/report_csv.pot @@ -0,0 +1,92 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_csv +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 16.0\n" +"Report-Msgid-Bugs-To: \n" +"Last-Translator: \n" +"Language-Team: \n" +"MIME-Version: 1.0\n" +"Content-Type: text/plain; charset=UTF-8\n" +"Content-Transfer-Encoding: \n" +"Plural-Forms: \n" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_report_report_csv_abstract +msgid "Abstract Model for CSV reports" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__encode_error_handling +msgid "Encode Error Handling" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__encoding +msgid "Encoding" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__encoding +msgid "Encoding to be applied to the generated CSV file. e.g. cp932" +msgstr "" + +#. module: report_csv +#. odoo-python +#: code:addons/report_csv/report/report_csv.py:0 +#, python-format +msgid "Failed to encode the data with the encoding set in the report." +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__encode_error_handling +msgid "" +"If nothing is selected, CSV export will fail with an error message when " +"there is a character that fail to be encoded." +msgstr "" + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__encode_error_handling__ignore +msgid "Ignore" +msgstr "" + +#. module: report_csv +#: model:ir.actions.report,name:report_csv.partner_csv +msgid "Print to CSV" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__encode_error_handling__replace +msgid "Replace" +msgstr "" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_ir_actions_report +msgid "Report Action" +msgstr "" + +#. module: report_csv +#: model:ir.model,name:report_csv.model_report_report_csv_partner_csv +msgid "Report Partner to CSV" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_ir_actions_report__report_type +msgid "Report Type" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,help:report_csv.field_ir_actions_report__report_type +msgid "" +"The type of the report that will be rendered, each one having its own " +"rendering method. HTML means the report will be opened directly in your " +"browser PDF means the report will be rendered using Wkhtmltopdf and " +"downloaded by the user." +msgstr "" + +#. module: report_csv +#: model:ir.model.fields.selection,name:report_csv.selection__ir_actions_report__report_type__csv +msgid "csv" +msgstr "" diff --git a/report_csv/models/__init__.py b/report_csv/models/__init__.py new file mode 100644 index 0000000000..54dbf3df6e --- /dev/null +++ b/report_csv/models/__init__.py @@ -0,0 +1 @@ +from . import ir_report diff --git a/report_csv/models/ir_report.py b/report_csv/models/ir_report.py new file mode 100644 index 0000000000..6aa932db47 --- /dev/null +++ b/report_csv/models/ir_report.py @@ -0,0 +1,47 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from odoo import api, fields, models + + +class ReportAction(models.Model): + _inherit = "ir.actions.report" + + report_type = fields.Selection( + selection_add=[("csv", "csv")], ondelete={"csv": "set default"} + ) + encoding = fields.Char( + help="Encoding to be applied to the generated CSV file. e.g. cp932" + ) + encode_error_handling = fields.Selection( + selection=[("ignore", "Ignore"), ("replace", "Replace")], + help="If nothing is selected, CSV export will fail with an error message when " + "there is a character that fail to be encoded.", + ) + + @api.model + def _render_csv(self, report_ref, docids, data): + report_sudo = self._get_report(report_ref) + report_model_name = "report.%s" % report_sudo.report_name + report_model = self.env[report_model_name] + return report_model.with_context( + **{ + "active_model": report_sudo.model, + "encoding": self.encoding, + "encode_error_handling": self.encode_error_handling, + } + ).create_csv_report(docids, data) + + @api.model + def _get_report_from_name(self, report_name): + res = super()._get_report_from_name(report_name) + if res: + return res + report_obj = self.env["ir.actions.report"] + qwebtypes = ["csv"] + 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) diff --git a/report_csv/pyproject.toml b/report_csv/pyproject.toml new file mode 100644 index 0000000000..4231d0cccb --- /dev/null +++ b/report_csv/pyproject.toml @@ -0,0 +1,3 @@ +[build-system] +requires = ["whool"] +build-backend = "whool.buildapi" diff --git a/report_csv/readme/CONFIGURE.md b/report_csv/readme/CONFIGURE.md new file mode 100644 index 0000000000..3d2a2796f3 --- /dev/null +++ b/report_csv/readme/CONFIGURE.md @@ -0,0 +1,12 @@ +In case the exported CSV report should be encoded in another system than +UTF-8, following fields of the report record (*Settings \> Technical \> +Reports*) should be populated accordingly. + +- Encoding: set an encoding system (such as cp932) +- Encode Error Handling: select 'Ignore' or 'Replace' as necessary. + - 'Ignore': in case of an encoding error, the problematic character + will be removed from the exported file. + - 'Replace': in case of an encoding error, the problematic character + will be replaced with '?' symbol. + - Leaving the field blank: in case of an encoding error, the report + generation fails with an error message. diff --git a/report_csv/readme/CONTRIBUTORS.md b/report_csv/readme/CONTRIBUTORS.md new file mode 100644 index 0000000000..b80f61ba30 --- /dev/null +++ b/report_csv/readme/CONTRIBUTORS.md @@ -0,0 +1,5 @@ +- Enric Tobella \<\> +- Jaime Arroyo \<\> +- Rattapong Chokmasermkul \<\> +- [Quartile](https://www.quartile.co): + - Aung Ko Ko Lin diff --git a/report_csv/readme/DESCRIPTION.md b/report_csv/readme/DESCRIPTION.md new file mode 100644 index 0000000000..636b3884f3 --- /dev/null +++ b/report_csv/readme/DESCRIPTION.md @@ -0,0 +1 @@ +This module provides a basic report class to generate csv report. diff --git a/report_csv/readme/USAGE.md b/report_csv/readme/USAGE.md new file mode 100644 index 0000000000..672239895a --- /dev/null +++ b/report_csv/readme/USAGE.md @@ -0,0 +1,40 @@ +An example of CSV report for partners on a module called +\`module_name\`: + +A python class : + + from odoo import models + + class PartnerCSV(models.AbstractModel): + _name = 'report.report_csv.partner_csv' + _inherit = 'report.report_csv.abstract' + + def generate_csv_report(self, writer, data, partners): + writer.writeheader() + for obj in partners: + writer.writerow({ + 'name': obj.name, + 'email': obj.email, + }) + + def csv_report_options(self): + res = super().csv_report_options() + res['fieldnames'].append('name') + res['fieldnames'].append('email') + res['delimiter'] = ';' + res['quoting'] = csv.QUOTE_ALL + return res + +A report XML record : + + + +Update encoding with an appropriate value (e.g. cp932) as necessary. diff --git a/report_csv/report/__init__.py b/report_csv/report/__init__.py new file mode 100644 index 0000000000..9417550380 --- /dev/null +++ b/report_csv/report/__init__.py @@ -0,0 +1,2 @@ +from . import report_csv +from . import report_partner_csv diff --git a/report_csv/report/report_csv.py b/report_csv/report/report_csv.py new file mode 100644 index 0000000000..82fff52494 --- /dev/null +++ b/report_csv/report/report_csv.py @@ -0,0 +1,73 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import logging +from io import StringIO + +from odoo import _, models +from odoo.exceptions import UserError + +_logger = logging.getLogger(__name__) + +try: + import csv +except ImportError: + _logger.debug("Can not import csvwriter`.") + + +class ReportCSVAbstract(models.AbstractModel): + _name = "report.report_csv.abstract" + _description = "Abstract Model for CSV reports" + + def _get_objs_for_report(self, docids, data): + """ + Returns objects for csv report. From WebUI these + are either as docids taken from context.active_ids or + in the case of wizard are in data. Manual calls may rely + on regular context, setting docids, or setting data. + + :param docids: list of integers, typically provided by + qwebactionmanager for regular Models. + :param data: dictionary of data, if present typically provided + by qwebactionmanager for TransientModels. + :param ids: list of integers, provided by overrides. + :return: recordset of active model for ids. + """ + if docids: + ids = docids + elif data and "context" in data: + ids = data["context"].get("active_ids", []) + else: + ids = self.env.context.get("active_ids", []) + return self.env[self.env.context.get("active_model")].browse(ids) + + def create_csv_report(self, docids, data): + objs = self._get_objs_for_report(docids, data) + file_data = StringIO() + file = csv.DictWriter(file_data, **self.csv_report_options()) + self.generate_csv_report(file, data, objs) + file_data.seek(0) + encoding = self._context.get("encoding") + if not encoding: + return file_data.read(), "csv" + error_handling = self._context.get("encode_error_handling") + if error_handling: + return file_data.read().encode(encoding, errors=error_handling), "csv" + try: + return file_data.read().encode(encoding), "csv" + except Exception as e: + raise UserError( + _("Failed to encode the data with the encoding set in the report.") + ) from e + + def csv_report_options(self): + """ + :return: dictionary of parameters. At least return 'fieldnames', but + you can optionally return parameters that define the export format. + Valid parameters include 'delimiter', 'quotechar', 'escapechar', + 'doublequote', 'skipinitialspace', 'lineterminator', 'quoting'. + """ + return {"fieldnames": []} + + def generate_csv_report(self, file, data, objs): + raise NotImplementedError() diff --git a/report_csv/report/report_partner_csv.py b/report_csv/report/report_partner_csv.py new file mode 100644 index 0000000000..247c906e18 --- /dev/null +++ b/report_csv/report/report_partner_csv.py @@ -0,0 +1,24 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import csv + +from odoo import models + + +class PartnerCSV(models.AbstractModel): + _name = "report.report_csv.partner_csv" + _inherit = "report.report_csv.abstract" + _description = "Report Partner to CSV" + + def generate_csv_report(self, writer, data, partners): + writer.writeheader() + for obj in partners: + writer.writerow({"name": obj.name, "email": obj.email}) + + def csv_report_options(self): + res = super().csv_report_options() + res["fieldnames"].append("name") + res["fieldnames"].append("email") + res["delimiter"] = ";" + res["quoting"] = csv.QUOTE_ALL + return res diff --git a/report_csv/static/description/icon.png b/report_csv/static/description/icon.png new file mode 100644 index 0000000000..3a0328b516 Binary files /dev/null and b/report_csv/static/description/icon.png differ diff --git a/report_csv/static/description/index.html b/report_csv/static/description/index.html new file mode 100644 index 0000000000..c7695b67c3 --- /dev/null +++ b/report_csv/static/description/index.html @@ -0,0 +1,489 @@ + + + + + + +Base report csv + + + +
+

Base report csv

+ + +

Beta License: AGPL-3 OCA/reporting-engine Translate me on Weblate Try me on Runboat

+

This module provides a basic report class to generate csv report.

+

Table of contents

+ +
+

Configuration

+

In case the exported CSV report should be encoded in another system than +UTF-8, following fields of the report record (Settings > Technical > +Reports) should be populated accordingly.

+
    +
  • Encoding: set an encoding system (such as cp932)
  • +
  • Encode Error Handling: select ‘Ignore’ or ‘Replace’ as necessary.
      +
    • ‘Ignore’: in case of an encoding error, the problematic character +will be removed from the exported file.
    • +
    • ‘Replace’: in case of an encoding error, the problematic character +will be replaced with ‘?’ symbol.
    • +
    • Leaving the field blank: in case of an encoding error, the report +generation fails with an error message.
    • +
    +
  • +
+
+
+

Usage

+

An example of CSV report for partners on a module called +`module_name`:

+

A python class :

+
+from odoo import models
+
+class PartnerCSV(models.AbstractModel):
+    _name = 'report.report_csv.partner_csv'
+    _inherit = 'report.report_csv.abstract'
+
+    def generate_csv_report(self, writer, data, partners):
+        writer.writeheader()
+        for obj in partners:
+            writer.writerow({
+                'name': obj.name,
+                'email': obj.email,
+            })
+
+    def csv_report_options(self):
+        res = super().csv_report_options()
+        res['fieldnames'].append('name')
+        res['fieldnames'].append('email')
+        res['delimiter'] = ';'
+        res['quoting'] = csv.QUOTE_ALL
+        return res
+
+

A report XML record :

+
+<report
+    id="partner_csv"
+    model="res.partner"
+    string="Print to CSV"
+    report_type="csv"
+    name="module_name.report_name"
+    file="res_partner"
+    attachment_use="False"
+/>
+
+

Update encoding with an appropriate value (e.g. cp932) as necessary.

+
+
+

Bug Tracker

+

Bugs are tracked on GitHub 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.

+

Do not contact contributors directly about support or help with technical issues.

+
+
+

Credits

+
+

Authors

+
    +
  • Creu Blanca
  • +
+
+
+

Contributors

+ +
+
+

Maintainers

+

This module is maintained by the OCA.

+Odoo Community Association +

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.

+

This module is part of the OCA/reporting-engine project on GitHub.

+

You are welcome to contribute. To learn how please visit https://odoo-community.org/page/Contribute.

+
+
+
+ + diff --git a/report_csv/static/src/js/report/qwebactionmanager.esm.js b/report_csv/static/src/js/report/qwebactionmanager.esm.js new file mode 100644 index 0000000000..797cdec273 --- /dev/null +++ b/report_csv/static/src/js/report/qwebactionmanager.esm.js @@ -0,0 +1,54 @@ +/** @odoo-module **/ + +import {download} from "@web/core/network/download"; +import {registry} from "@web/core/registry"; + +registry + .category("ir.actions.report handlers") + .add("csv_handler", async function (action, options, env) { + if (action.report_type === "csv") { + const type = action.report_type; + let url = `/report/${type}/${action.report_name}`; + const actionContext = action.context || {}; + if (action.data && JSON.stringify(action.data) !== "{}") { + // Build a query string with `action.data` (it's the place where reports + // using a wizard to customize the output traditionally put their options) + const action_options = encodeURIComponent(JSON.stringify(action.data)); + const context = encodeURIComponent(JSON.stringify(actionContext)); + url += `?options=${action_options}&context=${context}`; + } else { + if (actionContext.active_ids) { + url += `/${actionContext.active_ids.join(",")}`; + } + if (type === "csv") { + const context = encodeURIComponent( + JSON.stringify(env.services.user.context) + ); + url += `?context=${context}`; + } + } + env.services.ui.block(); + try { + await download({ + url: "/report/download", + data: { + data: JSON.stringify([url, action.report_type]), + context: JSON.stringify(env.services.user.context), + }, + }); + } finally { + env.services.ui.unblock(); + } + const onClose = options.onClose; + if (action.close_on_report_download) { + return env.services.action.doAction( + {type: "ir.actions.act_window_close"}, + {onClose} + ); + } else if (onClose) { + onClose(); + } + return Promise.resolve(true); + } + return Promise.resolve(false); + }); diff --git a/report_csv/tests/__init__.py b/report_csv/tests/__init__.py new file mode 100644 index 0000000000..32ae3c2c34 --- /dev/null +++ b/report_csv/tests/__init__.py @@ -0,0 +1 @@ +from . import test_report diff --git a/report_csv/tests/test_report.py b/report_csv/tests/test_report.py new file mode 100644 index 0000000000..444841916e --- /dev/null +++ b/report_csv/tests/test_report.py @@ -0,0 +1,129 @@ +# Copyright 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import json +import logging +from io import StringIO +from unittest import mock + +from odoo import http +from odoo.exceptions import UserError +from odoo.tests import common +from odoo.tools import mute_logger + +from odoo.addons.web.controllers.report import ReportController + +_logger = logging.getLogger(__name__) +try: + import csv +except ImportError: + _logger.debug("Can not import csv.") + + +class TestCsvException(Exception): + def __init__(self, message): + """ + :param message: exception message and frontend modal content + """ + super().__init__(message) + + +class TestReport(common.TransactionCase): + def setUp(self): + super().setUp() + self.report_object = self.env["ir.actions.report"] + self.csv_report = self.env["report.report_csv.abstract"].with_context( + active_model="res.partner" + ) + self.report_name = "report_csv.partner_csv" + self.report = self.report_object._get_report_from_name(self.report_name) + self.docs = self.env["res.company"].search([], limit=1).partner_id + + def test_report(self): + # Test if not res: + report = self.report + self.assertEqual(report.report_type, "csv") + rep = self.report_object._render(self.report_name, self.docs.ids, {}) + str_io = StringIO(rep[0]) + dict_report = list(csv.DictReader(str_io, delimiter=";", quoting=csv.QUOTE_ALL)) + self.assertEqual(self.docs.name, dict(dict_report[0])["name"]) + + def test_id_retrieval(self): + # Typical call from WebUI with wizard + objs = self.csv_report._get_objs_for_report( + False, {"context": {"active_ids": self.docs.ids}} + ) + self.assertEqual(objs, self.docs) + + # Typical call from within code not to report_action + objs = self.csv_report.with_context( + active_ids=self.docs.ids + )._get_objs_for_report(False, False) + self.assertEqual(objs, self.docs) + + # Typical call from WebUI + objs = self.csv_report._get_objs_for_report( + self.docs.ids, {"data": [self.report_name, self.report.report_type]} + ) + self.assertEqual(objs, self.docs) + + # Typical call from render + objs = self.csv_report._get_objs_for_report(self.docs.ids, {}) + self.assertEqual(objs, self.docs) + + def test_report_with_encoding(self): + report = self.report + report.write({"encoding": "cp932"}) + rep = report._render_csv(self.report_name, self.docs.ids, {}) + str_io = StringIO(rep[0].decode()) + dict_report = list(csv.DictReader(str_io, delimiter=";", quoting=csv.QUOTE_ALL)) + self.assertEqual(self.docs.name, dict(dict_report[0])["name"]) + + report.write({"encoding": "xyz"}) + with self.assertRaises(UserError): + rep = report._render_csv(self.report_name, self.docs.ids, {}) + + +class TestCsvReport(common.HttpCase): + """ + Some tests calling controller + """ + + def setUp(self): + super().setUp() + self.report_object = self.env["ir.actions.report"] + self.csv_report = self.env["report.report_csv.abstract"].with_context( + active_model="res.partner" + ) + self.report_name = "report_csv.partner_csv" + self.report = self.report_object._get_report_from_name(self.report_name) + self.docs = self.env["res.company"].search([], limit=1).partner_id + self.session = self.authenticate("admin", "admin") + + def test_csv(self): + filename = self.get_report_headers().headers.get("Content-Disposition") + self.assertTrue(".csv" in filename) + + @mute_logger("odoo.addons.web.controllers.report") + def test_pdf_error(self): + with mock.patch.object( + ReportController, "report_routes" + ) as route_patch, self.assertLogs( + "odoo.addons.report_csv.controllers.main", level=logging.ERROR + ) as cm: + route_patch.side_effect = TestCsvException("Test") + self.get_report_headers( + suffix="/report/pdf/test/10", f_type="qweb-pdf" + ).headers.get("Content-Disposition") + [msg] = cm.output + self.assertIn("Error while generating report", msg) + + def get_report_headers( + self, suffix="/report/csv/report_csv.partner_csv/1", f_type="csv" + ): + return self.url_open( + url="/report/download", + data={ + "data": json.dumps([suffix, f_type]), + "csrf_token": http.Request.csrf_token(self), + }, + ) diff --git a/report_csv/views/ir_actions_views.xml b/report_csv/views/ir_actions_views.xml new file mode 100644 index 0000000000..7dfa02a358 --- /dev/null +++ b/report_csv/views/ir_actions_views.xml @@ -0,0 +1,14 @@ + + + + ir.actions.report + ir.actions.report + + + + + + + + +