[FIX] purchase_stock,stock_account: create only one SVL for price diff

When confirming a bill with a price diff, if the user received, returned
and received again the products, some unexpected valuation layers will
be generated

To reproduce the issue:
(Need account_accountant, sale_management)
1. Create a product category PC:
   - Costing Method: FIFO
   - Inventory Valuation: Automated
2. Create a product P
   - Type: Storable
   - Category: PC
3. Create and confirm a PO with
   - 3 x P at $50
4. Process the receipt R01
5. Create and process the return RT01 of R01
6. Create and process the return RT02 of RT01
7. Create and confirm a SO with
   - 1 X P
8. Process the delivery
9. Create the bill related to PO
10. Set the bill line unit price to $60
11. Confirm
12. Open the valuation of P

Error: There are two lines for the price difference, there should be
one line only. Moreover, the value is incorrect: 30 instead of 10 (i.e.
we should generate the price diff layer for the quantities in
stock only)

When confirming the bill, we check if we have to generate some
additional SVL (in case of price diff)
https://github.com/odoo/odoo/blob/d3d41c7679f9068a7b986925b15d0d7670233bfc/addons/stock_account/models/account_move.py#L288-L305
But, we get all ingoing SVL and process them, we don't consider that the
quantity of a SVL might actually be returned and should be ignored.
This is the reason why a SVL is created for the first receipt. This
also explains why the value is incorrect (we use the quantity of the
SVL instead of its remaining quantity)

OPW-3040171

X-original-commit: 18912b05239e6fc5dab26ec4c930f9080882d127
Part-of: odoo/odoo#107145
This commit is contained in:
Adrien Widart (awt)
2022-12-05 10:07:48 +01:00
parent 07b1aa5376
commit 8acc41c66c
2 changed files with 153 additions and 4 deletions
@@ -1281,3 +1281,140 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon):
# Check if something was posted in the stock valuation account.
stock_val_aml = invoice.line_ids.filtered(lambda l: l.account_id == self.stock_valuation_account)
self.assertEqual(len(stock_val_aml), 0, "No line should have been generated in the stock valuation account.")
def test_price_diff_with_return_backorder_and_partial_bill(self):
"""
Fifo + Real time.
Receive 10@50:
Receive 10 (R1)
Return 10 (RET)
Receive 7 (R2)
Receive 3 (R3)
Deliver 5
Bill
1@60 -> already out, should not generate any SVL
3@60 -> already out, should not generate any SVL
2@60 -> only one out, should generate an SVL for the other one and
this SVL should be linked to SVL_R1
4@60 -> should generate two SVL: one for one product and attached to
SVL_R1 and another one for the three last products of SVL_R2
Deliver 2
The SVL should include:
- 2 x 50 (the product cost)
- 2 x 10 (the price diff from step "Bill 2@60" and "Bill 4@60")
"""
expected_svl_values = []
warehouse = self.env['stock.warehouse'].search([('company_id', '=', self.env.company.id)], limit=1)
stock_location = warehouse.lot_stock_id
customer_location = self.env.ref('stock.stock_location_customers')
self.product1.categ_id.property_cost_method = 'fifo'
self.product1.categ_id.property_valuation = 'real_time'
po_form = Form(self.env['purchase.order'])
po_form.partner_id = self.partner_id
with po_form.order_line.new() as po_line:
po_line.product_id = self.product1
po_line.product_qty = 10
po_line.price_unit = 50.0
po = po_form.save()
po.button_confirm()
# Receive 10
receipt01 = po.picking_ids[0]
receipt01.move_ids.quantity_done = 10
receipt01.button_validate()
expected_svl_values += [10 * 50]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
# Return 10
return_form = Form(self.env['stock.return.picking'].with_context(active_id=receipt01.id, active_model='stock.picking'))
return_wizard = return_form.save()
return_picking_id, _pick_type_id = return_wizard._create_returns()
return_picking = self.env['stock.picking'].browse(return_picking_id)
return_picking.move_ids.quantity_done = 10
return_picking.button_validate()
expected_svl_values += [-10 * 50]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
# Receive 7 with backorder
return_form = Form(self.env['stock.return.picking'].with_context(active_id=return_picking_id, active_model='stock.picking'))
return_wizard = return_form.save()
receipt_id, _pick_type_id = return_wizard._create_returns()
receipt02 = self.env['stock.picking'].browse(receipt_id)
receipt02.move_ids.quantity_done = 7
action = receipt02.button_validate()
backorder_wizard = Form(self.env['stock.backorder.confirmation'].with_context(action['context'])).save()
backorder_wizard.process()
expected_svl_values += [7 * 50]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
# Receive 3
receipt03 = receipt02.backorder_ids
receipt03.move_ids.quantity_done = 3
receipt03.button_validate()
expected_svl_values += [3 * 50]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
# Delivery 5
delivery01 = self.env['stock.picking'].create({
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'picking_type_id': warehouse.out_type_id.id,
'move_ids': [(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_uom_qty': 5,
'product_uom': self.product1.uom_id.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})],
})
delivery01.action_confirm()
delivery01.move_ids.quantity_done = 5
delivery01.button_validate()
expected_svl_values += [-5 * 50]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
for qty, new_svl_expected in [(1, []), (3, []), (2, [1 * 10.0]), (4, [1 * 10.0, 3 * 10.0])]:
bill_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice'))
bill_form.invoice_date = bill_form.date
bill_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id)
bill = bill_form.save()
bill.invoice_line_ids.quantity = qty
bill.invoice_line_ids.price_unit = 60.0
bill.action_post()
expected_svl_values += new_svl_expected
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
# Delivery 2
delivery02 = self.env['stock.picking'].create({
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
'picking_type_id': warehouse.out_type_id.id,
'move_ids': [(0, 0, {
'name': self.product1.name,
'product_id': self.product1.id,
'product_uom_qty': 2,
'product_uom': self.product1.uom_id.id,
'location_id': stock_location.id,
'location_dest_id': customer_location.id,
})],
})
delivery02.action_confirm()
delivery02.move_ids.quantity_done = 2
delivery02.button_validate()
expected_svl_values += [-2 * 50 + -2 * 10]
self.assertEqual(self.product1.stock_valuation_layer_ids.mapped('value'), expected_svl_values)
_svl_r01, _svl_ret, svl_r02, svl_r03, _svl_d01, svl_diff_01, svl_diff_02, svl_diff_03, _svl_d02 = self.product1.stock_valuation_layer_ids
self.assertEqual(svl_diff_01.stock_valuation_layer_id, svl_r02)
self.assertEqual(svl_diff_02.stock_valuation_layer_id, svl_r02)
self.assertEqual(svl_diff_03.stock_valuation_layer_id, svl_r03)
+16 -4
View File
@@ -324,17 +324,29 @@ class AccountMoveLine(models.Model):
return price_unit_by_layer
def _get_stock_layer_price_difference(self, layers, layers_price_unit, price_unit):
self.ensure_one()
po_line = self.purchase_line_id
aml_qty = self.product_uom_id._compute_quantity(self.quantity, self.product_id.uom_id)
invoice_lines = po_line.invoice_lines - self
invoices_qty = 0
for invoice_line in invoice_lines:
invoices_qty += invoice_line.product_uom_id._compute_quantity(invoice_line.quantity, invoice_line.product_id.uom_id)
qty_received = po_line.product_uom._compute_quantity(po_line.qty_received, self.product_id.uom_id)
out_qty = qty_received - sum(layers.mapped('remaining_qty'))
out_and_not_billed_qty = max(0, out_qty - invoices_qty)
total_to_correct = max(0, aml_qty - out_and_not_billed_qty)
# we also need to skip the remaining qty that is already billed
total_to_skip = max(0, invoices_qty - out_qty)
layers_to_correct = {}
for layer in layers:
if layer.quantity <= invoices_qty:
invoices_qty -= layer.quantity
continue
qty_to_correct = layer.quantity - invoices_qty
if float_compare(total_to_correct, 0, precision_rounding=self.product_id.uom_id.rounding) <= 0:
break
remaining_qty = layer.remaining_qty
qty_to_skip = min(total_to_skip, remaining_qty)
remaining_qty = max(0, remaining_qty - qty_to_skip)
qty_to_correct = min(total_to_correct, remaining_qty)
total_to_skip -= qty_to_skip
total_to_correct -= qty_to_correct
layer_price_unit = self.company_id.currency_id._convert(
layers_price_unit[layer], po_line.currency_id, self.company_id, self.date, round=False)
price_difference = price_unit - layer_price_unit