##############################################################################
#
#    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