From 27322442103c60a48f8e689cbbc6205078dabde2 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Mon, 6 Jul 2026 21:11:00 +0000 Subject: [PATCH 1/2] fix: race condition causing save of stale values --- erpnext_mrp/mrp/tasks/mrp_run.py | 21 +++++- erpnext_mrp/mrp/tasks/test_mrp_run.py | 97 +++++++++++++++++++++++++++ 2 files changed, 117 insertions(+), 1 deletion(-) diff --git a/erpnext_mrp/mrp/tasks/mrp_run.py b/erpnext_mrp/mrp/tasks/mrp_run.py index 782f75b..f85a922 100644 --- a/erpnext_mrp/mrp/tasks/mrp_run.py +++ b/erpnext_mrp/mrp/tasks/mrp_run.py @@ -4,6 +4,19 @@ _NO_REORDER_SENTINEL = 9999 +# Fields to be persisted in _finalise_item_batch only +_FINALISE_OWNED_FIELDS = ( + "suggested_orders", + "suggested_orders_value", + "suggested_orders_value_payable", + "scheduled_receipts_value_payable", + "total_payable", + "days_to_reorder", + "needs_reorder", + "days_to_reorder_excl_reorder_level", + "needs_reorder_excl_reorder_level", +) + import frappe from erpnext.controllers.accounts_controller import get_due_date, get_payment_terms from erpnext.stock.report.stock_balance.stock_balance import execute as execute_stock_balance_report @@ -1113,7 +1126,12 @@ def _finalise_item_batch( entry.total_payable = (entry.suggested_orders_value_payable or 0) + ( entry.scheduled_receipts_value_payable or 0 ) - entry.db_update() + frappe.db.set_value( + "MRP Entry", + entry.name, + {field: entry.get(field) for field in _FINALISE_OWNED_FIELDS}, + update_modified=False, + ) if total_batches > 1: try: @@ -1381,6 +1399,7 @@ def _finalise_suggestions( frappe.enqueue( "erpnext_mrp.mrp.tasks.mrp_run._finalise_item_batch", queue="long", + enqueue_after_commit=True, item_codes=batch_codes, item_details_map=batch_map, stock_levels=stock_levels, diff --git a/erpnext_mrp/mrp/tasks/test_mrp_run.py b/erpnext_mrp/mrp/tasks/test_mrp_run.py index 3045d12..a1fd8c6 100644 --- a/erpnext_mrp/mrp/tasks/test_mrp_run.py +++ b/erpnext_mrp/mrp/tasks/test_mrp_run.py @@ -645,6 +645,103 @@ def test_item_with_long_lead_time(self, mock_date): # TODO: change assert to stock level check # self.assertEqual(mrp_entries[0].urgency_level, 0, "Expected an urgency_level of 0") + def test_finalise_does_not_clobber_on_hand(self, mock_date): + """ + Regression for the batch race that left in-stock items showing 0 on hand. + """ + test_start_day = datetime.date(2026, 1, 22) + mock_date.today.return_value = test_start_day + + mrp_settings = frappe.get_doc("MRP Settings", "MRP Settings") + mrp_settings.item_condition = "doc.item_code == 'SRZLONG123'" + mrp_settings.look_ahead = 4 + mrp_settings.requirement_based_on = "Forecast only" + mrp_settings.save() + + # Starting stock of 130 - the receipts phase should persist this as on_hand. + make_stock_entry( + item_code="SRZLONG123", + posting_date=add_days(test_start_day, -1), + qty=130, + to_warehouse="_Test Warehouse - _TC", + rate=1, + purpose="Material Receipt", + ) + + create_mrp_item_entries() + process_mrp_item_entries(enqueue=False) + + header = frappe.get_all( + "MRP Entry", filters={"item_code": "SRZLONG123", "is_header": 1}, pluck="name" + )[0] + self.assertEqual( + frappe.db.get_value("MRP Entry", header, "on_hand_inventory"), + 130, + "receipts phase should have written the current stock as on_hand", + ) + + # Simulate a finalise worker that loaded its rows before the receipts commit: + # the DB has 130 but every MRP Entry it reads still shows the stale 0. + real_get_doc = mrp_run_module.frappe.get_doc + + def stale_get_doc(*args, **kwargs): + doc = real_get_doc(*args, **kwargs) + if args and args[0] == "MRP Entry": + doc.on_hand_inventory = 0 + doc.on_hand_inventory_excl_reorder_level = 0 + doc.on_hand_inventory_no_action = 0 + return doc + + item_details_map = mrp_run_module._get_all_item_details() + with patch.object(mrp_run_module.frappe, "get_doc", side_effect=stale_get_doc): + mrp_run_module._finalise_suggestions( + stock_levels=[], item_details_map=item_details_map, enqueue=False + ) + + self.assertEqual( + frappe.db.get_value("MRP Entry", header, "on_hand_inventory"), + 130, + "finalise must not overwrite on_hand_inventory written by the receipts phase", + ) + + def test_finalise_batches_enqueued_after_commit(self, mock_date): + """ + The finalise batches must be enqueued with enqueue_after_commit=True. + """ + test_start_day = datetime.date(2026, 1, 22) + mock_date.today.return_value = test_start_day + + mrp_settings = frappe.get_doc("MRP Settings", "MRP Settings") + mrp_settings.item_condition = "doc.item_code == 'SRZLONG123'" + mrp_settings.look_ahead = 4 + mrp_settings.requirement_based_on = "Forecast only" + mrp_settings.save() + + make_stock_entry( + item_code="SRZLONG123", + posting_date=add_days(test_start_day, -1), + qty=130, + to_warehouse="_Test Warehouse - _TC", + rate=1, + purpose="Material Receipt", + ) + + create_mrp_item_entries() + with patch.object(mrp_run_module.frappe, "enqueue") as mock_enqueue: + process_mrp_item_entries(enqueue=True) + + finalise_calls = [ + call + for call in mock_enqueue.call_args_list + if call.args and "_finalise_item_batch" in call.args[0] + ] + self.assertTrue(finalise_calls, "expected the finalise batches to be enqueued") + for call in finalise_calls: + self.assertTrue( + call.kwargs.get("enqueue_after_commit"), + "finalise batches must be enqueued with enqueue_after_commit=True", + ) + def test_process_mrp_item_entry_has_correct_suggested_orders_value_payable(self, mock_date): """ Test that MRP Entry record has correct Suggested Orders Value Payable. From fdefdbf0a442762d2fa43409edb853a06fddeb37 Mon Sep 17 00:00:00 2001 From: Dirk van der Laarse Date: Mon, 6 Jul 2026 21:11:25 +0000 Subject: [PATCH 2/2] chore: bump to v0.8.8 --- erpnext_mrp/__init__.py | 2 +- package.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext_mrp/__init__.py b/erpnext_mrp/__init__.py index 686e10f..aa00ec3 100644 --- a/erpnext_mrp/__init__.py +++ b/erpnext_mrp/__init__.py @@ -1 +1 @@ -__version__ = "0.8.7" +__version__ = "0.8.8" diff --git a/package.json b/package.json index 8750d90..e9dccd2 100644 --- a/package.json +++ b/package.json @@ -1,7 +1,7 @@ { "private": true, "name": "erpnext_mrp", - "version": "0.8.7", + "version": "0.8.8", "author": "Starktail (Pty) Ltd ", "workspaces": [ "frontend",