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.

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