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.

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