commit
d3ab39e994
|
@ -0,0 +1,92 @@
|
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:alt: License: AGPL-3
|
||||
|
||||
MIS Builder
|
||||
===========
|
||||
|
||||
This module allows you to build Management Information Systems dashboards.
|
||||
Such style of reports presents KPI in rows and time periods in columns.
|
||||
Reports mainly fetch data from account moves, but can also combine data coming
|
||||
from arbitrary Odoo models. Reports can be exported to PDF, Excel and they
|
||||
can be added to Odoo dashboards.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
There is no specific installation procedure for this module.
|
||||
|
||||
Configuration and Usage
|
||||
=======================
|
||||
|
||||
To configure this module, you need to:
|
||||
|
||||
* Go to Accounting > Configuration > Financial Reports > MIS Report Templates where
|
||||
you can create report templates by defining KPI's. KPI's constitute the rows of your
|
||||
reports. Such report templates are time independent.
|
||||
|
||||
.. figure:: static/description/ex_report_template.png
|
||||
:scale: 80 %
|
||||
:alt: Sample report template
|
||||
|
||||
* Then in Accounting > Reporting > MIS Reports you can create report instance by
|
||||
binding the templates to time period, hence defining the columns of your reports.
|
||||
|
||||
.. figure:: static/description/ex_report.png
|
||||
:alt: Sample report configuration
|
||||
|
||||
* From the MIS Report view, you can preview the report, add it to and Odoo dashboard,
|
||||
and export it to PDF or Excel.
|
||||
|
||||
.. figure:: static/description/ex_dashboard.png
|
||||
:alt: Sample dashboard view
|
||||
|
||||
For further information, please visit:
|
||||
|
||||
* https://www.odoo.com/forum/help-1
|
||||
|
||||
Known issues / Roadmap
|
||||
======================
|
||||
|
||||
* More tests should be added. The first part is creating test data, then it will be
|
||||
easier. At the minimum, We need the following test data:
|
||||
|
||||
* one account charts with a few normal accounts and view accounts,
|
||||
* two fiscal years,
|
||||
* an opening entry in the second fiscal year,
|
||||
* to test multi-company consolidation, we need a second company with it's own
|
||||
account chart and two fiscal years, but without opening entry; we also need
|
||||
a third company which is the parent of the other two and has a consolidation
|
||||
chart of account.
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
|
||||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit http://odoo-community.org.
|
|
@ -0,0 +1,27 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import models
|
||||
from . import wizard
|
||||
from . import report
|
|
@ -0,0 +1,62 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'MIS Builder',
|
||||
'version': '0.2',
|
||||
'category': 'Reporting',
|
||||
'summary': """
|
||||
Build 'Management Information System' Reports and Dashboards
|
||||
""",
|
||||
'author': 'ACSONE SA/NV,'
|
||||
'Odoo Community Association (OCA)',
|
||||
'website': 'http://acsone.eu',
|
||||
'depends': [
|
||||
'account',
|
||||
'report_xls', # OCA/reporting-engine
|
||||
],
|
||||
'data': [
|
||||
'wizard/mis_builder_dashboard.xml',
|
||||
'views/mis_builder.xml',
|
||||
'security/ir.model.access.csv',
|
||||
'security/mis_builder_security.xml',
|
||||
'report/report_mis_report_instance.xml',
|
||||
],
|
||||
'test': [
|
||||
],
|
||||
'demo': [
|
||||
'tests/mis.report.kpi.csv',
|
||||
'tests/mis.report.query.csv',
|
||||
'tests/mis.report.csv',
|
||||
'tests/mis.report.instance.period.csv',
|
||||
'tests/mis.report.instance.csv',
|
||||
],
|
||||
'qweb': [
|
||||
'static/src/xml/*.xml'
|
||||
],
|
||||
'installable': True,
|
||||
'application': True,
|
||||
'auto_install': False,
|
||||
'license': 'AGPL-3',
|
||||
}
|
|
@ -0,0 +1,42 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
|
||||
def migrate(cr, version):
|
||||
if not version:
|
||||
return
|
||||
|
||||
cr.execute("""
|
||||
ALTER TABLE mis_report_instance
|
||||
ADD COLUMN root_account INTEGER
|
||||
""")
|
||||
cr.execute("""
|
||||
UPDATE mis_report_instance
|
||||
SET root_account = (
|
||||
SELECT id FROM account_account
|
||||
WHERE parent_id IS NULL
|
||||
AND company_id = mis_report_instance.company_id
|
||||
LIMIT 1
|
||||
)
|
||||
""")
|
|
@ -0,0 +1,26 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import mis_builder
|
||||
from . import aep
|
|
@ -0,0 +1,382 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import re
|
||||
from collections import defaultdict
|
||||
|
||||
from openerp.exceptions import Warning
|
||||
from openerp.osv import expression
|
||||
from openerp.tools.safe_eval import safe_eval
|
||||
from openerp.tools.translate import _
|
||||
|
||||
MODE_VARIATION = 'p'
|
||||
MODE_INITIAL = 'i'
|
||||
MODE_END = 'e'
|
||||
|
||||
|
||||
class AccountingExpressionProcessor(object):
|
||||
""" Processor for accounting expressions.
|
||||
|
||||
Expressions of the form <field><mode>[accounts][optional move line domain]
|
||||
are supported, where:
|
||||
* field is bal, crd, deb
|
||||
* mode is i (initial balance), e (ending balance),
|
||||
p (moves over period)
|
||||
* accounts is a list of accounts, possibly containing % wildcards
|
||||
* an optional domain on move lines allowing filters on eg analytic
|
||||
accounts or journal
|
||||
|
||||
Examples:
|
||||
* bal[70]: variation of the balance of moves on account 70
|
||||
over the period (it is the same as balp[70]);
|
||||
* bali[70,60]: balance of accounts 70 and 60 at the start of period;
|
||||
* bale[1%]: balance of accounts starting with 1 at end of period.
|
||||
|
||||
How to use:
|
||||
* repeatedly invoke parse_expr() for each expression containing
|
||||
accounting variables as described above; this lets the processor
|
||||
group domains and modes and accounts;
|
||||
* when all expressions have been parsed, invoke done_parsing()
|
||||
to notify the processor that it can prepare to query (mainly
|
||||
search all accounts - children, consolidation - that will need to
|
||||
be queried;
|
||||
* for each period, call do_queries(), then call replace_expr() for each
|
||||
expression to replace accounting variables with their resulting value
|
||||
for the given period.
|
||||
|
||||
How it works:
|
||||
* by accumulating the expressions before hand, it ensures to do the
|
||||
strict minimum number of queries to the database (for each period,
|
||||
one query per domain and mode);
|
||||
* it queries using the orm read_group which reduces to a query with
|
||||
sum on debit and credit and group by on account_id (note: it seems
|
||||
the orm then does one query per account to fetch the account
|
||||
name...);
|
||||
* additionally, one query per view/consolidation account is done to
|
||||
discover the children accounts.
|
||||
"""
|
||||
|
||||
ACC_RE = re.compile(r"(?P<field>\bbal|\bcrd|\bdeb)"
|
||||
r"(?P<mode>[pise])?"
|
||||
r"(?P<accounts>_[a-zA-Z0-9]+|\[.*?\])"
|
||||
r"(?P<domain>\[.*?\])?")
|
||||
|
||||
def __init__(self, env):
|
||||
self.env = env
|
||||
# before done_parsing: {(domain, mode): set(account_codes)}
|
||||
# after done_parsing: {(domain, mode): list(account_ids)}
|
||||
self._map_account_ids = defaultdict(set)
|
||||
self._account_ids_by_code = defaultdict(set)
|
||||
|
||||
def _load_account_codes(self, account_codes, root_account):
|
||||
account_model = self.env['account.account']
|
||||
# TODO: account_obj is necessary because _get_children_and_consol
|
||||
# does not work in new API?
|
||||
account_obj = self.env.registry('account.account')
|
||||
exact_codes = set()
|
||||
like_codes = set()
|
||||
for account_code in account_codes:
|
||||
if account_code in self._account_ids_by_code:
|
||||
continue
|
||||
if account_code is None:
|
||||
# by convention the root account is keyed as
|
||||
# None in _account_ids_by_code, so it is consistent
|
||||
# with what _parse_match_object returns for an
|
||||
# empty list of account codes, ie [None]
|
||||
exact_codes.add(root_account.code)
|
||||
elif '%' in account_code:
|
||||
like_codes.add(account_code)
|
||||
else:
|
||||
exact_codes.add(account_code)
|
||||
for account in account_model.\
|
||||
search([('code', 'in', list(exact_codes)),
|
||||
('parent_id', 'child_of', root_account.id)]):
|
||||
if account.code == root_account.code:
|
||||
code = None
|
||||
else:
|
||||
code = account.code
|
||||
if account.type in ('view', 'consolidation'):
|
||||
self._account_ids_by_code[code].update(
|
||||
account_obj._get_children_and_consol(
|
||||
self.env.cr, self.env.uid,
|
||||
[account.id],
|
||||
self.env.context))
|
||||
else:
|
||||
self._account_ids_by_code[code].add(account.id)
|
||||
for like_code in like_codes:
|
||||
for account in account_model.\
|
||||
search([('code', 'like', like_code),
|
||||
('parent_id', 'child_of', root_account.id)]):
|
||||
if account.type in ('view', 'consolidation'):
|
||||
self._account_ids_by_code[like_code].update(
|
||||
account_obj._get_children_and_consol(
|
||||
self.env.cr, self.env.uid,
|
||||
[account.id],
|
||||
self.env.context))
|
||||
else:
|
||||
self._account_ids_by_code[like_code].add(account.id)
|
||||
|
||||
def _parse_match_object(self, mo):
|
||||
"""Split a match object corresponding to an accounting variable
|
||||
|
||||
Returns field, mode, [account codes], (domain expression).
|
||||
"""
|
||||
field, mode, account_codes, domain = mo.groups()
|
||||
if not mode:
|
||||
mode = MODE_VARIATION
|
||||
elif mode == 's':
|
||||
mode = MODE_END
|
||||
if account_codes.startswith('_'):
|
||||
account_codes = account_codes[1:]
|
||||
else:
|
||||
account_codes = account_codes[1:-1]
|
||||
if account_codes.strip():
|
||||
account_codes = [a.strip() for a in account_codes.split(',')]
|
||||
else:
|
||||
account_codes = [None]
|
||||
domain = domain or '[]'
|
||||
domain = tuple(safe_eval(domain))
|
||||
return field, mode, account_codes, domain
|
||||
|
||||
def parse_expr(self, expr):
|
||||
"""Parse an expression, extracting accounting variables.
|
||||
|
||||
Domains and accounts are extracted and stored in the map
|
||||
so when all expressions have been parsed, we know which
|
||||
account codes to query for each domain and mode.
|
||||
"""
|
||||
for mo in self.ACC_RE.finditer(expr):
|
||||
_, mode, account_codes, domain = self._parse_match_object(mo)
|
||||
key = (domain, mode)
|
||||
self._map_account_ids[key].update(account_codes)
|
||||
|
||||
def done_parsing(self, root_account):
|
||||
"""Load account codes and replace account codes by
|
||||
account ids in map."""
|
||||
for key, account_codes in self._map_account_ids.items():
|
||||
self._load_account_codes(account_codes, root_account)
|
||||
account_ids = set()
|
||||
for account_code in account_codes:
|
||||
account_ids.update(self._account_ids_by_code[account_code])
|
||||
self._map_account_ids[key] = list(account_ids)
|
||||
|
||||
@classmethod
|
||||
def has_account_var(cls, expr):
|
||||
"""Test if an string contains an accounting variable."""
|
||||
return bool(cls.ACC_RE.search(expr))
|
||||
|
||||
def get_aml_domain_for_expr(self, expr,
|
||||
date_from, date_to,
|
||||
period_from, period_to,
|
||||
target_move):
|
||||
""" Get a domain on account.move.line for an expression.
|
||||
|
||||
Prerequisite: done_parsing() must have been invoked.
|
||||
|
||||
Returns a domain that can be used to search on account.move.line.
|
||||
"""
|
||||
aml_domains = []
|
||||
date_domain_by_mode = {}
|
||||
for mo in self.ACC_RE.finditer(expr):
|
||||
field, mode, account_codes, domain = self._parse_match_object(mo)
|
||||
aml_domain = list(domain)
|
||||
account_ids = set()
|
||||
for account_code in account_codes:
|
||||
account_ids.update(self._account_ids_by_code[account_code])
|
||||
aml_domain.append(('account_id', 'in', tuple(account_ids)))
|
||||
if field == 'crd':
|
||||
aml_domain.append(('credit', '>', 0))
|
||||
elif field == 'deb':
|
||||
aml_domain.append(('debit', '>', 0))
|
||||
aml_domains.append(expression.normalize_domain(aml_domain))
|
||||
if mode not in date_domain_by_mode:
|
||||
date_domain_by_mode[mode] = \
|
||||
self.get_aml_domain_for_dates(date_from, date_to,
|
||||
period_from, period_to,
|
||||
mode, target_move)
|
||||
return expression.OR(aml_domains) + \
|
||||
expression.OR(date_domain_by_mode.values())
|
||||
|
||||
def _period_has_moves(self, period):
|
||||
move_model = self.env['account.move']
|
||||
return bool(move_model.search([('period_id', '=', period.id)],
|
||||
limit=1))
|
||||
|
||||
def _get_previous_opening_period(self, period, company_id):
|
||||
period_model = self.env['account.period']
|
||||
periods = period_model.search(
|
||||
[('date_start', '<=', period.date_start),
|
||||
('special', '=', True),
|
||||
('company_id', '=', company_id)],
|
||||
order="date_start desc",
|
||||
limit=1)
|
||||
return periods and periods[0]
|
||||
|
||||
def _get_previous_normal_period(self, period, company_id):
|
||||
period_model = self.env['account.period']
|
||||
periods = period_model.search(
|
||||
[('date_start', '<', period.date_start),
|
||||
('special', '=', False),
|
||||
('company_id', '=', company_id)],
|
||||
order="date_start desc",
|
||||
limit=1)
|
||||
return periods and periods[0]
|
||||
|
||||
def _get_first_normal_period(self, company_id):
|
||||
period_model = self.env['account.period']
|
||||
periods = period_model.search(
|
||||
[('special', '=', False),
|
||||
('company_id', '=', company_id)],
|
||||
order="date_start asc",
|
||||
limit=1)
|
||||
return periods and periods[0]
|
||||
|
||||
def _get_period_ids_between(self, period_from, period_to, company_id):
|
||||
period_model = self.env['account.period']
|
||||
periods = period_model.search(
|
||||
[('date_start', '>=', period_from.date_start),
|
||||
('date_stop', '<=', period_to.date_stop),
|
||||
('special', '=', False),
|
||||
('company_id', '=', company_id)])
|
||||
period_ids = [p.id for p in periods]
|
||||
if period_from.special:
|
||||
period_ids.append(period_from.id)
|
||||
return period_ids
|
||||
|
||||
def _get_period_company_ids(self, period_from, period_to):
|
||||
period_model = self.env['account.period']
|
||||
periods = period_model.search(
|
||||
[('date_start', '>=', period_from.date_start),
|
||||
('date_stop', '<=', period_to.date_stop),
|
||||
('special', '=', False)])
|
||||
return set([p.company_id.id for p in periods])
|
||||
|
||||
def _get_period_ids_for_mode(self, period_from, period_to, mode):
|
||||
assert not period_from.special
|
||||
assert not period_to.special
|
||||
assert period_from.company_id == period_to.company_id
|
||||
assert period_from.date_start <= period_to.date_start
|
||||
period_ids = []
|
||||
for company_id in self._get_period_company_ids(period_from, period_to):
|
||||
if mode == MODE_VARIATION:
|
||||
period_ids.extend(self._get_period_ids_between(
|
||||
period_from, period_to, company_id))
|
||||
else:
|
||||
if mode == MODE_INITIAL:
|
||||
period_to = self._get_previous_normal_period(
|
||||
period_from, company_id)
|
||||
# look for opening period with moves
|
||||
opening_period = self._get_previous_opening_period(
|
||||
period_from, company_id)
|
||||
if opening_period and \
|
||||
self._period_has_moves(opening_period[0]):
|
||||
# found opening period with moves
|
||||
if opening_period.date_start == period_from.date_start and\
|
||||
mode == MODE_INITIAL:
|
||||
# if the opening period has the same start date as
|
||||
# period_from, then we'll find the initial balance
|
||||
# in the initial period and that's it
|
||||
period_ids.append(opening_period[0].id)
|
||||
continue
|
||||
period_from = opening_period[0]
|
||||
else:
|
||||
# no opening period with moves,
|
||||
# use very first normal period
|
||||
period_from = self._get_first_normal_period(company_id)
|
||||
if period_to:
|
||||
period_ids.extend(self._get_period_ids_between(
|
||||
period_from, period_to, company_id))
|
||||
return period_ids
|
||||
|
||||
def get_aml_domain_for_dates(self, date_from, date_to,
|
||||
period_from, period_to,
|
||||
mode,
|
||||
target_move):
|
||||
if period_from and period_to:
|
||||
period_ids = self._get_period_ids_for_mode(
|
||||
period_from, period_to, mode)
|
||||
domain = [('period_id', 'in', period_ids)]
|
||||
else:
|
||||
if mode == MODE_VARIATION:
|
||||
domain = [('date', '>=', date_from), ('date', '<=', date_to)]
|
||||
else:
|
||||
raise Warning(_("Modes i and e are only applicable for "
|
||||
"fiscal periods"))
|
||||
if target_move == 'posted':
|
||||
domain.append(('move_id.state', '=', 'posted'))
|
||||
return expression.normalize_domain(domain)
|
||||
|
||||
def do_queries(self, date_from, date_to, period_from, period_to,
|
||||
target_move):
|
||||
"""Query sums of debit and credit for all accounts and domains
|
||||
used in expressions.
|
||||
|
||||
This method must be executed after done_parsing().
|
||||
"""
|
||||
aml_model = self.env['account.move.line']
|
||||
# {(domain, mode): {account_id: (debit, credit)}}
|
||||
self._data = defaultdict(dict)
|
||||
domain_by_mode = {}
|
||||
for key in self._map_account_ids:
|
||||
domain, mode = key
|
||||
if mode not in domain_by_mode:
|
||||
domain_by_mode[mode] = \
|
||||
self.get_aml_domain_for_dates(date_from, date_to,
|
||||
period_from, period_to,
|
||||
mode, target_move)
|
||||
domain = list(domain) + domain_by_mode[mode]
|
||||
domain.append(('account_id', 'in', self._map_account_ids[key]))
|
||||
# fetch sum of debit/credit, grouped by account_id
|
||||
accs = aml_model.read_group(domain,
|
||||
['debit', 'credit', 'account_id'],
|
||||
['account_id'])
|
||||
for acc in accs:
|
||||
self._data[key][acc['account_id'][0]] = \
|
||||
(acc['debit'] or 0.0, acc['credit'] or 0.0)
|
||||
|
||||
def replace_expr(self, expr):
|
||||
"""Replace accounting variables in an expression by their amount.
|
||||
|
||||
Returns a new expression string.
|
||||
|
||||
This method must be executed after do_queries().
|
||||
"""
|
||||
def f(mo):
|
||||
field, mode, account_codes, domain = self._parse_match_object(mo)
|
||||
key = (domain, mode)
|
||||
account_ids_data = self._data[key]
|
||||
v = 0.0
|
||||
for account_code in account_codes:
|
||||
account_ids = self._account_ids_by_code[account_code]
|
||||
for account_id in account_ids:
|
||||
debit, credit = \
|
||||
account_ids_data.get(account_id, (0.0, 0.0))
|
||||
if field == 'bal':
|
||||
v += debit - credit
|
||||
elif field == 'deb':
|
||||
v += debit
|
||||
elif field == 'crd':
|
||||
v += credit
|
||||
return '(' + repr(v) + ')'
|
||||
return self.ACC_RE.sub(f, expr)
|
|
@ -0,0 +1,741 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import datetime
|
||||
import dateutil
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
import traceback
|
||||
|
||||
import pytz
|
||||
|
||||
from openerp import api, fields, models, _
|
||||
from openerp.tools.safe_eval import safe_eval
|
||||
|
||||
from .aep import AccountingExpressionProcessor as AEP
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class AutoStruct(object):
|
||||
|
||||
def __init__(self, **kwargs):
|
||||
for k, v in kwargs.items():
|
||||
setattr(self, k, v)
|
||||
|
||||
|
||||
def _get_selection_label(selection, value):
|
||||
for v, l in selection:
|
||||
if v == value:
|
||||
return l
|
||||
return ''
|
||||
|
||||
|
||||
def _utc_midnight(d, tz_name, add_day=0):
|
||||
d = fields.Datetime.from_string(d) + datetime.timedelta(days=add_day)
|
||||
utc_tz = pytz.timezone('UTC')
|
||||
context_tz = pytz.timezone(tz_name)
|
||||
local_timestamp = context_tz.localize(d, is_dst=False)
|
||||
return fields.Datetime.to_string(local_timestamp.astimezone(utc_tz))
|
||||
|
||||
|
||||
def _python_var(var_str):
|
||||
return re.sub(r'\W|^(?=\d)', '_', var_str).lower()
|
||||
|
||||
|
||||
def _is_valid_python_var(name):
|
||||
return re.match("[_A-Za-z][_a-zA-Z0-9]*$", name)
|
||||
|
||||
|
||||
def _sum(l):
|
||||
if not l:
|
||||
return None
|
||||
return sum(l)
|
||||
|
||||
|
||||
def _avg(l):
|
||||
if not l:
|
||||
return None
|
||||
return sum(l) / float(len(l))
|
||||
|
||||
|
||||
def _min(l):
|
||||
if not l:
|
||||
return None
|
||||
return min(l)
|
||||
|
||||
|
||||
def _max(l):
|
||||
if not l:
|
||||
return None
|
||||
return max(l)
|
||||
|
||||
|
||||
class MisReportKpi(models.Model):
|
||||
""" A KPI is an element (ie a line) of a MIS report.
|
||||
|
||||
In addition to a name and description, it has an expression
|
||||
to compute it based on queries defined in the MIS report.
|
||||
It also has various informations defining how to render it
|
||||
(numeric or percentage or a string, a suffix, divider) and
|
||||
how to render comparison of two values of the KPI.
|
||||
KPI's have a sequence and are ordered inside the MIS report.
|
||||
"""
|
||||
|
||||
_name = 'mis.report.kpi'
|
||||
|
||||
name = fields.Char(size=32, required=True,
|
||||
string='Name')
|
||||
description = fields.Char(required=True,
|
||||
string='Description',
|
||||
translate=True)
|
||||
expression = fields.Char(required=True,
|
||||
string='Expression')
|
||||
default_css_style = fields.Char(string='Default CSS style')
|
||||
css_style = fields.Char(string='CSS style expression')
|
||||
type = fields.Selection([('num', _('Numeric')),
|
||||
('pct', _('Percentage')),
|
||||
('str', _('String'))],
|
||||
required=True,
|
||||
string='Type',
|
||||
default='num')
|
||||
divider = fields.Selection([('1e-6', _('µ')),
|
||||
('1e-3', _('m')),
|
||||
('1', _('1')),
|
||||
('1e3', _('k')),
|
||||
('1e6', _('M'))],
|
||||
string='Factor',
|
||||
default='1')
|
||||
dp = fields.Integer(string='Rounding', default=0)
|
||||
suffix = fields.Char(size=16, string='Suffix')
|
||||
compare_method = fields.Selection([('diff', _('Difference')),
|
||||
('pct', _('Percentage')),
|
||||
('none', _('None'))],
|
||||
required=True,
|
||||
string='Comparison Method',
|
||||
default='pct')
|
||||
sequence = fields.Integer(string='Sequence', default=100)
|
||||
report_id = fields.Many2one('mis.report',
|
||||
string='Report',
|
||||
ondelete='cascade')
|
||||
|
||||
_order = 'sequence, id'
|
||||
|
||||
@api.one
|
||||
@api.constrains('name')
|
||||
def _check_name(self):
|
||||
return _is_valid_python_var(self.name)
|
||||
|
||||
@api.onchange('name')
|
||||
def _onchange_name(self):
|
||||
if self.name and not _is_valid_python_var(self.name):
|
||||
return {
|
||||
'warning': {
|
||||
'title': 'Invalid name %s' % self.name,
|
||||
'message': 'The name must be a valid python identifier'
|
||||
}
|
||||
}
|
||||
|
||||
@api.onchange('description')
|
||||
def _onchange_description(self):
|
||||
""" construct name from description """
|
||||
if self.description and not self.name:
|
||||
self.name = _python_var(self.description)
|
||||
|
||||
@api.onchange('type')
|
||||
def _onchange_type(self):
|
||||
if self.type == 'num':
|
||||
self.compare_method = 'pct'
|
||||
self.divider = '1'
|
||||
self.dp = 0
|
||||
elif self.type == 'pct':
|
||||
self.compare_method = 'diff'
|
||||
self.divider = '1'
|
||||
self.dp = 0
|
||||
elif self.type == 'str':
|
||||
self.compare_method = 'none'
|
||||
self.divider = ''
|
||||
self.dp = 0
|
||||
|
||||
def render(self, lang_id, value):
|
||||
""" render a KPI value as a unicode string, ready for display """
|
||||
assert len(self) == 1
|
||||
if value is None:
|
||||
return '#N/A'
|
||||
elif self.type == 'num':
|
||||
return self._render_num(lang_id, value, self.divider,
|
||||
self.dp, self.suffix)
|
||||
elif self.type == 'pct':
|
||||
return self._render_num(lang_id, value, 0.01,
|
||||
self.dp, '%')
|
||||
else:
|
||||
return unicode(value)
|
||||
|
||||
def render_comparison(self, lang_id, value, base_value,
|
||||
average_value, average_base_value):
|
||||
""" render the comparison of two KPI values, ready for display """
|
||||
assert len(self) == 1
|
||||
if value is None or base_value is None:
|
||||
return ''
|
||||
if self.type == 'pct':
|
||||
return self._render_num(
|
||||
lang_id,
|
||||
value - base_value,
|
||||
0.01, self.dp, _('pp'), sign='+')
|
||||
elif self.type == 'num':
|
||||
if average_value:
|
||||
value = value / float(average_value)
|
||||
if average_base_value:
|
||||
base_value = base_value / float(average_base_value)
|
||||
if self.compare_method == 'diff':
|
||||
return self._render_num(
|
||||
lang_id,
|
||||
value - base_value,
|
||||
self.divider, self.dp, self.suffix, sign='+')
|
||||
elif self.compare_method == 'pct':
|
||||
if round(base_value, self.dp) != 0:
|
||||
return self._render_num(
|
||||
lang_id,
|
||||
(value - base_value) / abs(base_value),
|
||||
0.01, self.dp, '%', sign='+')
|
||||
return ''
|
||||
|
||||
def _render_num(self, lang_id, value, divider,
|
||||
dp, suffix, sign='-'):
|
||||
divider_label = _get_selection_label(
|
||||
self._columns['divider'].selection, divider)
|
||||
if divider_label == '1':
|
||||
divider_label = ''
|
||||
# format number following user language
|
||||
value = round(value / float(divider or 1), dp) or 0
|
||||
value = self.env['res.lang'].browse(lang_id).format(
|
||||
'%%%s.%df' % (sign, dp),
|
||||
value,
|
||||
grouping=True)
|
||||
value = u'%s\N{NO-BREAK SPACE}%s%s' % \
|
||||
(value, divider_label, suffix or '')
|
||||
value = value.replace('-', u'\N{NON-BREAKING HYPHEN}')
|
||||
return value
|
||||
|
||||
|
||||
class MisReportQuery(models.Model):
|
||||
""" A query to fetch arbitrary data for a MIS report.
|
||||
|
||||
A query works on a model and has a domain and list of fields to fetch.
|
||||
At runtime, the domain is expanded with a "and" on the date/datetime field.
|
||||
"""
|
||||
|
||||
_name = 'mis.report.query'
|
||||
|
||||
@api.one
|
||||
@api.depends('field_ids')
|
||||
def _compute_field_names(self):
|
||||
field_names = [field.name for field in self.field_ids]
|
||||
self.field_names = ', '.join(field_names)
|
||||
|
||||
name = fields.Char(size=32, required=True,
|
||||
string='Name')
|
||||
model_id = fields.Many2one('ir.model', required=True,
|
||||
string='Model')
|
||||
field_ids = fields.Many2many('ir.model.fields', required=True,
|
||||
string='Fields to fetch')
|
||||
field_names = fields.Char(compute='_compute_field_names',
|
||||
string='Fetched fields name')
|
||||
aggregate = fields.Selection([('sum', _('Sum')),
|
||||
('avg', _('Average')),
|
||||
('min', _('Min')),
|
||||
('max', _('Max'))],
|
||||
string='Aggregate')
|
||||
date_field = fields.Many2one('ir.model.fields', required=True,
|
||||
string='Date field',
|
||||
domain=[('ttype', 'in',
|
||||
('date', 'datetime'))])
|
||||
domain = fields.Char(string='Domain')
|
||||
report_id = fields.Many2one('mis.report', string='Report',
|
||||
ondelete='cascade')
|
||||
|
||||
_order = 'name'
|
||||
|
||||
@api.one
|
||||
@api.constrains('name')
|
||||
def _check_name(self):
|
||||
return _is_valid_python_var(self.name)
|
||||
|
||||
|
||||
class MisReport(models.Model):
|
||||
""" A MIS report template (without period information)
|
||||
|
||||
The MIS report holds:
|
||||
* a list of explicit queries; the result of each query is
|
||||
stored in a variable with same name as a query, containing as list
|
||||
of data structures populated with attributes for each fields to fetch;
|
||||
when queries have an aggregate method and no fields to group, it returns
|
||||
a data structure with the aggregated fields
|
||||
* a list of KPI to be evaluated based on the variables resulting
|
||||
from the accounting data and queries (KPI expressions can references
|
||||
queries and accounting expression - see AccoutingExpressionProcessor)
|
||||
"""
|
||||
|
||||
_name = 'mis.report'
|
||||
|
||||
name = fields.Char(required=True,
|
||||
string='Name', translate=True)
|
||||
description = fields.Char(required=False,
|
||||
string='Description', translate=True)
|
||||
query_ids = fields.One2many('mis.report.query', 'report_id',
|
||||
string='Queries')
|
||||
kpi_ids = fields.One2many('mis.report.kpi', 'report_id',
|
||||
string='KPI\'s')
|
||||
|
||||
# TODO: kpi name cannot be start with query name
|
||||
|
||||
|
||||
class MisReportInstancePeriod(models.Model):
|
||||
""" A MIS report instance has the logic to compute
|
||||
a report template for a given date period.
|
||||
|
||||
Periods have a duration (day, week, fiscal period) and
|
||||
are defined as an offset relative to a pivot date.
|
||||
"""
|
||||
|
||||
@api.one
|
||||
@api.depends('report_instance_id.pivot_date', 'type', 'offset', 'duration')
|
||||
def _compute_dates(self):
|
||||
self.date_from = False
|
||||
self.date_to = False
|
||||
self.period_from = False
|
||||
self.period_to = False
|
||||
self.valid = False
|
||||
d = fields.Date.from_string(self.report_instance_id.pivot_date)
|
||||
if self.type == 'd':
|
||||
date_from = d + datetime.timedelta(days=self.offset)
|
||||
date_to = date_from + \
|
||||
datetime.timedelta(days=self.duration - 1)
|
||||
self.date_from = fields.Date.to_string(date_from)
|
||||
self.date_to = fields.Date.to_string(date_to)
|
||||
self.valid = True
|
||||
elif self.type == 'w':
|
||||
date_from = d - datetime.timedelta(d.weekday())
|
||||
date_from = date_from + datetime.timedelta(days=self.offset * 7)
|
||||
date_to = date_from + \
|
||||
datetime.timedelta(days=(7 * self.duration) - 1)
|
||||
self.date_from = fields.Date.to_string(date_from)
|
||||
self.date_to = fields.Date.to_string(date_to)
|
||||
self.valid = True
|
||||
elif self.type == 'fp':
|
||||
current_periods = self.env['account.period'].search(
|
||||
[('special', '=', False),
|
||||
('date_start', '<=', d),
|
||||
('date_stop', '>=', d),
|
||||
('company_id', '=',
|
||||
self.report_instance_id.company_id.id)])
|
||||
if current_periods:
|
||||
all_periods = self.env['account.period'].search(
|
||||
[('special', '=', False),
|
||||
('company_id', '=',
|
||||
self.report_instance_id.company_id.id)],
|
||||
order='date_start')
|
||||
all_period_ids = [p.id for p in all_periods]
|
||||
p = all_period_ids.index(current_periods[0].id) + self.offset
|
||||
if p >= 0 and p + self.duration <= len(all_period_ids):
|
||||
periods = all_periods[p:p + self.duration]
|
||||
self.date_from = periods[0].date_start
|
||||
self.date_to = periods[-1].date_stop
|
||||
self.period_from = periods[0]
|
||||
self.period_to = periods[-1]
|
||||
self.valid = True
|
||||
|
||||
_name = 'mis.report.instance.period'
|
||||
|
||||
name = fields.Char(size=32, required=True,
|
||||
string='Description', translate=True)
|
||||
type = fields.Selection([('d', _('Day')),
|
||||
('w', _('Week')),
|
||||
('fp', _('Fiscal Period')),
|
||||
# ('fy', _('Fiscal Year'))
|
||||
],
|
||||
required=True,
|
||||
string='Period type')
|
||||
offset = fields.Integer(string='Offset',
|
||||
help='Offset from current period',
|
||||
default=-1)
|
||||
duration = fields.Integer(string='Duration',
|
||||
help='Number of periods',
|
||||
default=1)
|
||||
date_from = fields.Date(compute='_compute_dates', string="From")
|
||||
date_to = fields.Date(compute='_compute_dates', string="To")
|
||||
period_from = fields.Many2one(compute='_compute_dates',
|
||||
comodel_name='account.period',
|
||||
string="From period")
|
||||
period_to = fields.Many2one(compute='_compute_dates',
|
||||
comodel_name='account.period',
|
||||
string="To period")
|
||||
valid = fields.Boolean(compute='_compute_dates',
|
||||
type='boolean',
|
||||
string='Valid')
|
||||
sequence = fields.Integer(string='Sequence', default=100)
|
||||
report_instance_id = fields.Many2one('mis.report.instance',
|
||||
string='Report Instance',
|
||||
ondelete='cascade')
|
||||
comparison_column_ids = fields.Many2many(
|
||||
comodel_name='mis.report.instance.period',
|
||||
relation='mis_report_instance_period_rel',
|
||||
column1='period_id',
|
||||
column2='compare_period_id',
|
||||
string='Compare with')
|
||||
normalize_factor = fields.Integer(
|
||||
string='Factor',
|
||||
help='Factor to use to normalize the period (used in comparison',
|
||||
default=1)
|
||||
|
||||
_order = 'sequence, id'
|
||||
|
||||
_sql_constraints = [
|
||||
('duration', 'CHECK (duration>0)',
|
||||
'Wrong duration, it must be positive!'),
|
||||
('normalize_factor', 'CHECK (normalize_factor>0)',
|
||||
'Wrong normalize factor, it must be positive!'),
|
||||
('name_unique', 'unique(name, report_instance_id)',
|
||||
'Period name should be unique by report'),
|
||||
]
|
||||
|
||||
@api.multi
|
||||
def drilldown(self, expr):
|
||||
assert len(self) == 1
|
||||
if AEP.has_account_var(expr):
|
||||
aep = AEP(self.env)
|
||||
aep.parse_expr(expr)
|
||||
aep.done_parsing(self.report_instance_id.root_account)
|
||||
domain = aep.get_aml_domain_for_expr(
|
||||
expr,
|
||||
self.date_from, self.date_to,
|
||||
self.period_from, self.period_to,
|
||||
self.report_instance_id.target_move)
|
||||
return {
|
||||
'name': expr + ' - ' + self.name,
|
||||
'domain': domain,
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'account.move.line',
|
||||
'views': [[False, 'list'], [False, 'form']],
|
||||
'view_type': 'list',
|
||||
'view_mode': 'list',
|
||||
'target': 'current',
|
||||
}
|
||||
else:
|
||||
return False
|
||||
|
||||
def _fetch_queries(self):
|
||||
assert len(self) == 1
|
||||
res = {}
|
||||
for query in self.report_instance_id.report_id.query_ids:
|
||||
model = self.env[query.model_id.model]
|
||||
eval_context = {
|
||||
'env': self.env,
|
||||
'time': time,
|
||||
'datetime': datetime,
|
||||
'dateutil': dateutil,
|
||||
# deprecated
|
||||
'uid': self.env.uid,
|
||||
'context': self.env.context,
|
||||
}
|
||||
domain = query.domain and \
|
||||
safe_eval(query.domain, eval_context) or []
|
||||
if query.date_field.ttype == 'date':
|
||||
domain.extend([(query.date_field.name, '>=', self.date_from),
|
||||
(query.date_field.name, '<=', self.date_to)])
|
||||
else:
|
||||
datetime_from = _utc_midnight(
|
||||
self.date_from, self._context.get('tz', 'UTC'))
|
||||
datetime_to = _utc_midnight(
|
||||
self.date_to, self._context.get('tz', 'UTC'), add_day=1)
|
||||
domain.extend([(query.date_field.name, '>=', datetime_from),
|
||||
(query.date_field.name, '<', datetime_to)])
|
||||
field_names = [f.name for f in query.field_ids]
|
||||
if not query.aggregate:
|
||||
data = model.search_read(domain, field_names)
|
||||
res[query.name] = [AutoStruct(**d) for d in data]
|
||||
elif query.aggregate == 'sum':
|
||||
data = model.read_group(
|
||||
domain, field_names, [])
|
||||
s = AutoStruct(count=data[0]['__count'])
|
||||
for field_name in field_names:
|
||||
v = data[0][field_name]
|
||||
setattr(s, field_name, v)
|
||||
res[query.name] = s
|
||||
else:
|
||||
data = model.search_read(domain, field_names)
|
||||
s = AutoStruct(count=len(data))
|
||||
if query.aggregate == 'min':
|
||||
agg = _min
|
||||
elif query.aggregate == 'max':
|
||||
agg = _max
|
||||
elif query.aggregate == 'avg':
|
||||
agg = _avg
|
||||
for field_name in field_names:
|
||||
setattr(s, field_name,
|
||||
agg([d[field_name] for d in data]))
|
||||
res[query.name] = s
|
||||
return res
|
||||
|
||||
def _compute(self, lang_id, aep):
|
||||
res = {}
|
||||
|
||||
localdict = {
|
||||
'registry': self.pool,
|
||||
'sum': _sum,
|
||||
'min': _min,
|
||||
'max': _max,
|
||||
'len': len,
|
||||
'avg': _avg,
|
||||
}
|
||||
|
||||
localdict.update(self._fetch_queries())
|
||||
|
||||
aep.do_queries(self.date_from, self.date_to,
|
||||
self.period_from, self.period_to,
|
||||
self.report_instance_id.target_move)
|
||||
|
||||
compute_queue = self.report_instance_id.report_id.kpi_ids
|
||||
recompute_queue = []
|
||||
while True:
|
||||
for kpi in compute_queue:
|
||||
try:
|
||||
kpi_val_comment = kpi.name + " = " + kpi.expression
|
||||
kpi_eval_expression = aep.replace_expr(kpi.expression)
|
||||
kpi_val = safe_eval(kpi_eval_expression, localdict)
|
||||
except ZeroDivisionError:
|
||||
kpi_val = None
|
||||
kpi_val_rendered = '#DIV/0'
|
||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||
except (NameError, ValueError):
|
||||
recompute_queue.append(kpi)
|
||||
kpi_val = None
|
||||
kpi_val_rendered = '#ERR'
|
||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||
except:
|
||||
kpi_val = None
|
||||
kpi_val_rendered = '#ERR'
|
||||
kpi_val_comment += '\n\n%s' % (traceback.format_exc(),)
|
||||
else:
|
||||
kpi_val_rendered = kpi.render(lang_id, kpi_val)
|
||||
|
||||
localdict[kpi.name] = kpi_val
|
||||
try:
|
||||
kpi_style = None
|
||||
if kpi.css_style:
|
||||
kpi_style = safe_eval(kpi.css_style, localdict)
|
||||
except:
|
||||
_logger.warning("error evaluating css stype expression %s",
|
||||
kpi.css_style, exc_info=True)
|
||||
kpi_style = None
|
||||
|
||||
drilldown = (kpi_val is not None and
|
||||
AEP.has_account_var(kpi.expression))
|
||||
|
||||
res[kpi.name] = {
|
||||
'val': kpi_val,
|
||||
'val_r': kpi_val_rendered,
|
||||
'val_c': kpi_val_comment,
|
||||
'style': kpi_style,
|
||||
'suffix': kpi.suffix,
|
||||
'dp': kpi.dp,
|
||||
'is_percentage': kpi.type == 'pct',
|
||||
'period_id': self.id,
|
||||
'expr': kpi.expression,
|
||||
'drilldown': drilldown,
|
||||
}
|
||||
|
||||
if len(recompute_queue) == 0:
|
||||
# nothing to recompute, we are done
|
||||
break
|
||||
if len(recompute_queue) == len(compute_queue):
|
||||
# could not compute anything in this iteration
|
||||
# (ie real Value errors or cyclic dependency)
|
||||
# so we stop trying
|
||||
break
|
||||
# try again
|
||||
compute_queue = recompute_queue
|
||||
recompute_queue = []
|
||||
|
||||
return res
|
||||
|
||||
|
||||
class MisReportInstance(models.Model):
|
||||
"""The MIS report instance combines everything to compute
|
||||
a MIS report template for a set of periods."""
|
||||
|
||||
@api.one
|
||||
@api.depends('date')
|
||||
def _compute_pivot_date(self):
|
||||
if self.date:
|
||||
self.pivot_date = self.date
|
||||
else:
|
||||
self.pivot_date = fields.Date.context_today(self)
|
||||
|
||||
_name = 'mis.report.instance'
|
||||
|
||||
name = fields.Char(required=True,
|
||||
string='Name', translate=True)
|
||||
description = fields.Char(required=False,
|
||||
string='Description', translate=True)
|
||||
date = fields.Date(string='Base date',
|
||||
help='Report base date '
|
||||
'(leave empty to use current date)')
|
||||
pivot_date = fields.Date(compute='_compute_pivot_date',
|
||||
string="Pivot date")
|
||||
report_id = fields.Many2one('mis.report',
|
||||
required=True,
|
||||
string='Report')
|
||||
period_ids = fields.One2many('mis.report.instance.period',
|
||||
'report_instance_id',
|
||||
required=True,
|
||||
string='Periods')
|
||||
target_move = fields.Selection([('posted', 'All Posted Entries'),
|
||||
('all', 'All Entries')],
|
||||
string='Target Moves',
|
||||
required=True,
|
||||
default='posted')
|
||||
company_id = fields.Many2one(comodel_name='res.company',
|
||||
string='Company',
|
||||
readonly=True,
|
||||
related='root_account.company_id',
|
||||
store=True)
|
||||
root_account = fields.Many2one(comodel_name='account.account',
|
||||
domain='[("parent_id", "=", False)]',
|
||||
string="Account chart",
|
||||
required=True)
|
||||
landscape_pdf = fields.Boolean(string='Landscape PDF')
|
||||
|
||||
def _format_date(self, lang_id, date):
|
||||
# format date following user language
|
||||
date_format = self.env['res.lang'].browse(lang_id).date_format
|
||||
return datetime.datetime.strftime(
|
||||
fields.Date.from_string(date), date_format)
|
||||
|
||||
@api.multi
|
||||
def preview(self):
|
||||
assert len(self) == 1
|
||||
view_id = self.env.ref('mis_builder.'
|
||||
'mis_report_instance_result_view_form')
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'res_model': 'mis.report.instance',
|
||||
'res_id': self.id,
|
||||
'view_mode': 'form',
|
||||
'view_type': 'form',
|
||||
'view_id': view_id.id,
|
||||
'target': 'new',
|
||||
}
|
||||
|
||||
@api.multi
|
||||
def compute(self):
|
||||
assert len(self) == 1
|
||||
|
||||
# prepare AccountingExpressionProcessor
|
||||
aep = AEP(self.env)
|
||||
for kpi in self.report_id.kpi_ids:
|
||||
aep.parse_expr(kpi.expression)
|
||||
aep.done_parsing(self.root_account)
|
||||
|
||||
# fetch user language only once
|
||||
# TODO: is this necessary?
|
||||
lang = self.env.user.lang
|
||||
if not lang:
|
||||
lang = 'en_US'
|
||||
lang_id = self.env['res.lang'].search([('code', '=', lang)]).id
|
||||
|
||||
# compute kpi values for each period
|
||||
kpi_values_by_period_ids = {}
|
||||
for period in self.period_ids:
|
||||
if not period.valid:
|
||||
continue
|
||||
kpi_values = period._compute(lang_id, aep)
|
||||
kpi_values_by_period_ids[period.id] = kpi_values
|
||||
|
||||
# prepare header and content
|
||||
header = []
|
||||
header.append({
|
||||
'kpi_name': '',
|
||||
'cols': []
|
||||
})
|
||||
content = []
|
||||
rows_by_kpi_name = {}
|
||||
for kpi in self.report_id.kpi_ids:
|
||||
rows_by_kpi_name[kpi.name] = {
|
||||
'kpi_name': kpi.description,
|
||||
'cols': [],
|
||||
'default_style': kpi.default_css_style
|
||||
}
|
||||
content.append(rows_by_kpi_name[kpi.name])
|
||||
|
||||
# populate header and content
|
||||
for period in self.period_ids:
|
||||
if not period.valid:
|
||||
continue
|
||||
# add the column header
|
||||
if period.duration > 1 or period.type == 'w':
|
||||
# from, to
|
||||
if period.period_from and period.period_to:
|
||||
date_from = period.period_from.name
|
||||
date_to = period.period_to.name
|
||||
else:
|
||||
date_from = self._format_date(lang_id, period.date_from)
|
||||
date_to = self._format_date(lang_id, period.date_to)
|
||||
header_date = _('from %s to %s') % (date_from, date_to)
|
||||
else:
|
||||
# one period or one day
|
||||
if period.period_from and period.period_to:
|
||||
header_date = period.period_from.name
|
||||
else:
|
||||
header_date = self._format_date(lang_id, period.date_from)
|
||||
header[0]['cols'].append(dict(name=period.name, date=header_date))
|
||||
# add kpi values
|
||||
kpi_values = kpi_values_by_period_ids[period.id]
|
||||
for kpi_name in kpi_values:
|
||||
rows_by_kpi_name[kpi_name]['cols'].append(kpi_values[kpi_name])
|
||||
|
||||
# add comparison columns
|
||||
for compare_col in period.comparison_column_ids:
|
||||
compare_kpi_values = \
|
||||
kpi_values_by_period_ids.get(compare_col.id)
|
||||
if compare_kpi_values:
|
||||
# add the comparison column header
|
||||
header[0]['cols'].append(
|
||||
dict(name=_('%s vs %s') % (period.name,
|
||||
compare_col.name),
|
||||
date=''))
|
||||
# add comparison values
|
||||
for kpi in self.report_id.kpi_ids:
|
||||
rows_by_kpi_name[kpi.name]['cols'].append({
|
||||
'val_r': kpi.render_comparison(
|
||||
lang_id,
|
||||
kpi_values[kpi.name]['val'],
|
||||
compare_kpi_values[kpi.name]['val'],
|
||||
period.normalize_factor,
|
||||
compare_col.normalize_factor)
|
||||
})
|
||||
|
||||
return {'header': header,
|
||||
'content': content}
|
|
@ -0,0 +1,26 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import mis_builder_xls
|
||||
from . import report_mis_report_instance
|
|
@ -0,0 +1,138 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import xlwt
|
||||
from openerp.report import report_sxw
|
||||
from openerp.addons.report_xls.report_xls import report_xls
|
||||
import logging
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class mis_builder_xls_parser(report_sxw.rml_parse):
|
||||
|
||||
def __init__(self, cr, uid, name, context):
|
||||
super(mis_builder_xls_parser, self).__init__(
|
||||
cr, uid, name, context=context)
|
||||
self.context = context
|
||||
|
||||
|
||||
class mis_builder_xls(report_xls):
|
||||
|
||||
def __init__(self, name, table, rml=False, parser=False, header=True,
|
||||
store=False):
|
||||
super(mis_builder_xls, self).__init__(
|
||||
name, table, rml, parser, header, store)
|
||||
|
||||
# Cell Styles
|
||||
_xs = self.xls_styles
|
||||
# header
|
||||
rh_cell_format = _xs['bold'] + _xs['fill'] + \
|
||||
_xs['borders_all'] + _xs['right']
|
||||
self.rh_cell_style = xlwt.easyxf(rh_cell_format)
|
||||
self.rh_cell_style_date = xlwt.easyxf(
|
||||
rh_cell_format, num_format_str=report_xls.date_format)
|
||||
# lines
|
||||
self.mis_rh_cell_style = xlwt.easyxf(
|
||||
_xs['borders_all'] + _xs['bold'] + _xs['fill'])
|
||||
|
||||
def generate_xls_report(self, _p, _xs, data, objects, wb):
|
||||
|
||||
report_name = objects[0].name
|
||||
ws = wb.add_sheet(report_name[:31])
|
||||
ws.panes_frozen = True
|
||||
ws.remove_splits = True
|
||||
ws.portrait = 0 # Landscape
|
||||
ws.fit_width_to_pages = 1
|
||||
row_pos = 0
|
||||
|
||||
# set print header/footer
|
||||
ws.header_str = self.xls_headers['standard']
|
||||
ws.footer_str = self.xls_footers['standard']
|
||||
|
||||
# Title
|
||||
c_specs = [
|
||||
('report_name', 1, 0, 'text', report_name),
|
||||
]
|
||||
row_data = self.xls_row_template(c_specs, ['report_name'])
|
||||
row_pos = self.xls_write_row(
|
||||
ws, row_pos, row_data, row_style=xlwt.easyxf(_xs['xls_title']))
|
||||
row_pos += 1
|
||||
|
||||
# get the computed result of the report
|
||||
data = self.pool.get('mis.report.instance').compute(
|
||||
self.cr, self.uid, objects[0].id)
|
||||
|
||||
# Column headers
|
||||
header_name_list = ['']
|
||||
col_specs_template = {'': {'header': [1, 30, 'text', ''],
|
||||
'header_date': [1, 1, 'text', '']}}
|
||||
for col in data['header'][0]['cols']:
|
||||
col_specs_template[col['name']] = {'header': [1, 30, 'text',
|
||||
col['name']],
|
||||
'header_date': [1, 1, 'text',
|
||||
col['date']]}
|
||||
header_name_list.append(col['name'])
|
||||
c_specs = map(
|
||||
lambda x: self.render(x, col_specs_template, 'header'),
|
||||
header_name_list)
|
||||
row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs])
|
||||
row_pos = self.xls_write_row(
|
||||
ws, row_pos, row_data, row_style=self.rh_cell_style,
|
||||
set_column_size=True)
|
||||
c_specs = map(lambda x: self.render(
|
||||
x, col_specs_template, 'header_date'), header_name_list)
|
||||
row_data = self.xls_row_template(c_specs, [x[0] for x in c_specs])
|
||||
row_pos = self.xls_write_row(
|
||||
ws, row_pos, row_data, row_style=self.rh_cell_style_date)
|
||||
|
||||
ws.set_horz_split_pos(row_pos)
|
||||
ws.set_vert_split_pos(1)
|
||||
|
||||
for line in data['content']:
|
||||
col = 0
|
||||
ws.write(row_pos, col, line['kpi_name'], self.mis_rh_cell_style)
|
||||
for value in line['cols']:
|
||||
col += 1
|
||||
num_format_str = '#'
|
||||
if value.get('dp'):
|
||||
num_format_str += '.'
|
||||
num_format_str += '0' * int(value['dp'])
|
||||
if value.get('suffix'):
|
||||
num_format_str = num_format_str + ' "%s"' % value['suffix']
|
||||
kpi_cell_style = xlwt.easyxf(
|
||||
_xs['borders_all'] + _xs['right'],
|
||||
num_format_str=num_format_str)
|
||||
if value.get('val'):
|
||||
val = value['val']
|
||||
if value.get('is_percentage'):
|
||||
val = val / 0.01
|
||||
ws.write(row_pos, col, val, kpi_cell_style)
|
||||
else:
|
||||
ws.write(row_pos, col, value['val_r'], kpi_cell_style)
|
||||
row_pos += 1
|
||||
|
||||
|
||||
mis_builder_xls('report.mis.report.instance.xls',
|
||||
'mis.report.instance',
|
||||
parser=mis_builder_xls_parser)
|
|
@ -0,0 +1,67 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import logging
|
||||
|
||||
from openerp import api, models
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class ReportMisReportInstance(models.AbstractModel):
|
||||
|
||||
_name = 'report.mis_builder.report_mis_report_instance'
|
||||
|
||||
@api.multi
|
||||
def render_html(self, data=None):
|
||||
docs = self.env['mis.report.instance'].browse(self._ids)
|
||||
docs_computed = {}
|
||||
for doc in docs:
|
||||
docs_computed[doc.id] = doc.compute()
|
||||
docargs = {
|
||||
'doc_ids': self._ids,
|
||||
'doc_model': 'mis.report.instance',
|
||||
'docs': docs,
|
||||
'docs_computed': docs_computed,
|
||||
}
|
||||
return self.env['report'].\
|
||||
render('mis_builder.report_mis_report_instance', docargs)
|
||||
|
||||
|
||||
class Report(models.Model):
|
||||
_inherit = "report"
|
||||
|
||||
@api.v7
|
||||
def get_pdf(self, cr, uid, ids, report_name, html=None, data=None,
|
||||
context=None):
|
||||
if ids:
|
||||
report = self._get_report_from_name(cr, uid, report_name)
|
||||
obj = self.pool[report.model].browse(cr, uid, ids,
|
||||
context=context)[0]
|
||||
context = context.copy()
|
||||
if hasattr(obj, 'landscape_pdf') and obj.landscape_pdf:
|
||||
context.update({'landscape': True})
|
||||
return super(Report, self).get_pdf(cr, uid, ids, report_name,
|
||||
html=html, data=data,
|
||||
context=context)
|
|
@ -0,0 +1,55 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<template id="report_mis_report_instance">
|
||||
<t t-call="report.html_container">
|
||||
<t t-foreach="docs" t-as="o">
|
||||
<t t-call="report.internal_layout">
|
||||
<div class="page">
|
||||
<h2 t-field="o.name"></h2>
|
||||
<table class="table table-condensed">
|
||||
<thead>
|
||||
<tr>
|
||||
<t t-foreach="docs_computed[o.id]['header']" t-as="h">
|
||||
<th>
|
||||
<div>
|
||||
<t t-esc="h_value['kpi_name']"/>
|
||||
</div>
|
||||
</th>
|
||||
<th t-foreach="h_value['cols']" t-as="col" class="text-center">
|
||||
<div>
|
||||
<t t-esc="col['name']"/>
|
||||
</div>
|
||||
<div>
|
||||
<t t-esc="col['date']"/>
|
||||
</div>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="docs_computed[o.id]['content']" t-as="c">
|
||||
<td t-att-style="c_value['default_style']">
|
||||
<div class="text-left">
|
||||
<t t-esc="c_value['kpi_name']"/>
|
||||
</div>
|
||||
</td>
|
||||
<t t-foreach="c_value['cols']" t-as="value">
|
||||
<td t-att-style="c_value['default_style']">
|
||||
<div t-att-style="value_value.get('style')" class="text-right">
|
||||
<t t-esc="value_value['val_r']"/>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</t>
|
||||
</t>
|
||||
</t>
|
||||
</template>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,11 @@
|
|||
"id","name","model_id:id","group_id:id","perm_read","perm_write","perm_create","perm_unlink"
|
||||
manage_mis_report_kpi,manage_mis_report_kpi,model_mis_report_kpi,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_kpi,access_mis_report_kpi,model_mis_report_kpi,base.group_user,1,0,0,0
|
||||
manage_mis_report_query,manage_mis_report_query,model_mis_report_query,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_query,access_mis_report_query,model_mis_report_query,base.group_user,1,0,0,0
|
||||
manage_mis_report,manage_mis_report,model_mis_report,account.group_account_manager,1,1,1,1
|
||||
access_mis_report,access_mis_report,model_mis_report,base.group_user,1,0,0,0
|
||||
manage_mis_report_instance_period,manage_mis_report_instance_period,model_mis_report_instance_period,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_instance_period,access_mis_report_instance_period,model_mis_report_instance_period,base.group_user,1,0,0,0
|
||||
manage_mis_report_instance,manage_mis_report_instance,model_mis_report_instance,account.group_account_manager,1,1,1,1
|
||||
access_mis_report_instance,access_mis_report_instance,model_mis_report_instance,base.group_user,1,0,0,0
|
|
|
@ -0,0 +1,13 @@
|
|||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<openerp>
|
||||
<data noupdate="0">
|
||||
|
||||
<record id="mis_builder_multi_company_rule" model="ir.rule">
|
||||
<field name="name">Mis Builder multi company</field>
|
||||
<field name="model_id" ref="model_mis_report_instance"/>
|
||||
<field name="global" eval="True"/>
|
||||
<field name="domain_force">['|',('company_id','=',False),('company_id','child_of',[user.company_id.id])]</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
Binary file not shown.
After Width: | Height: | Size: 34 KiB |
Binary file not shown.
After Width: | Height: | Size: 81 KiB |
Binary file not shown.
After Width: | Height: | Size: 98 KiB |
Binary file not shown.
After Width: | Height: | Size: 9.2 KiB |
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 45 KiB |
|
@ -0,0 +1,13 @@
|
|||
.openerp .mis_builder_ralign {
|
||||
text-align: right;
|
||||
}
|
||||
|
||||
.openerp .mis_builder a {
|
||||
/* we don't want the link color, to respect user styles */
|
||||
color: inherit;
|
||||
}
|
||||
|
||||
.openerp .mis_builder a:hover {
|
||||
/* underline links on hover to give a visual cue */
|
||||
text-decoration: underline;
|
||||
}
|
Binary file not shown.
After Width: | Height: | Size: 3.4 KiB |
|
@ -0,0 +1,48 @@
|
|||
openerp.mis_builder = function(instance) {
|
||||
|
||||
instance.mis_builder.MisReport = instance.web.form.FormWidget.extend({
|
||||
template: "mis_builder.MisReport",
|
||||
|
||||
init: function() {
|
||||
this._super.apply(this, arguments);
|
||||
this.mis_report_data = null;
|
||||
},
|
||||
|
||||
start: function() {
|
||||
this._super.apply(this, arguments);
|
||||
var self = this;
|
||||
new instance.web.Model("mis.report.instance").call(
|
||||
"compute",
|
||||
[self.getParent().dataset.context.active_id],
|
||||
{'context': new instance.web.CompoundContext()}
|
||||
).then(function(result){
|
||||
self.mis_report_data = result;
|
||||
self.renderElement();
|
||||
});
|
||||
},
|
||||
|
||||
events: {
|
||||
"click a.mis_builder_drilldown": "drilldown",
|
||||
},
|
||||
|
||||
drilldown: function(event) {
|
||||
var self = this;
|
||||
var drilldown = JSON.parse($(event.target).data("drilldown"));
|
||||
if (drilldown) {
|
||||
var period_id = JSON.parse($(event.target).data("period-id"));
|
||||
var val_c = JSON.parse($(event.target).data("expr"));
|
||||
new instance.web.Model("mis.report.instance.period").call(
|
||||
"drilldown",
|
||||
[period_id, val_c],
|
||||
{'context': new instance.web.CompoundContext()}
|
||||
).then(function(result) {
|
||||
if (result) {
|
||||
self.do_action(result);
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
instance.web.form.custom_widgets.add('mis_report', 'instance.mis_builder.MisReport');
|
||||
}
|
|
@ -0,0 +1,62 @@
|
|||
<template>
|
||||
<t t-name="mis_builder.MisReport">
|
||||
<p> </p>
|
||||
<table t-if="widget.mis_report_data" class="oe_list_content mis_builder">
|
||||
<thead>
|
||||
<tr class="oe_list_header_columns">
|
||||
<t t-foreach="widget.mis_report_data.header" t-as="h">
|
||||
<th class="oe_list_header_char">
|
||||
<div>
|
||||
<t t-esc="h_value.kpi_name"/>
|
||||
</div>
|
||||
</th>
|
||||
<th t-foreach="h_value.cols" t-as="col" class="oe_list_header_char mis_builder_ralign">
|
||||
<div>
|
||||
<t t-esc="col.name"/>
|
||||
</div>
|
||||
<div>
|
||||
<t t-esc="col.date"/>
|
||||
</div>
|
||||
</th>
|
||||
</t>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr t-foreach="widget.mis_report_data.content" t-as="c">
|
||||
<td t-att="{'style': c_value.default_style}">
|
||||
<div>
|
||||
<t t-esc="c_value.kpi_name"/>
|
||||
</div>
|
||||
</td>
|
||||
<t t-foreach="c_value.cols" t-as="value">
|
||||
<td t-att="{'style': c_value.default_style}" class="mis_builder_ralign">
|
||||
<div t-att="{'style': value_value.style, 'title': value_value.val_c}">
|
||||
<t t-if="value_value.drilldown">
|
||||
<a href="javascript:void(0)"
|
||||
class="mis_builder_drilldown"
|
||||
t-att-data-drilldown="JSON.stringify(value_value.drilldown)"
|
||||
t-att-data-period-id="JSON.stringify(value_value.period_id)"
|
||||
t-att-data-expr="JSON.stringify(value_value.expr)"
|
||||
>
|
||||
<t t-esc="value_value.val_r"/>
|
||||
</a>
|
||||
</t>
|
||||
<t t-if="!value_value.drilldown">
|
||||
<t t-esc="value_value.val_r"/>
|
||||
</t>
|
||||
</div>
|
||||
</td>
|
||||
</t>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tfoot>
|
||||
<tr>
|
||||
<td class="oe_list_footer" />
|
||||
<t t-foreach="widget.mis_report_data.header" t-as="f">
|
||||
<td t-foreach="f_value.cols" class="oe_list_footer" />
|
||||
</t>
|
||||
</tr>
|
||||
</tfoot>
|
||||
</table>
|
||||
</t>
|
||||
</template>
|
|
@ -0,0 +1,25 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import test_mis_builder
|
|
@ -0,0 +1,2 @@
|
|||
"id","description","kpi_ids/id","name","query_ids/id"
|
||||
"mis_report_test","","mis_report_kpi_test","Test report","mis_report_query_test"
|
|
|
@ -0,0 +1,2 @@
|
|||
"id","date","description","name","period_ids/id","report_id/id","root_account/id"
|
||||
"mis_report_instance_test","2014-07-31","","Test-report-instance without company","mis_report_instance_period_test","mis_report_test","account.chart0"
|
|
|
@ -0,0 +1,2 @@
|
|||
"id","duration","name","offset","type","sequence"
|
||||
"mis_report_instance_period_test","1","today","","Day",""
|
|
|
@ -0,0 +1,2 @@
|
|||
"id","compare_method","description","expression","divider","name","dp","sequence","type","suffix"
|
||||
"mis_report_kpi_test","Percentage","total test","len(test)","","total_test","","1","Numeric",""
|
|
|
@ -0,0 +1,2 @@
|
|||
"id","date_field/id","domain","field_ids/id","model_id/id","name"
|
||||
"mis_report_query_test","account.field_account_analytic_balance_date1","","account.field_account_analytic_balance_empty_acc","account.model_account_analytic_balance","test"
|
|
|
@ -0,0 +1,82 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
import openerp.tests.common as common
|
||||
|
||||
from ..models import mis_builder
|
||||
|
||||
|
||||
class test_mis_builder(common.TransactionCase):
|
||||
|
||||
def setUp(self):
|
||||
super(test_mis_builder, self).setUp()
|
||||
|
||||
def test_datetime_conversion(self):
|
||||
date_to_convert = '2014-07-05'
|
||||
date_time_convert = mis_builder._utc_midnight(
|
||||
date_to_convert, 'Europe/Brussels')
|
||||
self.assertEqual(date_time_convert, '2014-07-04 22:00:00',
|
||||
'The converted date time convert must contains hour')
|
||||
date_time_convert = mis_builder._utc_midnight(
|
||||
date_to_convert, 'Europe/Brussels', add_day=1)
|
||||
self.assertEqual(date_time_convert, '2014-07-05 22:00:00',
|
||||
'The converted date time convert must contains hour')
|
||||
date_time_convert = mis_builder._utc_midnight(
|
||||
date_to_convert, 'US/Pacific')
|
||||
self.assertEqual(date_time_convert, '2014-07-05 07:00:00',
|
||||
'The converted date time convert must contains hour')
|
||||
date_time_convert = mis_builder._utc_midnight(
|
||||
date_to_convert, 'US/Pacific', add_day=1)
|
||||
self.assertEqual(date_time_convert, '2014-07-06 07:00:00',
|
||||
'The converted date time convert must contains hour')
|
||||
|
||||
def test_fetch_query(self):
|
||||
# create a report on a model without company_id field :
|
||||
# account.analytic.balance
|
||||
data = self.registry('mis.report.instance').compute(
|
||||
self.cr, self.uid,
|
||||
self.ref('mis_builder.mis_report_instance_test'))
|
||||
self.assertDictContainsSubset(
|
||||
{'content':
|
||||
[{'kpi_name': u'total test',
|
||||
'default_style': False,
|
||||
'cols': [{'period_id': self.ref('mis_builder.'
|
||||
'mis_report_instance_'
|
||||
'period_test'),
|
||||
'style': None,
|
||||
'suffix': False,
|
||||
'expr': 'len(test)',
|
||||
'val_c': 'total_test = len(test)',
|
||||
'val': 0,
|
||||
'val_r': u'0\xa0',
|
||||
'is_percentage': False,
|
||||
'dp': 0,
|
||||
'drilldown': False}]
|
||||
}],
|
||||
'header':
|
||||
[{'kpi_name': '',
|
||||
'cols': [{'date': '07/31/2014',
|
||||
'name': u'today'}]
|
||||
}],
|
||||
}, data)
|
|
@ -0,0 +1,211 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<template id="assets_backend" name="mis_builder" inherit_id="web.assets_backend">
|
||||
<xpath expr="." position="inside">
|
||||
<link rel="stylesheet" href="/mis_builder/static/src/css/custom.css"/>
|
||||
<script type="text/javascript" src="/mis_builder/static/src/js/mis_builder.js"></script>
|
||||
</xpath>
|
||||
</template>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_view_tree">
|
||||
<field name="name">mis.report.view.tree</field>
|
||||
<field name="model">mis.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="MIS Reports">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_view_form">
|
||||
<field name="name">mis.report.view.form</field>
|
||||
<field name="model">mis.report</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report" version="7.0">
|
||||
<sheet>
|
||||
<group col="2">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="query_ids">
|
||||
<tree string="Queries" editable="bottom">
|
||||
<field name="name"/>
|
||||
<field name="model_id"/>
|
||||
<field name="field_ids" domain="[('model_id', '=', model_id)]" widget="many2many_tags"/>
|
||||
<field name="field_names"/>
|
||||
<field name="aggregate"/>
|
||||
<field name="date_field" domain="[('model_id', '=', model_id), ('ttype', 'in', ('date', 'datetime'))]"/>
|
||||
<field name="domain"/>
|
||||
</tree>
|
||||
</field>
|
||||
<field name="kpi_ids">
|
||||
<tree string="KPI's" editable="bottom">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="description"/>
|
||||
<field name="name"/>
|
||||
<field name="expression"/>
|
||||
<field name="type"/>
|
||||
<field name="dp" attrs="{'invisible': [('type', '=', 'str')]}"/>
|
||||
<field name="divider" attrs="{'invisible': [('type', '=', 'str')]}"/>
|
||||
<field name="suffix"/>
|
||||
<field name="compare_method" attrs="{'invisible': [('type', '=', 'str')]}"/>
|
||||
<field name="default_css_style"/>
|
||||
<field name="css_style"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
<group col="2" string="Legend (for expression)">
|
||||
<group>
|
||||
<label colspan="2" string="Expressions are of the form <field><mode>[accounts][domain]"/>
|
||||
<label colspan="2" string="Possible values for 'field' can be:"/>
|
||||
<group>
|
||||
<label colspan="2" string="* bal for balance (debit - credit)"/>
|
||||
<label colspan="2" string="* crd for credit"/>
|
||||
<label colspan="2" string="* deb for debit"/>
|
||||
</group>
|
||||
<label colspan="2" string="Possible values for 'mode' are:"/>
|
||||
<group>
|
||||
<label colspan="2" string="* nothing or p: variation over the period"/>
|
||||
<label colspan="2" string="* i: at the beginning of the period"/>
|
||||
<label colspan="2" string="* e: at the end of the period"/>
|
||||
</group>
|
||||
<label colspan="2" string="'accounts' is a comma-separated list of account codes, possibly containing %% wildcards"/>
|
||||
<label colspan="2" string="'domain' is an optional filter on move lines (eg to filter on analytic accounts or journal)"/>
|
||||
</group>
|
||||
<group>
|
||||
<label colspan="2" string="Examples"/>
|
||||
<group>
|
||||
<label colspan="2" string="* bal[70]: variation of the balance of account 70 over the period (it is the same as balp[70]);"/>
|
||||
<label colspan="2" string="* bali[70,60]: initial balance of accounts 70 and 60;"/>
|
||||
<label colspan="2" string="* bale[1%%]: balance of accounts starting with 1 at end of period."/>
|
||||
</group>
|
||||
</group>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mis_report_view_action">
|
||||
<field name="name">MIS Report Templates</field>
|
||||
<field name="view_id" ref="mis_report_view_tree"/>
|
||||
<field name="res_model">mis.report</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="mis_report_view_menu" parent="account.menu_account_reports" name="MIS Report Templates" action="mis_report_view_action" sequence="21"/>
|
||||
|
||||
<record id="xls_export" model="ir.actions.report.xml">
|
||||
<field name="name">MIS report instance XLS report</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="type">ir.actions.report.xml</field>
|
||||
<field name="report_name">mis.report.instance.xls</field>
|
||||
<field name="report_type">xls</field>
|
||||
<field name="auto" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record id="qweb_pdf_export" model="ir.actions.report.xml">
|
||||
<field name="name">MIS report instance QWEB PDF report</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="type">ir.actions.report.xml</field>
|
||||
<field name="report_name">mis_builder.report_mis_report_instance</field>
|
||||
<field name="report_type">qweb-pdf</field>
|
||||
<field name="auto" eval="False"/>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_instance_result_view_form">
|
||||
<field name="name">mis.report.instance.result.view.form</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="priority" eval="17"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report Result" version="7.0">
|
||||
<widget type="mis_report"></widget>
|
||||
<button icon="gtk-print" name="%(qweb_pdf_export)d" string="Print" type="action" colspan="2"/>
|
||||
<button icon="gtk-execute" name="%(xls_export)d" string="Export" type="action" colspan="2"/>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_instance_view_tree">
|
||||
<field name="name">mis.report.instance.view.tree</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="MIS Report Instances">
|
||||
<field name="name"/>
|
||||
<field name="description"/>
|
||||
<field name="report_id"/>
|
||||
<field name="target_move"/>
|
||||
<field name="pivot_date"/>
|
||||
<field name="company_id"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_instance_view_form">
|
||||
<field name="name">mis.report.instance.view.form</field>
|
||||
<field name="model">mis.report.instance</field>
|
||||
<field name="priority" eval="16"/>
|
||||
<field name="arch" type="xml">
|
||||
<form string="MIS Report Instance" version="7.0">
|
||||
<sheet>
|
||||
<div class="oe_title">
|
||||
<div class="oe_edit_only">
|
||||
<label for="name"/>
|
||||
</div>
|
||||
<h1>
|
||||
<field name="name" placeholder="Name"/>
|
||||
</h1>
|
||||
</div>
|
||||
<div class="oe_right oe_button_box" name="buttons">
|
||||
<button type="object" name="preview" string="Preview" icon="gtk-print-preview" />
|
||||
<button type="action" name="%(qweb_pdf_export)d" string="Print" icon="gtk-print" />
|
||||
<button type="action" name="%(xls_export)d" string="Export" icon="gtk-execute" />
|
||||
<button type="action" name="%(mis_report_instance_add_to_dashboard_action)d" string="Add to dashboard" icon="gtk-add" />
|
||||
</div>
|
||||
<group col="4">
|
||||
<field name="report_id" colspan="4"/>
|
||||
<field name="description"/>
|
||||
<field name="landscape_pdf" />
|
||||
<field name="root_account"/>
|
||||
<field name="company_id" groups="base.group_multi_company"/>
|
||||
<field name="date"/>
|
||||
<field name="target_move"/>
|
||||
<field name="period_ids" colspan="4">
|
||||
<tree string="KPI's" editable="bottom" colors="red:valid==False">
|
||||
<field name="sequence" widget="handle"/>
|
||||
<field name="name"/>
|
||||
<field name="type"/>
|
||||
<field name="offset"/>
|
||||
<field name="duration"/>
|
||||
<field name="normalize_factor"/>
|
||||
<field name="date_from"/>
|
||||
<field name="date_to"/>
|
||||
<field name="period_from"/>
|
||||
<field name="period_to"/>
|
||||
<field name="valid" invisible="1"/>
|
||||
<field name="report_instance_id" invisible="1"/>
|
||||
<field name="id" invisible="1"/>
|
||||
<field name="comparison_column_ids" domain="[('report_instance_id', '=', report_instance_id), ('id', '!=', id)]" widget="many2many_tags"/>
|
||||
</tree>
|
||||
</field>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mis_report_instance_view_action">
|
||||
<field name="name">MIS Reports</field>
|
||||
<field name="view_id" ref="mis_report_instance_view_tree"/>
|
||||
<field name="res_model">mis.report.instance</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
</record>
|
||||
|
||||
<menuitem id="mis_report_instance_view_menu" parent="account.menu_finance_reports" name="MIS Reports" action="mis_report_instance_view_action" sequence="101"/>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,25 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from . import mis_builder_dashboard
|
|
@ -0,0 +1,87 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for Odoo, Management Information System Builder
|
||||
# Copyright (C) 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
from openerp.osv import orm, fields
|
||||
from lxml import etree
|
||||
|
||||
|
||||
class add_mis_report_instance_dashboard(orm.TransientModel):
|
||||
_name = "add.mis.report.instance.dashboard.wizard"
|
||||
|
||||
_columns = {'name': fields.char('Name', size=32, required=True),
|
||||
'dashboard_id': fields.many2one(
|
||||
'ir.actions.act_window',
|
||||
string="Dashboard", required=True,
|
||||
domain="[('res_model', '=', 'board.board')]"),
|
||||
}
|
||||
|
||||
def default_get(self, cr, uid, fields, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
if context.get('active_id'):
|
||||
res = super(add_mis_report_instance_dashboard, self).default_get(
|
||||
cr, uid, fields, context=context)
|
||||
# get report instance name
|
||||
res['name'] = self.pool['mis.report.instance'].read(
|
||||
cr, uid, context['active_id'], ['name'])['name']
|
||||
return res
|
||||
|
||||
def action_add_to_dashboard(self, cr, uid, ids, context=None):
|
||||
if context is None:
|
||||
context = {}
|
||||
assert 'active_id' in context, "active_id missing in context"
|
||||
wizard_data = self.browse(cr, uid, ids, context=context)[0]
|
||||
# create the act_window corresponding to this report
|
||||
view_id = self.pool['ir.model.data'].get_object_reference(
|
||||
cr, uid, 'mis_builder', 'mis_report_instance_result_view_form')[1]
|
||||
report_result = self.pool['ir.actions.act_window'].create(
|
||||
cr, uid,
|
||||
{'name': 'mis.report.instance.result.view.action.%d'
|
||||
% context['active_id'],
|
||||
'res_model': 'mis.report.instance',
|
||||
'res_id': context['active_id'],
|
||||
'target': 'current',
|
||||
'view_mode': 'form',
|
||||
'view_id': view_id})
|
||||
# add this result in the selected dashboard
|
||||
last_customization = self.pool['ir.ui.view.custom'].search(
|
||||
cr, uid,
|
||||
[('user_id', '=', uid),
|
||||
('ref_id', '=', wizard_data.dashboard_id.view_id.id)], limit=1)
|
||||
arch = wizard_data.dashboard_id.view_id.arch
|
||||
if last_customization:
|
||||
arch = self.pool['ir.ui.view.custom'].read(
|
||||
cr, uid, last_customization[0], ['arch'])['arch']
|
||||
new_arch = etree.fromstring(arch)
|
||||
column = new_arch.xpath("//column")[0]
|
||||
column.append(etree.Element('action', {'context': str(context),
|
||||
'name': str(report_result),
|
||||
'string': wizard_data.name,
|
||||
'view_mode': 'form'}))
|
||||
self.pool['ir.ui.view.custom'].create(
|
||||
cr, uid, {'user_id': uid,
|
||||
'ref_id': wizard_data.dashboard_id.view_id.id,
|
||||
'arch': etree.tostring(new_arch, pretty_print=True)})
|
||||
|
||||
return {'type': 'ir.actions.act_window_close', }
|
|
@ -0,0 +1,33 @@
|
|||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<openerp>
|
||||
<data>
|
||||
|
||||
<record model="ir.ui.view" id="mis_report_instance_add_to_dashboard_form_view">
|
||||
<field name="name">add.mis.report.instance.dashboard.wizard.view</field>
|
||||
<field name="model">add.mis.report.instance.dashboard.wizard</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="Add to dashboard" version="7.0">
|
||||
<group>
|
||||
<field name="name"/>
|
||||
<field name="dashboard_id"/>
|
||||
</group>
|
||||
<footer>
|
||||
<button name="action_add_to_dashboard" string="Add to dashboard" type="object" default_focus="1" class="oe_highlight"/>
|
||||
or
|
||||
<button string="Cancel" class="oe_link" special="cancel"/>
|
||||
</footer>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record model="ir.actions.act_window" id="mis_report_instance_add_to_dashboard_action">
|
||||
<field name="name">Add to dashboard</field>
|
||||
<field name="res_model">add.mis.report.instance.dashboard.wizard</field>
|
||||
<field name="view_type">form</field>
|
||||
<field name="view_mode">form</field>
|
||||
<field name="view_id" ref="mis_report_instance_add_to_dashboard_form_view"/>
|
||||
<field name="target">new</field>
|
||||
</record>
|
||||
|
||||
</data>
|
||||
</openerp>
|
|
@ -0,0 +1,46 @@
|
|||
.. image:: https://img.shields.io/badge/licence-AGPL--3-blue.svg
|
||||
:alt: License: AGPL-3
|
||||
|
||||
MIS Builder demo data
|
||||
=====================
|
||||
|
||||
This module adds some demo data for the mis_builder module.
|
||||
|
||||
Installation
|
||||
============
|
||||
|
||||
There is no specific installation procedure for this module.
|
||||
|
||||
|
||||
Bug Tracker
|
||||
===========
|
||||
|
||||
Bugs are tracked on `GitHub Issues <https://github.com/OCA/account-financial-reporting/issues>`_.
|
||||
In case of trouble, please check there if your issue has already been reported.
|
||||
If you spotted it first, help us smashing it by providing a detailed and welcomed feedback
|
||||
`here <https://github.com/OCA/account-financial-reporting/issues/new?body=module:%20mis_builder%0Aversion:%208.0%0A%0A**Steps%20to%20reproduce**%0A-%20...%0A%0A**Current%20behavior**%0A%0A**Expected%20behavior**>`_.
|
||||
|
||||
Credits
|
||||
=======
|
||||
|
||||
Contributors
|
||||
------------
|
||||
|
||||
* Stéphane Bidoul <stephane.bidoul@acsone.eu>
|
||||
* Laetitia Gangloff <laetitia.gangloff@acsone.eu>
|
||||
* Adrien Peiffer <adrien.peiffer@acsone.eu>
|
||||
|
||||
Maintainer
|
||||
----------
|
||||
|
||||
.. image:: https://odoo-community.org/logo.png
|
||||
:alt: Odoo Community Association
|
||||
:target: https://odoo-community.org
|
||||
|
||||
This module is maintained by the OCA.
|
||||
|
||||
OCA, or the Odoo Community Association, is a nonprofit organization whose
|
||||
mission is to support the collaborative development of Odoo features and
|
||||
promote its widespread use.
|
||||
|
||||
To contribute to this module, please visit http://odoo-community.org.
|
|
@ -0,0 +1,23 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for OpenERP, Management Information System Builder
|
||||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
|
@ -0,0 +1,53 @@
|
|||
# -*- encoding: utf-8 -*-
|
||||
##############################################################################
|
||||
#
|
||||
# mis_builder module for OpenERP, Management Information System Builder
|
||||
# Copyright (C) 2014 ACSONE SA/NV (<http://acsone.eu>)
|
||||
#
|
||||
# This file is a part of mis_builder
|
||||
#
|
||||
# mis_builder is free software: you can redistribute it and/or modify
|
||||
# it under the terms of the GNU Affero General Public License v3 or later
|
||||
# as published by the Free Software Foundation, either version 3 of the
|
||||
# License, or (at your option) any later version.
|
||||
#
|
||||
# mis_builder is distributed in the hope that it will be useful,
|
||||
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
# GNU Affero General Public License v3 or later for more details.
|
||||
#
|
||||
# You should have received a copy of the GNU Affero General Public License
|
||||
# v3 or later along with this program.
|
||||
# If not, see <http://www.gnu.org/licenses/>.
|
||||
#
|
||||
##############################################################################
|
||||
|
||||
{
|
||||
'name': 'MIS Builder demo data',
|
||||
'version': '0.1',
|
||||
'category': 'Reporting',
|
||||
'summary': """
|
||||
Demo data for the mis_builder module
|
||||
""",
|
||||
'author': 'ACSONE SA/NV,'
|
||||
'Odoo Community Association (OCA)',
|
||||
'website': 'http://acsone.eu',
|
||||
'depends': [
|
||||
'account_accountant',
|
||||
'mis_builder',
|
||||
'crm'
|
||||
],
|
||||
'data': [
|
||||
],
|
||||
'demo': [
|
||||
'mis.report.kpi.csv',
|
||||
'mis.report.query.csv',
|
||||
'mis.report.csv',
|
||||
'mis.report.instance.period.csv',
|
||||
'mis.report.instance.csv',
|
||||
],
|
||||
'installable': True,
|
||||
'application': False,
|
||||
'auto_install': False,
|
||||
'license': 'AGPL-3',
|
||||
}
|
|
@ -0,0 +1,3 @@
|
|||
"id","description","kpi_ids/id","name","query_ids/id"
|
||||
"mis_report","","mis_report_kpi_1,mis_report_kpi_2,mis_report_kpi_3,mis_report_kpi_4,mis_report_kpi_5","Demo margin report","mis_report_query"
|
||||
"mis_report_phonecall","","mis_report_phonecall_kpi_1,mis_report_phonecall_kpi_2,mis_report_phonecall_kpi_3,mis_report_phonecall_kpi_4","Demo phonecall report","mis_report_phonecall_query"
|
|
|
@ -0,0 +1,3 @@
|
|||
id,date,description,name,period_ids/id,report_id/id,root_account/id
|
||||
mis_report_instance,,,Test-margin-report-instance,"mis_report_instance_period_1,mis_report_instance_period_2,mis_report_instance_period_3,mis_report_instance_period_4",mis_report,account.chart0
|
||||
mis_report_phonecall_instance,,,Test phonecall report instance,"mis_report_phonecall_instance_period_1,mis_report_phonecall_instance_period_2,mis_report_phonecall_instance_period_3",mis_report_phonecall,account.chart0
|
|
|
@ -0,0 +1,8 @@
|
|||
id,duration,name,offset,type,sequence
|
||||
mis_report_instance_period_1,1,today,0,Day,1
|
||||
mis_report_instance_period_2,1,yesterday,-1,Day,2
|
||||
mis_report_instance_period_3,1,last week,-1,Week,3
|
||||
mis_report_instance_period_4,1,last period,-1,Fiscal Period,4
|
||||
mis_report_phonecall_instance_period_1,1,today,0,Day,1
|
||||
mis_report_phonecall_instance_period_2,1,this period,0,Fiscal Period,2
|
||||
mis_report_phonecall_instance_period_3,1,previous period,-1,Fiscal Period,3
|
|
|
@ -0,0 +1,10 @@
|
|||
id,compare_method,description,expression,divider,name,dp,sequence,type,suffix
|
||||
mis_report_kpi_1,Percentage,CA,-bal[X2001],,ca,,1,Numeric,€
|
||||
mis_report_kpi_2,Percentage,CAHT invoice,sum([s.amount_untaxed for s in inv]),,total_invoice,,2,Numeric,€
|
||||
mis_report_kpi_3,Percentage,Cost,bal[X2110],,cost,,3,Numeric,€
|
||||
mis_report_kpi_4,Percentage,Profit,ca - cost,,profit,,4,Numeric,€
|
||||
mis_report_kpi_5,Difference,Margin,profit/ca,,margin,,5,Percentage,%
|
||||
mis_report_phonecall_kpi_1,Percentage,Total phone call,len(phone),,total_phone_call,,1,Numeric,
|
||||
mis_report_phonecall_kpi_2,Percentage,Average duration phone call,sum([p.duration for p in phone])/total_phone_call,,average_duration_phone_call,2,2,Numeric,
|
||||
mis_report_phonecall_kpi_3,Percentage,Total converted phone call,sum([p.opportunity_id and 1 or 0 for p in phone]),,phone_call_convert,,3,Numeric,
|
||||
mis_report_phonecall_kpi_4,Percentage,Average duration converted phone call,sum([p.opportunity_id and p.duration or 0 for p in phone]),,average_convert_duration_phone_c,2,4,Numeric,
|
|
|
@ -0,0 +1,3 @@
|
|||
"id","date_field/id","domain","field_ids/id","model_id/id","name"
|
||||
"mis_report_query","account.field_account_invoice_date_invoice","","account.field_account_invoice_amount_untaxed","account.model_account_invoice","inv"
|
||||
"mis_report_phonecall_query","crm.field_crm_phonecall_date","","crm.field_crm_phonecall_duration,crm.field_crm_phonecall_opportunity_id","crm.model_crm_phonecall","phone"
|
|
Loading…
Reference in New Issue