############################################################################## # # Accounting periods, for Odoo # Copyright (C) 2018 XCG Consulting <http://odoo.consulting> # # 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 odoo.modules.module as odoo_module_manager from odoo import _, api, exceptions, fields, models from odoo.tools import config class AccountMove(models.Model): # pylint: disable=no-member """Add a period & dates onto accounting documents. See module README for details. """ _inherit = "account.move" # This field is kind of a duplicate of the account date field. # Instead of defaulting to today, it is empty while the move is not posted accounting_date = fields.Date( string="Accounting date", copy=False, help="Validation date of the accounting document.", readonly=True, states={"draft": [("readonly", False)]}, ) 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 create method to set a transaction date from invoices. Args: vals_list (list): List of dictionaries containing the values for each record to create. Returns: recordset: Recordset of the created records. """ for vals in vals_list: self._update_dates(vals) return super().create(vals_list) def write(self, vals): """ Override write method to update date fields. Args: vals (dict): Dictionary containing the values to update. Returns: bool: True if the write operation was successful, False otherwise. """ if ( "date" in vals or "accounting_date" in vals or "invoice_date" in vals and not self.env.context.get("posting", False) ): result = True # compatibility with account. for move in self: values = vals.copy() if values.get("state", move.state) != "posted": # if accounting date is put to False, date is left as is. self._update_dates(values) result &= super(AccountMove, move).write(values) return result return super().write(vals) def _update_dates(self, vals): """ Private method to update date fields in `vals`. Args: vals (dict): Dictionary containing the values to update. Returns: None """ invoice_date = vals.get("invoice_date") if invoice_date: vals["transaction_date"] = invoice_date if not self and invoice_date: vals["date"] = invoice_date # compatibility with account tests and any module expecting setting # the date would change the accounting date. if "accounting_date" in vals and vals["accounting_date"]: vals["date"] = vals["accounting_date"] elif "date" in vals: vals["accounting_date"] = vals["date"] @api.onchange("invoice_date") def _onchange_invoice_date(self): """Overrides the original `_onchange_invoice_date` method to customize its behavior. The line `self.date = self.invoice_date` has been removed in this override to avoid updating the 'date' field. The 'date' field is expected to be updated in a different context or by another method. Removing this line ensures that only the relevant fields related to the invoice date are updated without affecting other fields. """ if self.invoice_date: if not self.invoice_payment_term_id and ( not self.invoice_date_due or self.invoice_date_due < self.invoice_date ): self.invoice_date_due = self.invoice_date if not self.env.context.get("skip_date_update", False): self.date = self.invoice_date self._onchange_currency() def post(self): """Override accounting document validation to fill accounting dates & period. """ self.with_context(posting=True).fill_accounting_dates() # Don't block during tests that are not aware of this module (tests in # the base "account" module, notably). current_test = odoo_module_manager.current_test if current_test is not None and current_test != "account_period": return super().post() return super( AccountMove, self.with_context(skip_date_update=True) ).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"): # XXX accdoc.date is ever empty, it is a computed field # 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: # if doing tests, and any matching period not closed if config["test_enable"] and not self.env[ "account.period" ].search( [ ("company_id", "=", company.id), ("date_start", "<=", acc_date), ("date_effective_cutoff", ">=", acc_date), ("state", "=", "done"), ], limit=1, ): period = ( self.env["account.period"] .sudo() .find(acc_date, create=True) ) else: raise exceptions.Warning( _( "No period found around %(date)s in the " '"%(company_name)s" company.' ) % { "date": acc_date, "company_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, } # This is a change from the standard that does not expect the field # to be automatically modified on post. For non invoices, the # standard expects the date field to be manually updated before # posting; there is no tests that validate its value except for # some specific cases where it is set. This is covered by the code # in the create and write. if accdoc.date != acc_date: accdoc_values["date"] = acc_date # 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