############################################################################## # # Accounting periods, for Odoo # Copyright (C) 2018, 2022 XCG Consulting <https://xcg-consulting.fr/> # # This program is free software: you can redistribute it and/or modify # it under the terms of the GNU Affero General Public License as # published by the Free Software Foundation, either version 3 of the # License, or (at your option) any later version. # # This program is distributed in the hope that it will be useful, # but WITHOUT ANY WARRANTY; without even the implied warranty of # MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the # GNU Affero General Public License for more details. # # You should have received a copy of the GNU Affero General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. # ############################################################################## import logging from math import log10 from dateutil.relativedelta import relativedelta from odoo import _, api, exceptions, fields, models, tools _logger = logging.getLogger(__name__) class AccountMove(models.Model): """Add a period & dates onto accounting documents.""" _inherit = "account.move" accounting_date = fields.Date( string="Accounting date", copy=False, help="Validation date of the accounting document.", states={"posted": [("readonly", True)]}, ) period_id = fields.Many2one( comodel_name="account.period", string="Period", ondelete="restrict", help="The period this accounting document is in.", states={"posted": [("readonly", True), ("required", True)]}, ) transaction_date = fields.Date( string="Transaction date", copy=False, help=( "Invoicing date when provided; otherwise this is the accounting " "date." ), states={"posted": [("readonly", True)]}, ) @api.model_create_multi def create(self, vals_list): """Override to set a transaction date from invoices.""" for vals in vals_list: invoice_date = vals.get("invoice_date") if invoice_date: vals["transaction_date"] = invoice_date return super().create(vals_list) def action_post(self): """Override accounting document validation to fill accounting dates & period. """ self.fill_accounting_dates() return super().action_post() def fill_accounting_dates(self): """- Set the accounting date. - Also set the transaction date ("transaction_date" field) when empty. - Force the period to always be around the current date. - Only select open periods. """ today = fields.Date.today() for accdoc in self: # Cache some data. acc_date = accdoc.accounting_date or today company = accdoc.company_id # Set the acc_date only if the force_period_on_date # context has provided. if self.env.context.get("force_period_on_date"): # If accounting document date is empty, get today date. acc_date = accdoc.date or acc_date # Periods are ordered by date so selecting the first one is fine. period = self.env["account.period"].search( [ ("company_id", "=", company.id), ("date_start", "<=", acc_date), ("date_effective_cutoff", ">=", acc_date), ("state", "!=", "done"), ], limit=1, ) if not period: raise exceptions.UserError( _("No period found around %(date)s in the company %(name)s.") % {"date": acc_date, "name": company.sudo().name} ) # When we are between the period end and cut-off date, force the # last day of the period. in_cutoff = False if accdoc.journal_id.type == "sale": period_end = period.date_stop in_cutoff = acc_date > period_end acc_date = period_end if in_cutoff else acc_date # The data to update the accounting document with. accdoc_values = { "accounting_date": acc_date, "period_id": period.id, } # Set a transaction date when no previous one set. Also, force it # during cut-off. if in_cutoff or not accdoc.transaction_date: accdoc_values["transaction_date"] = acc_date # Ready! Update the accounting document. accdoc.write(accdoc_values) return True def _populate_factories(self) -> list: """Add periods to the generated account.move""" today = fields.Date.today() def get_accounting_date(values, counter, random): """return an accounting date""" accounting_date = ( values["date"].date() if values["date"].date() < today else None ) # make sure target period exists (or action_post will throw an exception) if accounting_date: # make it random between the date and today seconds_after = (today - accounting_date).total_seconds() accounting_date = accounting_date + relativedelta( seconds=seconds_after * -log10(0.001 + 0.999 * random.random()) / 3 ) # make sure that period exists self.env["account.period"].with_context( company_id=values["company_id"] ).find(accounting_date.date(), True) return accounting_date result = super()._populate_factories() result.append( # set some accounting_date so that when posting everything does not end up # on the current period ("accounting_date", tools.populate.compute(get_accounting_date)) ) return result