1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199
|
# 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,
}
|