Stéphane Bidoul (ACSONE)
7 years ago
No known key found for this signature in database
GPG Key ID: 866D394B4986F82D
71 changed files with 0 additions and 10645 deletions
-
108mis_builder/CHANGES.rst
-
128mis_builder/README.rst
-
7mis_builder/__init__.py
-
46mis_builder/__manifest__.py
-
18mis_builder/datas/ir_cron.xml
-
1097mis_builder/i18n/es.po
-
1088mis_builder/i18n/fr.po
-
1097mis_builder/i18n/hr_HR.po
-
1097mis_builder/i18n/nl_NL.po
-
1097mis_builder/i18n/pt.po
-
8mis_builder/models/__init__.py
-
191mis_builder/models/accounting_none.py
-
442mis_builder/models/aep.py
-
129mis_builder/models/aggregate.py
-
15mis_builder/models/data_error.py
-
1043mis_builder/models/mis_report.py
-
422mis_builder/models/mis_report_instance.py
-
280mis_builder/models/mis_report_style.py
-
33mis_builder/models/mis_safe_eval.py
-
131mis_builder/models/simple_array.py
-
6mis_builder/report/__init__.py
-
24mis_builder/report/mis_report_instance_qweb.py
-
89mis_builder/report/mis_report_instance_qweb.xml
-
142mis_builder/report/mis_report_instance_xlsx.py
-
15mis_builder/report/mis_report_instance_xlsx.xml
-
17mis_builder/security/ir.model.access.csv
-
13mis_builder/security/mis_builder_security.xml
-
BINmis_builder/static/description/ex_dashboard.png
-
BINmis_builder/static/description/ex_report.png
-
BINmis_builder/static/description/ex_report_template.png
-
BINmis_builder/static/description/icon.png
-
79mis_builder/static/description/icon.svg
-
75mis_builder/static/description/index.html
-
30mis_builder/static/src/css/custom.css
-
45mis_builder/static/src/css/report.css
-
BINmis_builder/static/src/img/icon.png
-
173mis_builder/static/src/js/mis_builder.js
-
57mis_builder/static/src/xml/mis_widget.xml
-
13mis_builder/tests/__init__.py
-
2mis_builder/tests/mis.report.csv
-
2mis_builder/tests/mis.report.instance.csv
-
2mis_builder/tests/mis.report.instance.period.csv
-
2mis_builder/tests/mis.report.kpi.csv
-
2mis_builder/tests/mis.report.query.csv
-
12mis_builder/tests/test_accounting_none.py
-
226mis_builder/tests/test_aep.py
-
12mis_builder/tests/test_aggregate.py
-
40mis_builder/tests/test_fetch_query.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
-
172mis_builder/views/mis_report.xml
-
213mis_builder/views/mis_report_instance.xml
-
80mis_builder/views/mis_report_style.xml
-
5mis_builder/wizard/__init__.py
-
66mis_builder/wizard/mis_builder_dashboard.py
-
33mis_builder/wizard/mis_builder_dashboard.xml
-
46mis_builder_demo/README.rst
-
23mis_builder_demo/__init__.py
-
53mis_builder_demo/__manifest__.py
-
3mis_builder_demo/mis.report.csv
-
3mis_builder_demo/mis.report.instance.csv
-
8mis_builder_demo/mis.report.instance.period.csv
-
10mis_builder_demo/mis.report.kpi.csv
-
3mis_builder_demo/mis.report.query.csv
-
1setup/mis_builder/odoo/__init__.py
-
1setup/mis_builder/odoo/addons/__init__.py
-
1setup/mis_builder/odoo/addons/mis_builder
-
6setup/mis_builder/setup.py
@ -1,108 +0,0 @@ |
|||||
Changelog |
|
||||
--------- |
|
||||
|
|
||||
.. Future (?) |
|
||||
.. ~~~~~~~~~~ |
|
||||
.. |
|
||||
.. * |
|
||||
|
|
||||
10.0.2.0.3 (unreleased) |
|
||||
~~~~~~~~~~~~~~~~~~~~~~~ |
|
||||
|
|
||||
* [IMP] more robust behaviour in presence of missing expressions |
|
||||
* [FIX] indent style |
|
||||
* [FIX] local variable 'ctx' referenced before assignment when generating |
|
||||
reports with no objects |
|
||||
* [IMP] use fontawesome icons |
|
||||
* [MIG] migrate to 10.0 |
|
||||
* [FIX] unicode error when exporting to Excel |
|
||||
* [IMP] provide full access to mis builder style for group Adviser. |
|
||||
|
|
||||
9.0.2.0.2 (2016-09-27) |
|
||||
~~~~~~~~~~~~~~~~~~~~~~ |
|
||||
|
|
||||
* [IMP] Add refresh button in mis report preview. |
|
||||
* [IMP] Widget code changes to allow to add fields in the widget more easily. |
|
||||
|
|
||||
9.0.2.0.1 (2016-05-26) |
|
||||
~~~~~~~~~~~~~~~~~~~~~~ |
|
||||
|
|
||||
* [IMP] remove unused argument in declare_and_compute_period() |
|
||||
for a cleaner API. This is a breaking API changing merged in |
|
||||
urgency before it is used by other modules. |
|
||||
|
|
||||
9.0.2.0.0 (2016-05-24) |
|
||||
~~~~~~~~~~~~~~~~~~~~~~ |
|
||||
|
|
||||
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,128 +0,0 @@ |
|||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|
||||
:target: http://www.gnu.org/licenses/agpl-3.0-standalone.html |
|
||||
:alt: License: AGPL-3 |
|
||||
|
|
||||
=========== |
|
||||
MIS Builder |
|
||||
=========== |
|
||||
|
|
||||
This module allows you to build Management Information Systems dashboards. |
|
||||
Such style of reports presents KPI in rows and time periods in columns. |
|
||||
Reports mainly fetch data from account moves, but can also combine data coming |
|
||||
from arbitrary Odoo models. Reports can be exported to PDF, Excel and they |
|
||||
can be added to Odoo dashboards. |
|
||||
|
|
||||
Installation |
|
||||
============ |
|
||||
|
|
||||
There is no specific installation procedure for this module. |
|
||||
|
|
||||
Configuration and Usage |
|
||||
======================= |
|
||||
|
|
||||
To configure this module, you need to: |
|
||||
|
|
||||
* Go to Accounting > Configuration > Financial Reports > MIS Report Templates where |
|
||||
you can create report templates by defining KPI's. KPI's constitute the rows of your |
|
||||
reports. Such report templates are time independent. |
|
||||
|
|
||||
.. figure:: static/description/ex_report_template.png |
|
||||
:scale: 80 % |
|
||||
:alt: Sample report template |
|
||||
|
|
||||
* Then in Accounting > Reporting > MIS Reports you can create report instance by |
|
||||
binding the templates to time period, hence defining the columns of your reports. |
|
||||
|
|
||||
.. figure:: static/description/ex_report.png |
|
||||
:alt: Sample report configuration |
|
||||
|
|
||||
* From the MIS Report view, you can preview the report, add it to and Odoo dashboard, |
|
||||
and export it to PDF or Excel. |
|
||||
|
|
||||
.. figure:: static/description/ex_dashboard.png |
|
||||
:alt: Sample dashboard view |
|
||||
|
|
||||
.. image:: https://odoo-community.org/website/image/ir.attachment/5784_f2813bd/datas |
|
||||
:alt: Try me on Runbot |
|
||||
:target: https://runbot.odoo-community.org/runbot/91/10.0 |
|
||||
|
|
||||
Developer notes |
|
||||
=============== |
|
||||
|
|
||||
A typical extension is to provide a mechanism to filter reports on analytic dimensions |
|
||||
or operational units. To implement this, you can override _get_additional_move_line_filter |
|
||||
and _get_additional_filter to further filter move lines or queries based on a user |
|
||||
selection. A typical use case could be to add an analytic account field on mis.report.instance, |
|
||||
or even on mis.report.instance.period if you want different columns to show different |
|
||||
analytic accounts. |
|
||||
|
|
||||
Known issues / Roadmap |
|
||||
====================== |
|
||||
|
|
||||
* V9 thoughts: |
|
||||
|
|
||||
* select accounts by tag (see also select accounts by type below) |
|
||||
* how to handle multi-company consolidation now that consolidation children are gone? |
|
||||
* what replaces root accounts / account charts in v9? nothing it seems, so |
|
||||
we are limited to one chart of accounts per company; |
|
||||
* for multi-company consolidation, must we replace the consolidation chart |
|
||||
of account by a list of companies? |
|
||||
|
|
||||
* Allow selecting accounts by type. This is currently possible by expressing |
|
||||
a query such as balp[][('account_id.user_type.code', '=', ...)]. This will work |
|
||||
but would be more efficient if one could write balp[user_type=...], as it would |
|
||||
involve much less queries to the database. |
|
||||
Possible syntax could be balp[code:60%,70%], balp[type:...], balp[tag:...], |
|
||||
with code: being optional and the default. |
|
||||
|
|
||||
* More tests should be added. The first part is creating test data, then it will be |
|
||||
easier. At the minimum, We need the following test data: |
|
||||
|
|
||||
* one account charts with a few normal accounts and view accounts, |
|
||||
* two fiscal years, |
|
||||
* an opening entry in the second fiscal year, |
|
||||
* to test multi-company consolidation, we need a second company with it's own |
|
||||
account chart and two fiscal years, but without opening entry; we also need |
|
||||
a third company which is the parent of the other two and has a consolidation |
|
||||
chart of account. |
|
||||
|
|
||||
Bug Tracker |
|
||||
=========== |
|
||||
|
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_. |
|
||||
In case of trouble, please check there if your issue has already been reported. |
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback |
|
||||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|
||||
|
|
||||
Credits |
|
||||
======= |
|
||||
|
|
||||
Contributors |
|
||||
------------ |
|
||||
|
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu> |
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu> |
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu> |
|
||||
* Alexis de Lattre <alexis.delattre@akretion.com> |
|
||||
* Alexandre Fayolle <alexandre.fayolle@camptocamp.com> |
|
||||
* Jordi Ballester <jordi.ballester@eficent.com> |
|
||||
* Thomas Binsfeld <thomas.binsfeld@gmail.com> |
|
||||
* Giovanni Capalbo <giovanni@therp.nl> |
|
||||
* Marco Calcagni <mcalcagni@dinamicheaziendali.it> |
|
||||
* Sébastien Beau <sebastien.beau@akretion.com> |
|
||||
* Laurent Mignon <laurent.mignon@acsone.eu> |
|
||||
|
|
||||
Maintainer |
|
||||
---------- |
|
||||
|
|
||||
.. image:: https://odoo-community.org/logo.png |
|
||||
:alt: Odoo Community Association |
|
||||
:target: https://odoo-community.org |
|
||||
|
|
||||
This module is maintained by the OCA. |
|
||||
|
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|
||||
mission is to support the collaborative development of Odoo features and |
|
||||
promote its widespread use. |
|
||||
|
|
||||
To contribute to this module, please visit http://odoo-community.org. |
|
@ -1,7 +0,0 @@ |
|||||
# -*- 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 |
|
||||
from . import report |
|
@ -1,46 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
||||
|
|
||||
{ |
|
||||
'name': 'MIS Builder', |
|
||||
'version': '10.0.2.0.2', |
|
||||
'category': 'Reporting', |
|
||||
'summary': """ |
|
||||
Build 'Management Information System' Reports and Dashboards |
|
||||
""", |
|
||||
'author': 'ACSONE SA/NV,' |
|
||||
'Odoo Community Association (OCA)', |
|
||||
'website': 'http://acsone.eu', |
|
||||
'depends': [ |
|
||||
'account', |
|
||||
'board', |
|
||||
'report_xlsx', # OCA/reporting-engine |
|
||||
'date_range', # OCA/server-tools |
|
||||
'web_widget_color', # OCA/web |
|
||||
], |
|
||||
'data': [ |
|
||||
'wizard/mis_builder_dashboard.xml', |
|
||||
'views/mis_report.xml', |
|
||||
'views/mis_report_instance.xml', |
|
||||
'views/mis_report_style.xml', |
|
||||
'datas/ir_cron.xml', |
|
||||
'security/ir.model.access.csv', |
|
||||
'security/mis_builder_security.xml', |
|
||||
'report/mis_report_instance_qweb.xml', |
|
||||
'report/mis_report_instance_xlsx.xml', |
|
||||
], |
|
||||
'demo': [ |
|
||||
'tests/mis.report.kpi.csv', |
|
||||
'tests/mis.report.query.csv', |
|
||||
'tests/mis.report.csv', |
|
||||
'tests/mis.report.instance.period.csv', |
|
||||
'tests/mis.report.instance.csv', |
|
||||
], |
|
||||
'qweb': [ |
|
||||
'static/src/xml/*.xml' |
|
||||
], |
|
||||
'installable': True, |
|
||||
'application': True, |
|
||||
'license': 'AGPL-3', |
|
||||
} |
|
@ -1,18 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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> |
|
||||
</odoo> |
|
1097
mis_builder/i18n/es.po
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1088
mis_builder/i18n/fr.po
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1097
mis_builder/i18n/hr_HR.po
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1097
mis_builder/i18n/nl_NL.po
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
1097
mis_builder/i18n/pt.po
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,8 +0,0 @@ |
|||||
# -*- 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_report |
|
||||
from . import mis_report_instance |
|
||||
from . import mis_report_style |
|
||||
from . import aep |
|
@ -1,191 +0,0 @@ |
|||||
# -*- 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() |
|
@ -1,442 +0,0 @@ |
|||||
# -*- 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 re |
|
||||
from collections import defaultdict |
|
||||
from itertools import izip |
|
||||
|
|
||||
from odoo import fields |
|
||||
from odoo.models import expression |
|
||||
from odoo.tools.safe_eval import safe_eval |
|
||||
from odoo.tools.float_utils import float_is_zero |
|
||||
from .accounting_none import AccountingNone |
|
||||
|
|
||||
|
|
||||
class AccountingExpressionProcessor(object): |
|
||||
""" Processor for accounting expressions. |
|
||||
|
|
||||
Expressions of the form <field><mode>[accounts][optional move line domain] |
|
||||
are supported, where: |
|
||||
* field is bal, crd, deb |
|
||||
* mode is i (initial balance), e (ending balance), |
|
||||
p (moves over period) |
|
||||
* there is also a special u mode (unallocated P&L) which computes |
|
||||
the sum from the beginning until the beginning of the fiscal year |
|
||||
of the period; it is only meaningful for P&L accounts |
|
||||
* accounts is a list of accounts, possibly containing % wildcards |
|
||||
* an optional domain on move lines allowing filters on eg analytic |
|
||||
accounts or journal |
|
||||
|
|
||||
Examples: |
|
||||
* bal[70]: variation of the balance of moves on account 70 |
|
||||
over the period (it is the same as balp[70]); |
|
||||
* bali[70,60]: balance of accounts 70 and 60 at the start of period; |
|
||||
* bale[1%]: balance of accounts starting with 1 at end of period. |
|
||||
|
|
||||
How to use: |
|
||||
* repeatedly invoke parse_expr() for each expression containing |
|
||||
accounting variables as described above; this lets the processor |
|
||||
group domains and modes and accounts; |
|
||||
* when all expressions have been parsed, invoke done_parsing() |
|
||||
to notify the processor that it can prepare to query (mainly |
|
||||
search all accounts - children, consolidation - that will need to |
|
||||
be queried; |
|
||||
* for each period, call do_queries(), then call replace_expr() for each |
|
||||
expression to replace accounting variables with their resulting value |
|
||||
for the given period. |
|
||||
|
|
||||
How it works: |
|
||||
* by accumulating the expressions before hand, it ensures to do the |
|
||||
strict minimum number of queries to the database (for each period, |
|
||||
one query per domain and mode); |
|
||||
* it queries using the orm read_group which reduces to a query with |
|
||||
sum on debit and credit and group by on account_id (note: it seems |
|
||||
the orm then does one query per account to fetch the account |
|
||||
name...); |
|
||||
* additionally, one query per view/consolidation account is done to |
|
||||
discover the children accounts. |
|
||||
""" |
|
||||
MODE_VARIATION = 'p' |
|
||||
MODE_INITIAL = 'i' |
|
||||
MODE_END = 'e' |
|
||||
MODE_UNALLOCATED = 'u' |
|
||||
|
|
||||
_ACC_RE = re.compile(r"(?P<field>\bbal|\bcrd|\bdeb)" |
|
||||
r"(?P<mode>[piseu])?" |
|
||||
r"(?P<accounts>_[a-zA-Z0-9]+|\[.*?\])" |
|
||||
r"(?P<domain>\[.*?\])?") |
|
||||
|
|
||||
def __init__(self, company): |
|
||||
self.company = company |
|
||||
self.dp = company.currency_id.decimal_places |
|
||||
# before done_parsing: {(domain, mode): set(account_codes)} |
|
||||
# after done_parsing: {(domain, mode): list(account_ids)} |
|
||||
self._map_account_ids = defaultdict(set) |
|
||||
# {account_code: account_id} where account_code can be |
|
||||
# - None for all accounts |
|
||||
# - NNN% for a like |
|
||||
# - NNN for a code with an exact match |
|
||||
self._account_ids_by_code = defaultdict(set) |
|
||||
# smart ending balance (returns AccountingNone if there |
|
||||
# are no moves in period and 0 initial balance), implies |
|
||||
# a first query to get the initial balance and another |
|
||||
# to get the variation, so it's a bit slower |
|
||||
self.smart_end = True |
|
||||
|
|
||||
def _load_account_codes(self, account_codes): |
|
||||
account_model = self.company.env['account.account'] |
|
||||
exact_codes = set() |
|
||||
for account_code in account_codes: |
|
||||
if account_code in self._account_ids_by_code: |
|
||||
continue |
|
||||
if account_code is None: |
|
||||
# None means we want all accounts |
|
||||
account_ids = account_model.\ |
|
||||
search([('company_id', '=', self.company.id)]).ids |
|
||||
self._account_ids_by_code[account_code].update(account_ids) |
|
||||
elif '%' in account_code: |
|
||||
account_ids = account_model.\ |
|
||||
search([('code', '=like', account_code), |
|
||||
('company_id', '=', self.company.id)]).ids |
|
||||
self._account_ids_by_code[account_code].update(account_ids) |
|
||||
else: |
|
||||
# search exact codes after the loop to do less queries |
|
||||
exact_codes.add(account_code) |
|
||||
for account in account_model.\ |
|
||||
search([('code', 'in', list(exact_codes)), |
|
||||
('company_id', '=', self.company.id)]): |
|
||||
self._account_ids_by_code[account.code].add(account.id) |
|
||||
|
|
||||
def _parse_match_object(self, mo): |
|
||||
"""Split a match object corresponding to an accounting variable |
|
||||
|
|
||||
Returns field, mode, [account codes], (domain expression). |
|
||||
""" |
|
||||
field, mode, account_codes, domain = mo.groups() |
|
||||
if not mode: |
|
||||
mode = self.MODE_VARIATION |
|
||||
elif mode == 's': |
|
||||
mode = self.MODE_END |
|
||||
if account_codes.startswith('_'): |
|
||||
account_codes = account_codes[1:] |
|
||||
else: |
|
||||
account_codes = account_codes[1:-1] |
|
||||
if account_codes.strip(): |
|
||||
account_codes = [a.strip() for a in account_codes.split(',')] |
|
||||
else: |
|
||||
account_codes = [None] # None means we want all accounts |
|
||||
domain = domain or '[]' |
|
||||
domain = tuple(safe_eval(domain)) |
|
||||
return field, mode, account_codes, domain |
|
||||
|
|
||||
def parse_expr(self, expr): |
|
||||
"""Parse an expression, extracting accounting variables. |
|
||||
|
|
||||
Domains and accounts are extracted and stored in the map |
|
||||
so when all expressions have been parsed, we know which |
|
||||
account codes to query for each domain and mode. |
|
||||
""" |
|
||||
for mo in self._ACC_RE.finditer(expr): |
|
||||
_, mode, account_codes, domain = self._parse_match_object(mo) |
|
||||
if mode == self.MODE_END and self.smart_end: |
|
||||
modes = (self.MODE_INITIAL, self.MODE_VARIATION, self.MODE_END) |
|
||||
else: |
|
||||
modes = (mode, ) |
|
||||
for mode in modes: |
|
||||
key = (domain, mode) |
|
||||
self._map_account_ids[key].update(account_codes) |
|
||||
|
|
||||
def done_parsing(self): |
|
||||
"""Load account codes and replace account codes by |
|
||||
account ids in map.""" |
|
||||
for key, account_codes in self._map_account_ids.items(): |
|
||||
# TODO _load_account_codes could be done |
|
||||
# for all account_codes at once (also in v8) |
|
||||
self._load_account_codes(account_codes) |
|
||||
account_ids = set() |
|
||||
for account_code in account_codes: |
|
||||
account_ids.update(self._account_ids_by_code[account_code]) |
|
||||
self._map_account_ids[key] = list(account_ids) |
|
||||
|
|
||||
@classmethod |
|
||||
def has_account_var(cls, expr): |
|
||||
"""Test if an string contains an accounting variable.""" |
|
||||
return bool(cls._ACC_RE.search(expr)) |
|
||||
|
|
||||
def get_aml_domain_for_expr(self, expr, |
|
||||
date_from, date_to, |
|
||||
target_move, |
|
||||
account_id=None): |
|
||||
""" Get a domain on account.move.line for an expression. |
|
||||
|
|
||||
Prerequisite: done_parsing() must have been invoked. |
|
||||
|
|
||||
Returns a domain that can be used to search on account.move.line. |
|
||||
""" |
|
||||
aml_domains = [] |
|
||||
date_domain_by_mode = {} |
|
||||
for mo in self._ACC_RE.finditer(expr): |
|
||||
field, mode, account_codes, domain = self._parse_match_object(mo) |
|
||||
aml_domain = list(domain) |
|
||||
account_ids = set() |
|
||||
for account_code in account_codes: |
|
||||
account_ids.update(self._account_ids_by_code[account_code]) |
|
||||
if not account_id: |
|
||||
aml_domain.append(('account_id', 'in', tuple(account_ids))) |
|
||||
else: |
|
||||
# filter on account_id |
|
||||
if account_id in account_ids: |
|
||||
aml_domain.append(('account_id', '=', account_id)) |
|
||||
else: |
|
||||
continue |
|
||||
if field == 'crd': |
|
||||
aml_domain.append(('credit', '>', 0)) |
|
||||
elif field == 'deb': |
|
||||
aml_domain.append(('debit', '>', 0)) |
|
||||
aml_domains.append(expression.normalize_domain(aml_domain)) |
|
||||
if mode not in date_domain_by_mode: |
|
||||
date_domain_by_mode[mode] = \ |
|
||||
self.get_aml_domain_for_dates(date_from, date_to, |
|
||||
mode, target_move) |
|
||||
assert aml_domains |
|
||||
return expression.OR(aml_domains) + \ |
|
||||
expression.OR(date_domain_by_mode.values()) |
|
||||
|
|
||||
def get_aml_domain_for_dates(self, date_from, date_to, |
|
||||
mode, |
|
||||
target_move): |
|
||||
if mode == self.MODE_VARIATION: |
|
||||
domain = [('date', '>=', date_from), ('date', '<=', date_to)] |
|
||||
elif mode in (self.MODE_INITIAL, self.MODE_END): |
|
||||
# for income and expense account, sum from the beginning |
|
||||
# of the current fiscal year only, for balance sheet accounts |
|
||||
# sum from the beginning of time |
|
||||
date_from_date = fields.Date.from_string(date_from) |
|
||||
fy_date_from = \ |
|
||||
self.company.\ |
|
||||
compute_fiscalyear_dates(date_from_date)['date_from'] |
|
||||
domain = ['|', |
|
||||
('date', '>=', fields.Date.to_string(fy_date_from)), |
|
||||
('user_type_id.include_initial_balance', '=', True)] |
|
||||
if mode == self.MODE_INITIAL: |
|
||||
domain.append(('date', '<', date_from)) |
|
||||
elif mode == self.MODE_END: |
|
||||
domain.append(('date', '<=', date_to)) |
|
||||
elif mode == self.MODE_UNALLOCATED: |
|
||||
date_from_date = fields.Date.from_string(date_from) |
|
||||
fy_date_from = \ |
|
||||
self.company.\ |
|
||||
compute_fiscalyear_dates(date_from_date)['date_from'] |
|
||||
domain = [('date', '<', fields.Date.to_string(fy_date_from)), |
|
||||
('user_type_id.include_initial_balance', '=', False)] |
|
||||
if target_move == 'posted': |
|
||||
domain.append(('move_id.state', '=', 'posted')) |
|
||||
return expression.normalize_domain(domain) |
|
||||
|
|
||||
def do_queries(self, date_from, date_to, |
|
||||
target_move='posted', additional_move_line_filter=None): |
|
||||
"""Query sums of debit and credit for all accounts and domains |
|
||||
used in expressions. |
|
||||
|
|
||||
This method must be executed after done_parsing(). |
|
||||
""" |
|
||||
aml_model = self.company.env['account.move.line'] |
|
||||
# {(domain, mode): {account_id: (debit, credit)}} |
|
||||
self._data = defaultdict(dict) |
|
||||
domain_by_mode = {} |
|
||||
ends = [] |
|
||||
for key in self._map_account_ids: |
|
||||
domain, mode = key |
|
||||
if mode == self.MODE_END and self.smart_end: |
|
||||
# postpone computation of ending balance |
|
||||
ends.append((domain, mode)) |
|
||||
continue |
|
||||
if mode not in domain_by_mode: |
|
||||
domain_by_mode[mode] = \ |
|
||||
self.get_aml_domain_for_dates(date_from, date_to, |
|
||||
mode, target_move) |
|
||||
domain = list(domain) + domain_by_mode[mode] |
|
||||
domain.append(('account_id', 'in', self._map_account_ids[key])) |
|
||||
if additional_move_line_filter: |
|
||||
domain.extend(additional_move_line_filter) |
|
||||
# fetch sum of debit/credit, grouped by account_id |
|
||||
accs = aml_model.read_group(domain, |
|
||||
['debit', 'credit', 'account_id'], |
|
||||
['account_id']) |
|
||||
for acc in accs: |
|
||||
debit = acc['debit'] or 0.0 |
|
||||
credit = acc['credit'] or 0.0 |
|
||||
if mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ |
|
||||
float_is_zero(debit-credit, |
|
||||
precision_rounding=self.dp): |
|
||||
# in initial mode, ignore accounts with 0 balance |
|
||||
continue |
|
||||
self._data[key][acc['account_id'][0]] = (debit, credit) |
|
||||
# compute ending balances by summing initial and variation |
|
||||
for key in ends: |
|
||||
domain, mode = key |
|
||||
initial_data = self._data[(domain, self.MODE_INITIAL)] |
|
||||
variation_data = self._data[(domain, self.MODE_VARIATION)] |
|
||||
account_ids = set(initial_data.keys()) | set(variation_data.keys()) |
|
||||
for account_id in account_ids: |
|
||||
di, ci = initial_data.get(account_id, |
|
||||
(AccountingNone, AccountingNone)) |
|
||||
dv, cv = variation_data.get(account_id, |
|
||||
(AccountingNone, AccountingNone)) |
|
||||
self._data[key][account_id] = (di + dv, ci + cv) |
|
||||
|
|
||||
def replace_expr(self, expr): |
|
||||
"""Replace accounting variables in an expression by their amount. |
|
||||
|
|
||||
Returns a new expression string. |
|
||||
|
|
||||
This method must be executed after do_queries(). |
|
||||
""" |
|
||||
def f(mo): |
|
||||
field, mode, account_codes, domain = self._parse_match_object(mo) |
|
||||
key = (domain, mode) |
|
||||
account_ids_data = self._data[key] |
|
||||
v = AccountingNone |
|
||||
for account_code in account_codes: |
|
||||
account_ids = self._account_ids_by_code[account_code] |
|
||||
for account_id in account_ids: |
|
||||
debit, credit = \ |
|
||||
account_ids_data.get(account_id, |
|
||||
(AccountingNone, AccountingNone)) |
|
||||
if field == 'bal': |
|
||||
v += debit - credit |
|
||||
elif field == 'deb': |
|
||||
v += debit |
|
||||
elif field == 'crd': |
|
||||
v += credit |
|
||||
# in initial balance mode, assume 0 is None |
|
||||
# as it does not make sense to distinguish 0 from "no data" |
|
||||
if v is not AccountingNone and \ |
|
||||
mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ |
|
||||
float_is_zero(v, precision_rounding=self.dp): |
|
||||
v = AccountingNone |
|
||||
return '(' + repr(v) + ')' |
|
||||
|
|
||||
return self._ACC_RE.sub(f, expr) |
|
||||
|
|
||||
def replace_exprs_by_account_id(self, exprs): |
|
||||
"""Replace accounting variables in a list of expression |
|
||||
by their amount, iterating by accounts involved in the expression. |
|
||||
|
|
||||
yields account_id, replaced_expr |
|
||||
|
|
||||
This method must be executed after do_queries(). |
|
||||
""" |
|
||||
def f(mo): |
|
||||
field, mode, account_codes, domain = self._parse_match_object(mo) |
|
||||
key = (domain, mode) |
|
||||
account_ids_data = self._data[key] |
|
||||
debit, credit = \ |
|
||||
account_ids_data.get(account_id, |
|
||||
(AccountingNone, AccountingNone)) |
|
||||
if field == 'bal': |
|
||||
v = debit - credit |
|
||||
elif field == 'deb': |
|
||||
v = debit |
|
||||
elif field == 'crd': |
|
||||
v = credit |
|
||||
# in initial balance mode, assume 0 is None |
|
||||
# as it does not make sense to distinguish 0 from "no data" |
|
||||
if v is not AccountingNone and \ |
|
||||
mode in (self.MODE_INITIAL, self.MODE_UNALLOCATED) and \ |
|
||||
float_is_zero(v, precision_rounding=self.dp): |
|
||||
v = AccountingNone |
|
||||
return '(' + repr(v) + ')' |
|
||||
|
|
||||
account_ids = set() |
|
||||
for expr in exprs: |
|
||||
for mo in self._ACC_RE.finditer(expr): |
|
||||
field, mode, account_codes, domain = \ |
|
||||
self._parse_match_object(mo) |
|
||||
key = (domain, mode) |
|
||||
account_ids_data = self._data[key] |
|
||||
for account_code in account_codes: |
|
||||
for account_id in self._account_ids_by_code[account_code]: |
|
||||
if account_id in account_ids_data: |
|
||||
account_ids.add(account_id) |
|
||||
|
|
||||
for account_id in account_ids: |
|
||||
yield account_id, [self._ACC_RE.sub(f, expr) for expr in exprs] |
|
||||
|
|
||||
@classmethod |
|
||||
def _get_balances(cls, mode, company, date_from, date_to, |
|
||||
target_move='posted'): |
|
||||
expr = 'deb{mode}[], crd{mode}[]'.format(mode=mode) |
|
||||
aep = AccountingExpressionProcessor(company) |
|
||||
# disable smart_end to have the data at once, instead |
|
||||
# of initial + variation |
|
||||
aep.smart_end = False |
|
||||
aep.parse_expr(expr) |
|
||||
aep.done_parsing() |
|
||||
aep.do_queries(date_from, date_to, target_move) |
|
||||
return aep._data[((), mode)] |
|
||||
|
|
||||
@classmethod |
|
||||
def get_balances_initial(cls, company, date, target_move='posted'): |
|
||||
""" A convenience method to obtain the initial balances of all accounts |
|
||||
at a given date. |
|
||||
|
|
||||
It is the same as get_balances_end(date-1). |
|
||||
|
|
||||
:param company: |
|
||||
:param date: |
|
||||
:param target_move: if 'posted', consider only posted moves |
|
||||
|
|
||||
Returns a dictionary: {account_id, (debit, credit)} |
|
||||
""" |
|
||||
return cls._get_balances(cls.MODE_INITIAL, company, |
|
||||
date, date, target_move) |
|
||||
|
|
||||
@classmethod |
|
||||
def get_balances_end(cls, company, date, target_move='posted'): |
|
||||
""" A convenience method to obtain the ending balances of all accounts |
|
||||
at a given date. |
|
||||
|
|
||||
It is the same as get_balances_initial(date+1). |
|
||||
|
|
||||
:param company: |
|
||||
:param date: |
|
||||
:param target_move: if 'posted', consider only posted moves |
|
||||
|
|
||||
Returns a dictionary: {account_id, (debit, credit)} |
|
||||
""" |
|
||||
return cls._get_balances(cls.MODE_END, company, |
|
||||
date, date, target_move) |
|
||||
|
|
||||
@classmethod |
|
||||
def get_balances_variation(cls, company, date_from, date_to, |
|
||||
target_move='posted'): |
|
||||
""" A convenience method to obtain the variation of the |
|
||||
balances of all accounts over a period. |
|
||||
|
|
||||
:param company: |
|
||||
:param date: |
|
||||
:param target_move: if 'posted', consider only posted moves |
|
||||
|
|
||||
Returns a dictionary: {account_id, (debit, credit)} |
|
||||
""" |
|
||||
return cls._get_balances(cls.MODE_VARIATION, company, |
|
||||
date_from, date_to, target_move) |
|
||||
|
|
||||
@classmethod |
|
||||
def get_unallocated_pl(cls, company, date, target_move='posted'): |
|
||||
""" A convenience method to obtain the unallocated profit/loss |
|
||||
of the previous fiscal years at a given date. |
|
||||
|
|
||||
:param company: |
|
||||
:param date: |
|
||||
:param target_move: if 'posted', consider only posted moves |
|
||||
|
|
||||
Returns a tuple (debit, credit) |
|
||||
""" |
|
||||
# TODO shoud we include here the accounts of type "unaffected" |
|
||||
# or leave that to the caller? |
|
||||
bals = cls._get_balances(cls.MODE_UNALLOCATED, company, |
|
||||
date, date, target_move) |
|
||||
return tuple(map(sum, izip(*bals.values()))) |
|
@ -1,129 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
||||
|
|
||||
|
|
||||
def _sum(l): |
|
||||
""" Same as stdlib sum but returns None instead of 0 |
|
||||
in case of empty sequence. |
|
||||
|
|
||||
>>> sum([1]) |
|
||||
1 |
|
||||
>>> _sum([1]) |
|
||||
1 |
|
||||
>>> sum([1, 2]) |
|
||||
3 |
|
||||
>>> _sum([1, 2]) |
|
||||
3 |
|
||||
>>> sum([]) |
|
||||
0 |
|
||||
>>> _sum([]) |
|
||||
""" |
|
||||
if not l: |
|
||||
return None |
|
||||
return sum(l) |
|
||||
|
|
||||
|
|
||||
def _avg(l): |
|
||||
""" Arithmetic mean of a sequence. Returns None in case of empty sequence. |
|
||||
|
|
||||
>>> _avg([1]) |
|
||||
1.0 |
|
||||
>>> _avg([1, 2]) |
|
||||
1.5 |
|
||||
>>> _avg([]) |
|
||||
""" |
|
||||
if not l: |
|
||||
return None |
|
||||
return sum(l) / float(len(l)) |
|
||||
|
|
||||
|
|
||||
def _min(*args): |
|
||||
""" Same as stdlib min but returns None instead of exception |
|
||||
in case of empty sequence. |
|
||||
|
|
||||
>>> min(1, 2) |
|
||||
1 |
|
||||
>>> _min(1, 2) |
|
||||
1 |
|
||||
>>> min([1, 2]) |
|
||||
1 |
|
||||
>>> _min([1, 2]) |
|
||||
1 |
|
||||
>>> min(1) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: 'int' object is not iterable |
|
||||
>>> _min(1) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: 'int' object is not iterable |
|
||||
>>> min([1]) |
|
||||
1 |
|
||||
>>> _min([1]) |
|
||||
1 |
|
||||
>>> min() |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: min expected 1 arguments, got 0 |
|
||||
>>> _min() |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: min expected 1 arguments, got 0 |
|
||||
>>> min([]) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
ValueError: min() arg is an empty sequence |
|
||||
>>> _min([]) |
|
||||
""" |
|
||||
if len(args) == 1 and not args[0]: |
|
||||
return None |
|
||||
return min(*args) |
|
||||
|
|
||||
|
|
||||
def _max(*args): |
|
||||
""" Same as stdlib max but returns None instead of exception |
|
||||
in case of empty sequence. |
|
||||
|
|
||||
>>> max(1, 2) |
|
||||
2 |
|
||||
>>> _max(1, 2) |
|
||||
2 |
|
||||
>>> max([1, 2]) |
|
||||
2 |
|
||||
>>> _max([1, 2]) |
|
||||
2 |
|
||||
>>> max(1) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: 'int' object is not iterable |
|
||||
>>> _max(1) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: 'int' object is not iterable |
|
||||
>>> max([1]) |
|
||||
1 |
|
||||
>>> _max([1]) |
|
||||
1 |
|
||||
>>> max() |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: max expected 1 arguments, got 0 |
|
||||
>>> _max() |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
TypeError: max expected 1 arguments, got 0 |
|
||||
>>> max([]) |
|
||||
Traceback (most recent call last): |
|
||||
File "<stdin>", line 1, in ? |
|
||||
ValueError: max() arg is an empty sequence |
|
||||
>>> _max([]) |
|
||||
""" |
|
||||
if len(args) == 1 and not args[0]: |
|
||||
return None |
|
||||
return max(*args) |
|
||||
|
|
||||
|
|
||||
if __name__ == "__main__": |
|
||||
import doctest |
|
||||
doctest.testmod() |
|
@ -1,15 +0,0 @@ |
|||||
# -*- 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 |
|
1043
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
@ -1,422 +0,0 @@ |
|||||
# -*- 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 odoo 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', |
|
||||
'date_range_type_id', |
|
||||
'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', 'Fixed dates'), |
|
||||
('relative', 'Relative to report base date'), |
|
||||
], 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 = lang_model._lang_get(self.env.user.lang) |
|
||||
date_format = lang.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, |
|
||||
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 |
|
@ -1,280 +0,0 @@ |
|||||
# -*- 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 odoo import api, fields, models, _ |
|
||||
from odoo.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, no_indent=False): |
|
||||
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), |
|
||||
('num_format', num_format), |
|
||||
] |
|
||||
if props.indent_level is not None and not no_indent: |
|
||||
xlsx_attributes.append( |
|
||||
('indent', props.indent_level)) |
|
||||
return dict([a for a in xlsx_attributes |
|
||||
if a[1] is not None]) |
|
||||
|
|
||||
@api.model |
|
||||
def to_css_style(self, props, no_indent=False): |
|
||||
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), |
|
||||
] |
|
||||
if props.indent_level is not None and not no_indent: |
|
||||
css_attributes.append( |
|
||||
('text-indent', '{}em'.format(props.indent_level))) |
|
||||
return '; '.join(['%s: %s' % a for a in css_attributes |
|
||||
if a[1] is not None]) or None |
|
@ -1,33 +0,0 @@ |
|||||
# -*- 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 odoo.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 |
|
@ -1,131 +0,0 @@ |
|||||
# -*- 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,6 +0,0 @@ |
|||||
# -*- 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_report_instance_qweb |
|
||||
from . import mis_report_instance_xlsx |
|
@ -1,24 +0,0 @@ |
|||||
# -*- 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 odoo import api, models |
|
||||
|
|
||||
_logger = logging.getLogger(__name__) |
|
||||
|
|
||||
|
|
||||
class Report(models.Model): |
|
||||
_inherit = "report" |
|
||||
|
|
||||
@api.model |
|
||||
def get_pdf(self, docids, report_name, html=None, data=None): |
|
||||
ctx = self.env.context.copy() |
|
||||
if docids: |
|
||||
report = self._get_report_from_name(report_name) |
|
||||
obj = self.env[report.model].browse(docids)[0] |
|
||||
if hasattr(obj, 'landscape_pdf') and obj.landscape_pdf: |
|
||||
ctx.update({'landscape': True}) |
|
||||
return super(Report, self.with_context(ctx)).get_pdf( |
|
||||
docids, report_name, html=html, data=data) |
|
@ -1,89 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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> |
|
||||
</odoo> |
|
@ -1,142 +0,0 @@ |
|||||
# -*- 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 odoo.report import report_sxw |
|
||||
|
|
||||
from ..models.accounting_none import AccountingNone |
|
||||
from ..models.data_error import DataError |
|
||||
|
|
||||
_logger = logging.getLogger(__name__) |
|
||||
|
|
||||
try: |
|
||||
from odoo.addons.report_xlsx.report.report_xlsx import ReportXlsx |
|
||||
except ImportError: |
|
||||
_logger.debug("report_xlsx not installed, Excel export non functional") |
|
||||
|
|
||||
class ReportXlsx(object): |
|
||||
def __init__(self, *args, **kwargs): |
|
||||
pass |
|
||||
|
|
||||
|
|
||||
ROW_HEIGHT = 15 # xlsxwriter units |
|
||||
COL_WIDTH = 0.9 # xlsxwriter units |
|
||||
MIN_COL_WIDTH = 10 # characters |
|
||||
MAX_COL_WIDTH = 50 # characters |
|
||||
|
|
||||
|
|
||||
class MisBuilderXlsx(ReportXlsx): |
|
||||
|
|
||||
def __init__(self, name, table, rml=False, parser=False, header=True, |
|
||||
store=False): |
|
||||
super(MisBuilderXlsx, 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 = u'{} - {}'.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, no_indent=True) |
|
||||
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) |
|
||||
|
|
||||
|
|
||||
MisBuilderXlsx('report.mis.report.instance.xlsx', |
|
||||
'mis.report.instance', parser=report_sxw.rml_parse) |
|
@ -1,15 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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> |
|
||||
</odoo> |
|
@ -1,17 +0,0 @@ |
|||||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink" |
|
||||
manage_mis_report_kpi,manage_mis_report_kpi,model_mis_report_kpi,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_kpi,access_mis_report_kpi,model_mis_report_kpi,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_query,manage_mis_report_query,model_mis_report_query,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_query,access_mis_report_query,model_mis_report_query,base.group_user,1,0,0,0 |
|
||||
manage_mis_report,manage_mis_report,model_mis_report,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report,access_mis_report,model_mis_report,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_instance_period,manage_mis_report_instance_period,model_mis_report_instance_period,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_instance_period,access_mis_report_instance_period,model_mis_report_instance_period,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_instance,manage_mis_report_instance,model_mis_report_instance,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_instance,access_mis_report_instance,model_mis_report_instance,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_subkpi,access_mis_report_subkpi,model_mis_report_subkpi,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_kpi_expression,access_mis_report_kpi_expression,model_mis_report_kpi_expression,base.group_user,1,0,0,0 |
|
||||
manage_mis_report_style,access_mis_report_style,model_mis_report_style,account.group_account_manager,1,1,1,1 |
|
||||
access_mis_report_style,access_mis_report_style,model_mis_report_style,base.group_user,1,0,0,0 |
|
@ -1,13 +0,0 @@ |
|||||
<?xml version="1.0" encoding="utf-8"?> |
|
||||
<odoo> |
|
||||
<data noupdate="0"> |
|
||||
|
|
||||
<record id="mis_builder_multi_company_rule" model="ir.rule"> |
|
||||
<field name="name">Mis Builder multi company</field> |
|
||||
<field name="model_id" ref="model_mis_report_instance"/> |
|
||||
<field name="global" eval="True"/> |
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field> |
|
||||
</record> |
|
||||
|
|
||||
</data> |
|
||||
</odoo> |
|
Before Width: 749 | Height: 391 | Size: 34 KiB |
Before Width: 1111 | Height: 560 | Size: 81 KiB |
Before Width: 1174 | Height: 654 | Size: 98 KiB |
Before Width: 128 | Height: 128 | Size: 9.2 KiB |
79
mis_builder/static/description/icon.svg
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
@ -1,75 +0,0 @@ |
|||||
<section id="addons-mis-builder"> |
|
||||
<img src="https://img.shields.io/badge/licence-AGPL--3-blue.svg" alt="License: AGPL-3" /> |
|
||||
<section id="mis-builder"> |
|
||||
<h1>MIS Builder</h1> |
|
||||
<p>This module allows you to build Management Information Systems dashboards. Such style of reports presents KPI in rows and time periods in columns. Reports mainly fetch data from account moves, but can also combine data coming from arbitrary Odoo models. Reports can be exported to PDF, Excel and they can be added to Odoo dashboards.</p> |
|
||||
<section id="installation"> |
|
||||
<h2>Installation</h2> |
|
||||
<p>There is no specific installation procedure for this module.</p> |
|
||||
</section> |
|
||||
<section id="configuration-and-usage"> |
|
||||
<h2>Configuration and Usage</h2> |
|
||||
<p>To configure this module, you need to:</p> |
|
||||
<ul> |
|
||||
<li>Go to Accounting > Configuration > Financial Reports > MIS Report Templates where you can create report templates by defining KPI's. KPI's constitute the rows of your reports. Such report templates are time independent.</li> |
|
||||
</ul> |
|
||||
<figure> |
|
||||
<img src="ex_report_template.png" alt="Sample report template" scale="80" /> |
|
||||
</figure> |
|
||||
<ul> |
|
||||
<li>Then in Accounting > Reporting > MIS Reports you can create report instance by binding the templates to time period, hence defining the columns of your reports.</li> |
|
||||
</ul> |
|
||||
<figure> |
|
||||
<img src="ex_report.png" alt="Sample report configuration" /> |
|
||||
</figure> |
|
||||
<ul> |
|
||||
<li>From the MIS Report view, you can preview the report, add it to and Odoo dashboard, and export it to PDF or Excel.</li> |
|
||||
</ul> |
|
||||
<figure> |
|
||||
<img src="ex_dashboard.png" alt="Sample dashboard view" /> |
|
||||
</figure> |
|
||||
<p>For further information, please visit:</p> |
|
||||
<ul> |
|
||||
<li><a href="https://www.odoo.com/forum/help-1">https://www.odoo.com/forum/help-1</a></li> |
|
||||
</ul> |
|
||||
</section> |
|
||||
<section id="known-issues-roadmap"> |
|
||||
<h2>Known issues / Roadmap</h2> |
|
||||
<ul> |
|
||||
<li>More tests should be added. The first part is creating test data, then it will be easier. At the minimum, We need the following test data: |
|
||||
<ul> |
|
||||
<li>one account charts with a few normal accounts and view accounts,</li> |
|
||||
<li>two fiscal years,</li> |
|
||||
<li>an opening entry in the second fiscal year,</li> |
|
||||
<li>to test multi-company consolidation, we need a second company with it's own account chart and two fiscal years, but without opening entry; we also need a third company which is the parent of the other two and has a consolidation chart of account.</li> |
|
||||
</ul> |
|
||||
</li> |
|
||||
</ul> |
|
||||
</section> |
|
||||
<section id="bug-tracker"> |
|
||||
<h2>Bug Tracker</h2> |
|
||||
<p>Bugs are tracked on <a href="https://github.com/OCA/account-financial-reporting/issues">GitHub Issues</a>. In case of trouble, please check there if your issue has already been reported. If you spotted it first, help us smashing it by providing a detailed and welcomed feedback <a href="https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**">here</a>.</p> |
|
||||
</section> |
|
||||
<section id="credits"> |
|
||||
<h2>Credits</h2> |
|
||||
<section id="contributors"> |
|
||||
<h3>Contributors</h3> |
|
||||
<ul> |
|
||||
<li>Stéphane Bidoul <<a href="mailto:stephane.bidoul@acsone.eu">stephane.bidoul@acsone.eu</a>></li> |
|
||||
<li>Laetitia Gangloff <<a href="mailto:laetitia.gangloff@acsone.eu">laetitia.gangloff@acsone.eu</a>></li> |
|
||||
<li>Adrien Peiffer <<a href="mailto:adrien.peiffer@acsone.eu">adrien.peiffer@acsone.eu</a>></li> |
|
||||
</ul> |
|
||||
</section> |
|
||||
<section id="maintainer"> |
|
||||
<h3>Maintainer</h3> |
|
||||
<a href="https://odoo-community.org"> |
|
||||
<img src="https://odoo-community.org/logo.png" alt="Odoo Community Association" /> |
|
||||
</a> |
|
||||
<p>This module is maintained by the OCA.</p> |
|
||||
<p>OCA, or the Odoo Community Association, is a nonprofit organization whose mission is to support the collaborative development of Odoo features and promote its widespread use.</p> |
|
||||
<p>To contribute to this module, please visit <a href="http://odoo-community.org">http://odoo-community.org</a>.</p> |
|
||||
</section> |
|
||||
</section> |
|
||||
</section> |
|
||||
</section> |
|
||||
|
|
@ -1,30 +0,0 @@ |
|||||
.o_web_client .mis_builder_amount { |
|
||||
text-align: right; |
|
||||
} |
|
||||
|
|
||||
.o_web_client .mis_builder_collabel { |
|
||||
text-align: center; |
|
||||
} |
|
||||
|
|
||||
.o_web_client .mis_builder_rowlabel { |
|
||||
text-align: left; |
|
||||
} |
|
||||
|
|
||||
.o_web_client .mis_builder a { |
|
||||
/* we don't want the link color, to respect user styles */ |
|
||||
color: inherit; |
|
||||
} |
|
||||
|
|
||||
.o_web_client .mis_builder a:hover { |
|
||||
/* underline links on hover to give a visual cue */ |
|
||||
text-decoration: underline; |
|
||||
} |
|
||||
|
|
||||
.odoo .oe_mis_builder_buttons { |
|
||||
padding-bottom: 10px; |
|
||||
padding-top: 10px; |
|
||||
} |
|
||||
|
|
||||
.oe_mis_builder_content { |
|
||||
padding: 10px; |
|
||||
} |
|
@ -1,45 +0,0 @@ |
|||||
.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; |
|
||||
} |
|
Before Width: 64 | Height: 64 | Size: 3.4 KiB |
@ -1,173 +0,0 @@ |
|||||
odoo.define('mis.builder', function (require) { |
|
||||
"use strict"; |
|
||||
|
|
||||
var core = require('web.core'); |
|
||||
var form_common = require('web.form_common'); |
|
||||
var Model = require('web.DataModel'); |
|
||||
var data = require('web.data'); |
|
||||
var ActionManager = require('web.ActionManager'); |
|
||||
|
|
||||
var MisReport = form_common.FormWidget.extend({ |
|
||||
/** |
|
||||
* @constructs instance.mis_builder.MisReport |
|
||||
* @extends instance.web.form.FormWidget |
|
||||
* |
|
||||
*/ |
|
||||
template: "mis_builder.MisReport", |
|
||||
init: function() { |
|
||||
this._super.apply(this, arguments); |
|
||||
this.mis_report_data = null; |
|
||||
this.mis_report_instance_id = false; |
|
||||
this.field_manager.on("view_content_has_changed", this, this.reload_widget); |
|
||||
}, |
|
||||
|
|
||||
initialize_field: function() { |
|
||||
var self = this; |
|
||||
self.destroy_content(); |
|
||||
self.init_fields(); |
|
||||
}, |
|
||||
|
|
||||
init_fields: function() { |
|
||||
var self = this; |
|
||||
if (self.dfm) |
|
||||
return; |
|
||||
self.dfm = new form_common.DefaultFieldManager(self); |
|
||||
self.$(".oe_mis_builder_generate_content").click(_.bind(this.generate_content, this)); |
|
||||
}, |
|
||||
|
|
||||
destroy_content: function() { |
|
||||
if (this.dfm) { |
|
||||
this.dfm.destroy(); |
|
||||
this.dfm = undefined; |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
reload_widget: function() { |
|
||||
|
|
||||
}, |
|
||||
|
|
||||
start: function() { |
|
||||
this._super.apply(this, arguments); |
|
||||
var self = this; |
|
||||
self.mis_report_instance_id = self.getParent().datarecord.id; |
|
||||
if (self.mis_report_instance_id) { |
|
||||
self.getParent().dataset.context.no_destroy = true; |
|
||||
self.generate_content(); |
|
||||
} |
|
||||
}, |
|
||||
|
|
||||
get_context: function() { |
|
||||
var self = this; |
|
||||
var context = {}; |
|
||||
if (this.mis_report_instance_id){ |
|
||||
context.active_ids = [this.mis_report_instance_id]; |
|
||||
} |
|
||||
return context; |
|
||||
}, |
|
||||
|
|
||||
print: function() { |
|
||||
var self = this; |
|
||||
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
|
||||
new Model("mis.report.instance").call( |
|
||||
"print_pdf", |
|
||||
[self.mis_report_instance_id], |
|
||||
{'context': context} |
|
||||
).then(function(result){ |
|
||||
self.do_action(result); |
|
||||
}); |
|
||||
}, |
|
||||
export_pdf: function() { |
|
||||
var self = this; |
|
||||
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
|
||||
new Model("mis.report.instance").call( |
|
||||
"export_xls", |
|
||||
[self.mis_report_instance_id], |
|
||||
{'context': context} |
|
||||
).then(function(result){ |
|
||||
self.do_action(result); |
|
||||
}); |
|
||||
}, |
|
||||
display_settings: function() { |
|
||||
var self = this; |
|
||||
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
|
||||
new Model("mis.report.instance").call( |
|
||||
"display_settings", |
|
||||
[self.mis_report_instance_id], |
|
||||
{'context': context} |
|
||||
).then(function(result){ |
|
||||
self.do_action(result); |
|
||||
}); |
|
||||
}, |
|
||||
generate_content: function() { |
|
||||
var self = this; |
|
||||
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
|
||||
new Model("mis.report.instance").call( |
|
||||
"compute", |
|
||||
[self.mis_report_instance_id], |
|
||||
{'context': context} |
|
||||
).then(function(result){ |
|
||||
self.mis_report_data = result; |
|
||||
self.renderElement(); |
|
||||
}); |
|
||||
}, |
|
||||
renderElement: function() { |
|
||||
this._super(); |
|
||||
var self = this; |
|
||||
self.$(".oe_mis_builder_print").click(_.bind(this.print, this)); |
|
||||
self.$(".oe_mis_builder_export").click(_.bind(this.export_pdf, this)); |
|
||||
self.$(".oe_mis_builder_settings").click(_.bind(this.display_settings, this)); |
|
||||
var Users = new Model('res.users'); |
|
||||
Users.call('has_group', ['account.group_account_user']).done(function (res) { |
|
||||
if (res) { |
|
||||
self.$(".oe_mis_builder_settings").show(); |
|
||||
} |
|
||||
}); |
|
||||
self.initialize_field(); |
|
||||
}, |
|
||||
events: { |
|
||||
"click a.mis_builder_drilldown": "drilldown", |
|
||||
}, |
|
||||
|
|
||||
drilldown: function(event) { |
|
||||
var self = this; |
|
||||
var context = new data.CompoundContext(self.build_context(), self.get_context()|| {}); |
|
||||
var drilldown = $(event.target).data("drilldown"); |
|
||||
if (drilldown) { |
|
||||
new Model("mis.report.instance").call( |
|
||||
"drilldown", |
|
||||
[self.mis_report_instance_id, drilldown], |
|
||||
{'context': context} |
|
||||
).then(function(result) { |
|
||||
if (result) { |
|
||||
self.do_action(result); |
|
||||
} |
|
||||
}); |
|
||||
} |
|
||||
}, |
|
||||
}); |
|
||||
|
|
||||
ActionManager.include({ |
|
||||
/* |
|
||||
* In the case where we would be open in modal view, this is |
|
||||
* necessary to avoid to close the popup on click on button like print, |
|
||||
* export, ... |
|
||||
*/ |
|
||||
dialog_stop: function (reason) { |
|
||||
var self = this; |
|
||||
if (self.dialog_widget && self.dialog_widget.dataset && self.dialog_widget.dataset.context) { |
|
||||
var context = self.dialog_widget.dataset.context; |
|
||||
if (!context.no_destroy) { |
|
||||
this._super.apply(this, arguments); |
|
||||
} |
|
||||
} else { |
|
||||
this._super.apply(this, arguments); |
|
||||
} |
|
||||
} |
|
||||
}); |
|
||||
core.form_custom_registry.add('mis_report', MisReport); |
|
||||
|
|
||||
return { |
|
||||
MisReport: MisReport, |
|
||||
}; |
|
||||
|
|
||||
}); |
|
@ -1,57 +0,0 @@ |
|||||
<template> |
|
||||
<t t-name="mis_builder.MisReport"> |
|
||||
<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_generate_content btn btn-sm oe_button"><span class="fa fa-refresh"/> Refresh</button> |
|
||||
<button class="oe_mis_builder_print btn btn-sm oe_button"><span class="fa fa-print"/> Print</button> |
|
||||
<button class="oe_mis_builder_export btn btn-sm oe_button"><span class="fa fa-download"/>Export</button> |
|
||||
<button style="display: none;" class="oe_mis_builder_settings btn btn-sm oe_button"><span class="fa fa-cog"/> Settings</button> |
|
||||
</div> |
|
||||
<table class="oe_list_content o_list_view table table-condensed table-striped mis_builder"> |
|
||||
<thead> |
|
||||
<tr t-foreach="widget.mis_report_data.header" t-as="row" class="oe_list_header_columns"> |
|
||||
<th class="oe_list_header_char"> |
|
||||
</th> |
|
||||
<th t-foreach="row.cols" t-as="col" class="oe_list_header_char mis_builder_collabel" t-att-colspan="col.colspan"> |
|
||||
<t t-esc="col.label"/> |
|
||||
<t t-if="col.description"> |
|
||||
<br/> |
|
||||
<t t-esc="col.description"/> |
|
||||
</t> |
|
||||
</th> |
|
||||
</tr> |
|
||||
</thead> |
|
||||
<tbody> |
|
||||
<tr t-foreach="widget.mis_report_data.body" t-as="row"> |
|
||||
<td t-att="{'style': row.style}"> |
|
||||
<t t-esc="row.label"/> |
|
||||
<t t-if="row.description"> |
|
||||
<br/> |
|
||||
<t t-esc="row.description"/> |
|
||||
</t> |
|
||||
</td> |
|
||||
<td t-foreach="row.cells" t-as="cell" t-att="{'style': cell.style, 'title': cell.val_c}" class="mis_builder_amount"> |
|
||||
<t t-if="cell.drilldown_arg"> |
|
||||
<a href="javascript:void(0)" |
|
||||
class="mis_builder_drilldown" |
|
||||
t-att-data-drilldown="JSON.stringify(cell.drilldown_arg)" |
|
||||
> |
|
||||
<t t-esc="cell.val_r"/> |
|
||||
</a> |
|
||||
</t> |
|
||||
<t t-if="!cell.drilldown_arg"> |
|
||||
<t t-esc="cell.val_r"/> |
|
||||
</t> |
|
||||
</td> |
|
||||
</tr> |
|
||||
</tbody> |
|
||||
<tfoot> |
|
||||
<tr></tr> |
|
||||
</tfoot> |
|
||||
</table> |
|
||||
</t> |
|
||||
</div> |
|
||||
</t> |
|
||||
</template> |
|
@ -1,13 +0,0 @@ |
|||||
# -*- 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_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 +0,0 @@ |
|||||
"id","description","kpi_ids/id","name","query_ids/id" |
|
||||
"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test" |
|
@ -1,2 +0,0 @@ |
|||||
"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 +0,0 @@ |
|||||
"id","duration","name","offset","type","sequence","mode" |
|
||||
"mis_report_instance_period_test","1","today","","Day","","relative" |
|
@ -1,2 +0,0 @@ |
|||||
"id","description","expression","name" |
|
||||
"mis_report_kpi_test","total test","len(test)","total_test" |
|
@ -1,2 +0,0 @@ |
|||||
"id","date_field/id","domain","field_ids/id","model_id/id","name" |
|
||||
"mis_report_query_test","analytic.field_account_analytic_line_date","","analytic.field_account_analytic_line_amount","analytic.model_account_analytic_line","test" |
|
@ -1,12 +0,0 @@ |
|||||
# -*- 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 odoo.addons.mis_builder.models import accounting_none |
|
||||
|
|
||||
|
|
||||
def load_tests(loader, tests, ignore): |
|
||||
tests.addTests(doctest.DocTestSuite(accounting_none)) |
|
||||
return tests |
|
@ -1,226 +0,0 @@ |
|||||
# -*- 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 odoo import fields |
|
||||
import odoo.tests.common as common |
|
||||
from odoo.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)) |
|
@ -1,12 +0,0 @@ |
|||||
# -*- 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 odoo.addons.mis_builder.models import aggregate |
|
||||
|
|
||||
|
|
||||
def load_tests(loader, tests, ignore): |
|
||||
tests.addTests(doctest.DocTestSuite(aggregate)) |
|
||||
return tests |
|
@ -1,40 +0,0 @@ |
|||||
# -*- 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 odoo.tests.common as common |
|
||||
|
|
||||
|
|
||||
class TestFetchQuery(common.TransactionCase): |
|
||||
|
|
||||
def test_fetch_query(self): |
|
||||
# create a report on account.analytic.line |
|
||||
report = self.env.ref('mis_builder.mis_report_instance_test') |
|
||||
data = report.compute() |
|
||||
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,141 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
||||
|
|
||||
import odoo.tests.common as common |
|
||||
from odoo.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') |
|
@ -1,27 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
||||
|
|
||||
import odoo.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') |
|
@ -1,158 +0,0 @@ |
|||||
# -*- coding: utf-8 -*- |
|
||||
# © 2016 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html). |
|
||||
|
|
||||
import odoo.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)) |
|
@ -1,12 +0,0 @@ |
|||||
# -*- 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 odoo.addons.mis_builder.models import simple_array |
|
||||
|
|
||||
|
|
||||
def load_tests(loader, tests, ignore): |
|
||||
tests.addTests(doctest.DocTestSuite(simple_array)) |
|
||||
return tests |
|
@ -1,25 +0,0 @@ |
|||||
# -*- 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 odoo.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,172 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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" />--> |
|
||||
<field name="report_id" invisible="1"/> |
|
||||
</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" domain="[('report_id', '=', parent.report_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_conf_menu" parent="account.menu_finance_configuration" name="MIS Reporting" sequence="90"/> |
|
||||
<menuitem id="mis_report_view_menu" parent="mis_report_conf_menu" name="MIS Report Templates" action="mis_report_view_action" sequence="21"/> |
|
||||
|
|
||||
</data> |
|
||||
</odoo> |
|
@ -1,213 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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="fa-search" /> |
|
||||
<button type="object" name="print_pdf" string="Print" icon="fa-print" /> |
|
||||
<button type="object" name="export_xls" string="Export" icon="fa-download" /> |
|
||||
<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="fa-search" /> |
|
||||
<button type="object" name="print_pdf" string="Print" icon="fa-print" /> |
|
||||
<button type="object" name="export_xls" string="Export" icon="fa-download" /> |
|
||||
<button type="action" name="%(mis_report_instance_add_to_dashboard_action)d" string="Add to dashboard" icon="fa-plus" attrs="{'invisible': [('temporary', '=', True)]}"/> |
|
||||
<button type="object" name="save_report" string="Save" icon="fa-save" 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="Columns" |
|
||||
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_finance_menu" parent="account.menu_finance_reports" name="MIS Reporting" sequence="101"/> |
|
||||
<menuitem id="mis_report_instance_view_menu" parent="mis_report_finance_menu" name="MIS Reports" action="mis_report_instance_view_action" sequence="10"/> |
|
||||
|
|
||||
<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="mis_report_finance_menu" |
|
||||
name="Last Reports Generated" |
|
||||
action="last_mis_report_instance_view_action" |
|
||||
sequence="20"/> |
|
||||
|
|
||||
<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> |
|
||||
<group> |
|
||||
<field name="name" placeholder="Name"/> |
|
||||
<field name="valid" invisible="1"/> |
|
||||
<field name="report_instance_id" invisible="1"/> |
|
||||
<field name="id" invisible="1"/> |
|
||||
</group> |
|
||||
<notebook> |
|
||||
<page string="Dates"> |
|
||||
<group> |
|
||||
<field name="mode" widget="radio"/> |
|
||||
</group> |
|
||||
<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> |
|
||||
</page> |
|
||||
<page string="Advanced"> |
|
||||
<group> |
|
||||
<field name="subkpi_ids" |
|
||||
domain="[('report_id', '=', parent.report_id)]" |
|
||||
widget="many2many_tags"/> |
|
||||
<field name="normalize_factor"/> |
|
||||
<field name="comparison_column_ids" |
|
||||
domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" |
|
||||
widget="many2many_tags"/> |
|
||||
</group> |
|
||||
</page> |
|
||||
</notebook> |
|
||||
</form> |
|
||||
</field> |
|
||||
</record> |
|
||||
|
|
||||
</data> |
|
||||
</odoo> |
|
@ -1,80 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<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="mis_report_conf_menu" name="MIS Report Styles" action="mis_report_style_view_action" sequence="22"/> |
|
||||
|
|
||||
</data> |
|
||||
</odoo> |
|
@ -1,5 +0,0 @@ |
|||||
# -*- 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,66 +0,0 @@ |
|||||
# -*- 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 odoo import api, fields, models |
|
||||
from lxml import etree |
|
||||
|
|
||||
|
|
||||
class AddMisReportInstanceDashboard(models.TransientModel): |
|
||||
_name = "add.mis.report.instance.dashboard.wizard" |
|
||||
|
|
||||
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')]") |
|
||||
|
|
||||
@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.env['mis.report.instance'].browse( |
|
||||
self.env.context['active_id']).name |
|
||||
return res |
|
||||
|
|
||||
@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 |
|
||||
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' |
|
||||
% self.env.context['active_id'], |
|
||||
'res_model': 'mis.report.instance', |
|
||||
'res_id': self.env.context['active_id'], |
|
||||
'target': 'current', |
|
||||
'view_mode': 'form', |
|
||||
'view_id': view.id}) |
|
||||
# add this result in the selected dashboard |
|
||||
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.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( |
|
||||
self.env.context), |
|
||||
'name': str(report_result.id), |
|
||||
'string': self.name, |
|
||||
'view_mode': 'form'})) |
|
||||
self.env['ir.ui.view.custom'].create( |
|
||||
{'user_id': self.env.uid, |
|
||||
'ref_id': self.dashboard_id.view_id.id, |
|
||||
'arch': etree.tostring(new_arch, pretty_print=True)}) |
|
||||
|
|
||||
return {'type': 'ir.actions.act_window_close', } |
|
@ -1,33 +0,0 @@ |
|||||
<?xml version="1.0" encoding="UTF-8"?> |
|
||||
<odoo> |
|
||||
<data> |
|
||||
|
|
||||
<record model="ir.ui.view" id="mis_report_instance_add_to_dashboard_form_view"> |
|
||||
<field name="name">add.mis.report.instance.dashboard.wizard.view</field> |
|
||||
<field name="model">add.mis.report.instance.dashboard.wizard</field> |
|
||||
<field name="arch" type="xml"> |
|
||||
<form string="Add to dashboard" version="7.0"> |
|
||||
<group> |
|
||||
<field name="name"/> |
|
||||
<field name="dashboard_id"/> |
|
||||
</group> |
|
||||
<footer> |
|
||||
<button name="action_add_to_dashboard" string="Add to dashboard" type="object" default_focus="1" class="oe_highlight"/> |
|
||||
or |
|
||||
<button string="Cancel" class="oe_link" special="cancel"/> |
|
||||
</footer> |
|
||||
</form> |
|
||||
</field> |
|
||||
</record> |
|
||||
|
|
||||
<record model="ir.actions.act_window" id="mis_report_instance_add_to_dashboard_action"> |
|
||||
<field name="name">Add to dashboard</field> |
|
||||
<field name="res_model">add.mis.report.instance.dashboard.wizard</field> |
|
||||
<field name="view_type">form</field> |
|
||||
<field name="view_mode">form</field> |
|
||||
<field name="view_id" ref="mis_report_instance_add_to_dashboard_form_view"/> |
|
||||
<field name="target">new</field> |
|
||||
</record> |
|
||||
|
|
||||
</data> |
|
||||
</odoo> |
|
@ -1,46 +0,0 @@ |
|||||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg |
|
||||
:alt: License: AGPL-3 |
|
||||
|
|
||||
MIS Builder demo data |
|
||||
===================== |
|
||||
|
|
||||
This module adds some demo data for the mis_builder module. |
|
||||
|
|
||||
Installation |
|
||||
============ |
|
||||
|
|
||||
There is no specific installation procedure for this module. |
|
||||
|
|
||||
|
|
||||
Bug Tracker |
|
||||
=========== |
|
||||
|
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_. |
|
||||
In case of trouble, please check there if your issue has already been reported. |
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback |
|
||||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_. |
|
||||
|
|
||||
Credits |
|
||||
======= |
|
||||
|
|
||||
Contributors |
|
||||
------------ |
|
||||
|
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu> |
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu> |
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu> |
|
||||
|
|
||||
Maintainer |
|
||||
---------- |
|
||||
|
|
||||
.. image:: https://odoo-community.org/logo.png |
|
||||
:alt: Odoo Community Association |
|
||||
:target: https://odoo-community.org |
|
||||
|
|
||||
This module is maintained by the OCA. |
|
||||
|
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose |
|
||||
mission is to support the collaborative development of Odoo features and |
|
||||
promote its widespread use. |
|
||||
|
|
||||
To contribute to this module, please visit http://odoo-community.org. |
|
@ -1,23 +0,0 @@ |
|||||
# -*- encoding: utf-8 -*- |
|
||||
############################################################################## |
|
||||
# |
|
||||
# mis_builder module for OpenERP, Management Information System Builder |
|
||||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# |
|
||||
# This file is a part of mis_builder |
|
||||
# |
|
||||
# mis_builder is free software: you can redistribute it and/or modify |
|
||||
# it under the terms of the GNU Affero General Public License v3 or later |
|
||||
# as published by the Free Software Foundation, either version 3 of the |
|
||||
# License, or (at your option) any later version. |
|
||||
# |
|
||||
# mis_builder is distributed in the hope that it will be useful, |
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
# GNU Affero General Public License v3 or later for more details. |
|
||||
# |
|
||||
# You should have received a copy of the GNU Affero General Public License |
|
||||
# v3 or later along with this program. |
|
||||
# If not, see <http://www.gnu.org/licenses/>. |
|
||||
# |
|
||||
############################################################################## |
|
@ -1,53 +0,0 @@ |
|||||
# -*- encoding: utf-8 -*- |
|
||||
############################################################################## |
|
||||
# |
|
||||
# mis_builder module for OpenERP, Management Information System Builder |
|
||||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>) |
|
||||
# |
|
||||
# This file is a part of mis_builder |
|
||||
# |
|
||||
# mis_builder is free software: you can redistribute it and/or modify |
|
||||
# it under the terms of the GNU Affero General Public License v3 or later |
|
||||
# as published by the Free Software Foundation, either version 3 of the |
|
||||
# License, or (at your option) any later version. |
|
||||
# |
|
||||
# mis_builder is distributed in the hope that it will be useful, |
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of |
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
|
||||
# GNU Affero General Public License v3 or later for more details. |
|
||||
# |
|
||||
# You should have received a copy of the GNU Affero General Public License |
|
||||
# v3 or later along with this program. |
|
||||
# If not, see <http://www.gnu.org/licenses/>. |
|
||||
# |
|
||||
############################################################################## |
|
||||
|
|
||||
{ |
|
||||
'name': 'MIS Builder demo data', |
|
||||
'version': '9.0.1.0.0', |
|
||||
'category': 'Reporting', |
|
||||
'summary': """ |
|
||||
Demo data for the mis_builder module |
|
||||
""", |
|
||||
'author': 'ACSONE SA/NV,' |
|
||||
'Odoo Community Association (OCA)', |
|
||||
'website': 'http://acsone.eu', |
|
||||
'depends': [ |
|
||||
'account_accountant', |
|
||||
'mis_builder', |
|
||||
'crm' |
|
||||
], |
|
||||
'data': [ |
|
||||
], |
|
||||
'demo': [ |
|
||||
'mis.report.kpi.csv', |
|
||||
'mis.report.query.csv', |
|
||||
'mis.report.csv', |
|
||||
'mis.report.instance.period.csv', |
|
||||
'mis.report.instance.csv', |
|
||||
], |
|
||||
'installable': False, |
|
||||
'application': False, |
|
||||
'auto_install': False, |
|
||||
'license': 'AGPL-3', |
|
||||
} |
|
@ -1,3 +0,0 @@ |
|||||
"id","description","kpi_ids/id","name","query_ids/id" |
|
||||
"mis_report","","mis_report_kpi_1,mis_report_kpi_2,mis_report_kpi_3,mis_report_kpi_4,mis_report_kpi_5","Demo margin report","mis_report_query" |
|
||||
"mis_report_phonecall","","mis_report_phonecall_kpi_1,mis_report_phonecall_kpi_2,mis_report_phonecall_kpi_3,mis_report_phonecall_kpi_4","Demo phonecall report","mis_report_phonecall_query" |
|
@ -1,3 +0,0 @@ |
|||||
id,date,description,name,period_ids/id,report_id/id,root_account/id |
|
||||
mis_report_instance,,,Test-margin-report-instance,"mis_report_instance_period_1,mis_report_instance_period_2,mis_report_instance_period_3,mis_report_instance_period_4",mis_report,account.chart0 |
|
||||
mis_report_phonecall_instance,,,Test phonecall report instance,"mis_report_phonecall_instance_period_1,mis_report_phonecall_instance_period_2,mis_report_phonecall_instance_period_3",mis_report_phonecall,account.chart0 |
|
@ -1,8 +0,0 @@ |
|||||
id,duration,name,offset,type,sequence |
|
||||
mis_report_instance_period_1,1,today,0,Day,1 |
|
||||
mis_report_instance_period_2,1,yesterday,-1,Day,2 |
|
||||
mis_report_instance_period_3,1,last week,-1,Week,3 |
|
||||
mis_report_instance_period_4,1,last period,-1,Fiscal Period,4 |
|
||||
mis_report_phonecall_instance_period_1,1,today,0,Day,1 |
|
||||
mis_report_phonecall_instance_period_2,1,this period,0,Fiscal Period,2 |
|
||||
mis_report_phonecall_instance_period_3,1,previous period,-1,Fiscal Period,3 |
|
@ -1,10 +0,0 @@ |
|||||
id,compare_method,description,expression,divider,name,dp,sequence,type,suffix |
|
||||
mis_report_kpi_1,Percentage,CA,-bal[X2001],,ca,,1,Numeric,€ |
|
||||
mis_report_kpi_2,Percentage,CAHT invoice,sum([s.amount_untaxed for s in inv]),,total_invoice,,2,Numeric,€ |
|
||||
mis_report_kpi_3,Percentage,Cost,bal[X2110],,cost,,3,Numeric,€ |
|
||||
mis_report_kpi_4,Percentage,Profit,ca - cost,,profit,,4,Numeric,€ |
|
||||
mis_report_kpi_5,Difference,Margin,profit/ca,,margin,,5,Percentage,% |
|
||||
mis_report_phonecall_kpi_1,Percentage,Total phone call,len(phone),,total_phone_call,,1,Numeric, |
|
||||
mis_report_phonecall_kpi_2,Percentage,Average duration phone call,sum([p.duration for p in phone])/total_phone_call,,average_duration_phone_call,2,2,Numeric, |
|
||||
mis_report_phonecall_kpi_3,Percentage,Total converted phone call,sum([p.opportunity_id and 1 or 0 for p in phone]),,phone_call_convert,,3,Numeric, |
|
||||
mis_report_phonecall_kpi_4,Percentage,Average duration converted phone call,sum([p.opportunity_id and p.duration or 0 for p in phone]),,average_convert_duration_phone_c,2,4,Numeric, |
|
@ -1,3 +0,0 @@ |
|||||
"id","date_field/id","domain","field_ids/id","model_id/id","name" |
|
||||
"mis_report_query","account.field_account_invoice_date_invoice","","account.field_account_invoice_amount_untaxed","account.model_account_invoice","inv" |
|
||||
"mis_report_phonecall_query","crm.field_crm_phonecall_date","","crm.field_crm_phonecall_duration,crm.field_crm_phonecall_opportunity_id","crm.model_crm_phonecall","phone" |
|
@ -1 +0,0 @@ |
|||||
__import__('pkg_resources').declare_namespace(__name__) |
|
@ -1 +0,0 @@ |
|||||
__import__('pkg_resources').declare_namespace(__name__) |
|
@ -1 +0,0 @@ |
|||||
../../../../mis_builder |
|
@ -1,6 +0,0 @@ |
|||||
import setuptools |
|
||||
|
|
||||
setuptools.setup( |
|
||||
setup_requires=['setuptools-odoo'], |
|
||||
odoo_addon=True, |
|
||||
) |
|
Write
Preview
Loading…
Cancel
Save
Reference in new issue