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