From 8acc41c66ca337a725833b6cdd5188ea042d438d Mon Sep 17 00:00:00 2001 From: "Adrien Widart (awt)" Date: Wed, 23 Nov 2022 15:43:43 +0000 Subject: [PATCH] [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 --- .../tests/test_stockvaluation.py | 137 ++++++++++++++++++ addons/stock_account/models/account_move.py | 20 ++- 2 files changed, 153 insertions(+), 4 deletions(-) diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py index a1a5ffeab4f..5f363f2333b 100644 --- a/addons/purchase_stock/tests/test_stockvaluation.py +++ b/addons/purchase_stock/tests/test_stockvaluation.py @@ -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) diff --git a/addons/stock_account/models/account_move.py b/addons/stock_account/models/account_move.py index 5a581755fa8..e1023bae0b9 100644 --- a/addons/stock_account/models/account_move.py +++ b/addons/stock_account/models/account_move.py @@ -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