From c321f825f9fc9a6fd6dc66cfdf9b44e682a6f045 Mon Sep 17 00:00:00 2001 From: "Adrien Widart (awt)" Date: Fri, 9 Dec 2022 14:29:53 +0000 Subject: [PATCH] [FIX] purchase_stock,stock_account: reconcile lines In some cases, when billing a received quantity that is already delivered, the account move lines of Stock Interim (Received) will not be reconciled 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 - 5 x P at $50 4. Process the receipt R 5. Create and confirm a SO with - 5 X P 6. Process the delivery 7. Create and post partial bill B01 related to PO: - 1 x P @ $60 8. Accounting > Accounting > Journal Items - Group By: Account - Note: There are three lines in Stock Interim (Receipt): - [AML01] Debit: 0, Credit: 250 (receipt R) - [AML02] Debit: 60, Credit: 0 (bill B01) - [AML03] Debit: 0, Credit: 10 (price diff from B01 because quantity already delivered) There is a partial reconcile between AML01 and AML02, AML03 is not part of this partial reconciliation 9. Create and post partial bill B02 related to PO: - 4 x P @ $60 10. Accounting > Accounting > Journal Items - Group By: Account Error: The lines of account 'Stock Interim (Received)' are not reconciled while it should When confirming B01, we try to reconcile AML01, AML02, AML03. In the reconciliation process, we first sort the AMLs and try to create a partial reconciliation: https://github.com/odoo/odoo/blob/01cb7e960f912c4758d30c04653b599937785799/addons/account/models/account_move_line.py#L2282-L2293 Because of the sorting, here is the order of the AMLs: - [AML01] Debit: 0, Credit: 250 - [AML03] Debit: 0, Credit: 10 - [AML02] Debit: 60, Credit: 0 In `_create_reconciliation_partials`, we consume the AMLs in that specific order. So, it first uses one credit line and a debit one: https://github.com/odoo/odoo/blob/01cb7e960f912c4758d30c04653b599937785799/addons/account/models/account_move_line.py#L1863-L1872 (i.e. AML01 and AML02) and creates a partial reconciliation for theses AMLs. It gives a temporary credit line of 190, but there is no more debit lines, so the partial reconciliation is stopped (this explains the note at step 8) Later on, while posting B02, we try again to reconcile the lines of Stock Interim (Received) https://github.com/odoo/odoo/blob/493020b9317a439c0a61a34f37cc92b6779ef633/addons/stock_account/models/account_move.py#L249 At that point, we have three AMLs: - [AML01] Debit: 0, Credit: 250 (same as above) - [AML04] Debit: 0, Credit: 40 (price diff from B02) - [AML05] Debit: 240, Credit: 0 (B02) Back in the reconciliation progress, we try to get all involved AMLs: https://github.com/odoo/odoo/blob/01cb7e960f912c4758d30c04653b599937785799/addons/account/models/account_move_line.py#L2284-L2287 It will be used later for the full reconciliation. To get the involved ones, we recursively get the AMLs implied in the partial reconciliation of AML01, AML04, AML05: https://github.com/odoo/odoo/blob/01cb7e960f912c4758d30c04653b599937785799/addons/account/models/account_move_line.py#L2481-L2489 Therefore, because of the first partial reconciliation explained above, we will find AML02 but not AML03. This is the reason why the full reconciliation will not happen. Working on the assumption that the note of step 8 is not a bug (i.e., AML03 is not expected to be part of the first partial reconciliation), we need to provide as much AML as we can when calling the reconciliation process. OPW-3040171 closes odoo/odoo#107908 X-original-commit: 3050e23f94c865f9938863f191076f761b80fedb Signed-off-by: William Henrotin (whe) Signed-off-by: Adrien Widart --- addons/purchase_stock/models/stock_move.py | 3 + .../tests/test_stockvaluation.py | 66 +++++++++++++++++++ addons/stock_account/models/account_move.py | 5 +- addons/stock_account/models/stock_move.py | 3 + 4 files changed, 75 insertions(+), 2 deletions(-) diff --git a/addons/purchase_stock/models/stock_move.py b/addons/purchase_stock/models/stock_move.py index 6c4909de3c0..0bad8336972 100644 --- a/addons/purchase_stock/models/stock_move.py +++ b/addons/purchase_stock/models/stock_move.py @@ -196,3 +196,6 @@ class StockMove(models.Model): and self.warehouse_id and self.location_dest_id not in self.env["stock.location"].search([("id", "child_of", self.warehouse_id.view_location_id.id)]) ) + + def _get_all_related_aml(self): + return super()._get_all_related_aml() | self.purchase_line_id.invoice_lines.move_id.line_ids diff --git a/addons/purchase_stock/tests/test_stockvaluation.py b/addons/purchase_stock/tests/test_stockvaluation.py index 60c13be78b0..cea6052c18f 100644 --- a/addons/purchase_stock/tests/test_stockvaluation.py +++ b/addons/purchase_stock/tests/test_stockvaluation.py @@ -1462,3 +1462,69 @@ class TestStockValuationWithCOA(AccountTestInvoicingCommon): self.assertEqual(svl_diff_01.stock_valuation_layer_id, svl_r01) self.assertEqual(svl_diff_02.stock_valuation_layer_id, svl_r01) self.assertEqual(svl_diff_03.stock_valuation_layer_id, svl_r02) + + def test_partial_bills_and_reconciliation(self): + """ + Fifo, Auto + Receive 5 + Deliver 5 + Bill 1 (with price diff) + Bill 4 (with price diff) + The lines in stock input account should be reconciled + """ + self.product1.categ_id.property_cost_method = 'fifo' + self.product1.categ_id.property_valuation = 'real_time' + + 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') + + 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 = 5 + po_line.price_unit = 50.0 + po = po_form.save() + po.button_confirm() + + receipt = po.picking_ids[0] + receipt.move_ids._set_quantities_to_reservation() + receipt.button_validate() + + delivery = 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, + 'location_id': stock_location.id, + 'location_dest_id': customer_location.id, + })], + }) + delivery.action_confirm() + delivery.move_ids._set_quantities_to_reservation() + delivery.button_validate() + + bill01_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + bill01_form.invoice_date = bill01_form.date + bill01_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id) + bill01 = bill01_form.save() + bill01.invoice_line_ids.quantity = 1 + bill01.invoice_line_ids.price_unit = 60 + bill01.action_post() + + bill02_form = Form(self.env['account.move'].with_context(default_move_type='in_invoice')) + bill02_form.invoice_date = bill02_form.date + bill02_form.purchase_vendor_bill_id = self.env['purchase.bill.union'].browse(-po.id) + bill02 = bill02_form.save() + bill02.invoice_line_ids.quantity = 4 + bill02.invoice_line_ids.price_unit = 60 + bill02.action_post() + + input_amls = (bill01 + bill02).line_ids.filtered(lambda aml: aml.account_id == self.stock_input_account) + full_reconcile = input_amls[0].full_reconcile_id + self.assertTrue(full_reconcile) + self.assertTrue(all(aml.full_reconcile_id == full_reconcile for aml in input_amls)) diff --git a/addons/stock_account/models/account_move.py b/addons/stock_account/models/account_move.py index 5ea9009a272..d1601c4eed7 100644 --- a/addons/stock_account/models/account_move.py +++ b/addons/stock_account/models/account_move.py @@ -232,8 +232,9 @@ class AccountMove(models.Model): # Search for anglo-saxon lines linked to the product in the stock moves. product_stock_moves = stock_moves.filtered(lambda stock_move: stock_move.product_id == prod) - product_account_moves += product_stock_moves.mapped('account_move_ids.line_ids')\ - .filtered(lambda line: line.account_id == product_interim_account and not line.reconciled) + product_account_moves |= product_stock_moves._get_all_related_aml().filtered( + lambda line: line.account_id == product_interim_account and not line.reconciled + ) # Reconcile. if any(aml.amount_currency and not aml.balance for aml in product_account_moves): diff --git a/addons/stock_account/models/stock_move.py b/addons/stock_account/models/stock_move.py index f08fbcbd0d2..a96d74a5e3c 100644 --- a/addons/stock_account/models/stock_move.py +++ b/addons/stock_account/models/stock_move.py @@ -593,3 +593,6 @@ class StockMove(models.Model): return self.location_id and self.location_id.usage == 'customer' # goods returned from customer if valued_type == 'out': return self.location_dest_id and self.location_dest_id.usage == 'supplier' # goods returned to supplier + + def _get_all_related_aml(self): + return self.account_move_ids.line_ids