Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion erpnext_mrp/__init__.py
Original file line number Diff line number Diff line change
@@ -1 +1 @@
__version__ = "0.8.7"
__version__ = "0.8.8"
21 changes: 20 additions & 1 deletion erpnext_mrp/mrp/tasks/mrp_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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,
Expand Down
97 changes: 97 additions & 0 deletions erpnext_mrp/mrp/tasks/test_mrp_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"private": true,
"name": "erpnext_mrp",
"version": "0.8.7",
"version": "0.8.8",
"author": "Starktail (Pty) Ltd <support@starktail.com>",
"workspaces": [
"frontend",
Expand Down
Loading