Skip to content
Snippets Groups Projects
account_move.py 6.04 KiB
Newer Older
##############################################################################
#
#    Accounting periods, for Odoo
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
#    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):
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    """Add a period & dates onto accounting documents."""
    _inherit = "account.move"
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
    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",
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
        ondelete="restrict",
        help="The period this accounting document is in.",
        states={"posted": [("readonly", True), ("required", True)]},
    )

    transaction_date = fields.Date(
        string="Transaction date",
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
        copy=False,
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
            "Invoicing date when provided; otherwise this is the accounting " "date."
        states={"posted": [("readonly", True)]},
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
    @api.model_create_multi
    def create(self, vals_list):
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        """Override to set a transaction date from invoices."""
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
        for vals in vals_list:
            invoice_date = vals.get("invoice_date")
            if invoice_date:
                vals["transaction_date"] = invoice_date
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        return super().create(vals_list)
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
    def action_post(self):
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
        """Override accounting document validation to fill accounting dates &
        period.
        """

        self.fill_accounting_dates()

Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
        return super().action_post()

    def fill_accounting_dates(self):
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
        """- 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.
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
                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:
Vincent Hatakeyama's avatar
Vincent Hatakeyama committed
                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.
            if accdoc.journal_id.type == "sale":
Houzefa Abbasbhay's avatar
Houzefa Abbasbhay committed
                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)

    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