mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-11 13:41:47 +00:00
fix: order smallest purchase UOM qty that meets min order qty
A Production Plan with Consider Minimum Order Qty raises the requirement to the item's minimum in stock UOM, then converts it to the purchase UOM with round-to-nearest. Nearest rounding can land below the minimum it just applied: min order qty 50000 with purchase UOM conversion factor 453.592292197 becomes 110.231, which is 49999.932 in stock UOM, and the mapped Purchase Order is then rejected by validate_minimum_order_qty. When the minimum binds and the nearest-rounded value dips below it, quantize to the smallest representable purchase-UOM quantity whose stock equivalent meets the minimum, using Decimal grid-ceiling arithmetic. 110.232 converts to 50000.386: demand stays as planned and the overage is order-unit granularity, the standard MRP lot-sizing outcome. Ordinary conversions keep the historical round-to-nearest behavior.
This commit is contained in:
@@ -11,6 +11,7 @@ existing imports of ``...services.material_planning`` keep working through here.
|
||||
import copy
|
||||
import json
|
||||
from collections import defaultdict
|
||||
from decimal import ROUND_CEILING, Decimal
|
||||
|
||||
import frappe
|
||||
from frappe import _, msgprint
|
||||
@@ -493,8 +494,16 @@ def get_material_request_items(
|
||||
)
|
||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||
conversion_factor = _mr_purchase_conversion_factor(row)
|
||||
min_order_qty = flt(row.get("min_order_qty")) if doc.get("consider_minimum_order_qty") else 0
|
||||
return _material_request_item_row(
|
||||
row, sales_order, target_warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults
|
||||
row,
|
||||
sales_order,
|
||||
target_warehouse,
|
||||
bin_dict,
|
||||
required_qty,
|
||||
conversion_factor,
|
||||
item_group_defaults,
|
||||
min_order_qty,
|
||||
)
|
||||
|
||||
|
||||
@@ -539,6 +548,18 @@ def _adjust_required_qty_for_uom(row, required_qty):
|
||||
return required_qty
|
||||
|
||||
|
||||
def _quantity_in_purchase_uom(required_qty, conversion_factor, min_order_qty=0):
|
||||
"""Convert to purchase UOM; a binding minimum order qty takes the smallest
|
||||
representable quantity whose stock equivalent still meets it."""
|
||||
precision = frappe.get_precision("Material Request Plan Item", "quantity")
|
||||
quantity = flt(required_qty / conversion_factor, precision)
|
||||
if min_order_qty and quantity * conversion_factor < min_order_qty <= required_qty:
|
||||
grid = Decimal(10) ** -precision
|
||||
exact = Decimal(str(min_order_qty)) / Decimal(str(conversion_factor))
|
||||
quantity = flt(exact.quantize(grid, rounding=ROUND_CEILING))
|
||||
return quantity
|
||||
|
||||
|
||||
def _mr_purchase_conversion_factor(row):
|
||||
item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1)
|
||||
if (
|
||||
@@ -551,7 +572,14 @@ def _mr_purchase_conversion_factor(row):
|
||||
|
||||
|
||||
def _material_request_item_row(
|
||||
row, sales_order, warehouse, bin_dict, required_qty, conversion_factor, item_group_defaults
|
||||
row,
|
||||
sales_order,
|
||||
warehouse,
|
||||
bin_dict,
|
||||
required_qty,
|
||||
conversion_factor,
|
||||
item_group_defaults,
|
||||
min_order_qty=0,
|
||||
):
|
||||
warehouse = (
|
||||
warehouse
|
||||
@@ -559,11 +587,10 @@ def _material_request_item_row(
|
||||
or row.get("default_warehouse")
|
||||
or item_group_defaults.get("default_warehouse")
|
||||
)
|
||||
precision = frappe.get_precision("Material Request Plan Item", "quantity")
|
||||
return {
|
||||
"item_code": row.item_code,
|
||||
"item_name": row.item_name,
|
||||
"quantity": flt(required_qty / conversion_factor, precision),
|
||||
"quantity": _quantity_in_purchase_uom(required_qty, conversion_factor, min_order_qty),
|
||||
"conversion_factor": conversion_factor,
|
||||
"required_bom_qty": row.get("qty"),
|
||||
"stock_uom": row.get("stock_uom"),
|
||||
@@ -640,7 +667,8 @@ def _add_remaining_purchase_request(item, new_mr_items, required_qty, consider_m
|
||||
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
|
||||
required_qty = ceil(required_qty)
|
||||
|
||||
item["quantity"] = flt(required_qty / item.get("conversion_factor"), precision)
|
||||
min_order_qty = flt(item.get("min_order_qty")) if consider_minimum_order_qty else 0
|
||||
item["quantity"] = _quantity_in_purchase_uom(required_qty, item.get("conversion_factor"), min_order_qty)
|
||||
new_mr_items.append(item)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user