mirror of
https://github.com/frappe/erpnext.git
synced 2026-06-02 19:59:12 +00:00
Merge pull request #55340 from nishkagosalia/gh-55106
feat: over order allowance setting
This commit is contained in:
@@ -17,6 +17,7 @@
|
||||
"section_break_vwgg",
|
||||
"maintain_same_rate",
|
||||
"column_break_lwxs",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"maintain_same_rate_action",
|
||||
"role_to_override_stop_action",
|
||||
"transaction_settings_section",
|
||||
@@ -24,7 +25,8 @@
|
||||
"po_required",
|
||||
"pr_required",
|
||||
"project_update_frequency",
|
||||
"column_break_12",
|
||||
"over_order_allowance",
|
||||
"column_break_kdcm",
|
||||
"allow_multiple_items",
|
||||
"allow_negative_rates_for_items",
|
||||
"set_valuation_rate_for_rejected_materials",
|
||||
@@ -33,7 +35,6 @@
|
||||
"purchase_invoice_settings_section",
|
||||
"bill_for_rejected_quantity_in_purchase_invoice",
|
||||
"use_transaction_date_exchange_rate",
|
||||
"set_landed_cost_based_on_purchase_invoice_rate",
|
||||
"zero_quantity_line_items_section",
|
||||
"allow_zero_qty_in_supplier_quotation",
|
||||
"allow_zero_qty_in_request_for_quotation",
|
||||
@@ -156,10 +157,6 @@
|
||||
"fieldtype": "Tab Break",
|
||||
"label": "Transaction Settings"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_12",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Prevents the system from automatically using the rate from the last purchase transaction when creating new purchase orders or transactions.",
|
||||
@@ -335,6 +332,16 @@
|
||||
"hidden": 1,
|
||||
"is_virtual": 1,
|
||||
"label": "Naming Series options"
|
||||
},
|
||||
{
|
||||
"description": "The percentage by which you are allowed to order more on a Purchase Order than the quantity requested on the originating Material Request. For example, if the Material Request has 100 units and the allowance is 10%, you can order up to 110 units",
|
||||
"fieldname": "over_order_allowance",
|
||||
"fieldtype": "Float",
|
||||
"label": "Over Order Allowance (%)"
|
||||
},
|
||||
{
|
||||
"fieldname": "column_break_kdcm",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"grid_page_length": 50,
|
||||
@@ -343,7 +350,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"issingle": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-05 16:30:37.184607",
|
||||
"modified": "2026-05-27 23:04:00.842393",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Buying Settings",
|
||||
|
||||
@@ -34,6 +34,7 @@ class BuyingSettings(Document):
|
||||
fixed_email: DF.Link | None
|
||||
maintain_same_rate: DF.Check
|
||||
maintain_same_rate_action: DF.Literal["Stop", "Warn"]
|
||||
over_order_allowance: DF.Float
|
||||
over_transfer_allowance: DF.Float
|
||||
po_required: DF.Literal["No", "Yes"]
|
||||
pr_required: DF.Literal["No", "Yes"]
|
||||
|
||||
@@ -178,6 +178,9 @@ class PurchaseOrder(BuyingController):
|
||||
"target_ref_field": "stock_qty",
|
||||
"source_field": "stock_qty",
|
||||
"percent_join_field": "material_request",
|
||||
"global_allowance_field": "over_order_allowance",
|
||||
"global_allowance_doctype": "Buying Settings",
|
||||
"item_allowance_field": "over_order_allowance",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
@@ -128,6 +128,44 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
frappe.db.set_value("Item", "_Test Item", "over_billing_allowance", 0)
|
||||
frappe.db.set_single_value("Accounts Settings", "over_billing_allowance", 0)
|
||||
|
||||
def test_over_order_allowance_against_material_request(self) -> None:
|
||||
"""Over Order Allowance in Buying Settings must govern PO qty vs MR qty independently
|
||||
from Over Delivery/Receipt Allowance which governs receipt/delivery against a PO."""
|
||||
mr = make_material_request(qty=100)
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.items[0].qty = 110 # 10% over the MR qty
|
||||
|
||||
# Without any allowance, submitting should raise an OverAllowanceError
|
||||
from erpnext.controllers.status_updater import OverAllowanceError
|
||||
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||
self.assertRaises(OverAllowanceError, po.submit)
|
||||
|
||||
# Granting 10% in Over Order Allowance (Buying Settings) must allow the submit
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 10)
|
||||
po.reload()
|
||||
po.items[0].qty = 110
|
||||
po.submit()
|
||||
self.assertEqual(po.docstatus, 1)
|
||||
po.cancel()
|
||||
|
||||
# Over Delivery/Receipt Allowance must remain independent — changing it must not
|
||||
# affect the MR → PO validation when Over Order Allowance is 0.
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 50)
|
||||
|
||||
mr2 = make_material_request(qty=100)
|
||||
po2 = make_purchase_order(mr2.name)
|
||||
po2.supplier = "_Test Supplier"
|
||||
po2.items[0].qty = 110
|
||||
self.assertRaises(OverAllowanceError, po2.submit)
|
||||
|
||||
# cleanup
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
|
||||
|
||||
def test_update_remove_child_linked_to_mr(self):
|
||||
"""Test impact on linked PO and MR on deleting/updating row."""
|
||||
mr = make_material_request(qty=10)
|
||||
|
||||
@@ -262,15 +262,17 @@ class StatusUpdater(Document):
|
||||
|
||||
def validate_qty(self):
|
||||
"""Validates qty at row level"""
|
||||
self.item_allowance = {}
|
||||
self.global_qty_allowance = None
|
||||
self.global_amount_allowance = None
|
||||
|
||||
for args in self.status_updater:
|
||||
if "target_ref_field" not in args or args.get("validate_qty") is False:
|
||||
# if target_ref_field is not specified or validate_qty is explicitly set to False, skip validation
|
||||
continue
|
||||
|
||||
# Reset per-args so each config block uses its own allowance source without
|
||||
# leaking cached values from a previous config block.
|
||||
self.item_allowance = {}
|
||||
self.global_qty_allowance = None
|
||||
self.global_amount_allowance = None
|
||||
|
||||
items_to_validate = []
|
||||
selling_negative_rate_allowed = frappe.get_single_value(
|
||||
"Selling Settings", "allow_negative_rates_for_items"
|
||||
@@ -402,9 +404,12 @@ class StatusUpdater(Document):
|
||||
|
||||
def check_overflow_with_allowance(self, item, args):
|
||||
"""
|
||||
Checks if there is overflow condering a relaxation allowance
|
||||
Checks if there is overflow considering a relaxation allowance.
|
||||
"""
|
||||
qty_or_amount = "qty" if "qty" in args["target_ref_field"] else "amount"
|
||||
global_qty_allowance_field = args.get("global_allowance_field", "over_delivery_receipt_allowance")
|
||||
global_qty_allowance_doctype = args.get("global_allowance_doctype", "Stock Settings")
|
||||
item_qty_allowance_field = args.get("item_allowance_field", "over_delivery_receipt_allowance")
|
||||
|
||||
# check if overflow is within allowance
|
||||
(
|
||||
@@ -419,6 +424,9 @@ class StatusUpdater(Document):
|
||||
self.global_qty_allowance,
|
||||
self.global_amount_allowance,
|
||||
qty_or_amount,
|
||||
global_qty_allowance_field,
|
||||
global_qty_allowance_doctype,
|
||||
item_qty_allowance_field,
|
||||
)
|
||||
if args["source_dt"] != "Pick List Item"
|
||||
else (0, {}, None, None)
|
||||
@@ -463,7 +471,9 @@ class StatusUpdater(Document):
|
||||
"Quotation Item",
|
||||
"Packed Item",
|
||||
]:
|
||||
if qty_or_amount == "qty":
|
||||
if args.get("target_dt") == "Material Request Item":
|
||||
action_msg = _('To allow over ordering, update "Over Order Allowance" in Buying Settings.')
|
||||
elif qty_or_amount == "qty":
|
||||
action_msg = _(
|
||||
'To allow over receipt / delivery, update "Over Receipt/Delivery Allowance" in Stock Settings or the Item.'
|
||||
)
|
||||
@@ -724,16 +734,28 @@ class StatusUpdater(Document):
|
||||
ref_doc.set_status(update=True)
|
||||
|
||||
|
||||
@frappe.request_cache
|
||||
def get_allowance_for(
|
||||
item_code,
|
||||
item_allowance=None,
|
||||
global_qty_allowance=None,
|
||||
global_amount_allowance=None,
|
||||
qty_or_amount="qty",
|
||||
global_qty_allowance_field="over_delivery_receipt_allowance",
|
||||
global_qty_allowance_doctype="Stock Settings",
|
||||
item_qty_allowance_field="over_delivery_receipt_allowance",
|
||||
):
|
||||
"""
|
||||
Returns the allowance for the item, if not set, returns global allowance
|
||||
Returns the allowance for the item, if not set, returns global allowance.
|
||||
|
||||
Args:
|
||||
item_code: The item to get allowance for.
|
||||
item_allowance: Cached per-item allowances from a previous call.
|
||||
global_qty_allowance: Cached global qty allowance from a previous call.
|
||||
global_amount_allowance: Cached global amount allowance from a previous call.
|
||||
qty_or_amount: Whether to return qty or amount allowance.
|
||||
global_qty_allowance_field: The field name on the settings doctype to use for the global qty allowance.
|
||||
global_qty_allowance_doctype: The settings doctype to read the global qty allowance from.
|
||||
item_qty_allowance_field: The field name on the Item doctype to use for the item-level qty allowance override.
|
||||
"""
|
||||
if item_allowance is None:
|
||||
item_allowance = {}
|
||||
@@ -755,13 +777,13 @@ def get_allowance_for(
|
||||
)
|
||||
|
||||
qty_allowance, over_billing_allowance = frappe.get_cached_value(
|
||||
"Item", item_code, ["over_delivery_receipt_allowance", "over_billing_allowance"]
|
||||
"Item", item_code, [item_qty_allowance_field, "over_billing_allowance"]
|
||||
)
|
||||
|
||||
if qty_or_amount == "qty" and not qty_allowance:
|
||||
if global_qty_allowance is None:
|
||||
global_qty_allowance = flt(
|
||||
frappe.get_cached_value("Stock Settings", None, "over_delivery_receipt_allowance")
|
||||
frappe.get_cached_value(global_qty_allowance_doctype, None, global_qty_allowance_field)
|
||||
)
|
||||
qty_allowance = global_qty_allowance
|
||||
elif qty_or_amount == "amount" and not over_billing_allowance:
|
||||
|
||||
@@ -57,13 +57,13 @@ class TestPurchaseReceipt(ERPNextTestSuite):
|
||||
)
|
||||
mr.insert()
|
||||
mr.submit()
|
||||
frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 200)
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 200)
|
||||
po = make_purchase_order(mr.name)
|
||||
po.supplier = "_Test Supplier"
|
||||
po.items[0].qty = 300
|
||||
po.save()
|
||||
po.submit()
|
||||
frappe.db.set_value("Item", item.name, "over_delivery_receipt_allowance", 20)
|
||||
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
|
||||
pr = make_purchase_receipt(qty=300, item_code=item.name, do_not_save=True)
|
||||
pr.save()
|
||||
pr.submit()
|
||||
|
||||
Reference in New Issue
Block a user