From b1a5dc51a9f6b296257f05d33870f1cfdfc5bc71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 21 Oct 2015 22:51:20 +0200 Subject: [PATCH 001/182] Migrate aep to 9.0: periods are gone, so far, so good --- mis_builder/models/aep.py | 114 +++----------------------------------- 1 file changed, 7 insertions(+), 107 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 740a58ec..ca1f47f8 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -25,7 +25,6 @@ import re from collections import defaultdict -from openerp.exceptions import Warning from openerp.osv import expression from openerp.tools.safe_eval import safe_eval from openerp.tools.translate import _ @@ -188,7 +187,6 @@ class AccountingExpressionProcessor(object): def get_aml_domain_for_expr(self, expr, date_from, date_to, - period_from, period_to, target_move): """ Get a domain on account.move.line for an expression. @@ -213,121 +211,24 @@ class AccountingExpressionProcessor(object): if mode not in date_domain_by_mode: date_domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - period_from, period_to, mode, target_move) return expression.OR(aml_domains) + \ expression.OR(date_domain_by_mode.values()) - def _period_has_moves(self, period): - move_model = self.env['account.move'] - return bool(move_model.search([('period_id', '=', period.id)], - limit=1)) - - def _get_previous_opening_period(self, period, company_id): - period_model = self.env['account.period'] - periods = period_model.search( - [('date_start', '<=', period.date_start), - ('special', '=', True), - ('company_id', '=', company_id)], - order="date_start desc", - limit=1) - return periods and periods[0] - - def _get_previous_normal_period(self, period, company_id): - period_model = self.env['account.period'] - periods = period_model.search( - [('date_start', '<', period.date_start), - ('special', '=', False), - ('company_id', '=', company_id)], - order="date_start desc", - limit=1) - return periods and periods[0] - - def _get_first_normal_period(self, company_id): - period_model = self.env['account.period'] - periods = period_model.search( - [('special', '=', False), - ('company_id', '=', company_id)], - order="date_start asc", - limit=1) - return periods and periods[0] - - def _get_period_ids_between(self, period_from, period_to, company_id): - period_model = self.env['account.period'] - periods = period_model.search( - [('date_start', '>=', period_from.date_start), - ('date_stop', '<=', period_to.date_stop), - ('special', '=', False), - ('company_id', '=', company_id)]) - period_ids = [p.id for p in periods] - if period_from.special: - period_ids.append(period_from.id) - return period_ids - - def _get_period_company_ids(self, period_from, period_to): - period_model = self.env['account.period'] - periods = period_model.search( - [('date_start', '>=', period_from.date_start), - ('date_stop', '<=', period_to.date_stop), - ('special', '=', False)]) - return set([p.company_id.id for p in periods]) - - def _get_period_ids_for_mode(self, period_from, period_to, mode): - assert not period_from.special - assert not period_to.special - assert period_from.company_id == period_to.company_id - assert period_from.date_start <= period_to.date_start - period_ids = [] - for company_id in self._get_period_company_ids(period_from, period_to): - if mode == MODE_VARIATION: - period_ids.extend(self._get_period_ids_between( - period_from, period_to, company_id)) - else: - if mode == MODE_INITIAL: - period_to = self._get_previous_normal_period( - period_from, company_id) - # look for opening period with moves - opening_period = self._get_previous_opening_period( - period_from, company_id) - if opening_period and \ - self._period_has_moves(opening_period[0]): - # found opening period with moves - if opening_period.date_start == period_from.date_start and\ - mode == MODE_INITIAL: - # if the opening period has the same start date as - # period_from, then we'll find the initial balance - # in the initial period and that's it - period_ids.append(opening_period[0].id) - continue - period_from = opening_period[0] - else: - # no opening period with moves, - # use very first normal period - period_from = self._get_first_normal_period(company_id) - if period_to: - period_ids.extend(self._get_period_ids_between( - period_from, period_to, company_id)) - return period_ids - def get_aml_domain_for_dates(self, date_from, date_to, - period_from, period_to, mode, target_move): - if period_from and period_to: - period_ids = self._get_period_ids_for_mode( - period_from, period_to, mode) - domain = [('period_id', 'in', period_ids)] - else: - if mode == MODE_VARIATION: - domain = [('date', '>=', date_from), ('date', '<=', date_to)] - else: - raise Warning(_("Modes i and e are only applicable for " - "fiscal periods")) + if mode == MODE_VARIATION: + domain = [('date', '>=', date_from), ('date', '<=', date_to)] + elif mode == MODE_INITIAL: + domain = [('date', '<', date_from)] + elif mode == MODE_END: + domain = [('date', '<=', date_to)] if target_move == 'posted': domain.append(('move_id.state', '=', 'posted')) return expression.normalize_domain(domain) - def do_queries(self, date_from, date_to, period_from, period_to, + def do_queries(self, date_from, date_to, target_move, additional_move_line_filter=None): """Query sums of debit and credit for all accounts and domains used in expressions. @@ -343,7 +244,6 @@ class AccountingExpressionProcessor(object): if mode not in domain_by_mode: domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - period_from, period_to, mode, target_move) domain = list(domain) + domain_by_mode[mode] domain.append(('account_id', 'in', self._map_account_ids[key])) From 68c48dd8bd686d0de3cd6747817aaaaf6be75386 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 22 Oct 2015 14:36:41 +0200 Subject: [PATCH 002/182] add v9 thoughts in the readme --- mis_builder/README.rst | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/mis_builder/README.rst b/mis_builder/README.rst index a9cbf083..25f9ce7b 100644 --- a/mis_builder/README.rst +++ b/mis_builder/README.rst @@ -63,12 +63,22 @@ analytic accounts. Known issues / Roadmap ====================== +* V9 thoughts: + * select accounts by tag (see also select accounts by type below) + * how to handle multi-company consolidation now that consolidation children are gone? + * what replaces root accounts / account charts in v9? nothing it seems, so + we are limited to one chart of accounts per company; + * for multi-company consolidation, must we replace the consolidation chart + of account by a list of companies? + * Add 'Fiscal Year' period type. * Allow selecting accounts by type. This is currently possible by expressing a query such as balp[][('account_id.user_type.code', '=', ...)]. This will work but would be more efficient if one could write balp[user_type=...], as it would involve much less queries to the database. + Possible syntax could be balp[code:60%,70%], balp[type:...], balp[tag:...], + with code: being optional and the default. * More tests should be added. The first part is creating test data, then it will be easier. At the minimum, We need the following test data: From f0fe48b322356f4c5a434f07c0d81f2503638fe5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 22 Oct 2015 14:37:14 +0200 Subject: [PATCH 003/182] no more parent_id, no more consolidation children Code is simpler too but here we really lose functionality. --- mis_builder/models/aep.py | 60 +++++++++++++-------------------------- 1 file changed, 20 insertions(+), 40 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index ca1f47f8..07a930e1 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -86,55 +86,33 @@ class AccountingExpressionProcessor(object): # before done_parsing: {(domain, mode): set(account_codes)} # after done_parsing: {(domain, mode): list(account_ids)} self._map_account_ids = defaultdict(set) + # {account_code: account_id} where account_code can be + # - None for all accounts + # - NNN% for a like + # - NNN for a code with an exact match self._account_ids_by_code = defaultdict(set) - def _load_account_codes(self, account_codes, root_account): + def _load_account_codes(self, account_codes): account_model = self.env['account.account'] - # TODO: account_obj is necessary because _get_children_and_consol - # does not work in new API? - account_obj = self.env.registry('account.account') exact_codes = set() - like_codes = set() for account_code in account_codes: if account_code in self._account_ids_by_code: continue if account_code is None: - # by convention the root account is keyed as - # None in _account_ids_by_code, so it is consistent - # with what _parse_match_object returns for an - # empty list of account codes, ie [None] - exact_codes.add(root_account.code) + # None means we want all accounts + account_ids = account_model.\ + search([]).mapped('id') + self._account_ids_by_code[account_code].update(account_ids) elif '%' in account_code: - like_codes.add(account_code) + account_ids = account_model.\ + search([('code', 'like', account_code)]).mapped('id') + self._account_ids_by_code[account_code].update(account_ids) else: + # search exact codes after the loop to do less queries exact_codes.add(account_code) for account in account_model.\ - search([('code', 'in', list(exact_codes)), - ('parent_id', 'child_of', root_account.id)]): - if account.code == root_account.code: - code = None - else: - code = account.code - if account.type in ('view', 'consolidation'): - self._account_ids_by_code[code].update( - account_obj._get_children_and_consol( - self.env.cr, self.env.uid, - [account.id], - self.env.context)) - else: - self._account_ids_by_code[code].add(account.id) - for like_code in like_codes: - for account in account_model.\ - search([('code', 'like', like_code), - ('parent_id', 'child_of', root_account.id)]): - if account.type in ('view', 'consolidation'): - self._account_ids_by_code[like_code].update( - account_obj._get_children_and_consol( - self.env.cr, self.env.uid, - [account.id], - self.env.context)) - else: - self._account_ids_by_code[like_code].add(account.id) + search([('code', 'in', list(exact_codes))]): + self._account_ids_by_code[account.code].add(account.id) def _parse_match_object(self, mo): """Split a match object corresponding to an accounting variable @@ -153,7 +131,7 @@ class AccountingExpressionProcessor(object): if account_codes.strip(): account_codes = [a.strip() for a in account_codes.split(',')] else: - account_codes = [None] + account_codes = [None] # None means we want all accounts domain = domain or '[]' domain = tuple(safe_eval(domain)) return field, mode, account_codes, domain @@ -170,11 +148,13 @@ class AccountingExpressionProcessor(object): key = (domain, mode) self._map_account_ids[key].update(account_codes) - def done_parsing(self, root_account): + def done_parsing(self): """Load account codes and replace account codes by account ids in map.""" for key, account_codes in self._map_account_ids.items(): - self._load_account_codes(account_codes, root_account) + # TODO _load_account_codes could be done + # for all account_codes at once (also in v8) + self._load_account_codes(account_codes) account_ids = set() for account_code in account_codes: account_ids.update(self._account_ids_by_code[account_code]) From cebd88244aaee9490b73854d595b62ab629c683b Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Thu, 17 Dec 2015 09:29:55 +0100 Subject: [PATCH 004/182] no more fiscal period, no more root account --- mis_builder/models/mis_builder.py | 60 ++++--------------------------- mis_builder/views/mis_builder.xml | 3 -- 2 files changed, 6 insertions(+), 57 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 423fb94d..4fe123d8 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -304,8 +304,6 @@ class MisReportInstancePeriod(models.Model): def _compute_dates(self): self.date_from = False self.date_to = False - self.period_from = False - self.period_to = False self.valid = False d = fields.Date.from_string(self.report_instance_id.pivot_date) if self.type == 'd': @@ -323,28 +321,6 @@ class MisReportInstancePeriod(models.Model): self.date_from = fields.Date.to_string(date_from) self.date_to = fields.Date.to_string(date_to) self.valid = True - elif self.type == 'fp': - current_periods = self.env['account.period'].search( - [('special', '=', False), - ('date_start', '<=', d), - ('date_stop', '>=', d), - ('company_id', '=', - self.report_instance_id.company_id.id)]) - if current_periods: - all_periods = self.env['account.period'].search( - [('special', '=', False), - ('company_id', '=', - self.report_instance_id.company_id.id)], - order='date_start') - all_period_ids = [p.id for p in all_periods] - p = all_period_ids.index(current_periods[0].id) + self.offset - if p >= 0 and p + self.duration <= len(all_period_ids): - periods = all_periods[p:p + self.duration] - self.date_from = periods[0].date_start - self.date_to = periods[-1].date_stop - self.period_from = periods[0] - self.period_to = periods[-1] - self.valid = True _name = 'mis.report.instance.period' @@ -352,7 +328,6 @@ class MisReportInstancePeriod(models.Model): string='Description', translate=True) type = fields.Selection([('d', _('Day')), ('w', _('Week')), - ('fp', _('Fiscal Period')), # ('fy', _('Fiscal Year')) ], required=True, @@ -365,12 +340,6 @@ class MisReportInstancePeriod(models.Model): default=1) date_from = fields.Date(compute='_compute_dates', string="From") date_to = fields.Date(compute='_compute_dates', string="To") - period_from = fields.Many2one(compute='_compute_dates', - comodel_name='account.period', - string="From period") - period_to = fields.Many2one(compute='_compute_dates', - comodel_name='account.period', - string="To period") valid = fields.Boolean(compute='_compute_dates', type='boolean', string='Valid') @@ -434,11 +403,10 @@ class MisReportInstancePeriod(models.Model): if AEP.has_account_var(expr): aep = AEP(self.env) aep.parse_expr(expr) - aep.done_parsing(self.report_instance_id.root_account) + aep.done_parsing() domain = aep.get_aml_domain_for_expr( expr, self.date_from, self.date_to, - self.period_from, self.period_to, self.report_instance_id.target_move) domain.extend(self._get_additional_move_line_filter()) return { @@ -523,7 +491,6 @@ class MisReportInstancePeriod(models.Model): localdict.update(self._fetch_queries()) aep.do_queries(self.date_from, self.date_to, - self.period_from, self.period_to, self.report_instance_id.target_move, self._get_additional_move_line_filter()) @@ -628,14 +595,7 @@ class MisReportInstance(models.Model): required=True, default='posted') company_id = fields.Many2one(comodel_name='res.company', - string='Company', - readonly=True, - related='root_account.company_id', - store=True) - root_account = fields.Many2one(comodel_name='account.account', - domain='[("parent_id", "=", False)]', - string="Account chart", - required=True) + string='Company') landscape_pdf = fields.Boolean(string='Landscape PDF') def _format_date(self, lang_id, date): @@ -667,7 +627,7 @@ class MisReportInstance(models.Model): aep = AEP(self.env) for kpi in self.report_id.kpi_ids: aep.parse_expr(kpi.expression) - aep.done_parsing(self.root_account) + aep.done_parsing() # fetch user language only once # TODO: is this necessary? @@ -707,19 +667,11 @@ class MisReportInstance(models.Model): # add the column header if period.duration > 1 or period.type == 'w': # from, to - if period.period_from and period.period_to: - date_from = period.period_from.name - date_to = period.period_to.name - else: - date_from = self._format_date(lang_id, period.date_from) - date_to = self._format_date(lang_id, period.date_to) + date_from = self._format_date(lang_id, period.date_from) + date_to = self._format_date(lang_id, period.date_to) header_date = _('from %s to %s') % (date_from, date_to) else: - # one period or one day - if period.period_from and period.period_to: - header_date = period.period_from.name - else: - header_date = self._format_date(lang_id, period.date_from) + header_date = self._format_date(lang_id, period.date_from) header[0]['cols'].append(dict(name=period.name, date=header_date)) # add kpi values kpi_values = kpi_values_by_period_ids[period.id] diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 31551f10..342a3fbf 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -169,7 +169,6 @@ - @@ -183,8 +182,6 @@ - - From 48415857a5dbabaab08f7d9f476ff68349cac6db Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Thu, 17 Dec 2015 09:31:27 +0100 Subject: [PATCH 005/182] Not really the same js in 9.0 ... --- mis_builder/static/src/js/mis_builder.js | 95 +++++++++++++----------- 1 file changed, 53 insertions(+), 42 deletions(-) diff --git a/mis_builder/static/src/js/mis_builder.js b/mis_builder/static/src/js/mis_builder.js index df369e06..3f314781 100644 --- a/mis_builder/static/src/js/mis_builder.js +++ b/mis_builder/static/src/js/mis_builder.js @@ -1,48 +1,59 @@ -openerp.mis_builder = function(instance) { +odoo.define('mis.builder', function (require) { +"use strict"; - instance.mis_builder.MisReport = instance.web.form.FormWidget.extend({ - template: "mis_builder.MisReport", +var core = require('web.core'); +var form_common = require('web.form_common'); +var Model = require('web.DataModel'); +var data = require('web.data'); - init: function() { - this._super.apply(this, arguments); - this.mis_report_data = null; - }, +var MisReport = form_common.FormWidget.extend({ + /** + * @constructs instance.mis_builder.MisReport + * @extends instance.web.form.FormWidget + * + */ + template: "mis_builder.MisReport", + init: function() { + this._super.apply(this, arguments); + this.mis_report_data = null; + }, - start: function() { - this._super.apply(this, arguments); - var self = this; - new instance.web.Model("mis.report.instance").call( - "compute", - [self.getParent().dataset.context.active_id], - {'context': new instance.web.CompoundContext()} - ).then(function(result){ - self.mis_report_data = result; - self.renderElement(); - }); - }, + start: function() { + this._super.apply(this, arguments); + var self = this; + new Model("mis.report.instance").call( + "compute", + [self.getParent().dataset.context.active_id], + {'context': new data.CompoundContext()} + ).then(function(result){ + self.mis_report_data = result; + self.renderElement(); + }); + }, + + events: { + "click a.mis_builder_drilldown": "drilldown", + }, - events: { - "click a.mis_builder_drilldown": "drilldown", - }, + drilldown: function(event) { + var self = this; + var drilldown = JSON.parse($(event.target).data("drilldown")); + if (drilldown) { + var period_id = JSON.parse($(event.target).data("period-id")); + var val_c = JSON.parse($(event.target).data("expr")); + new Model("mis.report.instance.period").call( + "drilldown", + [period_id, val_c], + {'context': new data.CompoundContext()} + ).then(function(result) { + if (result) { + self.do_action(result); + } + }); + } + }, +}); - drilldown: function(event) { - var self = this; - var drilldown = JSON.parse($(event.target).data("drilldown")); - if (drilldown) { - var period_id = JSON.parse($(event.target).data("period-id")); - var val_c = JSON.parse($(event.target).data("expr")); - new instance.web.Model("mis.report.instance.period").call( - "drilldown", - [period_id, val_c], - {'context': new instance.web.CompoundContext()} - ).then(function(result) { - if (result) { - self.do_action(result); - } - }); - } - }, - }); +core.form_custom_registry.add('mis_report', MisReport); - instance.web.form.custom_widgets.add('mis_report', 'instance.mis_builder.MisReport'); -} +}); From 0d32c70de50bdd59805c1f8905c16a1d9a9e35b1 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Thu, 17 Dec 2015 09:33:14 +0100 Subject: [PATCH 006/182] [mis_builder] First installable version in 9.0 ! Enjoy ! --- mis_builder/__openerp__.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index d84f07b6..250f88a9 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -24,7 +24,7 @@ { 'name': 'MIS Builder', - 'version': '8.0.0.2.0', + 'version': '9.0.0.2.0', 'category': 'Reporting', 'summary': """ Build 'Management Information System' Reports and Dashboards @@ -55,7 +55,7 @@ 'qweb': [ 'static/src/xml/*.xml' ], - 'installable': False, + 'installable': True, 'application': True, 'auto_install': False, 'license': 'AGPL-3', From 816dfc16144a35fe13eb8493cc2d8f893c453a46 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 18 Dec 2015 10:19:32 +0100 Subject: [PATCH 007/182] Bump version number --- mis_builder/__openerp__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 250f88a9..38f22c5a 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -24,7 +24,7 @@ { 'name': 'MIS Builder', - 'version': '9.0.0.2.0', + 'version': '9.0.1.0.0', 'category': 'Reporting', 'summary': """ Build 'Management Information System' Reports and Dashboards From 43edb9a8189f58f4a28e7fcf960a6d720c5853d7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 18 Dec 2015 10:31:38 +0100 Subject: [PATCH 008/182] [FIX] remove unused import --- mis_builder/models/aep.py | 1 - 1 file changed, 1 deletion(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 07a930e1..9425258b 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -27,7 +27,6 @@ from collections import defaultdict from openerp.osv import expression from openerp.tools.safe_eval import safe_eval -from openerp.tools.translate import _ MODE_VARIATION = 'p' MODE_INITIAL = 'i' From 02e6f8988c853cc76836b5b22166ec67cf7ad774 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Thu, 24 Dec 2015 15:16:44 +0100 Subject: [PATCH 009/182] [CHG][mis_builder] Use base_report_xlsx instead of report_xls --- mis_builder/__openerp__.py | 2 +- mis_builder/report/__init__.py | 2 +- mis_builder/report/mis_builder_xls.py | 138 ------------------------- mis_builder/report/mis_builder_xlsx.py | 102 ++++++++++++++++++ mis_builder/views/mis_builder.xml | 4 +- 5 files changed, 106 insertions(+), 142 deletions(-) delete mode 100644 mis_builder/report/mis_builder_xls.py create mode 100644 mis_builder/report/mis_builder_xlsx.py diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 38f22c5a..c82743a7 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -34,7 +34,7 @@ 'website': 'http://acsone.eu', 'depends': [ 'account', - 'report_xls', # OCA/reporting-engine + 'base_report_xlsx', # OCA/reporting-engine ], 'data': [ 'wizard/mis_builder_dashboard.xml', diff --git a/mis_builder/report/__init__.py b/mis_builder/report/__init__.py index 671c3f7b..4236b123 100644 --- a/mis_builder/report/__init__.py +++ b/mis_builder/report/__init__.py @@ -23,7 +23,7 @@ ############################################################################## try: - from . import mis_builder_xls + from . import mis_builder_xlsx except ImportError: pass # this module is not installed diff --git a/mis_builder/report/mis_builder_xls.py b/mis_builder/report/mis_builder_xls.py deleted file mode 100644 index 88dcd1cf..00000000 --- a/mis_builder/report/mis_builder_xls.py +++ /dev/null @@ -1,138 +0,0 @@ -# -*- encoding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## - -import xlwt -from openerp.report import report_sxw -from openerp.addons.report_xls.report_xls import report_xls -import logging -_logger = logging.getLogger(__name__) - - -class mis_builder_xls_parser(report_sxw.rml_parse): - - def __init__(self, cr, uid, name, context): - super(mis_builder_xls_parser, self).__init__( - cr, uid, name, context=context) - self.context = context - - -class mis_builder_xls(report_xls): - - def __init__(self, name, table, rml=False, parser=False, header=True, - store=False): - super(mis_builder_xls, self).__init__( - name, table, rml, parser, header, store) - - # Cell Styles - _xs = self.xls_styles - # header - rh_cell_format = _xs['bold'] + _xs['fill'] + \ - _xs['borders_all'] + _xs['right'] - self.rh_cell_style = xlwt.easyxf(rh_cell_format) - self.rh_cell_style_date = xlwt.easyxf( - rh_cell_format, num_format_str=report_xls.date_format) - # lines - self.mis_rh_cell_style = xlwt.easyxf( - _xs['borders_all'] + _xs['bold'] + _xs['fill']) - - def generate_xls_report(self, _p, _xs, data, objects, wb): - - report_name = objects[0].name - ws = wb.add_sheet(report_name[:31]) - ws.panes_frozen = True - ws.remove_splits = True - ws.portrait = 0 # Landscape - ws.fit_width_to_pages = 1 - row_pos = 0 - - # set print header/footer - ws.header_str = self.xls_headers['standard'] - ws.footer_str = self.xls_footers['standard'] - - # Title - c_specs = [ - ('report_name', 1, 0, 'text', report_name), - ] - row_data = self.xls_row_template(c_specs, ['report_name']) - row_pos = self.xls_write_row( - ws, row_pos, row_data, row_style=xlwt.easyxf(_xs['xls_title'])) - row_pos += 1 - - # get the computed result of the report - data = self.pool.get('mis.report.instance').compute( - self.cr, self.uid, objects[0].id) - - # Column headers - header_name_list = [''] - col_specs_template = {'': {'header': [1, 30, 'text', ''], - 'header_date': [1, 1, 'text', '']}} - for col in data['header'][0]['cols']: - col_specs_template[col['name']] = {'header': [1, 30, 'text', - col['name']], - 'header_date': [1, 1, 'text', - col['date']]} - header_name_list.append(col['name']) - c_specs = map( - lambda x: self.render(x, col_specs_template, 'header'), - header_name_list) - row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs]) - row_pos = self.xls_write_row( - ws, row_pos, row_data, row_style=self.rh_cell_style, - set_column_size=True) - c_specs = map(lambda x: self.render( - x, col_specs_template, 'header_date'), header_name_list) - row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs]) - row_pos = self.xls_write_row( - ws, row_pos, row_data, row_style=self.rh_cell_style_date) - - ws.set_horz_split_pos(row_pos) - ws.set_vert_split_pos(1) - - for line in data['content']: - col = 0 - ws.write(row_pos, col, line['kpi_name'], self.mis_rh_cell_style) - for value in line['cols']: - col += 1 - num_format_str = '#' - if value.get('dp'): - num_format_str += '.' - num_format_str += '0' * int(value['dp']) - if value.get('suffix'): - num_format_str = num_format_str + ' "%s"' % value['suffix'] - kpi_cell_style = xlwt.easyxf( - _xs['borders_all'] + _xs['right'], - num_format_str=num_format_str) - if value.get('val'): - val = value['val'] - if value.get('is_percentage'): - val = val / 0.01 - ws.write(row_pos, col, val, kpi_cell_style) - else: - ws.write(row_pos, col, value['val_r'], kpi_cell_style) - row_pos += 1 - - -mis_builder_xls('report.mis.report.instance.xls', - 'mis.report.instance', - parser=mis_builder_xls_parser) diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py new file mode 100644 index 00000000..d44e2a0a --- /dev/null +++ b/mis_builder/report/mis_builder_xlsx.py @@ -0,0 +1,102 @@ +# -*- encoding: utf-8 -*- +############################################################################## +# +# mis_builder module for Odoo, Management Information System Builder +# Copyright (C) 2014-2015 ACSONE SA/NV () +# +# This file is a part of mis_builder +# +# mis_builder is free software: you can redistribute it and/or modify +# it under the terms of the GNU Affero General Public License v3 or later +# as published by the Free Software Foundation, either version 3 of the +# License, or (at your option) any later version. +# +# mis_builder is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU Affero General Public License v3 or later for more details. +# +# You should have received a copy of the GNU Affero General Public License +# v3 or later along with this program. +# If not, see . +# +############################################################################## + +from openerp.report import report_sxw +from openerp.addons.base_report_xlsx.report.report_xlsx import ReportXlsx +import logging +_logger = logging.getLogger(__name__) + + +class mis_builder_xlsx_parser(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(mis_builder_xlsx_parser, self).__init__( + cr, uid, name, context=context) + self.context = context + + +class mis_builder_xlsx(ReportXlsx): + + def __init__(self, name, table, rml=False, parser=False, header=True, + store=False): + super(mis_builder_xlsx, self).__init__( + name, table, rml, parser, header, store) + + def generate_xlsx_report(self, workbook, data, objects): + + report_name = objects[0].name + sheet = workbook.add_worksheet(report_name[:31]) + row_pos = 0 + col_pos = 0 + + sheet.set_column(col_pos, col_pos, 30) + bold = workbook.add_format({'bold': True}) + header_format = workbook.add_format({'bold': True, + 'align': 'center', + 'border': True, + 'bg_color': '#FFFFCC'}) + kpi_name_format = workbook.add_format({'bold': True, + 'border': True, + 'bg_color': '#FFFFCC'}) + sheet.write(row_pos, 0, report_name, bold) + row_pos += 1 + col_pos += 1 + + # get the computed result of the report + data = objects.compute() + + # Column headers + for col in data['header'][0]['cols']: + sheet.set_column(col_pos, col_pos, 30) + sheet.write(row_pos, col_pos, col['name'], header_format) + sheet.write(row_pos+1, col_pos, col['date'], header_format) + col_pos += 1 + row_pos += 2 + for line in data['content']: + col = 0 + sheet.write(row_pos, col, line['kpi_name'], kpi_name_format) + for value in line['cols']: + col += 1 + num_format_str = '#' + if value.get('dp'): + num_format_str += '.' + num_format_str += '0' * int(value['dp']) + if value.get('suffix'): + num_format_str = num_format_str + ' "%s"' % value['suffix'] + kpi_format = workbook.add_format({'num_format': num_format_str, + 'border': 1, + 'align': 'right'}) + if value.get('val'): + val = value['val'] + if value.get('is_percentage'): + val = val / 0.01 + sheet.write(row_pos, col, val, kpi_format) + else: + sheet.write(row_pos, col, value['val_r'], kpi_format) + row_pos += 1 + + +mis_builder_xlsx('report.mis.report.instance.xlsx', + 'mis.report.instance', + parser=mis_builder_xlsx_parser) diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 342a3fbf..46da7ad4 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -102,8 +102,8 @@ MIS report instance XLS report mis.report.instance ir.actions.report.xml - mis.report.instance.xls - xls + mis.report.instance.xlsx + xlsx From 3cfc220486f236f67c0d1109da4cba8c8d57991b Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Tue, 12 Jan 2016 09:37:17 +0100 Subject: [PATCH 010/182] [FIX][mis_builder] Change parent menu for MIS report template to be compatible with Odoo enterprise edition. --- mis_builder/views/mis_builder.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 46da7ad4..6d4f5a16 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -96,7 +96,7 @@ tree,form - + MIS report instance XLS report From 58ec06520b3502f23ce74b688e06ae4a3f5d3189 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Tue, 12 Jan 2016 10:55:10 +0100 Subject: [PATCH 011/182] [IMP][mis_builder] Remove unnecessary code --- mis_builder/report/mis_builder_xlsx.py | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index d44e2a0a..ed70d78e 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -22,20 +22,11 @@ # ############################################################################## -from openerp.report import report_sxw from openerp.addons.base_report_xlsx.report.report_xlsx import ReportXlsx import logging _logger = logging.getLogger(__name__) -class mis_builder_xlsx_parser(report_sxw.rml_parse): - - def __init__(self, cr, uid, name, context): - super(mis_builder_xlsx_parser, self).__init__( - cr, uid, name, context=context) - self.context = context - - class mis_builder_xlsx(ReportXlsx): def __init__(self, name, table, rml=False, parser=False, header=True, @@ -98,5 +89,4 @@ class mis_builder_xlsx(ReportXlsx): mis_builder_xlsx('report.mis.report.instance.xlsx', - 'mis.report.instance', - parser=mis_builder_xlsx_parser) + 'mis.report.instance') From 38075ac441044a0835ddece5451a3027ade04299 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Tue, 12 Jan 2016 10:55:33 +0100 Subject: [PATCH 012/182] [FIX][mis_builder] Fix test suite. --- mis_builder/tests/mis.report.instance.csv | 4 ++-- mis_builder/tests/mis.report.query.csv | 2 +- mis_builder/tests/test_mis_builder.py | 3 +-- 3 files changed, 4 insertions(+), 5 deletions(-) diff --git a/mis_builder/tests/mis.report.instance.csv b/mis_builder/tests/mis.report.instance.csv index f53ed012..96f035f4 100644 --- a/mis_builder/tests/mis.report.instance.csv +++ b/mis_builder/tests/mis.report.instance.csv @@ -1,2 +1,2 @@ -"id","date","description","name","period_ids/id","report_id/id","root_account/id" -"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test","account.chart0" \ No newline at end of file +"id","date","description","name","period_ids/id","report_id/id" +"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test" \ No newline at end of file diff --git a/mis_builder/tests/mis.report.query.csv b/mis_builder/tests/mis.report.query.csv index 060a82f0..ccbdf789 100644 --- a/mis_builder/tests/mis.report.query.csv +++ b/mis_builder/tests/mis.report.query.csv @@ -1,2 +1,2 @@ "id","date_field/id","domain","field_ids/id","model_id/id","name" -"mis_report_query_test","account.field_account_analytic_balance_date1","","account.field_account_analytic_balance_empty_acc","account.model_account_analytic_balance","test" \ No newline at end of file +"mis_report_query_test","analytic.field_account_analytic_line_date","","analytic.field_account_analytic_line_amount","analytic.model_account_analytic_line","test" \ No newline at end of file diff --git a/mis_builder/tests/test_mis_builder.py b/mis_builder/tests/test_mis_builder.py index 4533f003..c7b6dab7 100644 --- a/mis_builder/tests/test_mis_builder.py +++ b/mis_builder/tests/test_mis_builder.py @@ -52,8 +52,7 @@ class test_mis_builder(common.TransactionCase): 'The converted date time convert must contains hour') def test_fetch_query(self): - # create a report on a model without company_id field : - # account.analytic.balance + # create a report on account.analytic.line data = self.registry('mis.report.instance').compute( self.cr, self.uid, self.ref('mis_builder.mis_report_instance_test')) From 1b11a87655f0c4e1443ea8987fde591bdf3f63da Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 18 Jan 2016 18:28:44 +0100 Subject: [PATCH 013/182] pep8 & co --- mis_builder/__init__.py | 2 +- mis_builder/__openerp__.py | 2 +- mis_builder/migrations/8.0.0.2/pre-migration.py | 2 +- mis_builder/models/__init__.py | 2 +- mis_builder/models/aep.py | 2 +- mis_builder/models/aggregate.py | 2 +- mis_builder/models/mis_builder.py | 10 ++++++---- mis_builder/report/__init__.py | 2 +- mis_builder/report/mis_builder_xlsx.py | 10 +++++----- mis_builder/report/report_mis_report_instance.py | 2 +- mis_builder/tests/__init__.py | 2 +- mis_builder/tests/test_mis_builder.py | 6 +++--- mis_builder/wizard/__init__.py | 2 +- mis_builder/wizard/mis_builder_dashboard.py | 6 +++--- 14 files changed, 27 insertions(+), 25 deletions(-) diff --git a/mis_builder/__init__.py b/mis_builder/__init__.py index 386e1d1d..426d492a 100644 --- a/mis_builder/__init__.py +++ b/mis_builder/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index c82743a7..e7c50eae 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/migrations/8.0.0.2/pre-migration.py b/mis_builder/migrations/8.0.0.2/pre-migration.py index 6530bda0..8b08c619 100644 --- a/mis_builder/migrations/8.0.0.2/pre-migration.py +++ b/mis_builder/migrations/8.0.0.2/pre-migration.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/models/__init__.py b/mis_builder/models/__init__.py index e99af9da..220a97a2 100644 --- a/mis_builder/models/__init__.py +++ b/mis_builder/models/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 9425258b..6d3a47a7 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/models/aggregate.py b/mis_builder/models/aggregate.py index 4788c4c2..f098a4c5 100644 --- a/mis_builder/models/aggregate.py +++ b/mis_builder/models/aggregate.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 4fe123d8..e466221b 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder @@ -31,7 +31,7 @@ import traceback import pytz -from openerp import api, fields, models, _, exceptions +from openerp import api, exceptions, fields, models, _ from openerp.tools.safe_eval import safe_eval from .aep import AccountingExpressionProcessor as AEP @@ -124,7 +124,8 @@ class MisReportKpi(models.Model): @api.constrains('name') def _check_name(self): if not _is_valid_python_var(self.name): - raise exception.Warning(_('The name must be a valid python identifier')) + raise exceptions.Warning(_('The name must be a valid ' + 'python identifier')) @api.onchange('name') def _onchange_name(self): @@ -260,7 +261,8 @@ class MisReportQuery(models.Model): @api.constrains('name') def _check_name(self): if not _is_valid_python_var(self.name): - raise exception.Warning(_('The name must be a valid python identifier')) + raise exceptions.Warning(_('The name must be a valid ' + 'python identifier')) class MisReport(models.Model): diff --git a/mis_builder/report/__init__.py b/mis_builder/report/__init__.py index 4236b123..250cef12 100644 --- a/mis_builder/report/__init__.py +++ b/mis_builder/report/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index ed70d78e..a1f50a11 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder @@ -27,11 +27,11 @@ import logging _logger = logging.getLogger(__name__) -class mis_builder_xlsx(ReportXlsx): +class MisBuilderXslx(ReportXlsx): def __init__(self, name, table, rml=False, parser=False, header=True, store=False): - super(mis_builder_xlsx, self).__init__( + super(MisBuilderXslx, self).__init__( name, table, rml, parser, header, store) def generate_xlsx_report(self, workbook, data, objects): @@ -88,5 +88,5 @@ class mis_builder_xlsx(ReportXlsx): row_pos += 1 -mis_builder_xlsx('report.mis.report.instance.xlsx', - 'mis.report.instance') +MisBuilderXslx('report.mis.report.instance.xlsx', + 'mis.report.instance') diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/report_mis_report_instance.py index 9305081c..1a85e0ea 100644 --- a/mis_builder/report/report_mis_report_instance.py +++ b/mis_builder/report/report_mis_report_instance.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/tests/__init__.py b/mis_builder/tests/__init__.py index 42df0b2e..46244c14 100644 --- a/mis_builder/tests/__init__.py +++ b/mis_builder/tests/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/tests/test_mis_builder.py b/mis_builder/tests/test_mis_builder.py index c7b6dab7..85d7af9f 100644 --- a/mis_builder/tests/test_mis_builder.py +++ b/mis_builder/tests/test_mis_builder.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder @@ -27,10 +27,10 @@ import openerp.tests.common as common from ..models import mis_builder -class test_mis_builder(common.TransactionCase): +class TestMisBuilder(common.TransactionCase): def setUp(self): - super(test_mis_builder, self).setUp() + super(TestMisBuilder, self).setUp() def test_datetime_conversion(self): date_to_convert = '2014-07-05' diff --git a/mis_builder/wizard/__init__.py b/mis_builder/wizard/__init__.py index 78ba3c22..17e53ebe 100644 --- a/mis_builder/wizard/__init__.py +++ b/mis_builder/wizard/__init__.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder diff --git a/mis_builder/wizard/mis_builder_dashboard.py b/mis_builder/wizard/mis_builder_dashboard.py index 19bf36fa..b2550b8d 100644 --- a/mis_builder/wizard/mis_builder_dashboard.py +++ b/mis_builder/wizard/mis_builder_dashboard.py @@ -1,4 +1,4 @@ -# -*- encoding: utf-8 -*- +# -*- coding: utf-8 -*- ############################################################################## # # mis_builder module for Odoo, Management Information System Builder @@ -26,7 +26,7 @@ from openerp.osv import orm, fields from lxml import etree -class add_mis_report_instance_dashboard(orm.TransientModel): +class AddMisReportInstanceDashboard(orm.TransientModel): _name = "add.mis.report.instance.dashboard.wizard" _columns = {'name': fields.char('Name', size=32, required=True), @@ -40,7 +40,7 @@ class add_mis_report_instance_dashboard(orm.TransientModel): if context is None: context = {} if context.get('active_id'): - res = super(add_mis_report_instance_dashboard, self).default_get( + res = super(AddMisReportInstanceDashboard, self).default_get( cr, uid, fields, context=context) # get report instance name res['name'] = self.pool['mis.report.instance'].read( From b4fc7b1bc7d12d521f270f4a4afca4241277f749 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Mon, 4 Apr 2016 20:32:12 +0200 Subject: [PATCH 014/182] [CHG][mis_builder] According with the rename of base_report_xlsx to report_xlsx. --- mis_builder/__openerp__.py | 2 +- mis_builder/report/mis_builder_xlsx.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index e7c50eae..f38e1582 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -34,7 +34,7 @@ 'website': 'http://acsone.eu', 'depends': [ 'account', - 'base_report_xlsx', # OCA/reporting-engine + 'report_xlsx', # OCA/reporting-engine ], 'data': [ 'wizard/mis_builder_dashboard.xml', diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index a1f50a11..69469142 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -22,7 +22,7 @@ # ############################################################################## -from openerp.addons.base_report_xlsx.report.report_xlsx import ReportXlsx +from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx import logging _logger = logging.getLogger(__name__) From 293ba89368f5414bea9535e310d03c56458cf61d Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Fri, 5 Feb 2016 18:42:55 +0100 Subject: [PATCH 015/182] [FIX] Flake8 issues --- .../report/aged_partner_balance.py | 12 ++++--- .../report/common_partner_balance_reports.py | 5 ++- .../report/common_reports.py | 7 ++-- .../report/open_invoices.py | 4 +-- .../report/partners_ledger.py | 4 +-- .../report/webkit_parser_header_fix.py | 5 +-- .../report/general_ledger_xls.py | 3 +- .../report/open_invoices_xls.py | 34 ++++++++++--------- .../report/partners_balance_xls.py | 8 +++-- .../report/trial_balance_xls.py | 5 +-- .../report/nov_account_journal.py | 18 +++++----- mis_builder/static/src/xml/mis_widget.xml | 2 +- 12 files changed, 59 insertions(+), 48 deletions(-) diff --git a/account_financial_report_webkit/report/aged_partner_balance.py b/account_financial_report_webkit/report/aged_partner_balance.py index fd9cb550..9275af2f 100644 --- a/account_financial_report_webkit/report/aged_partner_balance.py +++ b/account_financial_report_webkit/report/aged_partner_balance.py @@ -285,10 +285,14 @@ class AccountAgedTrialBalanceWebkit(PartnersOpenInvoicesWebkit): :returns: delta in days """ - sale_lines = [x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE - and line['rec_id'] == x['rec_id']] - refund_lines = [x for x in ledger_lines if x['jtype'] in REFUND_TYPE - and line['rec_id'] == x['rec_id']] + sale_lines = [ + x for x in ledger_lines if x['jtype'] in REC_PAY_TYPE and + line['rec_id'] == x['rec_id'] + ] + refund_lines = [ + x for x in ledger_lines if x['jtype'] in REFUND_TYPE and + line['rec_id'] == x['rec_id'] + ] if len(sale_lines) == 1: reference_line = sale_lines[0] elif len(refund_lines) == 1: diff --git a/account_financial_report_webkit/report/common_partner_balance_reports.py b/account_financial_report_webkit/report/common_partner_balance_reports.py index b291a8bf..87755388 100644 --- a/account_financial_report_webkit/report/common_partner_balance_reports.py +++ b/account_financial_report_webkit/report/common_partner_balance_reports.py @@ -312,7 +312,10 @@ class CommonPartnerBalanceReportHeaderWebkit(CommonBalanceReportHeaderWebkit, for partner_id, partner_values in \ values['partners_amounts'].copy().iteritems(): - base_partner_balance = partners_amounts_accounts[account.id][partner_id]['balance']\ + partners_amounts_account =\ + partners_amounts_accounts[account.id] + base_partner_balance =\ + partners_amounts_account[partner_id]['balance']\ if partners_amounts_accounts.get(account.id)\ and partners_amounts_accounts.get(account.id)\ .get(partner_id) else 0.0 diff --git a/account_financial_report_webkit/report/common_reports.py b/account_financial_report_webkit/report/common_reports.py index 11c4203d..cd7af7eb 100644 --- a/account_financial_report_webkit/report/common_reports.py +++ b/account_financial_report_webkit/report/common_reports.py @@ -130,9 +130,10 @@ class CommonReportHeaderWebkit(common_report_header): def recursive_sort_by_code(accounts, parent): sorted_accounts = [] # add all accounts with same parent - level_accounts = [account for account in accounts - if account['parent_id'] - and account['parent_id'][0] == parent['id']] + level_accounts = [ + account for account in accounts + if account['parent_id'] and + account['parent_id'][0] == parent['id']] # add consolidation children of parent, as they are logically on # the same level if parent.get('child_consol_ids'): diff --git a/account_financial_report_webkit/report/open_invoices.py b/account_financial_report_webkit/report/open_invoices.py index 3afdd099..158acf85 100644 --- a/account_financial_report_webkit/report/open_invoices.py +++ b/account_financial_report_webkit/report/open_invoices.py @@ -164,8 +164,8 @@ class PartnersOpenInvoicesWebkit(report_sxw.rml_parse, non_null_init_balances = dict([ (ib, amounts) for ib, amounts in init_balance[account.id].iteritems() - if amounts['init_balance'] - or amounts['init_balance_currency']]) + if amounts['init_balance'] or + amounts['init_balance_currency']]) init_bal_lines_pids = non_null_init_balances.keys() partners_order[account.id] = self._order_partners( diff --git a/account_financial_report_webkit/report/partners_ledger.py b/account_financial_report_webkit/report/partners_ledger.py index df66efac..dc0eeed6 100644 --- a/account_financial_report_webkit/report/partners_ledger.py +++ b/account_financial_report_webkit/report/partners_ledger.py @@ -165,8 +165,8 @@ class PartnersLedgerWebkit(report_sxw.rml_parse, non_null_init_balances = dict( [(ib, amounts) for ib, amounts in init_balance[account.id].iteritems() - if amounts['init_balance'] - or amounts['init_balance_currency']]) + if amounts['init_balance'] or + amounts['init_balance_currency']]) init_bal_lines_pids = non_null_init_balances.keys() else: init_balance[account.id] = {} diff --git a/account_financial_report_webkit/report/webkit_parser_header_fix.py b/account_financial_report_webkit/report/webkit_parser_header_fix.py index a92c90fa..9484aaf0 100644 --- a/account_financial_report_webkit/report/webkit_parser_header_fix.py +++ b/account_financial_report_webkit/report/webkit_parser_header_fix.py @@ -27,6 +27,9 @@ # Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA 02111-1307, USA. # ############################################################################## +from mako.template import Template +from mako.lookup import TemplateLookup + import os import subprocess import tempfile @@ -75,8 +78,6 @@ _logger = logging.getLogger('financial.reports.webkit') # redefine mako_template as this is overriden by jinja since saas-1 # from openerp.addons.report_webkit.webkit_report import mako_template -from mako.template import Template -from mako.lookup import TemplateLookup def mako_template(text): diff --git a/account_financial_report_webkit_xls/report/general_ledger_xls.py b/account_financial_report_webkit_xls/report/general_ledger_xls.py index 401ba3b7..52a3d759 100644 --- a/account_financial_report_webkit_xls/report/general_ledger_xls.py +++ b/account_financial_report_webkit_xls/report/general_ledger_xls.py @@ -200,8 +200,7 @@ class general_ledger_xls(report_xls): display_initial_balance = _p['init_balance'][account.id] and \ (_p['init_balance'][account.id].get( 'debit', 0.0) != 0.0 or - _p['init_balance'][account.id].get('credit', 0.0) - != 0.0) + _p['init_balance'][account.id].get('credit', 0.0) != 0.0) display_ledger_lines = _p['ledger_lines'][account.id] if _p.display_account_raw(data) == 'all' or \ diff --git a/account_financial_report_webkit_xls/report/open_invoices_xls.py b/account_financial_report_webkit_xls/report/open_invoices_xls.py index 5180c658..08d2135e 100644 --- a/account_financial_report_webkit_xls/report/open_invoices_xls.py +++ b/account_financial_report_webkit_xls/report/open_invoices_xls.py @@ -260,8 +260,9 @@ class open_invoices_xls(report_xls): partner_name): if regroupmode == "regroup": c_specs = [('acc_title', self.nbr_columns, 0, 'text', - ' - '.join([account.code, account.name, partner_name - or _('No partner')])), ] + ' - '.join([account.code, + account.name, + partner_name or _('No partner')])), ] else: c_specs = [ ('acc_title', self.nbr_columns, 0, 'text', ' - '. @@ -332,10 +333,10 @@ class open_invoices_xls(report_xls): else: c_specs += [('datedue', 1, 0, 'text', None)] c_specs += [ - ('debit', 1, 0, 'number', line.get('debit') - or 0.0, None, style_line_decimal), - ('credit', 1, 0, 'number', line.get('credit') - or 0.0, None, style_line_decimal), + ('debit', 1, 0, 'number', line.get('debit') or 0.0, None, + style_line_decimal), + ('credit', 1, 0, 'number', line.get('credit') or 0.0, None, + style_line_decimal), ] # determine the formula of the cumulated balance @@ -357,8 +358,9 @@ class open_invoices_xls(report_xls): if _p.amount_currency(data): if account.currency_id: c_specs += [ - ('curramount', 1, 0, 'number', line.get('amount_currency') - or 0.0, None, style_line_decimal), + ('curramount', 1, 0, 'number', + line.get('amount_currency') or 0.0, None, + style_line_decimal), ('currcode', 1, 0, 'text', line[ 'currency_code'], None, style_line_right), ] @@ -428,18 +430,18 @@ class open_invoices_xls(report_xls): else: c_specs += [('datedue', 1, 0, 'text', None)] c_specs += [ - ('debit', 1, 0, 'number', line.get('debit') - or 0.0, None, style_line_decimal), - ('credit', 1, 0, 'number', line.get('credit') - or 0.0, None, style_line_decimal), + ('debit', 1, 0, 'number', line.get('debit') or 0.0, None, + style_line_decimal), + ('credit', 1, 0, 'number', line.get('credit') or 0.0, None, + style_line_decimal), ('cumul', 1, 0, 'number', None, cumul_balance, style_line_decimal), ] if account.currency_id: c_specs += [ - ('curramount', 1, 0, 'number', line.get('amount_currency') - or 0.0, None, style_line_decimal), - ('currcode', 1, 0, 'text', line.get('currency_code') - or '', None, style_line_right), + ('curramount', 1, 0, 'number', + line.get('amount_currency') or 0.0, None, style_line_decimal), + ('currcode', 1, 0, 'text', + line.get('currency_code') or '', None, style_line_right), ] else: c_specs += [ diff --git a/account_financial_report_webkit_xls/report/partners_balance_xls.py b/account_financial_report_webkit_xls/report/partners_balance_xls.py index 7c16af9c..9255b567 100644 --- a/account_financial_report_webkit_xls/report/partners_balance_xls.py +++ b/account_financial_report_webkit_xls/report/partners_balance_xls.py @@ -137,9 +137,11 @@ class partners_balance_xls(report_xls): ('c', 2, 0, 'text', _('Comparison') + str(index + 1) + ' (C' + str(index + 1) + ')')] if params['comparison_filter'] == 'filter_date': - c_specs += [('f', 2, 0, 'text', _('Dates Filter') + ': ' + - _p.formatLang(params['start'], date=True) + ' - ' - + _p.formatLang(params['stop'], date=True))] + c_specs += [ + ('f', 2, 0, 'text', + _('Dates Filter') + ': ' + + _p.formatLang(params['start'], date=True) + ' - ' + + _p.formatLang(params['stop'], date=True))] elif params['comparison_filter'] == 'filter_period': c_specs += [('f', 2, 0, 'text', _('Periods Filter') + ': ' + params['start'].name + ' - ' + diff --git a/account_financial_report_webkit_xls/report/trial_balance_xls.py b/account_financial_report_webkit_xls/report/trial_balance_xls.py index b82fba80..476375a0 100644 --- a/account_financial_report_webkit_xls/report/trial_balance_xls.py +++ b/account_financial_report_webkit_xls/report/trial_balance_xls.py @@ -308,8 +308,9 @@ class trial_balance_xls(report_xls): ('diff', 1, 0, 'number', comp_account[ 'diff'], None, cell_style_decimal), ('diff_percent', 1, 0, 'number', comp_account[ - 'percent_diff'] and comp_account['percent_diff'] - or 0, None, cell_style_pct), + 'percent_diff'] and + comp_account['percent_diff'] or 0, None, + cell_style_pct), ] c_specs += [('type', 1, 0, 'text', diff --git a/account_journal_report_xls/report/nov_account_journal.py b/account_journal_report_xls/report/nov_account_journal.py index 2b0278c3..f400d14e 100644 --- a/account_journal_report_xls/report/nov_account_journal.py +++ b/account_journal_report_xls/report/nov_account_journal.py @@ -166,8 +166,8 @@ class nov_journal_print(report_sxw.rml_parse): "rc.symbol AS currency_symbol, " "coalesce(ai.internal_number,'-') AS inv_number, " "coalesce(abs.name,'-') AS st_number, " - "coalesce(av.number,'-') AS voucher_number " - + select_extra + + "coalesce(av.number,'-') AS voucher_number " + + select_extra + "FROM account_move_line l " "INNER JOIN account_move am ON l.move_id = am.id " "INNER JOIN account_account aa " @@ -192,11 +192,9 @@ class nov_journal_print(report_sxw.rml_parse): "LEFT OUTER JOIN account_analytic_account ana " "ON l.analytic_account_id = ana.id " "LEFT OUTER JOIN res_currency rc " - "ON l.currency_id = rc.id " - + join_extra + + "ON l.currency_id = rc.id " + join_extra + "WHERE l.period_id IN %s AND l.journal_id = %s " - "AND am.state IN %s " - + where_extra + + "AND am.state IN %s " + where_extra + "ORDER BY " + self.sort_selection + ", move_date, move_id, acc_code", (tuple(period_ids), journal_id, @@ -206,12 +204,12 @@ class nov_journal_print(report_sxw.rml_parse): # add reference of corresponding origin document if journal.type in ('sale', 'sale_refund', 'purchase', 'purchase_refund'): - [x.update({'docname': (_('Invoice') + ': ' + x['inv_number']) - or (_('Voucher') + ': ' + x['voucher_number']) or '-'}) + [x.update({'docname': (_('Invoice') + ': ' + x['inv_number']) or + (_('Voucher') + ': ' + x['voucher_number']) or '-'}) for x in lines] elif journal.type in ('bank', 'cash'): - [x.update({'docname': (_('Statement') + ': ' + x['st_number']) - or (_('Voucher') + ': ' + x['voucher_number']) or '-'}) + [x.update({'docname': (_('Statement') + ': ' + x['st_number']) or + (_('Voucher') + ': ' + x['voucher_number']) or '-'}) for x in lines] else: code_string = j_obj._report_xls_document_extra( diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 5cfd9994..9b922e61 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -42,7 +42,7 @@ - + From 1c094b386d130af7ace9efe777fca0043dae97b9 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Fri, 5 Feb 2016 19:34:02 +0100 Subject: [PATCH 016/182] [FIX] Bad modification --- mis_builder/static/src/xml/mis_widget.xml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 9b922e61..5cfd9994 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -42,7 +42,7 @@ - + From d526bb94c60331ffaa59b7f91bea63965622cb0d Mon Sep 17 00:00:00 2001 From: jbeficent Date: Thu, 11 Feb 2016 12:11:29 +0100 Subject: [PATCH 017/182] [IMP] add prefixes. comes from PR #145 --- mis_builder/__init__.py | 24 +--- mis_builder/__openerp__.py | 24 +--- mis_builder/models/__init__.py | 24 +--- mis_builder/models/aep.py | 27 +--- mis_builder/models/aggregate.py | 24 +--- mis_builder/models/mis_builder.py | 44 ++----- mis_builder/report/__init__.py | 24 +--- mis_builder/report/mis_builder_xls.py | 120 ++++++++++++++++++ mis_builder/report/mis_builder_xlsx.py | 2 + .../report/report_mis_report_instance.py | 24 +--- mis_builder/tests/__init__.py | 24 +--- mis_builder/tests/test_mis_builder.py | 27 +--- mis_builder/views/mis_builder.xml | 1 + mis_builder/wizard/__init__.py | 24 +--- mis_builder/wizard/mis_builder_dashboard.py | 107 +++++++--------- 15 files changed, 203 insertions(+), 317 deletions(-) create mode 100644 mis_builder/report/mis_builder_xls.py diff --git a/mis_builder/__init__.py b/mis_builder/__init__.py index 426d492a..409475d8 100644 --- a/mis_builder/__init__.py +++ b/mis_builder/__init__.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import models from . import wizard diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index f38e1582..6b1fe3bb 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). { 'name': 'MIS Builder', diff --git a/mis_builder/models/__init__.py b/mis_builder/models/__init__.py index 220a97a2..7f8b7fc8 100644 --- a/mis_builder/models/__init__.py +++ b/mis_builder/models/__init__.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import mis_builder from . import aep diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 6d3a47a7..4267c296 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -1,31 +1,12 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import re from collections import defaultdict -from openerp.osv import expression +from openerp.exceptions import Warning as UserError +from openerp.models import expression from openerp.tools.safe_eval import safe_eval MODE_VARIATION = 'p' diff --git a/mis_builder/models/aggregate.py b/mis_builder/models/aggregate.py index f098a4c5..bd2aa3c3 100644 --- a/mis_builder/models/aggregate.py +++ b/mis_builder/models/aggregate.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). def _sum(l): diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index e466221b..6be04b27 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import datetime import dateutil @@ -76,7 +56,7 @@ class MisReportKpi(models.Model): In addition to a name and description, it has an expression to compute it based on queries defined in the MIS report. It also has various informations defining how to render it - (numeric or percentage or a string, a suffix, divider) and + (numeric or percentage or a string, a prefix, a suffix, divider) and how to render comparison of two values of the KPI. KPI's have a sequence and are ordered inside the MIS report. """ @@ -106,6 +86,7 @@ class MisReportKpi(models.Model): string='Factor', default='1') dp = fields.Integer(string='Rounding', default=0) + prefix = fields.Char(size=16, string='Prefix') suffix = fields.Char(size=16, string='Suffix') compare_method = fields.Selection([('diff', _('Difference')), ('pct', _('Percentage')), @@ -165,10 +146,10 @@ class MisReportKpi(models.Model): return '#N/A' elif self.type == 'num': return self._render_num(lang_id, value, self.divider, - self.dp, self.suffix) + self.dp, self.prefix, self.suffix) elif self.type == 'pct': return self._render_num(lang_id, value, 0.01, - self.dp, '%') + self.dp, '', '%') else: return unicode(value) @@ -182,7 +163,7 @@ class MisReportKpi(models.Model): return self._render_num( lang_id, value - base_value, - 0.01, self.dp, _('pp'), sign='+') + 0.01, self.dp, '', _('pp'), sign='+') elif self.type == 'num': if average_value: value = value / float(average_value) @@ -192,17 +173,17 @@ class MisReportKpi(models.Model): return self._render_num( lang_id, value - base_value, - self.divider, self.dp, self.suffix, sign='+') + self.divider, self.dp, self.prefix, self.suffix, sign='+') elif self.compare_method == 'pct': if round(base_value, self.dp) != 0: return self._render_num( lang_id, (value - base_value) / abs(base_value), - 0.01, self.dp, '%', sign='+') + 0.01, self.dp, '', '%', sign='+') return '' def _render_num(self, lang_id, value, divider, - dp, suffix, sign='-'): + dp, prefix, suffix, sign='-'): divider_label = _get_selection_label( self._columns['divider'].selection, divider) if divider_label == '1': @@ -213,8 +194,8 @@ class MisReportKpi(models.Model): '%%%s.%df' % (sign, dp), value, grouping=True) - value = u'%s\N{NO-BREAK SPACE}%s%s' % \ - (value, divider_label, suffix or '') + value = u'%s\N{NARROW NO-BREAK SPACE}%s\N{NO-BREAK SPACE}%s%s' % \ + (prefix or '', value, divider_label, suffix or '') value = value.replace('-', u'\N{NON-BREAKING HYPHEN}') return value @@ -538,6 +519,7 @@ class MisReportInstancePeriod(models.Model): 'val_r': kpi_val_rendered, 'val_c': kpi_val_comment, 'style': kpi_style, + 'prefix': kpi.prefix, 'suffix': kpi.suffix, 'dp': kpi.dp, 'is_percentage': kpi.type == 'pct', diff --git a/mis_builder/report/__init__.py b/mis_builder/report/__init__.py index 250cef12..bdcf09d6 100644 --- a/mis_builder/report/__init__.py +++ b/mis_builder/report/__init__.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). try: from . import mis_builder_xlsx diff --git a/mis_builder/report/mis_builder_xls.py b/mis_builder/report/mis_builder_xls.py new file mode 100644 index 00000000..df3d3ee5 --- /dev/null +++ b/mis_builder/report/mis_builder_xls.py @@ -0,0 +1,120 @@ +# -*- coding: utf-8 -*- +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import xlwt +from openerp.report import report_sxw +from openerp.addons.report_xls.report_xls import report_xls +import logging +_logger = logging.getLogger(__name__) + + +class MisBuilderXlsParser(report_sxw.rml_parse): + + def __init__(self, cr, uid, name, context): + super(MisBuilderXlsParser, self).__init__( + cr, uid, name, context=context) + self.context = context + + +class MisBuilderXls(report_xls): + + def __init__(self, name, table, rml=False, parser=False, header=True, + store=False): + super(MisBuilderXls, self).__init__( + name, table, rml, parser, header, store) + + # Cell Styles + _xs = self.xls_styles + # header + rh_cell_format = _xs['bold'] + _xs['fill'] + \ + _xs['borders_all'] + _xs['right'] + self.rh_cell_style = xlwt.easyxf(rh_cell_format) + self.rh_cell_style_date = xlwt.easyxf( + rh_cell_format, num_format_str=report_xls.date_format) + # lines + self.mis_rh_cell_style = xlwt.easyxf( + _xs['borders_all'] + _xs['bold'] + _xs['fill']) + + def generate_xls_report(self, _p, _xs, data, objects, wb): + + report_name = objects[0].name + ws = wb.add_sheet(report_name[:31]) + ws.panes_frozen = True + ws.remove_splits = True + ws.portrait = 0 # Landscape + ws.fit_width_to_pages = 1 + row_pos = 0 + + # set print header/footer + ws.header_str = self.xls_headers['standard'] + ws.footer_str = self.xls_footers['standard'] + + # Title + c_specs = [ + ('report_name', 1, 0, 'text', report_name), + ] + row_data = self.xls_row_template(c_specs, ['report_name']) + row_pos = self.xls_write_row( + ws, row_pos, row_data, row_style=xlwt.easyxf(_xs['xls_title'])) + row_pos += 1 + + # get the computed result of the report + data = self.pool.get('mis.report.instance').compute( + self.cr, self.uid, objects[0].id) + + # Column headers + header_name_list = [''] + col_specs_template = {'': {'header': [1, 30, 'text', ''], + 'header_date': [1, 1, 'text', '']}} + for col in data['header'][0]['cols']: + col_specs_template[col['name']] = {'header': [1, 30, 'text', + col['name']], + 'header_date': [1, 1, 'text', + col['date']]} + header_name_list.append(col['name']) + c_specs = map( + lambda x: self.render(x, col_specs_template, 'header'), + header_name_list) + row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs]) + row_pos = self.xls_write_row( + ws, row_pos, row_data, row_style=self.rh_cell_style, + set_column_size=True) + c_specs = map(lambda x: self.render( + x, col_specs_template, 'header_date'), header_name_list) + row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs]) + row_pos = self.xls_write_row( + ws, row_pos, row_data, row_style=self.rh_cell_style_date) + + ws.set_horz_split_pos(row_pos) + ws.set_vert_split_pos(1) + + for line in data['content']: + col = 0 + ws.write(row_pos, col, line['kpi_name'], self.mis_rh_cell_style) + for value in line['cols']: + col += 1 + num_format_str = '#' + if value.get('dp'): + num_format_str += '.' + num_format_str += '0' * int(value['dp']) + if value.get('prefix'): + num_format_str = '"%s"' % value['prefix'] + num_format_str + if value.get('suffix'): + num_format_str += ' "%s"' % value['suffix'] + kpi_cell_style = xlwt.easyxf( + _xs['borders_all'] + _xs['right'], + num_format_str=num_format_str) + if value.get('val'): + val = value['val'] + if value.get('is_percentage'): + val = val / 0.01 + ws.write(row_pos, col, val, kpi_cell_style) + else: + ws.write(row_pos, col, value['val_r'], kpi_cell_style) + row_pos += 1 + + +MisBuilderXls('report.mis.report.instance.xls', + 'mis.report.instance', + parser=MisBuilderXlsParser) diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index 69469142..9aee3685 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -73,6 +73,8 @@ class MisBuilderXslx(ReportXlsx): if value.get('dp'): num_format_str += '.' num_format_str += '0' * int(value['dp']) + if value.get('prefix'): + num_format_str = '"%s"' % value['prefix'] + num_format_str if value.get('suffix'): num_format_str = num_format_str + ' "%s"' % value['suffix'] kpi_format = workbook.add_format({'num_format': num_format_str, diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/report_mis_report_instance.py index 1a85e0ea..44cfbeb8 100644 --- a/mis_builder/report/report_mis_report_instance.py +++ b/mis_builder/report/report_mis_report_instance.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import logging diff --git a/mis_builder/tests/__init__.py b/mis_builder/tests/__init__.py index 46244c14..622712e3 100644 --- a/mis_builder/tests/__init__.py +++ b/mis_builder/tests/__init__.py @@ -1,25 +1,5 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import test_mis_builder diff --git a/mis_builder/tests/test_mis_builder.py b/mis_builder/tests/test_mis_builder.py index 85d7af9f..637aed32 100644 --- a/mis_builder/tests/test_mis_builder.py +++ b/mis_builder/tests/test_mis_builder.py @@ -1,26 +1,6 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). import openerp.tests.common as common @@ -64,11 +44,12 @@ class TestMisBuilder(common.TransactionCase): 'mis_report_instance_' 'period_test'), 'style': None, + 'prefix': False, 'suffix': False, 'expr': 'len(test)', 'val_c': 'total_test = len(test)', 'val': 0, - 'val_r': u'0\xa0', + 'val_r': u'\u202f0\xa0', 'is_percentage': False, 'dp': 0, 'drilldown': False}] diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 6d4f5a16..f5154381 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -49,6 +49,7 @@ + diff --git a/mis_builder/wizard/__init__.py b/mis_builder/wizard/__init__.py index 17e53ebe..8cbd8455 100644 --- a/mis_builder/wizard/__init__.py +++ b/mis_builder/wizard/__init__.py @@ -1,25 +1,5 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from . import mis_builder_dashboard diff --git a/mis_builder/wizard/mis_builder_dashboard.py b/mis_builder/wizard/mis_builder_dashboard.py index b2550b8d..f4f91a1b 100644 --- a/mis_builder/wizard/mis_builder_dashboard.py +++ b/mis_builder/wizard/mis_builder_dashboard.py @@ -1,87 +1,66 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from openerp.osv import orm, fields +from openerp import api, fields, models from lxml import etree -class AddMisReportInstanceDashboard(orm.TransientModel): +class AddMisReportInstanceDashboard(models.TransientModel): _name = "add.mis.report.instance.dashboard.wizard" - _columns = {'name': fields.char('Name', size=32, required=True), - 'dashboard_id': fields.many2one( - 'ir.actions.act_window', - string="Dashboard", required=True, - domain="[('res_model', '=', 'board.board')]"), - } + name = fields.Char('Name', size=32, required=True) - def default_get(self, cr, uid, fields, context=None): - if context is None: - context = {} - if context.get('active_id'): + dashboard_id = fields.Many2one('ir.actions.act_window', + string="Dashboard", required=True, + domain="[('res_model', '=', " + "'board.board')]") + + @api.model + def default_get(self, fields): + res = {} + if self.env.context.get('active_id', False): res = super(AddMisReportInstanceDashboard, self).default_get( - cr, uid, fields, context=context) + fields) # get report instance name - res['name'] = self.pool['mis.report.instance'].read( - cr, uid, context['active_id'], ['name'])['name'] + res['name'] = self.env['mis.report.instance'].browse( + self.env.context['active_id']).name return res - def action_add_to_dashboard(self, cr, uid, ids, context=None): - if context is None: - context = {} - assert 'active_id' in context, "active_id missing in context" - wizard_data = self.browse(cr, uid, ids, context=context)[0] + @api.multi + def action_add_to_dashboard(self): + assert self.env.context.get('active_id', False), \ + "active_id missing in context" # create the act_window corresponding to this report - view_id = self.pool['ir.model.data'].get_object_reference( - cr, uid, 'mis_builder', 'mis_report_instance_result_view_form')[1] - report_result = self.pool['ir.actions.act_window'].create( - cr, uid, + self.env.ref('mis_builder.mis_report_instance_result_view_form') + view = self.env.ref( + 'mis_builder.mis_report_instance_result_view_form') + report_result = self.env['ir.actions.act_window'].create( {'name': 'mis.report.instance.result.view.action.%d' - % context['active_id'], + % self.env.context['active_id'], 'res_model': 'mis.report.instance', - 'res_id': context['active_id'], + 'res_id': self.env.context['active_id'], 'target': 'current', 'view_mode': 'form', - 'view_id': view_id}) + 'view_id': view.id}) # add this result in the selected dashboard - last_customization = self.pool['ir.ui.view.custom'].search( - cr, uid, - [('user_id', '=', uid), - ('ref_id', '=', wizard_data.dashboard_id.view_id.id)], limit=1) - arch = wizard_data.dashboard_id.view_id.arch + last_customization = self.env['ir.ui.view.custom'].search( + [('user_id', '=', self.env.uid), + ('ref_id', '=', self.dashboard_id.view_id.id)], limit=1) + arch = self.dashboard_id.view_id.arch if last_customization: - arch = self.pool['ir.ui.view.custom'].read( - cr, uid, last_customization[0], ['arch'])['arch'] + arch = self.env['ir.ui.view.custom'].browse( + last_customization[0].id).arch new_arch = etree.fromstring(arch) column = new_arch.xpath("//column")[0] - column.append(etree.Element('action', {'context': str(context), - 'name': str(report_result), - 'string': wizard_data.name, - 'view_mode': 'form'})) - self.pool['ir.ui.view.custom'].create( - cr, uid, {'user_id': uid, - 'ref_id': wizard_data.dashboard_id.view_id.id, - 'arch': etree.tostring(new_arch, pretty_print=True)}) + column.append(etree.Element('action', {'context': str( + self.env.context), + 'name': str(report_result.id), + 'string': self.name, + 'view_mode': 'form'})) + self.env['ir.ui.view.custom'].create( + {'user_id': self.env.uid, + 'ref_id': self.dashboard_id.view_id.id, + 'arch': etree.tostring(new_arch, pretty_print=True)}) return {'type': 'ir.actions.act_window_close', } From a4301c1c384248e1a21a30b8fbc403e0996fb16a Mon Sep 17 00:00:00 2001 From: Laetitia Gangloff Date: Thu, 11 Feb 2016 17:52:51 +0100 Subject: [PATCH 018/182] mis_builder : in compute do not set val in localdict if there was a computation error --- mis_builder/models/mis_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 6be04b27..b3b8fb15 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -485,6 +485,7 @@ class MisReportInstancePeriod(models.Model): kpi_val_comment = kpi.name + " = " + kpi.expression kpi_eval_expression = aep.replace_expr(kpi.expression) kpi_val = safe_eval(kpi_eval_expression, localdict) + localdict[kpi.name] = kpi_val except ZeroDivisionError: kpi_val = None kpi_val_rendered = '#DIV/0' @@ -501,7 +502,6 @@ class MisReportInstancePeriod(models.Model): else: kpi_val_rendered = kpi.render(lang_id, kpi_val) - localdict[kpi.name] = kpi_val try: kpi_style = None if kpi.css_style: From 350c996592503625059ab21d9424d252c16b71b7 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 27 Jan 2016 08:21:30 +0100 Subject: [PATCH 019/182] [IMP][mis_builder] MIS Builder widget improvement, add print and export button in the widget. --- mis_builder/models/mis_builder.py | 24 +++++ mis_builder/report/mis_builder_xls.py | 2 +- mis_builder/report/mis_builder_xlsx.py | 3 +- mis_builder/static/src/js/mis_builder.js | 67 ++++++++++++- mis_builder/static/src/xml/mis_widget.xml | 115 +++++++++++----------- mis_builder/views/mis_builder.xml | 24 +++-- 6 files changed, 161 insertions(+), 74 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index b3b8fb15..e48c0586 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -603,6 +603,30 @@ class MisReportInstance(models.Model): 'target': 'new', } + @api.multi + def print_pdf(self): + self.ensure_one() + return { + 'name': 'MIS report instance QWEB PDF report', + 'model': 'mis.report.instance', + 'type': 'ir.actions.report.xml', + 'report_name': 'mis_builder.report_mis_report_instance', + 'report_type': 'qweb-pdf', + 'context': self.env.context, + } + + @api.multi + def export_xls(self): + self.ensure_one() + return { + 'name': 'MIS report instance XLSX report', + 'model': 'mis.report.instance', + 'type': 'ir.actions.report.xml', + 'report_name': 'mis.report.instance.xlsx', + 'report_type': 'xlsx', + 'context': self.env.context, + } + @api.multi def compute(self): assert len(self) == 1 diff --git a/mis_builder/report/mis_builder_xls.py b/mis_builder/report/mis_builder_xls.py index df3d3ee5..903acb74 100644 --- a/mis_builder/report/mis_builder_xls.py +++ b/mis_builder/report/mis_builder_xls.py @@ -61,7 +61,7 @@ class MisBuilderXls(report_xls): # get the computed result of the report data = self.pool.get('mis.report.instance').compute( - self.cr, self.uid, objects[0].id) + self.cr, self.uid, objects[0].id, self.context) # Column headers header_name_list = [''] diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index 9aee3685..be7793aa 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -23,6 +23,7 @@ ############################################################################## from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx +from openerp.report import report_sxw import logging _logger = logging.getLogger(__name__) @@ -91,4 +92,4 @@ class MisBuilderXslx(ReportXlsx): MisBuilderXslx('report.mis.report.instance.xlsx', - 'mis.report.instance') + 'mis.report.instance', parser=report_sxw.rml_parse) diff --git a/mis_builder/static/src/js/mis_builder.js b/mis_builder/static/src/js/mis_builder.js index 3f314781..24daa055 100644 --- a/mis_builder/static/src/js/mis_builder.js +++ b/mis_builder/static/src/js/mis_builder.js @@ -5,6 +5,7 @@ var core = require('web.core'); var form_common = require('web.form_common'); var Model = require('web.DataModel'); var data = require('web.data'); +var ActionManager = require('web.ActionManager'); var MisReport = form_common.FormWidget.extend({ /** @@ -16,21 +17,67 @@ var MisReport = form_common.FormWidget.extend({ init: function() { this._super.apply(this, arguments); this.mis_report_data = null; + this.mis_report_instance_id = false; }, start: function() { this._super.apply(this, arguments); var self = this; + self.mis_report_instance_id = self.getParent().dataset.context.active_id + self.getParent().dataset.context['no_destroy'] = true; + self.generate_content(); + }, + + get_context: function() { + var self = this; + var context = {} + if (this.mis_report_instance_id){ + context['active_ids'] = [this.mis_report_instance_id]; + } + return context + }, + + print: function() { + var self = this + var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}) + new Model("mis.report.instance").call( + "print_pdf", + [self.mis_report_instance_id], + {'context': context} + ).then(function(result){ + self.do_action(result); + }); + }, + export_pdf: function() { + var self = this + var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}) + new Model("mis.report.instance").call( + "export_xls", + [self.mis_report_instance_id], + {'context': context} + ).then(function(result){ + self.do_action(result).done(function(result){ + }); + }); + }, + generate_content: function() { + var self = this + var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}) new Model("mis.report.instance").call( "compute", - [self.getParent().dataset.context.active_id], - {'context': new data.CompoundContext()} + [self.mis_report_instance_id], + {'context': context} ).then(function(result){ self.mis_report_data = result; self.renderElement(); }); }, - + renderElement: function() { + this._super(); + var self = this; + self.$(".oe_mis_builder_print").click(_.bind(this.print, this)); + self.$(".oe_mis_builder_export").click(_.bind(this.export_pdf, this)); + }, events: { "click a.mis_builder_drilldown": "drilldown", }, @@ -54,6 +101,18 @@ var MisReport = form_common.FormWidget.extend({ }, }); +ActionManager.include({ + dialog_stop: function (reason) { + var self = this + if (self.dialog_widget && self.dialog_widget.dataset && self.dialog_widget.dataset.context) { + var context = self.dialog_widget.dataset.context + if (!context['no_destroy']) { + this._super.apply(this, arguments); + } + } else { + this._super.apply(this, arguments); + } + } +}); core.form_custom_registry.add('mis_report', MisReport); - }); diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 5cfd9994..b668e8d1 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -1,62 +1,67 @@ diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index f5154381..58dce1bb 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -99,15 +99,15 @@ - - MIS report instance XLS report - mis.report.instance - ir.actions.report.xml - mis.report.instance.xlsx - xlsx - - - + + MIS report instance XLS report + mis.report.instance + ir.actions.report.xml + mis.report.instance.xlsx + xlsx + + + MIS report instance QWEB PDF report mis.report.instance @@ -124,8 +124,6 @@
-
- + + diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 58dce1bb..af55a854 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -120,7 +120,7 @@ mis.report.instance.result.view.form mis.report.instance - +
@@ -139,6 +139,7 @@ + - +
From cafbdda1c0faa63fe2ba35fc5cc6723707c20652 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 16 Mar 2016 10:16:01 +0100 Subject: [PATCH 025/182] [FIX][mis_builder] Fix the creation of a new mis.report.instance. --- mis_builder/models/mis_builder.py | 4 ++-- mis_builder/static/src/js/mis_builder.js | 15 ++++++++++++--- 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 0001552e..17bc7aee 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -629,12 +629,12 @@ class MisReportInstance(models.Model): @api.multi def display_settings(self): - self.ensure_one() + assert len(self.ids) <= 1 view_id = self.env.ref('mis_builder.mis_report_instance_view_form') return { 'type': 'ir.actions.act_window', 'res_model': 'mis.report.instance', - 'res_id': self.id, + 'res_id': self.id if self.id else False, 'view_mode': 'form', 'view_type': 'form', 'views': [(view_id.id, 'form')], diff --git a/mis_builder/static/src/js/mis_builder.js b/mis_builder/static/src/js/mis_builder.js index 7f184e00..cbd54f42 100644 --- a/mis_builder/static/src/js/mis_builder.js +++ b/mis_builder/static/src/js/mis_builder.js @@ -24,14 +24,23 @@ var MisReport = form_common.FormWidget.extend({ reload_widget: function() { var self = this self.mis_report_instance_id = self.getParent().datarecord.id - self.generate_content(); + if (self.mis_report_instance_id) { + self.generate_content(); + } else { + self.display_settings(); + } }, start: function() { this._super.apply(this, arguments); var self = this; self.mis_report_instance_id = self.getParent().datarecord.id - self.generate_content(); + if (self.mis_report_instance_id) { + self.getParent().dataset.context['no_destroy'] = true; + self.generate_content(); + } else { + self.display_settings(); + } }, get_context: function() { @@ -94,7 +103,7 @@ var MisReport = form_common.FormWidget.extend({ self.$(".oe_mis_builder_print").click(_.bind(this.print, this)); self.$(".oe_mis_builder_export").click(_.bind(this.export_pdf, this)); self.$(".oe_mis_builder_settings").click(_.bind(this.display_settings, this)); - var Users = new instance.web.Model('res.users'); + var Users = new Model('res.users'); Users.call('has_group', ['account.group_account_user']).done(function (res) { if (res) { self.$(".oe_mis_builder_settings").show(); From 8e4e046aee1e1e8e555bb468895457b38c1e6fb1 Mon Sep 17 00:00:00 2001 From: ThomasBinsfeld Date: Mon, 29 Feb 2016 12:28:10 +0100 Subject: [PATCH 026/182] [ADD] AccountingNone (singleton) to differentiate balances among which the debit and the credit are zero and balances among which debit and credit nullify --- mis_builder/__openerp__.py | 0 mis_builder/models/accounting_none.py | 136 ++++++++++++++++++++++++++ mis_builder/models/aep.py | 7 +- mis_builder/models/mis_builder.py | 8 +- 4 files changed, 146 insertions(+), 5 deletions(-) mode change 100644 => 100755 mis_builder/__openerp__.py create mode 100755 mis_builder/models/accounting_none.py mode change 100644 => 100755 mis_builder/models/aep.py mode change 100644 => 100755 mis_builder/models/mis_builder.py diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py old mode 100644 new mode 100755 diff --git a/mis_builder/models/accounting_none.py b/mis_builder/models/accounting_none.py new file mode 100755 index 00000000..bcf15ecb --- /dev/null +++ b/mis_builder/models/accounting_none.py @@ -0,0 +1,136 @@ +# -*- coding: utf-8 -*- + + +""" +Provides the AccountingNone singleton + +AccountingNone is a null value that dissolves in basic arithmetic operations, +as illustrated in the examples below + +>>> 1 + 1 +2 +>>> 1 + AccountingNone +1 +>>> AccountingNone + 1 +1 +>>> AccountingNone + None +AccountingNone +>>> +AccountingNone +AccountingNone +>>> -AccountingNone +AccountingNone +>>> -(AccountingNone) +AccountingNone +>>> AccountingNone - 1 +-1 +>>> 1 - AccountingNone +1 +>>> AccountingNone - None +AccountingNone +>>> AccountingNone / 2 +0.0 +>>> 2 / AccountingNone +Traceback (most recent call last): + ... +ZeroDivisionError +>>> AccountingNone / AccountingNone +AccountingNone +>>> AccountingNone // 2 +0.0 +>>> 2 // AccountingNone +Traceback (most recent call last): + ... +ZeroDivisionError +>>> AccountingNone // AccountingNone +AccountingNone +>>> AccountingNone * 2 +0.0 +>>> 2 * AccountingNone +0.0 +>>> AccountingNone * AccountingNone +AccountingNone +>>> AccountingNone * None +AccountingNone +""" + + +class AccountingNoneType(object): + + def __add__(self, other): + if other is None: + return AccountingNone + return other + + __radd__ = __add__ + + def __sub__(self, other): + if other is None: + return AccountingNone + return -other + + def __rsub__(self, other): + if other is None: + return AccountingNone + return other + + def __iadd__(self, other): + if other is None: + return AccountingNone + return other + + def __isub__(self, other): + if other is None: + return AccountingNone + return -other + + def __pos__(self): + return self + + def __neg__(self): + return self + + def __floordiv__(self, other): + """ + Overload of the // operator + """ + if other is AccountingNone: + return AccountingNone + return 0.0 + + def __rfloordiv__(self, other): + raise ZeroDivisionError + + def __truediv__(self, other): + """ + Overload of the / operator + """ + if other is AccountingNone: + return AccountingNone + return 0.0 + + def __rtruediv__(self, other): + raise ZeroDivisionError + + def __mul__(self, other): + if other is None or other is AccountingNone: + return AccountingNone + return 0.0 + + def __rmul__(self, other): + if other is None or other is AccountingNone: + return AccountingNone + return 0.0 + + def __repr__(self): + return 'AccountingNone' + + def __unicode__(self): + return '' + + +AccountingNone = AccountingNoneType() + + +if __name__ == '__main__': + import doctest + doctest.testmod() diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py old mode 100644 new mode 100755 index 4267c296..cbf63fea --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -8,6 +8,8 @@ from collections import defaultdict from openerp.exceptions import Warning as UserError from openerp.models import expression from openerp.tools.safe_eval import safe_eval +from openerp.tools.translate import _ +from .accounting_none import AccountingNone MODE_VARIATION = 'p' MODE_INITIAL = 'i' @@ -228,12 +230,13 @@ class AccountingExpressionProcessor(object): field, mode, account_codes, domain = self._parse_match_object(mo) key = (domain, mode) account_ids_data = self._data[key] - v = 0.0 + v = AccountingNone for account_code in account_codes: account_ids = self._account_ids_by_code[account_code] for account_id in account_ids: debit, credit = \ - account_ids_data.get(account_id, (0.0, 0.0)) + account_ids_data.get(account_id, + (AccountingNone, AccountingNone)) if field == 'bal': v += debit - credit elif field == 'deb': diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py old mode 100644 new mode 100755 index 17bc7aee..30106461 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -16,6 +16,7 @@ from openerp.tools.safe_eval import safe_eval from .aep import AccountingExpressionProcessor as AEP from .aggregate import _sum, _avg, _min, _max +from .accounting_none import AccountingNone _logger = logging.getLogger(__name__) @@ -142,8 +143,8 @@ class MisReportKpi(models.Model): def render(self, lang_id, value): """ render a KPI value as a unicode string, ready for display """ assert len(self) == 1 - if value is None: - return '#N/A' + if value is None or value is AccountingNone: + return '' elif self.type == 'num': return self._render_num(lang_id, value, self.divider, self.dp, self.prefix, self.suffix) @@ -469,6 +470,7 @@ class MisReportInstancePeriod(models.Model): 'max': _max, 'len': len, 'avg': _avg, + 'AccountingNone': AccountingNone, } localdict.update(self._fetch_queries()) @@ -515,7 +517,7 @@ class MisReportInstancePeriod(models.Model): AEP.has_account_var(kpi.expression)) res[kpi.name] = { - 'val': kpi_val, + 'val': None if kpi_val is AccountingNone else kpi_val, 'val_r': kpi_val_rendered, 'val_c': kpi_val_comment, 'style': kpi_style, From 261fd683ec6189df36d6831a0742d902f85c9a7c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 13 Mar 2016 22:17:04 +0100 Subject: [PATCH 027/182] [FIX] reset permissions that should not have changed --- mis_builder/__openerp__.py | 0 mis_builder/models/accounting_none.py | 0 mis_builder/models/aep.py | 0 mis_builder/models/mis_builder.py | 0 4 files changed, 0 insertions(+), 0 deletions(-) mode change 100755 => 100644 mis_builder/__openerp__.py mode change 100755 => 100644 mis_builder/models/accounting_none.py mode change 100755 => 100644 mis_builder/models/aep.py mode change 100755 => 100644 mis_builder/models/mis_builder.py diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py old mode 100755 new mode 100644 diff --git a/mis_builder/models/accounting_none.py b/mis_builder/models/accounting_none.py old mode 100755 new mode 100644 diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py old mode 100755 new mode 100644 diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py old mode 100755 new mode 100644 From f334f31a3e788345c9dc5097be0655a77d6561cb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 13 Mar 2016 22:19:41 +0100 Subject: [PATCH 028/182] [IMP] improve AccountingNone wrt comparisons mainly --- mis_builder/models/accounting_none.py | 67 ++++++++++++++++++++++----- 1 file changed, 55 insertions(+), 12 deletions(-) diff --git a/mis_builder/models/accounting_none.py b/mis_builder/models/accounting_none.py index bcf15ecb..8986ed41 100644 --- a/mis_builder/models/accounting_none.py +++ b/mis_builder/models/accounting_none.py @@ -5,7 +5,8 @@ Provides the AccountingNone singleton AccountingNone is a null value that dissolves in basic arithmetic operations, -as illustrated in the examples below +as illustrated in the examples below. In comparisons, AccountingNone behaves +the same as zero. >>> 1 + 1 2 @@ -51,6 +52,34 @@ AccountingNone AccountingNone >>> AccountingNone * None AccountingNone +>>> None * AccountingNone +AccountingNone +>>> str(AccountingNone) +'' +>>> bool(AccountingNone) +False +>>> AccountingNone > 0 +False +>>> AccountingNone < 0 +False +>>> AccountingNone < 1 +True +>>> AccountingNone > 1 +False +>>> 0 < AccountingNone +False +>>> 0 > AccountingNone +False +>>> 1 < AccountingNone +False +>>> 1 > AccountingNone +True +>>> AccountingNone == 0 +True +>>> AccountingNone == 0.0 +True +>>> AccountingNone == None +True """ @@ -89,10 +118,15 @@ class AccountingNoneType(object): def __neg__(self): return self + def __div__(self, other): + if other is AccountingNone: + return AccountingNone + return 0.0 + + def __rdiv__(self, other): + raise ZeroDivisionError + def __floordiv__(self, other): - """ - Overload of the // operator - """ if other is AccountingNone: return AccountingNone return 0.0 @@ -101,9 +135,6 @@ class AccountingNoneType(object): raise ZeroDivisionError def __truediv__(self, other): - """ - Overload of the / operator - """ if other is AccountingNone: return AccountingNone return 0.0 @@ -116,17 +147,29 @@ class AccountingNoneType(object): return AccountingNone return 0.0 - def __rmul__(self, other): - if other is None or other is AccountingNone: - return AccountingNone - return 0.0 + __rmul__ = __mul__ def __repr__(self): return 'AccountingNone' - def __unicode__(self): + def __str__(self): return '' + def __nonzero__(self): + return False + + def __bool__(self): + return False + + def __eq__(self, other): + return other == 0 or other is None or other is AccountingNone + + def __lt__(self, other): + return 0 < other + + def __gt__(self, other): + return 0 > other + AccountingNone = AccountingNoneType() From 29085160c16e86c47b5acd4c9c7c7186803d406d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 13 Mar 2016 22:56:39 +0100 Subject: [PATCH 029/182] Add copyright header and __all__ to accounting_none.py --- mis_builder/models/accounting_none.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/mis_builder/models/accounting_none.py b/mis_builder/models/accounting_none.py index 8986ed41..57f7f549 100644 --- a/mis_builder/models/accounting_none.py +++ b/mis_builder/models/accounting_none.py @@ -1,8 +1,9 @@ # -*- coding: utf-8 -*- - - +# © 2016 Thomas Binsfeld +# © 2016 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). """ -Provides the AccountingNone singleton +Provides the AccountingNone singleton. AccountingNone is a null value that dissolves in basic arithmetic operations, as illustrated in the examples below. In comparisons, AccountingNone behaves @@ -82,6 +83,8 @@ True True """ +__all__ = ['AccountingNone'] + class AccountingNoneType(object): From 16678d847ecc1a685b7918c8722c6e43340e650b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 14 Mar 2016 12:14:46 +0100 Subject: [PATCH 030/182] [FIX] fix comparison rendering in presence of AccountingNone --- mis_builder/models/accounting_none.py | 5 +++++ mis_builder/models/mis_builder.py | 12 +++++++----- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/mis_builder/models/accounting_none.py b/mis_builder/models/accounting_none.py index 57f7f549..5249e540 100644 --- a/mis_builder/models/accounting_none.py +++ b/mis_builder/models/accounting_none.py @@ -27,6 +27,8 @@ AccountingNone -1 >>> 1 - AccountingNone 1 +>>> abs(AccountingNone) +AccountingNone >>> AccountingNone - None AccountingNone >>> AccountingNone / 2 @@ -115,6 +117,9 @@ class AccountingNoneType(object): return AccountingNone return -other + def __abs__(self): + return self + def __pos__(self): return self diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 30106461..05d998e6 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -158,17 +158,19 @@ class MisReportKpi(models.Model): average_value, average_base_value): """ render the comparison of two KPI values, ready for display """ assert len(self) == 1 - if value is None or base_value is None: - return '' + if value is None: + value = AccountingNone + if base_value is None: + base_value = AccountingNone if self.type == 'pct': return self._render_num( lang_id, value - base_value, 0.01, self.dp, '', _('pp'), sign='+') elif self.type == 'num': - if average_value: + if value and average_value: value = value / float(average_value) - if average_base_value: + if base_value and average_base_value: base_value = base_value / float(average_base_value) if self.compare_method == 'diff': return self._render_num( @@ -176,7 +178,7 @@ class MisReportKpi(models.Model): value - base_value, self.divider, self.dp, self.prefix, self.suffix, sign='+') elif self.compare_method == 'pct': - if round(base_value, self.dp) != 0: + if base_value and round(base_value, self.dp) != 0: return self._render_num( lang_id, (value - base_value) / abs(base_value), From d1d5f704394fe637cd7754610d892d91a19080c2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 18 Mar 2016 13:09:01 +0100 Subject: [PATCH 031/182] mis_builder: render blank instead of +0 comparison columns --- mis_builder/models/mis_builder.py | 38 ++++++++++++++++++++----------- 1 file changed, 25 insertions(+), 13 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 05d998e6..a711d8d5 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -156,33 +156,45 @@ class MisReportKpi(models.Model): def render_comparison(self, lang_id, value, base_value, average_value, average_base_value): - """ render the comparison of two KPI values, ready for display """ + """ render the comparison of two KPI values, ready for display + + If the difference is 0, an empty string is returned. + """ assert len(self) == 1 if value is None: value = AccountingNone if base_value is None: base_value = AccountingNone if self.type == 'pct': - return self._render_num( - lang_id, - value - base_value, - 0.01, self.dp, '', _('pp'), sign='+') + delta = value - base_value + if delta and round(delta, self.dp) != 0: + return self._render_num( + lang_id, + delta, + 0.01, self.dp, '', _('pp'), + sign='+') elif self.type == 'num': if value and average_value: value = value / float(average_value) if base_value and average_base_value: base_value = base_value / float(average_base_value) if self.compare_method == 'diff': - return self._render_num( - lang_id, - value - base_value, - self.divider, self.dp, self.prefix, self.suffix, sign='+') - elif self.compare_method == 'pct': - if base_value and round(base_value, self.dp) != 0: + delta = value - base_value + if delta and round(delta, self.dp) != 0: return self._render_num( lang_id, - (value - base_value) / abs(base_value), - 0.01, self.dp, '', '%', sign='+') + delta, + self.divider, self.dp, self.prefix, self.suffix, + sign='+') + elif self.compare_method == 'pct': + if base_value and round(base_value, self.dp) != 0: + delta = (value - base_value) / abs(base_value) + if delta and round(delta, self.dp) != 0: + return self._render_num( + lang_id, + delta, + 0.01, self.dp, '', '%', + sign='+') return '' def _render_num(self, lang_id, value, divider, From adb3cfd389b3425597e2284728deb226c0cc2f52 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 4 Apr 2016 14:51:07 +0200 Subject: [PATCH 032/182] [IMP] mis_builder: meaningful copy for MIS Report templates --- mis_builder/models/mis_builder.py | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index a711d8d5..caf7a159 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -282,9 +282,17 @@ class MisReport(models.Model): description = fields.Char(required=False, string='Description', translate=True) query_ids = fields.One2many('mis.report.query', 'report_id', - string='Queries') + string='Queries', + copy=True) kpi_ids = fields.One2many('mis.report.kpi', 'report_id', - string='KPI\'s') + string='KPI\'s', + copy=True) + + @api.one + def copy(self, default=None): + default = dict(default or {}) + default['name'] = _('%s (copy)') % self.name + return super(MisReport, self).copy(default) # TODO: kpi name cannot be start with query name From 65d4d0b08dc05776eb7b9df26e2eb2222b4cfc76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 26 Apr 2016 15:49:24 +0200 Subject: [PATCH 033/182] [IMP] mis_builder: copy semantics for mis.report.instance --- mis_builder/models/mis_builder.py | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index caf7a159..eee3b052 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -596,7 +596,8 @@ class MisReportInstance(models.Model): period_ids = fields.One2many('mis.report.instance.period', 'report_instance_id', required=True, - string='Periods') + string='Periods', + copy=True) target_move = fields.Selection([('posted', 'All Posted Entries'), ('all', 'All Entries')], string='Target Moves', @@ -606,6 +607,12 @@ class MisReportInstance(models.Model): string='Company') landscape_pdf = fields.Boolean(string='Landscape PDF') + @api.one + def copy(self, default=None): + default = dict(default or {}) + default['name'] = _('%s (copy)') % self.name + return super(MisReportInstance, self).copy(default) + def _format_date(self, lang_id, date): # format date following user language date_format = self.env['res.lang'].browse(lang_id).date_format From ee033c135afba5bea3269397338e88e6515941a3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 6 Mar 2016 14:10:38 +0100 Subject: [PATCH 034/182] mis_builder: better layout of mis report screen on small screens --- mis_builder/views/mis_builder.xml | 41 ++++++++++++++++++------------- 1 file changed, 24 insertions(+), 17 deletions(-) diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index af55a854..3b075a7b 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -170,24 +170,31 @@ - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + From b10db9e301fcbe0c250decea37056c48884d6383 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 18 Mar 2016 13:31:17 +0100 Subject: [PATCH 035/182] mis_builder: better form layout for MIS Report templates --- mis_builder/views/mis_builder.xml | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 3b075a7b..2215378d 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -29,7 +29,9 @@ - + + + @@ -40,7 +42,9 @@ - + + + @@ -57,7 +61,7 @@ - + - + + - + From 5a2d226c03995b6abe7315a8a9ca070775d0ea61 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 27 Apr 2016 16:24:08 +0200 Subject: [PATCH 042/182] [WIP] domain for initial balance depending on account type --- mis_builder/models/aep.py | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 9503a3e8..726a812c 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -180,10 +180,19 @@ class AccountingExpressionProcessor(object): target_move): if mode == MODE_VARIATION: domain = [('date', '>=', date_from), ('date', '<=', date_to)] - elif mode == MODE_INITIAL: - domain = [('date', '<', date_from)] - elif mode == MODE_END: - domain = [('date', '<=', date_to)] + else: + # for income and expense account, get balance from the beginning + # of the current fiscal year + fy_date_from = \ + self.company.compute_fiscalyear_dates(date_from)['date_from'] + domain = ['|', + ('date', '>=', fy_date_from), + ('account_id.user_type_id.include_initial_balance', '=', + True)] + if mode == MODE_INITIAL: + domain.append(('date', '<', date_from)) + elif mode == MODE_END: + domain.append(('date', '<=', date_to)) if target_move == 'posted': domain.append(('move_id.state', '=', 'posted')) return expression.normalize_domain(domain) From 7203d992521375cacaa2ad7422593111837c010f Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 27 Apr 2016 17:58:42 +0200 Subject: [PATCH 043/182] [FIX][mis_builder] Better with company --- mis_builder/models/aep.py | 21 +++++++++++---------- mis_builder/models/mis_builder.py | 15 ++++++++++----- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 726a812c..3a877569 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -72,7 +72,7 @@ class AccountingExpressionProcessor(object): # - NNN for a code with an exact match self._account_ids_by_code = defaultdict(set) - def _load_account_codes(self, account_codes): + def _load_account_codes(self, account_codes, company): account_model = self.env['account.account'] exact_codes = set() for account_code in account_codes: @@ -85,13 +85,13 @@ class AccountingExpressionProcessor(object): self._account_ids_by_code[account_code].update(account_ids) elif '%' in account_code: account_ids = account_model.\ - search([('code', 'like', account_code)]).mapped('id') + search([('code', 'like', account_code), ('company_id', '=', company.id)]).mapped('id') self._account_ids_by_code[account_code].update(account_ids) else: # search exact codes after the loop to do less queries exact_codes.add(account_code) for account in account_model.\ - search([('code', 'in', list(exact_codes))]): + search([('code', 'in', list(exact_codes)), ('company_id', '=', company.id)]): self._account_ids_by_code[account.code].add(account.id) def _parse_match_object(self, mo): @@ -128,13 +128,13 @@ class AccountingExpressionProcessor(object): key = (domain, mode) self._map_account_ids[key].update(account_codes) - def done_parsing(self): + def done_parsing(self, company): """Load account codes and replace account codes by account ids in map.""" for key, account_codes in self._map_account_ids.items(): # TODO _load_account_codes could be done # for all account_codes at once (also in v8) - self._load_account_codes(account_codes) + self._load_account_codes(account_codes, company) account_ids = set() for account_code in account_codes: account_ids.update(self._account_ids_by_code[account_code]) @@ -147,7 +147,7 @@ class AccountingExpressionProcessor(object): def get_aml_domain_for_expr(self, expr, date_from, date_to, - target_move): + target_move, company): """ Get a domain on account.move.line for an expression. Prerequisite: done_parsing() must have been invoked. @@ -171,13 +171,14 @@ class AccountingExpressionProcessor(object): if mode not in date_domain_by_mode: date_domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - mode, target_move) + mode, target_move, + company) return expression.OR(aml_domains) + \ expression.OR(date_domain_by_mode.values()) def get_aml_domain_for_dates(self, date_from, date_to, mode, - target_move): + target_move, company): if mode == MODE_VARIATION: domain = [('date', '>=', date_from), ('date', '<=', date_to)] else: @@ -198,7 +199,7 @@ class AccountingExpressionProcessor(object): return expression.normalize_domain(domain) def do_queries(self, date_from, date_to, - target_move, additional_move_line_filter=None): + target_move, company, additional_move_line_filter=None): """Query sums of debit and credit for all accounts and domains used in expressions. @@ -213,7 +214,7 @@ class AccountingExpressionProcessor(object): if mode not in domain_by_mode: domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - mode, target_move) + mode, target_move, company) domain = list(domain) + domain_by_mode[mode] domain.append(('account_id', 'in', self._map_account_ids[key])) if additional_move_line_filter: diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 997a5d5f..ca77b080 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -297,12 +297,12 @@ class MisReport(models.Model): # TODO: kpi name cannot be start with query name @api.multi - def _prepare_aep(self): + def _prepare_aep(self, company): self.ensure_one() aep = AEP(self.env) for kpi in self.kpi_ids: aep.parse_expr(kpi.expression) - aep.done_parsing() + aep.done_parsing(company) return aep @api.multi @@ -366,6 +366,7 @@ class MisReport(models.Model): def _compute(self, lang_id, aep, date_from, date_to, target_move, + company, get_additional_move_line_filter=None, get_additional_query_filter=None, period_id=None): @@ -425,6 +426,7 @@ class MisReport(models.Model): additional_move_line_filter = get_additional_move_line_filter() aep.do_queries(date_from, date_to, target_move, + company, additional_move_line_filter) compute_queue = self.kpi_ids @@ -630,7 +632,8 @@ class MisReportInstancePeriod(models.Model): domain = aep.get_aml_domain_for_expr( expr, self.date_from, self.date_to, - self.report_instance_id.target_move) + self.report_instance_id.target_move, + self.report_instance_id.company_id) domain.extend(self._get_additional_move_line_filter()) return { 'name': expr + ' - ' + self.name, @@ -652,6 +655,7 @@ class MisReportInstancePeriod(models.Model): lang_id, aep, self.date_from, self.date_to, self.report_instance_id.target_move, + self.report_instance_id.company_id, self._get_additional_move_line_filter, self._get_additional_query_filter, period_id=self.id, @@ -701,7 +705,8 @@ class MisReportInstance(models.Model): default='posted') company_id = fields.Many2one(comodel_name='res.company', string='Company', - default=_default_company) + default=_default_company, + required=True) landscape_pdf = fields.Boolean(string='Landscape PDF') @api.one @@ -773,7 +778,7 @@ class MisReportInstance(models.Model): @api.multi def compute(self): self.ensure_one() - aep = self.report_id._prepare_aep() + aep = self.report_id._prepare_aep(self.company_id) # fetch user language only once # TODO: is this necessary? From 72d153c2cc89df1ebdecbd8fce8c030ea9680315 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 27 Apr 2016 18:01:21 +0200 Subject: [PATCH 044/182] [CHG][mis_builder] Use company to get date_from --- mis_builder/models/aep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 3a877569..579cd6d9 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -185,7 +185,7 @@ class AccountingExpressionProcessor(object): # for income and expense account, get balance from the beginning # of the current fiscal year fy_date_from = \ - self.company.compute_fiscalyear_dates(date_from)['date_from'] + company.compute_fiscalyear_dates(date_from)['date_from'] domain = ['|', ('date', '>=', fy_date_from), ('account_id.user_type_id.include_initial_balance', '=', From 19fcf3b9454924fdee01fdde6b51d620e002296c Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 27 Apr 2016 18:50:46 +0200 Subject: [PATCH 045/182] [FIX] Flake8 issues --- mis_builder/models/aep.py | 6 ++++-- mis_builder/models/mis_builder.py | 3 ++- 2 files changed, 6 insertions(+), 3 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 579cd6d9..0314b8ca 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -85,13 +85,15 @@ class AccountingExpressionProcessor(object): self._account_ids_by_code[account_code].update(account_ids) elif '%' in account_code: account_ids = account_model.\ - search([('code', 'like', account_code), ('company_id', '=', company.id)]).mapped('id') + search([('code', 'like', account_code), + ('company_id', '=', company.id)]).mapped('id') self._account_ids_by_code[account_code].update(account_ids) else: # search exact codes after the loop to do less queries exact_codes.add(account_code) for account in account_model.\ - search([('code', 'in', list(exact_codes)), ('company_id', '=', company.id)]): + search([('code', 'in', list(exact_codes)), + ('company_id', '=', company.id)]): self._account_ids_by_code[account.code].add(account.id) def _parse_match_object(self, mo): diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index ca77b080..37ea7094 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -535,7 +535,8 @@ class MisReportInstancePeriod(models.Model): if current_periods: all_periods = date_range_obj.search( [('type_id', '=', self.date_range_type_id.id), - ('company_id', '=', self.report_instance_id.company_id.id)], + ('company_id', '=', + self.report_instance_id.company_id.id)], order='date_start') all_period_ids = [p.id for p in all_periods] p = all_period_ids.index(current_periods[0].id) + self.offset From b1ac0fe630fc1846b69b5190faaa8f8c3ee87f87 Mon Sep 17 00:00:00 2001 From: "Adrien Peiffer (ACSONE)" Date: Wed, 27 Apr 2016 18:52:05 +0200 Subject: [PATCH 046/182] [FIX][mis_builder] Don't forget the company for sone_parsing method. --- mis_builder/models/mis_builder.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 37ea7094..282c3044 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -629,7 +629,7 @@ class MisReportInstancePeriod(models.Model): if AEP.has_account_var(expr): aep = AEP(self.env) aep.parse_expr(expr) - aep.done_parsing() + aep.done_parsing(self.report_instance_id.company_id) domain = aep.get_aml_domain_for_expr( expr, self.date_from, self.date_to, From f0a6ae0f036a6fa598990b66931af894d58f3eff Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 27 Apr 2016 19:30:32 +0200 Subject: [PATCH 047/182] [FIX] mis_builder: fix date type for fiscal year date start computation plus a bit of pep 8 and idiom to get ids from a recordset --- mis_builder/models/aep.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 0314b8ca..7b53d372 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -5,6 +5,7 @@ import re from collections import defaultdict +from openerp import fields from openerp.models import expression from openerp.tools.safe_eval import safe_eval from .accounting_none import AccountingNone @@ -81,12 +82,12 @@ class AccountingExpressionProcessor(object): if account_code is None: # None means we want all accounts account_ids = account_model.\ - search([]).mapped('id') + search([]).ids self._account_ids_by_code[account_code].update(account_ids) elif '%' in account_code: account_ids = account_model.\ search([('code', 'like', account_code), - ('company_id', '=', company.id)]).mapped('id') + ('company_id', '=', company.id)]).ids self._account_ids_by_code[account_code].update(account_ids) else: # search exact codes after the loop to do less queries @@ -186,8 +187,9 @@ class AccountingExpressionProcessor(object): else: # for income and expense account, get balance from the beginning # of the current fiscal year + date_from_date = fields.Date.from_string(date_from) fy_date_from = \ - company.compute_fiscalyear_dates(date_from)['date_from'] + company.compute_fiscalyear_dates(date_from_date)['date_from'] domain = ['|', ('date', '>=', fy_date_from), ('account_id.user_type_id.include_initial_balance', '=', From 9a246ce8c080f7f39f08cbd75957cc014dda45d6 Mon Sep 17 00:00:00 2001 From: marco Date: Wed, 27 Apr 2016 19:45:14 +0200 Subject: [PATCH 048/182] [IMP] add a form view for kpi report line --- mis_builder/views/mis_builder.xml | 63 ++++++++++++++++++++++++++++--- 1 file changed, 58 insertions(+), 5 deletions(-) diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 8adbab28..a3762c2a 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -45,7 +45,7 @@ - + @@ -56,8 +56,6 @@ - - @@ -93,6 +91,61 @@ + + mis.report.view.kpi.form + mis.report.kpi + +
+ + + + + + + + + + + + + + + + + + + + + + +
+
+ + MIS Report Templates @@ -111,7 +164,7 @@ xlsx - + MIS report instance QWEB PDF report mis.report.instance @@ -163,7 +216,7 @@ -
+
- - + + - - - + + diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index e1742615..36b611b4 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -30,6 +30,14 @@ + + + + + + + + @@ -49,7 +57,9 @@ - + + @@ -99,7 +109,16 @@ - + + + + + + + + @@ -249,6 +268,9 @@ + From d976ede6448530b8bc0a5a82c28dc701a595b071 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Thu, 28 Apr 2016 00:45:38 +0200 Subject: [PATCH 053/182] [IMP] improve view and onchange on multi expression --- mis_builder/models/mis_builder.py | 48 ++++++++----------------------- mis_builder/views/mis_builder.xml | 27 +++++++++-------- 2 files changed, 27 insertions(+), 48 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 1452be70..67649f9d 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -158,38 +158,6 @@ class MisReportKpi(models.Model): })] }) - @api.model - def create(self, vals): - kpi = super(MisReportKpi, self).create(vals) - if kpi.multi: - kpi._populate_expression() - return kpi - - @api.multi - def write(self, vals): - res = super(MisReportKpi, self).write(vals) - if vals.get('multi'): - self._populate_expression() - return res - - @api.multi - def _populate_expression(self): - for kpi in self: - if kpi.multi: - if kpi.expression_ids: - expression = kpi.expression_ids[0].name - else: - expression = "AccountingNone" - existing_subkpis = kpi.expression_ids.mapped('subkpi_id') - expressions = [] - for subkpi in kpi.report_id.subkpi_ids: - if not subkpi in existing_subkpis: - self.env['mis.report.kpi.expression'].create({ - 'name': expression, - 'kpi_id': kpi.id, - 'subkpi_id': subkpi.id, - }) - @api.onchange('multi') def _onchange_multi(self): for kpi in self: @@ -198,6 +166,14 @@ class MisReportKpi(models.Model): kpi.expression = kpi.expression_ids[0].name else: kpi.expression = None + else: + expressions = [] + for subkpi in kpi.report_id.subkpi_ids: + expressions.append((0, 0, { + 'name': kpi.expression, + 'subkpi_id': subkpi.id, + })) + kpi.expression_ids = expressions @api.onchange('description') def _onchange_description(self): @@ -587,14 +563,14 @@ class MisReport(models.Model): '#ERR', '\n\n%s' % (traceback.format_exc(),))) except: - raise vals.append(DataError( '#ERR', '\n\n%s' % (traceback.format_exc(),))) - #TODO escape total - localdict[kpi.name] = SimpleArray(vals) - res[kpi] = SimpleArray(vals) + if kpi.multi: + vals = SimpleArray(vals) + localdict[kpi.name] = vals + res[kpi] = vals if len(recompute_queue) == 0: # nothing to recompute, we are done diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 36b611b4..226307f3 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -58,8 +58,7 @@ - + @@ -109,16 +108,6 @@ - - - - - - - - @@ -132,6 +121,20 @@ + + + + + + + + + +
+
@@ -23,8 +23,8 @@
- - + + - - - + + + + + From 4065376c1f7f9d7725ff598a21434838616c6463 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 28 Apr 2016 20:11:26 +0200 Subject: [PATCH 064/182] [WIP] mis_builder auto-detail --- mis_builder/models/aep.py | 28 ++++- mis_builder/models/mis_builder.py | 164 ++++++++++++++++++++++++------ 2 files changed, 158 insertions(+), 34 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 547df449..69f1a07b 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -192,8 +192,7 @@ class AccountingExpressionProcessor(object): company.compute_fiscalyear_dates(date_from_date)['date_from'] domain = ['|', ('date', '>=', fy_date_from), - ('account_id.user_type_id.include_initial_balance', '=', - True)] + ('user_type_id.include_initial_balance', '=', True)] if mode == MODE_INITIAL: domain.append(('date', '<', date_from)) elif mode == MODE_END: @@ -231,7 +230,7 @@ class AccountingExpressionProcessor(object): self._data[key][acc['account_id'][0]] = \ (acc['debit'] or 0.0, acc['credit'] or 0.0) - def replace_expr(self, expr): + def replace_expr(self, expr, account_ids_filter=None): """Replace accounting variables in an expression by their amount. Returns a new expression string. @@ -246,6 +245,9 @@ class AccountingExpressionProcessor(object): for account_code in account_codes: account_ids = self._account_ids_by_code[account_code] for account_id in account_ids: + if account_ids_filter and \ + account_id not in account_ids_filter: + continue debit, credit = \ account_ids_data.get(account_id, (AccountingNone, AccountingNone)) @@ -257,3 +259,23 @@ class AccountingExpressionProcessor(object): v += credit return '(' + repr(v) + ')' return self.ACC_RE.sub(f, expr) + + def get_accounts_in_expr(self, expr): + """Get the ids of all accounts involved in an expression. + This means only accounts for which contribute data to the expression. + + Returns a set of account ids. + + This method must be executed after do_queries(). + """ + res = set() + for mo in self.ACC_RE.finditer(expr): + _, mode, account_codes, domain = self._parse_match_object(mo) + key = (domain, mode) + account_ids_data = self._data[key] + for account_code in account_codes: + account_ids = self._account_ids_by_code[account_code] + for account_id in account_ids: + if account_id in account_ids_data: + res.add(account_id) + return res diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index cb1bc6e3..45ac6724 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -2,6 +2,7 @@ # © 2014-2015 ACSONE SA/NV () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). +from collections import defaultdict, OrderedDict import datetime import dateutil import logging @@ -37,6 +38,56 @@ class AutoStruct(object): setattr(self, k, v) +class ExplodedKpiItem(object): + + def __init__(self, account_id): + pass + + +class KpiMatrix(object): + + def __init__(self): + # { period: {kpi: vals} + self._kpi_vals = defaultdict(dict) + # { period: {kpi: {account_id: vals}}} + self._kpi_exploded_vals = defaultdict(dict) + # { period: localdict } + self._localdict = {} + # { kpi: set(account_ids) } + self._kpis = OrderedDict() + + def set_kpi_vals(self, period, kpi, vals): + self._kpi_vals[period][kpi] = vals + if kpi not in self._kpis: + self._kpis[kpi] = set() + + def set_kpi_exploded_vals(self, period, kpi, account_id, vals): + exploded_vals = self._kpi_exploded_vals[period] + if kpi not in exploded_vals: + exploded_vals[kpi] = {} + exploded_vals[kpi][account_id] = vals + self._kpis[kpi].add(account_id) + + def set_localdict(self, period, localdict): + self._localdict[period] = localdict + + def iter_kpi_vals(self, period): + for kpi, vals in self._kpi_vals[period].iteritems(): + yield kpi.name, kpi, vals + kpi_exploded_vals = self._kpi_exploded_vals[period] + if kpi not in kpi_exploded_vals: + continue + for account_id, account_id_vals in \ + kpi_exploded_vals[kpi].iteritems(): + yield "%s:%s" % (kpi.name, account_id), kpi, account_id_vals + + def iter_kpis(self): + for kpi, account_ids in self._kpis.iteritems(): + yield kpi.name, kpi + for account_id in account_ids: + yield "%s:%s" % (kpi.name, account_id), kpi + + def _get_selection_label(selection, value): for v, l in selection: if v == value: @@ -458,14 +509,19 @@ class MisReport(models.Model): return res @api.multi - def _compute(self, lang_id, aep, + def _compute(self, kpi_matrix, period_key, + lang_id, aep, date_from, date_to, target_move, company, - subkpi_ids, + subkpis_filter, get_additional_move_line_filter=None, get_additional_query_filter=None): - """ Compute + """ Evaluate a report for a given period, populating a KpiMatrix. + + :param kpi_matrix: the KpiMatrix object to be populated + :param kpi_matrix_period: the period key to use when populating + the KpiMatrix :param lang_id: id of a res.lang object :param aep: an AccountingExpressionProcessor instance created using _prepare_aep() @@ -480,9 +536,16 @@ class MisReport(models.Model): query argument and returns a domain compatible with the query underlying model + + For each kpi, it calls set_kpi_vals and set_kpi_exploded_vals + with vals being a tuple with the evaluation + result for sub-kpis, or a DataError object if the evaluation failed. + + When done, it also calls set_localdict to store the local values + that served for the computation of the period. + """ self.ensure_one() - res = {} localdict = { 'registry': self.pool, @@ -512,8 +575,9 @@ class MisReport(models.Model): vals = [] has_error = False for expression in kpi.expression_ids: - if expression.subkpi_id \ - and expression.subkpi_id not in subkpi_ids: + if expression.subkpi_id and \ + subkpis_filter and \ + expression.subkpi_id not in subkpis_filter: continue try: kpi_eval_expression = aep.replace_expr(expression.name) @@ -540,9 +604,35 @@ class MisReport(models.Model): else: vals = SimpleArray(vals) - if not has_error: - localdict[kpi.name] = vals - res[kpi] = vals + kpi_matrix.set_kpi_vals(period_key, kpi, vals) + + if has_error: + continue + + # no error, set it in localdict so it can be used + # in computing other kpis + localdict[kpi.name] = vals + + # let's compute the exploded values by account + # we assume there will be no errors, because it is a + # the same as the kpi, just filtered on one account; + # I'd say if we have an exception in this part, it's bug... + # TODO FIXME: do this only if requested for this KPI + for account_id in aep.get_accounts_in_expr(kpi.expression): + account_id_vals = [] + for expression in kpi.expression_ids: + if expression.subkpi_id and \ + subkpis_filter and \ + expression.subkpi_id not in subkpis_filter: + continue + kpi_eval_expression = \ + aep.replace_expr(expression.name, + account_ids_filter=[account_id]) + account_id_vals.\ + append(safe_eval(kpi_eval_expression, localdict)) + kpi_matrix.set_kpi_exploded_vals(period_key, kpi, + account_id, + account_id_vals) if len(recompute_queue) == 0: # nothing to recompute, we are done @@ -555,7 +645,8 @@ class MisReport(models.Model): # try again compute_queue = recompute_queue recompute_queue = [] - return res, localdict + + kpi_matrix.set_localdict(period_key, localdict) class MisReportInstancePeriod(models.Model): @@ -692,6 +783,7 @@ class MisReportInstancePeriod(models.Model): @api.multi def drilldown(self, expr): self.ensure_one() + # TODO FIXME: drilldown by account if AEP.has_account_var(expr): aep = AEP(self.env) aep.parse_expr(expr) @@ -716,7 +808,7 @@ class MisReportInstancePeriod(models.Model): return False @api.multi - def _compute(self, lang_id, aep): + def _compute(self, kpi_matrix, lang_id, aep): """ Compute and render a mis report instance period It returns a dictionary keyed on kpi.name with a list of dictionaries @@ -739,7 +831,8 @@ class MisReportInstancePeriod(models.Model): self.ensure_one() # first invoke the compute method on the mis report template # passing it all the information regarding period and filters - data, localdict = self.report_instance_id.report_id._compute( + self.report_instance_id.report_id._compute( + kpi_matrix, self, lang_id, aep, self.date_from, self.date_to, self.report_instance_id.target_move, @@ -750,12 +843,14 @@ class MisReportInstancePeriod(models.Model): ) # second, render it to something that can be used by the widget res = {} - for kpi, vals in data.items(): - res[kpi.name] = [] + for kpi_name, kpi, vals in kpi_matrix.iter_kpi_vals(self): + res[kpi_name] = [] try: + # TODO FIXME check css_style evaluation wrt subkpis kpi_style = None if kpi.css_style: - kpi_style = safe_eval(kpi.css_style, localdict) + kpi_style = safe_eval(kpi.css_style, + kpi_matrix.get_localdict(self)) except: _logger.warning("error evaluating css stype expression %s", kpi.css_style, exc_info=True) @@ -768,7 +863,7 @@ class MisReportInstancePeriod(models.Model): 'dp': kpi.dp, 'is_percentage': kpi.type == 'pct', 'period_id': self.id, - 'expr': kpi.expression, + 'expr': kpi.expression, # TODO FIXME } for idx, subkpi_val in enumerate(vals): vals = default_vals.copy() @@ -787,7 +882,9 @@ class MisReportInstancePeriod(models.Model): expression = kpi.expression_ids[idx].name else: expression = kpi.expression - comment = kpi.name + " = " + expression + # TODO FIXME: check we have meaningfulname for exploded + # kpis + comment = kpi_name + " = " + expression vals.update({ 'val': (None if subkpi_val is AccountingNone @@ -796,7 +893,7 @@ class MisReportInstancePeriod(models.Model): 'val_c': comment, 'drilldown': drilldown, }) - res[kpi.name].append(vals) + res[kpi_name].append(vals) return res @@ -927,10 +1024,11 @@ class MisReportInstance(models.Model): # compute kpi values for each period kpi_values_by_period_ids = {} + kpi_matrix = KpiMatrix() for period in self.period_ids: if not period.valid: continue - kpi_values = period._compute(lang_id, aep) + kpi_values = period._compute(kpi_matrix, lang_id, aep) kpi_values_by_period_ids[period.id] = kpi_values # prepare header and content @@ -943,13 +1041,14 @@ class MisReportInstance(models.Model): }] content = [] rows_by_kpi_name = {} - for kpi in self.report_id.kpi_ids: - rows_by_kpi_name[kpi.name] = { - 'kpi_name': kpi.description, + for kpi_name, kpi in kpi_matrix.iter_kpis(): + rows_by_kpi_name[kpi_name] = { + # TODO FIXME + 'kpi_name': kpi.description if ':' not in kpi_name else kpi_name, 'cols': [], 'default_style': kpi.default_css_style } - content.append(rows_by_kpi_name[kpi.name]) + content.append(rows_by_kpi_name[kpi_name]) # populate header and content for period in self.period_ids: @@ -963,17 +1062,20 @@ class MisReportInstance(models.Model): header_date = _('from %s to %s') % (date_from, date_to) else: header_date = self._format_date(lang_id, period.date_from) + subkpis = period.subkpi_ids or \ + period.report_instance_id.report_id.subkpi_ids header[0]['cols'].append(dict( name=period.name, date=header_date, - colspan=len(period.subkpi_ids) or 1, + colspan=len(subkpis) or 1, )) - for subkpi in period.subkpi_ids: - header[1]['cols'].append(dict( - name=subkpi.name, - colspan=1, - )) - if not period.subkpi_ids: + if subkpis: + for subkpi in subkpis: + header[1]['cols'].append(dict( + name=subkpi.name, + colspan=1, + )) + else: header[1]['cols'].append(dict( name="", colspan=1, @@ -1006,4 +1108,4 @@ class MisReportInstance(models.Model): return { 'header': header, 'content': content, - } + } From 55207c194eb8250de27a5240c7bbd79dce1edb65 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 28 Apr 2016 22:13:09 +0200 Subject: [PATCH 065/182] [WIP] mis_builder: disable the WIP auto-detail feature --- mis_builder/models/mis_builder.py | 1 + 1 file changed, 1 insertion(+) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 45ac6724..121014e2 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -618,6 +618,7 @@ class MisReport(models.Model): # the same as the kpi, just filtered on one account; # I'd say if we have an exception in this part, it's bug... # TODO FIXME: do this only if requested for this KPI + continue for account_id in aep.get_accounts_in_expr(kpi.expression): account_id_vals = [] for expression in kpi.expression_ids: From 4505af107df468708166afe950418f267617a4b4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?S=C3=A9bastien=20BEAU?= Date: Thu, 28 Apr 2016 22:40:39 +0200 Subject: [PATCH 066/182] [IMP] improve report view --- mis_builder/models/mis_builder.py | 72 +++++++++++++++- mis_builder/views/mis_builder.xml | 132 +++++++++++++++++++++--------- 2 files changed, 162 insertions(+), 42 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 121014e2..7591c282 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -659,13 +659,23 @@ class MisReportInstancePeriod(models.Model): """ @api.one - @api.depends('report_instance_id.pivot_date', 'type', 'offset', 'duration') + @api.depends('report_instance_id.pivot_date', 'type', 'offset', + 'duration', 'report_instance_id.comparison_mode') def _compute_dates(self): self.date_from = False self.date_to = False self.valid = False - d = fields.Date.from_string(self.report_instance_id.pivot_date) - if self.type == 'd': + report = self.report_instance_id + d = fields.Date.from_string(report.pivot_date) + if not report.comparison_mode: + self.date_from = report.date_from + self.date_to = report.date_to + self.valid = True + elif self.mode == 'fix': + self.date_from = self.manual_date_from + self.date_to = self.manual_date_to + self.valid = True + elif self.type == 'd': date_from = d + datetime.timedelta(days=self.offset) date_to = date_from + \ datetime.timedelta(days=self.duration - 1) @@ -705,6 +715,10 @@ class MisReportInstancePeriod(models.Model): name = fields.Char(size=32, required=True, string='Description', translate=True) + mode = fields.Selection([('fix', 'Fix'), + ('relative', 'Relative'), + ], required=True, + default='fix') type = fields.Selection([('d', _('Day')), ('w', _('Week')), ('date_range', _('Date Range')) @@ -721,6 +735,11 @@ class MisReportInstancePeriod(models.Model): default=1) date_from = fields.Date(compute='_compute_dates', string="From") date_to = fields.Date(compute='_compute_dates', string="To") + manual_date_from = fields.Date(string="From") + manual_date_to = fields.Date(string="To") + date_range_id = fields.Many2one( + comodel_name='date.range', + string='Date Range') valid = fields.Boolean(compute='_compute_dates', type='boolean', string='Valid') @@ -740,7 +759,7 @@ class MisReportInstancePeriod(models.Model): default=1) subkpi_ids = fields.Many2many( 'mis.report.subkpi', - string="Sub KPI") + string="Sub KPI Filter") _order = 'sequence, id' @@ -753,6 +772,12 @@ class MisReportInstancePeriod(models.Model): 'Period name should be unique by report'), ] + @api.onchange('date_range_id') + def onchange_date_range(self): + for record in self: + record.manual_date_from = record.date_range_id.date_start + record.manual_date_to = record.date_range_id.date_end + @api.multi def _get_additional_move_line_filter(self): """ Prepare a filter to apply on all move lines @@ -944,6 +969,14 @@ class MisReportInstance(models.Model): default=_default_company, required=True) landscape_pdf = fields.Boolean(string='Landscape PDF') + comparison_mode = fields.Boolean( + compute="_compute_comparison_mode", + inverse="_inverse_comparison_mode") + date_range_id = fields.Many2one( + comodel_name='date.range', + string='Date Range') + date_from = fields.Date(string="From") + date_to = fields.Date(string="To") @api.one def copy(self, default=None): @@ -957,6 +990,37 @@ class MisReportInstance(models.Model): return datetime.datetime.strftime( fields.Date.from_string(date), date_format) + @api.multi + @api.depends('date_from') + def _compute_comparison_mode(self): + for instance in self: + instance.advanced_mode = not bool(instance.date_from) + + @api.multi + def _inverse_comparison_mode(self): + for record in self: + if not record.comparison_mode: + if not record.date_from: + record.date_from = datetime.now() + if not record.date_to: + record.date_to = datetime.now() + record.period_ids.unlink() + record.write({'period_ids': [ + (0, 0, { + 'name': 'Default', + 'type': 'd', + }) + ]}) + else: + record.date_from = None + record.date_to = None + + @api.onchange('date_range_id') + def onchange_date_range(self): + for record in self: + record.date_from = record.date_range_id.date_start + record.date_to = record.date_range_id.date_end + @api.multi def preview(self): assert len(self) == 1 diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 226307f3..df80f979 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -230,6 +230,12 @@
+
+
-
-
- - - - - - - - - +
+ + + + + + + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + +
@@ -294,5 +299,56 @@ + + mis.report.instance.period + + +
+ +
+
+
+

+ +

+
+ + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+ From fc2356d4e445da7d62181b7d1b6adb36b3c147c4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 28 Apr 2016 22:45:47 +0200 Subject: [PATCH 067/182] [WIP] mis_builder: auto-expand by account feature --- mis_builder/models/mis_builder.py | 5 +++-- mis_builder/views/mis_builder.xml | 3 +++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 7591c282..b9d35a8a 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -134,6 +134,7 @@ class MisReportKpi(models.Model): compute='_compute_expression', inverse='_inverse_expression') expression_ids = fields.One2many('mis.report.kpi.expression', 'kpi_id') + auto_expand_accounts = fields.Boolean(string='Display details by account') default_css_style = fields.Char(string='Default CSS style') css_style = fields.Char(string='CSS style expression') type = fields.Selection([('num', _('Numeric')), @@ -617,8 +618,8 @@ class MisReport(models.Model): # we assume there will be no errors, because it is a # the same as the kpi, just filtered on one account; # I'd say if we have an exception in this part, it's bug... - # TODO FIXME: do this only if requested for this KPI - continue + if not kpi.auto_expand_accounts: + continue for account_id in aep.get_accounts_in_expr(kpi.expression): account_id_vals = [] for expression in kpi.expression_ids: diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index df80f979..ff56ea94 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -135,6 +135,9 @@ attrs="{'invisible': [('multi', '=', True)], 'readonly': [('multi', '=', True)]}"/> + + +
@@ -32,7 +26,7 @@ From e7126a06895db3de0762733e140baa60ad674c99 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 11:48:53 +0200 Subject: [PATCH 113/182] [FIX] mis_builder: aep issue with smart ending balance computation --- mis_builder/models/aep.py | 47 ++++++++++++++++------------------- mis_builder/tests/test_aep.py | 5 ++++ 2 files changed, 27 insertions(+), 25 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index 47a3b6d5..49f6b5cb 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -139,7 +139,7 @@ class AccountingExpressionProcessor(object): for mo in self._ACC_RE.finditer(expr): _, mode, account_codes, domain = self._parse_match_object(mo) if mode == self.MODE_END and self.smart_end: - modes = (self.MODE_INITIAL, self.MODE_VARIATION) + modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END) else: modes = (mode, ) for mode in modes: @@ -234,8 +234,13 @@ class AccountingExpressionProcessor(object): # {(domain, mode): {account_id: (debit, credit)}} self._data = defaultdict(dict) domain_by_mode = {} + ends = [] for key in self._map_account_ids: domain, mode = key + if mode == self.MODE_END and self.smart_end: + # postpone computation of ending balance + ends.append((domain, mode)) + continue if mode not in domain_by_mode: domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, @@ -256,6 +261,18 @@ class AccountingExpressionProcessor(object): # in initial mode, ignore accounts with 0 balance continue self._data[key][acc['account_id'][0]] = (debit, credit) + # compute ending balances by summing initial and variation + for key in ends: + domain, mode = key + initial_data = self._data[(domain, self.MODE_INITIAL)] + variation_data = self._data[(domain, self.MODE_VARIATION)] + account_ids = set(initial_data.keys()) | set(variation_data.keys()) + for account_id in account_ids: + di, ci = initial_data.get(account_id, + (AccountingNone, AccountingNone)) + dv, cv = variation_data.get(account_id, + (AccountingNone, AccountingNone)) + self._data[key][account_id] = (di + dv, ci + cv) def replace_expr(self, expr): """Replace accounting variables in an expression by their amount. @@ -264,7 +281,8 @@ class AccountingExpressionProcessor(object): This method must be executed after do_queries(). """ - def s(field, mode, account_codes, domain): + def f(mo): + field, mode, account_codes, domain = self._parse_match_object(mo) key = (domain, mode) account_ids_data = self._data[key] v = AccountingNone @@ -286,17 +304,6 @@ class AccountingExpressionProcessor(object): mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ float_is_zero(v, precision_rounding=2): v = AccountingNone - return v - - def f(mo): - field, mode, account_codes, domain = self._parse_match_object(mo) - if mode == self.MODE_END and self.smart_end: - # split ending balance in initial+variation, so - # if there is no move in period, we end up with AccountingNone - v = s(field, self.MODE_INITIAL, account_codes, domain) + \ - s(field, self.MODE_VARIATION, account_codes, domain) - else: - v = s(field, mode, account_codes, domain) return '(' + repr(v) + ')' return self._ACC_RE.sub(f, expr) @@ -309,7 +316,8 @@ class AccountingExpressionProcessor(object): This method must be executed after do_queries(). """ - def s(field, mode, account_codes, domain): + def f(mo): + field, mode, account_codes, domain = self._parse_match_object(mo) key = (domain, mode) account_ids_data = self._data[key] debit, credit = \ @@ -327,17 +335,6 @@ class AccountingExpressionProcessor(object): mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ float_is_zero(v, precision_rounding=2): v = AccountingNone - return v - - def f(mo): - field, mode, account_codes, domain = self._parse_match_object(mo) - if mode == self.MODE_END and self.smart_end: - # split ending balance in initial+variation, so - # if there is no move in period, we end up with AccountingNone - v = s(field, self.MODE_INITIAL, account_codes, domain) + \ - s(field, self.MODE_VARIATION, account_codes, domain) - else: - v = s(field, mode, account_codes, domain) return '(' + repr(v) + ')' account_ids = set() diff --git a/mis_builder/tests/test_aep.py b/mis_builder/tests/test_aep.py index 9f8db4d4..1be10b13 100644 --- a/mis_builder/tests/test_aep.py +++ b/mis_builder/tests/test_aep.py @@ -188,6 +188,11 @@ class TestAEP(common.TransactionCase): self.assertEquals(variation, { self.account_in.id: -500, }) + end = self._eval_by_account_id('bale[]') + self.assertEquals(end, { + self.account_ar.id: 900, + self.account_in.id: -800, + }) def test_aep_convenience_methods(self): initial = AEP.get_balances_initial( From fd718ca93353e4d69b9acb6598fed774cd88ce80 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 11:49:47 +0200 Subject: [PATCH 114/182] [WIP] mis_builder refactoring: restore proper display and sorting of detail rows --- mis_builder/models/mis_builder.py | 106 +++++++++++++----------------- 1 file changed, 45 insertions(+), 61 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index e172d157..4d9d85b2 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -34,16 +34,26 @@ class AutoStruct(object): class KpiMatrixRow(object): - def __init__(self, kpi, account_id=None, parent_row=None): + def __init__(self, matrix, kpi, account_id=None, parent_row=None): + self._matrix = matrix self.kpi = kpi self.account_id = account_id - self.description = kpi.description self.comment = '' self.parent_row = parent_row + @property + def description(self): + if not self.account_id: + return self.kpi.description + else: + return self._matrix.get_account_name(self.account_id) + @property def style(self): - return self.kpi.style + if not self.account_id: + return self.kpi.style + else: + return None # TODO style for expanded accounts def iter_cell_tuples(self, cols): for col in cols: @@ -98,6 +108,8 @@ class KpiMatrixSubCol(object): def get_cell_for_row(self, row): cell_tuple = self.col.get_cell_tuple_for_row(row) + if cell_tuple is None: + return None return cell_tuple[self.index] @@ -124,9 +136,11 @@ class KpiMatrix(object): self._kpi_rows = OrderedDict() # { kpi: KpiMatrixRow } self._detail_rows = {} # { kpi: {account_id: KpiMatrixRow} } self._cols = OrderedDict() # { period_key: KpiMatrixCol } + self._account_model = env['account.account'] + self._account_names = {} # { account_id: account_name } def declare_kpi(self, kpi): - self._kpi_rows[kpi] = KpiMatrixRow(kpi) + self._kpi_rows[kpi] = KpiMatrixRow(self, kpi) self._detail_rows[kpi] = {} def declare_period(self, period_key, description, comment, @@ -142,7 +156,7 @@ class KpiMatrix(object): row = self._kpi_rows[kpi] else: kpi_row = self._kpi_rows[kpi] - row = KpiMatrixRow(kpi, account_id, parent_row=kpi_row) + row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row) self._detail_rows[kpi][account_id] = row col = self._cols[period_key] cell_tuple = [] @@ -163,8 +177,9 @@ class KpiMatrix(object): def iter_rows(self): for kpi_row in self._kpi_rows.values(): yield kpi_row - # TODO FIXME sort detail rows - for detail_row in self._detail_rows[kpi_row.kpi].values(): + detail_rows = self._detail_rows[kpi_row.kpi].values() + detail_rows = sorted(detail_rows, key=KpiMatrixRow.description) + for detail_row in detail_rows: yield detail_row def iter_cols(self): @@ -175,53 +190,19 @@ class KpiMatrix(object): for subcol in col.iter_subcols(): yield subcol + def _load_account_names(self): + account_ids = set() + for detail_rows in self._detail_rows.values(): + account_ids.update(detail_rows.keys()) + account_ids = list(account_ids) + accounts = self._account_model.search([('id', 'in', account_ids)]) + self._account_names = {a.id: u'{} {}'.format(a.code, a.name) + for a in accounts} -class old_KpiMatrix(object): - - def __iter_kpis(self): - """ Iterate kpis, including auto-expanded details by accounts - - It yields, in display order: - * kpi technical name - * kpi display name - * kpi object - """ - for kpi, account_ids in self._kpis.iteritems(): - yield kpi.name, kpi.description, kpi - for account_id in sorted(account_ids, key=self.get_account_name): - yield "%s:%s" % (kpi.name, account_id), \ - self.get_account_name(account_id), kpi - - def __get_exploded_account_ids(self): - """ Get the list of auto-expanded account ids - - It returns the complete list, across all periods and kpis. - This method must be called after setting all kpi values - using set_kpi_vals and set_exploded_kpi_vals. - """ - res = set() - for kpi, account_ids in self._kpis.iteritems(): - res.update(account_ids) - return list(res) - - def __load_account_names(self, account_obj): - """ Load account names for all exploded account ids - - This method must be called after setting all kpi values - using set_kpi_vals and set_exploded_kpi_vals, and before - calling get_account_name(). - """ - account_data = account_obj.browse(self.get_exploded_account_ids()) - self._account_names_by_id = {a.id: u"{} {}".format(a.code, a.name) - for a in account_data} - - def __get_account_name(self, account_id): - """ Get account display name from it's id - - This method must be called after loading account names with - load_account_names(). - """ - return self._account_names_by_id.get(account_id, account_id) + def get_account_name(self, account_id): + if account_id not in self._account_names: + self._load_account_names() + return self._account_names[account_id] def _get_selection_label(selection, value): @@ -1366,14 +1347,17 @@ class MisReportInstance(models.Model): 'cols': [] } for cell in row.iter_cells(kpi_matrix.iter_subcols()): - row_data['cols'].append({ - 'val': (cell.val - if cell.val is not AccountingNone else None), - 'val_r': cell.val_rendered, - 'val_c': cell.val_comment, - # TODO FIXME style - # TODO FIXME drilldown - }) + if cell is None: + row_data['cols'].append({}) + else: + row_data['cols'].append({ + 'val': (cell.val + if cell.val is not AccountingNone else None), + 'val_r': cell.val_rendered, + 'val_c': cell.val_comment, + # TODO FIXME style + # TODO FIXME drilldown + }) content.append(row_data) return { From 0744a803a774d32841640c03b6f489fc3d73a341 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 12:48:27 +0200 Subject: [PATCH 115/182] [WIP] mis_builder refactoring: restore period titles in widget --- mis_builder/models/mis_builder.py | 27 ++++++++++++++++------- mis_builder/static/src/xml/mis_widget.xml | 4 ++++ 2 files changed, 23 insertions(+), 8 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 4d9d85b2..9e8a2aea 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -130,8 +130,9 @@ class KpiMatrix(object): def __init__(self, env): # cache language id for faster rendering - lang = env.user.lang or 'en_US' - self.lang = env['res.lang'].search([('code', '=', lang)]) + lang_model = env['res.lang'] + lang_id = lang_model._lang_get(env.user.lang) + self.lang = lang_model.browse(lang_id) # data structures self._kpi_rows = OrderedDict() # { kpi: KpiMatrixRow } self._detail_rows = {} # { kpi: {account_id: KpiMatrixRow} } @@ -178,7 +179,7 @@ class KpiMatrix(object): for kpi_row in self._kpi_rows.values(): yield kpi_row detail_rows = self._detail_rows[kpi_row.kpi].values() - detail_rows = sorted(detail_rows, key=KpiMatrixRow.description) + detail_rows = sorted(detail_rows, key=lambda r: r.description) for detail_row in detail_rows: yield detail_row @@ -387,7 +388,7 @@ class MisReportKpi(models.Model): return self._render_num(lang, value, 0.01, self.dp, '', '%') else: - return unicode(value) # noqa - silence python3 error + return unicode(value) def render_comparison(self, lang, value, base_value, average_value, average_base_value): @@ -1210,9 +1211,11 @@ class MisReportInstance(models.Model): default['name'] = _('%s (copy)') % self.name return super(MisReportInstance, self).copy(default) - def _format_date(self, lang_id, date): + def _format_date(self, date): # format date following user language - date_format = self.env['res.lang'].browse(lang_id).date_format + lang_model = self.env['res.lang'] + lang_id = lang_model._lang_get(self.env.user.lang) + date_format = lang_model.browse(lang_id).date_format return datetime.datetime.strftime( fields.Date.from_string(date), date_format) @@ -1308,11 +1311,19 @@ class MisReportInstance(models.Model): aep = self.report_id._prepare_aep(self.company_id) kpi_matrix = self.report_id._prepare_kpi_matrix() for period in self.period_ids: + # add the column header + if period.duration == 1 and period.type == 'd': + comment = self._format_date(period.date_from) + else: + # from, to + date_from = self._format_date(period.date_from) + date_to = self._format_date(period.date_to) + comment = _('from %s to %s') % (date_from, date_to) self.report_id._compute_period( kpi_matrix, period.id, - 'period name', # TODO FIXME - 'period comment', # TODO FIXME + period.name, + comment, aep, period.date_from, period.date_to, diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 15add5eb..0b66ee80 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -17,6 +17,10 @@ From 059ccdb9750cba710980ca97b24fcb348dac5ed8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 13:06:28 +0200 Subject: [PATCH 116/182] [WIP] mis_builder refactoring: fix bug with detail accounts --- mis_builder/models/mis_builder.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 9e8a2aea..c79d16fc 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -157,8 +157,11 @@ class KpiMatrix(object): row = self._kpi_rows[kpi] else: kpi_row = self._kpi_rows[kpi] - row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row) - self._detail_rows[kpi][account_id] = row + if account_id in self._detail_rows[kpi]: + row = self._detail_rows[kpi][account_id] + else: + row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row) + self._detail_rows[kpi][account_id] = row col = self._cols[period_key] cell_tuple = [] assert len(vals) == col.colspan From 087906c5e80988bc47e5ce0ea4cd5fb37fcafe86 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 13:19:29 +0200 Subject: [PATCH 117/182] [FIX] mis_builder: widget footer was broken --- mis_builder/static/src/xml/mis_widget.xml | 7 +------ 1 file changed, 1 insertion(+), 6 deletions(-) diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 0b66ee80..811a02e9 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -55,12 +55,7 @@ - - +
-
- -
-
- -
-
+
+ +
+ +
+ +
+
+
-
- -
- -
-
- +
- +
+ +
+ +
From dd52a844a64b3da5d1589796bf8bb063efb5cc26 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 13:19:55 +0200 Subject: [PATCH 118/182] [FIX] mis_builder: fix period comment (date from/to) display --- mis_builder/models/mis_builder.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index c79d16fc..2ae17ba6 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -850,8 +850,9 @@ class MisReportInstancePeriod(models.Model): """ @api.one - @api.depends('report_instance_id.pivot_date', 'type', 'offset', - 'duration', 'report_instance_id.comparison_mode') + @api.depends('report_instance_id.pivot_date', + 'report_instance_id.comparison_mode', + 'type', 'offset', 'duration', 'mode') def _compute_dates(self): self.date_from = False self.date_to = False @@ -1315,7 +1316,7 @@ class MisReportInstance(models.Model): kpi_matrix = self.report_id._prepare_kpi_matrix() for period in self.period_ids: # add the column header - if period.duration == 1 and period.type == 'd': + if period.date_from == period.date_to: comment = self._format_date(period.date_from) else: # from, to @@ -1335,6 +1336,7 @@ class MisReportInstance(models.Model): period.subkpi_ids, period._get_additional_move_line_filter, period._get_additional_query_filter) + # TODO FIXME comparison columns header = [{'cols': []}, {'cols': []}] for col in kpi_matrix.iter_cols(): From e8993c90f9debe8d6eae9d43a7af4e6ddc901220 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 8 May 2016 13:23:23 +0200 Subject: [PATCH 119/182] [WIP] mis_builder refactoring: remove old, ugly code \o/ --- mis_builder/models/mis_builder.py | 212 ------------------------------ 1 file changed, 212 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 2ae17ba6..d5b0ff3a 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -1025,109 +1025,6 @@ class MisReportInstancePeriod(models.Model): else: return False - @api.multi - def _render_period(self, kpi_matrix, lang_id, aep): - """ Compute and render a mis report instance period - - It returns a dictionary keyed on kpi.name with a list of dictionaries - with the following values (one item in the list for each subkpi): - * val: the evaluated kpi, or None if there is no data or an error - * val_r: the rendered kpi as a string, or #ERR, #DIV - * val_c: a comment (explaining the error, typically) - * style: the css style of the kpi - (may change in the future!) - * prefix: a prefix to display in front of the rendered value - * suffix: a prefix to display after rendered value - * dp: the decimal precision of the kpi - * is_percentage: true if the kpi is of percentage type - (may change in the future!) - * expr: the kpi expression - * drilldown: true if the drilldown method of - mis.report.instance.period is going to do something - useful in this kpi - """ - # TODO FIXME remove this method - self.ensure_one() - # first invoke the compute method on the mis report template - # passing it all the information regarding period and filters - self.report_instance_id.report_id._compute_period( - kpi_matrix, self, - aep, - self.date_from, self.date_to, - self.report_instance_id.target_move, - self.report_instance_id.company_id, - self.subkpi_ids, - self._get_additional_move_line_filter, - self._get_additional_query_filter, - ) - # second, render it to something that can be used by the widget - res = {} - mis_report_kpi_style = self.env['mis.report.kpi.style'] - for kpi_name, kpi, vals in kpi_matrix.iter_kpi_vals(self): - res[kpi_name] = [] - try: - # TODO FIXME check style_expression evaluation wrt subkpis - kpi_style = None - if kpi.style_expression: - style_name = safe_eval(kpi.style_expression, - kpi_matrix.get_locals_dict(self)) - styles = mis_report_kpi_style.search( - [('name', '=', style_name)]) - kpi_style = styles and styles[0] - except: - _logger.warning("error evaluating css stype expression %s", - kpi.style, exc_info=True) - - default_vals = { - 'prefix': kpi.prefix, - 'suffix': kpi.suffix, - 'dp': kpi.dp, - 'is_percentage': kpi.type == 'pct', - 'period_id': self.id, - 'style': '', - 'xlsx_style': {}, - } - if kpi_style: - default_vals.update({ - 'style': kpi_style.to_css_style(), - 'xlsx_style': kpi_style.to_xlsx_forma_properties(), - }) - - for idx, subkpi_val in enumerate(vals): - vals = default_vals.copy() - if isinstance(subkpi_val, DataError): - vals.update({ - 'val': subkpi_val.name, - 'val_r': subkpi_val.name, - 'val_c': subkpi_val.msg, - 'drilldown': False, - }) - else: - if kpi.multi: - expression = kpi.expression_ids[idx].name - comment = '{}.{} = {}'.format( - kpi.name, - kpi.expression_ids[idx].subkpi_id.name, - expression) - else: - expression = kpi.expression - comment = '{} = {}'.format( - kpi.name, - expression) - drilldown = (subkpi_val is not AccountingNone and - AEP.has_account_var(expression)) - vals.update({ - 'val': (None - if subkpi_val is AccountingNone - else subkpi_val), - 'val_r': kpi.render(lang_id, subkpi_val), - 'val_c': comment, - 'expr': expression, - 'drilldown': drilldown, - }) - res[kpi_name].append(vals) - return res - class MisReportInstance(models.Model): """The MIS report instance combines everything to compute @@ -1380,112 +1277,3 @@ class MisReportInstance(models.Model): 'header': header, 'content': content, } - - @api.multi - def old_compute(self): - self.ensure_one() - aep = self.report_id._prepare_aep(self.company_id) - - # fetch user language only once - # TODO: is this necessary? - lang = self.env.user.lang - if not lang: - lang = 'en_US' - lang_id = self.env['res.lang'].search([('code', '=', lang)]).id - - # compute kpi values for each period - kpi_values_by_period_ids = {} - kpi_matrix = KpiMatrix(lang_id) - for period in self.period_ids: - if not period.valid: - continue - kpi_values = period._render_period(kpi_matrix, lang_id, aep) - kpi_values_by_period_ids[period.id] = kpi_values - kpi_matrix.load_account_names(self.env['account.account']) - - # prepare header and content - header = [{ - 'kpi_name': '', - 'cols': [] - }, { - 'kpi_name': '', - 'cols': [] - }] - content = [] - rows_by_kpi_name = {} - for kpi_name, kpi_description, kpi in kpi_matrix.iter_kpis(): - props = { - 'kpi_name': kpi_description, - 'cols': [], - 'default_style': '', - 'default_xlsx_style': {}, - } - rows_by_kpi_name[kpi_name] = props - if kpi.style: - props.update({ - 'default_style': kpi.style.to_css_style(), - 'default_xlsx_style': kpi.style.to_xlsx_format_properties() - }) - - content.append(rows_by_kpi_name[kpi_name]) - - # populate header and content - for period in self.period_ids: - if not period.valid: - continue - # add the column header - if period.duration > 1 or period.type in ('w', 'date_range'): - # from, to - date_from = self._format_date(lang_id, period.date_from) - date_to = self._format_date(lang_id, period.date_to) - header_date = _('from %s to %s') % (date_from, date_to) - else: - header_date = self._format_date(lang_id, period.date_from) - subkpis = period.subkpi_ids or \ - period.report_instance_id.report_id.subkpi_ids - header[0]['cols'].append(dict( - name=period.name, - date=header_date, - colspan=len(subkpis) or 1, - )) - if subkpis: - for subkpi in subkpis: - header[1]['cols'].append(dict( - name=subkpi.description, - colspan=1, - )) - else: - header[1]['cols'].append(dict( - name="", - colspan=1, - )) - # add kpi values - kpi_values = kpi_values_by_period_ids[period.id] - for kpi_name in kpi_values: - rows_by_kpi_name[kpi_name]['cols'] += kpi_values[kpi_name] - - # add comparison columns - for compare_col in period.comparison_column_ids: - compare_kpi_values = \ - kpi_values_by_period_ids.get(compare_col.id) - if compare_kpi_values: - # add the comparison column header - header[0]['cols'].append( - dict(name=_('%s vs %s') % (period.name, - compare_col.name), - date='')) - # add comparison values - for kpi in self.report_id.kpi_ids: - rows_by_kpi_name[kpi.name]['cols'].append({ - 'val_r': kpi.render_comparison( - lang_id, - kpi_values[kpi.name]['val'], - compare_kpi_values[kpi.name]['val'], - period.normalize_factor, - compare_col.normalize_factor) - }) - return { - 'report_name': self.name, - 'header': header, - 'content': content, - } From 3461d123d3d1cda371cd337711ffbcb1511723b1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 10 May 2016 18:30:45 +0200 Subject: [PATCH 120/182] [WIP] mis_builder refactoring: restore and improve comparison columns --- mis_builder/models/mis_builder.py | 166 ++++++++++++++++++++++++------ 1 file changed, 134 insertions(+), 32 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index d5b0ff3a..5a179be9 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -2,7 +2,7 @@ # © 2014-2016 ACSONE SA/NV () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from collections import OrderedDict +from collections import defaultdict, OrderedDict import datetime import dateutil from itertools import izip @@ -12,13 +12,13 @@ import time import pytz -from openerp import api, exceptions, fields, models, _ +from openerp import api, fields, models, _ +from openerp.exceptions import UserError from openerp.tools.safe_eval import safe_eval from .aep import AccountingExpressionProcessor as AEP from .aggregate import _sum, _avg, _min, _max from .accounting_none import AccountingNone -from openerp.exceptions import UserError from .simple_array import SimpleArray from .mis_safe_eval import mis_safe_eval, DataError @@ -72,6 +72,7 @@ class KpiMatrixCol(object): self.locals_dict = locals_dict self.colspan = subkpis and len(subkpis) or 1 self._subcols = [] + self.subkpis = subkpis if not subkpis: subcol = KpiMatrixSubCol(self, '', '', 0) self._subcols.append(subcol) @@ -102,6 +103,10 @@ class KpiMatrixSubCol(object): self.comment = comment self.index = index + @property + def subkpi(self): + return self.col.subkpis[self.index] + def iter_cells(self): for cells in self.col.iter_cell_tuples(): yield cells[self.index] @@ -133,26 +138,58 @@ class KpiMatrix(object): lang_model = env['res.lang'] lang_id = lang_model._lang_get(env.user.lang) self.lang = lang_model.browse(lang_id) - # data structures - self._kpi_rows = OrderedDict() # { kpi: KpiMatrixRow } - self._detail_rows = {} # { kpi: {account_id: KpiMatrixRow} } - self._cols = OrderedDict() # { period_key: KpiMatrixCol } self._account_model = env['account.account'] - self._account_names = {} # { account_id: account_name } + # data structures + # { kpi: KpiMatrixRow } + self._kpi_rows = OrderedDict() + # { kpi: {account_id: KpiMatrixRow} } + self._detail_rows = {} + # { period_key: KpiMatrixCol } + self._cols = OrderedDict() + # { period_key (left of comparison): [(period_key, base_period_key)] } + self._comparison_todo = defaultdict(list) + self._comparison_cols = defaultdict(list) + # { account_id: account_name } + self._account_names = {} def declare_kpi(self, kpi): + """ Declare a new kpi (row) in the matrix. + + Invoke this first for all kpi, in display order. + """ self._kpi_rows[kpi] = KpiMatrixRow(self, kpi) self._detail_rows[kpi] = {} def declare_period(self, period_key, description, comment, locals_dict, subkpis): + """ Declare a new period (column), giving it an identifier (key). + + Invoke this and declare_comparison in display order. + """ self._cols[period_key] = KpiMatrixCol(description, comment, locals_dict, subkpis) + def declare_comparison(self, period_key, base_period_key): + """ Declare a new comparison column. + + Invoke this and declare_period in display order. + """ + last_period_key = list(self._cols.keys())[-1] + self._comparison_todo[last_period_key].append( + (period_key, base_period_key)) + def set_values(self, kpi, period_key, vals): + """ Set values for a kpi and a period. + + Invoke this after declaring the kpi and the period. + """ self.set_values_detail_account(kpi, period_key, None, vals) def set_values_detail_account(self, kpi, period_key, account_id, vals): + """ Set values for a kpi and a period and a detail account. + + Invoke this after declaring the kpi and the period. + """ if not account_id: row = self._kpi_rows[kpi] else: @@ -178,7 +215,57 @@ class KpiMatrix(object): cell_tuple.append(cell) col._set_cell_tuple(row, cell_tuple) + def compute_comparisons(self): + """ Compute comparisons. + + Invoke this after setting all values. + """ + for pos_period_key, comparisons in self._comparison_todo.items(): + for period_key, base_period_key in comparisons: + col = self._cols[period_key] + base_col = self._cols[base_period_key] + common_subkpis = set(col.subkpis) & set(base_col.subkpis) + if not common_subkpis: + raise UserError('Columns {} and {} are not comparable'. + format(col.description, + base_col.description)) + description = u'{} vs {}'.\ + format(col.description, base_col.description) + comparison_col = KpiMatrixCol(description, None, + {}, col.subkpis) + for row in self.iter_rows(): + cell_tuple = col.get_cell_tuple_for_row(row) + base_cell_tuple = base_col.get_cell_tuple_for_row(row) + if cell_tuple is None and base_cell_tuple is None: + continue + if cell_tuple is None: + vals = [AccountingNone] * len(common_subkpis) + else: + vals = [cell.val for cell in cell_tuple + if cell.subcol.subkpi in common_subkpis] + if base_cell_tuple is None: + base_vals = [AccountingNone] * len(common_subkpis) + else: + base_vals = [cell.val for cell in base_cell_tuple + if cell.subcol.subkpi in common_subkpis] + comparison_cell_tuple = [] + for val, base_val, comparison_subcol in \ + izip(vals, + base_vals, + comparison_col.iter_subcols()): + # TODO FIXME average factors + delta, delta_r = row.kpi.compare_and_render( + self.lang, val, base_val, 1, 1) + comparison_cell_tuple.append(KpiMatrixCell( + row, comparison_subcol, delta, delta_r, None)) + comparison_col._set_cell_tuple(row, comparison_cell_tuple) + self._comparison_cols[pos_period_key].append(comparison_col) + def iter_rows(self): + """ Iterate rows in display order. + + yields KpiMatrixRow. + """ for kpi_row in self._kpi_rows.values(): yield kpi_row detail_rows = self._detail_rows[kpi_row.kpi].values() @@ -187,9 +274,21 @@ class KpiMatrix(object): yield detail_row def iter_cols(self): - return self._cols.values() + """ Iterate columns in display order. + + yields KpiMatrixCol: one for each period or comparison. + """ + for period_key, col in self._cols.items(): + yield col + for comparison_col in self._comparison_cols[period_key]: + yield comparison_col def iter_subcols(self): + """ Iterate sub columns in display order. + + yields KpiMatrixSubCol: one for each subkpi in each period + and comparison. + """ for col in self.iter_cols(): for subcol in col.iter_subcols(): yield subcol @@ -297,8 +396,7 @@ class MisReportKpi(models.Model): @api.constrains('name') def _check_name(self): if not _is_valid_python_var(self.name): - raise exceptions.Warning(_('The name must be a valid ' - 'python identifier')) + raise UserError(_('The name must be a valid python identifier')) @api.onchange('name') def _onchange_name(self): @@ -393,10 +491,12 @@ class MisReportKpi(models.Model): else: return unicode(value) - def render_comparison(self, lang, value, base_value, - average_value, average_base_value): + def compare_and_render(self, lang, value, base_value, + average_value, average_base_value): """ render the comparison of two KPI values, ready for display + Returns a tuple, with the numeric comparison and its string rendering. + If the difference is 0, an empty string is returned. """ assert len(self) == 1 @@ -407,7 +507,7 @@ class MisReportKpi(models.Model): if self.type == 'pct': delta = value - base_value if delta and round(delta, self.dp) != 0: - return self._render_num( + return delta, self._render_num( lang, delta, 0.01, self.dp, '', _('pp'), @@ -420,7 +520,7 @@ class MisReportKpi(models.Model): if self.compare_method == 'diff': delta = value - base_value if delta and round(delta, self.dp) != 0: - return self._render_num( + return delta, self._render_num( lang, delta, self.divider, self.dp, self.prefix, self.suffix, @@ -429,12 +529,12 @@ class MisReportKpi(models.Model): if base_value and round(base_value, self.dp) != 0: delta = (value - base_value) / abs(base_value) if delta and round(delta, self.dp) != 0: - return self._render_num( + return delta, self._render_num( lang, delta, 0.01, self.dp, '', '%', sign='+') - return '' + return 0, '' def _render_num(self, lang, value, divider, dp, prefix, suffix, sign='-'): @@ -471,8 +571,7 @@ class MisReportSubkpi(models.Model): @api.constrains('name') def _check_name(self): if not _is_valid_python_var(self.name): - raise exceptions.Warning(_('The name must be a valid ' - 'python identifier')) + raise UserError(_('The name must be a valid python identifier')) @api.onchange('name') def _onchange_name(self): @@ -564,8 +663,7 @@ class MisReportQuery(models.Model): @api.constrains('name') def _check_name(self): if not _is_valid_python_var(self.name): - raise exceptions.Warning(_('The name must be a valid ' - 'python identifier')) + raise UserError(_('The name must be a valid python identifier')) class MisReport(models.Model): @@ -723,15 +821,17 @@ class MisReport(models.Model): return res @api.multi - def _compute_period(self, kpi_matrix, - period_key, period_description, period_comment, - aep, - date_from, date_to, - target_move, - company, - subkpis_filter=None, - get_additional_move_line_filter=None, - get_additional_query_filter=None): + def _declare_and_compute_period(self, kpi_matrix, + period_key, + period_description, + period_comment, + aep, + date_from, date_to, + target_move, + company, + subkpis_filter=None, + get_additional_move_line_filter=None, + get_additional_query_filter=None): """ Evaluate a report for a given period, populating a KpiMatrix. :param kpi_matrix: the KpiMatrix object to be populated @@ -1220,7 +1320,7 @@ class MisReportInstance(models.Model): date_from = self._format_date(period.date_from) date_to = self._format_date(period.date_to) comment = _('from %s to %s') % (date_from, date_to) - self.report_id._compute_period( + self.report_id._declare_and_compute_period( kpi_matrix, period.id, period.name, @@ -1233,7 +1333,9 @@ class MisReportInstance(models.Model): period.subkpi_ids, period._get_additional_move_line_filter, period._get_additional_query_filter) - # TODO FIXME comparison columns + for comparison_column in period.comparison_column_ids: + kpi_matrix.declare_comparison(period.id, comparison_column.id) + kpi_matrix.compute_comparisons() header = [{'cols': []}, {'cols': []}] for col in kpi_matrix.iter_cols(): From 83d943be5ff54bc65c455ef987b6aa203c8777bd Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 10 May 2016 18:40:02 +0200 Subject: [PATCH 121/182] [WIP] mis_builder refactoring: move the "json" conversion to the matrix mis.report.instance.compute() is now as simple as possible, and rest is nicely factored in manageable methods \o/ --- mis_builder/models/mis_builder.py | 96 ++++++++++++++++--------------- 1 file changed, 51 insertions(+), 45 deletions(-) diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_builder.py index 5a179be9..42f8ae5e 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_builder.py @@ -55,11 +55,15 @@ class KpiMatrixRow(object): else: return None # TODO style for expanded accounts - def iter_cell_tuples(self, cols): + def iter_cell_tuples(self, cols=None): + if cols is None: + cols = self._matrix.iter_cols() for col in cols: yield col.get_cell_tuple_for_row(self) - def iter_cells(self, subcols): + def iter_cells(self, subcols=None): + if subcols is None: + subcols = self._matrix.iter_subcols() for subcol in subcols: yield subcol.get_cell_for_row(self) @@ -307,6 +311,50 @@ class KpiMatrix(object): self._load_account_names() return self._account_names[account_id] + def as_dict(self): + header = [{'cols': []}, {'cols': []}] + for col in self.iter_cols(): + header[0]['cols'].append({ + 'description': col.description, + 'comment': col.comment, + 'colspan': col.colspan, + }) + for subcol in col.iter_subcols(): + header[1]['cols'].append({ + 'description': subcol.description, + 'comment': subcol.comment, + 'colspan': 1, + }) + + content = [] + for row in self.iter_rows(): + row_data = { + 'row_id': id(row), + 'parent_row_id': row.parent_row and id(row.parent_row) or None, + 'description': row.description, + 'comment': row.comment, + 'style': row.style and row.style.to_css_style() or '', + 'cols': [] + } + for cell in row.iter_cells(): + if cell is None: + row_data['cols'].append({}) + else: + row_data['cols'].append({ + 'val': (cell.val + if cell.val is not AccountingNone else None), + 'val_r': cell.val_rendered, + 'val_c': cell.val_comment, + # TODO FIXME style + # TODO FIXME drilldown + }) + content.append(row_data) + + return { + 'header': header, + 'content': content, + } + def _get_selection_label(selection, value): for v, l in selection: @@ -1336,46 +1384,4 @@ class MisReportInstance(models.Model): for comparison_column in period.comparison_column_ids: kpi_matrix.declare_comparison(period.id, comparison_column.id) kpi_matrix.compute_comparisons() - - header = [{'cols': []}, {'cols': []}] - for col in kpi_matrix.iter_cols(): - header[0]['cols'].append({ - 'description': col.description, - 'comment': col.comment, - 'colspan': col.colspan, - }) - for subcol in col.iter_subcols(): - header[1]['cols'].append({ - 'description': subcol.description, - 'comment': subcol.comment, - 'colspan': 1, - }) - - content = [] - for row in kpi_matrix.iter_rows(): - row_data = { - 'row_id': id(row), - 'parent_row_id': row.parent_row and id(row.parent_row) or None, - 'description': row.description, - 'comment': row.comment, - 'style': row.style and row.style.to_css_style() or '', - 'cols': [] - } - for cell in row.iter_cells(kpi_matrix.iter_subcols()): - if cell is None: - row_data['cols'].append({}) - else: - row_data['cols'].append({ - 'val': (cell.val - if cell.val is not AccountingNone else None), - 'val_r': cell.val_rendered, - 'val_c': cell.val_comment, - # TODO FIXME style - # TODO FIXME drilldown - }) - content.append(row_data) - - return { - 'header': header, - 'content': content, - } + return kpi_matrix.as_dict() From 8324ef2e019c27c495b24de52e30c5f54dbe9cb8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 10 May 2016 18:47:31 +0200 Subject: [PATCH 122/182] [WIP] mis_builder refactoring: split mis_builder.py in two mis_report.py has the heavy stuff, and contains all the computation and rendering code --- mis_builder/models/__init__.py | 3 +- .../models/{mis_builder.py => mis_report.py} | 398 ----------------- mis_builder/models/mis_report_instance.py | 410 ++++++++++++++++++ 3 files changed, 412 insertions(+), 399 deletions(-) rename mis_builder/models/{mis_builder.py => mis_report.py} (70%) create mode 100644 mis_builder/models/mis_report_instance.py diff --git a/mis_builder/models/__init__.py b/mis_builder/models/__init__.py index 90f38748..2c6b63fd 100644 --- a/mis_builder/models/__init__.py +++ b/mis_builder/models/__init__.py @@ -2,6 +2,7 @@ # © 2014-2015 ACSONE SA/NV () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -from . import mis_builder +from . import mis_report +from . import mis_report_instance from . import mis_builder_style from . import aep diff --git a/mis_builder/models/mis_builder.py b/mis_builder/models/mis_report.py similarity index 70% rename from mis_builder/models/mis_builder.py rename to mis_builder/models/mis_report.py index 42f8ae5e..3f93f945 100644 --- a/mis_builder/models/mis_builder.py +++ b/mis_builder/models/mis_report.py @@ -987,401 +987,3 @@ class MisReport(models.Model): # try again compute_queue = recompute_queue recompute_queue = [] - - -class MisReportInstancePeriod(models.Model): - """ A MIS report instance has the logic to compute - a report template for a given date period. - - Periods have a duration (day, week, fiscal period) and - are defined as an offset relative to a pivot date. - """ - - @api.one - @api.depends('report_instance_id.pivot_date', - 'report_instance_id.comparison_mode', - 'type', 'offset', 'duration', 'mode') - def _compute_dates(self): - self.date_from = False - self.date_to = False - self.valid = False - report = self.report_instance_id - d = fields.Date.from_string(report.pivot_date) - if not report.comparison_mode: - self.date_from = report.date_from - self.date_to = report.date_to - self.valid = True - elif self.mode == 'fix': - self.date_from = self.manual_date_from - self.date_to = self.manual_date_to - self.valid = True - elif self.type == 'd': - date_from = d + datetime.timedelta(days=self.offset) - date_to = date_from + \ - datetime.timedelta(days=self.duration - 1) - self.date_from = fields.Date.to_string(date_from) - self.date_to = fields.Date.to_string(date_to) - self.valid = True - elif self.type == 'w': - date_from = d - datetime.timedelta(d.weekday()) - date_from = date_from + datetime.timedelta(days=self.offset * 7) - date_to = date_from + \ - datetime.timedelta(days=(7 * self.duration) - 1) - self.date_from = fields.Date.to_string(date_from) - self.date_to = fields.Date.to_string(date_to) - self.valid = True - elif self.type == 'date_range': - date_range_obj = self.env['date.range'] - current_periods = date_range_obj.search( - [('type_id', '=', self.date_range_type_id.id), - ('date_start', '<=', d), - ('date_end', '>=', d), - ('company_id', '=', self.report_instance_id.company_id.id)]) - if current_periods: - all_periods = date_range_obj.search( - [('type_id', '=', self.date_range_type_id.id), - ('company_id', '=', - self.report_instance_id.company_id.id)], - order='date_start') - all_period_ids = [p.id for p in all_periods] - p = all_period_ids.index(current_periods[0].id) + self.offset - if p >= 0 and p + self.duration <= len(all_period_ids): - periods = all_periods[p:p + self.duration] - self.date_from = periods[0].date_start - self.date_to = periods[-1].date_end - self.valid = True - - _name = 'mis.report.instance.period' - - name = fields.Char(size=32, required=True, - string='Description', translate=True) - mode = fields.Selection([('fix', 'Fix'), - ('relative', 'Relative'), - ], required=True, - default='fix') - type = fields.Selection([('d', _('Day')), - ('w', _('Week')), - ('date_range', _('Date Range')) - ], - string='Period type') - date_range_type_id = fields.Many2one( - comodel_name='date.range.type', string='Date Range Type') - offset = fields.Integer(string='Offset', - help='Offset from current period', - default=-1) - duration = fields.Integer(string='Duration', - help='Number of periods', - default=1) - date_from = fields.Date(compute='_compute_dates', string="From") - date_to = fields.Date(compute='_compute_dates', string="To") - manual_date_from = fields.Date(string="From") - manual_date_to = fields.Date(string="To") - date_range_id = fields.Many2one( - comodel_name='date.range', - string='Date Range') - valid = fields.Boolean(compute='_compute_dates', - type='boolean', - string='Valid') - sequence = fields.Integer(string='Sequence', default=100) - report_instance_id = fields.Many2one('mis.report.instance', - string='Report Instance', - ondelete='cascade') - comparison_column_ids = fields.Many2many( - comodel_name='mis.report.instance.period', - relation='mis_report_instance_period_rel', - column1='period_id', - column2='compare_period_id', - string='Compare with') - normalize_factor = fields.Integer( - string='Factor', - help='Factor to use to normalize the period (used in comparison', - default=1) - subkpi_ids = fields.Many2many( - 'mis.report.subkpi', - string="Sub KPI Filter") - - _order = 'sequence, id' - - _sql_constraints = [ - ('duration', 'CHECK (duration>0)', - 'Wrong duration, it must be positive!'), - ('normalize_factor', 'CHECK (normalize_factor>0)', - 'Wrong normalize factor, it must be positive!'), - ('name_unique', 'unique(name, report_instance_id)', - 'Period name should be unique by report'), - ] - - @api.onchange('date_range_id') - def onchange_date_range(self): - for record in self: - record.manual_date_from = record.date_range_id.date_start - record.manual_date_to = record.date_range_id.date_end - record.name = record.date_range_id.name - - @api.multi - def _get_additional_move_line_filter(self): - """ Prepare a filter to apply on all move lines - - This filter is applied with a AND operator on all - accounting expression domains. This hook is intended - to be inherited, and is useful to implement filtering - on analytic dimensions or operational units. - - Returns an Odoo domain expression (a python list) - compatible with account.move.line.""" - self.ensure_one() - return [] - - @api.multi - def _get_additional_query_filter(self, query): - """ Prepare an additional filter to apply on the query - - This filter is combined to the query domain with a AND - operator. This hook is intended - to be inherited, and is useful to implement filtering - on analytic dimensions or operational units. - - Returns an Odoo domain expression (a python list) - compatible with the model of the query.""" - self.ensure_one() - return [] - - @api.multi - def drilldown(self, expr): - self.ensure_one() - # TODO FIXME: drilldown by account - if AEP.has_account_var(expr): - aep = AEP(self.env) - aep.parse_expr(expr) - aep.done_parsing(self.report_instance_id.company_id) - domain = aep.get_aml_domain_for_expr( - expr, - self.date_from, self.date_to, - self.report_instance_id.target_move, - self.report_instance_id.company_id) - domain.extend(self._get_additional_move_line_filter()) - return { - 'name': expr + ' - ' + self.name, - 'domain': domain, - 'type': 'ir.actions.act_window', - 'res_model': 'account.move.line', - 'views': [[False, 'list'], [False, 'form']], - 'view_type': 'list', - 'view_mode': 'list', - 'target': 'current', - } - else: - return False - - -class MisReportInstance(models.Model): - """The MIS report instance combines everything to compute - a MIS report template for a set of periods.""" - - @api.one - @api.depends('date') - def _compute_pivot_date(self): - if self.date: - self.pivot_date = self.date - else: - self.pivot_date = fields.Date.context_today(self) - - @api.model - def _default_company(self): - return self.env['res.company'].\ - _company_default_get('mis.report.instance') - - _name = 'mis.report.instance' - - name = fields.Char(required=True, - string='Name', translate=True) - description = fields.Char(related='report_id.description', - readonly=True) - date = fields.Date(string='Base date', - help='Report base date ' - '(leave empty to use current date)') - pivot_date = fields.Date(compute='_compute_pivot_date', - string="Pivot date") - report_id = fields.Many2one('mis.report', - required=True, - string='Report') - period_ids = fields.One2many('mis.report.instance.period', - 'report_instance_id', - required=True, - string='Periods', - copy=True) - target_move = fields.Selection([('posted', 'All Posted Entries'), - ('all', 'All Entries')], - string='Target Moves', - required=True, - default='posted') - company_id = fields.Many2one(comodel_name='res.company', - string='Company', - default=_default_company, - required=True) - landscape_pdf = fields.Boolean(string='Landscape PDF') - comparison_mode = fields.Boolean( - compute="_compute_comparison_mode", - inverse="_inverse_comparison_mode") - date_range_id = fields.Many2one( - comodel_name='date.range', - string='Date Range') - date_from = fields.Date(string="From") - date_to = fields.Date(string="To") - temporary = fields.Boolean(default=False) - - @api.multi - def save_report(self): - self.ensure_one() - self.write({'temporary': False}) - action = self.env.ref('mis_builder.mis_report_instance_view_action') - res = action.read()[0] - view = self.env.ref('mis_builder.mis_report_instance_view_form') - res.update({ - 'views': [(view.id, 'form')], - 'res_id': self.id, - }) - return res - - @api.model - def _vacuum_report(self, hours=24): - clear_date = fields.Datetime.to_string( - datetime.datetime.now() - datetime.timedelta(hours=hours)) - reports = self.search([ - ('write_date', '<', clear_date), - ('temporary', '=', True), - ]) - _logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports)) - return reports.unlink() - - @api.one - def copy(self, default=None): - default = dict(default or {}) - default['name'] = _('%s (copy)') % self.name - return super(MisReportInstance, self).copy(default) - - def _format_date(self, date): - # format date following user language - lang_model = self.env['res.lang'] - lang_id = lang_model._lang_get(self.env.user.lang) - date_format = lang_model.browse(lang_id).date_format - return datetime.datetime.strftime( - fields.Date.from_string(date), date_format) - - @api.multi - @api.depends('date_from') - def _compute_comparison_mode(self): - for instance in self: - instance.comparison_mode = bool(instance.period_ids) and\ - not bool(instance.date_from) - - @api.multi - def _inverse_comparison_mode(self): - for record in self: - if not record.comparison_mode: - if not record.date_from: - record.date_from = datetime.now() - if not record.date_to: - record.date_to = datetime.now() - record.period_ids.unlink() - record.write({'period_ids': [ - (0, 0, { - 'name': 'Default', - 'type': 'd', - }) - ]}) - else: - record.date_from = None - record.date_to = None - - @api.onchange('date_range_id') - def onchange_date_range(self): - for record in self: - record.date_from = record.date_range_id.date_start - record.date_to = record.date_range_id.date_end - - @api.multi - def preview(self): - assert len(self) == 1 - view_id = self.env.ref('mis_builder.' - 'mis_report_instance_result_view_form') - return { - 'type': 'ir.actions.act_window', - 'res_model': 'mis.report.instance', - 'res_id': self.id, - 'view_mode': 'form', - 'view_type': 'form', - 'view_id': view_id.id, - 'target': 'current', - } - - @api.multi - def print_pdf(self): - self.ensure_one() - return { - 'name': 'MIS report instance QWEB PDF report', - 'model': 'mis.report.instance', - 'type': 'ir.actions.report.xml', - 'report_name': 'mis_builder.report_mis_report_instance', - 'report_type': 'qweb-pdf', - 'context': self.env.context, - } - - @api.multi - def export_xls(self): - self.ensure_one() - return { - 'name': 'MIS report instance XLSX report', - 'model': 'mis.report.instance', - 'type': 'ir.actions.report.xml', - 'report_name': 'mis.report.instance.xlsx', - 'report_type': 'xlsx', - 'context': self.env.context, - } - - @api.multi - def display_settings(self): - assert len(self.ids) <= 1 - view_id = self.env.ref('mis_builder.mis_report_instance_view_form') - return { - 'type': 'ir.actions.act_window', - 'res_model': 'mis.report.instance', - 'res_id': self.id if self.id else False, - 'view_mode': 'form', - 'view_type': 'form', - 'views': [(view_id.id, 'form')], - 'view_id': view_id.id, - 'target': 'current', - } - - @api.multi - def compute(self): - self.ensure_one() - aep = self.report_id._prepare_aep(self.company_id) - kpi_matrix = self.report_id._prepare_kpi_matrix() - for period in self.period_ids: - # add the column header - if period.date_from == period.date_to: - comment = self._format_date(period.date_from) - else: - # from, to - date_from = self._format_date(period.date_from) - date_to = self._format_date(period.date_to) - comment = _('from %s to %s') % (date_from, date_to) - self.report_id._declare_and_compute_period( - kpi_matrix, - period.id, - period.name, - comment, - aep, - period.date_from, - period.date_to, - self.target_move, - self.company_id, - period.subkpi_ids, - period._get_additional_move_line_filter, - period._get_additional_query_filter) - for comparison_column in period.comparison_column_ids: - kpi_matrix.declare_comparison(period.id, comparison_column.id) - kpi_matrix.compute_comparisons() - return kpi_matrix.as_dict() diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py new file mode 100644 index 00000000..d905f8c1 --- /dev/null +++ b/mis_builder/models/mis_report_instance.py @@ -0,0 +1,410 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from openerp import api, fields, models, _ + +import datetime +import logging + +from .aep import AccountingExpressionProcessor as AEP + +_logger = logging.getLogger(__name__) + + +class MisReportInstancePeriod(models.Model): + """ A MIS report instance has the logic to compute + a report template for a given date period. + + Periods have a duration (day, week, fiscal period) and + are defined as an offset relative to a pivot date. + """ + + @api.one + @api.depends('report_instance_id.pivot_date', + 'report_instance_id.comparison_mode', + 'type', 'offset', 'duration', 'mode') + def _compute_dates(self): + self.date_from = False + self.date_to = False + self.valid = False + report = self.report_instance_id + d = fields.Date.from_string(report.pivot_date) + if not report.comparison_mode: + self.date_from = report.date_from + self.date_to = report.date_to + self.valid = True + elif self.mode == 'fix': + self.date_from = self.manual_date_from + self.date_to = self.manual_date_to + self.valid = True + elif self.type == 'd': + date_from = d + datetime.timedelta(days=self.offset) + date_to = date_from + \ + datetime.timedelta(days=self.duration - 1) + self.date_from = fields.Date.to_string(date_from) + self.date_to = fields.Date.to_string(date_to) + self.valid = True + elif self.type == 'w': + date_from = d - datetime.timedelta(d.weekday()) + date_from = date_from + datetime.timedelta(days=self.offset * 7) + date_to = date_from + \ + datetime.timedelta(days=(7 * self.duration) - 1) + self.date_from = fields.Date.to_string(date_from) + self.date_to = fields.Date.to_string(date_to) + self.valid = True + elif self.type == 'date_range': + date_range_obj = self.env['date.range'] + current_periods = date_range_obj.search( + [('type_id', '=', self.date_range_type_id.id), + ('date_start', '<=', d), + ('date_end', '>=', d), + ('company_id', '=', self.report_instance_id.company_id.id)]) + if current_periods: + all_periods = date_range_obj.search( + [('type_id', '=', self.date_range_type_id.id), + ('company_id', '=', + self.report_instance_id.company_id.id)], + order='date_start') + all_period_ids = [p.id for p in all_periods] + p = all_period_ids.index(current_periods[0].id) + self.offset + if p >= 0 and p + self.duration <= len(all_period_ids): + periods = all_periods[p:p + self.duration] + self.date_from = periods[0].date_start + self.date_to = periods[-1].date_end + self.valid = True + + _name = 'mis.report.instance.period' + + name = fields.Char(size=32, required=True, + string='Description', translate=True) + mode = fields.Selection([('fix', 'Fix'), + ('relative', 'Relative'), + ], required=True, + default='fix') + type = fields.Selection([('d', _('Day')), + ('w', _('Week')), + ('date_range', _('Date Range')) + ], + string='Period type') + date_range_type_id = fields.Many2one( + comodel_name='date.range.type', string='Date Range Type') + offset = fields.Integer(string='Offset', + help='Offset from current period', + default=-1) + duration = fields.Integer(string='Duration', + help='Number of periods', + default=1) + date_from = fields.Date(compute='_compute_dates', string="From") + date_to = fields.Date(compute='_compute_dates', string="To") + manual_date_from = fields.Date(string="From") + manual_date_to = fields.Date(string="To") + date_range_id = fields.Many2one( + comodel_name='date.range', + string='Date Range') + valid = fields.Boolean(compute='_compute_dates', + type='boolean', + string='Valid') + sequence = fields.Integer(string='Sequence', default=100) + report_instance_id = fields.Many2one('mis.report.instance', + string='Report Instance', + ondelete='cascade') + comparison_column_ids = fields.Many2many( + comodel_name='mis.report.instance.period', + relation='mis_report_instance_period_rel', + column1='period_id', + column2='compare_period_id', + string='Compare with') + normalize_factor = fields.Integer( + string='Factor', + help='Factor to use to normalize the period (used in comparison', + default=1) + subkpi_ids = fields.Many2many( + 'mis.report.subkpi', + string="Sub KPI Filter") + + _order = 'sequence, id' + + _sql_constraints = [ + ('duration', 'CHECK (duration>0)', + 'Wrong duration, it must be positive!'), + ('normalize_factor', 'CHECK (normalize_factor>0)', + 'Wrong normalize factor, it must be positive!'), + ('name_unique', 'unique(name, report_instance_id)', + 'Period name should be unique by report'), + ] + + @api.onchange('date_range_id') + def onchange_date_range(self): + for record in self: + record.manual_date_from = record.date_range_id.date_start + record.manual_date_to = record.date_range_id.date_end + record.name = record.date_range_id.name + + @api.multi + def _get_additional_move_line_filter(self): + """ Prepare a filter to apply on all move lines + + This filter is applied with a AND operator on all + accounting expression domains. This hook is intended + to be inherited, and is useful to implement filtering + on analytic dimensions or operational units. + + Returns an Odoo domain expression (a python list) + compatible with account.move.line.""" + self.ensure_one() + return [] + + @api.multi + def _get_additional_query_filter(self, query): + """ Prepare an additional filter to apply on the query + + This filter is combined to the query domain with a AND + operator. This hook is intended + to be inherited, and is useful to implement filtering + on analytic dimensions or operational units. + + Returns an Odoo domain expression (a python list) + compatible with the model of the query.""" + self.ensure_one() + return [] + + @api.multi + def drilldown(self, expr): + self.ensure_one() + # TODO FIXME: drilldown by account + if AEP.has_account_var(expr): + aep = AEP(self.env) + aep.parse_expr(expr) + aep.done_parsing(self.report_instance_id.company_id) + domain = aep.get_aml_domain_for_expr( + expr, + self.date_from, self.date_to, + self.report_instance_id.target_move, + self.report_instance_id.company_id) + domain.extend(self._get_additional_move_line_filter()) + return { + 'name': expr + ' - ' + self.name, + 'domain': domain, + 'type': 'ir.actions.act_window', + 'res_model': 'account.move.line', + 'views': [[False, 'list'], [False, 'form']], + 'view_type': 'list', + 'view_mode': 'list', + 'target': 'current', + } + else: + return False + + +class MisReportInstance(models.Model): + """The MIS report instance combines everything to compute + a MIS report template for a set of periods.""" + + @api.one + @api.depends('date') + def _compute_pivot_date(self): + if self.date: + self.pivot_date = self.date + else: + self.pivot_date = fields.Date.context_today(self) + + @api.model + def _default_company(self): + return self.env['res.company'].\ + _company_default_get('mis.report.instance') + + _name = 'mis.report.instance' + + name = fields.Char(required=True, + string='Name', translate=True) + description = fields.Char(related='report_id.description', + readonly=True) + date = fields.Date(string='Base date', + help='Report base date ' + '(leave empty to use current date)') + pivot_date = fields.Date(compute='_compute_pivot_date', + string="Pivot date") + report_id = fields.Many2one('mis.report', + required=True, + string='Report') + period_ids = fields.One2many('mis.report.instance.period', + 'report_instance_id', + required=True, + string='Periods', + copy=True) + target_move = fields.Selection([('posted', 'All Posted Entries'), + ('all', 'All Entries')], + string='Target Moves', + required=True, + default='posted') + company_id = fields.Many2one(comodel_name='res.company', + string='Company', + default=_default_company, + required=True) + landscape_pdf = fields.Boolean(string='Landscape PDF') + comparison_mode = fields.Boolean( + compute="_compute_comparison_mode", + inverse="_inverse_comparison_mode") + date_range_id = fields.Many2one( + comodel_name='date.range', + string='Date Range') + date_from = fields.Date(string="From") + date_to = fields.Date(string="To") + temporary = fields.Boolean(default=False) + + @api.multi + def save_report(self): + self.ensure_one() + self.write({'temporary': False}) + action = self.env.ref('mis_builder.mis_report_instance_view_action') + res = action.read()[0] + view = self.env.ref('mis_builder.mis_report_instance_view_form') + res.update({ + 'views': [(view.id, 'form')], + 'res_id': self.id, + }) + return res + + @api.model + def _vacuum_report(self, hours=24): + clear_date = fields.Datetime.to_string( + datetime.datetime.now() - datetime.timedelta(hours=hours)) + reports = self.search([ + ('write_date', '<', clear_date), + ('temporary', '=', True), + ]) + _logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports)) + return reports.unlink() + + @api.one + def copy(self, default=None): + default = dict(default or {}) + default['name'] = _('%s (copy)') % self.name + return super(MisReportInstance, self).copy(default) + + def _format_date(self, date): + # format date following user language + lang_model = self.env['res.lang'] + lang_id = lang_model._lang_get(self.env.user.lang) + date_format = lang_model.browse(lang_id).date_format + return datetime.datetime.strftime( + fields.Date.from_string(date), date_format) + + @api.multi + @api.depends('date_from') + def _compute_comparison_mode(self): + for instance in self: + instance.comparison_mode = bool(instance.period_ids) and\ + not bool(instance.date_from) + + @api.multi + def _inverse_comparison_mode(self): + for record in self: + if not record.comparison_mode: + if not record.date_from: + record.date_from = datetime.now() + if not record.date_to: + record.date_to = datetime.now() + record.period_ids.unlink() + record.write({'period_ids': [ + (0, 0, { + 'name': 'Default', + 'type': 'd', + }) + ]}) + else: + record.date_from = None + record.date_to = None + + @api.onchange('date_range_id') + def onchange_date_range(self): + for record in self: + record.date_from = record.date_range_id.date_start + record.date_to = record.date_range_id.date_end + + @api.multi + def preview(self): + assert len(self) == 1 + view_id = self.env.ref('mis_builder.' + 'mis_report_instance_result_view_form') + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mis.report.instance', + 'res_id': self.id, + 'view_mode': 'form', + 'view_type': 'form', + 'view_id': view_id.id, + 'target': 'current', + } + + @api.multi + def print_pdf(self): + self.ensure_one() + return { + 'name': 'MIS report instance QWEB PDF report', + 'model': 'mis.report.instance', + 'type': 'ir.actions.report.xml', + 'report_name': 'mis_builder.report_mis_report_instance', + 'report_type': 'qweb-pdf', + 'context': self.env.context, + } + + @api.multi + def export_xls(self): + self.ensure_one() + return { + 'name': 'MIS report instance XLSX report', + 'model': 'mis.report.instance', + 'type': 'ir.actions.report.xml', + 'report_name': 'mis.report.instance.xlsx', + 'report_type': 'xlsx', + 'context': self.env.context, + } + + @api.multi + def display_settings(self): + assert len(self.ids) <= 1 + view_id = self.env.ref('mis_builder.mis_report_instance_view_form') + return { + 'type': 'ir.actions.act_window', + 'res_model': 'mis.report.instance', + 'res_id': self.id if self.id else False, + 'view_mode': 'form', + 'view_type': 'form', + 'views': [(view_id.id, 'form')], + 'view_id': view_id.id, + 'target': 'current', + } + + @api.multi + def compute(self): + self.ensure_one() + aep = self.report_id._prepare_aep(self.company_id) + kpi_matrix = self.report_id._prepare_kpi_matrix() + for period in self.period_ids: + # add the column header + if period.date_from == period.date_to: + comment = self._format_date(period.date_from) + else: + # from, to + date_from = self._format_date(period.date_from) + date_to = self._format_date(period.date_to) + comment = _('from %s to %s') % (date_from, date_to) + self.report_id._declare_and_compute_period( + kpi_matrix, + period.id, + period.name, + comment, + aep, + period.date_from, + period.date_to, + self.target_move, + self.company_id, + period.subkpi_ids, + period._get_additional_move_line_filter, + period._get_additional_query_filter) + for comparison_column in period.comparison_column_ids: + kpi_matrix.declare_comparison(period.id, comparison_column.id) + kpi_matrix.compute_comparisons() + return kpi_matrix.as_dict() From aed8023d4944c6a73fc473181c490842f5a0a29e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 11:11:52 +0200 Subject: [PATCH 123/182] [FIX] mis_builder: a few bug in mis_builder_style --- mis_builder/models/mis_builder_style.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mis_builder/models/mis_builder_style.py b/mis_builder/models/mis_builder_style.py index 2c2db31c..3208623e 100644 --- a/mis_builder/models/mis_builder_style.py +++ b/mis_builder/models/mis_builder_style.py @@ -1,5 +1,6 @@ # -*- coding: utf-8 -*- -# © 2016 Therp BV SA/NV () +# © 2016 Therp BV () +# © 2016 ACSONE SA/NV () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). from openerp import api, fields, models @@ -47,11 +48,9 @@ class MisReportKpiStyle(models.Model): } color = fields.Char( - required=True, help='Line color in valid RGB code (from #000000 to #FFFFFF)', ) background_color = fields.Char( - required=True, help='Line color in valid RGB code (from #000000 to #FFFFFF)' ) font_style = fields.Selection( @@ -92,10 +91,10 @@ class MisReportKpiStyle(models.Model): ('font-size', self.font_size), ('color', self.color), ('background-color', self.background_color), - ('indent-level', str(self.indent_level)) + ('indent-level', self.indent_level) ] css_list = [ - x[0] + ':' + x[1] for x in css_attributes if x[1] + '%s:%s' % x for x in css_attributes if x[1] ] return ';'.join(css_item for css_item in css_list) From 53f27b2edd130112c7bc78856423be75654af38e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 11:15:54 +0200 Subject: [PATCH 124/182] [WIP] mis_builder refactoring: restore tooltips and make them work with subkpis --- mis_builder/models/mis_report.py | 23 +++++++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 3f93f945..d71758df 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -109,11 +109,12 @@ class KpiMatrixSubCol(object): @property def subkpi(self): - return self.col.subkpis[self.index] + if self.col.subkpis: + return self.col.subkpis[self.index] def iter_cells(self): - for cells in self.col.iter_cell_tuples(): - yield cells[self.index] + for cell_tuple in self.col.iter_cell_tuples(): + yield cell_tuple[self.index] def get_cell_for_row(self, row): cell_tuple = self.col.get_cell_tuple_for_row(row) @@ -132,6 +133,7 @@ class KpiMatrixCell(object): self.val = val self.val_rendered = val_rendered self.val_comment = val_comment + self.style = style self.drilldown_key = None @@ -212,7 +214,15 @@ class KpiMatrix(object): val_comment = val.msg else: val_rendered = kpi.render(self.lang, val) - val_comment = '' # TODO FIXME get subkpi expression + if subcol.subkpi: + val_comment = "{}.{} = {}".format( + row.kpi.name, + subcol.subkpi.name, + row.kpi.get_expression_for_subkpi(subcol.subkpi)) + else: + val_comment = "{} = {}".format( + row.kpi.name, + row.kpi.expression) # TODO style # TODO drilldown_key cell = KpiMatrixCell(row, subcol, val, val_rendered, val_comment) @@ -525,6 +535,11 @@ class MisReportKpi(models.Model): self.divider = '' self.dp = 0 + def get_expression_for_subkpi(self, subkpi): + for expression in self.expression_ids: + if expression.subkpi_id == subkpi: + return expression.name + def render(self, lang, value): """ render a KPI value as a unicode string, ready for display """ assert len(self) == 1 From b86c7c2c059e31e67a867dbca0009801b5900130 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 12:54:07 +0200 Subject: [PATCH 125/182] [FIX] mis_builder: remove redundant css --- mis_builder/static/src/css/custom.css | 19 ------------------- 1 file changed, 19 deletions(-) diff --git a/mis_builder/static/src/css/custom.css b/mis_builder/static/src/css/custom.css index 14f4e6b1..1945d5df 100644 --- a/mis_builder/static/src/css/custom.css +++ b/mis_builder/static/src/css/custom.css @@ -12,25 +12,6 @@ text-decoration: underline; } -.openerp .oe_mis_builder_buttons { - padding-bottom: 10px; -} - - -.openerp .mis_builder_ralign { - text-align: right; -} - -.openerp .mis_builder a { - /* we don't want the link color, to respect user styles */ - color: inherit; -} - -.openerp .mis_builder a:hover { - /* underline links on hover to give a visual cue */ - text-decoration: underline; -} - .openerp .oe_mis_builder_buttons { padding-bottom: 10px; padding-top: 10px; From 8e9a245dd27abcd88af1851980da5770ba369da4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 13:00:57 +0200 Subject: [PATCH 126/182] [WIP] mis_builder refactoring: split compute method So the matrix computation can be used without the json rendering. --- mis_builder/models/mis_report_instance.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index d905f8c1..eecfcf7a 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -378,7 +378,7 @@ class MisReportInstance(models.Model): } @api.multi - def compute(self): + def _compute_matrix(self): self.ensure_one() aep = self.report_id._prepare_aep(self.company_id) kpi_matrix = self.report_id._prepare_kpi_matrix() @@ -407,4 +407,10 @@ class MisReportInstance(models.Model): for comparison_column in period.comparison_column_ids: kpi_matrix.declare_comparison(period.id, comparison_column.id) kpi_matrix.compute_comparisons() + return kpi_matrix + + @api.multi + def compute(self): + self.ensure_one() + kpi_matrix = self._compute_matrix() return kpi_matrix.as_dict() From eca96c9f6c2892fc669003144c0e0a914451487c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 13:58:07 +0200 Subject: [PATCH 127/182] [WIP] mis_builder refactoring: cleanup widget styles a bit --- mis_builder/static/src/css/custom.css | 10 +++++++++- mis_builder/static/src/xml/mis_widget.xml | 22 +++++++++------------- 2 files changed, 18 insertions(+), 14 deletions(-) diff --git a/mis_builder/static/src/css/custom.css b/mis_builder/static/src/css/custom.css index 1945d5df..063eb2d6 100644 --- a/mis_builder/static/src/css/custom.css +++ b/mis_builder/static/src/css/custom.css @@ -1,7 +1,15 @@ -.openerp .mis_builder_ralign { +.openerp .mis_builder_amount { text-align: right; } +.openerp .mis_builder_collabel { + text-align: center; +} + +.openerp .mis_builder_rowlabel { + text-align: left; +} + .openerp .mis_builder a { /* we don't want the link color, to respect user styles */ color: inherit; diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 811a02e9..baef3e27 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -14,27 +14,23 @@ - -
- - -
- - -
+ + + +
+ +
- -
- -
+ + - +
Date: Wed, 11 May 2016 13:58:27 +0200 Subject: [PATCH 128/182] [WIP] mis_builder refactoring: refactor and improve PDF report --- .../report/report_mis_report_instance.py | 20 ---- .../report/report_mis_report_instance.xml | 99 +++++++++---------- mis_builder/static/src/css/report.css | 62 ++++++------ 3 files changed, 78 insertions(+), 103 deletions(-) diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/report_mis_report_instance.py index 44cfbeb8..162b146a 100644 --- a/mis_builder/report/report_mis_report_instance.py +++ b/mis_builder/report/report_mis_report_instance.py @@ -9,26 +9,6 @@ from openerp import api, models _logger = logging.getLogger(__name__) -class ReportMisReportInstance(models.AbstractModel): - - _name = 'report.mis_builder.report_mis_report_instance' - - @api.multi - def render_html(self, data=None): - docs = self.env['mis.report.instance'].browse(self._ids) - docs_computed = {} - for doc in docs: - docs_computed[doc.id] = doc.compute() - docargs = { - 'doc_ids': self._ids, - 'doc_model': 'mis.report.instance', - 'docs': docs, - 'docs_computed': docs_computed, - } - return self.env['report'].\ - render('mis_builder.report_mis_report_instance', docargs) - - class Report(models.Model): _inherit = "report" diff --git a/mis_builder/report/report_mis_report_instance.xml b/mis_builder/report/report_mis_report_instance.xml index a960e874..940b917a 100644 --- a/mis_builder/report/report_mis_report_instance.xml +++ b/mis_builder/report/report_mis_report_instance.xml @@ -4,72 +4,69 @@ diff --git a/mis_builder/static/src/css/report.css b/mis_builder/static/src/css/report.css index 2ea21e43..581a8560 100644 --- a/mis_builder/static/src/css/report.css +++ b/mis_builder/static/src/css/report.css @@ -1,47 +1,45 @@ .mis_table { - display: table ; + display: table; width: 100%; - table-layout: fixed ; + table-layout: fixed; } .mis_row { - display: table-row ; + display: table-row; page-break-inside: avoid; } .mis_cell { - display: table-cell ; + display: table-cell; page-break-inside: avoid; } .mis_thead { - display: table-header-group ; + display: table-header-group; } .mis_tbody { - display: table-row-group ; -} -.mis_row.labels { - background-color:#F0F0F0 ; + 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.labels, .mis_table .mis_row.lines { - border-color:grey ; - border-bottom:1px solid lightGrey ; -} -.mis_table .mis_cell { - word-wrap: break-word; -} -.mis_cell.amount { - word-wrap:normal; - text-align:right; -} -.mis_table .mis_cell{ - padding-left: 5px; -/* border-right:1px solid lightGrey; uncomment to active column lines */ + 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 { + font-weight: bold; + background-color: #F0F0F0; + text-align: center; +} +.mis_table .mis_cell.mis_rowlabel { + text-align: left; + /*white-space: nowrap;*/ +} +.mis_table .mis_cell.mis_amount { + text-align: right; } From 585516b11685710127d0ea6a8693e9ad882c513e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Wed, 11 May 2016 14:01:55 +0200 Subject: [PATCH 129/182] [FIX] mis_builder: minor tweak to avoid empty style attributs in widget --- mis_builder/models/mis_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index d71758df..a993a5c3 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -343,7 +343,7 @@ class KpiMatrix(object): 'parent_row_id': row.parent_row and id(row.parent_row) or None, 'description': row.description, 'comment': row.comment, - 'style': row.style and row.style.to_css_style() or '', + 'style': row.style and row.style.to_css_style() or None, 'cols': [] } for cell in row.iter_cells(): From 6e7936378f56d27455fb3950ec227a9a4530f776 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 12 May 2016 18:58:23 +0200 Subject: [PATCH 130/182] [WIP] mis_builder refactoring: restore and improve xslx Including decent column widths. --- mis_builder/report/mis_builder_xlsx.py | 189 ++++++++++++++----------- 1 file changed, 104 insertions(+), 85 deletions(-) diff --git a/mis_builder/report/mis_builder_xlsx.py b/mis_builder/report/mis_builder_xlsx.py index 49cadbd4..45f7929e 100644 --- a/mis_builder/report/mis_builder_xlsx.py +++ b/mis_builder/report/mis_builder_xlsx.py @@ -1,33 +1,24 @@ # -*- coding: utf-8 -*- -############################################################################## -# -# mis_builder module for Odoo, Management Information System Builder -# Copyright (C) 2014-2015 ACSONE SA/NV () -# -# This file is a part of mis_builder -# -# mis_builder is free software: you can redistribute it and/or modify -# it under the terms of the GNU Affero General Public License v3 or later -# as published by the Free Software Foundation, either version 3 of the -# License, or (at your option) any later version. -# -# mis_builder is distributed in the hope that it will be useful, -# but WITHOUT ANY WARRANTY; without even the implied warranty of -# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the -# GNU Affero General Public License v3 or later for more details. -# -# You should have received a copy of the GNU Affero General Public License -# v3 or later along with this program. -# If not, see . -# -############################################################################## +# © 2014-2016 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +from collections import defaultdict +import logging from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx from openerp.report import report_sxw -import logging + +from ..models.accounting_none import AccountingNone + _logger = logging.getLogger(__name__) +ROW_HEIGHT = 15 # xlsxwriter units +COL_WIDTH = 0.9 # xlsxwriter units +MIN_COL_WIDTH = 10 # characters +MAX_COL_WIDTH = 50 # characters + + class MisBuilderXslx(ReportXlsx): def __init__(self, name, table, rml=False, parser=False, header=True, @@ -35,82 +26,110 @@ class MisBuilderXslx(ReportXlsx): super(MisBuilderXslx, self).__init__( name, table, rml, parser, header, store) + def make_number_format(self, kpi, comparison=False): + # TODO FIXME comparison + number_format = '#' + if kpi.dp: + number_format += '.' + number_format += '0' * kpi.dp + # TODO FIXME factor + if kpi.prefix: + number_format = u'"{} "{}'.format(kpi.prefix, number_format) + if kpi.suffix: + number_format = u'{}" {}"'.format(number_format, kpi.suffix) + return number_format + def generate_xlsx_report(self, workbook, data, objects): - report_name = objects[0].name + # get the computed result of the report + matrix = objects._compute_matrix() + + # create worksheet + report_name = '{} - {}'.format( + objects[0].name, objects[0].company_id.name) sheet = workbook.add_worksheet(report_name[:31]) row_pos = 0 col_pos = 0 + # width of the labels column + label_col_width = MIN_COL_WIDTH + # {col_pos: max width in characters} + col_width = defaultdict(lambda: MIN_COL_WIDTH) - sheet.set_column(col_pos, col_pos, 30) + # document title bold = workbook.add_format({'bold': True}) - header_format = workbook.add_format({'bold': True, - 'align': 'center', - 'bg_color': '#F0EEEE'}) + header_format = workbook.add_format({ + 'bold': True, 'align': 'center', 'bg_color': '#F0EEEE'}) sheet.write(row_pos, 0, report_name, bold) + row_pos += 2 + + # column headers + sheet.write(row_pos, 0, '', header_format) + col_pos = 1 + for col in matrix.iter_cols(): + label = col.description + if col.comment: + label += '\n' + col.comment + sheet.set_row(row_pos, ROW_HEIGHT * 2) + sheet.merge_range( + row_pos, col_pos, row_pos, + col_pos + col.colspan-1, + label, header_format) + col_pos += col.colspan row_pos += 1 - col_pos += 1 - # get the computed result of the report - data = objects.compute() - - # Column headers - for header in data['header']: - has_col_date = False - for col in header['cols']: - colspan = col['colspan'] - col_date = col.get('date') - col_name = col['name'] - has_col_date = has_col_date or col_date - if colspan > 1: - sheet.merge_range( - row_pos, col_pos, row_pos, col_pos + colspan-1, - col_name, header_format) - if col_date: - sheet.merge_range( - row_pos+1, col_pos, row_pos+1, col_pos + colspan-1, - col_date, header_format) - col_pos += colspan - else: - sheet.write(row_pos, col_pos, col['name'], header_format) - if col_date: - sheet.write( - row_pos+1, col_pos, col.get('date'), header_format) - col_pos += 1 - col_pos = 1 - row_pos += 1 - if has_col_date: - row_pos += 1 - for line in data['content']: - row_xlsx_syle = line.get('default_xlsx_style', {}) - row_format = workbook.add_format(row_xlsx_syle) - col = 0 - sheet.write(row_pos, col, line['kpi_name'], row_format) - for value in line['cols']: - col += 1 - num_format_str = '#' - if value.get('dp'): - num_format_str += '.' - num_format_str += '0' * int(value['dp']) - if value.get('prefix'): - num_format_str = '"%s"' % value['prefix'] + num_format_str - if value.get('suffix'): - num_format_str = num_format_str + ' "%s"' % value['suffix'] - kpi_xlsx_syle = value.get('xlsx_style', {}) or row_xlsx_syle - kpi_xlsx_syle.update({ - 'num_format': num_format_str, + # sub column headers + sheet.write(row_pos, 0, '', header_format) + col_pos = 1 + for subcol in matrix.iter_subcols(): + label = subcol.description + if subcol.comment: + label += '\n' + subcol.comment + sheet.set_row(row_pos, ROW_HEIGHT * 2) + sheet.write(row_pos, col_pos, label, header_format) + col_width[col_pos] = max(col_width[col_pos], + len(subcol.description or ''), + len(subcol.comment or '')) + col_pos += 1 + row_pos += 1 + + # rows + for row in matrix.iter_rows(): + if row.style: + row_xlsx_style = row.style.to_xlsx_format_properties() + else: + row_xlsx_style = {} + row_format = workbook.add_format(row_xlsx_style) + col_pos = 0 + sheet.write(row_pos, col_pos, row.description, row_format) + label_col_width = max(label_col_width, len(row.description or '')) + for cell in row.iter_cells(): + col_pos += 1 + if not cell or cell.val is AccountingNone: + sheet.write(row_pos, col_pos, '', row_format) + continue + kpi_xlsx_style = dict(row_xlsx_style) + kpi_xlsx_style.update({ + 'num_format': self.make_number_format(row.kpi), 'align': 'right' }) - kpi_format = workbook.add_format(kpi_xlsx_syle) - if value.get('val'): - val = value['val'] - if value.get('is_percentage'): - val = val / 0.01 - sheet.write(row_pos, col, val, kpi_format) - else: - sheet.write(row_pos, col, value['val_r'], kpi_format) + kpi_format = workbook.add_format(kpi_xlsx_style) + # TODO FIXME kpi computed style + # TODO FIXME pct in comparision columns + val = cell.val + if row.kpi.type == 'pct': + val = val / 0.01 + sheet.write(row_pos, col_pos, val, kpi_format) + col_width[col_pos] = max(col_width[col_pos], + len(cell.val_rendered or '')) row_pos += 1 + # adjust col widths + sheet.set_column(0, 0, min(label_col_width, MAX_COL_WIDTH) * COL_WIDTH) + data_col_width = min(MAX_COL_WIDTH, max(*col_width.values())) + min_col_pos = min(*col_width.keys()) + max_col_pos = max(*col_width.keys()) + sheet.set_column(min_col_pos, max_col_pos, data_col_width * COL_WIDTH) + MisBuilderXslx('report.mis.report.instance.xlsx', 'mis.report.instance', parser=report_sxw.rml_parse) From c96e366cdc75bff5c13bf7c023a7bcab7fbdf8b5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 12 May 2016 21:39:03 +0200 Subject: [PATCH 131/182] [FIX] mis_builder: correct use of color picker widget --- mis_builder/__openerp__.py | 1 + mis_builder/views/mis_builder.xml | 4 ++-- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 39ba012e..5f44ff06 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -16,6 +16,7 @@ 'account', 'report_xlsx', # OCA/reporting-engine 'date_range', # OCA/server-tools + 'web_widget_color', # OCA/web ], 'data': [ 'wizard/mis_builder_dashboard.xml', diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index d19f1ce8..40341e5f 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -113,8 +113,8 @@
- - + + From 272427006b8b4effa0b2d5f4dd4003d1c73594d3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Thu, 12 May 2016 22:02:56 +0200 Subject: [PATCH 132/182] [WIP] mis_builder refactoring: fix and clean the test --- mis_builder/models/mis_report.py | 18 +++++-- mis_builder/tests/__init__.py | 3 +- mis_builder/tests/test_fetch_query.py | 39 ++++++++++++++ mis_builder/tests/test_mis_builder.py | 72 -------------------------- mis_builder/tests/test_utc_midnight.py | 25 +++++++++ 5 files changed, 79 insertions(+), 78 deletions(-) create mode 100644 mis_builder/tests/test_fetch_query.py delete mode 100644 mis_builder/tests/test_mis_builder.py create mode 100644 mis_builder/tests/test_utc_midnight.py diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index a993a5c3..411efdb5 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -55,6 +55,13 @@ class KpiMatrixRow(object): else: return None # TODO style for expanded accounts + @property + def row_id(self): + if not self.account_id: + return self.kpi.name + else: + return '{}:{}'.format(self.kpi.name, self.account_id) + def iter_cell_tuples(self, cols=None): if cols is None: cols = self._matrix.iter_cols() @@ -215,12 +222,12 @@ class KpiMatrix(object): else: val_rendered = kpi.render(self.lang, val) if subcol.subkpi: - val_comment = "{}.{} = {}".format( + val_comment = u'{}.{} = {}'.format( row.kpi.name, subcol.subkpi.name, row.kpi.get_expression_for_subkpi(subcol.subkpi)) else: - val_comment = "{} = {}".format( + val_comment = u'{} = {}'.format( row.kpi.name, row.kpi.expression) # TODO style @@ -339,8 +346,9 @@ class KpiMatrix(object): content = [] for row in self.iter_rows(): row_data = { - 'row_id': id(row), - 'parent_row_id': row.parent_row and id(row.parent_row) or None, + 'row_id': row.row_id, + 'parent_row_id': (row.parent_row and + row.parent_row.row_id or None), 'description': row.description, 'comment': row.comment, 'style': row.style and row.style.to_css_style() or None, @@ -472,7 +480,7 @@ class MisReportKpi(models.Model): l = [] for expression in kpi.expression_ids: if expression.subkpi_id: - l.append('{}={}'.format( + l.append(u'{} = {}'.format( expression.subkpi_id.name, expression.name)) else: l.append( diff --git a/mis_builder/tests/__init__.py b/mis_builder/tests/__init__.py index e2db2cba..d6e30b64 100644 --- a/mis_builder/tests/__init__.py +++ b/mis_builder/tests/__init__.py @@ -5,6 +5,7 @@ from . import test_accounting_none from . import test_aep from . import test_aggregate -from . import test_mis_builder +from . import test_fetch_query from . import test_mis_safe_eval from . import test_simple_array +from . import test_utc_midnight diff --git a/mis_builder/tests/test_fetch_query.py b/mis_builder/tests/test_fetch_query.py new file mode 100644 index 00000000..980868f7 --- /dev/null +++ b/mis_builder/tests/test_fetch_query.py @@ -0,0 +1,39 @@ +# -*- coding: utf-8 -*- +# © 2014-2016 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import openerp.tests.common as common + + +class TestFetchQuery(common.TransactionCase): + + def test_fetch_query(self): + # create a report on account.analytic.line + data = self.registry('mis.report.instance').compute( + self.cr, self.uid, + self.ref('mis_builder.mis_report_instance_test')) + self.assertEquals( + {'content': + [{'description': u'total test', + 'comment': '', + 'style': None, + 'parent_row_id': None, + 'row_id': u'total_test', + 'cols': [{'val': 0, + 'val_r': u'\xa00\xa0', + 'val_c': u'total_test = len(test)', + }] + }], + 'header': + [{'cols': [{'comment': '07/31/2014', + 'colspan': 1, + 'description': u'today', + }], + }, + {'cols': [{'colspan': 1, + 'description': '', + 'comment': '', + }], + }, + ], + }, data) diff --git a/mis_builder/tests/test_mis_builder.py b/mis_builder/tests/test_mis_builder.py deleted file mode 100644 index 75bfac09..00000000 --- a/mis_builder/tests/test_mis_builder.py +++ /dev/null @@ -1,72 +0,0 @@ -# -*- coding: utf-8 -*- -# © 2014-2015 ACSONE SA/NV () -# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). - -import openerp.tests.common as common - -from ..models import mis_builder - - -class TestMisBuilder(common.TransactionCase): - - def setUp(self): - super(TestMisBuilder, self).setUp() - - def test_datetime_conversion(self): - date_to_convert = '2014-07-05' - date_time_convert = mis_builder._utc_midnight( - date_to_convert, 'Europe/Brussels') - self.assertEqual(date_time_convert, '2014-07-04 22:00:00', - 'The converted date time convert must contains hour') - date_time_convert = mis_builder._utc_midnight( - date_to_convert, 'Europe/Brussels', add_day=1) - self.assertEqual(date_time_convert, '2014-07-05 22:00:00', - 'The converted date time convert must contains hour') - date_time_convert = mis_builder._utc_midnight( - date_to_convert, 'US/Pacific') - self.assertEqual(date_time_convert, '2014-07-05 07:00:00', - 'The converted date time convert must contains hour') - date_time_convert = mis_builder._utc_midnight( - date_to_convert, 'US/Pacific', add_day=1) - self.assertEqual(date_time_convert, '2014-07-06 07:00:00', - 'The converted date time convert must contains hour') - - def test_fetch_query(self): - # create a report on account.analytic.line - data = self.registry('mis.report.instance').compute( - self.cr, self.uid, - self.ref('mis_builder.mis_report_instance_test')) - self.assertDictContainsSubset( - {'content': - [{'kpi_name': u'total test', - 'default_style': '', - 'default_xlsx_style': {}, - 'cols': [{'period_id': self.ref('mis_builder.' - 'mis_report_instance_' - 'period_test'), - 'style': '', - 'xlsx_style': {}, - 'prefix': False, - 'suffix': False, - 'expr': u'len(test)', - 'val_c': u'total_test = len(test)', - 'val': 0, - 'val_r': u'\u202f0\xa0', - 'is_percentage': False, - 'dp': 0, - 'drilldown': False}] - }], - 'header': - [{'kpi_name': '', - 'cols': [{'date': '07/31/2014', - 'colspan': 1, - 'name': u'today', - }], - }, - {'kpi_name': '', - 'cols': [{'colspan': 1, - 'name': '', - }], - }, - ], - }, data) diff --git a/mis_builder/tests/test_utc_midnight.py b/mis_builder/tests/test_utc_midnight.py new file mode 100644 index 00000000..4e5d3df2 --- /dev/null +++ b/mis_builder/tests/test_utc_midnight.py @@ -0,0 +1,25 @@ +# -*- coding: utf-8 -*- +# © 2014-2015 ACSONE SA/NV () +# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). + +import openerp.tests.common as common + +from ..models.mis_report import _utc_midnight + + +class TestUtcMidnight(common.TransactionCase): + + def test_utc_midnight(self): + date_to_convert = '2014-07-05' + date_time_convert = _utc_midnight( + date_to_convert, 'Europe/Brussels') + self.assertEqual(date_time_convert, '2014-07-04 22:00:00') + date_time_convert = _utc_midnight( + date_to_convert, 'Europe/Brussels', add_day=1) + self.assertEqual(date_time_convert, '2014-07-05 22:00:00') + date_time_convert = _utc_midnight( + date_to_convert, 'US/Pacific') + self.assertEqual(date_time_convert, '2014-07-05 07:00:00') + date_time_convert = _utc_midnight( + date_to_convert, 'US/Pacific', add_day=1) + self.assertEqual(date_time_convert, '2014-07-06 07:00:00') From d66f7b92fcbcfd46a2c7784251b6a00f54a87c9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 08:24:41 +0200 Subject: [PATCH 133/182] [IMP] mis_builder: style for auto-expanded detail rows --- mis_builder/models/mis_report.py | 12 +++++++++--- mis_builder/views/mis_builder.xml | 7 ++++--- 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 411efdb5..a2e20b30 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -53,7 +53,7 @@ class KpiMatrixRow(object): if not self.account_id: return self.kpi.style else: - return None # TODO style for expanded accounts + return self.kpi.auto_expand_accounts_style @property def row_id(self): @@ -421,14 +421,20 @@ class MisReportKpi(models.Model): inverse='_inverse_expression') expression_ids = fields.One2many('mis.report.kpi.expression', 'kpi_id') auto_expand_accounts = fields.Boolean(string='Display details by account') + auto_expand_accounts_style = fields.Many2one( + string="Style for account detail rows", + comodel_name="mis.report.kpi.style", + required=False + ) style = fields.Many2one( - string="Default style for KPI", + string="Row style", comodel_name="mis.report.kpi.style", required=False ) style_expression = fields.Char( string='Style expression', - help='An expression that returns a style name for the kpi style') + help='An expression that returns a style depending on the KPI value. ' + 'Such style is applied on top of the row style.') type = fields.Selection([('num', _('Numeric')), ('pct', _('Percentage')), ('str', _('String'))], diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 40341e5f..7e725031 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -141,8 +141,8 @@ attrs="{'invisible': [('type', '=', 'str')]}"/> - - + + @@ -159,8 +159,9 @@ attrs="{'invisible': [('multi', '=', True)], 'readonly': [('multi', '=', True)]}"/> - + + From 1eaa80622a11f0a1f510defbc59696ff8fa33665 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 08:25:15 +0200 Subject: [PATCH 134/182] [IMP] mis_builder: add no-break space for better display of multi-formula expressions --- mis_builder/models/mis_report.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index a2e20b30..ef6bf03b 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -486,7 +486,7 @@ class MisReportKpi(models.Model): l = [] for expression in kpi.expression_ids: if expression.subkpi_id: - l.append(u'{} = {}'.format( + l.append(u'{}\xa0=\xa0{}'.format( expression.subkpi_id.name, expression.name)) else: l.append( From 3d51df95f1be5f7d0089063d719c600df88f4a10 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 08:29:43 +0200 Subject: [PATCH 135/182] [FIX] mis_builder: do not use color picker widget because it does not allow for null values --- mis_builder/__openerp__.py | 1 - mis_builder/views/mis_builder.xml | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 5f44ff06..39ba012e 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -16,7 +16,6 @@ 'account', 'report_xlsx', # OCA/reporting-engine 'date_range', # OCA/server-tools - 'web_widget_color', # OCA/web ], 'data': [ 'wizard/mis_builder_dashboard.xml', diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 7e725031..245be2de 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -113,8 +113,8 @@ - - + + From 7a17fca354f426b9b2ffd6975bb05e22dc1b78f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 08:36:57 +0200 Subject: [PATCH 136/182] [IMP] mis_builder: cosmetics in style form --- mis_builder/models/mis_builder_style.py | 10 +++++----- mis_builder/views/mis_builder.xml | 13 +++++++++++-- 2 files changed, 16 insertions(+), 7 deletions(-) diff --git a/mis_builder/models/mis_builder_style.py b/mis_builder/models/mis_builder_style.py index 3208623e..91f831c1 100644 --- a/mis_builder/models/mis_builder_style.py +++ b/mis_builder/models/mis_builder_style.py @@ -10,9 +10,6 @@ class MisReportKpiStyle(models.Model): _name = 'mis.report.kpi.style' - # TODO use WEB WIdget color picker - name = fields.Char(string='style name', required=True) - @api.depends('indent_level') def check_positive_val(self): return self.indent_level > 0 @@ -47,11 +44,14 @@ class MisReportKpiStyle(models.Model): 'xx-large': 17 } + name = fields.Char(string='Style name', required=True) + color = fields.Char( - help='Line color in valid RGB code (from #000000 to #FFFFFF)', + string='Text color', + help='Text color in valid RGB code (from #000000 to #FFFFFF)', ) background_color = fields.Char( - help='Line color in valid RGB code (from #000000 to #FFFFFF)' + help='Background color in valid RGB code (from #000000 to #FFFFFF)' ) font_style = fields.Selection( selection=_font_style_selection, diff --git a/mis_builder/views/mis_builder.xml b/mis_builder/views/mis_builder.xml index 245be2de..52d67c9b 100644 --- a/mis_builder/views/mis_builder.xml +++ b/mis_builder/views/mis_builder.xml @@ -111,15 +111,23 @@ mis.report.kpi.style - + + + + + + + + + @@ -161,7 +169,8 @@ - + From e1e83a3efebad23babbc4e9f3a310c3137706774 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 08:45:46 +0200 Subject: [PATCH 137/182] [IMP] mis_builder: update contributors in README --- mis_builder/README.rst | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/mis_builder/README.rst b/mis_builder/README.rst index 976cea5e..b76c0431 100644 --- a/mis_builder/README.rst +++ b/mis_builder/README.rst @@ -106,8 +106,14 @@ Contributors * Stéphane Bidoul * Laetitia Gangloff * Adrien Peiffer +* Alexis de Lattre +* Alexandre Fayolle +* Jordi Ballester +* Thomas Binsfeld * Giovanni Capalbo +* Marco Calcagni * Sébastien Beau +* Laurent Mignon Maintainer ---------- From 6e2d369b1da54220b580ac499ccc22cba52d4fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Fri, 13 May 2016 13:17:43 +0200 Subject: [PATCH 138/182] [WIP] mis_builder refactoring: rename view and report sources for clarity --- mis_builder/__openerp__.py | 7 +- mis_builder/report/__init__.py | 8 +- ...nstance.py => mis_report_instance_qweb.py} | 0 ...tance.xml => mis_report_instance_qweb.xml} | 15 + ...er_xlsx.py => mis_report_instance_xlsx.py} | 9 +- .../report/mis_report_instance_xlsx.xml | 15 + mis_builder/views/mis_builder.xml | 429 ------------------ mis_builder/views/mis_builder_style.xml | 32 ++ mis_builder/views/mis_report.xml | 180 ++++++++ mis_builder/views/mis_report_instance.xml | 205 +++++++++ 10 files changed, 462 insertions(+), 438 deletions(-) rename mis_builder/report/{report_mis_report_instance.py => mis_report_instance_qweb.py} (100%) rename mis_builder/report/{report_mis_report_instance.xml => mis_report_instance_qweb.xml} (78%) rename mis_builder/report/{mis_builder_xlsx.py => mis_report_instance_xlsx.py} (95%) create mode 100644 mis_builder/report/mis_report_instance_xlsx.xml delete mode 100644 mis_builder/views/mis_builder.xml create mode 100644 mis_builder/views/mis_builder_style.xml create mode 100644 mis_builder/views/mis_report.xml create mode 100644 mis_builder/views/mis_report_instance.xml diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 39ba012e..6dc054fa 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -19,11 +19,14 @@ ], 'data': [ 'wizard/mis_builder_dashboard.xml', - 'views/mis_builder.xml', + 'views/mis_report.xml', + 'views/mis_report_instance.xml', + 'views/mis_builder_style.xml', 'datas/ir_cron.xml', 'security/ir.model.access.csv', 'security/mis_builder_security.xml', - 'report/report_mis_report_instance.xml', + 'report/mis_report_instance_qweb.xml', + 'report/mis_report_instance_xlsx.xml', ], 'test': [ ], diff --git a/mis_builder/report/__init__.py b/mis_builder/report/__init__.py index bdcf09d6..592a2ccf 100644 --- a/mis_builder/report/__init__.py +++ b/mis_builder/report/__init__.py @@ -2,9 +2,5 @@ # © 2014-2015 ACSONE SA/NV () # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). -try: - from . import mis_builder_xlsx -except ImportError: - pass # this module is not installed - -from . import report_mis_report_instance +from . import mis_report_instance_qweb +from . import mis_report_instance_xlsx diff --git a/mis_builder/report/report_mis_report_instance.py b/mis_builder/report/mis_report_instance_qweb.py similarity index 100% rename from mis_builder/report/report_mis_report_instance.py rename to mis_builder/report/mis_report_instance_qweb.py diff --git a/mis_builder/report/report_mis_report_instance.xml b/mis_builder/report/mis_report_instance_qweb.xml similarity index 78% rename from mis_builder/report/report_mis_report_instance.xml rename to mis_builder/report/mis_report_instance_qweb.xml index 940b917a..a33ad383 100644 --- a/mis_builder/report/report_mis_report_instance.xml +++ b/mis_builder/report/mis_report_instance_qweb.xml @@ -2,6 +2,21 @@ + + MIS report instance QWEB PDF report + mis.report.instance + ir.actions.report.xml + mis_builder.report_mis_report_instance + qweb-pdf + + + + + + + diff --git a/mis_builder/report/mis_report_instance_xlsx.py b/mis_builder/report/mis_report_instance_xlsx.py index f1f8acc6..a9c15631 100644 --- a/mis_builder/report/mis_report_instance_xlsx.py +++ b/mis_builder/report/mis_report_instance_xlsx.py @@ -108,9 +108,9 @@ class MisBuilderXslx(ReportXlsx): continue cell_xlsx_style = style_obj.to_xlsx_style(cell.style_props) cell_xlsx_style['align'] = 'right' - kpi_format = workbook.add_format(cell_xlsx_style) + cell_format = workbook.add_format(cell_xlsx_style) val = cell.val / float(cell.style_props.get('divider', 1)) - sheet.write(row_pos, col_pos, val, kpi_format) + sheet.write(row_pos, col_pos, val, cell_format) col_width[col_pos] = max(col_width[col_pos], len(cell.val_rendered or '')) row_pos += 1 From 5fd1c48574e3253cdf5b04deb47332e101b9f118 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 16:57:10 +0200 Subject: [PATCH 151/182] [FIX] mis_builder: multi-kpi comparison column ordering --- mis_builder/models/mis_report.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 1d5d1f33..668de86c 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -259,8 +259,9 @@ class KpiMatrix(object): base_col.description)) description = u'{} vs {}'.\ format(col.description, base_col.description) - comparison_col = KpiMatrixCol(description, None, - {}, col.subkpis) + comparison_col = KpiMatrixCol(description, None, {}, + sorted(common_subkpis, + key=lambda s: s.sequence)) for row in self.iter_rows(): cell_tuple = col.get_cell_tuple_for_row(row) base_cell_tuple = base_col.get_cell_tuple_for_row(row) From da6556a23919153456c92a34038e330dd0daf693 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 17:41:49 +0200 Subject: [PATCH 152/182] [IMP] mis_builder: move comparison code to the style class Ultimately KpiMatrix should not have to know about kpi at all, it should become a kind of spreadsheet object that can render values in row/cols/subcols with styles. At this stage, the qweb and pdf reports, as well as the as_dict() method of the matrix already work without knowning anything about kpis. --- mis_builder/models/mis_report.py | 111 ++++++------------------- mis_builder/models/mis_report_style.py | 69 +++++++++++++++ mis_builder/tests/test_render.py | 61 +++++++------- 3 files changed, 124 insertions(+), 117 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 668de86c..028d6281 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -21,6 +21,9 @@ from .aggregate import _sum, _avg, _min, _max from .accounting_none import AccountingNone from .simple_array import SimpleArray from .mis_safe_eval import mis_safe_eval, DataError +from .mis_report_style import ( + TYPE_NUM, TYPE_PCT, TYPE_STR, CMP_DIFF, CMP_PCT, CMP_NONE +) _logger = logging.getLogger(__name__) @@ -227,7 +230,8 @@ class KpiMatrix(object): val_rendered = val.name val_comment = val.msg else: - val_rendered = kpi.render(self.lang, row.style_props, val) + val_rendered = self._style_model.render( + self.lang, row.style_props, kpi.type, val) if subcol.subkpi: val_comment = u'{}.{} = {}'.format( row.kpi.name, @@ -283,8 +287,11 @@ class KpiMatrix(object): base_vals, comparison_col.iter_subcols()): # TODO FIXME average factors - delta, delta_r, style_r = row.kpi.compare_and_render( - self.lang, row.style_props, val, base_val, 1, 1) + delta, delta_r, style_r = \ + self._style_model.compare_and_render( + self.lang, row.style_props, + row.kpi.type, row.kpi.compare_method, + val, base_val, 1, 1) comparison_cell_tuple.append(KpiMatrixCell( row, comparison_subcol, delta, delta_r, None, style_r, None)) @@ -442,18 +449,18 @@ class MisReportKpi(models.Model): string='Style expression', help='An expression that returns a style depending on the KPI value. ' 'Such style is applied on top of the row style.') - type = fields.Selection([('num', _('Numeric')), - ('pct', _('Percentage')), - ('str', _('String'))], + type = fields.Selection([(TYPE_NUM, _('Numeric')), + (TYPE_PCT, _('Percentage')), + (TYPE_STR, _('String'))], required=True, string='Value type', - default='num') - compare_method = fields.Selection([('diff', _('Difference')), - ('pct', _('Percentage')), - ('none', _('None'))], + default=TYPE_NUM) + compare_method = fields.Selection([(CMP_DIFF, _('Difference')), + (CMP_PCT, _('Percentage')), + (CMP_NONE, _('None'))], required=True, string='Comparison Method', - default='pct') + default=CMP_PCT) sequence = fields.Integer(string='Sequence', default=100) report_id = fields.Many2one('mis.report', string='Report', @@ -533,88 +540,18 @@ class MisReportKpi(models.Model): @api.onchange('type') def _onchange_type(self): - if self.type == 'num': - self.compare_method = 'pct' - elif self.type == 'pct': - self.compare_method = 'diff' - elif self.type == 'str': - self.compare_method = 'none' + if self.type == TYPE_NUM: + self.compare_method = CMP_PCT + elif self.type == TYPE_PCT: + self.compare_method = CMP_DIFF + elif self.type == TYPE_STR: + self.compare_method = CMP_NONE def get_expression_for_subkpi(self, subkpi): for expression in self.expression_ids: if expression.subkpi_id == subkpi: return expression.name - @api.multi - def render(self, lang, style_props, value): - """ render a KPI value as a unicode string, ready for display """ - self.ensure_one() - style_obj = self.env['mis.report.style'] - if self.type == 'num': - return style_obj.render_num(lang, value, style_props.divider, - style_props.dp, - style_props.prefix, style_props.suffix) - elif self.type == 'pct': - return style_obj.render_pct(lang, value, style_props.dp) - else: - return style_obj.render_str(lang, value) - - @api.multi - def compare_and_render(self, lang, style_props, value, base_value, - average_value=1, average_base_value=1): - """ render the comparison of two KPI values, ready for display - - Returns a triple, with - * the numeric comparison - * its string rendering - * the update style properties - - If the difference is 0, an empty string is returned. - """ - self.ensure_one() - style_obj = self.env['mis.report.style'] - delta = AccountingNone - style_r = style_props.copy() - if value is None: - value = AccountingNone - if base_value is None: - base_value = AccountingNone - if self.type == 'pct': - delta = value - base_value - if delta and round(delta, (style_props.dp or 0) + 2) != 0: - style_r.update(dict( - divider=0.01, prefix='', suffix=_('pp'))) - else: - delta = AccountingNone - elif self.type == 'num': - if value and average_value: - value = value / float(average_value) - if base_value and average_base_value: - base_value = base_value / float(average_base_value) - if self.compare_method == 'diff': - delta = value - base_value - if delta and round(delta, style_props.dp or 0) != 0: - pass - else: - delta = AccountingNone - elif self.compare_method == 'pct': - if base_value and round(base_value, style_props.dp or 0) != 0: - delta = (value - base_value) / abs(base_value) - if delta and round(delta, 1) != 0: - style_r.update(dict( - divider=0.01, dp=1, prefix='', suffix='%')) - else: - delta = AccountingNone - if delta is not AccountingNone: - delta_r = style_obj.render_num( - lang, delta, - style_r.divider, style_r.dp, - style_r.prefix, style_r.suffix, - sign='+') - return delta, delta_r, style_r - else: - return AccountingNone, '', style_r - class MisReportSubkpi(models.Model): _name = 'mis.report.subkpi' diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py index f163a790..1cc300a6 100644 --- a/mis_builder/models/mis_report_style.py +++ b/mis_builder/models/mis_report_style.py @@ -31,6 +31,14 @@ PROPS = [ 'divider', ] +TYPE_NUM = 'num' +TYPE_PCT = 'pct' +TYPE_STR = 'str' + +CMP_DIFF = 'diff' +CMP_PCT = 'pct' +CMP_NONE = 'none' + class MisReportKpiStyle(models.Model): @@ -121,6 +129,10 @@ class MisReportKpiStyle(models.Model): @api.model def merge(self, styles): + """ Merge several styles, giving priority to the last. + + Returns a PropertyDict of style properties. + """ r = PropertyDict() for style in styles: if not style: @@ -139,6 +151,17 @@ class MisReportKpiStyle(models.Model): r[prop] = value return r + @api.model + def render(self, lang, style_props, type, value): + if type == 'num': + return self.render_num(lang, value, style_props.divider, + style_props.dp, + style_props.prefix, style_props.suffix) + elif type == 'pct': + return self.render_pct(lang, value, style_props.dp) + else: + return self.render_str(lang, value) + @api.model def render_num(self, lang, value, divider=1.0, dp=0, prefix=None, suffix=None, sign='-'): @@ -165,6 +188,52 @@ class MisReportKpiStyle(models.Model): return u'' return unicode(value) + @api.model + def compare_and_render(self, lang, style_props, type, compare_method, + value, base_value, + average_value=1, average_base_value=1): + delta = AccountingNone + style_r = style_props.copy() + if value is None: + value = AccountingNone + if base_value is None: + base_value = AccountingNone + if type == TYPE_PCT: + delta = value - base_value + if delta and round(delta, (style_props.dp or 0) + 2) != 0: + style_r.update(dict( + divider=0.01, prefix='', suffix=_('pp'))) + else: + delta = AccountingNone + elif type == TYPE_NUM: + if value and average_value: + value = value / float(average_value) + if base_value and average_base_value: + base_value = base_value / float(average_base_value) + if compare_method == CMP_DIFF: + delta = value - base_value + if delta and round(delta, style_props.dp or 0) != 0: + pass + else: + delta = AccountingNone + elif compare_method == CMP_PCT: + if base_value and round(base_value, style_props.dp or 0) != 0: + delta = (value - base_value) / abs(base_value) + if delta and round(delta, 1) != 0: + style_r.update(dict( + divider=0.01, dp=1, prefix='', suffix='%')) + else: + delta = AccountingNone + if delta is not AccountingNone: + delta_r = self.render_num( + lang, delta, + style_r.divider, style_r.dp, + style_r.prefix, style_r.suffix, + sign='+') + return delta, delta_r, style_r + else: + return AccountingNone, '', style_r + @api.model def to_xlsx_style(self, props): num_format = '0' diff --git a/mis_builder/tests/test_render.py b/mis_builder/tests/test_render.py index 62bf7d84..4a08a6ab 100644 --- a/mis_builder/tests/test_render.py +++ b/mis_builder/tests/test_render.py @@ -5,6 +5,9 @@ import openerp.tests.common as common from ..models.accounting_none import AccountingNone +from ..models.mis_report_style import ( + TYPE_NUM, TYPE_PCT, TYPE_STR, CMP_DIFF, CMP_PCT +) class TestRendering(common.TransactionCase): @@ -16,22 +19,18 @@ class TestRendering(common.TransactionCase): self.style = self.style_obj.create(dict( name='teststyle', )) - self.kpi = self.kpi_obj.create(dict( - name='testkpi', - description='test kpi', - type='num', - style_id=self.style.id, - )) self.lang = self.env['res.lang'].search([('code', '=', 'en_US')])[0] - def _render(self, value): + def _render(self, value, type=TYPE_NUM): style_props = self.style_obj.merge([self.style]) - return self.kpi.render(self.lang, style_props, value) + return self.style_obj.render(self.lang, style_props, type, value) - def _compare_and_render(self, value, base_value): + def _compare_and_render(self, value, base_value, + type=TYPE_NUM, compare_method=CMP_PCT): style_props = self.style_obj.merge([self.style]) - return self.kpi.compare_and_render(self.lang, style_props, - value, base_value)[:2] + return self.style_obj.compare_and_render(self.lang, style_props, + type, compare_method, + value, base_value)[:2] def test_render(self): self.assertEquals(u'1', self._render(1)) @@ -89,21 +88,18 @@ class TestRendering(common.TransactionCase): self.assertEquals(u'1,000,000', self._render(1)) def test_render_pct(self): - self.kpi.type = 'pct' - self.assertEquals(u'100\xa0%', self._render(1)) - self.assertEquals(u'50\xa0%', self._render(0.5)) + self.assertEquals(u'100\xa0%', self._render(1, TYPE_PCT)) + self.assertEquals(u'50\xa0%', self._render(0.5, TYPE_PCT)) self.style.dp_inherit = False self.style.dp = 2 - self.assertEquals(u'51.23\xa0%', self._render(0.5123)) + self.assertEquals(u'51.23\xa0%', self._render(0.5123, TYPE_PCT)) def test_render_string(self): - self.kpi.type = 'str' - self.assertEquals(u'', self._render('')) - self.assertEquals(u'', self._render(None)) - self.assertEquals(u'abcdé', self._render(u'abcdé')) + self.assertEquals(u'', self._render('', TYPE_STR)) + self.assertEquals(u'', self._render(None, TYPE_STR)) + self.assertEquals(u'abcdé', self._render(u'abcdé', TYPE_STR)) def test_compare_num_pct(self): - self.assertEquals('pct', self.kpi.compare_method) self.assertEquals((1.0, u'+100.0\xa0%'), self._compare_and_render(100, 50)) self.assertEquals((0.5, u'+50.0\xa0%'), @@ -132,26 +128,31 @@ class TestRendering(common.TransactionCase): self._compare_and_render(None, 50)) def test_compare_num_diff(self): - self.kpi.compare_method = 'diff' self.assertEquals((25, u'+25'), - self._compare_and_render(75, 50)) + self._compare_and_render(75, 50, + TYPE_NUM, CMP_DIFF)) self.assertEquals((-25, u'\u201125'), - self._compare_and_render(25, 50)) + self._compare_and_render(25, 50, + TYPE_NUM, CMP_DIFF)) self.style.suffix_inherit = False self.style.suffix = u'€' self.assertEquals((-25, u'\u201125\xa0€'), - self._compare_and_render(25, 50)) + self._compare_and_render(25, 50, + TYPE_NUM, CMP_DIFF)) self.style.suffix = u'' self.assertEquals((50.0, u'+50'), - self._compare_and_render(50, AccountingNone)) + self._compare_and_render(50, AccountingNone, + TYPE_NUM, CMP_DIFF)) self.assertEquals((50.0, u'+50'), - self._compare_and_render(50, None)) + self._compare_and_render(50, None, + TYPE_NUM, CMP_DIFF)) self.assertEquals((-50.0, u'\u201150'), - self._compare_and_render(AccountingNone, 50)) + self._compare_and_render(AccountingNone, 50, + TYPE_NUM, CMP_DIFF)) self.assertEquals((-50.0, u'\u201150'), - self._compare_and_render(None, 50)) + self._compare_and_render(None, 50, + TYPE_NUM, CMP_DIFF)) def test_compare_pct(self): - self.kpi.type = 'pct' self.assertEquals((0.25, u'+25\xa0pp'), - self._compare_and_render(0.75, 0.50)) + self._compare_and_render(0.75, 0.50, TYPE_PCT)) From 517e5fdb861daf02391177293905dd887d8adf66 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 17:47:31 +0200 Subject: [PATCH 153/182] [IMP] mis_builder: tweak report instance view --- mis_builder/views/mis_report_instance.xml | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/mis_builder/views/mis_report_instance.xml b/mis_builder/views/mis_report_instance.xml index 5a8b3928..235af5f3 100644 --- a/mis_builder/views/mis_report_instance.xml +++ b/mis_builder/views/mis_report_instance.xml @@ -29,11 +29,10 @@
- + + - Date: Sun, 15 May 2016 17:59:10 +0200 Subject: [PATCH 154/182] [IMP] mis_builder: do not talk about periods in KpiMatrix, only columns --- mis_builder/models/mis_report.py | 85 +++++++++++++++++--------------- 1 file changed, 45 insertions(+), 40 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 028d6281..5ced5ac2 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -37,6 +37,11 @@ class AutoStruct(object): class KpiMatrixRow(object): + # TODO: ultimately, the kpi matrix will become ignorant of KPI's and + # accounts and know about rows, columns, sub columns and styles only. + # It is already ignorant of period and only knowns about columns. + # This will require a correct abstraction for expanding row details. + def __init__(self, matrix, kpi, account_id=None, parent_row=None): self._matrix = matrix self.kpi = kpi @@ -163,9 +168,9 @@ class KpiMatrix(object): self._kpi_rows = OrderedDict() # { kpi: {account_id: KpiMatrixRow} } self._detail_rows = {} - # { period_key: KpiMatrixCol } + # { col_key: KpiMatrixCol } self._cols = OrderedDict() - # { period_key (left of comparison): [(period_key, base_period_key)] } + # { col_key (left of comparison): [(col_key, base_col_key)] } self._comparison_todo = defaultdict(list) self._comparison_cols = defaultdict(list) # { account_id: account_name } @@ -179,38 +184,38 @@ class KpiMatrix(object): self._kpi_rows[kpi] = KpiMatrixRow(self, kpi) self._detail_rows[kpi] = {} - def declare_period(self, period_key, description, comment, - locals_dict, subkpis): - """ Declare a new period (column), giving it an identifier (key). + def declare_col(self, col_key, description, comment, + locals_dict, subkpis): + """ Declare a new column, giving it an identifier (key). Invoke this and declare_comparison in display order. """ - self._cols[period_key] = KpiMatrixCol(description, comment, - locals_dict, subkpis) + self._cols[col_key] = KpiMatrixCol(description, comment, + locals_dict, subkpis) - def declare_comparison(self, period_key, base_period_key): + def declare_comparison(self, col_key, base_col_key): """ Declare a new comparison column. - Invoke this and declare_period in display order. + Invoke this and declare_col in display order. """ - last_period_key = list(self._cols.keys())[-1] - self._comparison_todo[last_period_key].append( - (period_key, base_period_key)) + last_col_key = list(self._cols.keys())[-1] + self._comparison_todo[last_col_key].append( + (col_key, base_col_key)) - def set_values(self, kpi, period_key, vals, + def set_values(self, kpi, col_key, vals, drilldown_args): - """ Set values for a kpi and a period. + """ Set values for a kpi and a colum. - Invoke this after declaring the kpi and the period. + Invoke this after declaring the kpi and the column. """ - self.set_values_detail_account(kpi, period_key, None, vals, + self.set_values_detail_account(kpi, col_key, None, vals, drilldown_args) - def set_values_detail_account(self, kpi, period_key, account_id, vals, + def set_values_detail_account(self, kpi, col_key, account_id, vals, drilldown_args): - """ Set values for a kpi and a period and a detail account. + """ Set values for a kpi and a column and a detail account. - Invoke this after declaring the kpi and the period. + Invoke this after declaring the kpi and the column. """ if not account_id: row = self._kpi_rows[kpi] @@ -221,7 +226,7 @@ class KpiMatrix(object): else: row = KpiMatrixRow(self, kpi, account_id, parent_row=kpi_row) self._detail_rows[kpi][account_id] = row - col = self._cols[period_key] + col = self._cols[col_key] cell_tuple = [] assert len(vals) == col.colspan for val, drilldown_arg, subcol in \ @@ -252,10 +257,10 @@ class KpiMatrix(object): Invoke this after setting all values. """ - for pos_period_key, comparisons in self._comparison_todo.items(): - for period_key, base_period_key in comparisons: - col = self._cols[period_key] - base_col = self._cols[base_period_key] + for pos_col_key, comparisons in self._comparison_todo.items(): + for col_key, base_col_key in comparisons: + col = self._cols[col_key] + base_col = self._cols[base_col_key] common_subkpis = set(col.subkpis) & set(base_col.subkpis) if not common_subkpis: raise UserError('Columns {} and {} are not comparable'. @@ -296,7 +301,7 @@ class KpiMatrix(object): row, comparison_subcol, delta, delta_r, None, style_r, None)) comparison_col._set_cell_tuple(row, comparison_cell_tuple) - self._comparison_cols[pos_period_key].append(comparison_col) + self._comparison_cols[pos_col_key].append(comparison_col) def iter_rows(self): """ Iterate rows in display order. @@ -313,17 +318,17 @@ class KpiMatrix(object): def iter_cols(self): """ Iterate columns in display order. - yields KpiMatrixCol: one for each period or comparison. + yields KpiMatrixCol: one for each column or comparison. """ - for period_key, col in self._cols.items(): + for col_key, col in self._cols.items(): yield col - for comparison_col in self._comparison_cols[period_key]: + for comparison_col in self._comparison_cols[col_key]: yield comparison_col def iter_subcols(self): """ Iterate sub columns in display order. - yields KpiMatrixSubCol: one for each subkpi in each period + yields KpiMatrixSubCol: one for each subkpi in each column and comparison. """ for col in self.iter_cols(): @@ -823,9 +828,9 @@ class MisReport(models.Model): @api.multi def _declare_and_compute_period(self, kpi_matrix, - period_key, - period_description, - period_comment, + col_key, + col_description, + col_comment, aep, date_from, date_to, target_move, @@ -836,7 +841,7 @@ class MisReport(models.Model): """ Evaluate a report for a given period, populating a KpiMatrix. :param kpi_matrix: the KpiMatrix object to be populated - :param period_key: the period key to use when populating the KpiMatrix + :param col_key: the period key to use when populating the KpiMatrix :param aep: an AccountingExpressionProcessor instance created using _prepare_aep() :param date_from, date_to: the starting and ending date @@ -881,9 +886,9 @@ class MisReport(models.Model): if subkpi in subkpis_filter] else: subkpis = self.subkpi_ids - kpi_matrix.declare_period(period_key, - period_description, period_comment, - locals_dict, subkpis) + kpi_matrix.declare_col(col_key, + col_description, col_comment, + locals_dict, subkpis) compute_queue = self.kpi_ids recompute_queue = [] @@ -907,7 +912,7 @@ class MisReport(models.Model): mis_safe_eval(replaced_expr, locals_dict)) if replaced_expr != expression: drilldown_args.append({ - 'period_id': period_key, + 'period_id': col_key, 'expr': expression, }) else: @@ -924,7 +929,7 @@ class MisReport(models.Model): locals_dict[kpi.name] = SimpleArray(vals) kpi_matrix.set_values( - kpi, period_key, vals, drilldown_args) + kpi, col_key, vals, drilldown_args) if not kpi.auto_expand_accounts: continue @@ -938,14 +943,14 @@ class MisReport(models.Model): vals.append(mis_safe_eval(replaced_expr, locals_dict)) if replaced_expr != expression: drilldown_args.append({ - 'period_id': period_key, + 'period_id': col_key, 'expr': expression, 'account_id': account_id }) else: drilldown_args.append(None) kpi_matrix.set_values_detail_account( - kpi, period_key, account_id, vals, drilldown_args) + kpi, col_key, account_id, vals, drilldown_args) if len(recompute_queue) == 0: # nothing to recompute, we are done From 27706de15c248279286f317996a7148b9e9f1e34 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 18:18:39 +0200 Subject: [PATCH 155/182] [IMP] mis_builder: remove unneeded div in mis widget --- mis_builder/static/src/xml/mis_widget.xml | 26 +++++++++++------------ 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index bc5178a9..8880caff 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -30,20 +30,18 @@ - -
- - - - - - - - -
+ + + + + + + + + From 542ac94eda899c5904a2a05351585285db73cc2f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 18:19:05 +0200 Subject: [PATCH 156/182] [WIP] mis_builder: add TODO related to sub-kpi naming in SimpleArray --- mis_builder/models/simple_array.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/mis_builder/models/simple_array.py b/mis_builder/models/simple_array.py index e6a51395..b9b10b86 100644 --- a/mis_builder/models/simple_array.py +++ b/mis_builder/models/simple_array.py @@ -61,6 +61,9 @@ from .data_error import DataError __all__ = ['SimpleArray'] +# TODO named tuple-like behaviour, so expressions can work on subkpis + + class SimpleArray(tuple): def _op(self, op, other): From c8320207496fa1ab3566c2cd2c6c3ba69abb358f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 18:19:28 +0200 Subject: [PATCH 157/182] [IMP] mis_builder: default values for colors --- mis_builder/models/mis_report_style.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py index 1cc300a6..03072946 100644 --- a/mis_builder/models/mis_report_style.py +++ b/mis_builder/models/mis_report_style.py @@ -90,10 +90,12 @@ class MisReportKpiStyle(models.Model): color = fields.Char( string='Text color', help='Text color in valid RGB code (from #000000 to #FFFFFF)', + default='#000000', ) background_color_inherit = fields.Boolean(default=True) background_color = fields.Char( - help='Background color in valid RGB code (from #000000 to #FFFFFF)' + help='Background color in valid RGB code (from #000000 to #FFFFFF)', + default='#FFFFFF', ) # font font_style_inherit = fields.Boolean(default=True) From 222874d4f67975b18855c7851fdc94f5753d59f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 18:20:29 +0200 Subject: [PATCH 158/182] [IMP] mis_builder: restore style expressions The addition is local and propagates automatically to the the widget, xls and pdf exports \o/ --- mis_builder/models/mis_report.py | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 5ced5ac2..d61a9249 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -246,9 +246,25 @@ class KpiMatrix(object): val_comment = u'{} = {}'.format( row.kpi.name, row.kpi.expression) - # TODO FIXME style expression + cell_style_props = row.style_props + if row.kpi.style_expression: + # evaluate style expression + try: + style_name = mis_safe_eval(row.kpi.style_expression, + col.locals_dict) + except: + _logger.error("Error evaluating style expression <%s>", + row.kpi.style_expression, exc_info=True) + if style_name: + style = self._style_model.search( + [('name', '=', style_name)]) + if style: + cell_style_props = self._style_model.merge( + [row.style_props, style[0]]) + else: + _logger.error("Style '%s' not found.", style_name) cell = KpiMatrixCell(row, subcol, val, val_rendered, val_comment, - row.style_props, drilldown_arg) + cell_style_props, drilldown_arg) cell_tuple.append(cell) col._set_cell_tuple(row, cell_tuple) From a812c15447f5867ffebda3986a5db3c47891bc0b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 22:21:31 +0200 Subject: [PATCH 159/182] [FIX] mis_builder: fix sum aggregation of non-stored field in non-accounting queries fixes #178 --- mis_builder/models/mis_report.py | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index d61a9249..e008d3ee 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -816,15 +816,23 @@ class MisReport(models.Model): domain.extend([(query.date_field.name, '>=', datetime_from), (query.date_field.name, '<', datetime_to)]) field_names = [f.name for f in query.field_ids] + all_stored = all([model._fields[f].store for f in field_names]) if not query.aggregate: data = model.search_read(domain, field_names) res[query.name] = [AutoStruct(**d) for d in data] - elif query.aggregate == 'sum': + elif query.aggregate == 'sum' and all_stored: + # use read_group to sum stored fields data = model.read_group( domain, field_names, []) s = AutoStruct(count=data[0]['__count']) for field_name in field_names: - v = data[0][field_name] + try: + v = data[0][field_name] + except KeyError: + _logger.error('field %s not found in read_group ' + 'for %s; not summable?', + field_name, model._name) + v = AccountingNone setattr(s, field_name, v) res[query.name] = s else: @@ -836,6 +844,8 @@ class MisReport(models.Model): agg = _max elif query.aggregate == 'avg': agg = _avg + elif query.aggregate == 'sum': + agg = _sum for field_name in field_names: setattr(s, field_name, agg([d[field_name] for d in data])) From 6fdb58ce23c9813e1ff76f6fe09c88e304bdfaed Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 22:24:27 +0200 Subject: [PATCH 160/182] [IMP] mis_builder: remove _ from some method names because they are part of the public API (though not usable over rpc) --- mis_builder/models/mis_report.py | 29 ++++++++++++----------- mis_builder/models/mis_report_instance.py | 6 ++--- 2 files changed, 18 insertions(+), 17 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index e008d3ee..c4f5e4c7 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -768,7 +768,7 @@ class MisReport(models.Model): # TODO: kpi name cannot be start with query name @api.multi - def _prepare_kpi_matrix(self): + def prepare_kpi_matrix(self): self.ensure_one() kpi_matrix = KpiMatrix(self.env) for kpi in self.kpi_ids: @@ -776,7 +776,7 @@ class MisReport(models.Model): return kpi_matrix @api.multi - def _prepare_aep(self, company): + def prepare_aep(self, company): self.ensure_one() aep = AEP(self.env) for kpi in self.kpi_ids: @@ -853,20 +853,21 @@ class MisReport(models.Model): return res @api.multi - def _declare_and_compute_period(self, kpi_matrix, - col_key, - col_description, - col_comment, - aep, - date_from, date_to, - target_move, - company, - subkpis_filter=None, - get_additional_move_line_filter=None, - get_additional_query_filter=None): + def declare_and_compute_period(self, kpi_matrix, + col_key, + col_description, + col_comment, + aep, + date_from, date_to, + target_move, + company, + subkpis_filter=None, + get_additional_move_line_filter=None, + get_additional_query_filter=None): """ Evaluate a report for a given period, populating a KpiMatrix. - :param kpi_matrix: the KpiMatrix object to be populated + :param kpi_matrix: the KpiMatrix object to be populated created + with prepare_kpi_matrix() :param col_key: the period key to use when populating the KpiMatrix :param aep: an AccountingExpressionProcessor instance created using _prepare_aep() diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index 2e65934c..431f9500 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -353,8 +353,8 @@ class MisReportInstance(models.Model): @api.multi def _compute_matrix(self): self.ensure_one() - aep = self.report_id._prepare_aep(self.company_id) - kpi_matrix = self.report_id._prepare_kpi_matrix() + aep = self.report_id.prepare_aep(self.company_id) + kpi_matrix = self.report_id.prepare_kpi_matrix() for period in self.period_ids: if period.date_from == period.date_to: comment = self._format_date(period.date_from) @@ -362,7 +362,7 @@ class MisReportInstance(models.Model): date_from = self._format_date(period.date_from) date_to = self._format_date(period.date_to) comment = _('from %s to %s') % (date_from, date_to) - self.report_id._declare_and_compute_period( + self.report_id.declare_and_compute_period( kpi_matrix, period.id, period.name, From c6e0afe3a49cc79242e8aeb3933dd05dc1de6088 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 22:27:53 +0200 Subject: [PATCH 161/182] [IMP] mis_builder: changelog and bump to version 2.0.0 --- mis_builder/CHANGES.rst | 3 ++- mis_builder/__openerp__.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/mis_builder/CHANGES.rst b/mis_builder/CHANGES.rst index 78796ca5..f9754f09 100644 --- a/mis_builder/CHANGES.rst +++ b/mis_builder/CHANGES.rst @@ -6,13 +6,14 @@ Changelog .. .. * -9.0.1.0.0 (2016-??-??) +9.0.2.0.0 (2016-??-??) ~~~~~~~~~~~~~~~~~~~~~~ Part of the work for this release has been done at the Sorrento sprint April 26-29, 2016. The rest (ie a major refactoring) has been done in the weeks after. +* [FIX] Fix sum aggregation of non-stored fields (issue #178) * [IMP] There is now a default style at the report level * [CHG] Number display properties (rounding, prefix, suffix, factor) are now defined in styles diff --git a/mis_builder/__openerp__.py b/mis_builder/__openerp__.py index 47a9e513..3d8f0d8e 100644 --- a/mis_builder/__openerp__.py +++ b/mis_builder/__openerp__.py @@ -4,7 +4,7 @@ { 'name': 'MIS Builder', - 'version': '9.0.1.0.0', + 'version': '9.0.2.0.0', 'category': 'Reporting', 'summary': """ Build 'Management Information System' Reports and Dashboards From 4dd0aec548b04d80024fcfd7b244b41935ebe0c8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Sun, 15 May 2016 22:49:07 +0200 Subject: [PATCH 162/182] [FIX] mis_builder: avoid calling compute twice in widget --- mis_builder/static/src/js/mis_builder.js | 1 - 1 file changed, 1 deletion(-) diff --git a/mis_builder/static/src/js/mis_builder.js b/mis_builder/static/src/js/mis_builder.js index 60dda3e9..8e932168 100644 --- a/mis_builder/static/src/js/mis_builder.js +++ b/mis_builder/static/src/js/mis_builder.js @@ -37,7 +37,6 @@ var MisReport = form_common.FormWidget.extend({ self.mis_report_instance_id = self.getParent().datarecord.id if (self.mis_report_instance_id) { self.getParent().dataset.context['no_destroy'] = true; - self.generate_content(); } else { self.display_settings(); } From 992a14b3167abb12f3ccb96d8ebc4027a6f4c804 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 16 May 2016 09:38:51 +0200 Subject: [PATCH 163/182] [IMP] mis_builder: simplify widget template Plus more consistent vocabulary for row/col labels --- mis_builder/models/mis_report.py | 20 ++++---- mis_builder/static/src/xml/mis_widget.xml | 62 +++++++++++------------ 2 files changed, 41 insertions(+), 41 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index c4f5e4c7..2e654dd1 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -369,8 +369,8 @@ class KpiMatrix(object): header = [{'cols': []}, {'cols': []}] for col in self.iter_cols(): header[0]['cols'].append({ - 'description': col.description, - 'comment': col.comment, + 'label': col.description, + 'description': col.comment, 'colspan': col.colspan, }) for subcol in col.iter_subcols(): @@ -380,22 +380,22 @@ class KpiMatrix(object): 'colspan': 1, }) - content = [] + body = [] for row in self.iter_rows(): row_data = { 'row_id': row.row_id, 'parent_row_id': (row.parent_row and row.parent_row.row_id or None), - 'description': row.description, - 'comment': row.comment, + 'label': row.description, + 'description': row.comment, 'style': self._style_model.to_css_style( row.style_props), - 'cols': [] + 'cells': [] } for cell in row.iter_cells(): if cell is None: # TODO use subcol style here - row_data['cols'].append({}) + row_data['cells'].append({}) else: col_data = { 'val': (cell.val @@ -407,12 +407,12 @@ class KpiMatrix(object): } if cell.drilldown_arg: col_data['drilldown_arg'] = cell.drilldown_arg - row_data['cols'].append(col_data) - content.append(row_data) + row_data['cells'].append(col_data) + body.append(row_data) return { 'header': header, - 'content': content, + 'body': body, } diff --git a/mis_builder/static/src/xml/mis_widget.xml b/mis_builder/static/src/xml/mis_widget.xml index 8880caff..e5c92c5c 100644 --- a/mis_builder/static/src/xml/mis_widget.xml +++ b/mis_builder/static/src/xml/mis_widget.xml @@ -10,40 +10,40 @@ - - - - - - + + + + - - + + - - - From 5167dec5aaeddaec15cfcde1cb88c5d4a806f7d5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 16 May 2016 12:49:35 +0200 Subject: [PATCH 164/182] [IMP] mis_builder: be more consistent in naming row/column labels - label: the main row/column label - description: a more detailed description of the row/column, currently displayed as a second line in the header cell - comment: a even more detailed comment, currently displayed as a tooltip in the interactive widget only --- mis_builder/models/mis_report.py | 36 +++++++++---------- .../report/mis_report_instance_qweb.xml | 18 +++++----- .../report/mis_report_instance_xlsx.py | 30 +++++++++------- 3 files changed, 45 insertions(+), 39 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 2e654dd1..e319a3d9 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -46,7 +46,7 @@ class KpiMatrixRow(object): self._matrix = matrix self.kpi = kpi self.account_id = account_id - self.comment = '' + self.description = '' self.parent_row = parent_row if not self.account_id: self.style_props = self._matrix._style_model.merge([ @@ -58,7 +58,7 @@ class KpiMatrixRow(object): self.kpi.auto_expand_accounts_style_id]) @property - def description(self): + def label(self): if not self.account_id: return self.kpi.description else: @@ -86,9 +86,9 @@ class KpiMatrixRow(object): class KpiMatrixCol(object): - def __init__(self, description, comment, locals_dict, subkpis): + def __init__(self, label, description, locals_dict, subkpis): + self.label = label self.description = description - self.comment = comment self.locals_dict = locals_dict self.colspan = subkpis and len(subkpis) or 1 self._subcols = [] @@ -117,10 +117,10 @@ class KpiMatrixCol(object): class KpiMatrixSubCol(object): - def __init__(self, col, description, comment, index=0): + def __init__(self, col, label, description, index=0): self.col = col + self.label = label self.description = description - self.comment = comment self.index = index @property @@ -184,13 +184,13 @@ class KpiMatrix(object): self._kpi_rows[kpi] = KpiMatrixRow(self, kpi) self._detail_rows[kpi] = {} - def declare_col(self, col_key, description, comment, + def declare_col(self, col_key, label, description, locals_dict, subkpis): """ Declare a new column, giving it an identifier (key). Invoke this and declare_comparison in display order. """ - self._cols[col_key] = KpiMatrixCol(description, comment, + self._cols[col_key] = KpiMatrixCol(label, description, locals_dict, subkpis) def declare_comparison(self, col_key, base_col_key): @@ -282,9 +282,9 @@ class KpiMatrix(object): raise UserError('Columns {} and {} are not comparable'. format(col.description, base_col.description)) - description = u'{} vs {}'.\ - format(col.description, base_col.description) - comparison_col = KpiMatrixCol(description, None, {}, + label = u'{} vs {}'.\ + format(col.label, base_col.label) + comparison_col = KpiMatrixCol(label, None, {}, sorted(common_subkpis, key=lambda s: s.sequence)) for row in self.iter_rows(): @@ -369,14 +369,14 @@ class KpiMatrix(object): header = [{'cols': []}, {'cols': []}] for col in self.iter_cols(): header[0]['cols'].append({ - 'label': col.description, - 'description': col.comment, + 'label': col.label, + 'description': col.description, 'colspan': col.colspan, }) for subcol in col.iter_subcols(): header[1]['cols'].append({ + 'label': subcol.label, 'description': subcol.description, - 'comment': subcol.comment, 'colspan': 1, }) @@ -386,8 +386,8 @@ class KpiMatrix(object): 'row_id': row.row_id, 'parent_row_id': (row.parent_row and row.parent_row.row_id or None), - 'label': row.description, - 'description': row.comment, + 'label': row.label, + 'description': row.description, 'style': self._style_model.to_css_style( row.style_props), 'cells': [] @@ -855,8 +855,8 @@ class MisReport(models.Model): @api.multi def declare_and_compute_period(self, kpi_matrix, col_key, + col_label, col_description, - col_comment, aep, date_from, date_to, target_move, @@ -914,7 +914,7 @@ class MisReport(models.Model): else: subkpis = self.subkpi_ids kpi_matrix.declare_col(col_key, - col_description, col_comment, + col_label, col_description, locals_dict, subkpis) compute_queue = self.kpi_ids diff --git a/mis_builder/report/mis_report_instance_qweb.xml b/mis_builder/report/mis_report_instance_qweb.xml index 05385e10..769034bc 100644 --- a/mis_builder/report/mis_report_instance_qweb.xml +++ b/mis_builder/report/mis_report_instance_qweb.xml @@ -37,10 +37,10 @@
- - + +
- +
@@ -53,10 +53,10 @@
- - + +
- +
@@ -65,10 +65,10 @@
- - + +
- +
diff --git a/mis_builder/report/mis_report_instance_xlsx.py b/mis_builder/report/mis_report_instance_xlsx.py index a9c15631..faf93fda 100644 --- a/mis_builder/report/mis_report_instance_xlsx.py +++ b/mis_builder/report/mis_report_instance_xlsx.py @@ -61,9 +61,9 @@ class MisBuilderXslx(ReportXlsx): sheet.write(row_pos, 0, '', header_format) col_pos = 1 for col in matrix.iter_cols(): - label = col.description - if col.comment: - label += '\n' + col.comment + label = col.label + if col.description: + label += '\n' + col.description sheet.set_row(row_pos, ROW_HEIGHT * 2) if col.colspan > 1: sheet.merge_range( @@ -73,8 +73,8 @@ class MisBuilderXslx(ReportXlsx): else: sheet.write(row_pos, col_pos, label, header_format) col_width[col_pos] = max(col_width[col_pos], - len(col.description or ''), - len(col.comment or '')) + len(col.label or ''), + len(col.description or '')) col_pos += col.colspan row_pos += 1 @@ -82,14 +82,14 @@ class MisBuilderXslx(ReportXlsx): sheet.write(row_pos, 0, '', header_format) col_pos = 1 for subcol in matrix.iter_subcols(): - label = subcol.description - if subcol.comment: - label += '\n' + subcol.comment + label = subcol.label + if subcol.description: + label += '\n' + subcol.description sheet.set_row(row_pos, ROW_HEIGHT * 2) sheet.write(row_pos, col_pos, label, header_format) col_width[col_pos] = max(col_width[col_pos], - len(subcol.description or ''), - len(subcol.comment or '')) + len(subcol.label or ''), + len(subcol.description or '')) col_pos += 1 row_pos += 1 @@ -98,8 +98,14 @@ class MisBuilderXslx(ReportXlsx): row_xlsx_style = style_obj.to_xlsx_style(row.style_props) row_format = workbook.add_format(row_xlsx_style) col_pos = 0 - sheet.write(row_pos, col_pos, row.description, row_format) - label_col_width = max(label_col_width, len(row.description or '')) + label = row.label + if row.description: + label += '\n' + row.description + sheet.set_row(row_pos, ROW_HEIGHT * 2) + sheet.write(row_pos, col_pos, label, row_format) + label_col_width = max(label_col_width, + len(row.label or ''), + len(row.description or '')) for cell in row.iter_cells(): col_pos += 1 if not cell or cell.val is AccountingNone: From c6da1aa9a6e490e7c2b66fa27c9f0096dec987e1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 16 May 2016 12:55:56 +0200 Subject: [PATCH 165/182] [IMP] mis_builder: hide button box in edit mode on the report instance settings form --- mis_builder/CHANGES.rst | 1 + mis_builder/views/mis_report_instance.xml | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/mis_builder/CHANGES.rst b/mis_builder/CHANGES.rst index f9754f09..4b8b1019 100644 --- a/mis_builder/CHANGES.rst +++ b/mis_builder/CHANGES.rst @@ -13,6 +13,7 @@ Part of the work for this release has been done at the Sorrento sprint April 26-29, 2016. The rest (ie a major refactoring) has been done in the weeks after. +* [IMP] hide button box in edit mode on the report instance settings form * [FIX] Fix sum aggregation of non-stored fields (issue #178) * [IMP] There is now a default style at the report level * [CHG] Number display properties (rounding, prefix, suffix, factor) are diff --git a/mis_builder/views/mis_report_instance.xml b/mis_builder/views/mis_report_instance.xml index 235af5f3..7c8f3f9e 100644 --- a/mis_builder/views/mis_report_instance.xml +++ b/mis_builder/views/mis_report_instance.xml @@ -45,7 +45,7 @@
-
+
- +
- - - -
- - -
+ + + +
+ + +
- +
+ + +
+ + +
+ + + + + + + + - - - - - - - - -
- +
@@ -47,7 +47,7 @@
diff --git a/mis_builder/tests/mis.report.csv b/mis_builder/tests/mis.report.csv index 51b55125..761c37a3 100644 --- a/mis_builder/tests/mis.report.csv +++ b/mis_builder/tests/mis.report.csv @@ -1,2 +1,2 @@ -"id","description","kpi_ids/id","name","query_ids/id" -"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" \ No newline at end of file +"id","description","kpi_ids/id","name","query_ids/id" +"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" diff --git a/mis_builder/tests/mis.report.instance.csv b/mis_builder/tests/mis.report.instance.csv index 96f035f4..73776865 100644 --- a/mis_builder/tests/mis.report.instance.csv +++ b/mis_builder/tests/mis.report.instance.csv @@ -1,2 +1,2 @@ -"id","date","description","name","period_ids/id","report_id/id" -"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test" \ No newline at end of file +"id","date","description","name","period_ids/id","report_id/id" +"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test" diff --git a/mis_builder/tests/mis.report.query.csv b/mis_builder/tests/mis.report.query.csv index ccbdf789..6ef1ac21 100644 --- a/mis_builder/tests/mis.report.query.csv +++ b/mis_builder/tests/mis.report.query.csv @@ -1,2 +1,2 @@ "id","date_field/id","domain","field_ids/id","model_id/id","name" -"mis_report_query_test","analytic.field_account_analytic_line_date","","analytic.field_account_analytic_line_amount","analytic.model_account_analytic_line","test" \ No newline at end of file +"mis_report_query_test","analytic.field_account_analytic_line_date","","analytic.field_account_analytic_line_amount","analytic.model_account_analytic_line","test" diff --git a/mis_builder/wizard/mis_builder_dashboard.xml b/mis_builder/wizard/mis_builder_dashboard.xml index 99802637..fbbb0bab 100644 --- a/mis_builder/wizard/mis_builder_dashboard.xml +++ b/mis_builder/wizard/mis_builder_dashboard.xml @@ -30,4 +30,4 @@
- \ No newline at end of file + From 343a570bbd4f358e6d97ca6be9b48fed76b6697f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 17 May 2016 11:20:47 +0200 Subject: [PATCH 175/182] [IMP] mis_builder: more lint --- mis_builder/models/mis_report_style.py | 2 +- mis_builder/static/src/js/mis_builder.js | 8 ++++---- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py index 7b124922..9c0541c8 100644 --- a/mis_builder/models/mis_report_style.py +++ b/mis_builder/models/mis_report_style.py @@ -15,7 +15,7 @@ class PropertyDict(dict): def __getattr__(self, name): return self.get(name) - def copy(self): # pylint: disable=copy-wo-api-one + def copy(self): # pylint: disable=copy-wo-api-one,method-required-super return PropertyDict(self) diff --git a/mis_builder/static/src/js/mis_builder.js b/mis_builder/static/src/js/mis_builder.js index 22277261..977786cf 100644 --- a/mis_builder/static/src/js/mis_builder.js +++ b/mis_builder/static/src/js/mis_builder.js @@ -34,7 +34,7 @@ var MisReport = form_common.FormWidget.extend({ var self = this; self.mis_report_instance_id = self.getParent().datarecord.id; if (self.mis_report_instance_id) { - self.getParent().dataset.context['no_destroy'] = true; + self.getParent().dataset.context.no_destroy = true; } }, @@ -42,9 +42,9 @@ var MisReport = form_common.FormWidget.extend({ var self = this; var context = {}; if (this.mis_report_instance_id){ - context['active_ids'] = [this.mis_report_instance_id]; + context.active_ids = [this.mis_report_instance_id]; } - return context + return context; }, print: function() { @@ -135,7 +135,7 @@ ActionManager.include({ var self = this; if (self.dialog_widget && self.dialog_widget.dataset && self.dialog_widget.dataset.context) { var context = self.dialog_widget.dataset.context; - if (!context['no_destroy']) { + if (!context.no_destroy) { this._super.apply(this, arguments); } } else { From 3e68d3d435ae989febc082858b7dd2235aa58f4b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 17 May 2016 12:08:45 +0200 Subject: [PATCH 176/182] [IMP] mis_builder: remove deprecated api.one --- mis_builder/models/mis_report.py | 30 +++--- mis_builder/models/mis_report_instance.py | 113 +++++++++++----------- mis_builder/models/mis_report_style.py | 8 +- 3 files changed, 80 insertions(+), 71 deletions(-) diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index ebdb78ef..15fe11cb 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -496,11 +496,12 @@ class MisReportKpi(models.Model): _order = 'sequence, id' - @api.one @api.constrains('name') def _check_name(self): - if not _is_valid_python_var(self.name): - raise UserError(_('The name must be a valid python identifier')) + for record in self: + if not _is_valid_python_var(record.name): + raise UserError(_('The name must be a valid python ' + 'identifier')) @api.onchange('name') def _onchange_name(self): @@ -595,11 +596,12 @@ class MisReportSubkpi(models.Model): translate=True) expression_ids = fields.One2many('mis.report.kpi.expression', 'subkpi_id') - @api.one @api.constrains('name') def _check_name(self): - if not _is_valid_python_var(self.name): - raise UserError(_('The name must be a valid python identifier')) + for record in self: + if not _is_valid_python_var(record.name): + raise UserError(_('The name must be a valid python ' + 'identifier')) @api.onchange('name') def _onchange_name(self): @@ -658,11 +660,11 @@ class MisReportQuery(models.Model): _name = 'mis.report.query' - @api.one @api.depends('field_ids') def _compute_field_names(self): - field_names = [field.name for field in self.field_ids] - self.field_names = ', '.join(field_names) + for record in self: + field_names = [field.name for field in record.field_ids] + record.field_names = ', '.join(field_names) name = fields.Char(size=32, required=True, string='Name') @@ -687,11 +689,12 @@ class MisReportQuery(models.Model): _order = 'name' - @api.one @api.constrains('name') def _check_name(self): - if not _is_valid_python_var(self.name): - raise UserError(_('The name must be a valid python identifier')) + for record in self: + if not _is_valid_python_var(record.name): + raise UserError(_('The name must be a valid python ' + 'identifier')) class MisReport(models.Model): @@ -767,8 +770,9 @@ class MisReport(models.Model): }) return res - @api.one + @api.multi def copy(self, default=None): + self.ensure_one() default = dict(default or {}) default['name'] = _('%s (copy)') % self.name return super(MisReport, self).copy(default) diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index 1c20efff..e0462307 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -20,59 +20,63 @@ class MisReportInstancePeriod(models.Model): are defined as an offset relative to a pivot date. """ - @api.one + @api.multi @api.depends('report_instance_id.pivot_date', 'report_instance_id.comparison_mode', 'type', 'offset', 'duration', 'mode') def _compute_dates(self): - self.date_from = False - self.date_to = False - self.valid = False - report = self.report_instance_id - d = fields.Date.from_string(report.pivot_date) - if not report.comparison_mode: - self.date_from = report.date_from - self.date_to = report.date_to - self.valid = True - elif self.mode == 'fix': - self.date_from = self.manual_date_from - self.date_to = self.manual_date_to - self.valid = True - elif self.type == 'd': - date_from = d + datetime.timedelta(days=self.offset) - date_to = date_from + \ - datetime.timedelta(days=self.duration - 1) - self.date_from = fields.Date.to_string(date_from) - self.date_to = fields.Date.to_string(date_to) - self.valid = True - elif self.type == 'w': - date_from = d - datetime.timedelta(d.weekday()) - date_from = date_from + datetime.timedelta(days=self.offset * 7) - date_to = date_from + \ - datetime.timedelta(days=(7 * self.duration) - 1) - self.date_from = fields.Date.to_string(date_from) - self.date_to = fields.Date.to_string(date_to) - self.valid = True - elif self.type == 'date_range': - date_range_obj = self.env['date.range'] - current_periods = date_range_obj.search( - [('type_id', '=', self.date_range_type_id.id), - ('date_start', '<=', d), - ('date_end', '>=', d), - ('company_id', '=', self.report_instance_id.company_id.id)]) - if current_periods: - all_periods = date_range_obj.search( - [('type_id', '=', self.date_range_type_id.id), + for record in self: + record.date_from = False + record.date_to = False + record.valid = False + report = record.report_instance_id + d = fields.Date.from_string(report.pivot_date) + if not report.comparison_mode: + record.date_from = report.date_from + record.date_to = report.date_to + record.valid = True + elif record.mode == 'fix': + record.date_from = record.manual_date_from + record.date_to = record.manual_date_to + record.valid = True + elif record.type == 'd': + date_from = d + datetime.timedelta(days=record.offset) + date_to = date_from + \ + datetime.timedelta(days=record.duration - 1) + record.date_from = fields.Date.to_string(date_from) + record.date_to = fields.Date.to_string(date_to) + record.valid = True + elif record.type == 'w': + date_from = d - datetime.timedelta(d.weekday()) + date_from = date_from + \ + datetime.timedelta(days=record.offset * 7) + date_to = date_from + \ + datetime.timedelta(days=(7 * record.duration) - 1) + record.date_from = fields.Date.to_string(date_from) + record.date_to = fields.Date.to_string(date_to) + record.valid = True + elif record.type == 'date_range': + date_range_obj = record.env['date.range'] + current_periods = date_range_obj.search( + [('type_id', '=', record.date_range_type_id.id), + ('date_start', '<=', d), + ('date_end', '>=', d), ('company_id', '=', - self.report_instance_id.company_id.id)], - order='date_start') - all_period_ids = [p.id for p in all_periods] - p = all_period_ids.index(current_periods[0].id) + self.offset - if p >= 0 and p + self.duration <= len(all_period_ids): - periods = all_periods[p:p + self.duration] - self.date_from = periods[0].date_start - self.date_to = periods[-1].date_end - self.valid = True + record.report_instance_id.company_id.id)]) + if current_periods: + all_periods = date_range_obj.search( + [('type_id', '=', record.date_range_type_id.id), + ('company_id', '=', + record.report_instance_id.company_id.id)], + order='date_start') + all_period_ids = [p.id for p in all_periods] + p = all_period_ids.index(current_periods[0].id) + \ + record.offset + if p >= 0 and p + record.duration <= len(all_period_ids): + periods = all_periods[p:p + record.duration] + record.date_from = periods[0].date_start + record.date_to = periods[-1].date_end + record.valid = True _name = 'mis.report.instance.period' @@ -174,13 +178,13 @@ class MisReportInstance(models.Model): """The MIS report instance combines everything to compute a MIS report template for a set of periods.""" - @api.one @api.depends('date') def _compute_pivot_date(self): - if self.date: - self.pivot_date = self.date - else: - self.pivot_date = fields.Date.context_today(self) + for record in self: + if record.date: + record.pivot_date = record.date + else: + record.pivot_date = fields.Date.context_today(record) @api.model def _default_company(self): @@ -250,8 +254,9 @@ class MisReportInstance(models.Model): _logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports)) return reports.unlink() - @api.one + @api.multi def copy(self, default=None): + self.ensure_one() default = dict(default or {}) default['name'] = _('%s (copy)') % self.name return super(MisReportInstance, self).copy(default) diff --git a/mis_builder/models/mis_report_style.py b/mis_builder/models/mis_report_style.py index 9c0541c8..20fb236f 100644 --- a/mis_builder/models/mis_report_style.py +++ b/mis_builder/models/mis_report_style.py @@ -45,12 +45,12 @@ class MisReportKpiStyle(models.Model): _name = 'mis.report.style' - @api.one @api.constrains('indent_level') def check_positive_val(self): - if self.indent_level < 0: - raise UserError(_('Indent level must be greater than ' - 'or equal to 0')) + for record in self: + if record.indent_level < 0: + raise UserError(_('Indent level must be greater than ' + 'or equal to 0')) _font_style_selection = [ ('normal', 'Normal'), From 022b8a8998d7f38fb40f0e82185211b2f044ab62 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 17 May 2016 13:05:05 +0200 Subject: [PATCH 177/182] [IMP] mis_builder: more lint --- .../report/mis_report_instance_xlsx.py | 2 - mis_builder/tests/test_mis_report_instance.py | 103 ++++++++---------- 2 files changed, 48 insertions(+), 57 deletions(-) diff --git a/mis_builder/report/mis_report_instance_xlsx.py b/mis_builder/report/mis_report_instance_xlsx.py index ae7767ec..68cc1a91 100644 --- a/mis_builder/report/mis_report_instance_xlsx.py +++ b/mis_builder/report/mis_report_instance_xlsx.py @@ -122,8 +122,6 @@ class MisBuilderXslx(ReportXlsx): elif cell.val is None or cell.val is AccountingNone: val = '' else: - _logger.info("*** %s %s %s", cell.row.label, - cell.subcol.label, cell.val) val = cell.val / float(cell.style_props.get('divider', 1)) sheet.write(row_pos, col_pos, val, cell_format) col_width[col_pos] = max(col_width[col_pos], diff --git a/mis_builder/tests/test_mis_report_instance.py b/mis_builder/tests/test_mis_report_instance.py index 848ed8a9..052b8601 100644 --- a/mis_builder/tests/test_mis_report_instance.py +++ b/mis_builder/tests/test_mis_report_instance.py @@ -25,23 +25,21 @@ class TestMisReportInstance(common.TransactionCase): self.report = self.env['mis.report'].create(dict( name='test report', subkpi_ids=[(0, 0, dict( - name='sk1', - description='subkpi 1', - sequence=1, - )), (0, 0, dict( - name='sk2', - description='subkpi 2', - sequence=2, - )), - ], + name='sk1', + description='subkpi 1', + sequence=1, + )), (0, 0, dict( + name='sk2', + description='subkpi 2', + sequence=2, + ))], query_ids=[(0, 0, dict( - name='partner', - model_id=partner_model_id, - field_ids=[(4, partner_debit_field_id, None)], - date_field=partner_create_date_field_id, - aggregate='sum', - )), - ], + name='partner', + model_id=partner_model_id, + field_ids=[(4, partner_debit_field_id, None)], + date_field=partner_create_date_field_id, + aggregate='sum', + ))], )) # kpi with accounting formulas self.env['mis.report.kpi'].create(dict( @@ -50,13 +48,12 @@ class TestMisReportInstance(common.TransactionCase): name='k1', multi=True, expression_ids=[(0, 0, dict( - name='bale[200%]', - subkpi_id=self.report.subkpi_ids[0].id, - )), (0, 0, dict( - name='balp[200%]', - subkpi_id=self.report.subkpi_ids[1].id, - )), - ], + name='bale[200%]', + subkpi_id=self.report.subkpi_ids[0].id, + )), (0, 0, dict( + name='balp[200%]', + subkpi_id=self.report.subkpi_ids[1].id, + ))], )) # kpi with accounting formula and query self.env['mis.report.kpi'].create(dict( @@ -65,13 +62,12 @@ class TestMisReportInstance(common.TransactionCase): name='k2', multi=True, expression_ids=[(0, 0, dict( - name='balp[200%]', - subkpi_id=self.report.subkpi_ids[0].id, - )), (0, 0, dict( - name='partner.debit', - subkpi_id=self.report.subkpi_ids[1].id, - )), - ], + name='balp[200%]', + subkpi_id=self.report.subkpi_ids[0].id, + )), (0, 0, dict( + name='partner.debit', + subkpi_id=self.report.subkpi_ids[1].id, + ))], )) # kpi with a simple expression summing other multi-valued kpis self.env['mis.report.kpi'].create(dict( @@ -88,13 +84,12 @@ class TestMisReportInstance(common.TransactionCase): name='k3', multi=True, expression_ids=[(0, 0, dict( - name='AccountingNone', - subkpi_id=self.report.subkpi_ids[0].id, - )), (0, 0, dict( - name='1.0', - subkpi_id=self.report.subkpi_ids[1].id, - )), - ], + name='AccountingNone', + subkpi_id=self.report.subkpi_ids[0].id, + )), (0, 0, dict( + name='1.0', + subkpi_id=self.report.subkpi_ids[1].id, + ))], )) # kpi with a NameError (x not defined) self.env['mis.report.kpi'].create(dict( @@ -103,13 +98,12 @@ class TestMisReportInstance(common.TransactionCase): name='k5', multi=True, expression_ids=[(0, 0, dict( - name='x', - subkpi_id=self.report.subkpi_ids[0].id, - )), (0, 0, dict( - name='1.0', - subkpi_id=self.report.subkpi_ids[1].id, - )), - ], + name='x', + subkpi_id=self.report.subkpi_ids[0].id, + )), (0, 0, dict( + name='1.0', + subkpi_id=self.report.subkpi_ids[1].id, + ))], )) # create a report instance self.report_instance = self.env['mis.report.instance'].create(dict( @@ -117,17 +111,16 @@ class TestMisReportInstance(common.TransactionCase): report_id=self.report.id, company_id=self.env.ref('base.main_company').id, period_ids=[(0, 0, dict( - name='p1', - mode='relative', - type='d', - subkpi_ids=[(4, self.report.subkpi_ids[0].id, None)], - )), (0, 0, dict( - name='p2', - mode='fix', - manual_date_from='2014-01-01', - manual_date_to='2014-12-31', - )), - ], + name='p1', + mode='relative', + type='d', + subkpi_ids=[(4, self.report.subkpi_ids[0].id, None)], + )), (0, 0, dict( + name='p2', + mode='fix', + manual_date_from='2014-01-01', + manual_date_to='2014-12-31', + ))], )) self.report_instance.period_ids[1].comparison_column_ids = \ [(4, self.report_instance.period_ids[0].id, None)] From 910cd1e6ffaafe30010feaf6409f8ca820f6c142 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 17 May 2016 17:36:28 +0200 Subject: [PATCH 178/182] [FIX] mis_builder: duplicate record ids in ACLs --- mis_builder/security/ir.model.access.csv | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mis_builder/security/ir.model.access.csv b/mis_builder/security/ir.model.access.csv index 04325c52..f6ee127c 100644 --- a/mis_builder/security/ir.model.access.csv +++ b/mis_builder/security/ir.model.access.csv @@ -9,9 +9,9 @@ manage_mis_report_instance_period,manage_mis_report_instance_period,model_mis_re access_mis_report_instance_period,access_mis_report_instance_period,model_mis_report_instance_period,base.group_user,1,0,0,0 manage_mis_report_instance,manage_mis_report_instance,model_mis_report_instance,account.group_account_manager,1,1,1,1 access_mis_report_instance,access_mis_report_instance,model_mis_report_instance,base.group_user,1,0,0,0 -access_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,account.group_account_manager,1,1,1,1 +manage_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,account.group_account_manager,1,1,1,1 access_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,base.group_user,1,0,0,0 -access_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,account.group_account_manager,1,1,1,1 +manage_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,account.group_account_manager,1,1,1,1 access_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,base.group_user,1,0,0,0 -access_mis_report_style,access_mis_report_style,model_mis_report_style,account.group_account_manager,1,0,0,0 +manage_mis_report_style,access_mis_report_style,model_mis_report_style,account.group_account_manager,1,0,0,0 access_mis_report_style,access_mis_report_style,model_mis_report_style,base.group_user,1,0,0,0 From 5569b6b6f3c7749805c70f632d631309acec8fb4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 23 May 2016 17:52:39 +0200 Subject: [PATCH 179/182] [FIX] mis_builder: better xmlid and name for vacuum cron --- mis_builder/datas/ir_cron.xml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/mis_builder/datas/ir_cron.xml b/mis_builder/datas/ir_cron.xml index f99d6959..199770c8 100644 --- a/mis_builder/datas/ir_cron.xml +++ b/mis_builder/datas/ir_cron.xml @@ -2,8 +2,8 @@ - - Vaccum temporary report + + Vacuum temporary reports 4 hours -1 From f4a9b0d08295487f2916a13ffb7c8c6562031cf4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 23 May 2016 18:03:37 +0200 Subject: [PATCH 180/182] [FIX] mis_builder: typo in docstring --- mis_builder/models/aep.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index f3c3b005..d08ab7c9 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -409,7 +409,7 @@ class AccountingExpressionProcessor(object): @classmethod def get_balances_variation(cls, company, date_from, date_to, target_move='posted'): - """ A convenience method to obtain the variantion of the + """ A convenience method to obtain the variation of the balances of all accounts over a period. :param company: From 189483bba6b218425c1bc537e631a06a015071d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Mon, 23 May 2016 18:29:46 +0200 Subject: [PATCH 181/182] [IMP] mis_builder: change precision rounding from 2 to 4 to distinguish 0 from null in initial balances This should be slightly on the safer side. Ideally, this rounding precision should come from the kpi style (which defaults to the report style), but that would be a lot of code for little benefits. --- mis_builder/models/aep.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index d08ab7c9..e50f84cc 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -266,7 +266,7 @@ class AccountingExpressionProcessor(object): debit = acc['debit'] or 0.0 credit = acc['credit'] or 0.0 if mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(debit-credit, precision_rounding=2): + float_is_zero(debit-credit, precision_rounding=4): # in initial mode, ignore accounts with 0 balance continue self._data[key][acc['account_id'][0]] = (debit, credit) @@ -311,7 +311,7 @@ class AccountingExpressionProcessor(object): # as it does not make sense to distinguish 0 from "no data" if v is not AccountingNone and \ mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(v, precision_rounding=2): + float_is_zero(v, precision_rounding=4): v = AccountingNone return '(' + repr(v) + ')' @@ -342,7 +342,7 @@ class AccountingExpressionProcessor(object): # as it does not make sense to distinguish 0 from "no data" if v is not AccountingNone and \ mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(v, precision_rounding=2): + float_is_zero(v, precision_rounding=4): v = AccountingNone return '(' + repr(v) + ')' From 53192385fb67590b83ae0f425b7ed7dda49f4a35 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?St=C3=A9phane=20Bidoul?= Date: Tue, 24 May 2016 08:31:29 +0200 Subject: [PATCH 182/182] [IMP] mis_builder: use company currency decimal place in deciding if initial balances are null or 0 --- mis_builder/models/aep.py | 53 ++++++++++++----------- mis_builder/models/mis_report.py | 7 ++- mis_builder/models/mis_report_instance.py | 5 +-- mis_builder/tests/test_aep.py | 7 ++- 4 files changed, 36 insertions(+), 36 deletions(-) diff --git a/mis_builder/models/aep.py b/mis_builder/models/aep.py index e50f84cc..25f8970d 100644 --- a/mis_builder/models/aep.py +++ b/mis_builder/models/aep.py @@ -67,8 +67,9 @@ class AccountingExpressionProcessor(object): r"(?P_[a-zA-Z0-9]+|\[.*?\])" r"(?P\[.*?\])?") - def __init__(self, env): - self.env = env + def __init__(self, company): + self.company = company + self.dp = company.currency_id.decimal_places # before done_parsing: {(domain, mode): set(account_codes)} # after done_parsing: {(domain, mode): list(account_ids)} self._map_account_ids = defaultdict(set) @@ -83,8 +84,8 @@ class AccountingExpressionProcessor(object): # to get the variation, so it's a bit slower self.smart_end = True - def _load_account_codes(self, account_codes, company): - account_model = self.env['account.account'] + def _load_account_codes(self, account_codes): + account_model = self.company.env['account.account'] exact_codes = set() for account_code in account_codes: if account_code in self._account_ids_by_code: @@ -92,19 +93,19 @@ class AccountingExpressionProcessor(object): if account_code is None: # None means we want all accounts account_ids = account_model.\ - search([('company_id', '=', company.id)]).ids + search([('company_id', '=', self.company.id)]).ids self._account_ids_by_code[account_code].update(account_ids) elif '%' in account_code: account_ids = account_model.\ search([('code', '=like', account_code), - ('company_id', '=', company.id)]).ids + ('company_id', '=', self.company.id)]).ids self._account_ids_by_code[account_code].update(account_ids) else: # search exact codes after the loop to do less queries exact_codes.add(account_code) for account in account_model.\ search([('code', 'in', list(exact_codes)), - ('company_id', '=', company.id)]): + ('company_id', '=', self.company.id)]): self._account_ids_by_code[account.code].add(account.id) def _parse_match_object(self, mo): @@ -146,13 +147,13 @@ class AccountingExpressionProcessor(object): key = (domain, mode) self._map_account_ids[key].update(account_codes) - def done_parsing(self, company): + def done_parsing(self): """Load account codes and replace account codes by account ids in map.""" for key, account_codes in self._map_account_ids.items(): # TODO _load_account_codes could be done # for all account_codes at once (also in v8) - self._load_account_codes(account_codes, company) + self._load_account_codes(account_codes) account_ids = set() for account_code in account_codes: account_ids.update(self._account_ids_by_code[account_code]) @@ -165,7 +166,7 @@ class AccountingExpressionProcessor(object): def get_aml_domain_for_expr(self, expr, date_from, date_to, - target_move, company, + target_move, account_id=None): """ Get a domain on account.move.line for an expression. @@ -197,15 +198,14 @@ class AccountingExpressionProcessor(object): if mode not in date_domain_by_mode: date_domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - mode, target_move, - company) + mode, target_move) assert aml_domains return expression.OR(aml_domains) + \ expression.OR(date_domain_by_mode.values()) def get_aml_domain_for_dates(self, date_from, date_to, mode, - target_move, company): + target_move): if mode == self.MODE_VARIATION: domain = [('date', '>=', date_from), ('date', '<=', date_to)] elif mode in (self.MODE_INITIAL, self.MODE_END): @@ -214,7 +214,8 @@ class AccountingExpressionProcessor(object): # sum from the beginning of time date_from_date = fields.Date.from_string(date_from) fy_date_from = \ - company.compute_fiscalyear_dates(date_from_date)['date_from'] + self.company.\ + compute_fiscalyear_dates(date_from_date)['date_from'] domain = ['|', ('date', '>=', fields.Date.to_string(fy_date_from)), ('user_type_id.include_initial_balance', '=', True)] @@ -225,21 +226,22 @@ class AccountingExpressionProcessor(object): elif mode == self.MODE_UNALLOCATED: date_from_date = fields.Date.from_string(date_from) fy_date_from = \ - company.compute_fiscalyear_dates(date_from_date)['date_from'] + self.company.\ + compute_fiscalyear_dates(date_from_date)['date_from'] domain = [('date', '<', fields.Date.to_string(fy_date_from)), ('user_type_id.include_initial_balance', '=', False)] if target_move == 'posted': domain.append(('move_id.state', '=', 'posted')) return expression.normalize_domain(domain) - def do_queries(self, company, date_from, date_to, + def do_queries(self, date_from, date_to, target_move='posted', additional_move_line_filter=None): """Query sums of debit and credit for all accounts and domains used in expressions. This method must be executed after done_parsing(). """ - aml_model = self.env['account.move.line'] + aml_model = self.company.env['account.move.line'] # {(domain, mode): {account_id: (debit, credit)}} self._data = defaultdict(dict) domain_by_mode = {} @@ -253,7 +255,7 @@ class AccountingExpressionProcessor(object): if mode not in domain_by_mode: domain_by_mode[mode] = \ self.get_aml_domain_for_dates(date_from, date_to, - mode, target_move, company) + mode, target_move) domain = list(domain) + domain_by_mode[mode] domain.append(('account_id', 'in', self._map_account_ids[key])) if additional_move_line_filter: @@ -266,7 +268,8 @@ class AccountingExpressionProcessor(object): debit = acc['debit'] or 0.0 credit = acc['credit'] or 0.0 if mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(debit-credit, precision_rounding=4): + float_is_zero(debit-credit, + precision_rounding=self.dp): # in initial mode, ignore accounts with 0 balance continue self._data[key][acc['account_id'][0]] = (debit, credit) @@ -311,7 +314,7 @@ class AccountingExpressionProcessor(object): # as it does not make sense to distinguish 0 from "no data" if v is not AccountingNone and \ mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(v, precision_rounding=4): + float_is_zero(v, precision_rounding=self.dp): v = AccountingNone return '(' + repr(v) + ')' @@ -342,7 +345,7 @@ class AccountingExpressionProcessor(object): # as it does not make sense to distinguish 0 from "no data" if v is not AccountingNone and \ mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ - float_is_zero(v, precision_rounding=4): + float_is_zero(v, precision_rounding=self.dp): v = AccountingNone return '(' + repr(v) + ')' @@ -365,13 +368,13 @@ class AccountingExpressionProcessor(object): def _get_balances(cls, mode, company, date_from, date_to, target_move='posted'): expr = 'deb{mode}[], crd{mode}[]'.format(mode=mode) - aep = AccountingExpressionProcessor(company.env) + aep = AccountingExpressionProcessor(company) # disable smart_end to have the data at once, instead # of initial + variation aep.smart_end = False aep.parse_expr(expr) - aep.done_parsing(company) - aep.do_queries(company, date_from, date_to, target_move) + aep.done_parsing() + aep.do_queries(date_from, date_to, target_move) return aep._data[((), mode)] @classmethod @@ -395,7 +398,7 @@ class AccountingExpressionProcessor(object): """ A convenience method to obtain the ending balances of all accounts at a given date. - It is the same as get_balances_init(date+1). + It is the same as get_balances_initial(date+1). :param company: :param date: diff --git a/mis_builder/models/mis_report.py b/mis_builder/models/mis_report.py index 15fe11cb..ed8f8215 100644 --- a/mis_builder/models/mis_report.py +++ b/mis_builder/models/mis_report.py @@ -790,11 +790,11 @@ class MisReport(models.Model): @api.multi def prepare_aep(self, company): self.ensure_one() - aep = AEP(self.env) + aep = AEP(company) for kpi in self.kpi_ids: for expression in kpi.expression_ids: aep.parse_expr(expression.name) - aep.done_parsing(company) + aep.done_parsing() return aep @api.multi @@ -915,8 +915,7 @@ class MisReport(models.Model): additional_move_line_filter = None if get_additional_move_line_filter: additional_move_line_filter = get_additional_move_line_filter() - aep.do_queries(company, - date_from, date_to, + aep.do_queries(date_from, date_to, target_move, additional_move_line_filter) diff --git a/mis_builder/models/mis_report_instance.py b/mis_builder/models/mis_report_instance.py index e0462307..8d436343 100644 --- a/mis_builder/models/mis_report_instance.py +++ b/mis_builder/models/mis_report_instance.py @@ -399,14 +399,13 @@ class MisReportInstance(models.Model): account_id = arg.get('account_id') if period_id and expr and AEP.has_account_var(expr): period = self.env['mis.report.instance.period'].browse(period_id) - aep = AEP(self.env) + aep = AEP(self.company_id) aep.parse_expr(expr) - aep.done_parsing(self.company_id) + aep.done_parsing() domain = aep.get_aml_domain_for_expr( expr, period.date_from, period.date_to, self.target_move, - self.company_id, account_id) domain.extend(period._get_additional_move_line_filter()) return { diff --git a/mis_builder/tests/test_aep.py b/mis_builder/tests/test_aep.py index 1be10b13..5b1f349a 100644 --- a/mis_builder/tests/test_aep.py +++ b/mis_builder/tests/test_aep.py @@ -66,7 +66,7 @@ class TestAEP(common.TransactionCase): debit_acc=self.account_ar, credit_acc=self.account_in) # create the AEP, and prepare the expressions we'll need - self.aep = AEP(self.env) + self.aep = AEP(self.company) self.aep.parse_expr("bali[]") self.aep.parse_expr("bale[]") self.aep.parse_expr("balp[]") @@ -81,7 +81,7 @@ class TestAEP(common.TransactionCase): self.aep.parse_expr("crdp[700I%]") self.aep.parse_expr("bal_700IN") # deprecated self.aep.parse_expr("bals[700IN]") # deprecated - self.aep.done_parsing(self.company) + self.aep.done_parsing() def _create_move(self, date, amount, debit_acc, credit_acc): move = self.move_model.create({ @@ -103,8 +103,7 @@ class TestAEP(common.TransactionCase): self.aep.do_queries( date_from=fields.Date.to_string(date_from), date_to=fields.Date.to_string(date_to), - target_move='posted', - company=self.company) + target_move='posted') def _eval(self, expr): eval_dict = {'AccountingNone': AccountingNone}