Browse Source
Merge pull request #189 from oca-sorrento/9.0-mis_builder-sorrento
Merge pull request #189 from oca-sorrento/9.0-mis_builder-sorrento
[9.0] mis_builder Sorrentopull/197/head
Leonardo Pistone
9 years ago
67 changed files with 4272 additions and 1908 deletions
-
1.travis.yml
-
12account_financial_report_webkit/report/aged_partner_balance.py
-
5account_financial_report_webkit/report/common_partner_balance_reports.py
-
7account_financial_report_webkit/report/common_reports.py
-
4account_financial_report_webkit/report/open_invoices.py
-
4account_financial_report_webkit/report/partners_ledger.py
-
5account_financial_report_webkit/report/webkit_parser_header_fix.py
-
3account_financial_report_webkit_xls/report/general_ledger_xls.py
-
34account_financial_report_webkit_xls/report/open_invoices_xls.py
-
8account_financial_report_webkit_xls/report/partners_balance_xls.py
-
5account_financial_report_webkit_xls/report/trial_balance_xls.py
-
18account_journal_report_xls/report/nov_account_journal.py
-
83mis_builder/CHANGES.rst
-
19mis_builder/README.rst
-
26mis_builder/__init__.py
-
46mis_builder/__openerp__.py
-
18mis_builder/datas/ir_cron.xml
-
2mis_builder/migrations/8.0.0.2/pre-migration.py
-
30mis_builder/models/__init__.py
-
191mis_builder/models/accounting_none.py
-
460mis_builder/models/aep.py
-
26mis_builder/models/aggregate.py
-
15mis_builder/models/data_error.py
-
751mis_builder/models/mis_builder.py
-
1021mis_builder/models/mis_report.py
-
422mis_builder/models/mis_report_instance.py
-
276mis_builder/models/mis_report_style.py
-
33mis_builder/models/mis_safe_eval.py
-
131mis_builder/models/simple_array.py
-
34mis_builder/report/__init__.py
-
138mis_builder/report/mis_builder_xls.py
-
27mis_builder/report/mis_report_instance_qweb.py
-
89mis_builder/report/mis_report_instance_qweb.xml
-
140mis_builder/report/mis_report_instance_xlsx.py
-
15mis_builder/report/mis_report_instance_xlsx.xml
-
67mis_builder/report/report_mis_report_instance.py
-
55mis_builder/report/report_mis_report_instance.xml
-
6mis_builder/security/ir.model.access.csv
-
19mis_builder/static/src/css/custom.css
-
45mis_builder/static/src/css/report.css
-
183mis_builder/static/src/js/mis_builder.js
-
108mis_builder/static/src/xml/mis_widget.xml
-
36mis_builder/tests/__init__.py
-
4mis_builder/tests/mis.report.csv
-
4mis_builder/tests/mis.report.instance.csv
-
4mis_builder/tests/mis.report.instance.period.csv
-
4mis_builder/tests/mis.report.kpi.csv
-
2mis_builder/tests/mis.report.query.csv
-
12mis_builder/tests/test_accounting_none.py
-
226mis_builder/tests/test_aep.py
-
12mis_builder/tests/test_aggregate.py
-
41mis_builder/tests/test_fetch_query.py
-
82mis_builder/tests/test_mis_builder.py
-
141mis_builder/tests/test_mis_report_instance.py
-
27mis_builder/tests/test_mis_safe_eval.py
-
158mis_builder/tests/test_render.py
-
12mis_builder/tests/test_simple_array.py
-
25mis_builder/tests/test_utc_midnight.py
-
211mis_builder/views/mis_builder.xml
-
170mis_builder/views/mis_report.xml
-
204mis_builder/views/mis_report_instance.xml
-
80mis_builder/views/mis_report_style.xml
-
26mis_builder/wizard/__init__.py
-
111mis_builder/wizard/mis_builder_dashboard.py
-
2mis_builder/wizard/mis_builder_dashboard.xml
-
2mis_builder_demo/__openerp__.py
-
2oca_dependencies.txt
@ -0,0 +1,83 @@ |
|||||
|
Changelog |
||||
|
--------- |
||||
|
|
||||
|
.. Future (?) |
||||
|
.. ~~~~~~~~~~ |
||||
|
.. |
||||
|
.. * |
||||
|
|
||||
|
9.0.2.0.0 (2016-??-??) |
||||
|
~~~~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
Part of the work for this release has been done at the Sorrento sprint |
||||
|
April 26-29, 2016. The rest (ie a major refactoring) has been done in |
||||
|
the weeks after. |
||||
|
|
||||
|
* [IMP] hide button box in edit mode on the report instance settings form |
||||
|
* [FIX] Fix sum aggregation of non-stored fields (issue #178) |
||||
|
* [IMP] There is now a default style at the report level |
||||
|
* [CHG] Number display properties (rounding, prefix, suffix, factor) are |
||||
|
now defined in styles |
||||
|
* [CHG] Percentage difference are rounded to 1 digit instead of the kpi's |
||||
|
rounding, as the KPI rounding does not make sense in this case |
||||
|
* [CHG] The divider suffix (k, M, etc) is not inserted automatically anymore |
||||
|
because it is inconsistent when working with prefixes; you need to add it |
||||
|
manually in the suffix |
||||
|
* [IMP] AccountingExpressionProcessor now supports 'balu' expressions |
||||
|
to obtain the unallocated profit/loss of previous fiscal years; |
||||
|
get_unallocated_pl is the corresponding convenience method |
||||
|
* [IMP] AccountingExpressionProcessor now has easy methods to obtain |
||||
|
balances by account: get_balances_initial, get_balances_end, |
||||
|
get_balances_variation |
||||
|
* [IMP] there is now an auto-expand feature to automatically display |
||||
|
a detail by account for selected kpis |
||||
|
* [IMP] the kpi and period lists are now manipulated through forms instead |
||||
|
of directly in the tree views |
||||
|
* [IMP] it is now possible to create a report through a wizard, such |
||||
|
reports are deemed temporary and available through a "Last Reports Generated" |
||||
|
menu, they are garbaged collected automatically, unless saved permanently, |
||||
|
which can be done using a Save button |
||||
|
* [IMP] there is now a beginner mode to configure simple reports with |
||||
|
only one period |
||||
|
* [IMP] it is now easier to configure periods with fixed start/end dates |
||||
|
* [IMP] the new sub-kpi mechanism allows the creation of columns |
||||
|
with multiple values, or columns with different values |
||||
|
* [IMP] thanks to the new style model, the Excel export is now styled |
||||
|
* [IMP] a new style model is now used to centralize style configuration |
||||
|
* [FIX] use =like instead of like to search for accounts, because |
||||
|
the % are added by the user in the expressions |
||||
|
* [FIX] Correctly compute the initial balance of income and expense account |
||||
|
based on the start of the fiscal year |
||||
|
* [IMP] Support date ranges (from OCA/server-tools/date_range) as a more |
||||
|
flexible alternative to fiscal periods |
||||
|
* v9 migration: fiscal periods are removed, account charts are removed, |
||||
|
consolidation accounts have been removed |
||||
|
|
||||
|
8.0.1.0.0 (2016-04-27) |
||||
|
~~~~~~~~~~~~~~~~~~~~~~ |
||||
|
|
||||
|
* The copy of a MIS Report Instance now copies period. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/181 |
||||
|
* The copy of a MIS Report Template now copies KPIs and queries. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/177 |
||||
|
* Usability: the default view for MIS Report instances is now the rendered preview, |
||||
|
and the settings are accessible through a gear icon in the list view and |
||||
|
a button in the preview. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/170 |
||||
|
* Display blank cells instead of 0.0 when there is no data. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/169 |
||||
|
* Usability: better layout of the MIS Report periods settings on small screens. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/167 |
||||
|
* Include the download buttons inside the MIS Builder widget, and refactor |
||||
|
the widget to open the door to analytic filtering in the previews. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/151 |
||||
|
* Add KPI rendering prefixes (so you can print $ in front of the value). |
||||
|
https://github.com/OCA/account-financial-reporting/pull/158 |
||||
|
* Add hooks for analytic filtering. |
||||
|
https://github.com/OCA/account-financial-reporting/pull/128 |
||||
|
https://github.com/OCA/account-financial-reporting/pull/131 |
||||
|
|
||||
|
8.0.0.2.0 |
||||
|
~~~~~~~~~ |
||||
|
|
||||
|
Pre-history. Or rather, you need to look at the git log. |
@ -1,26 +1,6 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
from . import models |
from . import models |
||||
from . import wizard |
from . import wizard |
||||
|
@ -0,0 +1,18 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data noupdate="1"> |
||||
|
|
||||
|
<record id="ir_cron_vacuum_temp_reports" model="ir.cron"> |
||||
|
<field name="name">Vacuum temporary reports</field> |
||||
|
<field name="interval_number">4</field> |
||||
|
<field name="interval_type">hours</field> |
||||
|
<field name="numbercall">-1</field> |
||||
|
<field eval="False" name="doall"/> |
||||
|
<field eval="'mis.report.instance'" name="model"/> |
||||
|
<field eval="'_vacuum_report'" name="function"/> |
||||
|
<field eval="'(24,)'" name="args"/> |
||||
|
<field name="active" eval="True" /> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -1,26 +1,8 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
from . import mis_builder |
|
||||
|
from . import mis_report |
||||
|
from . import mis_report_instance |
||||
|
from . import mis_report_style |
||||
from . import aep |
from . import aep |
@ -0,0 +1,191 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Thomas Binsfeld |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
""" |
||||
|
Provides the AccountingNone singleton. |
||||
|
|
||||
|
AccountingNone is a null value that dissolves in basic arithmetic operations, |
||||
|
as illustrated in the examples below. In comparisons, AccountingNone behaves |
||||
|
the same as zero. |
||||
|
|
||||
|
>>> 1 + 1 |
||||
|
2 |
||||
|
>>> 1 + AccountingNone |
||||
|
1 |
||||
|
>>> AccountingNone + 1 |
||||
|
1 |
||||
|
>>> AccountingNone + None |
||||
|
AccountingNone |
||||
|
>>> None + AccountingNone |
||||
|
AccountingNone |
||||
|
>>> +AccountingNone |
||||
|
AccountingNone |
||||
|
>>> -AccountingNone |
||||
|
AccountingNone |
||||
|
>>> -(AccountingNone) |
||||
|
AccountingNone |
||||
|
>>> AccountingNone - 1 |
||||
|
-1 |
||||
|
>>> 1 - AccountingNone |
||||
|
1 |
||||
|
>>> abs(AccountingNone) |
||||
|
AccountingNone |
||||
|
>>> AccountingNone - None |
||||
|
AccountingNone |
||||
|
>>> None - AccountingNone |
||||
|
AccountingNone |
||||
|
>>> AccountingNone / 2 |
||||
|
0.0 |
||||
|
>>> 2 / AccountingNone |
||||
|
Traceback (most recent call last): |
||||
|
... |
||||
|
ZeroDivisionError |
||||
|
>>> AccountingNone / AccountingNone |
||||
|
AccountingNone |
||||
|
>>> AccountingNone // 2 |
||||
|
0.0 |
||||
|
>>> 2 // AccountingNone |
||||
|
Traceback (most recent call last): |
||||
|
... |
||||
|
ZeroDivisionError |
||||
|
>>> AccountingNone // AccountingNone |
||||
|
AccountingNone |
||||
|
>>> AccountingNone * 2 |
||||
|
0.0 |
||||
|
>>> 2 * AccountingNone |
||||
|
0.0 |
||||
|
>>> AccountingNone * AccountingNone |
||||
|
AccountingNone |
||||
|
>>> AccountingNone * None |
||||
|
AccountingNone |
||||
|
>>> None * AccountingNone |
||||
|
AccountingNone |
||||
|
>>> str(AccountingNone) |
||||
|
'' |
||||
|
>>> bool(AccountingNone) |
||||
|
False |
||||
|
>>> AccountingNone > 0 |
||||
|
False |
||||
|
>>> AccountingNone < 0 |
||||
|
False |
||||
|
>>> AccountingNone < 1 |
||||
|
True |
||||
|
>>> AccountingNone > 1 |
||||
|
False |
||||
|
>>> 0 < AccountingNone |
||||
|
False |
||||
|
>>> 0 > AccountingNone |
||||
|
False |
||||
|
>>> 1 < AccountingNone |
||||
|
False |
||||
|
>>> 1 > AccountingNone |
||||
|
True |
||||
|
>>> AccountingNone == 0 |
||||
|
True |
||||
|
>>> AccountingNone == 0.0 |
||||
|
True |
||||
|
>>> AccountingNone == None |
||||
|
True |
||||
|
""" |
||||
|
|
||||
|
__all__ = ['AccountingNone'] |
||||
|
|
||||
|
|
||||
|
class AccountingNoneType(object): |
||||
|
|
||||
|
def __add__(self, other): |
||||
|
if other is None: |
||||
|
return AccountingNone |
||||
|
return other |
||||
|
|
||||
|
__radd__ = __add__ |
||||
|
|
||||
|
def __sub__(self, other): |
||||
|
if other is None: |
||||
|
return AccountingNone |
||||
|
return -other |
||||
|
|
||||
|
def __rsub__(self, other): |
||||
|
if other is None: |
||||
|
return AccountingNone |
||||
|
return other |
||||
|
|
||||
|
def __iadd__(self, other): |
||||
|
if other is None: |
||||
|
return AccountingNone |
||||
|
return other |
||||
|
|
||||
|
def __isub__(self, other): |
||||
|
if other is None: |
||||
|
return AccountingNone |
||||
|
return -other |
||||
|
|
||||
|
def __abs__(self): |
||||
|
return self |
||||
|
|
||||
|
def __pos__(self): |
||||
|
return self |
||||
|
|
||||
|
def __neg__(self): |
||||
|
return self |
||||
|
|
||||
|
def __div__(self, other): |
||||
|
if other is AccountingNone: |
||||
|
return AccountingNone |
||||
|
return 0.0 |
||||
|
|
||||
|
def __rdiv__(self, other): |
||||
|
raise ZeroDivisionError |
||||
|
|
||||
|
def __floordiv__(self, other): |
||||
|
if other is AccountingNone: |
||||
|
return AccountingNone |
||||
|
return 0.0 |
||||
|
|
||||
|
def __rfloordiv__(self, other): |
||||
|
raise ZeroDivisionError |
||||
|
|
||||
|
def __truediv__(self, other): |
||||
|
if other is AccountingNone: |
||||
|
return AccountingNone |
||||
|
return 0.0 |
||||
|
|
||||
|
def __rtruediv__(self, other): |
||||
|
raise ZeroDivisionError |
||||
|
|
||||
|
def __mul__(self, other): |
||||
|
if other is None or other is AccountingNone: |
||||
|
return AccountingNone |
||||
|
return 0.0 |
||||
|
|
||||
|
__rmul__ = __mul__ |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return 'AccountingNone' |
||||
|
|
||||
|
def __str__(self): |
||||
|
return '' |
||||
|
|
||||
|
def __nonzero__(self): |
||||
|
return False |
||||
|
|
||||
|
def __bool__(self): |
||||
|
return False |
||||
|
|
||||
|
def __eq__(self, other): |
||||
|
return other == 0 or other is None or other is AccountingNone |
||||
|
|
||||
|
def __lt__(self, other): |
||||
|
return 0 < other |
||||
|
|
||||
|
def __gt__(self, other): |
||||
|
return 0 > other |
||||
|
|
||||
|
|
||||
|
AccountingNone = AccountingNoneType() |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
import doctest |
||||
|
doctest.testmod() |
@ -0,0 +1,15 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# © 2016 Akretion (<http://akretion.com>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
|
||||
|
class DataError(Exception): |
||||
|
|
||||
|
def __init__(self, name, msg): |
||||
|
self.name = name |
||||
|
self.msg = msg |
||||
|
|
||||
|
|
||||
|
class NameDataError(DataError): |
||||
|
pass |
@ -1,751 +0,0 @@ |
|||||
# -*- 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, _, exceptions |
|
||||
from openerp.tools.safe_eval import safe_eval |
|
||||
|
|
||||
from .aep import AccountingExpressionProcessor as AEP |
|
||||
from .aggregate import _sum, _avg, _min, _max |
|
||||
|
|
||||
_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) |
|
||||
|
|
||||
|
|
||||
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): |
|
||||
if not _is_valid_python_var(self.name): |
|
||||
raise exception.Warning(_('The name must be a valid python identifier')) |
|
||||
|
|
||||
@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): |
|
||||
if not _is_valid_python_var(self.name): |
|
||||
raise exception.Warning(_('The name must be a valid python identifier')) |
|
||||
|
|
||||
|
|
||||
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 _get_additional_move_line_filter(self): |
|
||||
""" Prepare a filter to apply on all move lines |
|
||||
|
|
||||
This filter is applied with a AND operator on all |
|
||||
accounting expression domains. This hook is intended |
|
||||
to be inherited, and is useful to implement filtering |
|
||||
on analytic dimensions or operational units. |
|
||||
|
|
||||
Returns an Odoo domain expression (a python list) |
|
||||
compatible with account.move.line.""" |
|
||||
self.ensure_one() |
|
||||
return [] |
|
||||
|
|
||||
@api.multi |
|
||||
def _get_additional_query_filter(self, query): |
|
||||
""" Prepare an additional filter to apply on the query |
|
||||
|
|
||||
This filter is combined to the query domain with a AND |
|
||||
operator. This hook is intended |
|
||||
to be inherited, and is useful to implement filtering |
|
||||
on analytic dimensions or operational units. |
|
||||
|
|
||||
Returns an Odoo domain expression (a python list) |
|
||||
compatible with the model of the query.""" |
|
||||
self.ensure_one() |
|
||||
return [] |
|
||||
|
|
||||
@api.multi |
|
||||
def drilldown(self, expr): |
|
||||
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) |
|
||||
domain.extend(self._get_additional_move_line_filter()) |
|
||||
return { |
|
||||
'name': expr + ' - ' + self.name, |
|
||||
'domain': domain, |
|
||||
'type': 'ir.actions.act_window', |
|
||||
'res_model': 'account.move.line', |
|
||||
'views': [[False, 'list'], [False, 'form']], |
|
||||
'view_type': 'list', |
|
||||
'view_mode': 'list', |
|
||||
'target': 'current', |
|
||||
} |
|
||||
else: |
|
||||
return False |
|
||||
|
|
||||
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 [] |
|
||||
domain.extend(self._get_additional_query_filter(query)) |
|
||||
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, |
|
||||
self._get_additional_move_line_filter()) |
|
||||
|
|
||||
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} |
|
1021
mis_builder/models/mis_report.py
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -0,0 +1,422 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from openerp import api, fields, models, _ |
||||
|
|
||||
|
import datetime |
||||
|
import logging |
||||
|
|
||||
|
from .aep import AccountingExpressionProcessor as AEP |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
class MisReportInstancePeriod(models.Model): |
||||
|
""" A MIS report instance has the logic to compute |
||||
|
a report template for a given date period. |
||||
|
|
||||
|
Periods have a duration (day, week, fiscal period) and |
||||
|
are defined as an offset relative to a pivot date. |
||||
|
""" |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends('report_instance_id.pivot_date', |
||||
|
'report_instance_id.comparison_mode', |
||||
|
'type', 'offset', 'duration', 'mode') |
||||
|
def _compute_dates(self): |
||||
|
for record in self: |
||||
|
record.date_from = False |
||||
|
record.date_to = False |
||||
|
record.valid = False |
||||
|
report = record.report_instance_id |
||||
|
d = fields.Date.from_string(report.pivot_date) |
||||
|
if not report.comparison_mode: |
||||
|
record.date_from = report.date_from |
||||
|
record.date_to = report.date_to |
||||
|
record.valid = True |
||||
|
elif record.mode == 'fix': |
||||
|
record.date_from = record.manual_date_from |
||||
|
record.date_to = record.manual_date_to |
||||
|
record.valid = True |
||||
|
elif record.type == 'd': |
||||
|
date_from = d + datetime.timedelta(days=record.offset) |
||||
|
date_to = date_from + \ |
||||
|
datetime.timedelta(days=record.duration - 1) |
||||
|
record.date_from = fields.Date.to_string(date_from) |
||||
|
record.date_to = fields.Date.to_string(date_to) |
||||
|
record.valid = True |
||||
|
elif record.type == 'w': |
||||
|
date_from = d - datetime.timedelta(d.weekday()) |
||||
|
date_from = date_from + \ |
||||
|
datetime.timedelta(days=record.offset * 7) |
||||
|
date_to = date_from + \ |
||||
|
datetime.timedelta(days=(7 * record.duration) - 1) |
||||
|
record.date_from = fields.Date.to_string(date_from) |
||||
|
record.date_to = fields.Date.to_string(date_to) |
||||
|
record.valid = True |
||||
|
elif record.type == 'date_range': |
||||
|
date_range_obj = record.env['date.range'] |
||||
|
current_periods = date_range_obj.search( |
||||
|
[('type_id', '=', record.date_range_type_id.id), |
||||
|
('date_start', '<=', d), |
||||
|
('date_end', '>=', d), |
||||
|
('company_id', '=', |
||||
|
record.report_instance_id.company_id.id)]) |
||||
|
if current_periods: |
||||
|
all_periods = date_range_obj.search( |
||||
|
[('type_id', '=', record.date_range_type_id.id), |
||||
|
('company_id', '=', |
||||
|
record.report_instance_id.company_id.id)], |
||||
|
order='date_start') |
||||
|
all_period_ids = [p.id for p in all_periods] |
||||
|
p = all_period_ids.index(current_periods[0].id) + \ |
||||
|
record.offset |
||||
|
if p >= 0 and p + record.duration <= len(all_period_ids): |
||||
|
periods = all_periods[p:p + record.duration] |
||||
|
record.date_from = periods[0].date_start |
||||
|
record.date_to = periods[-1].date_end |
||||
|
record.valid = True |
||||
|
|
||||
|
_name = 'mis.report.instance.period' |
||||
|
|
||||
|
name = fields.Char(size=32, required=True, |
||||
|
string='Description', translate=True) |
||||
|
mode = fields.Selection([('fix', 'Fix'), |
||||
|
('relative', 'Relative'), |
||||
|
], required=True, |
||||
|
default='fix') |
||||
|
type = fields.Selection([('d', _('Day')), |
||||
|
('w', _('Week')), |
||||
|
('date_range', _('Date Range')) |
||||
|
], |
||||
|
string='Period type') |
||||
|
date_range_type_id = fields.Many2one( |
||||
|
comodel_name='date.range.type', string='Date Range Type') |
||||
|
offset = fields.Integer(string='Offset', |
||||
|
help='Offset from current period', |
||||
|
default=-1) |
||||
|
duration = fields.Integer(string='Duration', |
||||
|
help='Number of periods', |
||||
|
default=1) |
||||
|
date_from = fields.Date(compute='_compute_dates', string="From") |
||||
|
date_to = fields.Date(compute='_compute_dates', string="To") |
||||
|
manual_date_from = fields.Date(string="From") |
||||
|
manual_date_to = fields.Date(string="To") |
||||
|
date_range_id = fields.Many2one( |
||||
|
comodel_name='date.range', |
||||
|
string='Date Range') |
||||
|
valid = fields.Boolean(compute='_compute_dates', |
||||
|
type='boolean', |
||||
|
string='Valid') |
||||
|
sequence = fields.Integer(string='Sequence', default=100) |
||||
|
report_instance_id = fields.Many2one('mis.report.instance', |
||||
|
string='Report Instance', |
||||
|
ondelete='cascade') |
||||
|
comparison_column_ids = fields.Many2many( |
||||
|
comodel_name='mis.report.instance.period', |
||||
|
relation='mis_report_instance_period_rel', |
||||
|
column1='period_id', |
||||
|
column2='compare_period_id', |
||||
|
string='Compare with') |
||||
|
normalize_factor = fields.Integer( |
||||
|
string='Factor', |
||||
|
help='Factor to use to normalize the period (used in comparison', |
||||
|
default=1) |
||||
|
subkpi_ids = fields.Many2many( |
||||
|
'mis.report.subkpi', |
||||
|
string="Sub KPI Filter") |
||||
|
|
||||
|
_order = 'sequence, id' |
||||
|
|
||||
|
_sql_constraints = [ |
||||
|
('duration', 'CHECK (duration>0)', |
||||
|
'Wrong duration, it must be positive!'), |
||||
|
('normalize_factor', 'CHECK (normalize_factor>0)', |
||||
|
'Wrong normalize factor, it must be positive!'), |
||||
|
('name_unique', 'unique(name, report_instance_id)', |
||||
|
'Period name should be unique by report'), |
||||
|
] |
||||
|
|
||||
|
@api.onchange('date_range_id') |
||||
|
def onchange_date_range(self): |
||||
|
for record in self: |
||||
|
record.manual_date_from = record.date_range_id.date_start |
||||
|
record.manual_date_to = record.date_range_id.date_end |
||||
|
record.name = record.date_range_id.name |
||||
|
|
||||
|
@api.multi |
||||
|
def _get_additional_move_line_filter(self): |
||||
|
""" Prepare a filter to apply on all move lines |
||||
|
|
||||
|
This filter is applied with a AND operator on all |
||||
|
accounting expression domains. This hook is intended |
||||
|
to be inherited, and is useful to implement filtering |
||||
|
on analytic dimensions or operational units. |
||||
|
|
||||
|
Returns an Odoo domain expression (a python list) |
||||
|
compatible with account.move.line.""" |
||||
|
self.ensure_one() |
||||
|
return [] |
||||
|
|
||||
|
@api.multi |
||||
|
def _get_additional_query_filter(self, query): |
||||
|
""" Prepare an additional filter to apply on the query |
||||
|
|
||||
|
This filter is combined to the query domain with a AND |
||||
|
operator. This hook is intended |
||||
|
to be inherited, and is useful to implement filtering |
||||
|
on analytic dimensions or operational units. |
||||
|
|
||||
|
Returns an Odoo domain expression (a python list) |
||||
|
compatible with the model of the query.""" |
||||
|
self.ensure_one() |
||||
|
return [] |
||||
|
|
||||
|
|
||||
|
class MisReportInstance(models.Model): |
||||
|
"""The MIS report instance combines everything to compute |
||||
|
a MIS report template for a set of periods.""" |
||||
|
|
||||
|
@api.depends('date') |
||||
|
def _compute_pivot_date(self): |
||||
|
for record in self: |
||||
|
if record.date: |
||||
|
record.pivot_date = record.date |
||||
|
else: |
||||
|
record.pivot_date = fields.Date.context_today(record) |
||||
|
|
||||
|
@api.model |
||||
|
def _default_company(self): |
||||
|
return self.env['res.company'].\ |
||||
|
_company_default_get('mis.report.instance') |
||||
|
|
||||
|
_name = 'mis.report.instance' |
||||
|
|
||||
|
name = fields.Char(required=True, |
||||
|
string='Name', translate=True) |
||||
|
description = fields.Char(related='report_id.description', |
||||
|
readonly=True) |
||||
|
date = fields.Date(string='Base date', |
||||
|
help='Report base date ' |
||||
|
'(leave empty to use current date)') |
||||
|
pivot_date = fields.Date(compute='_compute_pivot_date', |
||||
|
string="Pivot date") |
||||
|
report_id = fields.Many2one('mis.report', |
||||
|
required=True, |
||||
|
string='Report') |
||||
|
period_ids = fields.One2many('mis.report.instance.period', |
||||
|
'report_instance_id', |
||||
|
required=True, |
||||
|
string='Periods', |
||||
|
copy=True) |
||||
|
target_move = fields.Selection([('posted', 'All Posted Entries'), |
||||
|
('all', 'All Entries')], |
||||
|
string='Target Moves', |
||||
|
required=True, |
||||
|
default='posted') |
||||
|
company_id = fields.Many2one(comodel_name='res.company', |
||||
|
string='Company', |
||||
|
default=_default_company, |
||||
|
required=True) |
||||
|
landscape_pdf = fields.Boolean(string='Landscape PDF') |
||||
|
comparison_mode = fields.Boolean( |
||||
|
compute="_compute_comparison_mode", |
||||
|
inverse="_inverse_comparison_mode") |
||||
|
date_range_id = fields.Many2one( |
||||
|
comodel_name='date.range', |
||||
|
string='Date Range') |
||||
|
date_from = fields.Date(string="From") |
||||
|
date_to = fields.Date(string="To") |
||||
|
temporary = fields.Boolean(default=False) |
||||
|
|
||||
|
@api.multi |
||||
|
def save_report(self): |
||||
|
self.ensure_one() |
||||
|
self.write({'temporary': False}) |
||||
|
action = self.env.ref('mis_builder.mis_report_instance_view_action') |
||||
|
res = action.read()[0] |
||||
|
view = self.env.ref('mis_builder.mis_report_instance_view_form') |
||||
|
res.update({ |
||||
|
'views': [(view.id, 'form')], |
||||
|
'res_id': self.id, |
||||
|
}) |
||||
|
return res |
||||
|
|
||||
|
@api.model |
||||
|
def _vacuum_report(self, hours=24): |
||||
|
clear_date = fields.Datetime.to_string( |
||||
|
datetime.datetime.now() - datetime.timedelta(hours=hours)) |
||||
|
reports = self.search([ |
||||
|
('write_date', '<', clear_date), |
||||
|
('temporary', '=', True), |
||||
|
]) |
||||
|
_logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports)) |
||||
|
return reports.unlink() |
||||
|
|
||||
|
@api.multi |
||||
|
def copy(self, default=None): |
||||
|
self.ensure_one() |
||||
|
default = dict(default or {}) |
||||
|
default['name'] = _('%s (copy)') % self.name |
||||
|
return super(MisReportInstance, self).copy(default) |
||||
|
|
||||
|
def _format_date(self, date): |
||||
|
# format date following user language |
||||
|
lang_model = self.env['res.lang'] |
||||
|
lang_id = lang_model._lang_get(self.env.user.lang) |
||||
|
date_format = lang_model.browse(lang_id).date_format |
||||
|
return datetime.datetime.strftime( |
||||
|
fields.Date.from_string(date), date_format) |
||||
|
|
||||
|
@api.multi |
||||
|
@api.depends('date_from') |
||||
|
def _compute_comparison_mode(self): |
||||
|
for instance in self: |
||||
|
instance.comparison_mode = bool(instance.period_ids) and\ |
||||
|
not bool(instance.date_from) |
||||
|
|
||||
|
@api.multi |
||||
|
def _inverse_comparison_mode(self): |
||||
|
for record in self: |
||||
|
if not record.comparison_mode: |
||||
|
if not record.date_from: |
||||
|
record.date_from = datetime.now() |
||||
|
if not record.date_to: |
||||
|
record.date_to = datetime.now() |
||||
|
record.period_ids.unlink() |
||||
|
record.write({'period_ids': [ |
||||
|
(0, 0, { |
||||
|
'name': 'Default', |
||||
|
'type': 'd', |
||||
|
}) |
||||
|
]}) |
||||
|
else: |
||||
|
record.date_from = None |
||||
|
record.date_to = None |
||||
|
|
||||
|
@api.onchange('date_range_id') |
||||
|
def onchange_date_range(self): |
||||
|
for record in self: |
||||
|
record.date_from = record.date_range_id.date_start |
||||
|
record.date_to = record.date_range_id.date_end |
||||
|
|
||||
|
@api.multi |
||||
|
def preview(self): |
||||
|
self.ensure_one() |
||||
|
view_id = self.env.ref('mis_builder.' |
||||
|
'mis_report_instance_result_view_form') |
||||
|
return { |
||||
|
'type': 'ir.actions.act_window', |
||||
|
'res_model': 'mis.report.instance', |
||||
|
'res_id': self.id, |
||||
|
'view_mode': 'form', |
||||
|
'view_type': 'form', |
||||
|
'view_id': view_id.id, |
||||
|
'target': 'current', |
||||
|
} |
||||
|
|
||||
|
@api.multi |
||||
|
def print_pdf(self): |
||||
|
self.ensure_one() |
||||
|
return { |
||||
|
'name': 'MIS report instance QWEB PDF report', |
||||
|
'model': 'mis.report.instance', |
||||
|
'type': 'ir.actions.report.xml', |
||||
|
'report_name': 'mis_builder.report_mis_report_instance', |
||||
|
'report_type': 'qweb-pdf', |
||||
|
'context': self.env.context, |
||||
|
} |
||||
|
|
||||
|
@api.multi |
||||
|
def export_xls(self): |
||||
|
self.ensure_one() |
||||
|
return { |
||||
|
'name': 'MIS report instance XLSX report', |
||||
|
'model': 'mis.report.instance', |
||||
|
'type': 'ir.actions.report.xml', |
||||
|
'report_name': 'mis.report.instance.xlsx', |
||||
|
'report_type': 'xlsx', |
||||
|
'context': self.env.context, |
||||
|
} |
||||
|
|
||||
|
@api.multi |
||||
|
def display_settings(self): |
||||
|
assert len(self.ids) <= 1 |
||||
|
view_id = self.env.ref('mis_builder.mis_report_instance_view_form') |
||||
|
return { |
||||
|
'type': 'ir.actions.act_window', |
||||
|
'res_model': 'mis.report.instance', |
||||
|
'res_id': self.id if self.id else False, |
||||
|
'view_mode': 'form', |
||||
|
'view_type': 'form', |
||||
|
'views': [(view_id.id, 'form')], |
||||
|
'view_id': view_id.id, |
||||
|
'target': 'current', |
||||
|
} |
||||
|
|
||||
|
@api.multi |
||||
|
def _compute_matrix(self): |
||||
|
self.ensure_one() |
||||
|
aep = self.report_id.prepare_aep(self.company_id) |
||||
|
kpi_matrix = self.report_id.prepare_kpi_matrix() |
||||
|
for period in self.period_ids: |
||||
|
if period.date_from == period.date_to: |
||||
|
comment = self._format_date(period.date_from) |
||||
|
else: |
||||
|
date_from = self._format_date(period.date_from) |
||||
|
date_to = self._format_date(period.date_to) |
||||
|
comment = _('from %s to %s') % (date_from, date_to) |
||||
|
self.report_id.declare_and_compute_period( |
||||
|
kpi_matrix, |
||||
|
period.id, |
||||
|
period.name, |
||||
|
comment, |
||||
|
aep, |
||||
|
period.date_from, |
||||
|
period.date_to, |
||||
|
self.target_move, |
||||
|
self.company_id, |
||||
|
period.subkpi_ids, |
||||
|
period._get_additional_move_line_filter, |
||||
|
period._get_additional_query_filter) |
||||
|
for comparison_column in period.comparison_column_ids: |
||||
|
kpi_matrix.declare_comparison(period.id, comparison_column.id) |
||||
|
kpi_matrix.compute_comparisons() |
||||
|
return kpi_matrix |
||||
|
|
||||
|
@api.multi |
||||
|
def compute(self): |
||||
|
self.ensure_one() |
||||
|
kpi_matrix = self._compute_matrix() |
||||
|
return kpi_matrix.as_dict() |
||||
|
|
||||
|
@api.multi |
||||
|
def drilldown(self, arg): |
||||
|
self.ensure_one() |
||||
|
period_id = arg.get('period_id') |
||||
|
expr = arg.get('expr') |
||||
|
account_id = arg.get('account_id') |
||||
|
if period_id and expr and AEP.has_account_var(expr): |
||||
|
period = self.env['mis.report.instance.period'].browse(period_id) |
||||
|
aep = AEP(self.company_id) |
||||
|
aep.parse_expr(expr) |
||||
|
aep.done_parsing() |
||||
|
domain = aep.get_aml_domain_for_expr( |
||||
|
expr, |
||||
|
period.date_from, period.date_to, |
||||
|
self.target_move, |
||||
|
account_id) |
||||
|
domain.extend(period._get_additional_move_line_filter()) |
||||
|
return { |
||||
|
'name': u'{} - {}'.format(expr, period.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 |
@ -0,0 +1,276 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 Therp BV (<http://therp.nl>) |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from openerp import api, fields, models, _ |
||||
|
from openerp.exceptions import UserError |
||||
|
|
||||
|
from .accounting_none import AccountingNone |
||||
|
from .data_error import DataError |
||||
|
|
||||
|
|
||||
|
class PropertyDict(dict): |
||||
|
|
||||
|
def __getattr__(self, name): |
||||
|
return self.get(name) |
||||
|
|
||||
|
def copy(self): # pylint: disable=copy-wo-api-one,method-required-super |
||||
|
return PropertyDict(self) |
||||
|
|
||||
|
|
||||
|
PROPS = [ |
||||
|
'color', |
||||
|
'background_color', |
||||
|
'font_style', |
||||
|
'font_weight', |
||||
|
'font_size', |
||||
|
'indent_level', |
||||
|
'prefix', |
||||
|
'suffix', |
||||
|
'dp', |
||||
|
'divider', |
||||
|
] |
||||
|
|
||||
|
TYPE_NUM = 'num' |
||||
|
TYPE_PCT = 'pct' |
||||
|
TYPE_STR = 'str' |
||||
|
|
||||
|
CMP_DIFF = 'diff' |
||||
|
CMP_PCT = 'pct' |
||||
|
CMP_NONE = 'none' |
||||
|
|
||||
|
|
||||
|
class MisReportKpiStyle(models.Model): |
||||
|
|
||||
|
_name = 'mis.report.style' |
||||
|
|
||||
|
@api.constrains('indent_level') |
||||
|
def check_positive_val(self): |
||||
|
for record in self: |
||||
|
if record.indent_level < 0: |
||||
|
raise UserError(_('Indent level must be greater than ' |
||||
|
'or equal to 0')) |
||||
|
|
||||
|
_font_style_selection = [ |
||||
|
('normal', 'Normal'), |
||||
|
('italic', 'Italic'), |
||||
|
] |
||||
|
|
||||
|
_font_weight_selection = [ |
||||
|
('nornal', 'Normal'), |
||||
|
('bold', 'Bold'), |
||||
|
] |
||||
|
|
||||
|
_font_size_selection = [ |
||||
|
('medium', 'medium'), |
||||
|
('xx-small', 'xx-small'), |
||||
|
('x-small', 'x-small'), |
||||
|
('small', 'small'), |
||||
|
('large', 'large'), |
||||
|
('x-large', 'x-large'), |
||||
|
('xx-large', 'xx-large'), |
||||
|
] |
||||
|
|
||||
|
_font_size_to_xlsx_size = { |
||||
|
'medium': 11, |
||||
|
'xx-small': 5, |
||||
|
'x-small': 7, |
||||
|
'small': 9, |
||||
|
'large': 13, |
||||
|
'x-large': 15, |
||||
|
'xx-large': 17 |
||||
|
} |
||||
|
|
||||
|
# style name |
||||
|
# TODO enforce uniqueness |
||||
|
name = fields.Char(string='Style name', required=True) |
||||
|
|
||||
|
# color |
||||
|
color_inherit = fields.Boolean(default=True) |
||||
|
color = fields.Char( |
||||
|
string='Text color', |
||||
|
help='Text color in valid RGB code (from #000000 to #FFFFFF)', |
||||
|
default='#000000', |
||||
|
) |
||||
|
background_color_inherit = fields.Boolean(default=True) |
||||
|
background_color = fields.Char( |
||||
|
help='Background color in valid RGB code (from #000000 to #FFFFFF)', |
||||
|
default='#FFFFFF', |
||||
|
) |
||||
|
# font |
||||
|
font_style_inherit = fields.Boolean(default=True) |
||||
|
font_style = fields.Selection( |
||||
|
selection=_font_style_selection, |
||||
|
) |
||||
|
font_weight_inherit = fields.Boolean(default=True) |
||||
|
font_weight = fields.Selection( |
||||
|
selection=_font_weight_selection |
||||
|
) |
||||
|
font_size_inherit = fields.Boolean(default=True) |
||||
|
font_size = fields.Selection( |
||||
|
selection=_font_size_selection |
||||
|
) |
||||
|
# indent |
||||
|
indent_level_inherit = fields.Boolean(default=True) |
||||
|
indent_level = fields.Integer() |
||||
|
# number format |
||||
|
prefix_inherit = fields.Boolean(default=True) |
||||
|
prefix = fields.Char(size=16, string='Prefix') |
||||
|
suffix_inherit = fields.Boolean(default=True) |
||||
|
suffix = fields.Char(size=16, string='Suffix') |
||||
|
dp_inherit = fields.Boolean(default=True) |
||||
|
dp = fields.Integer(string='Rounding', default=0) |
||||
|
divider_inherit = fields.Boolean(default=True) |
||||
|
divider = fields.Selection([('1e-6', _('µ')), |
||||
|
('1e-3', _('m')), |
||||
|
('1', _('1')), |
||||
|
('1e3', _('k')), |
||||
|
('1e6', _('M'))], |
||||
|
string='Factor', |
||||
|
default='1') |
||||
|
|
||||
|
@api.model |
||||
|
def merge(self, styles): |
||||
|
""" Merge several styles, giving priority to the last. |
||||
|
|
||||
|
Returns a PropertyDict of style properties. |
||||
|
""" |
||||
|
r = PropertyDict() |
||||
|
for style in styles: |
||||
|
if not style: |
||||
|
continue |
||||
|
if isinstance(style, dict): |
||||
|
r.update(style) |
||||
|
else: |
||||
|
for prop in PROPS: |
||||
|
inherit = getattr(style, prop + '_inherit', None) |
||||
|
if inherit is None: |
||||
|
value = getattr(style, prop) |
||||
|
if value: |
||||
|
r[prop] = value |
||||
|
elif not inherit: |
||||
|
value = getattr(style, prop) |
||||
|
r[prop] = value |
||||
|
return r |
||||
|
|
||||
|
@api.model |
||||
|
def render(self, lang, style_props, type, value): |
||||
|
if type == 'num': |
||||
|
return self.render_num(lang, value, style_props.divider, |
||||
|
style_props.dp, |
||||
|
style_props.prefix, style_props.suffix) |
||||
|
elif type == 'pct': |
||||
|
return self.render_pct(lang, value, style_props.dp) |
||||
|
else: |
||||
|
return self.render_str(lang, value) |
||||
|
|
||||
|
@api.model |
||||
|
def render_num(self, lang, value, |
||||
|
divider=1.0, dp=0, prefix=None, suffix=None, sign='-'): |
||||
|
# format number following user language |
||||
|
if value is None or value is AccountingNone: |
||||
|
return u'' |
||||
|
value = round(value / float(divider or 1), dp or 0) or 0 |
||||
|
r = lang.format('%%%s.%df' % (sign, dp or 0), value, grouping=True) |
||||
|
r = r.replace('-', u'\N{NON-BREAKING HYPHEN}') |
||||
|
if prefix: |
||||
|
r = prefix + u'\N{NO-BREAK SPACE}' + r |
||||
|
if suffix: |
||||
|
r = r + u'\N{NO-BREAK SPACE}' + suffix |
||||
|
return r |
||||
|
|
||||
|
@api.model |
||||
|
def render_pct(self, lang, value, dp=1, sign='-'): |
||||
|
return self.render_num(lang, value, divider=0.01, |
||||
|
dp=dp, suffix='%', sign=sign) |
||||
|
|
||||
|
@api.model |
||||
|
def render_str(self, lang, value): |
||||
|
if value is None or value is AccountingNone: |
||||
|
return u'' |
||||
|
return unicode(value) |
||||
|
|
||||
|
@api.model |
||||
|
def compare_and_render(self, lang, style_props, type, compare_method, |
||||
|
value, base_value, |
||||
|
average_value=1, average_base_value=1): |
||||
|
delta = AccountingNone |
||||
|
style_r = style_props.copy() |
||||
|
if isinstance(value, DataError) or isinstance(base_value, DataError): |
||||
|
return AccountingNone, '', style_r |
||||
|
if value is None: |
||||
|
value = AccountingNone |
||||
|
if base_value is None: |
||||
|
base_value = AccountingNone |
||||
|
if type == TYPE_PCT: |
||||
|
delta = value - base_value |
||||
|
if delta and round(delta, (style_props.dp or 0) + 2) != 0: |
||||
|
style_r.update(dict( |
||||
|
divider=0.01, prefix='', suffix=_('pp'))) |
||||
|
else: |
||||
|
delta = AccountingNone |
||||
|
elif type == TYPE_NUM: |
||||
|
if value and average_value: |
||||
|
value = value / float(average_value) |
||||
|
if base_value and average_base_value: |
||||
|
base_value = base_value / float(average_base_value) |
||||
|
if compare_method == CMP_DIFF: |
||||
|
delta = value - base_value |
||||
|
if delta and round(delta, style_props.dp or 0) != 0: |
||||
|
pass |
||||
|
else: |
||||
|
delta = AccountingNone |
||||
|
elif compare_method == CMP_PCT: |
||||
|
if base_value and round(base_value, style_props.dp or 0) != 0: |
||||
|
delta = (value - base_value) / abs(base_value) |
||||
|
if delta and round(delta, 1) != 0: |
||||
|
style_r.update(dict( |
||||
|
divider=0.01, dp=1, prefix='', suffix='%')) |
||||
|
else: |
||||
|
delta = AccountingNone |
||||
|
if delta is not AccountingNone: |
||||
|
delta_r = self.render_num( |
||||
|
lang, delta, |
||||
|
style_r.divider, style_r.dp, |
||||
|
style_r.prefix, style_r.suffix, |
||||
|
sign='+') |
||||
|
return delta, delta_r, style_r |
||||
|
else: |
||||
|
return AccountingNone, '', style_r |
||||
|
|
||||
|
@api.model |
||||
|
def to_xlsx_style(self, props): |
||||
|
num_format = '0' |
||||
|
if props.dp: |
||||
|
num_format += '.' |
||||
|
num_format += '0' * props.dp |
||||
|
if props.prefix: |
||||
|
num_format = u'"{} "{}'.format(props.prefix, num_format) |
||||
|
if props.suffix: |
||||
|
num_format = u'{}" {}"'.format(num_format, props.suffix) |
||||
|
|
||||
|
xlsx_attributes = [ |
||||
|
('italic', props.font_style == 'italic'), |
||||
|
('bold', props.font_weight == 'bold'), |
||||
|
('size', self._font_size_to_xlsx_size.get(props.font_size, 11)), |
||||
|
('font_color', props.color), |
||||
|
('bg_color', props.background_color), |
||||
|
('indent', props.indent_level), |
||||
|
('num_format', num_format), |
||||
|
] |
||||
|
return dict([a for a in xlsx_attributes |
||||
|
if a[1] is not None]) |
||||
|
|
||||
|
@api.model |
||||
|
def to_css_style(self, props): |
||||
|
css_attributes = [ |
||||
|
('font-style', props.font_style), |
||||
|
('font-weight', props.font_weight), |
||||
|
('font-size', props.font_size), |
||||
|
('color', props.color), |
||||
|
('background-color', props.background_color), |
||||
|
('indent-level', props.indent_level) |
||||
|
] |
||||
|
return '; '.join(['%s: %s' % a for a in css_attributes |
||||
|
if a[1] is not None]) or None |
@ -0,0 +1,33 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import traceback |
||||
|
|
||||
|
from openerp.tools.safe_eval import test_expr, _SAFE_OPCODES, _BUILTINS |
||||
|
|
||||
|
from .data_error import DataError, NameDataError |
||||
|
|
||||
|
|
||||
|
__all__ = ['mis_safe_eval'] |
||||
|
|
||||
|
|
||||
|
def mis_safe_eval(expr, locals_dict): |
||||
|
""" Evaluate an expression using safe_eval |
||||
|
|
||||
|
Returns the evaluated value or DataError. |
||||
|
|
||||
|
Raises NameError if the evaluation depends on a variable that is not |
||||
|
present in local_dict. |
||||
|
""" |
||||
|
try: |
||||
|
c = test_expr(expr, _SAFE_OPCODES, mode='eval') |
||||
|
globals_dict = {'__builtins__': _BUILTINS} |
||||
|
val = eval(c, globals_dict, locals_dict) # pylint: disable=eval-used |
||||
|
except NameError: |
||||
|
val = NameDataError('#NAME', traceback.format_exc()) |
||||
|
except ZeroDivisionError: |
||||
|
val = DataError('#DIV/0', traceback.format_exc()) |
||||
|
except: |
||||
|
val = DataError('#ERR', traceback.format_exc()) |
||||
|
return val |
@ -0,0 +1,131 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
""" A trivial immutable array that supports basic arithmetic operations. |
||||
|
|
||||
|
>>> a = SimpleArray((1.0, 2.0, 3.0)) |
||||
|
>>> b = SimpleArray((4.0, 5.0, 6.0)) |
||||
|
>>> t = (4.0, 5.0, 6.0) |
||||
|
>>> +a |
||||
|
SimpleArray((1.0, 2.0, 3.0)) |
||||
|
>>> -a |
||||
|
SimpleArray((-1.0, -2.0, -3.0)) |
||||
|
>>> a + b |
||||
|
SimpleArray((5.0, 7.0, 9.0)) |
||||
|
>>> b + a |
||||
|
SimpleArray((5.0, 7.0, 9.0)) |
||||
|
>>> a + t |
||||
|
SimpleArray((5.0, 7.0, 9.0)) |
||||
|
>>> t + a |
||||
|
SimpleArray((5.0, 7.0, 9.0)) |
||||
|
>>> a - b |
||||
|
SimpleArray((-3.0, -3.0, -3.0)) |
||||
|
>>> a - t |
||||
|
SimpleArray((-3.0, -3.0, -3.0)) |
||||
|
>>> t - a |
||||
|
SimpleArray((3.0, 3.0, 3.0)) |
||||
|
>>> a * b |
||||
|
SimpleArray((4.0, 10.0, 18.0)) |
||||
|
>>> b * a |
||||
|
SimpleArray((4.0, 10.0, 18.0)) |
||||
|
>>> a * t |
||||
|
SimpleArray((4.0, 10.0, 18.0)) |
||||
|
>>> t * a |
||||
|
SimpleArray((4.0, 10.0, 18.0)) |
||||
|
>>> a / b |
||||
|
SimpleArray((0.25, 0.4, 0.5)) |
||||
|
>>> b / a |
||||
|
SimpleArray((4.0, 2.5, 2.0)) |
||||
|
>>> a / t |
||||
|
SimpleArray((0.25, 0.4, 0.5)) |
||||
|
>>> t / a |
||||
|
SimpleArray((4.0, 2.5, 2.0)) |
||||
|
>>> b / 2 |
||||
|
SimpleArray((2.0, 2.5, 3.0)) |
||||
|
>>> 2 * b |
||||
|
SimpleArray((8.0, 10.0, 12.0)) |
||||
|
>>> b += 2 ; b |
||||
|
SimpleArray((6.0, 7.0, 8.0)) |
||||
|
>>> a / ((1.0, 0.0, 1.0)) |
||||
|
SimpleArray((1.0, DataError(), 3.0)) |
||||
|
>>> a / 0.0 |
||||
|
SimpleArray((DataError(), DataError(), DataError())) |
||||
|
""" |
||||
|
|
||||
|
import operator |
||||
|
import traceback |
||||
|
|
||||
|
from .data_error import DataError |
||||
|
|
||||
|
|
||||
|
__all__ = ['SimpleArray'] |
||||
|
|
||||
|
|
||||
|
# TODO named tuple-like behaviour, so expressions can work on subkpis |
||||
|
|
||||
|
|
||||
|
class SimpleArray(tuple): |
||||
|
|
||||
|
def _op(self, op, other): |
||||
|
def _o2(x, y): |
||||
|
try: |
||||
|
return op(x, y) |
||||
|
except ZeroDivisionError: |
||||
|
return DataError('#DIV/0', traceback.format_exc()) |
||||
|
except: |
||||
|
return DataError('#ERR', traceback.format_exc()) |
||||
|
|
||||
|
if isinstance(other, tuple): |
||||
|
if len(other) != len(self): |
||||
|
raise TypeError("tuples must have same length for %s" % op) |
||||
|
return SimpleArray(map(_o2, self, other)) |
||||
|
else: |
||||
|
return SimpleArray(map(lambda z: _o2(z, other), self)) |
||||
|
|
||||
|
def __add__(self, other): |
||||
|
return self._op(operator.add, other) |
||||
|
|
||||
|
__radd__ = __add__ |
||||
|
|
||||
|
def __pos__(self): |
||||
|
return SimpleArray(map(operator.pos, self)) |
||||
|
|
||||
|
def __neg__(self): |
||||
|
return SimpleArray(map(operator.neg, self)) |
||||
|
|
||||
|
def __sub__(self, other): |
||||
|
return self._op(operator.sub, other) |
||||
|
|
||||
|
def __rsub__(self, other): |
||||
|
return SimpleArray(other)._op(operator.sub, self) |
||||
|
|
||||
|
def __mul__(self, other): |
||||
|
return self._op(operator.mul, other) |
||||
|
|
||||
|
__rmul__ = __mul__ |
||||
|
|
||||
|
def __div__(self, other): |
||||
|
return self._op(operator.div, other) |
||||
|
|
||||
|
def __floordiv__(self, other): |
||||
|
return self._op(operator.floordiv, other) |
||||
|
|
||||
|
def __truediv__(self, other): |
||||
|
return self._op(operator.truediv, other) |
||||
|
|
||||
|
def __rdiv__(self, other): |
||||
|
return SimpleArray(other)._op(operator.div, self) |
||||
|
|
||||
|
def __rfloordiv__(self, other): |
||||
|
return SimpleArray(other)._op(operator.floordiv, self) |
||||
|
|
||||
|
def __rtruediv__(self, other): |
||||
|
return SimpleArray(other)._op(operator.truediv, self) |
||||
|
|
||||
|
def __repr__(self): |
||||
|
return "SimpleArray(%s)" % tuple.__repr__(self) |
||||
|
|
||||
|
|
||||
|
if __name__ == '__main__': |
||||
|
import doctest |
||||
|
doctest.testmod() |
@ -1,30 +1,6 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
try: |
|
||||
from . import mis_builder_xls |
|
||||
except ImportError: |
|
||||
pass # this module is not installed |
|
||||
|
|
||||
from . import report_mis_report_instance |
|
||||
|
from . import mis_report_instance_qweb |
||||
|
from . import mis_report_instance_xlsx |
@ -1,138 +0,0 @@ |
|||||
# -*- 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,27 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import logging |
||||
|
|
||||
|
from openerp import api, models |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
|
||||
|
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,89 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<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> |
||||
|
|
||||
|
<template id="assets_report" inherit_id="report.assets_common"> |
||||
|
<xpath expr="." position="inside"> |
||||
|
<link href="/mis_builder/static/src/css/report.css" rel="stylesheet"/> |
||||
|
</xpath> |
||||
|
</template> |
||||
|
|
||||
|
<!-- |
||||
|
TODO we use divs with css table layout, but this has drawbacks: |
||||
|
(bad layout of first column, no colspan for first header row), |
||||
|
consider getting back to a plain HTML table. |
||||
|
--> |
||||
|
|
||||
|
<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"> |
||||
|
<t t-set="matrix" t-value="o._compute_matrix()"/> |
||||
|
<t t-set="style_obj" t-value="o.env['mis.report.style']"/> |
||||
|
<div class="page"> |
||||
|
<h2><span t-field="o.name" /> - <span t-field="o.company_id.name" /></h2> |
||||
|
<div class="mis_table"> |
||||
|
<div class="mis_thead"> |
||||
|
<div class="mis_row"> |
||||
|
<div class="mis_cell mis_collabel"></div> |
||||
|
<t t-foreach="matrix.iter_cols()" t-as="col"> |
||||
|
<div class="mis_cell mis_collabel"> |
||||
|
<t t-esc="col.label"/> |
||||
|
<t t-if="col.description"> |
||||
|
<br/> |
||||
|
<t t-esc="col.description"/> |
||||
|
</t> |
||||
|
</div> |
||||
|
<!-- add empty cells because we have no colspan with css tables --> |
||||
|
<t t-foreach="list(col.iter_subcols())[1:]" t-as="subcol"> |
||||
|
<div class="mis_cell mis_collabel"></div> |
||||
|
</t> |
||||
|
</t> |
||||
|
</div> |
||||
|
<div class="mis_row"> |
||||
|
<div class="mis_cell mis_collabel"></div> |
||||
|
<t t-foreach="matrix.iter_subcols()" t-as="subcol"> |
||||
|
<div class="mis_cell mis_collabel"> |
||||
|
<t t-esc="subcol.label"/> |
||||
|
<t t-if="subcol.description"> |
||||
|
<br/> |
||||
|
<t t-esc="subcol.description"/> |
||||
|
</t> |
||||
|
</div> |
||||
|
</t> |
||||
|
</div> |
||||
|
</div> |
||||
|
<div class="mis_tbody"> |
||||
|
<div t-foreach="matrix.iter_rows()" t-as="row" class="mis_row"> |
||||
|
<div t-att-style="style_obj.to_css_style(row.style_props)" class="mis_cell mis_rowlabel"> |
||||
|
<t t-esc="row.label"/> |
||||
|
<t t-if="row.description"> |
||||
|
<br/> |
||||
|
<t t-esc="row.description"/> |
||||
|
</t> |
||||
|
</div> |
||||
|
<t t-foreach="row.iter_cells()" t-as="cell"> |
||||
|
<div t-att-style="cell and style_obj.to_css_style(cell.style_props) or ''" class="mis_cell mis_amount"> |
||||
|
<t t-esc="cell and cell.val_rendered or ''"/> |
||||
|
</div> |
||||
|
</t> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</div> |
||||
|
</t> |
||||
|
</t> |
||||
|
</t> |
||||
|
</template> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,140 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
from collections import defaultdict |
||||
|
import logging |
||||
|
|
||||
|
from openerp.report import report_sxw |
||||
|
|
||||
|
from ..models.accounting_none import AccountingNone |
||||
|
from ..models.data_error import DataError |
||||
|
|
||||
|
_logger = logging.getLogger(__name__) |
||||
|
|
||||
|
try: |
||||
|
from openerp.addons.report_xlsx.report.report_xlsx import ReportXlsx |
||||
|
except ImportError: |
||||
|
_logger.debug("report_xlsx not installed, Excel export non functional") |
||||
|
|
||||
|
class ReportXslx: |
||||
|
pass |
||||
|
|
||||
|
|
||||
|
ROW_HEIGHT = 15 # xlsxwriter units |
||||
|
COL_WIDTH = 0.9 # xlsxwriter units |
||||
|
MIN_COL_WIDTH = 10 # characters |
||||
|
MAX_COL_WIDTH = 50 # characters |
||||
|
|
||||
|
|
||||
|
class MisBuilderXslx(ReportXlsx): |
||||
|
|
||||
|
def __init__(self, name, table, rml=False, parser=False, header=True, |
||||
|
store=False): |
||||
|
super(MisBuilderXslx, self).__init__( |
||||
|
name, table, rml, parser, header, store) |
||||
|
|
||||
|
def generate_xlsx_report(self, workbook, data, objects): |
||||
|
|
||||
|
# get the computed result of the report |
||||
|
matrix = objects._compute_matrix() |
||||
|
style_obj = self.env['mis.report.style'] |
||||
|
|
||||
|
# create worksheet |
||||
|
report_name = '{} - {}'.format( |
||||
|
objects[0].name, objects[0].company_id.name) |
||||
|
sheet = workbook.add_worksheet(report_name[:31]) |
||||
|
row_pos = 0 |
||||
|
col_pos = 0 |
||||
|
# width of the labels column |
||||
|
label_col_width = MIN_COL_WIDTH |
||||
|
# {col_pos: max width in characters} |
||||
|
col_width = defaultdict(lambda: MIN_COL_WIDTH) |
||||
|
|
||||
|
# document title |
||||
|
bold = workbook.add_format({'bold': True}) |
||||
|
header_format = workbook.add_format({ |
||||
|
'bold': True, 'align': 'center', 'bg_color': '#F0EEEE'}) |
||||
|
sheet.write(row_pos, 0, report_name, bold) |
||||
|
row_pos += 2 |
||||
|
|
||||
|
# column headers |
||||
|
sheet.write(row_pos, 0, '', header_format) |
||||
|
col_pos = 1 |
||||
|
for col in matrix.iter_cols(): |
||||
|
label = col.label |
||||
|
if col.description: |
||||
|
label += '\n' + col.description |
||||
|
sheet.set_row(row_pos, ROW_HEIGHT * 2) |
||||
|
if col.colspan > 1: |
||||
|
sheet.merge_range( |
||||
|
row_pos, col_pos, row_pos, |
||||
|
col_pos + col.colspan-1, |
||||
|
label, header_format) |
||||
|
else: |
||||
|
sheet.write(row_pos, col_pos, label, header_format) |
||||
|
col_width[col_pos] = max(col_width[col_pos], |
||||
|
len(col.label or ''), |
||||
|
len(col.description or '')) |
||||
|
col_pos += col.colspan |
||||
|
row_pos += 1 |
||||
|
|
||||
|
# sub column headers |
||||
|
sheet.write(row_pos, 0, '', header_format) |
||||
|
col_pos = 1 |
||||
|
for subcol in matrix.iter_subcols(): |
||||
|
label = subcol.label |
||||
|
if subcol.description: |
||||
|
label += '\n' + subcol.description |
||||
|
sheet.set_row(row_pos, ROW_HEIGHT * 2) |
||||
|
sheet.write(row_pos, col_pos, label, header_format) |
||||
|
col_width[col_pos] = max(col_width[col_pos], |
||||
|
len(subcol.label or ''), |
||||
|
len(subcol.description or '')) |
||||
|
col_pos += 1 |
||||
|
row_pos += 1 |
||||
|
|
||||
|
# rows |
||||
|
for row in matrix.iter_rows(): |
||||
|
row_xlsx_style = style_obj.to_xlsx_style(row.style_props) |
||||
|
row_format = workbook.add_format(row_xlsx_style) |
||||
|
col_pos = 0 |
||||
|
label = row.label |
||||
|
if row.description: |
||||
|
label += '\n' + row.description |
||||
|
sheet.set_row(row_pos, ROW_HEIGHT * 2) |
||||
|
sheet.write(row_pos, col_pos, label, row_format) |
||||
|
label_col_width = max(label_col_width, |
||||
|
len(row.label or ''), |
||||
|
len(row.description or '')) |
||||
|
for cell in row.iter_cells(): |
||||
|
col_pos += 1 |
||||
|
if not cell or cell.val is AccountingNone: |
||||
|
# TODO col/subcol format |
||||
|
sheet.write(row_pos, col_pos, '', row_format) |
||||
|
continue |
||||
|
cell_xlsx_style = style_obj.to_xlsx_style(cell.style_props) |
||||
|
cell_xlsx_style['align'] = 'right' |
||||
|
cell_format = workbook.add_format(cell_xlsx_style) |
||||
|
if isinstance(cell.val, DataError): |
||||
|
val = cell.val.name |
||||
|
# TODO display cell.val.msg as Excel comment? |
||||
|
elif cell.val is None or cell.val is AccountingNone: |
||||
|
val = '' |
||||
|
else: |
||||
|
val = cell.val / float(cell.style_props.get('divider', 1)) |
||||
|
sheet.write(row_pos, col_pos, val, cell_format) |
||||
|
col_width[col_pos] = max(col_width[col_pos], |
||||
|
len(cell.val_rendered or '')) |
||||
|
row_pos += 1 |
||||
|
|
||||
|
# adjust col widths |
||||
|
sheet.set_column(0, 0, min(label_col_width, MAX_COL_WIDTH) * COL_WIDTH) |
||||
|
data_col_width = min(MAX_COL_WIDTH, max(*col_width.values())) |
||||
|
min_col_pos = min(*col_width.keys()) |
||||
|
max_col_pos = max(*col_width.keys()) |
||||
|
sheet.set_column(min_col_pos, max_col_pos, data_col_width * COL_WIDTH) |
||||
|
|
||||
|
|
||||
|
MisBuilderXslx('report.mis.report.instance.xlsx', |
||||
|
'mis.report.instance', parser=report_sxw.rml_parse) |
@ -0,0 +1,15 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<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.xlsx</field> |
||||
|
<field name="report_type">xlsx</field> |
||||
|
<field name="auto" eval="False"/> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -1,67 +0,0 @@ |
|||||
# -*- 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) |
|
@ -1,55 +0,0 @@ |
|||||
<?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,45 @@ |
|||||
|
.mis_table { |
||||
|
display: table; |
||||
|
width: 100%; |
||||
|
table-layout: fixed; |
||||
|
} |
||||
|
.mis_row { |
||||
|
display: table-row; |
||||
|
page-break-inside: avoid; |
||||
|
} |
||||
|
.mis_cell { |
||||
|
display: table-cell; |
||||
|
page-break-inside: avoid; |
||||
|
} |
||||
|
.mis_thead { |
||||
|
display: table-header-group; |
||||
|
} |
||||
|
.mis_tbody { |
||||
|
display: table-row-group; |
||||
|
} |
||||
|
.mis_table, .mis_table .mis_row { |
||||
|
border-left: 0px; |
||||
|
border-right: 0px; |
||||
|
text-align: left; |
||||
|
padding-right: 3px; |
||||
|
padding-left: 3px; |
||||
|
padding-top: 2px; |
||||
|
padding-bottom: 2px; |
||||
|
border-collapse: collapse; |
||||
|
} |
||||
|
.mis_table .mis_row { |
||||
|
border-color: grey; |
||||
|
border-bottom: 1px solid lightGrey; |
||||
|
} |
||||
|
.mis_table .mis_cell.mis_collabel { |
||||
|
font-weight: bold; |
||||
|
background-color: #F0F0F0; |
||||
|
text-align: center; |
||||
|
} |
||||
|
.mis_table .mis_cell.mis_rowlabel { |
||||
|
text-align: left; |
||||
|
/*white-space: nowrap;*/ |
||||
|
} |
||||
|
.mis_table .mis_cell.mis_amount { |
||||
|
text-align: right; |
||||
|
} |
@ -1,48 +1,147 @@ |
|||||
openerp.mis_builder = function(instance) { |
|
||||
|
odoo.define('mis.builder', function (require) { |
||||
|
"use strict"; |
||||
|
|
||||
instance.mis_builder.MisReport = instance.web.form.FormWidget.extend({ |
|
||||
template: "mis_builder.MisReport", |
|
||||
|
var core = require('web.core'); |
||||
|
var form_common = require('web.form_common'); |
||||
|
var Model = require('web.DataModel'); |
||||
|
var data = require('web.data'); |
||||
|
var ActionManager = require('web.ActionManager'); |
||||
|
|
||||
init: function() { |
|
||||
this._super.apply(this, arguments); |
|
||||
this.mis_report_data = null; |
|
||||
}, |
|
||||
|
var MisReport = form_common.FormWidget.extend({ |
||||
|
/** |
||||
|
* @constructs instance.mis_builder.MisReport |
||||
|
* @extends instance.web.form.FormWidget |
||||
|
* |
||||
|
*/ |
||||
|
template: "mis_builder.MisReport", |
||||
|
init: function() { |
||||
|
this._super.apply(this, arguments); |
||||
|
this.mis_report_data = null; |
||||
|
this.mis_report_instance_id = false; |
||||
|
this.field_manager.on("view_content_has_changed", this, this.reload_widget); |
||||
|
}, |
||||
|
|
||||
|
reload_widget: function() { |
||||
|
var self = this; |
||||
|
self.mis_report_instance_id = self.getParent().datarecord.id; |
||||
|
if (self.mis_report_instance_id) { |
||||
|
self.generate_content(); |
||||
|
} |
||||
|
}, |
||||
|
|
||||
start: function() { |
|
||||
this._super.apply(this, arguments); |
|
||||
var self = this; |
|
||||
new instance.web.Model("mis.report.instance").call( |
|
||||
"compute", |
|
||||
[self.getParent().dataset.context.active_id], |
|
||||
{'context': new instance.web.CompoundContext()} |
|
||||
).then(function(result){ |
|
||||
self.mis_report_data = result; |
|
||||
self.renderElement(); |
|
||||
}); |
|
||||
}, |
|
||||
|
start: function() { |
||||
|
this._super.apply(this, arguments); |
||||
|
var self = this; |
||||
|
self.mis_report_instance_id = self.getParent().datarecord.id; |
||||
|
if (self.mis_report_instance_id) { |
||||
|
self.getParent().dataset.context.no_destroy = true; |
||||
|
} |
||||
|
}, |
||||
|
|
||||
|
get_context: function() { |
||||
|
var self = this; |
||||
|
var context = {}; |
||||
|
if (this.mis_report_instance_id){ |
||||
|
context.active_ids = [this.mis_report_instance_id]; |
||||
|
} |
||||
|
return context; |
||||
|
}, |
||||
|
|
||||
|
print: function() { |
||||
|
var self = this; |
||||
|
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
||||
|
new Model("mis.report.instance").call( |
||||
|
"print_pdf", |
||||
|
[self.mis_report_instance_id], |
||||
|
{'context': context} |
||||
|
).then(function(result){ |
||||
|
self.do_action(result); |
||||
|
}); |
||||
|
}, |
||||
|
export_pdf: function() { |
||||
|
var self = this; |
||||
|
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
||||
|
new Model("mis.report.instance").call( |
||||
|
"export_xls", |
||||
|
[self.mis_report_instance_id], |
||||
|
{'context': context} |
||||
|
).then(function(result){ |
||||
|
self.do_action(result); |
||||
|
}); |
||||
|
}, |
||||
|
display_settings: function() { |
||||
|
var self = this; |
||||
|
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
||||
|
new Model("mis.report.instance").call( |
||||
|
"display_settings", |
||||
|
[self.mis_report_instance_id], |
||||
|
{'context': context} |
||||
|
).then(function(result){ |
||||
|
self.do_action(result); |
||||
|
}); |
||||
|
}, |
||||
|
generate_content: function() { |
||||
|
var self = this; |
||||
|
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
||||
|
new Model("mis.report.instance").call( |
||||
|
"compute", |
||||
|
[self.mis_report_instance_id], |
||||
|
{'context': context} |
||||
|
).then(function(result){ |
||||
|
self.mis_report_data = result; |
||||
|
self.renderElement(); |
||||
|
}); |
||||
|
}, |
||||
|
renderElement: function() { |
||||
|
this._super(); |
||||
|
var self = this; |
||||
|
self.$(".oe_mis_builder_print").click(_.bind(this.print, this)); |
||||
|
self.$(".oe_mis_builder_export").click(_.bind(this.export_pdf, this)); |
||||
|
self.$(".oe_mis_builder_settings").click(_.bind(this.display_settings, this)); |
||||
|
var Users = new Model('res.users'); |
||||
|
Users.call('has_group', ['account.group_account_user']).done(function (res) { |
||||
|
if (res) { |
||||
|
self.$(".oe_mis_builder_settings").show(); |
||||
|
} |
||||
|
}); |
||||
|
}, |
||||
|
events: { |
||||
|
"click a.mis_builder_drilldown": "drilldown", |
||||
|
}, |
||||
|
|
||||
events: { |
|
||||
"click a.mis_builder_drilldown": "drilldown", |
|
||||
}, |
|
||||
|
drilldown: function(event) { |
||||
|
var self = this; |
||||
|
var drilldown = $(event.target).data("drilldown"); |
||||
|
if (drilldown) { |
||||
|
new Model("mis.report.instance").call( |
||||
|
"drilldown", |
||||
|
[self.mis_report_instance_id, drilldown] |
||||
|
).then(function(result) { |
||||
|
if (result) { |
||||
|
self.do_action(result); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
}, |
||||
|
}); |
||||
|
|
||||
drilldown: function(event) { |
|
||||
var self = this; |
|
||||
var drilldown = JSON.parse($(event.target).data("drilldown")); |
|
||||
if (drilldown) { |
|
||||
var period_id = JSON.parse($(event.target).data("period-id")); |
|
||||
var val_c = JSON.parse($(event.target).data("expr")); |
|
||||
new instance.web.Model("mis.report.instance.period").call( |
|
||||
"drilldown", |
|
||||
[period_id, val_c], |
|
||||
{'context': new instance.web.CompoundContext()} |
|
||||
).then(function(result) { |
|
||||
if (result) { |
|
||||
self.do_action(result); |
|
||||
} |
|
||||
}); |
|
||||
|
ActionManager.include({ |
||||
|
/* |
||||
|
* In the case where we would be open in modal view, this is |
||||
|
* necessary to avoid to close the popup on click on button like print, |
||||
|
* export, ... |
||||
|
*/ |
||||
|
dialog_stop: function (reason) { |
||||
|
var self = this; |
||||
|
if (self.dialog_widget && self.dialog_widget.dataset && self.dialog_widget.dataset.context) { |
||||
|
var context = self.dialog_widget.dataset.context; |
||||
|
if (!context.no_destroy) { |
||||
|
this._super.apply(this, arguments); |
||||
} |
} |
||||
}, |
|
||||
}); |
|
||||
|
|
||||
instance.web.form.custom_widgets.add('mis_report', 'instance.mis_builder.MisReport'); |
|
||||
} |
|
||||
|
} else { |
||||
|
this._super.apply(this, arguments); |
||||
|
} |
||||
|
} |
||||
|
}); |
||||
|
core.form_custom_registry.add('mis_report', MisReport); |
||||
|
}); |
@ -1,62 +1,56 @@ |
|||||
<template> |
<template> |
||||
<t t-name="mis_builder.MisReport"> |
<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> |
|
||||
|
<div class="oe_mis_builder_content"> |
||||
|
<t t-if="widget.mis_report_data"> |
||||
|
<h2><t t-esc="widget.mis_report_data.report_name" /></h2> |
||||
|
<div class="oe_mis_builder_buttons oe_right oe_button_box"> |
||||
|
<button class="oe_mis_builder_print btn btn-sm oe_button"><img src="/web/static/src/img/icons/gtk-print.png"/> Print</button> |
||||
|
<button class="oe_mis_builder_export btn btn-sm oe_button"><img src="/web/static/src/img/icons/gtk-go-down.png"/>Export</button> |
||||
|
<button style="display: none;" class="oe_mis_builder_settings btn btn-sm oe_button"><img src="/web/static/src/img/icons/gtk-execute.png"/> Settings</button> |
||||
|
</div> |
||||
|
<table class="oe_list_content mis_builder"> |
||||
|
<thead> |
||||
|
<tr t-foreach="widget.mis_report_data.header" t-as="row" class="oe_list_header_columns"> |
||||
|
<th class="oe_list_header_char"> |
||||
|
</th> |
||||
|
<th t-foreach="row.cols" t-as="col" class="oe_list_header_char mis_builder_collabel" t-att-colspan="col.colspan"> |
||||
|
<t t-esc="col.label"/> |
||||
|
<t t-if="col.description"> |
||||
|
<br/> |
||||
|
<t t-esc="col.description"/> |
||||
|
</t> |
||||
|
</th> |
||||
|
</tr> |
||||
|
</thead> |
||||
|
<tbody> |
||||
|
<tr t-foreach="widget.mis_report_data.body" t-as="row"> |
||||
|
<td t-att="{'style': row.style}"> |
||||
|
<t t-esc="row.label"/> |
||||
|
<t t-if="row.description"> |
||||
|
<br/> |
||||
|
<t t-esc="row.description"/> |
||||
</t> |
</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> |
|
||||
|
</td> |
||||
|
<td t-foreach="row.cells" t-as="cell" t-att="{'style': cell.style, 'title': cell.val_c}" class="mis_builder_amount"> |
||||
|
<t t-if="cell.drilldown_arg"> |
||||
|
<a href="javascript:void(0)" |
||||
|
class="mis_builder_drilldown" |
||||
|
t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)" |
||||
|
> |
||||
|
<t t-esc="cell.val_r"/> |
||||
|
</a> |
||||
|
</t> |
||||
|
<t t-if="!cell.drilldown_arg"> |
||||
|
<t t-esc="cell.val_r"/> |
||||
|
</t> |
||||
|
</td> |
||||
|
</tr> |
||||
|
</tbody> |
||||
|
<tfoot> |
||||
|
<tr></tr> |
||||
|
</tfoot> |
||||
|
</table> |
||||
|
</t> |
||||
|
</div> |
||||
</t> |
</t> |
||||
</template> |
</template> |
@ -1,25 +1,13 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
from . import test_mis_builder |
|
||||
|
from . import test_accounting_none |
||||
|
from . import test_aep |
||||
|
from . import test_aggregate |
||||
|
from . import test_fetch_query |
||||
|
from . import test_mis_report_instance |
||||
|
from . import test_mis_safe_eval |
||||
|
from . import test_render |
||||
|
from . import test_simple_array |
||||
|
from . import test_utc_midnight |
@ -1,2 +1,2 @@ |
|||||
"id","description","kpi_ids/id","name","query_ids/id" |
|
||||
"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" |
|
||||
|
"id","description","kpi_ids/id","name","query_ids/id" |
||||
|
"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" |
@ -1,2 +1,2 @@ |
|||||
"id","date","description","name","period_ids/id","report_id/id","root_account/id" |
|
||||
"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test","account.chart0" |
|
||||
|
"id","date","description","name","period_ids/id","report_id/id" |
||||
|
"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test" |
@ -1,2 +1,2 @@ |
|||||
"id","duration","name","offset","type","sequence" |
|
||||
"mis_report_instance_period_test","1","today","","Day","" |
|
||||
|
"id","duration","name","offset","type","sequence","mode" |
||||
|
"mis_report_instance_period_test","1","today","","Day","","relative" |
@ -1,2 +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","" |
|
||||
|
"id","description","expression","name" |
||||
|
"mis_report_kpi_test","total test","len(test)","total_test" |
@ -1,2 +1,2 @@ |
|||||
"id","date_field/id","domain","field_ids/id","model_id/id","name" |
"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" |
|
||||
|
"mis_report_query_test","analytic.field_account_analytic_line_date","","analytic.field_account_analytic_line_amount","analytic.model_account_analytic_line","test" |
@ -0,0 +1,12 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import doctest |
||||
|
|
||||
|
from openerp.addons.mis_builder.models import accounting_none |
||||
|
|
||||
|
|
||||
|
def load_tests(loader, tests, ignore): |
||||
|
tests.addTests(doctest.DocTestSuite(accounting_none)) |
||||
|
return tests |
@ -0,0 +1,226 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import datetime |
||||
|
import time |
||||
|
|
||||
|
from openerp import fields |
||||
|
import openerp.tests.common as common |
||||
|
from openerp.tools.safe_eval import safe_eval |
||||
|
|
||||
|
from ..models.aep import AccountingExpressionProcessor as AEP |
||||
|
from ..models.accounting_none import AccountingNone |
||||
|
|
||||
|
|
||||
|
class TestAEP(common.TransactionCase): |
||||
|
|
||||
|
def setUp(self): |
||||
|
super(TestAEP, self).setUp() |
||||
|
self.res_company = self.env['res.company'] |
||||
|
self.account_model = self.env['account.account'] |
||||
|
self.move_model = self.env['account.move'] |
||||
|
self.journal_model = self.env['account.journal'] |
||||
|
self.curr_year = datetime.date.today().year |
||||
|
self.prev_year = self.curr_year - 1 |
||||
|
# create company |
||||
|
self.company = self.res_company.create({ |
||||
|
'name': 'AEP Company'}) |
||||
|
# create receivable bs account |
||||
|
type_ar = self.browse_ref('account.data_account_type_receivable') |
||||
|
self.account_ar = self.account_model.create({ |
||||
|
'company_id': self.company.id, |
||||
|
'code': '400AR', |
||||
|
'name': 'Receivable', |
||||
|
'user_type_id': type_ar.id, |
||||
|
'reconcile': True}) |
||||
|
# create income pl account |
||||
|
type_in = self.browse_ref('account.data_account_type_revenue') |
||||
|
self.account_in = self.account_model.create({ |
||||
|
'company_id': self.company.id, |
||||
|
'code': '700IN', |
||||
|
'name': 'Income', |
||||
|
'user_type_id': type_in.id}) |
||||
|
# create journal |
||||
|
self.journal = self.journal_model.create({ |
||||
|
'company_id': self.company.id, |
||||
|
'name': 'Sale journal', |
||||
|
'code': 'VEN', |
||||
|
'type': 'sale'}) |
||||
|
# create move in december last year |
||||
|
self._create_move( |
||||
|
date=datetime.date(self.prev_year, 12, 1), |
||||
|
amount=100, |
||||
|
debit_acc=self.account_ar, |
||||
|
credit_acc=self.account_in) |
||||
|
# create move in january this year |
||||
|
self._create_move( |
||||
|
date=datetime.date(self.curr_year, 1, 1), |
||||
|
amount=300, |
||||
|
debit_acc=self.account_ar, |
||||
|
credit_acc=self.account_in) |
||||
|
# create move in february this year |
||||
|
self._create_move( |
||||
|
date=datetime.date(self.curr_year, 3, 1), |
||||
|
amount=500, |
||||
|
debit_acc=self.account_ar, |
||||
|
credit_acc=self.account_in) |
||||
|
# create the AEP, and prepare the expressions we'll need |
||||
|
self.aep = AEP(self.company) |
||||
|
self.aep.parse_expr("bali[]") |
||||
|
self.aep.parse_expr("bale[]") |
||||
|
self.aep.parse_expr("balp[]") |
||||
|
self.aep.parse_expr("balu[]") |
||||
|
self.aep.parse_expr("bali[700IN]") |
||||
|
self.aep.parse_expr("bale[700IN]") |
||||
|
self.aep.parse_expr("balp[700IN]") |
||||
|
self.aep.parse_expr("bali[400AR]") |
||||
|
self.aep.parse_expr("bale[400AR]") |
||||
|
self.aep.parse_expr("balp[400AR]") |
||||
|
self.aep.parse_expr("debp[400A%]") |
||||
|
self.aep.parse_expr("crdp[700I%]") |
||||
|
self.aep.parse_expr("bal_700IN") # deprecated |
||||
|
self.aep.parse_expr("bals[700IN]") # deprecated |
||||
|
self.aep.done_parsing() |
||||
|
|
||||
|
def _create_move(self, date, amount, debit_acc, credit_acc): |
||||
|
move = self.move_model.create({ |
||||
|
'journal_id': self.journal.id, |
||||
|
'date': fields.Date.to_string(date), |
||||
|
'line_ids': [(0, 0, { |
||||
|
'name': '/', |
||||
|
'debit': amount, |
||||
|
'account_id': debit_acc.id, |
||||
|
}), (0, 0, { |
||||
|
'name': '/', |
||||
|
'credit': amount, |
||||
|
'account_id': credit_acc.id, |
||||
|
})]}) |
||||
|
move.post() |
||||
|
return move |
||||
|
|
||||
|
def _do_queries(self, date_from, date_to): |
||||
|
self.aep.do_queries( |
||||
|
date_from=fields.Date.to_string(date_from), |
||||
|
date_to=fields.Date.to_string(date_to), |
||||
|
target_move='posted') |
||||
|
|
||||
|
def _eval(self, expr): |
||||
|
eval_dict = {'AccountingNone': AccountingNone} |
||||
|
return safe_eval(self.aep.replace_expr(expr), eval_dict) |
||||
|
|
||||
|
def _eval_by_account_id(self, expr): |
||||
|
res = {} |
||||
|
eval_dict = {'AccountingNone': AccountingNone} |
||||
|
for account_id, replaced_exprs in \ |
||||
|
self.aep.replace_exprs_by_account_id([expr]): |
||||
|
res[account_id] = safe_eval(replaced_exprs[0], eval_dict) |
||||
|
return res |
||||
|
|
||||
|
def test_sanity_check(self): |
||||
|
self.assertEquals(self.company.fiscalyear_last_day, 31) |
||||
|
self.assertEquals(self.company.fiscalyear_last_month, 12) |
||||
|
|
||||
|
def test_aep_basic(self): |
||||
|
# let's query for december |
||||
|
self._do_queries( |
||||
|
datetime.date(self.prev_year, 12, 1), |
||||
|
datetime.date(self.prev_year, 12, 31)) |
||||
|
# initial balance must be None |
||||
|
self.assertIs(self._eval('bali[400AR]'), AccountingNone) |
||||
|
self.assertIs(self._eval('bali[700IN]'), AccountingNone) |
||||
|
# check variation |
||||
|
self.assertEquals(self._eval('balp[400AR]'), 100) |
||||
|
self.assertEquals(self._eval('balp[700IN]'), -100) |
||||
|
# check ending balance |
||||
|
self.assertEquals(self._eval('bale[400AR]'), 100) |
||||
|
self.assertEquals(self._eval('bale[700IN]'), -100) |
||||
|
|
||||
|
# let's query for January |
||||
|
self._do_queries( |
||||
|
datetime.date(self.curr_year, 1, 1), |
||||
|
datetime.date(self.curr_year, 1, 31)) |
||||
|
# initial balance is None for income account (it's not carried over) |
||||
|
self.assertEquals(self._eval('bali[400AR]'), 100) |
||||
|
self.assertIs(self._eval('bali[700IN]'), AccountingNone) |
||||
|
# check variation |
||||
|
self.assertEquals(self._eval('balp[400AR]'), 300) |
||||
|
self.assertEquals(self._eval('balp[700IN]'), -300) |
||||
|
# check ending balance |
||||
|
self.assertEquals(self._eval('bale[400AR]'), 400) |
||||
|
self.assertEquals(self._eval('bale[700IN]'), -300) |
||||
|
|
||||
|
# let's query for March |
||||
|
self._do_queries( |
||||
|
datetime.date(self.curr_year, 3, 1), |
||||
|
datetime.date(self.curr_year, 3, 31)) |
||||
|
# initial balance is the ending balance fo January |
||||
|
self.assertEquals(self._eval('bali[400AR]'), 400) |
||||
|
self.assertEquals(self._eval('bali[700IN]'), -300) |
||||
|
# check variation |
||||
|
self.assertEquals(self._eval('balp[400AR]'), 500) |
||||
|
self.assertEquals(self._eval('balp[700IN]'), -500) |
||||
|
# check ending balance |
||||
|
self.assertEquals(self._eval('bale[400AR]'), 900) |
||||
|
self.assertEquals(self._eval('bale[700IN]'), -800) |
||||
|
# check some variant expressions, for coverage |
||||
|
self.assertEquals(self._eval('crdp[700I%]'), 500) |
||||
|
self.assertEquals(self._eval('debp[400A%]'), 500) |
||||
|
self.assertEquals(self._eval('bal_700IN'), -500) |
||||
|
self.assertEquals(self._eval('bals[700IN]'), -800) |
||||
|
|
||||
|
# unallocated p&l from previous year |
||||
|
self.assertEquals(self._eval('balu[]'), -100) |
||||
|
|
||||
|
# TODO allocate profits, and then... |
||||
|
|
||||
|
def test_aep_by_account(self): |
||||
|
self._do_queries( |
||||
|
datetime.date(self.curr_year, 3, 1), |
||||
|
datetime.date(self.curr_year, 3, 31)) |
||||
|
variation = self._eval_by_account_id('balp[]') |
||||
|
self.assertEquals(variation, { |
||||
|
self.account_ar.id: 500, |
||||
|
self.account_in.id: -500, |
||||
|
}) |
||||
|
variation = self._eval_by_account_id('balp[700IN]') |
||||
|
self.assertEquals(variation, { |
||||
|
self.account_in.id: -500, |
||||
|
}) |
||||
|
end = self._eval_by_account_id('bale[]') |
||||
|
self.assertEquals(end, { |
||||
|
self.account_ar.id: 900, |
||||
|
self.account_in.id: -800, |
||||
|
}) |
||||
|
|
||||
|
def test_aep_convenience_methods(self): |
||||
|
initial = AEP.get_balances_initial( |
||||
|
self.company, |
||||
|
time.strftime('%Y') + '-03-01', |
||||
|
'posted') |
||||
|
self.assertEquals(initial, { |
||||
|
self.account_ar.id: (400, 0), |
||||
|
self.account_in.id: (0, 300), |
||||
|
}) |
||||
|
variation = AEP.get_balances_variation( |
||||
|
self.company, |
||||
|
time.strftime('%Y') + '-03-01', |
||||
|
time.strftime('%Y') + '-03-31', |
||||
|
'posted') |
||||
|
self.assertEquals(variation, { |
||||
|
self.account_ar.id: (500, 0), |
||||
|
self.account_in.id: (0, 500), |
||||
|
}) |
||||
|
end = AEP.get_balances_end( |
||||
|
self.company, |
||||
|
time.strftime('%Y') + '-03-31', |
||||
|
'posted') |
||||
|
self.assertEquals(end, { |
||||
|
self.account_ar.id: (900, 0), |
||||
|
self.account_in.id: (0, 800), |
||||
|
}) |
||||
|
unallocated = AEP.get_unallocated_pl( |
||||
|
self.company, |
||||
|
time.strftime('%Y') + '-03-15', |
||||
|
'posted') |
||||
|
self.assertEquals(unallocated, (0, 100)) |
@ -0,0 +1,12 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import doctest |
||||
|
|
||||
|
from openerp.addons.mis_builder.models import aggregate |
||||
|
|
||||
|
|
||||
|
def load_tests(loader, tests, ignore): |
||||
|
tests.addTests(doctest.DocTestSuite(aggregate)) |
||||
|
return tests |
@ -0,0 +1,41 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import openerp.tests.common as common |
||||
|
|
||||
|
|
||||
|
class TestFetchQuery(common.TransactionCase): |
||||
|
|
||||
|
def test_fetch_query(self): |
||||
|
# create a report on account.analytic.line |
||||
|
data = self.registry('mis.report.instance').compute( |
||||
|
self.cr, self.uid, |
||||
|
self.ref('mis_builder.mis_report_instance_test')) |
||||
|
self.maxDiff = None |
||||
|
self.assertEquals( |
||||
|
{'body': |
||||
|
[{'label': u'total test', |
||||
|
'description': '', |
||||
|
'style': None, |
||||
|
'parent_row_id': None, |
||||
|
'row_id': u'total_test', |
||||
|
'cells': [{'val': 0, |
||||
|
'val_r': u'0', |
||||
|
'val_c': u'total_test = len(test)', |
||||
|
'style': None, |
||||
|
}] |
||||
|
}], |
||||
|
'header': |
||||
|
[{'cols': [{'description': '07/31/2014', |
||||
|
'label': u'today', |
||||
|
'colspan': 1, |
||||
|
}], |
||||
|
}, |
||||
|
{'cols': [{'label': '', |
||||
|
'description': '', |
||||
|
'colspan': 1, |
||||
|
}], |
||||
|
}, |
||||
|
], |
||||
|
}, data) |
@ -1,82 +0,0 @@ |
|||||
# -*- 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,141 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import openerp.tests.common as common |
||||
|
from openerp.tools import test_reports |
||||
|
|
||||
|
|
||||
|
class TestMisReportInstance(common.TransactionCase): |
||||
|
""" Basic integration test to exercise mis.report.instance. |
||||
|
|
||||
|
We don't check the actual results here too much as computation correctness |
||||
|
should be covered by lower level unit tests. |
||||
|
""" |
||||
|
|
||||
|
def setUp(self): |
||||
|
super(TestMisReportInstance, self).setUp() |
||||
|
partner_model_id = \ |
||||
|
self.env.ref('base.model_res_partner').id |
||||
|
partner_create_date_field_id = \ |
||||
|
self.env.ref('base.field_res_partner_create_date').id |
||||
|
partner_debit_field_id = \ |
||||
|
self.env.ref('account.field_res_partner_debit').id |
||||
|
# create a report with 2 subkpis and one query |
||||
|
self.report = self.env['mis.report'].create(dict( |
||||
|
name='test report', |
||||
|
subkpi_ids=[(0, 0, dict( |
||||
|
name='sk1', |
||||
|
description='subkpi 1', |
||||
|
sequence=1, |
||||
|
)), (0, 0, dict( |
||||
|
name='sk2', |
||||
|
description='subkpi 2', |
||||
|
sequence=2, |
||||
|
))], |
||||
|
query_ids=[(0, 0, dict( |
||||
|
name='partner', |
||||
|
model_id=partner_model_id, |
||||
|
field_ids=[(4, partner_debit_field_id, None)], |
||||
|
date_field=partner_create_date_field_id, |
||||
|
aggregate='sum', |
||||
|
))], |
||||
|
)) |
||||
|
# kpi with accounting formulas |
||||
|
self.env['mis.report.kpi'].create(dict( |
||||
|
report_id=self.report.id, |
||||
|
description='kpi 1', |
||||
|
name='k1', |
||||
|
multi=True, |
||||
|
expression_ids=[(0, 0, dict( |
||||
|
name='bale[200%]', |
||||
|
subkpi_id=self.report.subkpi_ids[0].id, |
||||
|
)), (0, 0, dict( |
||||
|
name='balp[200%]', |
||||
|
subkpi_id=self.report.subkpi_ids[1].id, |
||||
|
))], |
||||
|
)) |
||||
|
# kpi with accounting formula and query |
||||
|
self.env['mis.report.kpi'].create(dict( |
||||
|
report_id=self.report.id, |
||||
|
description='kpi 2', |
||||
|
name='k2', |
||||
|
multi=True, |
||||
|
expression_ids=[(0, 0, dict( |
||||
|
name='balp[200%]', |
||||
|
subkpi_id=self.report.subkpi_ids[0].id, |
||||
|
)), (0, 0, dict( |
||||
|
name='partner.debit', |
||||
|
subkpi_id=self.report.subkpi_ids[1].id, |
||||
|
))], |
||||
|
)) |
||||
|
# kpi with a simple expression summing other multi-valued kpis |
||||
|
self.env['mis.report.kpi'].create(dict( |
||||
|
report_id=self.report.id, |
||||
|
description='kpi 4', |
||||
|
name='k4', |
||||
|
multi=False, |
||||
|
expression='k1 + k2 + k3', |
||||
|
)) |
||||
|
# kpi with 2 constants |
||||
|
self.env['mis.report.kpi'].create(dict( |
||||
|
report_id=self.report.id, |
||||
|
description='kpi 3', |
||||
|
name='k3', |
||||
|
multi=True, |
||||
|
expression_ids=[(0, 0, dict( |
||||
|
name='AccountingNone', |
||||
|
subkpi_id=self.report.subkpi_ids[0].id, |
||||
|
)), (0, 0, dict( |
||||
|
name='1.0', |
||||
|
subkpi_id=self.report.subkpi_ids[1].id, |
||||
|
))], |
||||
|
)) |
||||
|
# kpi with a NameError (x not defined) |
||||
|
self.env['mis.report.kpi'].create(dict( |
||||
|
report_id=self.report.id, |
||||
|
description='kpi 5', |
||||
|
name='k5', |
||||
|
multi=True, |
||||
|
expression_ids=[(0, 0, dict( |
||||
|
name='x', |
||||
|
subkpi_id=self.report.subkpi_ids[0].id, |
||||
|
)), (0, 0, dict( |
||||
|
name='1.0', |
||||
|
subkpi_id=self.report.subkpi_ids[1].id, |
||||
|
))], |
||||
|
)) |
||||
|
# create a report instance |
||||
|
self.report_instance = self.env['mis.report.instance'].create(dict( |
||||
|
name='test instance', |
||||
|
report_id=self.report.id, |
||||
|
company_id=self.env.ref('base.main_company').id, |
||||
|
period_ids=[(0, 0, dict( |
||||
|
name='p1', |
||||
|
mode='relative', |
||||
|
type='d', |
||||
|
subkpi_ids=[(4, self.report.subkpi_ids[0].id, None)], |
||||
|
)), (0, 0, dict( |
||||
|
name='p2', |
||||
|
mode='fix', |
||||
|
manual_date_from='2014-01-01', |
||||
|
manual_date_to='2014-12-31', |
||||
|
))], |
||||
|
)) |
||||
|
self.report_instance.period_ids[1].comparison_column_ids = \ |
||||
|
[(4, self.report_instance.period_ids[0].id, None)] |
||||
|
|
||||
|
def test_json(self): |
||||
|
self.report_instance.compute() |
||||
|
|
||||
|
def test_qweb(self): |
||||
|
test_reports.try_report(self.env.cr, self.env.uid, |
||||
|
'mis_builder.report_mis_report_instance', |
||||
|
[self.report_instance.id], |
||||
|
report_type='qweb-pdf') |
||||
|
|
||||
|
def test_xlsx(self): |
||||
|
test_reports.try_report(self.env.cr, self.env.uid, |
||||
|
'mis.report.instance.xlsx', |
||||
|
[self.report_instance.id], |
||||
|
report_type='xlsx') |
@ -0,0 +1,27 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import openerp.tests.common as common |
||||
|
|
||||
|
from ..models.mis_safe_eval import mis_safe_eval, DataError, NameDataError |
||||
|
|
||||
|
|
||||
|
class TestMisSafeEval(common.TransactionCase): |
||||
|
|
||||
|
def test_nominal(self): |
||||
|
val = mis_safe_eval('a + 1', {'a': 1}) |
||||
|
self.assertEqual(val, 2) |
||||
|
|
||||
|
def test_exceptions(self): |
||||
|
val = mis_safe_eval('1/0', {}) # division by zero |
||||
|
self.assertTrue(isinstance(val, DataError)) |
||||
|
self.assertEqual(val.name, '#DIV/0') |
||||
|
val = mis_safe_eval('1a', {}) # syntax error |
||||
|
self.assertTrue(isinstance(val, DataError)) |
||||
|
self.assertEqual(val.name, '#ERR') |
||||
|
|
||||
|
def test_name_error(self): |
||||
|
val = mis_safe_eval('a + 1', {}) |
||||
|
self.assertTrue(isinstance(val, NameDataError)) |
||||
|
self.assertEqual(val.name, '#NAME') |
@ -0,0 +1,158 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import openerp.tests.common as common |
||||
|
|
||||
|
from ..models.accounting_none import AccountingNone |
||||
|
from ..models.mis_report_style import ( |
||||
|
TYPE_NUM, TYPE_PCT, TYPE_STR, CMP_DIFF, CMP_PCT |
||||
|
) |
||||
|
|
||||
|
|
||||
|
class TestRendering(common.TransactionCase): |
||||
|
|
||||
|
def setUp(self): |
||||
|
super(TestRendering, self).setUp() |
||||
|
self.style_obj = self.env['mis.report.style'] |
||||
|
self.kpi_obj = self.env['mis.report.kpi'] |
||||
|
self.style = self.style_obj.create(dict( |
||||
|
name='teststyle', |
||||
|
)) |
||||
|
self.lang = self.env['res.lang'].search([('code', '=', 'en_US')])[0] |
||||
|
|
||||
|
def _render(self, value, type=TYPE_NUM): |
||||
|
style_props = self.style_obj.merge([self.style]) |
||||
|
return self.style_obj.render(self.lang, style_props, type, value) |
||||
|
|
||||
|
def _compare_and_render(self, value, base_value, |
||||
|
type=TYPE_NUM, compare_method=CMP_PCT): |
||||
|
style_props = self.style_obj.merge([self.style]) |
||||
|
return self.style_obj.compare_and_render(self.lang, style_props, |
||||
|
type, compare_method, |
||||
|
value, base_value)[:2] |
||||
|
|
||||
|
def test_render(self): |
||||
|
self.assertEquals(u'1', self._render(1)) |
||||
|
self.assertEquals(u'1', self._render(1.1)) |
||||
|
self.assertEquals(u'2', self._render(1.6)) |
||||
|
self.style.dp_inherit = False |
||||
|
self.style.dp = 2 |
||||
|
self.assertEquals(u'1.00', self._render(1)) |
||||
|
self.assertEquals(u'1.10', self._render(1.1)) |
||||
|
self.assertEquals(u'1.60', self._render(1.6)) |
||||
|
self.assertEquals(u'1.61', self._render(1.606)) |
||||
|
self.assertEquals(u'12,345.67', self._render(12345.67)) |
||||
|
|
||||
|
def test_render_negative(self): |
||||
|
# non breaking hyphen |
||||
|
self.assertEquals(u'\u20111', self._render(-1)) |
||||
|
|
||||
|
def test_render_zero(self): |
||||
|
self.assertEquals(u'0', self._render(0)) |
||||
|
self.assertEquals(u'', self._render(None)) |
||||
|
self.assertEquals(u'', self._render(AccountingNone)) |
||||
|
|
||||
|
def test_render_suffix(self): |
||||
|
self.style.suffix_inherit = False |
||||
|
self.style.suffix = u'€' |
||||
|
self.assertEquals(u'1\xa0€', self._render(1)) |
||||
|
self.style.suffix = u'k€' |
||||
|
self.style.divider_inherit = False |
||||
|
self.style.divider = '1e3' |
||||
|
self.assertEquals(u'1\xa0k€', self._render(1000)) |
||||
|
|
||||
|
def test_render_prefix(self): |
||||
|
self.style.prefix_inherit = False |
||||
|
self.style.prefix = u'$' |
||||
|
self.assertEquals(u'$\xa01', self._render(1)) |
||||
|
self.style.prefix = u'k$' |
||||
|
self.style.divider_inherit = False |
||||
|
self.style.divider = '1e3' |
||||
|
self.assertEquals(u'k$\xa01', self._render(1000)) |
||||
|
|
||||
|
def test_render_divider(self): |
||||
|
self.style.divider_inherit = False |
||||
|
self.style.divider = '1e3' |
||||
|
self.style.dp_inherit = False |
||||
|
self.style.dp = 0 |
||||
|
self.assertEquals(u'1', self._render(1000)) |
||||
|
self.style.divider = '1e6' |
||||
|
self.style.dp = 3 |
||||
|
self.assertEquals(u'0.001', self._render(1000)) |
||||
|
self.style.divider = '1e-3' |
||||
|
self.style.dp = 0 |
||||
|
self.assertEquals(u'1,000', self._render(1)) |
||||
|
self.style.divider = '1e-6' |
||||
|
self.style.dp = 0 |
||||
|
self.assertEquals(u'1,000,000', self._render(1)) |
||||
|
|
||||
|
def test_render_pct(self): |
||||
|
self.assertEquals(u'100\xa0%', self._render(1, TYPE_PCT)) |
||||
|
self.assertEquals(u'50\xa0%', self._render(0.5, TYPE_PCT)) |
||||
|
self.style.dp_inherit = False |
||||
|
self.style.dp = 2 |
||||
|
self.assertEquals(u'51.23\xa0%', self._render(0.5123, TYPE_PCT)) |
||||
|
|
||||
|
def test_render_string(self): |
||||
|
self.assertEquals(u'', self._render('', TYPE_STR)) |
||||
|
self.assertEquals(u'', self._render(None, TYPE_STR)) |
||||
|
self.assertEquals(u'abcdé', self._render(u'abcdé', TYPE_STR)) |
||||
|
|
||||
|
def test_compare_num_pct(self): |
||||
|
self.assertEquals((1.0, u'+100.0\xa0%'), |
||||
|
self._compare_and_render(100, 50)) |
||||
|
self.assertEquals((0.5, u'+50.0\xa0%'), |
||||
|
self._compare_and_render(75, 50)) |
||||
|
self.assertEquals((0.5, u'+50.0\xa0%'), |
||||
|
self._compare_and_render(-25, -50)) |
||||
|
self.assertEquals((1.0, u'+100.0\xa0%'), |
||||
|
self._compare_and_render(0, -50)) |
||||
|
self.assertEquals((2.0, u'+200.0\xa0%'), |
||||
|
self._compare_and_render(50, -50)) |
||||
|
self.assertEquals((-0.5, u'\u201150.0\xa0%'), |
||||
|
self._compare_and_render(25, 50)) |
||||
|
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), |
||||
|
self._compare_and_render(0, 50)) |
||||
|
self.assertEquals((-2.0, u'\u2011200.0\xa0%'), |
||||
|
self._compare_and_render(-50, 50)) |
||||
|
self.assertEquals((-0.5, u'\u201150.0\xa0%'), |
||||
|
self._compare_and_render(-75, -50)) |
||||
|
self.assertEquals((AccountingNone, u''), |
||||
|
self._compare_and_render(50, AccountingNone)) |
||||
|
self.assertEquals((AccountingNone, u''), |
||||
|
self._compare_and_render(50, None)) |
||||
|
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), |
||||
|
self._compare_and_render(AccountingNone, 50)) |
||||
|
self.assertEquals((-1.0, u'\u2011100.0\xa0%'), |
||||
|
self._compare_and_render(None, 50)) |
||||
|
|
||||
|
def test_compare_num_diff(self): |
||||
|
self.assertEquals((25, u'+25'), |
||||
|
self._compare_and_render(75, 50, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.assertEquals((-25, u'\u201125'), |
||||
|
self._compare_and_render(25, 50, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.style.suffix_inherit = False |
||||
|
self.style.suffix = u'€' |
||||
|
self.assertEquals((-25, u'\u201125\xa0€'), |
||||
|
self._compare_and_render(25, 50, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.style.suffix = u'' |
||||
|
self.assertEquals((50.0, u'+50'), |
||||
|
self._compare_and_render(50, AccountingNone, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.assertEquals((50.0, u'+50'), |
||||
|
self._compare_and_render(50, None, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.assertEquals((-50.0, u'\u201150'), |
||||
|
self._compare_and_render(AccountingNone, 50, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
self.assertEquals((-50.0, u'\u201150'), |
||||
|
self._compare_and_render(None, 50, |
||||
|
TYPE_NUM, CMP_DIFF)) |
||||
|
|
||||
|
def test_compare_pct(self): |
||||
|
self.assertEquals((0.25, u'+25\xa0pp'), |
||||
|
self._compare_and_render(0.75, 0.50, TYPE_PCT)) |
@ -0,0 +1,12 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import doctest |
||||
|
|
||||
|
from openerp.addons.mis_builder.models import simple_array |
||||
|
|
||||
|
|
||||
|
def load_tests(loader, tests, ignore): |
||||
|
tests.addTests(doctest.DocTestSuite(simple_array)) |
||||
|
return tests |
@ -0,0 +1,25 @@ |
|||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
|
import openerp.tests.common as common |
||||
|
|
||||
|
from ..models.mis_report import _utc_midnight |
||||
|
|
||||
|
|
||||
|
class TestUtcMidnight(common.TransactionCase): |
||||
|
|
||||
|
def test_utc_midnight(self): |
||||
|
date_to_convert = '2014-07-05' |
||||
|
date_time_convert = _utc_midnight( |
||||
|
date_to_convert, 'Europe/Brussels') |
||||
|
self.assertEqual(date_time_convert, '2014-07-04 22:00:00') |
||||
|
date_time_convert = _utc_midnight( |
||||
|
date_to_convert, 'Europe/Brussels', add_day=1) |
||||
|
self.assertEqual(date_time_convert, '2014-07-05 22:00:00') |
||||
|
date_time_convert = _utc_midnight( |
||||
|
date_to_convert, 'US/Pacific') |
||||
|
self.assertEqual(date_time_convert, '2014-07-05 07:00:00') |
||||
|
date_time_convert = _utc_midnight( |
||||
|
date_to_convert, 'US/Pacific', add_day=1) |
||||
|
self.assertEqual(date_time_convert, '2014-07-06 07:00:00') |
@ -1,211 +0,0 @@ |
|||||
<?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,170 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<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="style_id"/> |
||||
|
</group> |
||||
|
<group string="Sub KPI's"> |
||||
|
<field name="subkpi_ids" nolabel="1" colspan="2"> |
||||
|
<tree string="Sub KPI's" editable="bottom"> |
||||
|
<field name="sequence" widget="handle"/> |
||||
|
<field name="description"/> |
||||
|
<field name="name"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</group> |
||||
|
<group string="Queries"> |
||||
|
<field name="query_ids" nolabel="1" colspan="2"> |
||||
|
<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> |
||||
|
</group> |
||||
|
<group string="KPI's"> |
||||
|
<field name="kpi_ids" nolabel="1" colspan="2"> |
||||
|
<tree string="KPI's"> |
||||
|
<field name="sequence" widget="handle"/> |
||||
|
<field name="description"/> |
||||
|
<field name="name"/> |
||||
|
<field name="type"/> |
||||
|
<field name="compare_method" attrs="{'invisible': [('type', '=', 'str')]}"/> |
||||
|
<field name="multi"/> |
||||
|
<field name="expression"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</group> |
||||
|
<group col="2" string="Legend (for kpi expressions)"> |
||||
|
<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 id="mis_report_view_kpi_form" model="ir.ui.view"> |
||||
|
<field name="name">mis.report.view.kpi.form</field> |
||||
|
<field name="model">mis.report.kpi</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="MIS Report KPI" version="7.0"> |
||||
|
<group col="4"> |
||||
|
<field name="description"/> |
||||
|
<field name="name"/> |
||||
|
<field name="type"/> |
||||
|
<field name="compare_method"/> |
||||
|
<field name="style_id"/> |
||||
|
<field name="style_expression"/> |
||||
|
<!--<field name="sequence" />--> |
||||
|
</group> |
||||
|
<group string="Expression"> |
||||
|
<field name="multi"/> |
||||
|
<field name="expression_ids" colspan="4" nolabel="1" |
||||
|
delete="0" create="0" |
||||
|
attrs="{'invisible': [('multi', '=', False)]}"> |
||||
|
<tree editable="bottom"> |
||||
|
<field name="subkpi_id"/> |
||||
|
<field name="name"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
<field name="expression" colspan="4" nolabel="1" |
||||
|
attrs="{'invisible': [('multi', '=', True)], |
||||
|
'readonly': [('multi', '=', True)]}"/> |
||||
|
</group> |
||||
|
<group col="4" string="Auto expand"> |
||||
|
<field name="auto_expand_accounts"/> |
||||
|
<field name="auto_expand_accounts_style_id" |
||||
|
attrs="{'invisible': [('auto_expand_accounts', '!=', True)]}"/> |
||||
|
</group> |
||||
|
<group col="2" string="Legend (for kpi expressions)"> |
||||
|
<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> |
||||
|
|
||||
|
</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"/> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,204 @@ |
|||||
|
<?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_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="20 "/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="MIS Report Result" version="7.0" edit="false" create="false" delete="false" > |
||||
|
<widget type="mis_report"></widget> |
||||
|
</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"> |
||||
|
<button name="preview" type="object" icon="gtk-print-preview" /> |
||||
|
<button type="object" name="print_pdf" string="Print" icon="gtk-print" /> |
||||
|
<button type="object" name="export_xls" string="Export" icon="gtk-go-down" /> |
||||
|
<field name="name"/> |
||||
|
<field name="report_id" string="Template"/> |
||||
|
<field name="company_id" groups="base.group_multi_company"/> |
||||
|
<field name="target_move"/> |
||||
|
<field name="pivot_date"/> |
||||
|
</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="15"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="MIS Report Instance" version="7.0"> |
||||
|
<sheet> |
||||
|
<field name="temporary" invisible="1"/> |
||||
|
<div class="oe_read_only oe_right oe_button_box" name="buttons"> |
||||
|
<button type="object" name="preview" string="Preview" icon="gtk-print-preview" /> |
||||
|
<button type="object" name="print_pdf" string="Print" icon="gtk-print" /> |
||||
|
<button type="object" name="export_xls" string="Export" icon="gtk-go-down" /> |
||||
|
<button type="action" name="%(mis_report_instance_add_to_dashboard_action)d" string="Add to dashboard" icon="gtk-add" attrs="{'invisible': [('temporary', '=', True)]}"/> |
||||
|
<button type="object" name="save_report" string="Save" icon="gtk-floppy" attrs="{'invisible': [('temporary', '=', False)]}"/> |
||||
|
</div> |
||||
|
<div class="oe_title"> |
||||
|
<div class="oe_edit_only"> |
||||
|
<label for="name"/> |
||||
|
</div> |
||||
|
<h1> |
||||
|
<field name="name" placeholder="Name"/> |
||||
|
</h1> |
||||
|
<field name="description"/> |
||||
|
</div> |
||||
|
<group> |
||||
|
<group> |
||||
|
<field name="report_id" string="Template"/> |
||||
|
<field name="company_id" groups="base.group_multi_company"/> |
||||
|
<field name="target_move" widget="radio"/> |
||||
|
<field name="landscape_pdf"/> |
||||
|
<field name="comparison_mode"/> |
||||
|
</group> |
||||
|
<group> |
||||
|
<group name="simple_mode" |
||||
|
attrs="{'invisible': [('comparison_mode', '=', True)]}" colspan="4"> |
||||
|
<field name="date_range_id"/> |
||||
|
<field name="date_from" attrs="{'required': [('comparison_mode', '=', False)]}"/> |
||||
|
<field name="date_to" attrs="{'required': [('comparison_mode', '=', False)]}"/> |
||||
|
</group> |
||||
|
</group> |
||||
|
</group> |
||||
|
<group name="comparison_mode" string="Comparison" |
||||
|
attrs="{'invisible': [('comparison_mode', '=', False)]}" colspan="4"> |
||||
|
<field name="period_ids" colspan="4" nolabel="1" attrs="{'required': [('comparison_mode', '=', True)]}"> |
||||
|
<tree string="KPI's" colors="red:valid==False"> |
||||
|
<field name="sequence" widget="handle"/> |
||||
|
<field name="name"/> |
||||
|
<field name="date_from"/> |
||||
|
<field name="date_to"/> |
||||
|
<field name="valid" invisible="1"/> |
||||
|
<field name="report_instance_id" invisible="1"/> |
||||
|
<field name="id" invisible="1"/> |
||||
|
<field name="subkpi_ids" |
||||
|
domain="[('report_id', '=', parent.report_id)]" |
||||
|
widget="many2many_tags"/> |
||||
|
<field name="comparison_column_ids" domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" widget="many2many_tags"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
<field name="date"/> |
||||
|
</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> |
||||
|
<field name="domain">[('temporary', '=', False)]</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"/> |
||||
|
|
||||
|
<record id="wizard_mis_report_instance_view_form" model="ir.ui.view"> |
||||
|
<field name="model">mis.report.instance</field> |
||||
|
<field name="inherit_id" ref="mis_builder.mis_report_instance_view_form"/> |
||||
|
<field name="mode">primary</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<field name="name" position="attributes"> |
||||
|
<attribute name="readonly">1</attribute> |
||||
|
</field> |
||||
|
<label for="name" position="replace"/> |
||||
|
<field name="report_id" position="attributes"> |
||||
|
<attribute name="invisible">1</attribute> |
||||
|
</field> |
||||
|
<div name="buttons" position="attributes"> |
||||
|
<attribute name="invisible">1</attribute> |
||||
|
</div> |
||||
|
<sheet position="after"> |
||||
|
<footer> |
||||
|
<button type="object" name="save_report" string="Save" icon="gtk-floppy"/> |
||||
|
<button type="object" name="preview" string="Preview" icon="gtk-print-preview" /> |
||||
|
<button type="object" name="print_pdf" string="Print" icon="gtk-print" /> |
||||
|
<button type="object" name="export_xls" string="Export" icon="gtk-go-down" /> |
||||
|
or <button string="Cancel" class="oe_link" special="cancel" /> |
||||
|
</footer> |
||||
|
</sheet> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.actions.act_window" id="last_mis_report_instance_view_action"> |
||||
|
<field name="name">Last Reports Generated</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> |
||||
|
<field name="domain">[('temporary', '=', True)]</field> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="last_wizard_mis_report_instance_view_menu" |
||||
|
parent="account.menu_finance_reports" |
||||
|
name="Last Reports Generated" |
||||
|
action="last_mis_report_instance_view_action" |
||||
|
sequence="102"/> |
||||
|
|
||||
|
<record model="ir.ui.view" id="mis_report_instance_period_view_form"> |
||||
|
<field name="model">mis.report.instance.period</field> |
||||
|
<field name="priority" eval="16"/> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="KPI's"> |
||||
|
<sheet> |
||||
|
<group> |
||||
|
<field name="mode" widget="radio"/> |
||||
|
<field name="valid" invisible="1"/> |
||||
|
<group name="relative" attrs="{'invisible': [('mode', '!=', 'relative')]}" colspan="4"> |
||||
|
<group> |
||||
|
<field name="type" attrs="{'required': [('mode', '=', 'relative')]}"/> |
||||
|
<field name="date_range_type_id" |
||||
|
attrs="{'invisible': [('type', '!=', 'date_range')], 'required': [('type', '=', 'date_range')]}"/> |
||||
|
<field name="offset"/> |
||||
|
<field name="duration"/> |
||||
|
</group> |
||||
|
<group> |
||||
|
<field name="date_from"/> |
||||
|
<field name="date_to"/> |
||||
|
</group> |
||||
|
</group> |
||||
|
<group name="fix" attrs="{'invisible': [('mode', '!=', 'fix')]}" colspan="4"> |
||||
|
<field name="date_range_id"/> |
||||
|
<field name="manual_date_from" |
||||
|
attrs="{'required': [('mode', '=', 'fix')]}"/> |
||||
|
<field name="manual_date_to" |
||||
|
attrs="{'required': [('mode', '=', 'fix')]}"/> |
||||
|
</group> |
||||
|
<field name="name" placeholder="Name"/> |
||||
|
<field name="normalize_factor"/> |
||||
|
<field name="report_instance_id" invisible="1"/> |
||||
|
<field name="id" invisible="1"/> |
||||
|
<field name="subkpi_ids" |
||||
|
domain="[('report_id', '=', parent.report_id)]" |
||||
|
widget="many2many_tags"/> |
||||
|
<field name="comparison_column_ids" |
||||
|
domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" |
||||
|
widget="many2many_tags"/> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -0,0 +1,80 @@ |
|||||
|
<?xml version="1.0" encoding="UTF-8"?> |
||||
|
<openerp> |
||||
|
<data> |
||||
|
|
||||
|
<record model="ir.ui.view" id="mis_report_style_view_tree"> |
||||
|
<field name="name">mis.report.style.view.tree</field> |
||||
|
<field name="model">mis.report.style</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<tree string="MIS Report Styles"> |
||||
|
<field name="name"/> |
||||
|
</tree> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record id="mis_report_style_view_form" model="ir.ui.view"> |
||||
|
<field name="name">mis.report.style.view.form</field> |
||||
|
<field name="model">mis.report.style</field> |
||||
|
<field name="arch" type="xml"> |
||||
|
<form string="MIS Report Style" version="7.0"> |
||||
|
<sheet> |
||||
|
<group string="Style" col="2"> |
||||
|
<field name="name" /> |
||||
|
</group> |
||||
|
<group string="Number" col="4"> |
||||
|
<field name="dp_inherit" string="Rounding inherit"/> |
||||
|
<field name="dp" |
||||
|
attrs="{'invisible': [('dp_inherit', '=', True)]}"/> |
||||
|
<field name="divider_inherit" string="Factor inherit"/> |
||||
|
<field name="divider" |
||||
|
attrs="{'invisible': [('divider_inherit', '=', True)]}"/> |
||||
|
<field name="prefix_inherit"/> |
||||
|
<field name="prefix" |
||||
|
attrs="{'invisible': [('prefix_inherit', '=', True)]}"/> |
||||
|
<field name="suffix_inherit"/> |
||||
|
<field name="suffix" |
||||
|
attrs="{'invisible': [('suffix_inherit', '=', True)]}"/> |
||||
|
</group> |
||||
|
<group string="Color" col="4"> |
||||
|
<field name="color_inherit" /> |
||||
|
<field name="color" |
||||
|
attrs="{'invisible': [('color_inherit', '=', True)]}" |
||||
|
widget="color" /> |
||||
|
<field name="background_color_inherit" /> |
||||
|
<field name="background_color" |
||||
|
attrs="{'invisible': [('background_color_inherit', '=', True)]}" |
||||
|
widget="color" /> |
||||
|
</group> |
||||
|
<group string="Font" col="4"> |
||||
|
<field name="font_style_inherit" /> |
||||
|
<field name="font_style" |
||||
|
attrs="{'invisible': [('font_style_inherit', '=', True)]}" /> |
||||
|
<field name="font_weight_inherit" /> |
||||
|
<field name="font_weight" |
||||
|
attrs="{'invisible': [('font_weight_inherit', '=', True)]}" /> |
||||
|
<field name="font_size_inherit" /> |
||||
|
<field name="font_size" |
||||
|
attrs="{'invisible': [('font_size_inherit', '=', True)]}" /> |
||||
|
</group> |
||||
|
<group string="Indent" col="4"> |
||||
|
<field name="indent_level_inherit" /> |
||||
|
<field name="indent_level" |
||||
|
attrs="{'invisible': [('indent_level_inherit', '=', True)]}" /> |
||||
|
</group> |
||||
|
</sheet> |
||||
|
</form> |
||||
|
</field> |
||||
|
</record> |
||||
|
|
||||
|
<record model="ir.actions.act_window" id="mis_report_style_view_action"> |
||||
|
<field name="name">MIS Report Styles</field> |
||||
|
<field name="view_id" ref="mis_report_style_view_tree"/> |
||||
|
<field name="res_model">mis.report.style</field> |
||||
|
<field name="view_type">form</field> |
||||
|
<field name="view_mode">tree,form</field> |
||||
|
</record> |
||||
|
|
||||
|
<menuitem id="mis_report_style_view_menu" parent="account.menu_account_reports" name="MIS Report Styles" action="mis_report_style_view_action" sequence="22"/> |
||||
|
|
||||
|
</data> |
||||
|
</openerp> |
@ -1,25 +1,5 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
from . import mis_builder_dashboard |
from . import mis_builder_dashboard |
@ -1,87 +1,66 @@ |
|||||
# -*- 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/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
# -*- coding: utf-8 -*- |
||||
|
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
||||
|
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
||||
|
|
||||
from openerp.osv import orm, fields |
|
||||
|
from openerp import api, fields, models |
||||
from lxml import etree |
from lxml import etree |
||||
|
|
||||
|
|
||||
class add_mis_report_instance_dashboard(orm.TransientModel): |
|
||||
|
class AddMisReportInstanceDashboard(models.TransientModel): |
||||
_name = "add.mis.report.instance.dashboard.wizard" |
_name = "add.mis.report.instance.dashboard.wizard" |
||||
|
|
||||
_columns = {'name': fields.char('Name', size=32, required=True), |
|
||||
'dashboard_id': fields.many2one( |
|
||||
'ir.actions.act_window', |
|
||||
string="Dashboard", required=True, |
|
||||
domain="[('res_model', '=', 'board.board')]"), |
|
||||
} |
|
||||
|
name = fields.Char('Name', size=32, required=True) |
||||
|
|
||||
def default_get(self, cr, uid, fields, context=None): |
|
||||
if context is None: |
|
||||
context = {} |
|
||||
if context.get('active_id'): |
|
||||
res = super(add_mis_report_instance_dashboard, self).default_get( |
|
||||
cr, uid, fields, context=context) |
|
||||
|
dashboard_id = fields.Many2one('ir.actions.act_window', |
||||
|
string="Dashboard", required=True, |
||||
|
domain="[('res_model', '=', " |
||||
|
"'board.board')]") |
||||
|
|
||||
|
@api.model |
||||
|
def default_get(self, fields): |
||||
|
res = {} |
||||
|
if self.env.context.get('active_id', False): |
||||
|
res = super(AddMisReportInstanceDashboard, self).default_get( |
||||
|
fields) |
||||
# get report instance name |
# get report instance name |
||||
res['name'] = self.pool['mis.report.instance'].read( |
|
||||
cr, uid, context['active_id'], ['name'])['name'] |
|
||||
|
res['name'] = self.env['mis.report.instance'].browse( |
||||
|
self.env.context['active_id']).name |
||||
return res |
return res |
||||
|
|
||||
def action_add_to_dashboard(self, cr, uid, ids, context=None): |
|
||||
if context is None: |
|
||||
context = {} |
|
||||
assert 'active_id' in context, "active_id missing in context" |
|
||||
wizard_data = self.browse(cr, uid, ids, context=context)[0] |
|
||||
|
@api.multi |
||||
|
def action_add_to_dashboard(self): |
||||
|
assert self.env.context.get('active_id', False), \ |
||||
|
"active_id missing in context" |
||||
# create the act_window corresponding to this report |
# create the act_window corresponding to this report |
||||
view_id = self.pool['ir.model.data'].get_object_reference( |
|
||||
cr, uid, 'mis_builder', 'mis_report_instance_result_view_form')[1] |
|
||||
report_result = self.pool['ir.actions.act_window'].create( |
|
||||
cr, uid, |
|
||||
|
self.env.ref('mis_builder.mis_report_instance_result_view_form') |
||||
|
view = self.env.ref( |
||||
|
'mis_builder.mis_report_instance_result_view_form') |
||||
|
report_result = self.env['ir.actions.act_window'].create( |
||||
{'name': 'mis.report.instance.result.view.action.%d' |
{'name': 'mis.report.instance.result.view.action.%d' |
||||
% context['active_id'], |
|
||||
|
% self.env.context['active_id'], |
||||
'res_model': 'mis.report.instance', |
'res_model': 'mis.report.instance', |
||||
'res_id': context['active_id'], |
|
||||
|
'res_id': self.env.context['active_id'], |
||||
'target': 'current', |
'target': 'current', |
||||
'view_mode': 'form', |
'view_mode': 'form', |
||||
'view_id': view_id}) |
|
||||
|
'view_id': view.id}) |
||||
# add this result in the selected dashboard |
# add this result in the selected dashboard |
||||
last_customization = self.pool['ir.ui.view.custom'].search( |
|
||||
cr, uid, |
|
||||
[('user_id', '=', uid), |
|
||||
('ref_id', '=', wizard_data.dashboard_id.view_id.id)], limit=1) |
|
||||
arch = wizard_data.dashboard_id.view_id.arch |
|
||||
|
last_customization = self.env['ir.ui.view.custom'].search( |
||||
|
[('user_id', '=', self.env.uid), |
||||
|
('ref_id', '=', self.dashboard_id.view_id.id)], limit=1) |
||||
|
arch = self.dashboard_id.view_id.arch |
||||
if last_customization: |
if last_customization: |
||||
arch = self.pool['ir.ui.view.custom'].read( |
|
||||
cr, uid, last_customization[0], ['arch'])['arch'] |
|
||||
|
arch = self.env['ir.ui.view.custom'].browse( |
||||
|
last_customization[0].id).arch |
||||
new_arch = etree.fromstring(arch) |
new_arch = etree.fromstring(arch) |
||||
column = new_arch.xpath("//column")[0] |
column = new_arch.xpath("//column")[0] |
||||
column.append(etree.Element('action', {'context': str(context), |
|
||||
'name': str(report_result), |
|
||||
'string': wizard_data.name, |
|
||||
'view_mode': 'form'})) |
|
||||
self.pool['ir.ui.view.custom'].create( |
|
||||
cr, uid, {'user_id': uid, |
|
||||
'ref_id': wizard_data.dashboard_id.view_id.id, |
|
||||
'arch': etree.tostring(new_arch, pretty_print=True)}) |
|
||||
|
column.append(etree.Element('action', {'context': str( |
||||
|
self.env.context), |
||||
|
'name': str(report_result.id), |
||||
|
'string': self.name, |
||||
|
'view_mode': 'form'})) |
||||
|
self.env['ir.ui.view.custom'].create( |
||||
|
{'user_id': self.env.uid, |
||||
|
'ref_id': self.dashboard_id.view_id.id, |
||||
|
'arch': etree.tostring(new_arch, pretty_print=True)}) |
||||
|
|
||||
return {'type': 'ir.actions.act_window_close', } |
return {'type': 'ir.actions.act_window_close', } |
@ -1,3 +1,5 @@ |
|||||
# list the OCA project dependencies, one per line |
# list the OCA project dependencies, one per line |
||||
# add a github url if you need a forked version |
# add a github url if you need a forked version |
||||
reporting-engine |
reporting-engine |
||||
|
server-tools |
||||
|
web |
Write
Preview
Loading…
Cancel
Save
Reference in new issue