# Copyright 2019-21 ForgeFlow S.L..
# License AGPL-3.0 or later (https://www.gnu.org/licenses/agpl.html).

from odoo import _, api, exceptions, fields, models
from odoo.osv import expression
from odoo.tools import float_is_zero


class PurchaseOrder(models.Model):
    _inherit = "purchase.order"

    unreconciled = fields.Boolean(
        compute="_compute_unreconciled",
        search="_search_unreconciled",
        help="Indicates that a Purchase Order has related Journal items not "
        "reconciled.\nNote that if it is false it can be either that "
        "everything is reconciled or that the related accounts do not "
        "allow reconciliation",
    )
    amount_unreconciled = fields.Float(compute="_compute_unreconciled")

    def _get_account_domain(self):
        included_accounts = (
            (
                self.env["product.category"]
                .with_company(self.company_id.id)
                .search([("property_valuation", "=", "real_time")])
            )
            .mapped("property_stock_account_input_categ_id")
            .ids
        )
        return [("account_id", "in", included_accounts)]

    @api.model
    def _get_purchase_unreconciled_base_domain(self):
        unreconciled_domain = [
            ("account_id.reconcile", "=", True),
            ("move_id.state", "=", "posted"),
            ("company_id", "in", self.env.companies.ids),
            # same condition than Odoo Unreconciled filter
            ("amount_residual", "!=", 0.0),
            ("balance", "!=", 0.0),
        ]
        return unreconciled_domain

    def _compute_unreconciled(self):
        acc_item = self.env["account.move.line"]
        for rec in self:
            domain = rec.with_company(
                rec.company_id
            )._get_purchase_unreconciled_base_domain()
            domain_account = rec._get_account_domain()
            unreconciled_domain = expression.AND([domain, domain_account])
            unreconciled_domain = expression.AND(
                [unreconciled_domain, [("purchase_order_id", "=", rec.id)]]
            )
            unreconciled_items = acc_item.search(unreconciled_domain)
            rec.unreconciled = len(unreconciled_items) > 0
            rec.amount_unreconciled = sum(unreconciled_items.mapped("amount_residual"))

    def _search_unreconciled(self, operator, value):
        if operator not in ("=", "!=") or not isinstance(value, bool):
            raise ValueError(_("Unsupported search operator"))
        acc_item = self.env["account.move.line"]
        domain = self._get_purchase_unreconciled_base_domain()
        domain = expression.AND([domain, [("purchase_order_id", "!=", False)]])
        domain_account = self._get_account_domain()
        domain = expression.AND([domain_account, domain])
        acc_items = acc_item.search(domain)
        unreconciled_pos_ids = acc_items.mapped("purchase_order_id").ids
        if value:
            return [("id", "in", unreconciled_pos_ids)]
        else:
            return [("id", "not in", unreconciled_pos_ids)]

    def action_view_unreconciled(self):
        self.ensure_one()
        acc_item = self.env["account.move.line"]
        domain = self.with_company(
            self.company_id.id
        )._get_purchase_unreconciled_base_domain()
        domain_account = self._get_account_domain()
        unreconciled_domain = expression.AND([domain, domain_account])
        unreconciled_domain = expression.AND(
            [unreconciled_domain, [("purchase_order_id", "=", self.id)]]
        )
        unreconciled_domain.remove(("amount_residual", "!=", 0.0))
        unreconciled_domain.remove("&")
        unreconciled_items = acc_item.search(unreconciled_domain)
        action = self.env.ref("account.action_account_moves_all")
        action_dict = action.read()[0]
        action_dict["domain"] = [("id", "in", unreconciled_items.ids)]
        return action_dict

    def action_reconcile(self):
        if (
            not self.company_id.purchase_reconcile_account_id
            or not self.company_id.purchase_reconcile_journal_id
        ):
            raise exceptions.ValidationError(
                _(
                    "The write-off account and journal for purchases is missing. An "
                    "accountant must fill that information"
                )
            )
        self.ensure_one()
        res = {}
        domain = self._get_purchase_unreconciled_base_domain()
        domain_account = self._get_account_domain()
        unreconciled_domain = expression.AND([domain, domain_account])
        unreconciled_domain = expression.AND(
            [domain, [("purchase_order_id", "=", self.id)]]
        )
        unreconciled_domain = expression.AND(
            [unreconciled_domain, [("company_id", "=", self.company_id.id)]]
        )
        writeoff_to_reconcile = self.env["account.move.line"]
        all_writeoffs = self.env["account.move.line"]
        reconciling_groups = self.env["account.move.line"].read_group(
            domain=unreconciled_domain,
            fields=["account_id", "product_id", "oca_purchase_line_id"],
            groupby=["account_id", "product_id", "oca_purchase_line_id"],
            lazy=False,
        )
        unreconciled_items = self.env["account.move.line"].search(unreconciled_domain)
        for group in reconciling_groups:
            account_id = group["account_id"][0]
            product_id = group["product_id"][0] if group["product_id"] else False
            purchase_line_id = (
                group["oca_purchase_line_id"][0]
                if group["oca_purchase_line_id"]
                else False
            )
            unreconciled_items_group = unreconciled_items.filtered(
                lambda line, account_id=account_id, product_id=product_id: (
                    line.account_id.id == account_id
                    and line.product_id.id == product_id
                )
            )
            # Check which type of force reconciling we are doing:
            # - Force reconciling amount_residual
            # - Force reconciling amount_residual_currency
            amount_residual_currency_reconcile = any(
                unreconciled_items_group.filtered(
                    lambda item_group,
                    account_id=account_id: item_group.amount_residual_currency != 0.0
                    and item_group.account_id.id == account_id
                )
            )
            if amount_residual_currency_reconcile:
                residual_field = "amount_residual_currency"
            else:
                residual_field = "amount_residual"
            if float_is_zero(
                sum(unreconciled_items_group.mapped(residual_field)),
                precision_rounding=self.company_id.currency_id.rounding,
            ):
                moves_to_reconcile = unreconciled_items_group
            else:
                writeoff_vals = self._get_purchase_writeoff_vals(
                    unreconciled_items_group, purchase_line_id, product_id
                )
                writeoff_to_reconcile = unreconciled_items_group._create_writeoff(
                    writeoff_vals
                )
                all_writeoffs |= writeoff_to_reconcile
                # add writeoff line to reconcile algorithm and finish the reconciliation
                moves_to_reconcile = unreconciled_items_group | writeoff_to_reconcile
            # Check if reconciliation is total or needs an exchange rate entry to be
            # created
            if moves_to_reconcile:
                moves_to_reconcile.filtered(
                    lambda move: not move.reconciled
                ).reconcile()
            reconciled_ids = unreconciled_items | all_writeoffs
            res = {
                "name": _("Reconciled journal items"),
                "type": "ir.actions.act_window",
                "view_type": "form",
                "view_mode": "list,form",
                "res_model": "account.move.line",
                "domain": [("id", "in", reconciled_ids.ids)],
            }
        if self.env.context.get("bypass_unreconciled", False):
            # When calling the method from the wizard, lock after reconciling
            self.button_done()
        return res

    def _get_purchase_writeoff_vals(self, amls, purchase_line_id, product_id):
        writeoff_date = self.env.context.get("writeoff_date", False)
        aml_date = max(amls.mapped("move_id.date"))
        res = {
            "account_id": self.company_id.purchase_reconcile_account_id.id,
            "journal_id": self.company_id.purchase_reconcile_journal_id.id,
            "purchase_order_id": self.id,
            "purchase_line_id": purchase_line_id or False,
            "product_id": product_id,
            "currency_id": self.currency_id.id or self.env.company.currency_id.id,
            "date": aml_date,
        }
        # hook for custom date:
        if writeoff_date:
            res.update({"date": writeoff_date})
        return res

    def reconcile_criteria(self):
        """Gets the criteria where POs are locked or not, by default uses the company
        configuration"""
        self.ensure_one()
        return self.unreconciled and self.company_id.purchase_lock_auto_reconcile

    def button_done(self):
        for rec in self:
            criteria = rec.reconcile_criteria()
            if criteria:
                if rec.unreconciled:
                    exception_msg = rec.unreconciled_exception_msg()
                    if exception_msg:
                        res = rec.purchase_unreconciled_exception(exception_msg)
                        return res
                    else:
                        rec.action_reconcile()
                        return super(PurchaseOrder, rec).button_done()
                else:
                    return super(PurchaseOrder, rec).button_done()
            else:
                return super(PurchaseOrder, rec).button_done()

    def purchase_unreconciled_exception(self, exception_msg=None):
        """This mean to be run when the SO cannot be reconciled because it is over
        tolerance"""
        self.ensure_one()
        if exception_msg:
            return (
                self.env["purchase.unreconciled.exceeded.wiz"]
                .create(
                    {
                        "exception_msg": exception_msg,
                        "purchase_id": self.id,
                        "origin_reference": "{},{}".format("purchase.order", self.id),
                        "continue_method": "action_reconcile",
                    }
                )
                .action_show()
            )

    def unreconciled_exception_msg(self):
        self.ensure_one()
        exception_msg = ""
        amount_total = self.amount_total
        if self.currency_id and self.company_id.currency_id != self.currency_id:
            amount_total = self.currency_id._convert(
                amount_total,
                self.company_id.currency_id,
                self.company_id,
                fields.Date.today(),
            )
        if (
            self.company_id.purchase_reconcile_tolerance
            and amount_total
            and abs(self.amount_unreconciled / amount_total)
            >= self.company_id.purchase_reconcile_tolerance / 100.0
        ):
            params = {
                "amount_unreconciled": self.amount_unreconciled,
                "amount_allowed": self.amount_total
                * self.company_id.purchase_reconcile_tolerance
                / 100.0,
            }
            exception_msg = (
                _(
                    "Finance Warning: \nUnreconciled amount is too high. Total "
                    "unreconciled amount: %(amount_unreconciled)s Maximum unreconciled"
                    " amount accepted: %(amount_allowed)s "
                )
                % params
            )
        return exception_msg
