diff --git a/report_csv/README.rst b/report_csv/README.rst new file mode 100644 index 000000000..a594624eb --- /dev/null +++ b/report_csv/README.rst @@ -0,0 +1,117 @@ +=============== +Base report csv +=============== + +.. !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + !! This file is generated by oca-gen-addon-readme !! + !! changes will be overwritten. !! + !!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!!! + +.. |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/13.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-13-0/reporting-engine-13-0-report_csv + :alt: Translate me on Weblate +.. |badge5| image:: https://img.shields.io/badge/runbot-Try%20me-875A7B.png + :target: https://runbot.odoo-community.org/runbot/143/13.0 + :alt: Try me on Runbot + +|badge1| |badge2| |badge3| |badge4| |badge5| + +This module provides a basic report class to generate csv report. + +**Table of contents** + +.. contents:: + :local: + +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 :: + + + +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 smashing 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 + +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 000000000..9b6fa04ee --- /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 000000000..6adf30c28 --- /dev/null +++ b/report_csv/__manifest__.py @@ -0,0 +1,15 @@ +# 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": "14.0.1.0.0", + "license": "AGPL-3", + "depends": ["base", "web"], + "data": ["views/webclient_templates.xml"], + "demo": ["demo/report.xml"], + "installable": True, +} diff --git a/report_csv/controllers/__init__.py b/report_csv/controllers/__init__.py new file mode 100644 index 000000000..12a7e529b --- /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 000000000..f6bae5979 --- /dev/null +++ b/report_csv/controllers/main.py @@ -0,0 +1,56 @@ +# Copyright (C) 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json + +from odoo.http import content_disposition, request, route +from odoo.tools.safe_eval import safe_eval + +from odoo.addons.web.controllers import main as report + + +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(docids, data=data)[0] + filename = "{}.{}".format(report.name, "csv") + if docids: + obj = request.env[report.model].browse(docids) + if report.print_report_name and not len(obj) > 1: + report_name = safe_eval( + report.print_report_name, + {"object": obj}, + ) + filename = "{}.{}".format(report_name, "csv") + # When we print multiple records we still allow a custom + # filename. + elif report.print_report_name and len(obj) > 1: + report_name = safe_eval( + report.print_report_name, + {"object": obj}, + ) + filename = "{}.{}".format(report_name, "csv") + csvhttpheaders = [ + ("Content-Type", "text/csv"), + ("Content-Length", len(csv)), + ("Content-Disposition", content_disposition(filename)), + ] + return request.make_response(csv, headers=csvhttpheaders) + return super(ReportController, self).report_routes( + reportname, docids, converter, **data + ) diff --git a/report_csv/demo/report.xml b/report_csv/demo/report.xml new file mode 100644 index 000000000..1bab560ac --- /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/report_csv.pot b/report_csv/i18n/report_csv.pot new file mode 100644 index 000000000..f5b7cca52 --- /dev/null +++ b/report_csv/i18n/report_csv.pot @@ -0,0 +1,93 @@ +# Translation of Odoo Server. +# This file contains the translation of the following modules: +# * report_csv +# +msgid "" +msgstr "" +"Project-Id-Version: Odoo Server 13.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 +#: code:addons/report_csv/models/ir_report.py:0 +#, python-format +msgid "%s model was not found" +msgstr "" + +#. module: report_csv +#. openerp-web +#: code:addons/report_csv/static/src/js/report/qwebactionmanager.js:0 +#, python-format +msgid "" +"A popup window with your report was blocked. You may need to change your " +"browser settings to allow popup windows for this page." +msgstr "" + +#. 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_report_report_csv_abstract__display_name +#: model:ir.model.fields,field_description:report_csv.field_report_report_csv_partner_csv__display_name +msgid "Display Name" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_report_report_csv_abstract__id +#: model:ir.model.fields,field_description:report_csv.field_report_report_csv_partner_csv__id +msgid "ID" +msgstr "" + +#. module: report_csv +#: model:ir.model.fields,field_description:report_csv.field_report_report_csv_abstract____last_update +#: model:ir.model.fields,field_description:report_csv.field_report_report_csv_partner_csv____last_update +msgid "Last Modified on" +msgstr "" + +#. module: report_csv +#: model:ir.actions.report,name:report_csv.partner_csv +msgid "Print to CSV" +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 +#. openerp-web +#: code:addons/report_csv/static/src/js/report/qwebactionmanager.js:0 +#, python-format +msgid "Warning" +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 000000000..54dbf3df6 --- /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 000000000..97e8f3a8e --- /dev/null +++ b/report_csv/models/ir_report.py @@ -0,0 +1,37 @@ +# Copyright 2019 Creu Blanca +# 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=[("csv", "csv")], ondelete={"csv": "set default"} + ) + + @api.model + def _render_csv(self, docids, data): + report_model_name = "report.%s" % self.report_name + report_model = self.env.get(report_model_name) + if report_model is None: + raise UserError(_("%s model was not found" % report_model_name)) + return report_model.with_context( + {"active_model": self.model} + ).create_csv_report(docids, data) + + @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 = ["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/readme/CONTRIBUTORS.rst b/report_csv/readme/CONTRIBUTORS.rst new file mode 100644 index 000000000..1ee404f73 --- /dev/null +++ b/report_csv/readme/CONTRIBUTORS.rst @@ -0,0 +1,3 @@ +* Enric Tobella +* Jaime Arroyo +* Rattapong Chokmasermkul diff --git a/report_csv/readme/DESCRIPTION.rst b/report_csv/readme/DESCRIPTION.rst new file mode 100644 index 000000000..636b3884f --- /dev/null +++ b/report_csv/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +This module provides a basic report class to generate csv report. diff --git a/report_csv/readme/USAGE.rst b/report_csv/readme/USAGE.rst new file mode 100644 index 000000000..e5d9964cb --- /dev/null +++ b/report_csv/readme/USAGE.rst @@ -0,0 +1,38 @@ +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 :: + + diff --git a/report_csv/report/__init__.py b/report_csv/report/__init__.py new file mode 100644 index 000000000..941755038 --- /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 000000000..0d9aeffdd --- /dev/null +++ b/report_csv/report/report_csv.py @@ -0,0 +1,61 @@ +# 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 + +_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) + return file_data.read(), "csv" + + 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 000000000..247c906e1 --- /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 000000000..3a0328b51 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 000000000..94ae1a3eb --- /dev/null +++ b/report_csv/static/description/index.html @@ -0,0 +1,462 @@ + + + + + + +Base report csv + + + +
+

Base report csv

+ + +

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

+

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

+

Table of contents

+ +
+

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"
+/>
+
+
+
+

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 smashing 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.js b/report_csv/static/src/js/report/qwebactionmanager.js new file mode 100644 index 000000000..26f787d34 --- /dev/null +++ b/report_csv/static/src/js/report/qwebactionmanager.js @@ -0,0 +1,98 @@ +// © 2019 Creu Blanca +// License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). +odoo.define("report_csv.report", function (require) { + "use strict"; + + var core = require("web.core"); + var ActionManager = require("web.ActionManager"); + var framework = require("web.framework"); + var session = require("web.session"); + var _t = core._t; + + ActionManager.include({ + _downloadReportCSV: function (url, actions) { + var self = this; + framework.blockUI(); + var type = "csv"; + var cloned_action = _.clone(actions); + var report_url = url; + + if ( + _.isUndefined(cloned_action.data) || + _.isNull(cloned_action.data) || + (_.isObject(cloned_action.data) && _.isEmpty(cloned_action.data)) + ) { + if (cloned_action.context.active_ids) { + report_url += "/" + cloned_action.context.active_ids.join(","); + } + } else { + report_url += + "?options=" + + encodeURIComponent(JSON.stringify(cloned_action.data)); + report_url += + "&context=" + + encodeURIComponent(JSON.stringify(cloned_action.context)); + } + + return new Promise(function (resolve, reject) { + var blocked = !session.get_file({ + url: report_url, + data: { + data: JSON.stringify([report_url, type]), + }, + success: resolve, + error: (error) => { + self.call("crash_manager", "rpc_error", error); + reject(); + }, + complete: framework.unblockUI, + }); + if (blocked) { + // AAB: this check should be done in get_file service directly, + // should not be the concern of the caller (and that way, get_file + // could return a deferred) + var message = _t( + "A popup window with your report was blocked. You " + + "may need to change your browser settings to allow " + + "popup windows for this page." + ); + this.do_warn(_t("Warning"), message, true); + } + }); + }, + + _triggerDownload: function (action, options, type) { + var self = this; + var reportUrls = this._makeReportUrls(action); + if (type === "csv") { + return this._downloadReportCSV(reportUrls[type], action).then( + function () { + if (action.close_on_report_download) { + var closeAction = {type: "ir.actions.act_window_close"}; + return self.doAction( + closeAction, + _.pick(options, "on_close") + ); + } + return options.on_close(); + } + ); + } + return this._super.apply(this, arguments); + }, + + _makeReportUrls: function (action) { + var reportUrls = this._super.apply(this, arguments); + reportUrls.csv = "/report/csv/" + action.report_name; + return reportUrls; + }, + + _executeReportAction: function (action, options) { + var self = this; + if (action.report_type === "csv") { + return self._triggerDownload(action, options, "csv"); + } + return this._super.apply(this, arguments); + }, + }); +}); diff --git a/report_csv/tests/__init__.py b/report_csv/tests/__init__.py new file mode 100644 index 000000000..32ae3c2c3 --- /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 000000000..cfaccc4ea --- /dev/null +++ b/report_csv/tests/test_report.py @@ -0,0 +1,57 @@ +# 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.tests import common + +_logger = logging.getLogger(__name__) +try: + import csv +except ImportError: + _logger.debug("Can not import csv.") + + +class TestReport(common.TransactionCase): + def setUp(self): + super().setUp() + 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 = 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 = report._render(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) diff --git a/report_csv/views/webclient_templates.xml b/report_csv/views/webclient_templates.xml new file mode 100644 index 000000000..62b6f161d --- /dev/null +++ b/report_csv/views/webclient_templates.xml @@ -0,0 +1,15 @@ + + + +