[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) <whe@odoo.com>
Signed-off-by: Adrien Widart <awt@odoo.com>
This commit is contained in:
Adrien Widart (awt)
2022-12-14 10:35:28 +01:00
parent 48f6cb4597
commit c321f825f9
4 changed files with 75 additions and 2 deletions
@@ -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
@@ -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))
+3 -2
View File
@@ -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):
@@ -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