200 lines
9.0 KiB
Python
200 lines
9.0 KiB
Python
|
# 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,
|
||
|
}
|