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
12 changes: 6 additions & 6 deletions docs/erpnext_mrp_run.md
Original file line number Diff line number Diff line change
Expand Up @@ -102,7 +102,7 @@ After all levels have been processed, the system computes the output and action

- **Suggested Orders**: Each `Suggested Receipt` is offset backward by the item's lead time to place a `Suggested Order` in the correct earlier period. For example, a suggested receipt in Week 42 for an item with a 2-week lead time generates a suggested order in Week 40.
- **Days to Reorder**: Calculated for the header period (week 0) only. The system finds the first period with a suggested receipt, works backward by the item's lead time, and computes how many days remain. A positive value means there is still time to act; a negative value means the order is already late. If no shortage is projected across the entire horizon, this field shows `—` to clearly distinguish "no action needed" from `0` (order today). Two variants are calculated: one including the safety stock floor and one excluding it.
- **Cash Requirements**: Finally, the system projects the financial impact of the plan across three fields.
- **Cash Requirements**: Finally, the system projects the financial impact of the plan for **purchased items only** (items without an active default BOM). Manufactured items are excluded because their cost arises from production, not procurement.
- **Suggested Orders Value**: Calculates the estimated cost of the `Suggested Orders` using a three-step price lookup:
1. **Supplier-specific price** — an `Item Price` record where `supplier` matches the item's default supplier, in the currency of the configured buying price list, with a valid date range.
2. **Buying price list** — if no supplier-specific price exists, the most recently valid `Item Price` on the price list configured in **Buying Settings → Buying Price List**, filtered to the correct currency and date range.
Expand Down Expand Up @@ -146,11 +146,11 @@ The following are the key fields calculated for each item in each period:
| `suggested_receipts` | The quantity the MRP calculation suggests should be received in this period to avoid a shortage. |
| `suggested_orders` | The quantity that should be ordered, offset by lead time. This is the primary action field. |
| `projected_on_hand_inventory` | The projected stock on hand at the end of the period after considering all demand, supply, and suggested receipts. |
| `suggested_orders_value` | The estimated value of the suggested orders. |
| `suggested_orders_value_payable`| Projected cash outflow for not-yet-placed orders, distributed by the supplier's payment terms. |
| `scheduled_receipts_value` | Base-currency value of open Purchase Order lines due in this period: `SUM((qty − received_qty) × base_rate)`. |
| `scheduled_receipts_value_payable` | Projected cash outflow for open Purchase Orders, distributed by the supplier's payment terms using the actual PO dates (`transaction_date`, `schedule_date`, or `custom_expected_arrival_date`) per payment term type. |
| `total_payable` | Combined projected cash outflow: `scheduled_receipts_value_payable + suggested_orders_value_payable`. |
| `suggested_orders_value` | The estimated value of the suggested orders. Only populated for purchased items (items without an active default BOM). |
| `suggested_orders_value_payable`| Projected cash outflow for not-yet-placed orders, distributed by the supplier's payment terms. Only populated for purchased items. |
| `scheduled_receipts_value` | Base-currency value of open Purchase Order lines due in this period: `SUM((qty − received_qty) × base_rate)`. Only populated for purchased items. |
| `scheduled_receipts_value_payable` | Projected cash outflow for open Purchase Orders, distributed by the supplier's payment terms using the actual PO dates (`transaction_date`, `schedule_date`, or `custom_expected_arrival_date`) per payment term type. Only populated for purchased items. |
| `total_payable` | Combined projected cash outflow: `scheduled_receipts_value_payable + suggested_orders_value_payable`. Only populated for purchased items. |
| **Urgency Indicators** (header period only) | |
| `days_to_reorder` | Days until the order must be placed, accounting for lead time and safety stock. Negative = already late. Blank (`—`) when no shortage is projected across the entire horizon. |
| `days_to_reorder_excl_reorder_level` | Same as above, but calculated without the safety stock floor. Blank (`—`) when no true shortage (excluding safety stock) is projected. |
Expand Down
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.4"
__version__ = "0.8.5"
261 changes: 131 additions & 130 deletions erpnext_mrp/mrp/tasks/mrp_run.py
Original file line number Diff line number Diff line change
Expand Up @@ -882,139 +882,140 @@ def _finalise_item_batch(
mrp_entry_docs[0].days_to_reorder_excl_reorder_level = _NO_REORDER_SENTINEL
mrp_entry_docs[0].needs_reorder_excl_reorder_level = 0

if price and price > 0:
for entry in mrp_entry_docs:
if entry.suggested_orders:
new_value = entry.suggested_orders * price
if entry.suggested_orders_value != new_value:
entry.suggested_orders_value = new_value

supplier_name = item_details.get("default_supplier") if item_details else None
payment_terms_template = supplier_payment_terms.get(supplier_name) if supplier_name else None

if supplier_name and payment_terms_template:
entry_map = {e.name: e for e in mrp_entry_docs}
for entry in mrp_entry_docs:
if entry.suggested_orders_value:
if not mrp_entry_docs[0].is_manufactured:
if price and price > 0:
for entry in mrp_entry_docs:
if entry.suggested_orders:
new_value = entry.suggested_orders * price
if entry.suggested_orders_value != new_value:
entry.suggested_orders_value = new_value

supplier_name = item_details.get("default_supplier") if item_details else None
payment_terms_template = supplier_payment_terms.get(supplier_name) if supplier_name else None

if supplier_name and payment_terms_template:
entry_map = {e.name: e for e in mrp_entry_docs}
for entry in mrp_entry_docs:
if entry.suggested_orders_value:
schedule = get_payment_terms(
payment_terms_template,
posting_date=entry.target_date,
grand_total=entry.suggested_orders_value,
base_grand_total=entry.suggested_orders_value,
)
if schedule:
for term in schedule:
base_date = None
payment_term_name = term.get("payment_term")
if payment_term_name:
custom_due_date_type = payment_term_details.get(
payment_term_name, {}
).get("custom_due_date")
if custom_due_date_type == "Order date":
base_date = entry.target_date
elif custom_due_date_type == "Shipment date":
base_date = add_days(
entry.target_date, item_details.get("primary_lead_time") or 0
)
elif custom_due_date_type == "Arrival date":
total_lead_time = (item_details.get("primary_lead_time") or 0) + (
item_details.get("additional_lead_time") or 0
)
base_date = add_days(entry.target_date, total_lead_time)
else:
base_date = entry.target_date
if base_date:
term.due_date = get_due_date(term, posting_date=base_date)
due_date = term.get("due_date")
payment_amount = term.get("payment_amount")
if due_date and payment_amount:
year, week, _day = getdate(due_date).isocalendar()
target_name = f"{item_code}-{year}CW{week:02d}"
target_entry = entry_map.get(target_name)
if target_entry:
target_entry.suggested_orders_value_payable = (
target_entry.suggested_orders_value_payable or 0
) + payment_amount
elif getdate(due_date) < mrp_entry_docs[0].target_date:
mrp_entry_docs[0].suggested_orders_value_payable = (
mrp_entry_docs[0].suggested_orders_value_payable or 0
) + payment_amount
else:
for entry in mrp_entry_docs:
entry.suggested_orders_value_payable = entry.suggested_orders_value

# Calculate Scheduled Receipts Payable using actual PO dates.
# Each open PO line is processed individually so its own transaction_date (order),
# schedule_date (shipment/ETD), and custom_expected_arrival_date (arrival/ETA) can be used
# as the base date for the matching payment term type.
item_po_lines = po_lines_by_item.get(item_code, [])

if supplier_name and payment_terms_template and item_po_lines:
entry_map = {e.name: e for e in mrp_entry_docs}
for po_line in item_po_lines:
remaining_value = po_line.get("remaining_value") or 0
if not remaining_value:
continue

order_date = (
getdate(po_line.get("transaction_date")) if po_line.get("transaction_date") else None
)
shipment_date = (
getdate(po_line.get("schedule_date")) if po_line.get("schedule_date") else None
)
arrival_date = (
getdate(po_line.get("custom_expected_arrival_date"))
if po_line.get("custom_expected_arrival_date")
else shipment_date
)
posting_date = arrival_date or shipment_date or order_date or date.today()

schedule = get_payment_terms(
payment_terms_template,
posting_date=entry.target_date,
grand_total=entry.suggested_orders_value,
base_grand_total=entry.suggested_orders_value,
posting_date=posting_date,
grand_total=remaining_value,
base_grand_total=remaining_value,
)
if schedule:
for term in schedule:
base_date = None
payment_term_name = term.get("payment_term")
if payment_term_name:
custom_due_date_type = payment_term_details.get(payment_term_name, {}).get(
"custom_due_date"
)
if custom_due_date_type == "Order date":
base_date = entry.target_date
elif custom_due_date_type == "Shipment date":
base_date = add_days(
entry.target_date, item_details.get("primary_lead_time") or 0
)
elif custom_due_date_type == "Arrival date":
total_lead_time = (item_details.get("primary_lead_time") or 0) + (
item_details.get("additional_lead_time") or 0
)
base_date = add_days(entry.target_date, total_lead_time)
else:
base_date = entry.target_date
if base_date:
term.due_date = get_due_date(term, posting_date=base_date)
due_date = term.get("due_date")
payment_amount = term.get("payment_amount")
if due_date and payment_amount:
year, week, _day = getdate(due_date).isocalendar()
target_name = f"{item_code}-{year}CW{week:02d}"
target_entry = entry_map.get(target_name)
if target_entry:
target_entry.suggested_orders_value_payable = (
target_entry.suggested_orders_value_payable or 0
) + payment_amount
elif getdate(due_date) < mrp_entry_docs[0].target_date:
mrp_entry_docs[0].suggested_orders_value_payable = (
mrp_entry_docs[0].suggested_orders_value_payable or 0
) + payment_amount
else:
for entry in mrp_entry_docs:
entry.suggested_orders_value_payable = entry.suggested_orders_value

# Calculate Scheduled Receipts Payable using actual PO dates.
# Each open PO line is processed individually so its own transaction_date (order),
# schedule_date (shipment/ETD), and custom_expected_arrival_date (arrival/ETA) can be used
# as the base date for the matching payment term type.
item_po_lines = po_lines_by_item.get(item_code, [])

if supplier_name and payment_terms_template and item_po_lines:
entry_map = {e.name: e for e in mrp_entry_docs}
for po_line in item_po_lines:
remaining_value = po_line.get("remaining_value") or 0
if not remaining_value:
continue

order_date = (
getdate(po_line.get("transaction_date")) if po_line.get("transaction_date") else None
)
shipment_date = (
getdate(po_line.get("schedule_date")) if po_line.get("schedule_date") else None
)
arrival_date = (
getdate(po_line.get("custom_expected_arrival_date"))
if po_line.get("custom_expected_arrival_date")
else shipment_date
)
posting_date = arrival_date or shipment_date or order_date or date.today()

schedule = get_payment_terms(
payment_terms_template,
posting_date=posting_date,
grand_total=remaining_value,
base_grand_total=remaining_value,
)
if not schedule:
continue

for term in schedule:
base_date = None
payment_term_name = term.get("payment_term")
if payment_term_name:
custom_due_date_type = payment_term_details.get(payment_term_name, {}).get(
"custom_due_date"
)
if custom_due_date_type == "Order date":
base_date = order_date
elif custom_due_date_type == "Shipment date":
base_date = shipment_date
elif custom_due_date_type == "Arrival date":
base_date = arrival_date
else:
base_date = posting_date

if base_date:
term.due_date = get_due_date(term, posting_date=base_date)

due_date = term.get("due_date")
payment_amount = term.get("payment_amount")
if due_date and payment_amount:
year, week, _day = getdate(due_date).isocalendar()
target_name = f"{item_code}-{year}CW{week:02d}"
target_entry = entry_map.get(target_name)
if target_entry:
target_entry.scheduled_receipts_value_payable = (
target_entry.scheduled_receipts_value_payable or 0
) + payment_amount
elif getdate(due_date) < mrp_entry_docs[0].target_date:
mrp_entry_docs[0].scheduled_receipts_value_payable = (
mrp_entry_docs[0].scheduled_receipts_value_payable or 0
) + payment_amount

elif not (supplier_name and payment_terms_template):
for entry in mrp_entry_docs:
entry.scheduled_receipts_value_payable = entry.scheduled_receipts_value
if not schedule:
continue

for term in schedule:
base_date = None
payment_term_name = term.get("payment_term")
if payment_term_name:
custom_due_date_type = payment_term_details.get(payment_term_name, {}).get(
"custom_due_date"
)
if custom_due_date_type == "Order date":
base_date = order_date
elif custom_due_date_type == "Shipment date":
base_date = shipment_date
elif custom_due_date_type == "Arrival date":
base_date = arrival_date
else:
base_date = posting_date

if base_date:
term.due_date = get_due_date(term, posting_date=base_date)

due_date = term.get("due_date")
payment_amount = term.get("payment_amount")
if due_date and payment_amount:
year, week, _day = getdate(due_date).isocalendar()
target_name = f"{item_code}-{year}CW{week:02d}"
target_entry = entry_map.get(target_name)
if target_entry:
target_entry.scheduled_receipts_value_payable = (
target_entry.scheduled_receipts_value_payable or 0
) + payment_amount
elif getdate(due_date) < mrp_entry_docs[0].target_date:
mrp_entry_docs[0].scheduled_receipts_value_payable = (
mrp_entry_docs[0].scheduled_receipts_value_payable or 0
) + payment_amount

elif not (supplier_name and payment_terms_template):
for entry in mrp_entry_docs:
entry.scheduled_receipts_value_payable = entry.scheduled_receipts_value

for entry in mrp_entry_docs:
entry.total_payable = (entry.suggested_orders_value_payable or 0) + (
Expand Down
2 changes: 1 addition & 1 deletion frontend/package.json
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
{
"name": "erpnext-mrp-ui",
"private": true,
"version": "0.8.4",
"version": "0.8.5",
"type": "module",
"scripts": {
"dev": "vite --host",
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.4",
"version": "0.8.5",
"author": "Starktail (Pty) Ltd <support@starktail.com>",
"workspaces": [
"frontend",
Expand Down
Loading