account_closing.py 9.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169
  1. # -*- coding: utf-8 -*-
  2. # Part of Odoo. See LICENSE file for full copyright and licensing details.
  3. from datetime import datetime, timedelta
  4. from odoo import models, api, fields
  5. from odoo.fields import Datetime as FieldDateTime
  6. from odoo.tools.translate import _
  7. from odoo.exceptions import UserError
  8. from odoo.osv.expression import AND
  9. class AccountClosing(models.Model):
  10. """
  11. This object holds an interval total and a grand total of the accounts of type receivable for a company,
  12. as well as the last account_move that has been counted in a previous object
  13. It takes its earliest brother to infer from when the computation needs to be done
  14. in order to compute its own data.
  15. """
  16. _name = 'account.sale.closing'
  17. _order = 'date_closing_stop desc, sequence_number desc'
  18. _description = "Sale Closing"
  19. name = fields.Char(help="Frequency and unique sequence number", required=True)
  20. company_id = fields.Many2one('res.company', string='Company', readonly=True, required=True)
  21. date_closing_stop = fields.Datetime(string="Closing Date", help='Date to which the values are computed', readonly=True, required=True)
  22. date_closing_start = fields.Datetime(string="Starting Date", help='Date from which the total interval is computed', readonly=True, required=True)
  23. frequency = fields.Selection(string='Closing Type', selection=[('daily', 'Daily'), ('monthly', 'Monthly'), ('annually', 'Annual')], readonly=True, required=True)
  24. total_interval = fields.Monetary(string="Period Total", help='Total in receivable accounts during the interval, excluding overlapping periods', readonly=True, required=True)
  25. cumulative_total = fields.Monetary(string="Cumulative Grand Total", help='Total in receivable accounts since the beginnig of times', readonly=True, required=True)
  26. sequence_number = fields.Integer('Sequence #', readonly=True, required=True)
  27. last_order_id = fields.Many2one('pos.order', string='Last Pos Order', help='Last Pos order included in the grand total', readonly=True)
  28. last_order_hash = fields.Char(string='Last Order entry\'s inalteralbility hash', readonly=True)
  29. currency_id = fields.Many2one('res.currency', string='Currency', help="The company's currency", readonly=True, related='company_id.currency_id', store=True)
  30. def _query_for_aml(self, company, first_move_sequence_number, date_start):
  31. params = {'company_id': company.id}
  32. query = '''WITH aggregate AS (SELECT m.id AS move_id,
  33. aml.balance AS balance,
  34. aml.id as line_id
  35. FROM account_move_line aml
  36. JOIN account_journal j ON aml.journal_id = j.id
  37. JOIN account_account acc ON acc.id = aml.account_id
  38. JOIN account_move m ON m.id = aml.move_id
  39. WHERE j.type = 'sale'
  40. AND aml.company_id = %(company_id)s
  41. AND m.state = 'posted'
  42. AND acc.account_type = 'asset_receivable' '''
  43. if first_move_sequence_number is not False and first_move_sequence_number is not None:
  44. params['first_move_sequence_number'] = first_move_sequence_number
  45. query += '''AND m.secure_sequence_number > %(first_move_sequence_number)s'''
  46. elif date_start:
  47. #the first time we compute the closing, we consider only from the installation of the module
  48. params['date_start'] = date_start
  49. query += '''AND m.date >= %(date_start)s'''
  50. query += " ORDER BY m.secure_sequence_number DESC) "
  51. query += '''SELECT array_agg(move_id) AS move_ids,
  52. array_agg(line_id) AS line_ids,
  53. sum(balance) AS balance
  54. FROM aggregate'''
  55. self.env.cr.execute(query, params)
  56. return self.env.cr.dictfetchall()[0]
  57. def _compute_amounts(self, frequency, company):
  58. """
  59. Method used to compute all the business data of the new object.
  60. It will search for previous closings of the same frequency to infer the move from which
  61. account move lines should be fetched.
  62. @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
  63. frequencies are literal (daily means 24 hours and so on)
  64. @param {recordset} company: the company for which the closing is done
  65. @return {dict} containing {field: value} for each business field of the object
  66. """
  67. interval_dates = self._interval_dates(frequency, company)
  68. previous_closing = self.search([
  69. ('frequency', '=', frequency),
  70. ('company_id', '=', company.id)], limit=1, order='sequence_number desc')
  71. first_order = self.env['pos.order']
  72. date_start = interval_dates['interval_from']
  73. cumulative_total = 0
  74. if previous_closing:
  75. first_order = previous_closing.last_order_id
  76. date_start = previous_closing.create_date
  77. cumulative_total += previous_closing.cumulative_total
  78. domain = [('company_id', '=', company.id), ('state', 'in', ('paid', 'done', 'invoiced'))]
  79. if first_order.l10n_fr_secure_sequence_number is not False and first_order.l10n_fr_secure_sequence_number is not None:
  80. domain = AND([domain, [('l10n_fr_secure_sequence_number', '>', first_order.l10n_fr_secure_sequence_number)]])
  81. elif date_start:
  82. #the first time we compute the closing, we consider only from the installation of the module
  83. domain = AND([domain, [('date_order', '>=', date_start)]])
  84. orders = self.env['pos.order'].search(domain, order='date_order desc')
  85. total_interval = sum(orders.mapped('amount_total'))
  86. cumulative_total += total_interval
  87. # We keep the reference to avoid gaps (like daily object during the weekend)
  88. last_order = first_order
  89. if orders:
  90. last_order = orders[0]
  91. return {'total_interval': total_interval,
  92. 'cumulative_total': cumulative_total,
  93. 'last_order_id': last_order.id,
  94. 'last_order_hash': last_order.l10n_fr_secure_sequence_number,
  95. 'date_closing_stop': interval_dates['date_stop'],
  96. 'date_closing_start': date_start,
  97. 'name': interval_dates['name_interval'] + ' - ' + interval_dates['date_stop'][:10]}
  98. def _interval_dates(self, frequency, company):
  99. """
  100. Method used to compute the theoretical date from which account move lines should be fetched
  101. @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
  102. frequencies are literal (daily means 24 hours and so on)
  103. @param {recordset} company: the company for which the closing is done
  104. @return {dict} the theoretical date from which account move lines are fetched.
  105. date_stop date to which the move lines are fetched, always now()
  106. the dates are in their Odoo Database string representation
  107. """
  108. date_stop = datetime.utcnow()
  109. interval_from = None
  110. name_interval = ''
  111. if frequency == 'daily':
  112. interval_from = date_stop - timedelta(days=1)
  113. name_interval = _('Daily Closing')
  114. elif frequency == 'monthly':
  115. month_target = date_stop.month > 1 and date_stop.month - 1 or 12
  116. year_target = month_target < 12 and date_stop.year or date_stop.year - 1
  117. interval_from = date_stop.replace(year=year_target, month=month_target)
  118. name_interval = _('Monthly Closing')
  119. elif frequency == 'annually':
  120. year_target = date_stop.year - 1
  121. interval_from = date_stop.replace(year=year_target)
  122. name_interval = _('Annual Closing')
  123. return {'interval_from': FieldDateTime.to_string(interval_from),
  124. 'date_stop': FieldDateTime.to_string(date_stop),
  125. 'name_interval': name_interval}
  126. def write(self, vals):
  127. raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
  128. @api.ondelete(at_uninstall=True)
  129. def _unlink_never(self):
  130. raise UserError(_('Sale Closings are not meant to be written or deleted under any circumstances.'))
  131. @api.model
  132. def _automated_closing(self, frequency='daily'):
  133. """To be executed by the CRON to create an object of the given frequency for each company that needs it
  134. @param {string} frequency: a valid value of the selection field on the object (daily, monthly, annually)
  135. frequencies are literal (daily means 24 hours and so on)
  136. @return {recordset} all the objects created for the given frequency
  137. """
  138. res_company = self.env['res.company'].search([])
  139. account_closings = self.env['account.sale.closing']
  140. for company in res_company.filtered(lambda c: c._is_accounting_unalterable()):
  141. new_sequence_number = company.l10n_fr_closing_sequence_id.next_by_id()
  142. values = self._compute_amounts(frequency, company)
  143. values['frequency'] = frequency
  144. values['company_id'] = company.id
  145. values['sequence_number'] = new_sequence_number
  146. account_closings |= account_closings.create(values)
  147. return account_closings