# Copyright 2019 Tecnativa - David Vidal
# Copyright 2020-2021 Tecnativa - Pedro M. Baeza
# Copyright 2021 Tecnativa - Víctor Martínez
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl).
from odoo import api, exceptions, fields, models
from odoo.tools import config


class AccountMove(models.Model):
    _inherit = "account.move"

    # HACK: Looks like UI doesn't behave well with Many2many fields and
    # negative groups when the same field is shown. In this case, we want to
    # show the readonly version to any not in the global discount group.
    # TODO: Check if it's fixed in future versions
    global_discount_ids_readonly = fields.Many2many(
        string="Invoice Global Discounts (readonly)",
        related="global_discount_ids",
        readonly=True,
    )
    global_discount_ids = fields.Many2many(
        comodel_name="global.discount",
        column1="invoice_id",
        column2="global_discount_id",
        string="Invoice Global Discounts",
        domain="[('discount_scope', 'in', {"
        "    'out_invoice': ['sale'], "
        "    'out_refund': ['sale'], "
        "    'in_refund': ['purchase'], "
        "    'in_invoice': ['purchase']"
        "}.get(move_type, [])), ('account_id', '!=', False), '|', "
        "('company_id', '=', company_id), ('company_id', '=', False)]",
        compute="_compute_global_discount_ids",
        store=True,
        readonly=False,
    )
    amount_global_discount = fields.Monetary(
        string="Total Global Discounts",
        compute="_compute_amount",
        currency_field="currency_id",
        readonly=True,
        compute_sudo=True,
        store=True,
    )
    amount_untaxed_before_global_discounts = fields.Monetary(
        string="Amount Untaxed Before Discounts",
        compute="_compute_amount",
        currency_field="currency_id",
        readonly=True,
        compute_sudo=True,
        store=True,
    )
    invoice_global_discount_ids = fields.One2many(
        comodel_name="account.invoice.global.discount",
        inverse_name="invoice_id",
        readonly=True,
    )

    def _prepare_global_discount_vals(self, global_discount, base, tax_ids):
        """Prepare the dictionary values for an invoice global discount
        line.
        """
        self.ensure_one()
        discount = global_discount._get_global_discount_vals(base)
        return {
            "name": global_discount.display_name,
            "invoice_id": self.id,
            "global_discount_id": global_discount.id,
            "discount": global_discount.discount,
            "base": base,
            "base_discounted": discount["base_discounted"],
            "account_id": global_discount.account_id.id,
            "tax_ids": [(4, tax_id) for tax_id in tax_ids],
        }

    def _set_global_discounts_by_tax(self):
        """Create invoice global discount lines by taxes combinations and
        discounts.

        This also resets previous global discounts in case they existed.
        """
        self.ensure_one()
        if not self.is_invoice():
            return
        in_draft_mode = self != self._origin
        taxes_keys = {}
        # Perform a sanity check for discarding cases that will lead to
        # incorrect data in discounts
        _self = self.filtered("global_discount_ids")
        for inv_line in _self.invoice_line_ids.filtered(
            lambda line: line.display_type not in ["line_section", "line_note"]
        ):
            if inv_line.product_id.bypass_global_discount:
                continue
            taxes_keys.setdefault(tuple(inv_line.tax_ids.ids), 0)
            taxes_keys[tuple(inv_line.tax_ids.ids)] += inv_line.price_subtotal
        # Reset previous global discounts
        self.invoice_global_discount_ids -= self.invoice_global_discount_ids
        model = "account.invoice.global.discount"
        create_method = in_draft_mode and self.env[model].new or self.env[model].create
        for tax_line in _self.line_ids.filtered("tax_line_id"):
            key = []
            discount_line_base = 0
            for key in taxes_keys:
                if tax_line.tax_line_id.id in key:
                    discount_line_base = taxes_keys[key]
                    taxes_keys[key] = 0  # mark for not duplicating
                    break  # we leave in key variable the proper taxes value
            if not discount_line_base:
                continue
            for global_discount in self.global_discount_ids:
                vals = self._prepare_global_discount_vals(
                    global_discount, discount_line_base, key
                )
                create_method(vals)
                discount_line_base = vals["base_discounted"]
        _self._set_global_discounts_by_zero_tax(taxes_keys, create_method)

    def _set_global_discounts_by_zero_tax(self, taxes_keys, create_method):
        # Check all moves with defined taxes to check if there's any discount not
        # created (tax amount is zero and only one tax is applied)
        base_total = 0
        zero_taxes = self.env["account.tax"]
        for line in self.line_ids.filtered("tax_ids"):
            if line.product_id.bypass_global_discount:
                continue
            key = tuple(line.tax_ids.ids)
            if taxes_keys.get(key):
                base_total += line.price_subtotal
                zero_taxes |= line.tax_ids
        for global_discount in self.global_discount_ids:
            if not base_total:
                break
            vals = self._prepare_global_discount_vals(
                global_discount, base_total, zero_taxes.ids
            )
            create_method(vals)
            base_total = vals["base_discounted"]

    def _recompute_global_discount_lines(self):
        """Append global discounts move lines.

        This is called when recomputing dynamic lines before calling
        `_recompute_payment_terms_lines`, but after calling `_recompute_tax_lines`.
        """
        self.ensure_one()
        in_draft_mode = self != self._origin
        model = "account.move.line"
        create_method = in_draft_mode and self.env[model].new or self.env[model].create
        for discount in self.invoice_global_discount_ids.filtered("discount"):
            sign = -1 if self.move_type in {"in_invoice", "out_refund"} else 1
            disc_amount = sign * discount.discount_amount
            disc_amount_company_currency = disc_amount
            if self.currency_id != self.company_id.currency_id:
                disc_amount_company_currency = self.currency_id._convert(
                    disc_amount,
                    self.company_id.currency_id,
                    self.company_id,
                    self.date or fields.Date.context_today(self),
                )
            create_method(
                {
                    "invoice_global_discount_id": discount.id,
                    "move_id": self.id,
                    "name": "{} - {}".format(
                        discount.name, ", ".join(discount.tax_ids.mapped("name"))
                    ),
                    "debit": disc_amount_company_currency > 0.0
                    and disc_amount_company_currency
                    or 0.0,
                    "credit": disc_amount_company_currency < 0.0
                    and -disc_amount_company_currency
                    or 0.0,
                    "amount_currency": (disc_amount > 0.0 and disc_amount or 0.0)
                    - (disc_amount < 0.0 and -disc_amount or 0.0),
                    "account_id": discount.account_id.id,
                    "tax_ids": [(4, x.id) for x in discount.tax_ids],
                    "partner_id": self.commercial_partner_id.id,
                    "currency_id": self.currency_id.id,
                    "price_unit": -1 * abs(disc_amount_company_currency),
                }
            )

    @api.depends("partner_id", "company_id", "move_type")
    def _compute_global_discount_ids(self):
        for move in self:
            discounts = self.env["global.discount"]
            move_discounts = self.env["global.discount"]
            if (
                move.move_type in ["out_invoice", "out_refund"]
                and move.partner_id.customer_global_discount_ids
            ):
                move_discounts = move.partner_id.customer_global_discount_ids
            elif (
                move.move_type in ["in_refund", "in_invoice"]
                and move.partner_id.supplier_global_discount_ids
            ):
                move_discounts = move.partner_id.supplier_global_discount_ids
            for discount in move_discounts:
                if discount.company_id == move.company_id:
                    discounts |= discount
            move.global_discount_ids = discounts

    def _compute_amount_one(self):
        """Perform totals computation of a move with global discounts."""
        if not self.invoice_global_discount_ids:
            self.amount_global_discount = 0.0
            self.amount_untaxed_before_global_discounts = 0.0
            return
        round_curr = self.currency_id.round
        self.amount_global_discount = sum(
            round_curr(discount.discount_amount) * -1
            for discount in self.invoice_global_discount_ids
        )
        self.amount_untaxed_before_global_discounts = (
            self.amount_untaxed - self.amount_global_discount
        )

    @api.depends(
        "line_ids.matched_debit_ids.debit_move_id.move_id.origin_payment_id.is_matched",
        "line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual",
        "line_ids.matched_debit_ids.debit_move_id.move_id.line_ids.amount_residual_currency",
        "line_ids.matched_credit_ids.credit_move_id.move_id.origin_payment_id.is_matched",
        "line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual",
        "line_ids.matched_credit_ids.credit_move_id.move_id.line_ids.amount_residual_currency",
        "line_ids.balance",
        "line_ids.currency_id",
        "line_ids.amount_currency",
        "line_ids.amount_residual",
        "line_ids.amount_residual_currency",
        "line_ids.payment_id.state",
        "line_ids.full_reconcile_id",
        "invoice_global_discount_ids",
        "global_discount_ids",
    )
    def _compute_amount(self):
        """Modify totals computation for including global discounts."""
        res = super()._compute_amount()
        for record in self:
            record._compute_amount_one()
        return res

    def _clean_global_discount_lines(self):
        self.ensure_one()
        gbl_disc_lines = self.env["account.move.line"].search(
            [
                ("move_id", "=", self.id),
                ("invoice_global_discount_id", "!=", False),
            ]
        )
        if gbl_disc_lines:
            move_container = {"records": self}
            with (
                self._check_balanced(move_container),
                self._sync_dynamic_lines(move_container),
            ):
                gbl_disc_lines.unlink()

    @api.model_create_multi
    def create(self, vals_list):
        """If we create the invoice with the discounts already set like from
        a sales order, we must compute the global discounts as well, as some data
        like ``tax_ids`` is not set until the final step.
        """
        moves = super().create(vals_list)
        for move in moves:
            if move.move_type in ["out_refund", "in_refund"]:
                move._clean_global_discount_lines()
            move._set_global_discounts_by_tax()
            move._recompute_global_discount_lines()
        return moves

    def write(self, vals):
        res = super().write(vals)
        if "invoice_line_ids" in vals or "global_discount_ids" in vals:
            for move in self:
                move._clean_global_discount_lines()
                move._set_global_discounts_by_tax()
                move._recompute_global_discount_lines()
            move_container = {"records": self}
            self._global_discount_check(move_container)
        return res

    def _global_discount_check(self, container):
        test_condition = not config["test_enable"] or self.env.context.get(
            "test_account_global_discount"
        )
        for move in container["records"]:
            if not move.is_invoice() or not move.global_discount_ids:
                continue
            taxes_keys = {}
            for inv_line in move.invoice_line_ids:
                if inv_line.display_type != "product":
                    continue
                if not inv_line.tax_ids and test_condition:
                    raise exceptions.UserError(
                        self.env._("With global discounts, taxes in lines are required")
                    )
                for key in taxes_keys:
                    if key == tuple(inv_line.tax_ids.ids):
                        break
                    elif set(key) & set(inv_line.tax_ids.ids) and test_condition:
                        raise exceptions.UserError(
                            self.env._("Incompatible taxes found for global discounts.")
                        )
                else:
                    taxes_keys[tuple(inv_line.tax_ids.ids)] = True
        return True


class AccountMoveLine(models.Model):
    _inherit = "account.move.line"

    invoice_global_discount_id = fields.Many2one(
        comodel_name="account.invoice.global.discount",
        string="Invoice Global Discount",
    )
    base_before_global_discounts = fields.Monetary(
        string="Amount Untaxed Before Discounts",
        readonly=True,
    )


class AccountInvoiceGlobalDiscount(models.Model):
    _name = "account.invoice.global.discount"
    _description = "Invoice Global Discount"

    name = fields.Char(string="Discount Name", required=True)
    invoice_id = fields.Many2one(
        "account.move",
        string="Invoice",
        ondelete="cascade",
        index=True,
        readonly=True,
        domain=[
            (
                "move_type",
                "in",
                ["out_invoice", "out_refund", "in_invoice", "in_refund"],
            )
        ],
    )
    global_discount_id = fields.Many2one(
        comodel_name="global.discount",
        string="Global Discount",
    )
    discount = fields.Float(string="Discount (number)")
    discount_display = fields.Char(
        compute="_compute_discount_display",
        string="Discount",
    )
    base = fields.Float(string="Base before discount", digits="Product Price")
    base_discounted = fields.Float(string="Base after discount", digits="Product Price")
    currency_id = fields.Many2one(related="invoice_id.currency_id", readonly=True)
    discount_amount = fields.Monetary(
        string="Discounted Amount",
        compute="_compute_discount_amount",
        currency_field="currency_id",
        compute_sudo=True,
    )
    tax_ids = fields.Many2many(comodel_name="account.tax", string="Taxes")
    account_id = fields.Many2one(
        comodel_name="account.account",
        required=True,
        string="Account",
        domain=(
            "[('account_type', 'not in', ['asset_receivable', 'liability_payable'])]"
        ),
    )
    account_analytic_id = fields.Many2one(
        comodel_name="account.analytic.account",
        string="Analytic account",
    )
    company_id = fields.Many2one(related="invoice_id.company_id", readonly=True)

    def _compute_discount_display(self):
        """Given a discount type, we need to render a different symbol"""
        for one in self:
            precision = self.env["decimal.precision"].precision_get("Discount")
            one.discount_display = "{0:.{1}f}%".format(one.discount * -1, precision)

    @api.depends("base", "base_discounted")
    def _compute_discount_amount(self):
        """Compute the amount discounted"""
        for one in self:
            one.discount_amount = one.base - one.base_discounted
