Browse Source

[WIP] mis_builder refactoring: split mis_builder.py in two

mis_report.py has the heavy stuff, and contains all the computation
and rendering code
pull/189/head
Stéphane Bidoul 8 years ago
parent
commit
8324ef2e01
  1. 3
      mis_builder/models/__init__.py
  2. 398
      mis_builder/models/mis_report.py
  3. 410
      mis_builder/models/mis_report_instance.py

3
mis_builder/models/__init__.py

@ -2,6 +2,7 @@
# © 2014-2015 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from . import mis_builder
from . import mis_report
from . import mis_report_instance
from . import mis_builder_style
from . import aep

398
mis_builder/models/mis_builder.py → mis_builder/models/mis_report.py

@ -987,401 +987,3 @@ class MisReport(models.Model):
# try again
compute_queue = recompute_queue
recompute_queue = []
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',
'report_instance_id.comparison_mode',
'type', 'offset', 'duration', 'mode')
def _compute_dates(self):
self.date_from = False
self.date_to = False
self.valid = False
report = self.report_instance_id
d = fields.Date.from_string(report.pivot_date)
if not report.comparison_mode:
self.date_from = report.date_from
self.date_to = report.date_to
self.valid = True
elif self.mode == 'fix':
self.date_from = self.manual_date_from
self.date_to = self.manual_date_to
self.valid = True
elif 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 == 'date_range':
date_range_obj = self.env['date.range']
current_periods = date_range_obj.search(
[('type_id', '=', self.date_range_type_id.id),
('date_start', '<=', d),
('date_end', '>=', d),
('company_id', '=', self.report_instance_id.company_id.id)])
if current_periods:
all_periods = date_range_obj.search(
[('type_id', '=', self.date_range_type_id.id),
('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_end
self.valid = True
_name = 'mis.report.instance.period'
name = fields.Char(size=32, required=True,
string='Description', translate=True)
mode = fields.Selection([('fix', 'Fix'),
('relative', 'Relative'),
], required=True,
default='fix')
type = fields.Selection([('d', _('Day')),
('w', _('Week')),
('date_range', _('Date Range'))
],
string='Period type')
date_range_type_id = fields.Many2one(
comodel_name='date.range.type', string='Date Range Type')
offset = fields.Integer(string='Offset',
help='Offset from current period',
default=-1)
duration = fields.Integer(string='Duration',
help='Number of periods',
default=1)
date_from = fields.Date(compute='_compute_dates', string="From")
date_to = fields.Date(compute='_compute_dates', string="To")
manual_date_from = fields.Date(string="From")
manual_date_to = fields.Date(string="To")
date_range_id = fields.Many2one(
comodel_name='date.range',
string='Date Range')
valid = fields.Boolean(compute='_compute_dates',
type='boolean',
string='Valid')
sequence = fields.Integer(string='Sequence', default=100)
report_instance_id = fields.Many2one('mis.report.instance',
string='Report Instance',
ondelete='cascade')
comparison_column_ids = fields.Many2many(
comodel_name='mis.report.instance.period',
relation='mis_report_instance_period_rel',
column1='period_id',
column2='compare_period_id',
string='Compare with')
normalize_factor = fields.Integer(
string='Factor',
help='Factor to use to normalize the period (used in comparison',
default=1)
subkpi_ids = fields.Many2many(
'mis.report.subkpi',
string="Sub KPI Filter")
_order = 'sequence, id'
_sql_constraints = [
('duration', 'CHECK (duration>0)',
'Wrong duration, it must be positive!'),
('normalize_factor', 'CHECK (normalize_factor>0)',
'Wrong normalize factor, it must be positive!'),
('name_unique', 'unique(name, report_instance_id)',
'Period name should be unique by report'),
]
@api.onchange('date_range_id')
def onchange_date_range(self):
for record in self:
record.manual_date_from = record.date_range_id.date_start
record.manual_date_to = record.date_range_id.date_end
record.name = record.date_range_id.name
@api.multi
def _get_additional_move_line_filter(self):
""" Prepare a filter to apply on all move lines
This filter is applied with a AND operator on all
accounting expression domains. This hook is intended
to be inherited, and is useful to implement filtering
on analytic dimensions or operational units.
Returns an Odoo domain expression (a python list)
compatible with account.move.line."""
self.ensure_one()
return []
@api.multi
def _get_additional_query_filter(self, query):
""" Prepare an additional filter to apply on the query
This filter is combined to the query domain with a AND
operator. This hook is intended
to be inherited, and is useful to implement filtering
on analytic dimensions or operational units.
Returns an Odoo domain expression (a python list)
compatible with the model of the query."""
self.ensure_one()
return []
@api.multi
def drilldown(self, expr):
self.ensure_one()
# TODO FIXME: drilldown by account
if AEP.has_account_var(expr):
aep = AEP(self.env)
aep.parse_expr(expr)
aep.done_parsing(self.report_instance_id.company_id)
domain = aep.get_aml_domain_for_expr(
expr,
self.date_from, self.date_to,
self.report_instance_id.target_move,
self.report_instance_id.company_id)
domain.extend(self._get_additional_move_line_filter())
return {
'name': expr + ' - ' + self.name,
'domain': domain,
'type': 'ir.actions.act_window',
'res_model': 'account.move.line',
'views': [[False, 'list'], [False, 'form']],
'view_type': 'list',
'view_mode': 'list',
'target': 'current',
}
else:
return False
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)
@api.model
def _default_company(self):
return self.env['res.company'].\
_company_default_get('mis.report.instance')
_name = 'mis.report.instance'
name = fields.Char(required=True,
string='Name', translate=True)
description = fields.Char(related='report_id.description',
readonly=True)
date = fields.Date(string='Base date',
help='Report base date '
'(leave empty to use current date)')
pivot_date = fields.Date(compute='_compute_pivot_date',
string="Pivot date")
report_id = fields.Many2one('mis.report',
required=True,
string='Report')
period_ids = fields.One2many('mis.report.instance.period',
'report_instance_id',
required=True,
string='Periods',
copy=True)
target_move = fields.Selection([('posted', 'All Posted Entries'),
('all', 'All Entries')],
string='Target Moves',
required=True,
default='posted')
company_id = fields.Many2one(comodel_name='res.company',
string='Company',
default=_default_company,
required=True)
landscape_pdf = fields.Boolean(string='Landscape PDF')
comparison_mode = fields.Boolean(
compute="_compute_comparison_mode",
inverse="_inverse_comparison_mode")
date_range_id = fields.Many2one(
comodel_name='date.range',
string='Date Range')
date_from = fields.Date(string="From")
date_to = fields.Date(string="To")
temporary = fields.Boolean(default=False)
@api.multi
def save_report(self):
self.ensure_one()
self.write({'temporary': False})
action = self.env.ref('mis_builder.mis_report_instance_view_action')
res = action.read()[0]
view = self.env.ref('mis_builder.mis_report_instance_view_form')
res.update({
'views': [(view.id, 'form')],
'res_id': self.id,
})
return res
@api.model
def _vacuum_report(self, hours=24):
clear_date = fields.Datetime.to_string(
datetime.datetime.now() - datetime.timedelta(hours=hours))
reports = self.search([
('write_date', '<', clear_date),
('temporary', '=', True),
])
_logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports))
return reports.unlink()
@api.one
def copy(self, default=None):
default = dict(default or {})
default['name'] = _('%s (copy)') % self.name
return super(MisReportInstance, self).copy(default)
def _format_date(self, date):
# format date following user language
lang_model = self.env['res.lang']
lang_id = lang_model._lang_get(self.env.user.lang)
date_format = lang_model.browse(lang_id).date_format
return datetime.datetime.strftime(
fields.Date.from_string(date), date_format)
@api.multi
@api.depends('date_from')
def _compute_comparison_mode(self):
for instance in self:
instance.comparison_mode = bool(instance.period_ids) and\
not bool(instance.date_from)
@api.multi
def _inverse_comparison_mode(self):
for record in self:
if not record.comparison_mode:
if not record.date_from:
record.date_from = datetime.now()
if not record.date_to:
record.date_to = datetime.now()
record.period_ids.unlink()
record.write({'period_ids': [
(0, 0, {
'name': 'Default',
'type': 'd',
})
]})
else:
record.date_from = None
record.date_to = None
@api.onchange('date_range_id')
def onchange_date_range(self):
for record in self:
record.date_from = record.date_range_id.date_start
record.date_to = record.date_range_id.date_end
@api.multi
def preview(self):
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': 'current',
}
@api.multi
def print_pdf(self):
self.ensure_one()
return {
'name': 'MIS report instance QWEB PDF report',
'model': 'mis.report.instance',
'type': 'ir.actions.report.xml',
'report_name': 'mis_builder.report_mis_report_instance',
'report_type': 'qweb-pdf',
'context': self.env.context,
}
@api.multi
def export_xls(self):
self.ensure_one()
return {
'name': 'MIS report instance XLSX report',
'model': 'mis.report.instance',
'type': 'ir.actions.report.xml',
'report_name': 'mis.report.instance.xlsx',
'report_type': 'xlsx',
'context': self.env.context,
}
@api.multi
def display_settings(self):
assert len(self.ids) <= 1
view_id = self.env.ref('mis_builder.mis_report_instance_view_form')
return {
'type': 'ir.actions.act_window',
'res_model': 'mis.report.instance',
'res_id': self.id if self.id else False,
'view_mode': 'form',
'view_type': 'form',
'views': [(view_id.id, 'form')],
'view_id': view_id.id,
'target': 'current',
}
@api.multi
def compute(self):
self.ensure_one()
aep = self.report_id._prepare_aep(self.company_id)
kpi_matrix = self.report_id._prepare_kpi_matrix()
for period in self.period_ids:
# add the column header
if period.date_from == period.date_to:
comment = self._format_date(period.date_from)
else:
# from, to
date_from = self._format_date(period.date_from)
date_to = self._format_date(period.date_to)
comment = _('from %s to %s') % (date_from, date_to)
self.report_id._declare_and_compute_period(
kpi_matrix,
period.id,
period.name,
comment,
aep,
period.date_from,
period.date_to,
self.target_move,
self.company_id,
period.subkpi_ids,
period._get_additional_move_line_filter,
period._get_additional_query_filter)
for comparison_column in period.comparison_column_ids:
kpi_matrix.declare_comparison(period.id, comparison_column.id)
kpi_matrix.compute_comparisons()
return kpi_matrix.as_dict()

410
mis_builder/models/mis_report_instance.py

@ -0,0 +1,410 @@
# -*- coding: utf-8 -*-
# © 2014-2016 ACSONE SA/NV (<http://acsone.eu>)
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
from openerp import api, fields, models, _
import datetime
import logging
from .aep import AccountingExpressionProcessor as AEP
_logger = logging.getLogger(__name__)
class MisReportInstancePeriod(models.Model):
""" A MIS report instance has the logic to compute
a report template for a given date period.
Periods have a duration (day, week, fiscal period) and
are defined as an offset relative to a pivot date.
"""
@api.one
@api.depends('report_instance_id.pivot_date',
'report_instance_id.comparison_mode',
'type', 'offset', 'duration', 'mode')
def _compute_dates(self):
self.date_from = False
self.date_to = False
self.valid = False
report = self.report_instance_id
d = fields.Date.from_string(report.pivot_date)
if not report.comparison_mode:
self.date_from = report.date_from
self.date_to = report.date_to
self.valid = True
elif self.mode == 'fix':
self.date_from = self.manual_date_from
self.date_to = self.manual_date_to
self.valid = True
elif 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 == 'date_range':
date_range_obj = self.env['date.range']
current_periods = date_range_obj.search(
[('type_id', '=', self.date_range_type_id.id),
('date_start', '<=', d),
('date_end', '>=', d),
('company_id', '=', self.report_instance_id.company_id.id)])
if current_periods:
all_periods = date_range_obj.search(
[('type_id', '=', self.date_range_type_id.id),
('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_end
self.valid = True
_name = 'mis.report.instance.period'
name = fields.Char(size=32, required=True,
string='Description', translate=True)
mode = fields.Selection([('fix', 'Fix'),
('relative', 'Relative'),
], required=True,
default='fix')
type = fields.Selection([('d', _('Day')),
('w', _('Week')),
('date_range', _('Date Range'))
],
string='Period type')
date_range_type_id = fields.Many2one(
comodel_name='date.range.type', string='Date Range Type')
offset = fields.Integer(string='Offset',
help='Offset from current period',
default=-1)
duration = fields.Integer(string='Duration',
help='Number of periods',
default=1)
date_from = fields.Date(compute='_compute_dates', string="From")
date_to = fields.Date(compute='_compute_dates', string="To")
manual_date_from = fields.Date(string="From")
manual_date_to = fields.Date(string="To")
date_range_id = fields.Many2one(
comodel_name='date.range',
string='Date Range')
valid = fields.Boolean(compute='_compute_dates',
type='boolean',
string='Valid')
sequence = fields.Integer(string='Sequence', default=100)
report_instance_id = fields.Many2one('mis.report.instance',
string='Report Instance',
ondelete='cascade')
comparison_column_ids = fields.Many2many(
comodel_name='mis.report.instance.period',
relation='mis_report_instance_period_rel',
column1='period_id',
column2='compare_period_id',
string='Compare with')
normalize_factor = fields.Integer(
string='Factor',
help='Factor to use to normalize the period (used in comparison',
default=1)
subkpi_ids = fields.Many2many(
'mis.report.subkpi',
string="Sub KPI Filter")
_order = 'sequence, id'
_sql_constraints = [
('duration', 'CHECK (duration>0)',
'Wrong duration, it must be positive!'),
('normalize_factor', 'CHECK (normalize_factor>0)',
'Wrong normalize factor, it must be positive!'),
('name_unique', 'unique(name, report_instance_id)',
'Period name should be unique by report'),
]
@api.onchange('date_range_id')
def onchange_date_range(self):
for record in self:
record.manual_date_from = record.date_range_id.date_start
record.manual_date_to = record.date_range_id.date_end
record.name = record.date_range_id.name
@api.multi
def _get_additional_move_line_filter(self):
""" Prepare a filter to apply on all move lines
This filter is applied with a AND operator on all
accounting expression domains. This hook is intended
to be inherited, and is useful to implement filtering
on analytic dimensions or operational units.
Returns an Odoo domain expression (a python list)
compatible with account.move.line."""
self.ensure_one()
return []
@api.multi
def _get_additional_query_filter(self, query):
""" Prepare an additional filter to apply on the query
This filter is combined to the query domain with a AND
operator. This hook is intended
to be inherited, and is useful to implement filtering
on analytic dimensions or operational units.
Returns an Odoo domain expression (a python list)
compatible with the model of the query."""
self.ensure_one()
return []
@api.multi
def drilldown(self, expr):
self.ensure_one()
# TODO FIXME: drilldown by account
if AEP.has_account_var(expr):
aep = AEP(self.env)
aep.parse_expr(expr)
aep.done_parsing(self.report_instance_id.company_id)
domain = aep.get_aml_domain_for_expr(
expr,
self.date_from, self.date_to,
self.report_instance_id.target_move,
self.report_instance_id.company_id)
domain.extend(self._get_additional_move_line_filter())
return {
'name': expr + ' - ' + self.name,
'domain': domain,
'type': 'ir.actions.act_window',
'res_model': 'account.move.line',
'views': [[False, 'list'], [False, 'form']],
'view_type': 'list',
'view_mode': 'list',
'target': 'current',
}
else:
return False
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)
@api.model
def _default_company(self):
return self.env['res.company'].\
_company_default_get('mis.report.instance')
_name = 'mis.report.instance'
name = fields.Char(required=True,
string='Name', translate=True)
description = fields.Char(related='report_id.description',
readonly=True)
date = fields.Date(string='Base date',
help='Report base date '
'(leave empty to use current date)')
pivot_date = fields.Date(compute='_compute_pivot_date',
string="Pivot date")
report_id = fields.Many2one('mis.report',
required=True,
string='Report')
period_ids = fields.One2many('mis.report.instance.period',
'report_instance_id',
required=True,
string='Periods',
copy=True)
target_move = fields.Selection([('posted', 'All Posted Entries'),
('all', 'All Entries')],
string='Target Moves',
required=True,
default='posted')
company_id = fields.Many2one(comodel_name='res.company',
string='Company',
default=_default_company,
required=True)
landscape_pdf = fields.Boolean(string='Landscape PDF')
comparison_mode = fields.Boolean(
compute="_compute_comparison_mode",
inverse="_inverse_comparison_mode")
date_range_id = fields.Many2one(
comodel_name='date.range',
string='Date Range')
date_from = fields.Date(string="From")
date_to = fields.Date(string="To")
temporary = fields.Boolean(default=False)
@api.multi
def save_report(self):
self.ensure_one()
self.write({'temporary': False})
action = self.env.ref('mis_builder.mis_report_instance_view_action')
res = action.read()[0]
view = self.env.ref('mis_builder.mis_report_instance_view_form')
res.update({
'views': [(view.id, 'form')],
'res_id': self.id,
})
return res
@api.model
def _vacuum_report(self, hours=24):
clear_date = fields.Datetime.to_string(
datetime.datetime.now() - datetime.timedelta(hours=hours))
reports = self.search([
('write_date', '<', clear_date),
('temporary', '=', True),
])
_logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports))
return reports.unlink()
@api.one
def copy(self, default=None):
default = dict(default or {})
default['name'] = _('%s (copy)') % self.name
return super(MisReportInstance, self).copy(default)
def _format_date(self, date):
# format date following user language
lang_model = self.env['res.lang']
lang_id = lang_model._lang_get(self.env.user.lang)
date_format = lang_model.browse(lang_id).date_format
return datetime.datetime.strftime(
fields.Date.from_string(date), date_format)
@api.multi
@api.depends('date_from')
def _compute_comparison_mode(self):
for instance in self:
instance.comparison_mode = bool(instance.period_ids) and\
not bool(instance.date_from)
@api.multi
def _inverse_comparison_mode(self):
for record in self:
if not record.comparison_mode:
if not record.date_from:
record.date_from = datetime.now()
if not record.date_to:
record.date_to = datetime.now()
record.period_ids.unlink()
record.write({'period_ids': [
(0, 0, {
'name': 'Default',
'type': 'd',
})
]})
else:
record.date_from = None
record.date_to = None
@api.onchange('date_range_id')
def onchange_date_range(self):
for record in self:
record.date_from = record.date_range_id.date_start
record.date_to = record.date_range_id.date_end
@api.multi
def preview(self):
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': 'current',
}
@api.multi
def print_pdf(self):
self.ensure_one()
return {
'name': 'MIS report instance QWEB PDF report',
'model': 'mis.report.instance',
'type': 'ir.actions.report.xml',
'report_name': 'mis_builder.report_mis_report_instance',
'report_type': 'qweb-pdf',
'context': self.env.context,
}
@api.multi
def export_xls(self):
self.ensure_one()
return {
'name': 'MIS report instance XLSX report',
'model': 'mis.report.instance',
'type': 'ir.actions.report.xml',
'report_name': 'mis.report.instance.xlsx',
'report_type': 'xlsx',
'context': self.env.context,
}
@api.multi
def display_settings(self):
assert len(self.ids) <= 1
view_id = self.env.ref('mis_builder.mis_report_instance_view_form')
return {
'type': 'ir.actions.act_window',
'res_model': 'mis.report.instance',
'res_id': self.id if self.id else False,
'view_mode': 'form',
'view_type': 'form',
'views': [(view_id.id, 'form')],
'view_id': view_id.id,
'target': 'current',
}
@api.multi
def compute(self):
self.ensure_one()
aep = self.report_id._prepare_aep(self.company_id)
kpi_matrix = self.report_id._prepare_kpi_matrix()
for period in self.period_ids:
# add the column header
if period.date_from == period.date_to:
comment = self._format_date(period.date_from)
else:
# from, to
date_from = self._format_date(period.date_from)
date_to = self._format_date(period.date_to)
comment = _('from %s to %s') % (date_from, date_to)
self.report_id._declare_and_compute_period(
kpi_matrix,
period.id,
period.name,
comment,
aep,
period.date_from,
period.date_to,
self.target_move,
self.company_id,
period.subkpi_ids,
period._get_additional_move_line_filter,
period._get_additional_query_filter)
for comparison_column in period.comparison_column_ids:
kpi_matrix.declare_comparison(period.id, comparison_column.id)
kpi_matrix.compute_comparisons()
return kpi_matrix.as_dict()
Loading…
Cancel
Save