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
-
444mis_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
-
127mis_builder/static/src/js/mis_builder.js
-
70mis_builder/static/src/xml/mis_widget.xml
-
36mis_builder/tests/__init__.py
-
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
-
105mis_builder/wizard/mis_builder_dashboard.py
-
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 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 |
@ -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,62 +1,56 @@ |
|||
<template> |
|||
<t t-name="mis_builder.MisReport"> |
|||
<p> </p> |
|||
<table t-if="widget.mis_report_data" class="oe_list_content mis_builder"> |
|||
<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 class="oe_list_header_columns"> |
|||
<t t-foreach="widget.mis_report_data.header" t-as="h"> |
|||
<tr t-foreach="widget.mis_report_data.header" t-as="row" class="oe_list_header_columns"> |
|||
<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> |
|||
<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.content" t-as="c"> |
|||
<td t-att="{'style': c_value.default_style}"> |
|||
<div> |
|||
<t t-esc="c_value.kpi_name"/> |
|||
</div> |
|||
<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> |
|||
</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"> |
|||
<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(value_value.drilldown)" |
|||
t-att-data-period-id="JSON.stringify(value_value.period_id)" |
|||
t-att-data-expr="JSON.stringify(value_value.expr)" |
|||
t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)" |
|||
> |
|||
<t t-esc="value_value.val_r"/> |
|||
<t t-esc="cell.val_r"/> |
|||
</a> |
|||
</t> |
|||
<t t-if="!value_value.drilldown"> |
|||
<t t-esc="value_value.val_r"/> |
|||
<t t-if="!cell.drilldown_arg"> |
|||
<t t-esc="cell.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> |
|||
<tr></tr> |
|||
</tfoot> |
|||
</table> |
|||
</t> |
|||
</div> |
|||
</t> |
|||
</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","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" |
|||
"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 |
@ -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 |
|||
|
|||
|
|||
class add_mis_report_instance_dashboard(orm.TransientModel): |
|||
class AddMisReportInstanceDashboard(models.TransientModel): |
|||
_name = "add.mis.report.instance.dashboard.wizard" |
|||
|
|||
_columns = {'name': fields.char('Name', size=32, required=True), |
|||
'dashboard_id': fields.many2one( |
|||
'ir.actions.act_window', |
|||
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')]"), |
|||
} |
|||
domain="[('res_model', '=', " |
|||
"'board.board')]") |
|||
|
|||
def default_get(self, cr, uid, fields, context=None): |
|||
if context is None: |
|||
context = {} |
|||
if context.get('active_id'): |
|||
res = super(add_mis_report_instance_dashboard, self).default_get( |
|||
cr, uid, fields, context=context) |
|||
@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 |
|||
res['name'] = self.pool['mis.report.instance'].read( |
|||
cr, uid, context['active_id'], ['name'])['name'] |
|||
res['name'] = self.env['mis.report.instance'].browse( |
|||
self.env.context['active_id']).name |
|||
return res |
|||
|
|||
def action_add_to_dashboard(self, cr, uid, ids, context=None): |
|||
if context is None: |
|||
context = {} |
|||
assert 'active_id' in context, "active_id missing in context" |
|||
wizard_data = self.browse(cr, uid, ids, context=context)[0] |
|||
@api.multi |
|||
def action_add_to_dashboard(self): |
|||
assert self.env.context.get('active_id', False), \ |
|||
"active_id missing in context" |
|||
# create the act_window corresponding to this report |
|||
view_id = self.pool['ir.model.data'].get_object_reference( |
|||
cr, uid, 'mis_builder', 'mis_report_instance_result_view_form')[1] |
|||
report_result = self.pool['ir.actions.act_window'].create( |
|||
cr, uid, |
|||
self.env.ref('mis_builder.mis_report_instance_result_view_form') |
|||
view = self.env.ref( |
|||
'mis_builder.mis_report_instance_result_view_form') |
|||
report_result = self.env['ir.actions.act_window'].create( |
|||
{'name': 'mis.report.instance.result.view.action.%d' |
|||
% context['active_id'], |
|||
% self.env.context['active_id'], |
|||
'res_model': 'mis.report.instance', |
|||
'res_id': context['active_id'], |
|||
'res_id': self.env.context['active_id'], |
|||
'target': 'current', |
|||
'view_mode': 'form', |
|||
'view_id': view_id}) |
|||
'view_id': view.id}) |
|||
# add this result in the selected dashboard |
|||
last_customization = self.pool['ir.ui.view.custom'].search( |
|||
cr, uid, |
|||
[('user_id', '=', uid), |
|||
('ref_id', '=', wizard_data.dashboard_id.view_id.id)], limit=1) |
|||
arch = wizard_data.dashboard_id.view_id.arch |
|||
last_customization = self.env['ir.ui.view.custom'].search( |
|||
[('user_id', '=', self.env.uid), |
|||
('ref_id', '=', self.dashboard_id.view_id.id)], limit=1) |
|||
arch = self.dashboard_id.view_id.arch |
|||
if last_customization: |
|||
arch = self.pool['ir.ui.view.custom'].read( |
|||
cr, uid, last_customization[0], ['arch'])['arch'] |
|||
arch = self.env['ir.ui.view.custom'].browse( |
|||
last_customization[0].id).arch |
|||
new_arch = etree.fromstring(arch) |
|||
column = new_arch.xpath("//column")[0] |
|||
column.append(etree.Element('action', {'context': str(context), |
|||
'name': str(report_result), |
|||
'string': wizard_data.name, |
|||
column.append(etree.Element('action', {'context': str( |
|||
self.env.context), |
|||
'name': str(report_result.id), |
|||
'string': self.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, |
|||
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', } |
@ -1,3 +1,5 @@ |
|||
# list the OCA project dependencies, one per line |
|||
# add a github url if you need a forked version |
|||
reporting-engine |
|||
server-tools |
|||
web |
Write
Preview
Loading…
Cancel
Save
Reference in new issue