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