Pedro M. Baeza
9 years ago
41 changed files with 2506 additions and 0 deletions
-
92mis_builder/README.rst
-
27mis_builder/__init__.py
-
62mis_builder/__openerp__.py
-
42mis_builder/migrations/8.0.0.2/pre-migration.py
-
26mis_builder/models/__init__.py
-
382mis_builder/models/aep.py
-
741mis_builder/models/mis_builder.py
-
26mis_builder/report/__init__.py
-
138mis_builder/report/mis_builder_xls.py
-
67mis_builder/report/report_mis_report_instance.py
-
55mis_builder/report/report_mis_report_instance.xml
-
11mis_builder/security/ir.model.access.csv
-
13mis_builder/security/mis_builder_security.xml
-
BINmis_builder/static/description/ex_dashboard.png
-
BINmis_builder/static/description/ex_report.png
-
BINmis_builder/static/description/ex_report_template.png
-
BINmis_builder/static/description/icon.png
-
79mis_builder/static/description/icon.svg
-
13mis_builder/static/src/css/custom.css
-
BINmis_builder/static/src/img/icon.png
-
48mis_builder/static/src/js/mis_builder.js
-
62mis_builder/static/src/xml/mis_widget.xml
-
25mis_builder/tests/__init__.py
-
2mis_builder/tests/mis.report.csv
-
2mis_builder/tests/mis.report.instance.csv
-
2mis_builder/tests/mis.report.instance.period.csv
-
2mis_builder/tests/mis.report.kpi.csv
-
2mis_builder/tests/mis.report.query.csv
-
82mis_builder/tests/test_mis_builder.py
-
211mis_builder/views/mis_builder.xml
-
25mis_builder/wizard/__init__.py
-
87mis_builder/wizard/mis_builder_dashboard.py
-
33mis_builder/wizard/mis_builder_dashboard.xml
-
46mis_builder_demo/README.rst
-
23mis_builder_demo/__init__.py
-
53mis_builder_demo/__openerp__.py
-
3mis_builder_demo/mis.report.csv
-
3mis_builder_demo/mis.report.instance.csv
-
8mis_builder_demo/mis.report.instance.period.csv
-
10mis_builder_demo/mis.report.kpi.csv
-
3mis_builder_demo/mis.report.query.csv
@ -0,0 +1,92 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:alt: License: AGPL-3 |
|||
|
|||
MIS Builder |
|||
=========== |
|||
|
|||
This module allows you to build Management Information Systems dashboards. |
|||
Such style of reports presents KPI in rows and time periods in columns. |
|||
Reports mainly fetch data from account moves, but can also combine data coming |
|||
from arbitrary Odoo models. Reports can be exported to PDF, Excel and they |
|||
can be added to Odoo dashboards. |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
There is no specific installation procedure for this module. |
|||
|
|||
Configuration and Usage |
|||
======================= |
|||
|
|||
To configure this module, you need to: |
|||
|
|||
* Go to Accounting > Configuration > Financial Reports > MIS Report Templates where |
|||
you can create report templates by defining KPI's. KPI's constitute the rows of your |
|||
reports. Such report templates are time independent. |
|||
|
|||
.. figure:: static/description/ex_report_template.png |
|||
:scale: 80 % |
|||
:alt: Sample report template |
|||
|
|||
* Then in Accounting > Reporting > MIS Reports you can create report instance by |
|||
binding the templates to time period, hence defining the columns of your reports. |
|||
|
|||
.. figure:: static/description/ex_report.png |
|||
:alt: Sample report configuration |
|||
|
|||
* From the MIS Report view, you can preview the report, add it to and Odoo dashboard, |
|||
and export it to PDF or Excel. |
|||
|
|||
.. figure:: static/description/ex_dashboard.png |
|||
:alt: Sample dashboard view |
|||
|
|||
For further information, please visit: |
|||
|
|||
* https://www.odoo.com/forum/help-1 |
|||
|
|||
Known issues / Roadmap |
|||
====================== |
|||
|
|||
* 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: |
|||
|
|||
* one account charts with a few normal accounts and view accounts, |
|||
* two fiscal years, |
|||
* an opening entry in the second fiscal year, |
|||
* to test multi-company consolidation, we need a second company with it's own |
|||
account chart and two fiscal years, but without opening entry; we also need |
|||
a third company which is the parent of the other two and has a consolidation |
|||
chart of account. |
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_. |
|||
In case of trouble, please check there if your issue has already been reported. |
|||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback |
|||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Stéphane Bidoul <stephane.bidoul@acsone.eu> |
|||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu> |
|||
* Adrien Peiffer <adrien.peiffer@acsone.eu> |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
To contribute to this module, please visit http://odoo-community.org. |
@ -0,0 +1,27 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import models |
|||
from . import wizard |
|||
from . import report |
@ -0,0 +1,62 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
{ |
|||
'name': 'MIS Builder', |
|||
'version': '0.2', |
|||
'category': 'Reporting', |
|||
'summary': """ |
|||
Build 'Management Information System' Reports and Dashboards |
|||
""", |
|||
'author': 'ACSONE SA/NV,' |
|||
'Odoo Community Association (OCA)', |
|||
'website': 'http://acsone.eu', |
|||
'depends': [ |
|||
'account', |
|||
'report_xls', # OCA/reporting-engine |
|||
], |
|||
'data': [ |
|||
'wizard/mis_builder_dashboard.xml', |
|||
'views/mis_builder.xml', |
|||
'security/ir.model.access.csv', |
|||
'security/mis_builder_security.xml', |
|||
'report/report_mis_report_instance.xml', |
|||
], |
|||
'test': [ |
|||
], |
|||
'demo': [ |
|||
'tests/mis.report.kpi.csv', |
|||
'tests/mis.report.query.csv', |
|||
'tests/mis.report.csv', |
|||
'tests/mis.report.instance.period.csv', |
|||
'tests/mis.report.instance.csv', |
|||
], |
|||
'qweb': [ |
|||
'static/src/xml/*.xml' |
|||
], |
|||
'installable': True, |
|||
'application': True, |
|||
'auto_install': False, |
|||
'license': 'AGPL-3', |
|||
} |
@ -0,0 +1,42 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
|
|||
def migrate(cr, version): |
|||
if not version: |
|||
return |
|||
|
|||
cr.execute(""" |
|||
ALTER TABLE mis_report_instance |
|||
ADD COLUMN root_account INTEGER |
|||
""") |
|||
cr.execute(""" |
|||
UPDATE mis_report_instance |
|||
SET root_account = ( |
|||
SELECT id FROM account_account |
|||
WHERE parent_id IS NULL |
|||
AND company_id = mis_report_instance.company_id |
|||
LIMIT 1 |
|||
) |
|||
""") |
@ -0,0 +1,26 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import mis_builder |
|||
from . import aep |
@ -0,0 +1,382 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
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 _ |
|||
|
|||
MODE_VARIATION = 'p' |
|||
MODE_INITIAL = 'i' |
|||
MODE_END = 'e' |
|||
|
|||
|
|||
class AccountingExpressionProcessor(object): |
|||
""" Processor for accounting expressions. |
|||
|
|||
Expressions of the form <field><mode>[accounts][optional move line domain] |
|||
are supported, where: |
|||
* field is bal, crd, deb |
|||
* mode is i (initial balance), e (ending balance), |
|||
p (moves over period) |
|||
* accounts is a list of accounts, possibly containing % wildcards |
|||
* an optional domain on move lines allowing filters on eg analytic |
|||
accounts or journal |
|||
|
|||
Examples: |
|||
* bal[70]: variation of the balance of moves on account 70 |
|||
over the period (it is the same as balp[70]); |
|||
* bali[70,60]: balance of accounts 70 and 60 at the start of period; |
|||
* bale[1%]: balance of accounts starting with 1 at end of period. |
|||
|
|||
How to use: |
|||
* repeatedly invoke parse_expr() for each expression containing |
|||
accounting variables as described above; this lets the processor |
|||
group domains and modes and accounts; |
|||
* when all expressions have been parsed, invoke done_parsing() |
|||
to notify the processor that it can prepare to query (mainly |
|||
search all accounts - children, consolidation - that will need to |
|||
be queried; |
|||
* for each period, call do_queries(), then call replace_expr() for each |
|||
expression to replace accounting variables with their resulting value |
|||
for the given period. |
|||
|
|||
How it works: |
|||
* by accumulating the expressions before hand, it ensures to do the |
|||
strict minimum number of queries to the database (for each period, |
|||
one query per domain and mode); |
|||
* it queries using the orm read_group which reduces to a query with |
|||
sum on debit and credit and group by on account_id (note: it seems |
|||
the orm then does one query per account to fetch the account |
|||
name...); |
|||
* additionally, one query per view/consolidation account is done to |
|||
discover the children accounts. |
|||
""" |
|||
|
|||
ACC_RE = re.compile(r"(?P<field>\bbal|\bcrd|\bdeb)" |
|||
r"(?P<mode>[pise])?" |
|||
r"(?P<accounts>_[a-zA-Z0-9]+|\[.*?\])" |
|||
r"(?P<domain>\[.*?\])?") |
|||
|
|||
def __init__(self, env): |
|||
self.env = env |
|||
# before done_parsing: {(domain, mode): set(account_codes)} |
|||
# after done_parsing: {(domain, mode): list(account_ids)} |
|||
self._map_account_ids = defaultdict(set) |
|||
self._account_ids_by_code = defaultdict(set) |
|||
|
|||
def _load_account_codes(self, account_codes, root_account): |
|||
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) |
|||
elif '%' in account_code: |
|||
like_codes.add(account_code) |
|||
else: |
|||
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) |
|||
|
|||
def _parse_match_object(self, mo): |
|||
"""Split a match object corresponding to an accounting variable |
|||
|
|||
Returns field, mode, [account codes], (domain expression). |
|||
""" |
|||
field, mode, account_codes, domain = mo.groups() |
|||
if not mode: |
|||
mode = MODE_VARIATION |
|||
elif mode == 's': |
|||
mode = MODE_END |
|||
if account_codes.startswith('_'): |
|||
account_codes = account_codes[1:] |
|||
else: |
|||
account_codes = account_codes[1:-1] |
|||
if account_codes.strip(): |
|||
account_codes = [a.strip() for a in account_codes.split(',')] |
|||
else: |
|||
account_codes = [None] |
|||
domain = domain or '[]' |
|||
domain = tuple(safe_eval(domain)) |
|||
return field, mode, account_codes, domain |
|||
|
|||
def parse_expr(self, expr): |
|||
"""Parse an expression, extracting accounting variables. |
|||
|
|||
Domains and accounts are extracted and stored in the map |
|||
so when all expressions have been parsed, we know which |
|||
account codes to query for each domain and mode. |
|||
""" |
|||
for mo in self.ACC_RE.finditer(expr): |
|||
_, mode, account_codes, domain = self._parse_match_object(mo) |
|||
key = (domain, mode) |
|||
self._map_account_ids[key].update(account_codes) |
|||
|
|||
def done_parsing(self, root_account): |
|||
"""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) |
|||
account_ids = set() |
|||
for account_code in account_codes: |
|||
account_ids.update(self._account_ids_by_code[account_code]) |
|||
self._map_account_ids[key] = list(account_ids) |
|||
|
|||
@classmethod |
|||
def has_account_var(cls, expr): |
|||
"""Test if an string contains an accounting variable.""" |
|||
return bool(cls.ACC_RE.search(expr)) |
|||
|
|||
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. |
|||
|
|||
Prerequisite: done_parsing() must have been invoked. |
|||
|
|||
Returns a domain that can be used to search on account.move.line. |
|||
""" |
|||
aml_domains = [] |
|||
date_domain_by_mode = {} |
|||
for mo in self.ACC_RE.finditer(expr): |
|||
field, mode, account_codes, domain = self._parse_match_object(mo) |
|||
aml_domain = list(domain) |
|||
account_ids = set() |
|||
for account_code in account_codes: |
|||
account_ids.update(self._account_ids_by_code[account_code]) |
|||
aml_domain.append(('account_id', 'in', tuple(account_ids))) |
|||
if field == 'crd': |
|||
aml_domain.append(('credit', '>', 0)) |
|||
elif field == 'deb': |
|||
aml_domain.append(('debit', '>', 0)) |
|||
aml_domains.append(expression.normalize_domain(aml_domain)) |
|||
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 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, |
|||
target_move): |
|||
"""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'] |
|||
# {(domain, mode): {account_id: (debit, credit)}} |
|||
self._data = defaultdict(dict) |
|||
domain_by_mode = {} |
|||
for key in self._map_account_ids: |
|||
domain, mode = key |
|||
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])) |
|||
# fetch sum of debit/credit, grouped by account_id |
|||
accs = aml_model.read_group(domain, |
|||
['debit', 'credit', 'account_id'], |
|||
['account_id']) |
|||
for acc in accs: |
|||
self._data[key][acc['account_id'][0]] = \ |
|||
(acc['debit'] or 0.0, acc['credit'] or 0.0) |
|||
|
|||
def replace_expr(self, expr): |
|||
"""Replace accounting variables in an expression by their amount. |
|||
|
|||
Returns a new expression string. |
|||
|
|||
This method must be executed after do_queries(). |
|||
""" |
|||
def f(mo): |
|||
field, mode, account_codes, domain = self._parse_match_object(mo) |
|||
key = (domain, mode) |
|||
account_ids_data = self._data[key] |
|||
v = 0.0 |
|||
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)) |
|||
if field == 'bal': |
|||
v += debit - credit |
|||
elif field == 'deb': |
|||
v += debit |
|||
elif field == 'crd': |
|||
v += credit |
|||
return '(' + repr(v) + ')' |
|||
return self.ACC_RE.sub(f, expr) |
@ -0,0 +1,741 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
import datetime |
|||
import dateutil |
|||
import logging |
|||
import re |
|||
import time |
|||
import traceback |
|||
|
|||
import pytz |
|||
|
|||
from openerp import api, fields, models, _ |
|||
from openerp.tools.safe_eval import safe_eval |
|||
|
|||
from .aep import AccountingExpressionProcessor as AEP |
|||
|
|||
_logger = logging.getLogger(__name__) |
|||
|
|||
|
|||
class AutoStruct(object): |
|||
|
|||
def __init__(self, **kwargs): |
|||
for k, v in kwargs.items(): |
|||
setattr(self, k, v) |
|||
|
|||
|
|||
def _get_selection_label(selection, value): |
|||
for v, l in selection: |
|||
if v == value: |
|||
return l |
|||
return '' |
|||
|
|||
|
|||
def _utc_midnight(d, tz_name, add_day=0): |
|||
d = fields.Datetime.from_string(d) + datetime.timedelta(days=add_day) |
|||
utc_tz = pytz.timezone('UTC') |
|||
context_tz = pytz.timezone(tz_name) |
|||
local_timestamp = context_tz.localize(d, is_dst=False) |
|||
return fields.Datetime.to_string(local_timestamp.astimezone(utc_tz)) |
|||
|
|||
|
|||
def _python_var(var_str): |
|||
return re.sub(r'\W|^(?=\d)', '_', var_str).lower() |
|||
|
|||
|
|||
def _is_valid_python_var(name): |
|||
return re.match("[_A-Za-z][_a-zA-Z0-9]*$", name) |
|||
|
|||
|
|||
def _sum(l): |
|||
if not l: |
|||
return None |
|||
return sum(l) |
|||
|
|||
|
|||
def _avg(l): |
|||
if not l: |
|||
return None |
|||
return sum(l) / float(len(l)) |
|||
|
|||
|
|||
def _min(l): |
|||
if not l: |
|||
return None |
|||
return min(l) |
|||
|
|||
|
|||
def _max(l): |
|||
if not l: |
|||
return None |
|||
return max(l) |
|||
|
|||
|
|||
class MisReportKpi(models.Model): |
|||
""" A KPI is an element (ie a line) of a MIS report. |
|||
|
|||
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 |
|||
how to render comparison of two values of the KPI. |
|||
KPI's have a sequence and are ordered inside the MIS report. |
|||
""" |
|||
|
|||
_name = 'mis.report.kpi' |
|||
|
|||
name = fields.Char(size=32, required=True, |
|||
string='Name') |
|||
description = fields.Char(required=True, |
|||
string='Description', |
|||
translate=True) |
|||
expression = fields.Char(required=True, |
|||
string='Expression') |
|||
default_css_style = fields.Char(string='Default CSS style') |
|||
css_style = fields.Char(string='CSS style expression') |
|||
type = fields.Selection([('num', _('Numeric')), |
|||
('pct', _('Percentage')), |
|||
('str', _('String'))], |
|||
required=True, |
|||
string='Type', |
|||
default='num') |
|||
divider = fields.Selection([('1e-6', _('µ')), |
|||
('1e-3', _('m')), |
|||
('1', _('1')), |
|||
('1e3', _('k')), |
|||
('1e6', _('M'))], |
|||
string='Factor', |
|||
default='1') |
|||
dp = fields.Integer(string='Rounding', default=0) |
|||
suffix = fields.Char(size=16, string='Suffix') |
|||
compare_method = fields.Selection([('diff', _('Difference')), |
|||
('pct', _('Percentage')), |
|||
('none', _('None'))], |
|||
required=True, |
|||
string='Comparison Method', |
|||
default='pct') |
|||
sequence = fields.Integer(string='Sequence', default=100) |
|||
report_id = fields.Many2one('mis.report', |
|||
string='Report', |
|||
ondelete='cascade') |
|||
|
|||
_order = 'sequence, id' |
|||
|
|||
@api.one |
|||
@api.constrains('name') |
|||
def _check_name(self): |
|||
return _is_valid_python_var(self.name) |
|||
|
|||
@api.onchange('name') |
|||
def _onchange_name(self): |
|||
if self.name and not _is_valid_python_var(self.name): |
|||
return { |
|||
'warning': { |
|||
'title': 'Invalid name %s' % self.name, |
|||
'message': 'The name must be a valid python identifier' |
|||
} |
|||
} |
|||
|
|||
@api.onchange('description') |
|||
def _onchange_description(self): |
|||
""" construct name from description """ |
|||
if self.description and not self.name: |
|||
self.name = _python_var(self.description) |
|||
|
|||
@api.onchange('type') |
|||
def _onchange_type(self): |
|||
if self.type == 'num': |
|||
self.compare_method = 'pct' |
|||
self.divider = '1' |
|||
self.dp = 0 |
|||
elif self.type == 'pct': |
|||
self.compare_method = 'diff' |
|||
self.divider = '1' |
|||
self.dp = 0 |
|||
elif self.type == 'str': |
|||
self.compare_method = 'none' |
|||
self.divider = '' |
|||
self.dp = 0 |
|||
|
|||
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' |
|||
elif self.type == 'num': |
|||
return self._render_num(lang_id, value, self.divider, |
|||
self.dp, self.suffix) |
|||
elif self.type == 'pct': |
|||
return self._render_num(lang_id, value, 0.01, |
|||
self.dp, '%') |
|||
else: |
|||
return unicode(value) |
|||
|
|||
def render_comparison(self, lang_id, value, base_value, |
|||
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 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: |
|||
value = value / float(average_value) |
|||
if 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.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='+') |
|||
return '' |
|||
|
|||
def _render_num(self, lang_id, value, divider, |
|||
dp, suffix, sign='-'): |
|||
divider_label = _get_selection_label( |
|||
self._columns['divider'].selection, divider) |
|||
if divider_label == '1': |
|||
divider_label = '' |
|||
# format number following user language |
|||
value = round(value / float(divider or 1), dp) or 0 |
|||
value = self.env['res.lang'].browse(lang_id).format( |
|||
'%%%s.%df' % (sign, dp), |
|||
value, |
|||
grouping=True) |
|||
value = u'%s\N{NO-BREAK SPACE}%s%s' % \ |
|||
(value, divider_label, suffix or '') |
|||
value = value.replace('-', u'\N{NON-BREAKING HYPHEN}') |
|||
return value |
|||
|
|||
|
|||
class MisReportQuery(models.Model): |
|||
""" A query to fetch arbitrary data for a MIS report. |
|||
|
|||
A query works on a model and has a domain and list of fields to fetch. |
|||
At runtime, the domain is expanded with a "and" on the date/datetime field. |
|||
""" |
|||
|
|||
_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) |
|||
|
|||
name = fields.Char(size=32, required=True, |
|||
string='Name') |
|||
model_id = fields.Many2one('ir.model', required=True, |
|||
string='Model') |
|||
field_ids = fields.Many2many('ir.model.fields', required=True, |
|||
string='Fields to fetch') |
|||
field_names = fields.Char(compute='_compute_field_names', |
|||
string='Fetched fields name') |
|||
aggregate = fields.Selection([('sum', _('Sum')), |
|||
('avg', _('Average')), |
|||
('min', _('Min')), |
|||
('max', _('Max'))], |
|||
string='Aggregate') |
|||
date_field = fields.Many2one('ir.model.fields', required=True, |
|||
string='Date field', |
|||
domain=[('ttype', 'in', |
|||
('date', 'datetime'))]) |
|||
domain = fields.Char(string='Domain') |
|||
report_id = fields.Many2one('mis.report', string='Report', |
|||
ondelete='cascade') |
|||
|
|||
_order = 'name' |
|||
|
|||
@api.one |
|||
@api.constrains('name') |
|||
def _check_name(self): |
|||
return _is_valid_python_var(self.name) |
|||
|
|||
|
|||
class MisReport(models.Model): |
|||
""" A MIS report template (without period information) |
|||
|
|||
The MIS report holds: |
|||
* a list of explicit queries; the result of each query is |
|||
stored in a variable with same name as a query, containing as list |
|||
of data structures populated with attributes for each fields to fetch; |
|||
when queries have an aggregate method and no fields to group, it returns |
|||
a data structure with the aggregated fields |
|||
* a list of KPI to be evaluated based on the variables resulting |
|||
from the accounting data and queries (KPI expressions can references |
|||
queries and accounting expression - see AccoutingExpressionProcessor) |
|||
""" |
|||
|
|||
_name = 'mis.report' |
|||
|
|||
name = fields.Char(required=True, |
|||
string='Name', translate=True) |
|||
description = fields.Char(required=False, |
|||
string='Description', translate=True) |
|||
query_ids = fields.One2many('mis.report.query', 'report_id', |
|||
string='Queries') |
|||
kpi_ids = fields.One2many('mis.report.kpi', 'report_id', |
|||
string='KPI\'s') |
|||
|
|||
# TODO: kpi name cannot be start with query 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', 'type', 'offset', 'duration') |
|||
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': |
|||
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 == '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' |
|||
|
|||
name = fields.Char(size=32, required=True, |
|||
string='Description', translate=True) |
|||
type = fields.Selection([('d', _('Day')), |
|||
('w', _('Week')), |
|||
('fp', _('Fiscal Period')), |
|||
# ('fy', _('Fiscal Year')) |
|||
], |
|||
required=True, |
|||
string='Period 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") |
|||
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') |
|||
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) |
|||
|
|||
_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.multi |
|||
def drilldown(self, expr): |
|||
assert len(self) == 1 |
|||
if AEP.has_account_var(expr): |
|||
aep = AEP(self.env) |
|||
aep.parse_expr(expr) |
|||
aep.done_parsing(self.report_instance_id.root_account) |
|||
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) |
|||
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 |
|||
|
|||
def _fetch_queries(self): |
|||
assert len(self) == 1 |
|||
res = {} |
|||
for query in self.report_instance_id.report_id.query_ids: |
|||
model = self.env[query.model_id.model] |
|||
eval_context = { |
|||
'env': self.env, |
|||
'time': time, |
|||
'datetime': datetime, |
|||
'dateutil': dateutil, |
|||
# deprecated |
|||
'uid': self.env.uid, |
|||
'context': self.env.context, |
|||
} |
|||
domain = query.domain and \ |
|||
safe_eval(query.domain, eval_context) or [] |
|||
if query.date_field.ttype == 'date': |
|||
domain.extend([(query.date_field.name, '>=', self.date_from), |
|||
(query.date_field.name, '<=', self.date_to)]) |
|||
else: |
|||
datetime_from = _utc_midnight( |
|||
self.date_from, self._context.get('tz', 'UTC')) |
|||
datetime_to = _utc_midnight( |
|||
self.date_to, self._context.get('tz', 'UTC'), add_day=1) |
|||
domain.extend([(query.date_field.name, '>=', datetime_from), |
|||
(query.date_field.name, '<', datetime_to)]) |
|||
field_names = [f.name for f in query.field_ids] |
|||
if not query.aggregate: |
|||
data = model.search_read(domain, field_names) |
|||
res[query.name] = [AutoStruct(**d) for d in data] |
|||
elif query.aggregate == 'sum': |
|||
data = model.read_group( |
|||
domain, field_names, []) |
|||
s = AutoStruct(count=data[0]['__count']) |
|||
for field_name in field_names: |
|||
v = data[0][field_name] |
|||
setattr(s, field_name, v) |
|||
res[query.name] = s |
|||
else: |
|||
data = model.search_read(domain, field_names) |
|||
s = AutoStruct(count=len(data)) |
|||
if query.aggregate == 'min': |
|||
agg = _min |
|||
elif query.aggregate == 'max': |
|||
agg = _max |
|||
elif query.aggregate == 'avg': |
|||
agg = _avg |
|||
for field_name in field_names: |
|||
setattr(s, field_name, |
|||
agg([d[field_name] for d in data])) |
|||
res[query.name] = s |
|||
return res |
|||
|
|||
def _compute(self, lang_id, aep): |
|||
res = {} |
|||
|
|||
localdict = { |
|||
'registry': self.pool, |
|||
'sum': _sum, |
|||
'min': _min, |
|||
'max': _max, |
|||
'len': len, |
|||
'avg': _avg, |
|||
} |
|||
|
|||
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) |
|||
|
|||
compute_queue = self.report_instance_id.report_id.kpi_ids |
|||
recompute_queue = [] |
|||
while True: |
|||
for kpi in compute_queue: |
|||
try: |
|||
kpi_val_comment = kpi.name + " = " + kpi.expression |
|||
kpi_eval_expression = aep.replace_expr(kpi.expression) |
|||
kpi_val = safe_eval(kpi_eval_expression, localdict) |
|||
except ZeroDivisionError: |
|||
kpi_val = None |
|||
kpi_val_rendered = '#DIV/0' |
|||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),) |
|||
except (NameError, ValueError): |
|||
recompute_queue.append(kpi) |
|||
kpi_val = None |
|||
kpi_val_rendered = '#ERR' |
|||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),) |
|||
except: |
|||
kpi_val = None |
|||
kpi_val_rendered = '#ERR' |
|||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),) |
|||
else: |
|||
kpi_val_rendered = kpi.render(lang_id, kpi_val) |
|||
|
|||
localdict[kpi.name] = kpi_val |
|||
try: |
|||
kpi_style = None |
|||
if kpi.css_style: |
|||
kpi_style = safe_eval(kpi.css_style, localdict) |
|||
except: |
|||
_logger.warning("error evaluating css stype expression %s", |
|||
kpi.css_style, exc_info=True) |
|||
kpi_style = None |
|||
|
|||
drilldown = (kpi_val is not None and |
|||
AEP.has_account_var(kpi.expression)) |
|||
|
|||
res[kpi.name] = { |
|||
'val': kpi_val, |
|||
'val_r': kpi_val_rendered, |
|||
'val_c': kpi_val_comment, |
|||
'style': kpi_style, |
|||
'suffix': kpi.suffix, |
|||
'dp': kpi.dp, |
|||
'is_percentage': kpi.type == 'pct', |
|||
'period_id': self.id, |
|||
'expr': kpi.expression, |
|||
'drilldown': drilldown, |
|||
} |
|||
|
|||
if len(recompute_queue) == 0: |
|||
# nothing to recompute, we are done |
|||
break |
|||
if len(recompute_queue) == len(compute_queue): |
|||
# could not compute anything in this iteration |
|||
# (ie real Value errors or cyclic dependency) |
|||
# so we stop trying |
|||
break |
|||
# try again |
|||
compute_queue = recompute_queue |
|||
recompute_queue = [] |
|||
|
|||
return res |
|||
|
|||
|
|||
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) |
|||
|
|||
_name = 'mis.report.instance' |
|||
|
|||
name = fields.Char(required=True, |
|||
string='Name', translate=True) |
|||
description = fields.Char(required=False, |
|||
string='Description', translate=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') |
|||
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', |
|||
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) |
|||
landscape_pdf = fields.Boolean(string='Landscape PDF') |
|||
|
|||
def _format_date(self, lang_id, date): |
|||
# format date following user language |
|||
date_format = self.env['res.lang'].browse(lang_id).date_format |
|||
return datetime.datetime.strftime( |
|||
fields.Date.from_string(date), date_format) |
|||
|
|||
@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': 'new', |
|||
} |
|||
|
|||
@api.multi |
|||
def compute(self): |
|||
assert len(self) == 1 |
|||
|
|||
# prepare AccountingExpressionProcessor |
|||
aep = AEP(self.env) |
|||
for kpi in self.report_id.kpi_ids: |
|||
aep.parse_expr(kpi.expression) |
|||
aep.done_parsing(self.root_account) |
|||
|
|||
# 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 = {} |
|||
for period in self.period_ids: |
|||
if not period.valid: |
|||
continue |
|||
kpi_values = period._compute(lang_id, aep) |
|||
kpi_values_by_period_ids[period.id] = kpi_values |
|||
|
|||
# prepare header and content |
|||
header = [] |
|||
header.append({ |
|||
'kpi_name': '', |
|||
'cols': [] |
|||
}) |
|||
content = [] |
|||
rows_by_kpi_name = {} |
|||
for kpi in self.report_id.kpi_ids: |
|||
rows_by_kpi_name[kpi.name] = { |
|||
'kpi_name': kpi.description, |
|||
'cols': [], |
|||
'default_style': kpi.default_css_style |
|||
} |
|||
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 == '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) |
|||
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[0]['cols'].append(dict(name=period.name, date=header_date)) |
|||
# 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'].append(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 {'header': header, |
|||
'content': content} |
@ -0,0 +1,26 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import mis_builder_xls |
|||
from . import report_mis_report_instance |
@ -0,0 +1,138 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
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) |
@ -0,0 +1,67 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
import logging |
|||
|
|||
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" |
|||
|
|||
@api.v7 |
|||
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None, |
|||
context=None): |
|||
if ids: |
|||
report = self._get_report_from_name(cr, uid, report_name) |
|||
obj = self.pool[report.model].browse(cr, uid, ids, |
|||
context=context)[0] |
|||
context = context.copy() |
|||
if hasattr(obj, 'landscape_pdf') and obj.landscape_pdf: |
|||
context.update({'landscape': True}) |
|||
return super(Report, self).get_pdf(cr, uid, ids, report_name, |
|||
html=html, data=data, |
|||
context=context) |
@ -0,0 +1,55 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<template id="report_mis_report_instance"> |
|||
<t t-call="report.html_container"> |
|||
<t t-foreach="docs" t-as="o"> |
|||
<t t-call="report.internal_layout"> |
|||
<div class="page"> |
|||
<h2 t-field="o.name"></h2> |
|||
<table class="table table-condensed"> |
|||
<thead> |
|||
<tr> |
|||
<t t-foreach="docs_computed[o.id]['header']" t-as="h"> |
|||
<th> |
|||
<div> |
|||
<t t-esc="h_value['kpi_name']"/> |
|||
</div> |
|||
</th> |
|||
<th t-foreach="h_value['cols']" t-as="col" class="text-center"> |
|||
<div> |
|||
<t t-esc="col['name']"/> |
|||
</div> |
|||
<div> |
|||
<t t-esc="col['date']"/> |
|||
</div> |
|||
</th> |
|||
</t> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr t-foreach="docs_computed[o.id]['content']" t-as="c"> |
|||
<td t-att-style="c_value['default_style']"> |
|||
<div class="text-left"> |
|||
<t t-esc="c_value['kpi_name']"/> |
|||
</div> |
|||
</td> |
|||
<t t-foreach="c_value['cols']" t-as="value"> |
|||
<td t-att-style="c_value['default_style']"> |
|||
<div t-att-style="value_value.get('style')" class="text-right"> |
|||
<t t-esc="value_value['val_r']"/> |
|||
</div> |
|||
</td> |
|||
</t> |
|||
</tr> |
|||
</tbody> |
|||
</table> |
|||
</div> |
|||
</t> |
|||
</t> |
|||
</t> |
|||
</template> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,11 @@ |
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
|||
manage_mis_report_kpi,manage_mis_report_kpi,model_mis_report_kpi,account.group_account_manager,1,1,1,1 |
|||
access_mis_report_kpi,access_mis_report_kpi,model_mis_report_kpi,base.group_user,1,0,0,0 |
|||
manage_mis_report_query,manage_mis_report_query,model_mis_report_query,account.group_account_manager,1,1,1,1 |
|||
access_mis_report_query,access_mis_report_query,model_mis_report_query,base.group_user,1,0,0,0 |
|||
manage_mis_report,manage_mis_report,model_mis_report,account.group_account_manager,1,1,1,1 |
|||
access_mis_report,access_mis_report,model_mis_report,base.group_user,1,0,0,0 |
|||
manage_mis_report_instance_period,manage_mis_report_instance_period,model_mis_report_instance_period,account.group_account_manager,1,1,1,1 |
|||
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 |
@ -0,0 +1,13 @@ |
|||
<?xml version="1.0" encoding="utf-8"?> |
|||
<openerp> |
|||
<data noupdate="0"> |
|||
|
|||
<record id="mis_builder_multi_company_rule" model="ir.rule"> |
|||
<field name="name">Mis Builder multi company</field> |
|||
<field name="model_id" ref="model_mis_report_instance"/> |
|||
<field name="global" eval="True"/> |
|||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> |
|||
</record> |
|||
|
|||
</data> |
|||
</openerp> |
After Width: 749 | Height: 391 | Size: 34 KiB |
After Width: 1111 | Height: 560 | Size: 81 KiB |
After Width: 1174 | Height: 654 | Size: 98 KiB |
After Width: 128 | Height: 128 | Size: 9.2 KiB |
79
mis_builder/static/description/icon.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,13 @@ |
|||
.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; |
|||
} |
After Width: 64 | Height: 64 | Size: 3.4 KiB |
@ -0,0 +1,48 @@ |
|||
openerp.mis_builder = function(instance) { |
|||
|
|||
instance.mis_builder.MisReport = instance.web.form.FormWidget.extend({ |
|||
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(); |
|||
}); |
|||
}, |
|||
|
|||
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 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); |
|||
} |
|||
}); |
|||
} |
|||
}, |
|||
}); |
|||
|
|||
instance.web.form.custom_widgets.add('mis_report', 'instance.mis_builder.MisReport'); |
|||
} |
@ -0,0 +1,62 @@ |
|||
<template> |
|||
<t t-name="mis_builder.MisReport"> |
|||
<p> </p> |
|||
<table t-if="widget.mis_report_data" class="oe_list_content mis_builder"> |
|||
<thead> |
|||
<tr class="oe_list_header_columns"> |
|||
<t t-foreach="widget.mis_report_data.header" t-as="h"> |
|||
<th class="oe_list_header_char"> |
|||
<div> |
|||
<t t-esc="h_value.kpi_name"/> |
|||
</div> |
|||
</th> |
|||
<th t-foreach="h_value.cols" t-as="col" class="oe_list_header_char mis_builder_ralign"> |
|||
<div> |
|||
<t t-esc="col.name"/> |
|||
</div> |
|||
<div> |
|||
<t t-esc="col.date"/> |
|||
</div> |
|||
</th> |
|||
</t> |
|||
</tr> |
|||
</thead> |
|||
<tbody> |
|||
<tr t-foreach="widget.mis_report_data.content" t-as="c"> |
|||
<td t-att="{'style': c_value.default_style}"> |
|||
<div> |
|||
<t t-esc="c_value.kpi_name"/> |
|||
</div> |
|||
</td> |
|||
<t t-foreach="c_value.cols" t-as="value"> |
|||
<td t-att="{'style': c_value.default_style}" class="mis_builder_ralign"> |
|||
<div t-att="{'style': value_value.style, 'title': value_value.val_c}"> |
|||
<t t-if="value_value.drilldown"> |
|||
<a href="javascript:void(0)" |
|||
class="mis_builder_drilldown" |
|||
t-att-data-drilldown="JSON.stringify(value_value.drilldown)" |
|||
t-att-data-period-id="JSON.stringify(value_value.period_id)" |
|||
t-att-data-expr="JSON.stringify(value_value.expr)" |
|||
> |
|||
<t t-esc="value_value.val_r"/> |
|||
</a> |
|||
</t> |
|||
<t t-if="!value_value.drilldown"> |
|||
<t t-esc="value_value.val_r"/> |
|||
</t> |
|||
</div> |
|||
</td> |
|||
</t> |
|||
</tr> |
|||
</tbody> |
|||
<tfoot> |
|||
<tr> |
|||
<td class="oe_list_footer" /> |
|||
<t t-foreach="widget.mis_report_data.header" t-as="f"> |
|||
<td t-foreach="f_value.cols" class="oe_list_footer" /> |
|||
</t> |
|||
</tr> |
|||
</tfoot> |
|||
</table> |
|||
</t> |
|||
</template> |
@ -0,0 +1,25 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import test_mis_builder |
@ -0,0 +1,2 @@ |
|||
"id","description","kpi_ids/id","name","query_ids/id" |
|||
"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" |
@ -0,0 +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" |
@ -0,0 +1,2 @@ |
|||
"id","duration","name","offset","type","sequence" |
|||
"mis_report_instance_period_test","1","today","","Day","" |
@ -0,0 +1,2 @@ |
|||
"id","compare_method","description","expression","divider","name","dp","sequence","type","suffix" |
|||
"mis_report_kpi_test","Percentage","total test","len(test)","","total_test","","1","Numeric","" |
@ -0,0 +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" |
@ -0,0 +1,82 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
import openerp.tests.common as common |
|||
|
|||
from ..models import mis_builder |
|||
|
|||
|
|||
class test_mis_builder(common.TransactionCase): |
|||
|
|||
def setUp(self): |
|||
super(test_mis_builder, 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 a model without company_id field : |
|||
# account.analytic.balance |
|||
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': False, |
|||
'cols': [{'period_id': self.ref('mis_builder.' |
|||
'mis_report_instance_' |
|||
'period_test'), |
|||
'style': None, |
|||
'suffix': False, |
|||
'expr': 'len(test)', |
|||
'val_c': 'total_test = len(test)', |
|||
'val': 0, |
|||
'val_r': u'0\xa0', |
|||
'is_percentage': False, |
|||
'dp': 0, |
|||
'drilldown': False}] |
|||
}], |
|||
'header': |
|||
[{'kpi_name': '', |
|||
'cols': [{'date': '07/31/2014', |
|||
'name': u'today'}] |
|||
}], |
|||
}, data) |
@ -0,0 +1,211 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<template id="assets_backend" name="mis_builder" inherit_id="web.assets_backend"> |
|||
<xpath expr="." position="inside"> |
|||
<link rel="stylesheet" href="/mis_builder/static/src/css/custom.css"/> |
|||
<script type="text/javascript" src="/mis_builder/static/src/js/mis_builder.js"></script> |
|||
</xpath> |
|||
</template> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_view_tree"> |
|||
<field name="name">mis.report.view.tree</field> |
|||
<field name="model">mis.report</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="MIS Reports"> |
|||
<field name="name"/> |
|||
<field name="description"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_view_form"> |
|||
<field name="name">mis.report.view.form</field> |
|||
<field name="model">mis.report</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="MIS Report" version="7.0"> |
|||
<sheet> |
|||
<group col="2"> |
|||
<field name="name"/> |
|||
<field name="description"/> |
|||
<field name="query_ids"> |
|||
<tree string="Queries" editable="bottom"> |
|||
<field name="name"/> |
|||
<field name="model_id"/> |
|||
<field name="field_ids" domain="[('model_id', '=', model_id)]" widget="many2many_tags"/> |
|||
<field name="field_names"/> |
|||
<field name="aggregate"/> |
|||
<field name="date_field" domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"/> |
|||
<field name="domain"/> |
|||
</tree> |
|||
</field> |
|||
<field name="kpi_ids"> |
|||
<tree string="KPI's" editable="bottom"> |
|||
<field name="sequence" widget="handle"/> |
|||
<field name="description"/> |
|||
<field name="name"/> |
|||
<field name="expression"/> |
|||
<field name="type"/> |
|||
<field name="dp" attrs="{'invisible': [('type', '=', 'str')]}"/> |
|||
<field name="divider" attrs="{'invisible': [('type', '=', 'str')]}"/> |
|||
<field name="suffix"/> |
|||
<field name="compare_method" attrs="{'invisible': [('type', '=', 'str')]}"/> |
|||
<field name="default_css_style"/> |
|||
<field name="css_style"/> |
|||
</tree> |
|||
</field> |
|||
</group> |
|||
<group col="2" string="Legend (for expression)"> |
|||
<group> |
|||
<label colspan="2" string="Expressions are of the form <field><mode>[accounts][domain]"/> |
|||
<label colspan="2" string="Possible values for 'field' can be:"/> |
|||
<group> |
|||
<label colspan="2" string="* bal for balance (debit - credit)"/> |
|||
<label colspan="2" string="* crd for credit"/> |
|||
<label colspan="2" string="* deb for debit"/> |
|||
</group> |
|||
<label colspan="2" string="Possible values for 'mode' are:"/> |
|||
<group> |
|||
<label colspan="2" string="* nothing or p: variation over the period"/> |
|||
<label colspan="2" string="* i: at the beginning of the period"/> |
|||
<label colspan="2" string="* e: at the end of the period"/> |
|||
</group> |
|||
<label colspan="2" string="'accounts' is a comma-separated list of account codes, possibly containing %% wildcards"/> |
|||
<label colspan="2" string="'domain' is an optional filter on move lines (eg to filter on analytic accounts or journal)"/> |
|||
</group> |
|||
<group> |
|||
<label colspan="2" string="Examples"/> |
|||
<group> |
|||
<label colspan="2" string="* bal[70]: variation of the balance of account 70 over the period (it is the same as balp[70]);"/> |
|||
<label colspan="2" string="* bali[70,60]: initial balance of accounts 70 and 60;"/> |
|||
<label colspan="2" string="* bale[1%%]: balance of accounts starting with 1 at end of period."/> |
|||
</group> |
|||
</group> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="mis_report_view_action"> |
|||
<field name="name">MIS Report Templates</field> |
|||
<field name="view_id" ref="mis_report_view_tree"/> |
|||
<field name="res_model">mis.report</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">tree,form</field> |
|||
</record> |
|||
|
|||
<menuitem id="mis_report_view_menu" parent="account.menu_account_reports" name="MIS Report Templates" action="mis_report_view_action" sequence="21"/> |
|||
|
|||
<record id="xls_export" model="ir.actions.report.xml"> |
|||
<field name="name">MIS report instance XLS report</field> |
|||
<field name="model">mis.report.instance</field> |
|||
<field name="type">ir.actions.report.xml</field> |
|||
<field name="report_name">mis.report.instance.xls</field> |
|||
<field name="report_type">xls</field> |
|||
<field name="auto" eval="False"/> |
|||
</record> |
|||
|
|||
<record id="qweb_pdf_export" model="ir.actions.report.xml"> |
|||
<field name="name">MIS report instance QWEB PDF report</field> |
|||
<field name="model">mis.report.instance</field> |
|||
<field name="type">ir.actions.report.xml</field> |
|||
<field name="report_name">mis_builder.report_mis_report_instance</field> |
|||
<field name="report_type">qweb-pdf</field> |
|||
<field name="auto" eval="False"/> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_instance_result_view_form"> |
|||
<field name="name">mis.report.instance.result.view.form</field> |
|||
<field name="model">mis.report.instance</field> |
|||
<field name="priority" eval="17"/> |
|||
<field name="arch" type="xml"> |
|||
<form string="MIS Report Result" version="7.0"> |
|||
<widget type="mis_report"></widget> |
|||
<button icon="gtk-print" name="%(qweb_pdf_export)d" string="Print" type="action" colspan="2"/> |
|||
<button icon="gtk-execute" name="%(xls_export)d" string="Export" type="action" colspan="2"/> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_instance_view_tree"> |
|||
<field name="name">mis.report.instance.view.tree</field> |
|||
<field name="model">mis.report.instance</field> |
|||
<field name="arch" type="xml"> |
|||
<tree string="MIS Report Instances"> |
|||
<field name="name"/> |
|||
<field name="description"/> |
|||
<field name="report_id"/> |
|||
<field name="target_move"/> |
|||
<field name="pivot_date"/> |
|||
<field name="company_id"/> |
|||
</tree> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_instance_view_form"> |
|||
<field name="name">mis.report.instance.view.form</field> |
|||
<field name="model">mis.report.instance</field> |
|||
<field name="priority" eval="16"/> |
|||
<field name="arch" type="xml"> |
|||
<form string="MIS Report Instance" version="7.0"> |
|||
<sheet> |
|||
<div class="oe_title"> |
|||
<div class="oe_edit_only"> |
|||
<label for="name"/> |
|||
</div> |
|||
<h1> |
|||
<field name="name" placeholder="Name"/> |
|||
</h1> |
|||
</div> |
|||
<div class="oe_right oe_button_box" name="buttons"> |
|||
<button type="object" name="preview" string="Preview" icon="gtk-print-preview" /> |
|||
<button type="action" name="%(qweb_pdf_export)d" string="Print" icon="gtk-print" /> |
|||
<button type="action" name="%(xls_export)d" string="Export" icon="gtk-execute" /> |
|||
<button type="action" name="%(mis_report_instance_add_to_dashboard_action)d" string="Add to dashboard" icon="gtk-add" /> |
|||
</div> |
|||
<group col="4"> |
|||
<field name="report_id" colspan="4"/> |
|||
<field name="description"/> |
|||
<field name="landscape_pdf" /> |
|||
<field name="root_account"/> |
|||
<field name="company_id" groups="base.group_multi_company"/> |
|||
<field name="date"/> |
|||
<field name="target_move"/> |
|||
<field name="period_ids" colspan="4"> |
|||
<tree string="KPI's" editable="bottom" colors="red:valid==False"> |
|||
<field name="sequence" widget="handle"/> |
|||
<field name="name"/> |
|||
<field name="type"/> |
|||
<field name="offset"/> |
|||
<field name="duration"/> |
|||
<field name="normalize_factor"/> |
|||
<field name="date_from"/> |
|||
<field name="date_to"/> |
|||
<field name="period_from"/> |
|||
<field name="period_to"/> |
|||
<field name="valid" invisible="1"/> |
|||
<field name="report_instance_id" invisible="1"/> |
|||
<field name="id" invisible="1"/> |
|||
<field name="comparison_column_ids" domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" widget="many2many_tags"/> |
|||
</tree> |
|||
</field> |
|||
</group> |
|||
</sheet> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="mis_report_instance_view_action"> |
|||
<field name="name">MIS Reports</field> |
|||
<field name="view_id" ref="mis_report_instance_view_tree"/> |
|||
<field name="res_model">mis.report.instance</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">tree,form</field> |
|||
</record> |
|||
|
|||
<menuitem id="mis_report_instance_view_menu" parent="account.menu_finance_reports" name="MIS Reports" action="mis_report_instance_view_action" sequence="101"/> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,25 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from . import mis_builder_dashboard |
@ -0,0 +1,87 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for Odoo, Management Information System Builder |
|||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
from openerp.osv import orm, fields |
|||
from lxml import etree |
|||
|
|||
|
|||
class add_mis_report_instance_dashboard(orm.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')]"), |
|||
} |
|||
|
|||
def default_get(self, cr, uid, fields, context=None): |
|||
if context is None: |
|||
context = {} |
|||
if context.get('active_id'): |
|||
res = super(add_mis_report_instance_dashboard, self).default_get( |
|||
cr, uid, fields, context=context) |
|||
# get report instance name |
|||
res['name'] = self.pool['mis.report.instance'].read( |
|||
cr, uid, context['active_id'], ['name'])['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] |
|||
# 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, |
|||
{'name': 'mis.report.instance.result.view.action.%d' |
|||
% context['active_id'], |
|||
'res_model': 'mis.report.instance', |
|||
'res_id': context['active_id'], |
|||
'target': 'current', |
|||
'view_mode': 'form', |
|||
'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 |
|||
if last_customization: |
|||
arch = self.pool['ir.ui.view.custom'].read( |
|||
cr, uid, last_customization[0], ['arch'])['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)}) |
|||
|
|||
return {'type': 'ir.actions.act_window_close', } |
@ -0,0 +1,33 @@ |
|||
<?xml version="1.0" encoding="UTF-8"?> |
|||
<openerp> |
|||
<data> |
|||
|
|||
<record model="ir.ui.view" id="mis_report_instance_add_to_dashboard_form_view"> |
|||
<field name="name">add.mis.report.instance.dashboard.wizard.view</field> |
|||
<field name="model">add.mis.report.instance.dashboard.wizard</field> |
|||
<field name="arch" type="xml"> |
|||
<form string="Add to dashboard" version="7.0"> |
|||
<group> |
|||
<field name="name"/> |
|||
<field name="dashboard_id"/> |
|||
</group> |
|||
<footer> |
|||
<button name="action_add_to_dashboard" string="Add to dashboard" type="object" default_focus="1" class="oe_highlight"/> |
|||
or |
|||
<button string="Cancel" class="oe_link" special="cancel"/> |
|||
</footer> |
|||
</form> |
|||
</field> |
|||
</record> |
|||
|
|||
<record model="ir.actions.act_window" id="mis_report_instance_add_to_dashboard_action"> |
|||
<field name="name">Add to dashboard</field> |
|||
<field name="res_model">add.mis.report.instance.dashboard.wizard</field> |
|||
<field name="view_type">form</field> |
|||
<field name="view_mode">form</field> |
|||
<field name="view_id" ref="mis_report_instance_add_to_dashboard_form_view"/> |
|||
<field name="target">new</field> |
|||
</record> |
|||
|
|||
</data> |
|||
</openerp> |
@ -0,0 +1,46 @@ |
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|||
:alt: License: AGPL-3 |
|||
|
|||
MIS Builder demo data |
|||
===================== |
|||
|
|||
This module adds some demo data for the mis_builder module. |
|||
|
|||
Installation |
|||
============ |
|||
|
|||
There is no specific installation procedure for this module. |
|||
|
|||
|
|||
Bug Tracker |
|||
=========== |
|||
|
|||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_. |
|||
In case of trouble, please check there if your issue has already been reported. |
|||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback |
|||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|||
|
|||
Credits |
|||
======= |
|||
|
|||
Contributors |
|||
------------ |
|||
|
|||
* Stéphane Bidoul <stephane.bidoul@acsone.eu> |
|||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu> |
|||
* Adrien Peiffer <adrien.peiffer@acsone.eu> |
|||
|
|||
Maintainer |
|||
---------- |
|||
|
|||
.. image:: https://odoo-community.org/logo.png |
|||
:alt: Odoo Community Association |
|||
:target: https://odoo-community.org |
|||
|
|||
This module is maintained by the OCA. |
|||
|
|||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|||
mission is to support the collaborative development of Odoo features and |
|||
promote its widespread use. |
|||
|
|||
To contribute to this module, please visit http://odoo-community.org. |
@ -0,0 +1,23 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for OpenERP, Management Information System Builder |
|||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
@ -0,0 +1,53 @@ |
|||
# -*- encoding: utf-8 -*- |
|||
############################################################################## |
|||
# |
|||
# mis_builder module for OpenERP, Management Information System Builder |
|||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>) |
|||
# |
|||
# 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 <http://www.gnu.org/licenses/>. |
|||
# |
|||
############################################################################## |
|||
|
|||
{ |
|||
'name': 'MIS Builder demo data', |
|||
'version': '0.1', |
|||
'category': 'Reporting', |
|||
'summary': """ |
|||
Demo data for the mis_builder module |
|||
""", |
|||
'author': 'ACSONE SA/NV,' |
|||
'Odoo Community Association (OCA)', |
|||
'website': 'http://acsone.eu', |
|||
'depends': [ |
|||
'account_accountant', |
|||
'mis_builder', |
|||
'crm' |
|||
], |
|||
'data': [ |
|||
], |
|||
'demo': [ |
|||
'mis.report.kpi.csv', |
|||
'mis.report.query.csv', |
|||
'mis.report.csv', |
|||
'mis.report.instance.period.csv', |
|||
'mis.report.instance.csv', |
|||
], |
|||
'installable': True, |
|||
'application': False, |
|||
'auto_install': False, |
|||
'license': 'AGPL-3', |
|||
} |
@ -0,0 +1,3 @@ |
|||
"id","description","kpi_ids/id","name","query_ids/id" |
|||
"mis_report","","mis_report_kpi_1,mis_report_kpi_2,mis_report_kpi_3,mis_report_kpi_4,mis_report_kpi_5","Demo margin report","mis_report_query" |
|||
"mis_report_phonecall","","mis_report_phonecall_kpi_1,mis_report_phonecall_kpi_2,mis_report_phonecall_kpi_3,mis_report_phonecall_kpi_4","Demo phonecall report","mis_report_phonecall_query" |
@ -0,0 +1,3 @@ |
|||
id,date,description,name,period_ids/id,report_id/id,root_account/id |
|||
mis_report_instance,,,Test-margin-report-instance,"mis_report_instance_period_1,mis_report_instance_period_2,mis_report_instance_period_3,mis_report_instance_period_4",mis_report,account.chart0 |
|||
mis_report_phonecall_instance,,,Test phonecall report instance,"mis_report_phonecall_instance_period_1,mis_report_phonecall_instance_period_2,mis_report_phonecall_instance_period_3",mis_report_phonecall,account.chart0 |
@ -0,0 +1,8 @@ |
|||
id,duration,name,offset,type,sequence |
|||
mis_report_instance_period_1,1,today,0,Day,1 |
|||
mis_report_instance_period_2,1,yesterday,-1,Day,2 |
|||
mis_report_instance_period_3,1,last week,-1,Week,3 |
|||
mis_report_instance_period_4,1,last period,-1,Fiscal Period,4 |
|||
mis_report_phonecall_instance_period_1,1,today,0,Day,1 |
|||
mis_report_phonecall_instance_period_2,1,this period,0,Fiscal Period,2 |
|||
mis_report_phonecall_instance_period_3,1,previous period,-1,Fiscal Period,3 |
@ -0,0 +1,10 @@ |
|||
id,compare_method,description,expression,divider,name,dp,sequence,type,suffix |
|||
mis_report_kpi_1,Percentage,CA,-bal[X2001],,ca,,1,Numeric,€ |
|||
mis_report_kpi_2,Percentage,CAHT invoice,sum([s.amount_untaxed for s in inv]),,total_invoice,,2,Numeric,€ |
|||
mis_report_kpi_3,Percentage,Cost,bal[X2110],,cost,,3,Numeric,€ |
|||
mis_report_kpi_4,Percentage,Profit,ca - cost,,profit,,4,Numeric,€ |
|||
mis_report_kpi_5,Difference,Margin,profit/ca,,margin,,5,Percentage,% |
|||
mis_report_phonecall_kpi_1,Percentage,Total phone call,len(phone),,total_phone_call,,1,Numeric, |
|||
mis_report_phonecall_kpi_2,Percentage,Average duration phone call,sum([p.duration for p in phone])/total_phone_call,,average_duration_phone_call,2,2,Numeric, |
|||
mis_report_phonecall_kpi_3,Percentage,Total converted phone call,sum([p.opportunity_id and 1 or 0 for p in phone]),,phone_call_convert,,3,Numeric, |
|||
mis_report_phonecall_kpi_4,Percentage,Average duration converted phone call,sum([p.opportunity_id and p.duration or 0 for p in phone]),,average_convert_duration_phone_c,2,4,Numeric, |
@ -0,0 +1,3 @@ |
|||
"id","date_field/id","domain","field_ids/id","model_id/id","name" |
|||
"mis_report_query","account.field_account_invoice_date_invoice","","account.field_account_invoice_amount_untaxed","account.model_account_invoice","inv" |
|||
"mis_report_phonecall_query","crm.field_crm_phonecall_date","","crm.field_crm_phonecall_duration,crm.field_crm_phonecall_opportunity_id","crm.model_crm_phonecall","phone" |
Write
Preview
Loading…
Cancel
Save
Reference in new issue