From 3953f7ffc6ca9981e1e528c0528c969d065bf918 Mon Sep 17 00:00:00 2001 From: ntsirintanis Date: Tue, 8 Feb 2022 11:44:05 +0100 Subject: [PATCH 01/26] [ADD] report_dynamic --- report_dynamic/README.rst | 32 ++ report_dynamic/__init__.py | 5 + report_dynamic/__manifest__.py | 28 ++ report_dynamic/data/report_dynamic_alias.xml | 29 ++ report_dynamic/data/res_users.xml | 11 + report_dynamic/demo/demo.xml | 69 +++++ report_dynamic/models/__init__.py | 5 + report_dynamic/models/header.py | 27 ++ report_dynamic/models/report_dynamic.py | 271 +++++++++++++++++ report_dynamic/models/report_dynamic_alias.py | 16 + .../models/report_dynamic_section.py | 224 ++++++++++++++ report_dynamic/readme/CONTRIBUTORS.rst | 1 + report_dynamic/readme/DESCRIPTION.rst | 1 + report_dynamic/readme/INSTALL.rst | 1 + report_dynamic/readme/ROADMAP.rst | 1 + report_dynamic/readme/USAGE.rst | 6 + .../report/report_dynamic_report.xml | 47 +++ report_dynamic/security/ir.model.access.csv | 8 + report_dynamic/security/ir_rule.xml | 32 ++ report_dynamic/security/res_groups.xml | 15 + report_dynamic/static/description/icon.png | Bin 0 -> 16145 bytes report_dynamic/tests/__init__.py | 2 + report_dynamic/tests/test_report_dynamic.py | 132 +++++++++ .../tests/test_report_dynamic_section.py | 81 ++++++ report_dynamic/views/report_dynamic.xml | 275 ++++++++++++++++++ report_dynamic/views/report_dynamic_alias.xml | 26 ++ .../views/report_dynamic_section.xml | 118 ++++++++ report_dynamic/wizards/__init__.py | 4 + report_dynamic/wizards/wizard_lock_report.py | 14 + report_dynamic/wizards/wizard_lock_report.xml | 23 ++ .../wizards/wizard_report_dynamic.py | 46 +++ .../wizards/wizard_report_dynamic.xml | 24 ++ .../report_dynamic/odoo/addons/report_dynamic | 1 + setup/report_dynamic/setup.py | 6 + 34 files changed, 1581 insertions(+) create mode 100644 report_dynamic/README.rst create mode 100644 report_dynamic/__init__.py create mode 100644 report_dynamic/__manifest__.py create mode 100644 report_dynamic/data/report_dynamic_alias.xml create mode 100644 report_dynamic/data/res_users.xml create mode 100644 report_dynamic/demo/demo.xml create mode 100644 report_dynamic/models/__init__.py create mode 100644 report_dynamic/models/header.py create mode 100644 report_dynamic/models/report_dynamic.py create mode 100644 report_dynamic/models/report_dynamic_alias.py create mode 100644 report_dynamic/models/report_dynamic_section.py create mode 100644 report_dynamic/readme/CONTRIBUTORS.rst create mode 100644 report_dynamic/readme/DESCRIPTION.rst create mode 100644 report_dynamic/readme/INSTALL.rst create mode 100644 report_dynamic/readme/ROADMAP.rst create mode 100644 report_dynamic/readme/USAGE.rst create mode 100644 report_dynamic/report/report_dynamic_report.xml create mode 100644 report_dynamic/security/ir.model.access.csv create mode 100644 report_dynamic/security/ir_rule.xml create mode 100644 report_dynamic/security/res_groups.xml create mode 100644 report_dynamic/static/description/icon.png create mode 100644 report_dynamic/tests/__init__.py create mode 100644 report_dynamic/tests/test_report_dynamic.py create mode 100644 report_dynamic/tests/test_report_dynamic_section.py create mode 100644 report_dynamic/views/report_dynamic.xml create mode 100644 report_dynamic/views/report_dynamic_alias.xml create mode 100644 report_dynamic/views/report_dynamic_section.xml create mode 100644 report_dynamic/wizards/__init__.py create mode 100644 report_dynamic/wizards/wizard_lock_report.py create mode 100644 report_dynamic/wizards/wizard_lock_report.xml create mode 100644 report_dynamic/wizards/wizard_report_dynamic.py create mode 100644 report_dynamic/wizards/wizard_report_dynamic.xml create mode 120000 setup/report_dynamic/odoo/addons/report_dynamic create mode 100644 setup/report_dynamic/setup.py diff --git a/report_dynamic/README.rst b/report_dynamic/README.rst new file mode 100644 index 0000000000..6bae1529ff --- /dev/null +++ b/report_dynamic/README.rst @@ -0,0 +1,32 @@ +.. 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 + +================= +Report Dynamic +================= + +Generate dynamic contracts based on Building Blocks/Section + +Installation +============ +* Just Install + +Configuration +============= +* No configurations needed + +Credits +======= + +Contributors +------------ + +* Sunflower IT + + + +Maintainer +---------- + +This module is maintained by Sunflower IT diff --git a/report_dynamic/__init__.py b/report_dynamic/__init__.py new file mode 100644 index 0000000000..0f8c2ca968 --- /dev/null +++ b/report_dynamic/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2022 Sunflower IT +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +from . import models +from . import wizards diff --git a/report_dynamic/__manifest__.py b/report_dynamic/__manifest__.py new file mode 100644 index 0000000000..8a25ade2c3 --- /dev/null +++ b/report_dynamic/__manifest__.py @@ -0,0 +1,28 @@ +# Copyright 2022 Sunflower IT +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). + +{ + "name": "Report Dynamic", + "version": "13.0.1.0.0", + "category": "Report", + "author": "Sunflower IT, Odoo Community Association (OCA)", + "website": "https://github.com/OCA/reporting-engine", + "license": "AGPL-3", + "summary": "Dynamic Report Builder", + "depends": ["base", "web_boolean_button"], + "data": [ + "security/res_groups.xml", + "security/ir_rule.xml", + "security/ir.model.access.csv", + "data/res_users.xml", + "data/report_dynamic_alias.xml", + "report/report_dynamic_report.xml", + "views/report_dynamic.xml", + "views/report_dynamic_section.xml", + "views/report_dynamic_alias.xml", + "wizards/wizard_lock_report.xml", + "wizards/wizard_report_dynamic.xml", + ], + "demo": ["demo/demo.xml"], + "installable": True, +} diff --git a/report_dynamic/data/report_dynamic_alias.xml b/report_dynamic/data/report_dynamic_alias.xml new file mode 100644 index 0000000000..6475d0642e --- /dev/null +++ b/report_dynamic/data/report_dynamic_alias.xml @@ -0,0 +1,29 @@ + + + + [H1val] + ${h.value} + + + [H2val] + ${h.child.value} + + + [H3val] + ${h.child.child.value} + + + [H1] + ${h.next} + + + [H2] + ${h.value}.${h.child.next} + + + [H3] + ${h.value}.${h.child.value}.${h.child.child.next} + + diff --git a/report_dynamic/data/res_users.xml b/report_dynamic/data/res_users.xml new file mode 100644 index 0000000000..7b36cd3846 --- /dev/null +++ b/report_dynamic/data/res_users.xml @@ -0,0 +1,11 @@ + + + + + + + + diff --git a/report_dynamic/demo/demo.xml b/report_dynamic/demo/demo.xml new file mode 100644 index 0000000000..ad598cf2aa --- /dev/null +++ b/report_dynamic/demo/demo.xml @@ -0,0 +1,69 @@ + + + + + Template for report + + True + + + + Demo report + + + + + A paragraph + True + Some Black content + object.name.startswith("D") + + + + Not a paragraph + False + + ${object.name} + [H1] # 1 + ${page} + [H2] # 1.1 + ${page} + [H2] # 1.2 + ${page} + [H2] # 1.3 + + [H3] # 1.3.1 + ${page} + [H3] # 1.3.2 + ${page} + [H3] # 1.3.3 + ${page} + [H3] # 1.3.4 + ${page} + [H1] # 2 + ${page} + [H2] # 2.1 + ${page} + [H2] # 2.2 + ${page} + [H3] # 2.2.1 + ${page} + [H1val].[H2val].[H3val] # 2.2.1 + + object.name.endswith("r") + + + + + Black + White + + + ${object.name} + Name: ${object.name} + + + + + + diff --git a/report_dynamic/models/__init__.py b/report_dynamic/models/__init__.py new file mode 100644 index 0000000000..9d3cb0ac5a --- /dev/null +++ b/report_dynamic/models/__init__.py @@ -0,0 +1,5 @@ +# Copyright 2022 Sunflower IT +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). +from . import report_dynamic +from . import report_dynamic_section +from . import report_dynamic_alias diff --git a/report_dynamic/models/header.py b/report_dynamic/models/header.py new file mode 100644 index 0000000000..bc2184ddef --- /dev/null +++ b/report_dynamic/models/header.py @@ -0,0 +1,27 @@ +class Header: + def __init__(self, child=False, parent=False): + self.value = 0 + self.base_value = 0 + self.child = child + if parent: + parent.child = self + + @property + def next(self): + self.value += 1 + if self.child: + self.child.reset # pylint: disable=pointless-statement + return self.value + + @property + def previous(self): + if self.value: + self.value -= 1 + return self.value + + @property + def reset(self): + self.value = self.base_value + if self.child: + self.child.reset # pylint: disable=pointless-statement + return self.value diff --git a/report_dynamic/models/report_dynamic.py b/report_dynamic/models/report_dynamic.py new file mode 100644 index 0000000000..523239c79c --- /dev/null +++ b/report_dynamic/models/report_dynamic.py @@ -0,0 +1,271 @@ +from odoo import _, api, fields, models +from odoo.exceptions import UserError + + +class ReportDynamic(models.Model): + _name = "report.dynamic" + _description = "Dynamically create reports" + + name = fields.Char(required=True) + model_id = fields.Many2one("ir.model") + # Inform the user about configured model_id + # in template + model_name = fields.Char(related="model_id.name") + model_model = fields.Char(related="model_id.model", string="Tech name of model_id") + res_id = fields.Integer() + resource_ref = fields.Reference( + string="Target record", + selection="_selection_target_model", + compute="_compute_resource_ref", + inverse="_inverse_resource_ref", + store=True, + ) + wrapper_report_id = fields.Many2one("ir.ui.view", domain="[('type', '=', 'qweb')]") + template_id = fields.Many2one( + "report.dynamic", + domain="[('is_template', '=', True)]", + copy=False, + default=lambda self: self.env.company.external_report_layout_id, + ) + report_ids = fields.One2many( + "report.dynamic", + "template_id", + domain="[('is_template', '=', False)]", + copy=False, + ) + documentation = fields.Text(default="Documentation placeholder", readonly=True) + condition_domain_global = fields.Char( + string="Global domain condition", default="[]" + ) + active = fields.Boolean(default=True) + is_template = fields.Boolean(default=False) + lock_date = fields.Date() + field_ids = fields.Many2many( + "ir.model.fields", "contextual_field_rel", "contextual_id", "field_id", "Fields" + ) + window_action_exists = fields.Boolean(compute="_compute_window_action_exists") + group_by_record_name = fields.Char( + compute="_compute_group_by_record_name", + store=True, + help="Computed field for grouping by record name in search view", + ) + + @api.model + def _selection_target_model(self): + models = self.env["ir.model"].search([]) + return [(model.model, model.name) for model in models] + + @api.depends("model_id", "res_id", "template_id") + def _compute_resource_ref(self): + for this in self: + model = ( + this.model_id.model + if this.is_template + else this.template_id.model_id.model + ) + if model: + # Return a meaningful message anytime this breaks + try: + sample_record = self.env[model].search([], limit=1) + except Exception as e: + raise UserError( + _("Model {} is not applicable for report. Reason: {}").format( + model, str(e) + ) + ) + # Tackle the problem of non-existing sample record + if not sample_record: + raise UserError( + _( + "No sample record exists for Model {}. " + "Please create one before proceeding" + ).format(model) + ) + # we need to give a default to id part of resource_ref + # otherwise it is not editable + this.resource_ref = "{},{}".format( + model, this.res_id or sample_record.id, + ) + else: + this.resource_ref = False + + def _inverse_resource_ref(self): + for this in self: + if this.resource_ref: + this.res_id = this.resource_ref.id + this.model_id = ( + self.env["ir.model"] + .search([("model", "=", this.resource_ref._name)], limit=1) + .id + ) + + def get_window_actions(self): + return self.env["ir.actions.act_window"].search( + [ + ("res_model", "=", "wizard.report.dynamic"), + ("binding_model_id", "=", self.model_id.id), + ] + ) + + def _compute_window_action_exists(self): + for this in self: + this.window_action_exists = bool(this.get_window_actions()) + + @api.depends("resource_ref") + def _compute_group_by_record_name(self): + for this in self: + this.group_by_record_name = "" + if this.is_template: + continue + if hasattr(this.resource_ref, "name"): + this.group_by_record_name = this.resource_ref.name + continue + # TODO: this is plainly wrong, fix + this.group_by_record_name = _("{} - {}").format( + this.resource_ref._name, this.resource_ref.id + ) + + def get_template_xml_id(self): + self.ensure_one() + if not self.wrapper_report_id: + # return a default + return "web.external_layout" + record = self.env["ir.model.data"].search( + [("model", "=", "ir.ui.view"), ("res_id", "=", self.wrapper_report_id.id)], + limit=1, + ) + return "{}.{}".format(record.module, record.name) + + section_ids = fields.One2many("report.dynamic.section", "report_id", copy=True) + section_count = fields.Integer(string="Sections", compute="_compute_section_count") + + @api.depends("section_ids") + def _compute_section_count(self): + for this in self: + this.section_count = len(this.section_ids) + + def action_view_section(self): + self.ensure_one() + return { + "name": _("Sections"), + "type": "ir.actions.act_window", + "res_model": "report.dynamic.section", + "view_mode": "tree,form", + "target": "current", + "context": {"default_report_id": self.id}, + "domain": [("id", "in", self.section_ids.ids)], + } + + def action_wizard_lock_report(self): + self.ensure_one() + return { + "type": "ir.actions.act_window", + "res_model": "wizard.lock.report", + "view_mode": "form", + "target": "new", + } + + def action_preview_content(self): + self.ensure_one() + action = self.env.ref("report_dynamic.report_dynamic_document_preview").read( + [] + )[0] + return action + + def action_toggle_active(self): + self.ensure_one() + self.active = not self.active + + # Override create() and write() to keep + # resource_ref always the same with template + # even if template.resource_ref=False + @api.model + def create(self, values): + records = super().create(values) + for this in records: + if this.template_id.resource_ref and not this.res_id: + this.resource_ref = this.template_id.resource_ref + # Give a default to wrapper_report_id when + # user sets template_id + this.wrapper_report_id = this._set_wrapper_report_id(this.template_id) + return records + + def write(self, values): + # If the template is changed back to a non-template + # (eg is_template is set to False), + # and the template already has children, then disallow. + if all( + [ + self.report_ids, + self.is_template, + "is_template" in values, + values.get("is_template") is False, + ] + ): + raise UserError( + _( + "You cannot switch this template because " + "it has reports connected to it" + ) + ) + # If the model is changed while + # the template already has children, disallow; + if all( + [ + self.report_ids, + "model_id" in values, + self.model_id != self.env["ir.model"].browse(values.get("model_id")), + ] + ): + raise UserError(_("You cannot change model for this report")) + if "template_id" in values and values.get("template_id"): + # if in a report we set a template + template = self.browse(values.get("template_id")) + self.resource_ref = template.resource_ref + # Give a default to wrapper_report_id when + # user sets template_id + self.wrapper_report_id = self._set_wrapper_report_id(template) + return super().write(values) + + def unlink(self): + for this in self: + if not this.is_template: + continue + if this.window_action_exists: + this.unlink_action() + return super().unlink() + + def _set_wrapper_report_id(self, template): + self.ensure_one() + return template.wrapper_report_id or self.env.company.external_report_layout_id + + # Contextual action for dynamic reports + def create_action(self): + self.ensure_one() + if self.window_action_exists: + return + if not self.model_id: + return + self.env["ir.actions.act_window"].create( + { + "name": "Dynamic Reporting", + "type": "ir.actions.act_window", + "res_model": "wizard.report.dynamic", + "context": "{'mass_report_object' : %d}" % (self.id), + "domain": [("model_id", "=", self.model_id.id)], + "view_mode": "form", + "target": "new", + "binding_type": "action", + "binding_model_id": self.model_id.id, + } + ) + + def unlink_action(self): + # We make sudo as any user with rights in this model should be able + # to delete the action, not only admin + self.env["ir.actions.act_window"].search( + [ + ("res_model", "=", "wizard.report.dynamic"), + ("binding_model_id", "=", self.model_id.id), + ] + ).sudo().unlink() diff --git a/report_dynamic/models/report_dynamic_alias.py b/report_dynamic/models/report_dynamic_alias.py new file mode 100644 index 0000000000..3b8e2dbeba --- /dev/null +++ b/report_dynamic/models/report_dynamic_alias.py @@ -0,0 +1,16 @@ +from odoo import fields, models + + +class ReportDynamicAlias(models.Model): + _name = "report.dynamic.alias" + _description = "Replace expressions before rendering" + + expression_from = fields.Char( + required=True, help="Look for this in report_id.section_ids.content" + ) + expression_to = fields.Char( + required=True, help="Replace with this in report_id.section_ids.content" + ) + is_active = fields.Boolean( + "Active", default=True, help="To use the record when prerendering" + ) diff --git a/report_dynamic/models/report_dynamic_section.py b/report_dynamic/models/report_dynamic_section.py new file mode 100644 index 0000000000..003a945736 --- /dev/null +++ b/report_dynamic/models/report_dynamic_section.py @@ -0,0 +1,224 @@ +import copy +import traceback + +from odoo import _, api, fields, models, tools +from odoo.exceptions import UserError +from odoo.tools import safe_eval + +from .header import Header + +try: + from jinja2.sandbox import SandboxedEnvironment + + mako_template_env = SandboxedEnvironment( + block_start_string="<%", + block_end_string="%>", + variable_start_string="${", + variable_end_string="}", + comment_start_string="<%doc>", + comment_end_string="", + line_statement_prefix="%", + line_comment_prefix="##", + trim_blocks=True, # do not output newline after blocks + autoescape=True, # XML/HTML automatic escaping + ) + # Let's keep these in case they are needed + # in the future + mako_template_env.globals.update( + { + "str": str, + "len": len, + "abs": abs, + "min": min, + "max": max, + "sum": sum, + "filter": filter, + "map": map, + "round": round, + "page": "

", + } + ) + mako_safe_template_env = copy.copy(mako_template_env) + mako_safe_template_env.autoescape = False +except ImportError: + pass + + +class ReportDynamicSection(models.Model): + _name = "report.dynamic.section" + _description = "Section blocks for report.dynamic" + + _order = "sequence" + + name = fields.Char() + sequence = fields.Integer("Sequence", default=10) + content = fields.Html("Content") + dynamic_content = fields.Html( + compute="_compute_dynamic_content", string="Dynamic Content", + ) + report_id = fields.Many2one("report.dynamic", string="Report", ondelete="cascade") + resource_ref = fields.Reference(related="report_id.resource_ref") + # duplicate the field to avoid including the same field twice in the view + resource_ref_preview = fields.Reference( + related="report_id.resource_ref", string="Preview Record", readonly=True + ) + res_id = fields.Integer(related="report_id.res_id") + resource_ref_model_id = fields.Many2one("ir.model", related="report_id.model_id") + + # Dynamic field editor + field_id = fields.Many2one("ir.model.fields", string="Field") + sub_object_id = fields.Many2one("ir.model", string="Sub-model") + sub_model_object_field_id = fields.Many2one("ir.model.fields", string="Sub-field") + default_value = fields.Char("Default Value") + copyvalue = fields.Char("Placeholder Expression") + is_paragraph = fields.Boolean( + default=True, string="Paragraph", help="To highlight lines" + ) + condition_python = fields.Text( + string="Python Condition", help="Condition for rendering section", + ) + condition_domain = fields.Char(string="Domain Condition", default="[]") + condition_python_preview = fields.Char( + "Preview", compute="_compute_condition_python_preview" + ) + model_id_model = fields.Char( + string="Model _description", related="report_id.model_id.model" + ) + + @api.onchange("field_id", "sub_model_object_field_id", "default_value") + def onchange_copyvalue(self): + self.sub_object_id = False + self.copyvalue = False + if self.field_id and not self.field_id.relation: + self.copyvalue = "${{object.{} or {}}}".format( + self.field_id.name, self._get_proper_default_value() + ) + self.sub_model_object_field_id = False + if self.field_id and self.field_id.relation: + self.sub_object_id = self.env["ir.model"].search( + [("model", "=", self.field_id.relation)] + )[0] + if self.sub_model_object_field_id: + self.copyvalue = "${{object.{}.{} or {}}}".format( + self.field_id.name, + self.sub_model_object_field_id.name, + self._get_proper_default_value(), + ) + + # this then needs to be an onchange, since user + # should be able to see preview after setting + # a condition, directly. + @api.onchange("condition_python") + def _compute_condition_python_preview(self): + """Compute condition and preview""" + for this in self: + this.condition_python_preview = False + if not (this.resource_ref_model_id and this.res_id and this.resource_ref): + continue + try: + # Check if there are any syntax errors etc + this.condition_python_preview = this._eval_condition_python() + except Exception as e: + # and show debug info + this.condition_python_preview = str(e) + continue + + def _eval_condition_python(self): + if not self.condition_python: + return True + condition_python = (self.condition_python or "").strip() + record = self.resource_ref + if not record: + return False + return safe_eval(condition_python, {"object": record}) + + def _eval_condition_domain(self): + condition_domain = (self.condition_domain or "[]").strip() + return self.resource_ref.filtered_domain(safe_eval(condition_domain)) + + def _get_proper_default_value(self): + self.ensure_one() + is_num = self.field_id.ttype in ("integer", "float") + value = 0 if is_num else "''" + if self.default_value: + if is_num: + value = "{}" + else: + value = "'{}'" + value = value.format(self.default_value) + return value + + # compute the dynamic content for jinja expression + def _compute_dynamic_content(self): + # a parent with two children + h = self._get_header_object() + for this in self: + if not (this._eval_condition_python() and this._eval_condition_domain()): + this.dynamic_content = "" + continue + prerendered_content = this._prerender() + try: + content = this._render_template( + prerendered_content, + this.resource_ref_model_id.model, + this.res_id, + datas={"h": h}, + ) + except Exception: + this.dynamic_content = "

%s
" % (traceback.format_exc()) + this.dynamic_content = content + + def _get_header_object(self): + h = Header(child=Header(child=Header())) + return h + + def _prerender(self): + """Substitute expressions using report.dynamic.alias records""" + self.ensure_one() + content = self.content + for alias in self.env["report.dynamic.alias"].search( + [("is_active", "=", True)] + ): + if alias.expression_from not in content: + continue + content = content.replace(alias.expression_from, alias.expression_to) + return content + + @api.model + def _render_template(self, template_txt, model, res_ids, datas=False): + """ + Render input provided by user, for report and preview + It is an edited version of mail.template._render_template() + """ + if isinstance(res_ids, int): + res_ids = [res_ids] + if datas and not isinstance(datas, dict): + raise UserError(_("datas argument is not a proper dict")) + results = dict.fromkeys(res_ids, u"") + # try to load the template + mako_env = mako_safe_template_env + template = mako_env.from_string(tools.ustr(template_txt)) + records = self.env[model].browse( + it for it in res_ids if it + ) # filter to avoid browsing [None] + res_to_rec = dict.fromkeys(res_ids, None) + for record in records: + res_to_rec[record.id] = record + # prepare template variables + variables = { + "ctx": self._context, # context kw would clash with mako internals + } + if datas: + variables.update(datas) + for res_id, record in res_to_rec.items(): + variables["object"] = record + try: + render_result = template.render(variables) + except Exception: + render_result = ( + "
Section {} could not be rendered {}
" + ).format(self.name or "(no name set)", traceback.format_exc()) + if render_result == u"False": + render_result = u"" + results[res_id] = render_result + return results[res_ids[0]] or results diff --git a/report_dynamic/readme/CONTRIBUTORS.rst b/report_dynamic/readme/CONTRIBUTORS.rst new file mode 100644 index 0000000000..6238d69d5d --- /dev/null +++ b/report_dynamic/readme/CONTRIBUTORS.rst @@ -0,0 +1 @@ +* Sunflower IT diff --git a/report_dynamic/readme/DESCRIPTION.rst b/report_dynamic/readme/DESCRIPTION.rst new file mode 100644 index 0000000000..72cb8ef06f --- /dev/null +++ b/report_dynamic/readme/DESCRIPTION.rst @@ -0,0 +1 @@ +Generate dynamic reports for any applicable model, based on Building Blocks/Sections diff --git a/report_dynamic/readme/INSTALL.rst b/report_dynamic/readme/INSTALL.rst new file mode 100644 index 0000000000..7d5eedb618 --- /dev/null +++ b/report_dynamic/readme/INSTALL.rst @@ -0,0 +1 @@ +* Just Install diff --git a/report_dynamic/readme/ROADMAP.rst b/report_dynamic/readme/ROADMAP.rst new file mode 100644 index 0000000000..bf6c307eca --- /dev/null +++ b/report_dynamic/readme/ROADMAP.rst @@ -0,0 +1 @@ +* Investigate if One2many relations can be rendered diff --git a/report_dynamic/readme/USAGE.rst b/report_dynamic/readme/USAGE.rst new file mode 100644 index 0000000000..0c19db5ea1 --- /dev/null +++ b/report_dynamic/readme/USAGE.rst @@ -0,0 +1,6 @@ +To use this module, you need to: + +1. Create a report template by selecting an appropriate model +2. Enable generating reports by clicking the Create Report Action on top right of the template form view +3. Add sections, paragraphs, domains, and overall create a report, +4. From the Action drop-down menu on a form or tree view select some records and launch the report wizard. Generate reports for each selected record using the template in step 3. diff --git a/report_dynamic/report/report_dynamic_report.xml b/report_dynamic/report/report_dynamic_report.xml new file mode 100644 index 0000000000..84d7480927 --- /dev/null +++ b/report_dynamic/report/report_dynamic_report.xml @@ -0,0 +1,47 @@ + + + + Report Dynamic + report.dynamic + + qweb-pdf + report_dynamic.report_dynamic_document_document + report_dynamic.report_dynamic_document_document + list,form + + + Report Dynamic Preview + report.dynamic + + qweb-html + report_dynamic.report_dynamic_document_document + report_dynamic.report_dynamic_document_document + list,form + + + diff --git a/report_dynamic/security/ir.model.access.csv b/report_dynamic/security/ir.model.access.csv new file mode 100644 index 0000000000..8092986b92 --- /dev/null +++ b/report_dynamic/security/ir.model.access.csv @@ -0,0 +1,8 @@ +id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink +access_report_dynamic_section_read,Read access on report_dynamic section to employees,model_report_dynamic_section,base.group_user,1,0,0,0 +access_report_dynamic_templ_crud,Full access on report_dynamic template records,model_report_dynamic,report_dynamic.group_report_dynamic_template_editors,1,1,1,1 +access_report_dynamic_report_crud,Full access on report_dynamic report records,model_report_dynamic,report_dynamic.group_report_dynamic_report_editors,1,1,1,1 +access_report_dynamic_report_r,Read access on report_dynamic report records,model_report_dynamic,report_dynamic.group_report_dynamic_report_users,1,0,0,0 +access_report_dynamic_section_full,Full access on report_dynamic section,model_report_dynamic_section,base.group_system,1,1,1,1 +access_report_dynamic_section_editors,Editor access on report_dynamic section,model_report_dynamic_section,report_dynamic.group_report_dynamic_report_editors,1,1,1,1 +access_report_dynamic_alias_full,Full access on report dynamic alias,model_report_dynamic_alias,base.group_system,1,1,1,1 diff --git a/report_dynamic/security/ir_rule.xml b/report_dynamic/security/ir_rule.xml new file mode 100644 index 0000000000..e7a84130b1 --- /dev/null +++ b/report_dynamic/security/ir_rule.xml @@ -0,0 +1,32 @@ + + + + + CRUD report_dynamic + + [('is_template','=', False)] + + + + + CRUD report_dynamic templates + + [(1,'=', 1)] + + + + CRUD report_dynamic reports + + [('is_template','=', False)] + + + + R report_dynamic reports + + [('is_template','=', False)] + + + diff --git a/report_dynamic/security/res_groups.xml b/report_dynamic/security/res_groups.xml new file mode 100644 index 0000000000..469136d8b5 --- /dev/null +++ b/report_dynamic/security/res_groups.xml @@ -0,0 +1,15 @@ + + + + Edit dynamic report template + + + + Edit dynamic report + + + + View dynamic report + + + diff --git a/report_dynamic/static/description/icon.png b/report_dynamic/static/description/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b540778f217368600cfb10f9c169b5fb1b3ea837 GIT binary patch literal 16145 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%0D(ok7z$klEdFSHl4xKadnO~K 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 White alias + self.alias = self.env.ref("report_dynamic.demo_alias_1") + + def test_section_count(self): + self.assertFalse(self.RD_template.section_count) + self.env["report.dynamic.section"].create({"report_id": self.RD_template.id}) + self.assertEqual(self.RD_template.section_count, 1) + + def test_condition_python_preview(self): + self.assertEqual(self.section1.condition_python, 'object.name.startswith("D")') + self.assertFalse(self.section1.condition_python_preview) + # Evaluate condition to true + # Selected partner record is 'Wood Corner' + self.section1.condition_python = 'object.name.startswith("W")' + # trigger the computation for preview + self.section1._compute_condition_python_preview() + # an empty python_condition evaluates to string True + self.assertEqual(self.section1.condition_python_preview, "True") + # Check for syntax errors + self.section1.condition_python = 'object_name.startswith("D")' + self.section1._compute_condition_python_preview() + # read preview + self.assertEqual( + self.section1.condition_python_preview, + ": " + "\"name 'object_name' is not defined" + '" while evaluating\n\'object_name.startswith("D")\'', + ) + # nullify condition python + self.section1.condition_python = False + self.section1._compute_condition_python_preview() + self.assertEqual(self.section1.condition_python_preview, "True") + # Finally, check that preview gets False if there's no record + self.section1.resource_ref = False + self.section1._compute_condition_python_preview() + self.assertFalse(self.section1.condition_python_preview) + + def test_compute_dynamic_content(self): + self.assertFalse(self.section1.dynamic_content) + self.section1.condition_python = 'object.name.startswith("W")' + self.alias.active = False + self.assertEqual(self.section1.content, "

Some Black content

") + self.alias.active = True + self.section1._compute_dynamic_content() + self.assertEqual(self.section1.dynamic_content, "

Some White content

") + + def test_header(self): + header = self.section1._get_header_object() + # a header with a child and a grandchild + self.assertTrue(header.child) + self.assertTrue(header.child.child) + self.assertFalse(header.value) + header.next # pylint: disable=pointless-statement + header.child.next # pylint: disable=pointless-statement + header.child.child.next # pylint: disable=pointless-statement + self.assertEqual(header.value, 1) + self.assertEqual(header.child.value, 1) + self.assertEqual(header.child.child.value, 1) + header.next # pylint: disable=pointless-statement + self.assertEqual(header.value, 2) + header.previous # pylint: disable=pointless-statement + self.assertEqual(header.value, 1) + self.assertEqual(header.child.value, 0) + self.assertEqual(header.child.child.value, 0) + header.child.next # pylint: disable=pointless-statement + self.assertEqual(header.child.value, 1) + self.assertEqual(header.child.child.value, 0) + header.child.child.next # pylint: disable=pointless-statement + self.assertEqual(header.child.child.value, 1) + header.reset # pylint: disable=pointless-statement + self.assertFalse(header.value + header.child.value + header.child.child.value) diff --git a/report_dynamic/views/report_dynamic.xml b/report_dynamic/views/report_dynamic.xml new file mode 100644 index 0000000000..05b0fcbec5 --- /dev/null +++ b/report_dynamic/views/report_dynamic.xml @@ -0,0 +1,275 @@ + + + + report.dynamic.form + report.dynamic + +
+ + +
+ + + +
+
+
+ + + +