mirror of
https://github.com/frappe/erpnext.git
synced 2026-09-01 07:37:04 +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 copy
|
||||||
import json
|
import json
|
||||||
from collections import defaultdict
|
from collections import defaultdict
|
||||||
|
from decimal import ROUND_CEILING, Decimal
|
||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe import _, msgprint
|
from frappe import _, msgprint
|
||||||
@@ -493,8 +494,16 @@ def get_material_request_items(
|
|||||||
)
|
)
|
||||||
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
item_group_defaults = get_item_group_defaults(row.item_code, company)
|
||||||
conversion_factor = _mr_purchase_conversion_factor(row)
|
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(
|
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
|
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):
|
def _mr_purchase_conversion_factor(row):
|
||||||
item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1)
|
item_details = frappe.get_cached_value("Item", row.item_code, ["purchase_uom", "stock_uom"], as_dict=1)
|
||||||
if (
|
if (
|
||||||
@@ -551,7 +572,14 @@ def _mr_purchase_conversion_factor(row):
|
|||||||
|
|
||||||
|
|
||||||
def _material_request_item_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 = (
|
||||||
warehouse
|
warehouse
|
||||||
@@ -559,11 +587,10 @@ def _material_request_item_row(
|
|||||||
or row.get("default_warehouse")
|
or row.get("default_warehouse")
|
||||||
or item_group_defaults.get("default_warehouse")
|
or item_group_defaults.get("default_warehouse")
|
||||||
)
|
)
|
||||||
precision = frappe.get_precision("Material Request Plan Item", "quantity")
|
|
||||||
return {
|
return {
|
||||||
"item_code": row.item_code,
|
"item_code": row.item_code,
|
||||||
"item_name": row.item_name,
|
"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,
|
"conversion_factor": conversion_factor,
|
||||||
"required_bom_qty": row.get("qty"),
|
"required_bom_qty": row.get("qty"),
|
||||||
"stock_uom": row.get("stock_uom"),
|
"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"):
|
if frappe.db.get_value("UOM", purchase_uom, "must_be_whole_number"):
|
||||||
required_qty = ceil(required_qty)
|
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)
|
new_mr_items.append(item)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user