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..cfcad4ad2 --- /dev/null +++ b/report_csv/__manifest__.py @@ -0,0 +1,19 @@ +# 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": "16.0.1.0.0", + "license": "AGPL-3", + "depends": ["base", "web"], + "demo": ["demo/report.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 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..41f1bfbf5 --- /dev/null +++ b/report_csv/controllers/main.py @@ -0,0 +1,105 @@ +# Copyright (C) 2019 Creu Blanca +# License AGPL-3.0 or later (https://www.gnuorg/licenses/agpl.html). + +import json +import logging + +from werkzeug.urls import url_decode + +from odoo.http import ( + content_disposition, + request, + route, + 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(ReportController, self).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] + 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 = "%s.%s" % (report.name, "csv") + + 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 = "%s.%s" % (report_name, "csv") + response.headers.add( + "Content-Disposition", content_disposition(filename) + ) + return response + else: + return super(ReportController, self).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} + return request.make_response(html_escape(json.dumps(error))) 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..4a5ad6adf --- /dev/null +++ b/report_csv/models/ir_report.py @@ -0,0 +1,35 @@ +# 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"} + ) + + @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 + ).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.esm.js b/report_csv/static/src/js/report/qwebactionmanager.esm.js new file mode 100644 index 000000000..797cdec27 --- /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 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..edce4e4db --- /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() + 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) diff --git a/setup/report_csv/odoo/addons/report_csv b/setup/report_csv/odoo/addons/report_csv new file mode 120000 index 000000000..bdfab6f25 --- /dev/null +++ b/setup/report_csv/odoo/addons/report_csv @@ -0,0 +1 @@ +../../../../report_csv \ No newline at end of file diff --git a/setup/report_csv/setup.py b/setup/report_csv/setup.py new file mode 100644 index 000000000..28c57bb64 --- /dev/null +++ b/setup/report_csv/setup.py @@ -0,0 +1,6 @@ +import setuptools + +setuptools.setup( + setup_requires=['setuptools-odoo'], + odoo_addon=True, +)