# Part of Odoo. See LICENSE file for full copyright and licensing details.

from odoo import api, fields, models, _

from odoo.tools import SQL
from odoo.exceptions import UserError


class PurchaseBillMatch(models.Model):
    _name = "purchase.bill.line.match"
    _description = "Purchase Line and Vendor Bill line matching view"
    _auto = False
    _order = 'product_id, aml_id, pol_id'

    pol_id = fields.Many2one(comodel_name='purchase.order.line')
    aml_id = fields.Many2one(comodel_name='account.move.line')
    company_id = fields.Many2one(comodel_name='res.company')
    partner_id = fields.Many2one(comodel_name='res.partner')
    product_id = fields.Many2one(comodel_name='product.product')
    line_qty = fields.Float()
    line_uom_id = fields.Many2one(comodel_name='uom.uom')
    qty_invoiced = fields.Float()
    purchase_order_id = fields.Many2one(comodel_name='purchase.order')
    account_move_id = fields.Many2one(comodel_name='account.move')
    line_amount_untaxed = fields.Monetary()
    currency_id = fields.Many2one(comodel_name='res.currency')
    state = fields.Char()

    product_uom_id = fields.Many2one(comodel_name='uom.uom', related='product_id.uom_id')
    product_uom_qty = fields.Float(compute='_compute_product_uom_qty', inverse='_inverse_product_uom_qty', readonly=False)
    product_uom_price = fields.Float(compute='_compute_product_uom_price', inverse='_inverse_product_uom_price', readonly=False)
    billed_amount_untaxed = fields.Monetary(compute='_compute_amount_untaxed_fields', currency_field='currency_id')
    purchase_amount_untaxed = fields.Monetary(compute='_compute_amount_untaxed_fields', currency_field='currency_id')
    reference = fields.Char(compute='_compute_reference')

    @api.onchange('product_uom_price')
    def _inverse_product_uom_price(self):
        for line in self:
            if line.aml_id:
                line.aml_id.price_unit = line.product_uom_price
            else:
                line.pol_id.price_unit = line.product_uom_price

    @api.onchange('product_uom_qty')
    def _inverse_product_uom_qty(self):
        for line in self:
            if line.aml_id:
                line.aml_id.quantity = line.product_uom_qty
            else:
                # on POL, setting product_qty will recompute price_unit to have the old value
                # this prevents the price to revert by saving the previous price and re-setting them again
                previous_price_unit = line.pol_id.price_unit
                line.pol_id.product_qty = line.product_uom_qty
                line.pol_id.price_unit = previous_price_unit

    def _compute_amount_untaxed_fields(self):
        for line in self:
            line.billed_amount_untaxed = line.line_amount_untaxed if line.account_move_id else False
            line.purchase_amount_untaxed = line.line_amount_untaxed if line.purchase_order_id else False

    def _compute_reference(self):
        for line in self:
            line.reference = line.purchase_order_id.display_name or line.account_move_id.display_name

    def _compute_display_name(self):
        for line in self:
            line.display_name = line.product_id.display_name or line.aml_id.name or line.pol_id.name

    def _compute_product_uom_qty(self):
        for line in self:
            line.product_uom_qty = line.line_uom_id._compute_quantity(line.line_qty, line.product_uom_id)

    @api.depends('aml_id.price_unit', 'pol_id.price_unit')
    def _compute_product_uom_price(self):
        for line in self:
            line.product_uom_price = line.aml_id.price_unit if line.aml_id else line.pol_id.price_unit

    @api.model
    def _select_po_line(self):
        return SQL("""
            SELECT pol.id,
                   pol.id as pol_id,
                   NULL as aml_id,
                   pol.company_id as company_id,
                   pol.partner_id as partner_id,
                   pol.product_id as product_id,
                   pol.product_qty as line_qty,
                   pol.product_uom as line_uom_id,
                   pol.qty_invoiced as qty_invoiced,
                   po.id as purchase_order_id,
                   NULL as account_move_id,
                   pol.price_subtotal as line_amount_untaxed,
                   pol.currency_id as currency_id,
                   po.state as state
              FROM purchase_order_line pol
         LEFT JOIN purchase_order po ON pol.order_id = po.id
             WHERE pol.state in ('purchase', 'done')
               AND pol.product_qty > pol.qty_invoiced
                OR ((pol.display_type = '' OR pol.display_type IS NULL) AND pol.is_downpayment AND pol.qty_invoiced > 0)
        """)

    @api.model
    def _select_am_line(self):
        return SQL("""
            SELECT -aml.id,
                   NULL as pol_id,
                   aml.id as aml_id,
                   aml.company_id as company_id,
                   aml.partner_id as partner_id,
                   aml.product_id as product_id,
                   aml.quantity as line_qty,
                   aml.product_uom_id as line_uom_id,
                   NULL as qty_invoiced,
                   NULL as purchase_order_id,
                   am.id as account_move_id,
                   aml.amount_currency as line_amount_untaxed,
                   aml.currency_id as currency_id,
                   aml.parent_state as state
              FROM account_move_line aml
         LEFT JOIN account_move am on aml.move_id = am.id
             WHERE aml.display_type = 'product'
               AND am.move_type in ('in_invoice', 'in_refund')
               AND aml.parent_state in ('draft', 'posted')
               AND aml.purchase_line_id IS NULL
        """)

    @property
    def _table_query(self):
        return SQL("%s UNION ALL %s", self._select_po_line(), self._select_am_line())

    def action_open_line(self):
        self.ensure_one()
        return {
            'type': 'ir.actions.act_window',
            'res_model': 'account.move' if self.account_move_id else 'purchase.order',
            'view_mode': 'form',
            'res_id': self.account_move_id.id if self.account_move_id else self.purchase_order_id.id,
        }

    @api.model
    def _action_create_bill_from_po_lines(self, partner, po_lines):
        """ Create a new vendor bill with the selected PO lines and returns an action to open it """
        bill = self.env['account.move'].create({
            'move_type': 'in_invoice',
            'partner_id': partner.id,
        })
        bill._add_purchase_order_lines(po_lines)
        return bill._get_records_action()

    def action_match_lines(self):
        if not self.pol_id:  # we need POL(s) to either match or create bill
            raise UserError(_("You must select at least one Purchase Order line to match or create bill."))
        if not self.aml_id:  # select POL(s) without AML -> create a draft bill with the POL(s)
            return self._action_create_bill_from_po_lines(self.partner_id, self.pol_id)
        if len(self.aml_id.move_id) > 1:  # for purchase matching, disallow matching multiple bills at the same time
            raise UserError(_("You can't select lines from multiple Vendor Bill to do the matching."))

        pol_by_product = self.pol_id.grouped('product_id')
        aml_by_product = self.aml_id.grouped('product_id')
        residual_purchase_order_lines = self.pol_id
        residual_account_move_lines = self.aml_id
        residual_bill = self.aml_id.move_id

        # Match all matchable POL-AML lines and remove them from the residual group
        for product, po_line in pol_by_product.items():
            po_line = po_line[0]  # in case of multiple POL with same product, only match the first one
            matching_bill_lines = aml_by_product.get(product)
            if matching_bill_lines:
                matching_bill_lines.purchase_line_id = po_line.id
                residual_purchase_order_lines -= po_line
                residual_account_move_lines -= matching_bill_lines

        # Delete all unmatched selected AML
        if residual_account_move_lines:
            residual_account_move_lines.unlink()

        # Add all remaining POL to the residual bill
        residual_bill._add_purchase_order_lines(residual_purchase_order_lines)

    def action_add_to_po(self):
        if not self or not self.aml_id:
            raise UserError(_("Select Vendor Bill lines to add to a Purchase Order"))
        context = {
            'default_partner_id': self.partner_id.id,
            'dialog_size': 'medium',
            'has_products': bool(self.aml_id.product_id),
        }
        if len(self.purchase_order_id) > 1:
            raise UserError(_("Vendor Bill lines can only be added to one Purchase Order."))
        elif self.purchase_order_id:
            context['default_purchase_order_id'] = self.purchase_order_id.id
        return {
            'type': 'ir.actions.act_window',
            'name': _("Add to Purchase Order"),
            'res_model': 'bill.to.po.wizard',
            'target': 'new',
            'views': [(self.env.ref('purchase.bill_to_po_wizard_form').id, 'form')],
            'context': context,
        }
