You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

416 lines
16 KiB

  1. # -*- coding: utf-8 -*-
  2. # © 2014-2016 ACSONE SA/NV (<http://acsone.eu>)
  3. # License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).
  4. from openerp import api, fields, models, _
  5. import datetime
  6. import logging
  7. from .aep import AccountingExpressionProcessor as AEP
  8. _logger = logging.getLogger(__name__)
  9. class MisReportInstancePeriod(models.Model):
  10. """ A MIS report instance has the logic to compute
  11. a report template for a given date period.
  12. Periods have a duration (day, week, fiscal period) and
  13. are defined as an offset relative to a pivot date.
  14. """
  15. @api.one
  16. @api.depends('report_instance_id.pivot_date',
  17. 'report_instance_id.comparison_mode',
  18. 'type', 'offset', 'duration', 'mode')
  19. def _compute_dates(self):
  20. self.date_from = False
  21. self.date_to = False
  22. self.valid = False
  23. report = self.report_instance_id
  24. d = fields.Date.from_string(report.pivot_date)
  25. if not report.comparison_mode:
  26. self.date_from = report.date_from
  27. self.date_to = report.date_to
  28. self.valid = True
  29. elif self.mode == 'fix':
  30. self.date_from = self.manual_date_from
  31. self.date_to = self.manual_date_to
  32. self.valid = True
  33. elif self.type == 'd':
  34. date_from = d + datetime.timedelta(days=self.offset)
  35. date_to = date_from + \
  36. datetime.timedelta(days=self.duration - 1)
  37. self.date_from = fields.Date.to_string(date_from)
  38. self.date_to = fields.Date.to_string(date_to)
  39. self.valid = True
  40. elif self.type == 'w':
  41. date_from = d - datetime.timedelta(d.weekday())
  42. date_from = date_from + datetime.timedelta(days=self.offset * 7)
  43. date_to = date_from + \
  44. datetime.timedelta(days=(7 * self.duration) - 1)
  45. self.date_from = fields.Date.to_string(date_from)
  46. self.date_to = fields.Date.to_string(date_to)
  47. self.valid = True
  48. elif self.type == 'date_range':
  49. date_range_obj = self.env['date.range']
  50. current_periods = date_range_obj.search(
  51. [('type_id', '=', self.date_range_type_id.id),
  52. ('date_start', '<=', d),
  53. ('date_end', '>=', d),
  54. ('company_id', '=', self.report_instance_id.company_id.id)])
  55. if current_periods:
  56. all_periods = date_range_obj.search(
  57. [('type_id', '=', self.date_range_type_id.id),
  58. ('company_id', '=',
  59. self.report_instance_id.company_id.id)],
  60. order='date_start')
  61. all_period_ids = [p.id for p in all_periods]
  62. p = all_period_ids.index(current_periods[0].id) + self.offset
  63. if p >= 0 and p + self.duration <= len(all_period_ids):
  64. periods = all_periods[p:p + self.duration]
  65. self.date_from = periods[0].date_start
  66. self.date_to = periods[-1].date_end
  67. self.valid = True
  68. _name = 'mis.report.instance.period'
  69. name = fields.Char(size=32, required=True,
  70. string='Description', translate=True)
  71. mode = fields.Selection([('fix', 'Fix'),
  72. ('relative', 'Relative'),
  73. ], required=True,
  74. default='fix')
  75. type = fields.Selection([('d', _('Day')),
  76. ('w', _('Week')),
  77. ('date_range', _('Date Range'))
  78. ],
  79. string='Period type')
  80. date_range_type_id = fields.Many2one(
  81. comodel_name='date.range.type', string='Date Range Type')
  82. offset = fields.Integer(string='Offset',
  83. help='Offset from current period',
  84. default=-1)
  85. duration = fields.Integer(string='Duration',
  86. help='Number of periods',
  87. default=1)
  88. date_from = fields.Date(compute='_compute_dates', string="From")
  89. date_to = fields.Date(compute='_compute_dates', string="To")
  90. manual_date_from = fields.Date(string="From")
  91. manual_date_to = fields.Date(string="To")
  92. date_range_id = fields.Many2one(
  93. comodel_name='date.range',
  94. string='Date Range')
  95. valid = fields.Boolean(compute='_compute_dates',
  96. type='boolean',
  97. string='Valid')
  98. sequence = fields.Integer(string='Sequence', default=100)
  99. report_instance_id = fields.Many2one('mis.report.instance',
  100. string='Report Instance',
  101. ondelete='cascade')
  102. comparison_column_ids = fields.Many2many(
  103. comodel_name='mis.report.instance.period',
  104. relation='mis_report_instance_period_rel',
  105. column1='period_id',
  106. column2='compare_period_id',
  107. string='Compare with')
  108. normalize_factor = fields.Integer(
  109. string='Factor',
  110. help='Factor to use to normalize the period (used in comparison',
  111. default=1)
  112. subkpi_ids = fields.Many2many(
  113. 'mis.report.subkpi',
  114. string="Sub KPI Filter")
  115. _order = 'sequence, id'
  116. _sql_constraints = [
  117. ('duration', 'CHECK (duration>0)',
  118. 'Wrong duration, it must be positive!'),
  119. ('normalize_factor', 'CHECK (normalize_factor>0)',
  120. 'Wrong normalize factor, it must be positive!'),
  121. ('name_unique', 'unique(name, report_instance_id)',
  122. 'Period name should be unique by report'),
  123. ]
  124. @api.onchange('date_range_id')
  125. def onchange_date_range(self):
  126. for record in self:
  127. record.manual_date_from = record.date_range_id.date_start
  128. record.manual_date_to = record.date_range_id.date_end
  129. record.name = record.date_range_id.name
  130. @api.multi
  131. def _get_additional_move_line_filter(self):
  132. """ Prepare a filter to apply on all move lines
  133. This filter is applied with a AND operator on all
  134. accounting expression domains. This hook is intended
  135. to be inherited, and is useful to implement filtering
  136. on analytic dimensions or operational units.
  137. Returns an Odoo domain expression (a python list)
  138. compatible with account.move.line."""
  139. self.ensure_one()
  140. return []
  141. @api.multi
  142. def _get_additional_query_filter(self, query):
  143. """ Prepare an additional filter to apply on the query
  144. This filter is combined to the query domain with a AND
  145. operator. This hook is intended
  146. to be inherited, and is useful to implement filtering
  147. on analytic dimensions or operational units.
  148. Returns an Odoo domain expression (a python list)
  149. compatible with the model of the query."""
  150. self.ensure_one()
  151. return []
  152. @api.multi
  153. def drilldown(self, expr):
  154. self.ensure_one()
  155. # TODO FIXME: drilldown by account
  156. if AEP.has_account_var(expr):
  157. aep = AEP(self.env)
  158. aep.parse_expr(expr)
  159. aep.done_parsing(self.report_instance_id.company_id)
  160. domain = aep.get_aml_domain_for_expr(
  161. expr,
  162. self.date_from, self.date_to,
  163. self.report_instance_id.target_move,
  164. self.report_instance_id.company_id)
  165. domain.extend(self._get_additional_move_line_filter())
  166. return {
  167. 'name': expr + ' - ' + self.name,
  168. 'domain': domain,
  169. 'type': 'ir.actions.act_window',
  170. 'res_model': 'account.move.line',
  171. 'views': [[False, 'list'], [False, 'form']],
  172. 'view_type': 'list',
  173. 'view_mode': 'list',
  174. 'target': 'current',
  175. }
  176. else:
  177. return False
  178. class MisReportInstance(models.Model):
  179. """The MIS report instance combines everything to compute
  180. a MIS report template for a set of periods."""
  181. @api.one
  182. @api.depends('date')
  183. def _compute_pivot_date(self):
  184. if self.date:
  185. self.pivot_date = self.date
  186. else:
  187. self.pivot_date = fields.Date.context_today(self)
  188. @api.model
  189. def _default_company(self):
  190. return self.env['res.company'].\
  191. _company_default_get('mis.report.instance')
  192. _name = 'mis.report.instance'
  193. name = fields.Char(required=True,
  194. string='Name', translate=True)
  195. description = fields.Char(related='report_id.description',
  196. readonly=True)
  197. date = fields.Date(string='Base date',
  198. help='Report base date '
  199. '(leave empty to use current date)')
  200. pivot_date = fields.Date(compute='_compute_pivot_date',
  201. string="Pivot date")
  202. report_id = fields.Many2one('mis.report',
  203. required=True,
  204. string='Report')
  205. period_ids = fields.One2many('mis.report.instance.period',
  206. 'report_instance_id',
  207. required=True,
  208. string='Periods',
  209. copy=True)
  210. target_move = fields.Selection([('posted', 'All Posted Entries'),
  211. ('all', 'All Entries')],
  212. string='Target Moves',
  213. required=True,
  214. default='posted')
  215. company_id = fields.Many2one(comodel_name='res.company',
  216. string='Company',
  217. default=_default_company,
  218. required=True)
  219. landscape_pdf = fields.Boolean(string='Landscape PDF')
  220. comparison_mode = fields.Boolean(
  221. compute="_compute_comparison_mode",
  222. inverse="_inverse_comparison_mode")
  223. date_range_id = fields.Many2one(
  224. comodel_name='date.range',
  225. string='Date Range')
  226. date_from = fields.Date(string="From")
  227. date_to = fields.Date(string="To")
  228. temporary = fields.Boolean(default=False)
  229. @api.multi
  230. def save_report(self):
  231. self.ensure_one()
  232. self.write({'temporary': False})
  233. action = self.env.ref('mis_builder.mis_report_instance_view_action')
  234. res = action.read()[0]
  235. view = self.env.ref('mis_builder.mis_report_instance_view_form')
  236. res.update({
  237. 'views': [(view.id, 'form')],
  238. 'res_id': self.id,
  239. })
  240. return res
  241. @api.model
  242. def _vacuum_report(self, hours=24):
  243. clear_date = fields.Datetime.to_string(
  244. datetime.datetime.now() - datetime.timedelta(hours=hours))
  245. reports = self.search([
  246. ('write_date', '<', clear_date),
  247. ('temporary', '=', True),
  248. ])
  249. _logger.debug('Vacuum %s Temporary MIS Builder Report', len(reports))
  250. return reports.unlink()
  251. @api.one
  252. def copy(self, default=None):
  253. default = dict(default or {})
  254. default['name'] = _('%s (copy)') % self.name
  255. return super(MisReportInstance, self).copy(default)
  256. def _format_date(self, date):
  257. # format date following user language
  258. lang_model = self.env['res.lang']
  259. lang_id = lang_model._lang_get(self.env.user.lang)
  260. date_format = lang_model.browse(lang_id).date_format
  261. return datetime.datetime.strftime(
  262. fields.Date.from_string(date), date_format)
  263. @api.multi
  264. @api.depends('date_from')
  265. def _compute_comparison_mode(self):
  266. for instance in self:
  267. instance.comparison_mode = bool(instance.period_ids) and\
  268. not bool(instance.date_from)
  269. @api.multi
  270. def _inverse_comparison_mode(self):
  271. for record in self:
  272. if not record.comparison_mode:
  273. if not record.date_from:
  274. record.date_from = datetime.now()
  275. if not record.date_to:
  276. record.date_to = datetime.now()
  277. record.period_ids.unlink()
  278. record.write({'period_ids': [
  279. (0, 0, {
  280. 'name': 'Default',
  281. 'type': 'd',
  282. })
  283. ]})
  284. else:
  285. record.date_from = None
  286. record.date_to = None
  287. @api.onchange('date_range_id')
  288. def onchange_date_range(self):
  289. for record in self:
  290. record.date_from = record.date_range_id.date_start
  291. record.date_to = record.date_range_id.date_end
  292. @api.multi
  293. def preview(self):
  294. assert len(self) == 1
  295. view_id = self.env.ref('mis_builder.'
  296. 'mis_report_instance_result_view_form')
  297. return {
  298. 'type': 'ir.actions.act_window',
  299. 'res_model': 'mis.report.instance',
  300. 'res_id': self.id,
  301. 'view_mode': 'form',
  302. 'view_type': 'form',
  303. 'view_id': view_id.id,
  304. 'target': 'current',
  305. }
  306. @api.multi
  307. def print_pdf(self):
  308. self.ensure_one()
  309. return {
  310. 'name': 'MIS report instance QWEB PDF report',
  311. 'model': 'mis.report.instance',
  312. 'type': 'ir.actions.report.xml',
  313. 'report_name': 'mis_builder.report_mis_report_instance',
  314. 'report_type': 'qweb-pdf',
  315. 'context': self.env.context,
  316. }
  317. @api.multi
  318. def export_xls(self):
  319. self.ensure_one()
  320. return {
  321. 'name': 'MIS report instance XLSX report',
  322. 'model': 'mis.report.instance',
  323. 'type': 'ir.actions.report.xml',
  324. 'report_name': 'mis.report.instance.xlsx',
  325. 'report_type': 'xlsx',
  326. 'context': self.env.context,
  327. }
  328. @api.multi
  329. def display_settings(self):
  330. assert len(self.ids) <= 1
  331. view_id = self.env.ref('mis_builder.mis_report_instance_view_form')
  332. return {
  333. 'type': 'ir.actions.act_window',
  334. 'res_model': 'mis.report.instance',
  335. 'res_id': self.id if self.id else False,
  336. 'view_mode': 'form',
  337. 'view_type': 'form',
  338. 'views': [(view_id.id, 'form')],
  339. 'view_id': view_id.id,
  340. 'target': 'current',
  341. }
  342. @api.multi
  343. def _compute_matrix(self):
  344. self.ensure_one()
  345. aep = self.report_id._prepare_aep(self.company_id)
  346. kpi_matrix = self.report_id._prepare_kpi_matrix()
  347. for period in self.period_ids:
  348. # add the column header
  349. if period.date_from == period.date_to:
  350. comment = self._format_date(period.date_from)
  351. else:
  352. # from, to
  353. date_from = self._format_date(period.date_from)
  354. date_to = self._format_date(period.date_to)
  355. comment = _('from %s to %s') % (date_from, date_to)
  356. self.report_id._declare_and_compute_period(
  357. kpi_matrix,
  358. period.id,
  359. period.name,
  360. comment,
  361. aep,
  362. period.date_from,
  363. period.date_to,
  364. self.target_move,
  365. self.company_id,
  366. period.subkpi_ids,
  367. period._get_additional_move_line_filter,
  368. period._get_additional_query_filter)
  369. for comparison_column in period.comparison_column_ids:
  370. kpi_matrix.declare_comparison(period.id, comparison_column.id)
  371. kpi_matrix.compute_comparisons()
  372. return kpi_matrix
  373. @api.multi
  374. def compute(self):
  375. self.ensure_one()
  376. kpi_matrix = self._compute_matrix()
  377. return kpi_matrix.as_dict()