From 83136c8e7569636d38e12fc45f297e4d6f22ad36 Mon Sep 17 00:00:00 2001 From: "Anh Thao Pham (pta)" Date: Tue, 27 Apr 2021 08:32:22 +0000 Subject: [PATCH] [FIX] stock_account: fix computation of anglo saxon price unit - Create a product with Category configured with: * Costing Method: First In First Out (FIFO) * Inventory Valuation: Automated - Create a PO to buy 2 units at $100 (PO1) and receive the products - Create a SO to sell 2 units (SO1) and deliver the products => COGS of SO1 should be $200 ($100 * 2) - Return 1 unit from previous delivery => COGS of SO1 should be $100 - Create a SO to sell 1 unit (SO2) and deliver product => COGS of SO2 is $100 - Create a PO to buy 1 unit at $200 (PO2) and receive the product - Re-deliver the returned unit from SO1 => COGS of SO1 should be $300 ($100 + $200) - Create invoice for SO1 and post it The journal items (account.move.line) corresponding to the COGS have an incorrect value: $200 ($100 + $100), instead of $300 ($100 + $200) The issue comes from the method computing the anglo saxon price unit. It does not take into account quantities from stock moves that have been returned. This commit computes the number of units that have been returned for each stock moves used to compute the anglo saxon price unit and to take the result into account during the computation (of the anglo saxon price unit). opw-2501260 closes odoo/odoo#71317 X-original-commit: 4d3147a0b123ab591c1ca03a2ee86479992424d8 Signed-off-by: William Henrotin Signed-off-by: Anh Thao PHAM --- .../tests/test_anglo_saxon_valuation.py | 119 ++++++++++++++++++ addons/stock_account/models/product.py | 11 +- 2 files changed, 127 insertions(+), 3 deletions(-) diff --git a/addons/sale_stock/tests/test_anglo_saxon_valuation.py b/addons/sale_stock/tests/test_anglo_saxon_valuation.py index e79c96ff87b6b..d9f55183cf6b8 100644 --- a/addons/sale_stock/tests/test_anglo_saxon_valuation.py +++ b/addons/sale_stock/tests/test_anglo_saxon_valuation.py @@ -1003,3 +1003,122 @@ def test_fifo_delivered_invoice_post_delivery_4(self): revalued_anglo_expense_amls = sale_order.picking_ids.mapped('move_lines.stock_valuation_layer_ids')[-1].stock_move_id.account_move_ids[-1].mapped('line_ids') revalued_cogs_aml = revalued_anglo_expense_amls.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense']) self.assertEqual(revalued_cogs_aml.debit, 4, 'Price difference should have correctly reflected in expense account.') + + def test_fifo_delivered_invoice_post_delivery_with_return(self): + """Receive 2@10. SO1 2@12. Return 1 from SO1. SO2 1@12. Receive 1@20. + Re-deliver returned from SO1. Invoice after delivering everything.""" + self.product.categ_id.property_cost_method = 'fifo' + self.product.invoice_policy = 'delivery' + + # Receive 2@10. + in_move_1 = self.env['stock.move'].create({ + 'name': 'a', + 'product_id': self.product.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'product_uom': self.product.uom_id.id, + 'product_uom_qty': 2, + 'price_unit': 10, + }) + in_move_1._action_confirm() + in_move_1.quantity_done = 2 + in_move_1._action_done() + + # Create, confirm and deliver a sale order for 2@12 (SO1) + so_1 = self._so_and_confirm_two_units() + so_1.picking_ids.move_lines.quantity_done = 2 + so_1.picking_ids.button_validate() + + # Return 1 from SO1 + stock_return_picking_form = Form( + self.env['stock.return.picking'].with_context( + active_ids=so_1.picking_ids.ids, active_id=so_1.picking_ids.ids[0], active_model='stock.picking') + ) + stock_return_picking = stock_return_picking_form.save() + stock_return_picking.product_return_moves.quantity = 1.0 + stock_return_picking_action = stock_return_picking.create_returns() + return_pick = self.env['stock.picking'].browse(stock_return_picking_action['res_id']) + return_pick.action_assign() + return_pick.move_lines.quantity_done = 1 + return_pick._action_done() + + # Create, confirm and deliver a sale order for 1@12 (SO2) + so_2 = self.env['sale.order'].create({ + 'partner_id': self.partner_a.id, + 'order_line': [ + (0, 0, { + 'name': self.product.name, + 'product_id': self.product.id, + 'product_uom_qty': 1.0, + 'product_uom': self.product.uom_id.id, + 'price_unit': 12, + 'tax_id': False, # no love taxes amls + })], + }) + so_2.action_confirm() + so_2.picking_ids.move_lines.quantity_done = 1 + so_2.picking_ids.button_validate() + + # Receive 1@20 + in_move_2 = self.env['stock.move'].create({ + 'name': 'a', + 'product_id': self.product.id, + 'location_id': self.env.ref('stock.stock_location_suppliers').id, + 'location_dest_id': self.company_data['default_warehouse'].lot_stock_id.id, + 'product_uom': self.product.uom_id.id, + 'product_uom_qty': 1, + 'price_unit': 20, + }) + in_move_2._action_confirm() + in_move_2.quantity_done = 1 + in_move_2._action_done() + + # Re-deliver returned 1 from SO1 + stock_redeliver_picking_form = Form( + self.env['stock.return.picking'].with_context( + active_ids=return_pick.ids, active_id=return_pick.ids[0], active_model='stock.picking') + ) + stock_redeliver_picking = stock_redeliver_picking_form.save() + stock_redeliver_picking.product_return_moves.quantity = 1.0 + stock_redeliver_picking_action = stock_redeliver_picking.create_returns() + redeliver_pick = self.env['stock.picking'].browse(stock_redeliver_picking_action['res_id']) + redeliver_pick.action_assign() + redeliver_pick.move_lines.quantity_done = 1 + redeliver_pick._action_done() + + # Invoice the sale orders + invoice_1 = so_1._create_invoices() + invoice_1.action_post() + invoice_2 = so_2._create_invoices() + invoice_2.action_post() + + # Check the resulting accounting entries + amls_1 = invoice_1.line_ids + self.assertEqual(len(amls_1), 4) + stock_out_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out']) + self.assertEqual(stock_out_aml_1.debit, 0) + self.assertEqual(stock_out_aml_1.credit, 30) + cogs_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense']) + self.assertEqual(cogs_aml_1.debit, 30) + self.assertEqual(cogs_aml_1.credit, 0) + receivable_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable']) + self.assertEqual(receivable_aml_1.debit, 24) + self.assertEqual(receivable_aml_1.credit, 0) + income_aml_1 = amls_1.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue']) + self.assertEqual(income_aml_1.debit, 0) + self.assertEqual(income_aml_1.credit, 24) + + amls_2 = invoice_2.line_ids + self.assertEqual(len(amls_2), 4) + stock_out_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_stock_out']) + self.assertEqual(stock_out_aml_2.debit, 0) + self.assertEqual(stock_out_aml_2.credit, 10) + cogs_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_expense']) + self.assertEqual(cogs_aml_2.debit, 10) + self.assertEqual(cogs_aml_2.credit, 0) + receivable_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_receivable']) + self.assertEqual(receivable_aml_2.debit, 12) + self.assertEqual(receivable_aml_2.credit, 0) + income_aml_2 = amls_2.filtered(lambda aml: aml.account_id == self.company_data['default_account_revenue']) + self.assertEqual(income_aml_2.debit, 0) + self.assertEqual(income_aml_2.credit, 12) diff --git a/addons/stock_account/models/product.py b/addons/stock_account/models/product.py index 278507aad06d5..226dad016ac1f 100644 --- a/addons/stock_account/models/product.py +++ b/addons/stock_account/models/product.py @@ -5,6 +5,7 @@ from odoo.exceptions import UserError from odoo.tools import float_is_zero, float_repr from odoo.exceptions import ValidationError +from collections import defaultdict class ProductTemplate(models.Model): @@ -659,17 +660,21 @@ def _compute_average_price(self, qty_invoiced, qty_to_invoice, stock_moves): if not qty_to_invoice: return 0.0 - if not qty_to_invoice: - return 0 - + returned_quantities = defaultdict(float) + for move in stock_moves: + if move.origin_returned_move_id: + returned_quantities[move.origin_returned_move_id.id] += abs(sum(move.stock_valuation_layer_ids.mapped('quantity'))) candidates = stock_moves\ .sudo()\ + .filtered(lambda m: not (m.origin_returned_move_id and sum(m.stock_valuation_layer_ids.mapped('quantity')) >= 0))\ .mapped('stock_valuation_layer_ids')\ .sorted() qty_to_take_on_candidates = qty_to_invoice tmp_value = 0 # to accumulate the value taken on the candidates for candidate in candidates: candidate_quantity = abs(candidate.quantity) + if candidate.stock_move_id.id in returned_quantities: + candidate_quantity -= returned_quantities[candidate.stock_move_id.id] if float_is_zero(candidate_quantity, precision_rounding=candidate.uom_id.rounding): continue # correction entries if not float_is_zero(qty_invoiced, precision_rounding=candidate.uom_id.rounding):