diff --git a/mis_builder/models/kpimatrix.py b/mis_builder/models/kpimatrix.py index 764eacb..a626a9d 100644 --- a/mis_builder/models/kpimatrix.py +++ b/mis_builder/models/kpimatrix.py @@ -6,6 +6,7 @@ from collections import OrderedDict, defaultdict from flectra import _ from flectra.exceptions import UserError +from flectra.tools import float_is_zero from .accounting_none import AccountingNone from .mis_kpi_data import ACC_SUM @@ -71,7 +72,8 @@ class KpiMatrixRow(object): def is_empty(self): for cell in self.iter_cells(): - if cell and cell.val not in (AccountingNone, None): + dp = cell and cell.row.kpi.env.user.company_id.currency_id.decimal_places or 6 + if cell and not float_is_zero(cell.val, dp) and cell.val not in (AccountingNone, None): return False return True diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index ce178b9..8869cf7 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -390,10 +390,15 @@ class MisReportQuery(models.Model): ) date_field = fields.Many2one( comodel_name="ir.model.fields", - required=True, + required=False, domain=[("ttype", "in", ("date", "datetime"))], ondelete="restrict", ) + query_context = fields.Text( + string='Context', + translate=False, + default='{}', + ) domain = fields.Char(string="Domain") report_id = fields.Many2one( comodel_name="mis.report", string="Report", required=True, ondelete="cascade" @@ -580,7 +585,21 @@ class MisReport(models.Model): self.ensure_one() res = {} for query in self.query_ids: - model = self.env[query.model_id.model] + eval_context = { + "env": self.env, + "fields": fields, + "time": time, + "datetime": datetime, + "dateutil": dateutil, + # deprecated + "uid": self.env.uid, + "context": self.env.context, + "date_from": date_from, + "date_to": date_to, + + } + ctx = query.query_context and safe_eval(query.query_context, eval_context) or {} + model = self.env[query.model_id.model].with_context(ctx) eval_context = { "env": self.env, "time": time, diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index a2e29c6..39cceb4 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -534,6 +534,7 @@ class MisReportInstance(models.Model): ) landscape_pdf = fields.Boolean(string="Landscape PDF") no_auto_expand_accounts = fields.Boolean(string="Disable account details expansion") + use_external_layout = fields.Boolean(string='Use External Layout') display_columns_description = fields.Boolean( help="Display the date range details in the column headers." ) diff --git a/mis_builder/report/mis_report_instance_qweb.xml b/mis_builder/report/mis_report_instance_qweb.xml index ca5f923..09966d0 100644 --- a/mis_builder/report/mis_report_instance_qweb.xml +++ b/mis_builder/report/mis_report_instance_qweb.xml @@ -20,7 +20,21 @@ + + diff --git a/mis_builder/static/src/css/report.css b/mis_builder/static/src/css/report.css index 0d58939..60c111b 100644 --- a/mis_builder/static/src/css/report.css +++ b/mis_builder/static/src/css/report.css @@ -1,46 +1,39 @@ .mis_table { - display: table; width: 100%; - table-layout: fixed; } .mis_row { - display: table-row; page-break-inside: avoid; } .mis_cell { - display: table-cell; page-break-inside: avoid; } -.mis_thead { - display: table-header-group; -} -.mis_tbody { - display: table-row-group; -} -.mis_table, -.mis_table .mis_row { - border-left: 0px; - border-right: 0px; - text-align: left; - padding-right: 3px; - padding-left: 3px; - padding-top: 2px; - padding-bottom: 2px; - border-collapse: collapse; -} + .mis_table .mis_row { border-color: grey; border-bottom: 1px solid lightGrey; } -.mis_table .mis_cell.mis_collabel { + +.mis_table .mis_cell.mis_collabel_group { font-weight: bold; background-color: #f0f0f0; text-align: center; } + +.mis_table .mis_cell.mis_collabel_group, .mis_table .mis_cell.mis_collabel.mis_first { + border-left: 2px solid #fff; +} + +.mis_table .mis_cell.mis_collabel { + font-weight: bold; + background-color: #f0f0f0; + text-align: right; + padding-right: 5px; +} .mis_table .mis_cell.mis_rowlabel { text-align: left; - /*white-space: nowrap;*/ + white-space: nowrap; } .mis_table .mis_cell.mis_amount { text-align: right; + padding-right: 5px; } diff --git a/mis_builder/views/mis_report.xml b/mis_builder/views/mis_report.xml index 14b8f1f..0cd66d4 100644 --- a/mis_builder/views/mis_report.xml +++ b/mis_builder/views/mis_report.xml @@ -68,6 +68,7 @@ name="date_field" domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]" /> + diff --git a/mis_builder/views/mis_report_instance.xml b/mis_builder/views/mis_report_instance.xml index 70adfee..73cc57a 100644 --- a/mis_builder/views/mis_report_instance.xml +++ b/mis_builder/views/mis_report_instance.xml @@ -189,6 +189,7 @@ + diff --git a/mis_builder/wizard/mis_builder_dashboard.py b/mis_builder/wizard/mis_builder_dashboard.py index b9ffe73..b58b8b0 100644 --- a/mis_builder/wizard/mis_builder_dashboard.py +++ b/mis_builder/wizard/mis_builder_dashboard.py @@ -17,7 +17,7 @@ class AddMisReportInstanceDashboard(models.TransientModel): "ir.actions.act_window", string="Dashboard", required=True, - domain="[('res_model', '=', " "'board.board')]", + domain="[('res_model', '=', 'board.board')]", ) @api.model diff --git a/mis_builder_budget/models/mis_budget_by_account_item.py b/mis_builder_budget/models/mis_budget_by_account_item.py index b14a219..9e30891 100644 --- a/mis_builder_budget/models/mis_budget_by_account_item.py +++ b/mis_builder_budget/models/mis_budget_by_account_item.py @@ -37,6 +37,12 @@ class MisBudgetByAccountItem(models.Model): required=True, # TODO domain (company_id) ) + user_type_id = fields.Many2one( + comodel_name='account.account.type', + related='account_id.user_type_id', + store=True, + readonly=True, + ) _sql_constraints = [ ( diff --git a/mis_builder_expimp/__init__.py b/mis_builder_expimp/__init__.py new file mode 100644 index 0000000..326c3db --- /dev/null +++ b/mis_builder_expimp/__init__.py @@ -0,0 +1 @@ +from . import wizards \ No newline at end of file diff --git a/mis_builder_expimp/__manifest__.py b/mis_builder_expimp/__manifest__.py new file mode 100644 index 0000000..7456fc5 --- /dev/null +++ b/mis_builder_expimp/__manifest__.py @@ -0,0 +1,16 @@ +# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl). + +{ + "name": "MIS Builder Import / Export", + "summary": """Import / Export Reports with all dependencies""", + "version": "1.0.1.0.0", + "license": "AGPL-3", + "author": "Jamotion GmbH", + "website": "https://gitlab.com/flectra-community/mis-builder", + "depends": ["mis_builder"], + "data": [ + "wizards/mis_builder_export_views.xml", + "wizards/mis_builder_import_views.xml", + ], + "installable": True, +} diff --git a/mis_builder_expimp/static/description/icon.png b/mis_builder_expimp/static/description/icon.png new file mode 100644 index 0000000..1afa781 Binary files /dev/null and b/mis_builder_expimp/static/description/icon.png differ diff --git a/mis_builder_expimp/static/description/index.html b/mis_builder_expimp/static/description/index.html new file mode 100644 index 0000000..b3c1c35 --- /dev/null +++ b/mis_builder_expimp/static/description/index.html @@ -0,0 +1,69 @@ +
+
+
+
+ +

MIS Builder - Import / Export

+

Function to import / export MIS Report Templates

+
+
+
+ +
+
+

Full export and import of report template

+
+ +
+
+

+ When exporting a report, all related data are exported too: +

+
    +
  • KPIs
  • +
  • SubKPIs
  • +
  • Expressions
  • +
  • Queries
  • +
  • Sub Reports
  • +
  • Styles
  • +
+
+
+
+ +
+
+

Quick Start

+
+

Installation

+
+

+ There are no dependencies other than flectra base modules, so you can simply install the module. +

+
+
+
+

Configuration

+
+

+ No configuration options available. +

+
+
+
+

Usage

+
+

+ You will find two new menu items at Finance -> Configuration -> MIS Reporting: +

+
    +
  • Export MIS Report -> shows a PopUp to select the report to export
  • +
  • Import MIS Report -> shows a PopUp to select the file of exported report to import
  • +
+
+
+ +
+
+ +
\ No newline at end of file diff --git a/mis_builder_expimp/static/description/screenshot.png b/mis_builder_expimp/static/description/screenshot.png new file mode 100644 index 0000000..9cbb176 Binary files /dev/null and b/mis_builder_expimp/static/description/screenshot.png differ diff --git a/mis_builder_expimp/wizards/__init__.py b/mis_builder_expimp/wizards/__init__.py new file mode 100644 index 0000000..f3eef5c --- /dev/null +++ b/mis_builder_expimp/wizards/__init__.py @@ -0,0 +1,2 @@ +from . import mis_builder_export_wizard +from . import mis_builder_import_wizard \ No newline at end of file diff --git a/mis_builder_expimp/wizards/mis_builder_export_views.xml b/mis_builder_expimp/wizards/mis_builder_export_views.xml new file mode 100644 index 0000000..24f99f5 --- /dev/null +++ b/mis_builder_expimp/wizards/mis_builder_export_views.xml @@ -0,0 +1,45 @@ + + + + + mis.builder.export.wizard.view + mis.builder.export.wizard + +
+ + +
+

This wizard will export a MIS Report including all dependencies. +
After export you can import this file in another system to have the same MIS Report. +

+
+ + + + + + + + +
+
+ +
+
+ + Export MIS Report + ir.actions.act_window + mis.builder.export.wizard + form + new + + +
+
\ No newline at end of file diff --git a/mis_builder_expimp/wizards/mis_builder_export_wizard.py b/mis_builder_expimp/wizards/mis_builder_export_wizard.py new file mode 100644 index 0000000..4613aaa --- /dev/null +++ b/mis_builder_expimp/wizards/mis_builder_export_wizard.py @@ -0,0 +1,162 @@ +# Copyright 2014 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +import base64 +import json + +from lxml import etree + +from flectra import api, fields, models, _ + +QUERY_FIELDS = ['name', 'model_id', 'field_ids', 'aggregate', 'date_field', 'query_context', 'domain'] + +REPORT_FIELDS = ['name', 'description', 'account_model', 'subreport_ids'] + +KPI_FIELDS = [ + 'name', 'description', 'multi', 'auto_expand_accounts', 'style_expression', + 'type', 'compare_method', 'accumulation_method', 'sequence', 'budgetable', + 'expression' +] + +STYLE_FIELDS = [ + 'name', + 'color_inherit', 'color', + 'background_color_inherit', 'background_color', + 'font_style_inherit', 'font_style', + 'font_weight_inherit', 'font_weight', + 'font_size_inherit', 'font_size', + 'indent_level_inherit', 'indent_level', + 'prefix_inherit', 'prefix', + 'suffix_inherit', 'suffix', + 'dp_inherit', 'dp', + 'divider_inherit', 'divider', + 'hide_empty_inherit', 'hide_empty', + 'hide_always_inherit', 'hide_alway', +] + + +class MisBuilderExportWizard(models.TransientModel): + _name = "mis.builder.export.wizard" + _description = "Export MIS Builder Report" + + report_id = fields.Many2one( + comodel_name='mis.report', + string='Report', + required=True, + ) + + name = fields.Char( + string='File Name', + default='mis_report.json', + ) + + file_save = fields.Binary( + string='Settings File', + readonly=True, + ) + + state = fields.Selection([ + ('draft', 'Draft'), + ('download', 'Download') + ], default='draft') + + def export(self): + self.ensure_one() + + report_data = self._read_subreport_data(self.report_id.subreport_ids) + report_data.append(self._read_report_data(self.report_id)) + + json_data = json.dumps(report_data, indent=2) + # change state of the wizard + self.write({ + 'name': '%s.json' % self.report_id.name, + 'file_save': base64.b64encode(json_data.encode()), + 'state': 'download' + }) + + return { + 'name': _('Save'), + 'view_type': 'form', + 'view_mode': 'form', + 'res_model': self._name, + 'type': 'ir.actions.act_window', + 'target': 'new', + 'res_id': self.id, + } + + @api.model + def _read_subreport_data(self, subreport_ids): + report_data = [] + for subreport_id in subreport_ids: + report_data.extend(self._read_subreport_data(subreport_id.subreport_id.subreport_ids)) + report_data.append(self._read_report_data(subreport_id.subreport_id)) + + return report_data + + @api.model + def _read_report_data(self, report_id): + report_data = report_id.read(REPORT_FIELDS)[0] + del report_data['id'] + report_data['extid'] = '__export__.mis_report_%s' % report_id.id + report_data['move_lines_source'] = report_id.move_lines_source.model + if report_id.style_id: + report_data['style_id'] = report_id.style_id.read(STYLE_FIELDS)[0] + del report_data['style_id']['id'] + report_data['style_id']['extid'] = '__export__.mis_report_style_%s' % report_id.style_id.id + if report_id.query_ids: + report_data['query_ids'] = [] + for query_id in report_id.query_ids: + query_data = query_id.read(QUERY_FIELDS)[0] + del query_data['id'] + query_data['model_id'] = query_id.model_id.model + query_data['field_ids'] = query_id.field_ids.mapped('name') + query_data['extid'] = '__export__.mis_report_query_%s' % query_id.id + report_data['query_ids'].append(query_data) + if report_id.kpi_ids: + report_data['kpi_ids'] = [] + last_kpi_sequence = -1 + for kpi_id in report_id.kpi_ids.sorted(key=lambda s: s.sequence): + kpi_data = kpi_id.read(KPI_FIELDS)[0] + # Fix of sequences to be unique - otherwise there are problems on importing reports with subkpis + if kpi_id.sequence <= last_kpi_sequence: + kpi_data['sequence'] = last_kpi_sequence + 1 + last_kpi_sequence = kpi_data['sequence'] + del kpi_data['id'] + if kpi_id.style_id: + kpi_data['style_id'] = kpi_id.style_id.read(STYLE_FIELDS)[0] + del kpi_data['style_id']['id'] + kpi_data['style_id']['extid'] = '__export__.mis_report_style_%s' % kpi_id.style_id.id + if kpi_id.auto_expand_accounts_style_id: + kpi_data['auto_expand_accounts_style_id'] = kpi_id.auto_expand_accounts_style_id.read(STYLE_FIELDS)[0] + del kpi_data['auto_expand_accounts_style_id']['id'] + kpi_data['auto_expand_accounts_style_id'][ + 'extid'] = '__export__.mis_report_style_%s' % kpi_id.auto_expand_accounts_style_id.id + kpi_data['extid'] = '__export__.mis_report_kpi_%s' % kpi_id.id + kpi_data['expression_ids'] = [] + for expression_id in kpi_id.expression_ids: + expression_data = expression_id.read(['sequence', 'name'])[0] + del expression_data['id'] + expression_data['extid'] = '__export__.mis_report_kpi_expression_%s' % expression_id.id + kpi_data['expression_ids'].append(expression_data) + report_data['kpi_ids'].append(kpi_data) + if report_id.subkpi_ids: + report_data['subkpi_ids'] = [] + for subkpi_id in report_id.subkpi_ids: + subkpi_data = subkpi_id.read(['sequence', 'name', 'description'])[0] + del subkpi_data['id'] + subkpi_data['extid'] = '__export__.mis_report_subkpi_%s' % subkpi_id.id + subkpi_data['expression_ids'] = [] + for expression_id in subkpi_id.expression_ids: + expression_data = expression_id.read(['sequence', 'name'])[0] + del expression_data['id'] + expression_data['kpi_id'] = '__export__.mis_report_kpi_%s' % expression_id.kpi_id.id + expression_data['extid'] = '__export__.mis_report_kpi_expression_%s' % expression_id.id + subkpi_data['expression_ids'].append(expression_data) + report_data['subkpi_ids'].append(subkpi_data) + if report_id.subreport_ids: + report_data['subreport_ids'] = [] + for subreport_id in report_id.subreport_ids: + subreport_data = subreport_id.read(['name'])[0] + del subreport_data['id'] + subreport_data['subreport_id'] = '__export__.mis_report_%s' % subreport_id.subreport_id.id + report_data['subreport_ids'].append(subreport_data) + return report_data diff --git a/mis_builder_expimp/wizards/mis_builder_import_views.xml b/mis_builder_expimp/wizards/mis_builder_import_views.xml new file mode 100644 index 0000000..b2f57ab --- /dev/null +++ b/mis_builder_expimp/wizards/mis_builder_import_views.xml @@ -0,0 +1,34 @@ + + + + + mis.builder.import.wizard.view + mis.builder.import.wizard + +
+
+

This wizard will import a MIS report from json File.

+
+ +
+
+ +
+
+ + Import MIS Report + ir.actions.act_window + mis.builder.import.wizard + form + new + + +
+
\ No newline at end of file diff --git a/mis_builder_expimp/wizards/mis_builder_import_wizard.py b/mis_builder_expimp/wizards/mis_builder_import_wizard.py new file mode 100644 index 0000000..19d674e --- /dev/null +++ b/mis_builder_expimp/wizards/mis_builder_import_wizard.py @@ -0,0 +1,153 @@ +import copy +import json +import logging +import subprocess +import sys +from io import StringIO +from pathlib import Path + +from flectra import models, fields, api, _ +from flectra.tools import config, etree, base64, convert_xml_import, tempfile + +import os + +from flectra.exceptions import ValidationError + +_logger = logging.getLogger(__name__) + + +class MisBuilderImportWizard(models.TransientModel): + # Private attributes + _name = "mis.builder.import.wizard" + _description = "Import MIS Builder Report" + + file = fields.Binary( + string='Settings File', + required=True, + ) + + def __int__(self): + super(MisBuilderImportWizard, self).__int__() + + def import_report(self): + self.ensure_one() + + json_data = base64.b64decode(self.file) + report_data = json.loads(json_data) + + for report_entry in report_data: + report_values = self.__prepare_report_values(report_entry) + + self.__prepare_style_values(report_entry, report_values) + + self.__prepare_subreport_values(report_entry, report_values) + + self.__prepare_query_values(report_values) + + self.__prepare_kpi_values(report_values) + + subkpi_values = [] + if report_values.get('subkpi_ids'): + for subkpi_value in report_values['subkpi_ids']: + subkpi_values.append(subkpi_value) + del report_values['subkpi_ids'] + + new_report = self.env['mis.report'].create(report_values) + self._create_external_reference(new_report, report_entry['extid']) + + self.__update_subkpi_values(new_report, report_entry, subkpi_values) + + def __prepare_style_values(self, report_entry, report_values): + if report_entry.get('style_id'): + report_values['style_id'] = self._create_style(report_values['style_id']).id + + def __prepare_report_values(self, report_entry): + result = self.env['ir.model.data'].xmlid_to_res_id(report_entry['extid']) + if result: + raise ValidationError('Report already exists! Please delete it first before importing') + report_values = copy.deepcopy(report_entry) + report_values['move_lines_source'] = self.env['ir.model'].search([('model', '=', report_values['move_lines_source'])]).id + return report_values + + def __update_subkpi_values(self, new_report, report_entry, subkpi_values): + temporary_extids = self.env['ir.model.data'] + subkpi_updates = [] + if subkpi_values: + for kpi_id in new_report.kpi_ids: + for kpi_data in report_entry['kpi_ids']: + if kpi_id.sequence == kpi_data['sequence']: + temporary_extids |= self._create_external_reference(kpi_id, kpi_data['extid']) + for subkpi_value in subkpi_values: + expression_updates = [] + for expression_value in subkpi_value['expression_ids']: + expression_value['kpi_id'] = self.env.ref(expression_value['kpi_id']).id + expression_updates.append((0, False, expression_value)) + subkpi_value['expression_ids'] = expression_updates + subkpi_updates.append((0, False, subkpi_value)) + if subkpi_updates: + new_report.write({'subkpi_ids': subkpi_updates}) + if temporary_extids: + temporary_extids.unlink() + + def __prepare_kpi_values(self, report_values): + if report_values.get('kpi_ids'): + kpi_values = [] + for kpi_value in report_values['kpi_ids']: + self.__prepare_style_values(kpi_value, kpi_value) + if kpi_value.get('auto_expand_accounts_style_id'): + kpi_value['auto_expand_accounts_style_id'] = self._create_style(kpi_value['auto_expand_accounts_style_id']).id + if report_values.get('subkpi_ids'): + del kpi_value['expression'] + del kpi_value['expression_ids'] + else: + if kpi_value.get('expression_ids'): + expression_values = [(0, False, v) for v in kpi_value['expression_ids']] + kpi_value['expression_ids'] = expression_values + kpi_values.append((0, False, kpi_value)) + report_values['kpi_ids'] = kpi_values + + def __prepare_query_values(self, report_values): + if report_values.get('query_ids'): + query_values = [] + for query_value in report_values['query_ids']: + query_model = self.env['ir.model'].search([('model', '=', query_value['model_id'])]) + query_value['model_id'] = query_model.id + query_value['field_ids'] = [(6, 0, [f.id for f in query_model.field_id if f.name in query_value['field_ids']])] + query_values.append((0, False, query_value)) + report_values['query_ids'] = query_values + + def __prepare_subreport_values(self, report_entry, report_values): + if report_entry.get('subreport_ids'): + subreport_values = [] + for subreport_value in report_entry['subreport_ids']: + subreport_value['subreport_id'] = self.env.ref(subreport_value['subreport_id']).id + subreport_values.append((0, False, subreport_value)) + report_values['subreport_ids'] = subreport_values + + @api.model + def _create_style(self, style_data): + result = self.env['ir.model.data'].xmlid_to_object(style_data['extid']) + if result: + return result + + new_style = self.env['mis.report.style'].create(style_data) + self._create_external_reference(new_style, style_data['extid']) + return new_style + + def _create_external_reference(self, obj, extid, temporary=False): + module, name = extid.split('.', 1) + existing = self.env['ir.model.data'].search([ + ('module', '=', module), + ('model', '=', obj._name), + ('name', '=', name), + ], limit=1) + if existing: + existing.res_id = obj.id + return existing + + return self.env['ir.model.data'].create({ + 'module': module, + 'model': obj._name, + 'name': name, + 'res_id': obj.id, + })