Merge pull request #57725 from aerele/fix/stock-over-delivery-role-scope

fix(stock): scope over deliver/receive role check to delivery and receipt overflow
This commit is contained in:
Mihir Kandoi
2026-08-03 12:39:08 +05:30
committed by GitHub
5 changed files with 63 additions and 13 deletions

View File

@@ -216,6 +216,21 @@ class TestPurchaseOrder(ERPNextTestSuite):
po2.items[0].qty = 110 po2.items[0].qty = 110
self.assertRaises(OverAllowanceError, po2.submit) self.assertRaises(OverAllowanceError, po2.submit)
# Stock over-delivery role must not bypass over-ordering against Material Request.
with self.change_settings(
"Stock Settings", {"role_allowed_to_over_deliver_receive": "Stock Manager"}
):
test_user = frappe.get_doc("User", "test@example.com")
test_user.add_roles("Stock Manager")
mr3 = make_material_request(qty=100)
po3 = make_purchase_order(mr3.name)
po3.supplier = "_Test Supplier"
po3.items[0].qty = 110
with self.set_user("test@example.com"):
po3.flags.ignore_permissions = True
self.assertRaises(OverAllowanceError, po3.submit)
# cleanup # cleanup
frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0) frappe.db.set_single_value("Buying Settings", "over_order_allowance", 0)
frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0) frappe.db.set_single_value("Stock Settings", "over_delivery_receipt_allowance", 0)
@@ -1044,6 +1059,8 @@ class TestPurchaseOrder(ERPNextTestSuite):
# self.assertEqual(po.payment_terms_template, pi.payment_terms_template) # self.assertEqual(po.payment_terms_template, pi.payment_terms_template)
compare_payment_schedules(self, po, pi) compare_payment_schedules(self, po, pi)
@ERPNextTestSuite.change_settings("Selling Settings", {"maintain_same_sales_rate": 1})
@ERPNextTestSuite.change_settings("Buying Settings", {"maintain_same_rate": 1})
def test_internal_transfer_flow(self): def test_internal_transfer_flow(self):
from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center from erpnext.accounts.doctype.cost_center.test_cost_center import create_cost_center
from erpnext.accounts.doctype.sales_invoice.mapper import ( from erpnext.accounts.doctype.sales_invoice.mapper import (
@@ -1055,9 +1072,6 @@ class TestPurchaseOrder(ERPNextTestSuite):
) )
from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt from erpnext.stock.doctype.delivery_note.mapper import make_inter_company_purchase_receipt
frappe.db.set_single_value("Selling Settings", "maintain_same_sales_rate", 1)
frappe.db.set_single_value("Buying Settings", "maintain_same_rate", 1)
prepare_data_for_internal_transfer() prepare_data_for_internal_transfer()
supplier = "_Test Internal Supplier 2" supplier = "_Test Internal Supplier 2"
@@ -1496,6 +1510,7 @@ class TestPurchaseOrder(ERPNextTestSuite):
self.assertEqual(pi_2.status, "Paid") self.assertEqual(pi_2.status, "Paid")
self.assertEqual(po.status, "Completed") self.assertEqual(po.status, "Completed")
@ERPNextTestSuite.change_settings("Buying Settings", {"maintain_same_rate": 0})
def test_purchase_order_over_billing_missing_item(self): def test_purchase_order_over_billing_missing_item(self):
item1 = make_item( item1 = make_item(
"_Test Item for Overbilling", "_Test Item for Overbilling",

View File

@@ -446,11 +446,12 @@ class StatusUpdater(Document):
else (0, {}, None, None) else (0, {}, None, None)
) )
role_allowed_to_over_deliver_receive = frappe.get_single_value( role = None
"Stock Settings", "role_allowed_to_over_deliver_receive" if qty_or_amount == "qty":
) if args.get("overflow_type") in ("delivery", "receipt"):
role_allowed_to_over_bill = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill") role = frappe.get_single_value("Stock Settings", "role_allowed_to_over_deliver_receive")
role = role_allowed_to_over_deliver_receive if qty_or_amount == "qty" else role_allowed_to_over_bill else:
role = frappe.get_single_value("Accounts Settings", "role_allowed_to_over_bill")
overflow_percent = ( overflow_percent = (
(item[args["target_field"]] - item[args["target_ref_field"]]) / item[args["target_ref_field"]] (item[args["target_field"]] - item[args["target_ref_field"]]) / item[args["target_ref_field"]]

View File

@@ -91,6 +91,32 @@ class TestBlanketOrder(ERPNextTestSuite):
frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10) frappe.db.set_single_value("Buying Settings", "blanket_order_allowance", 10)
po.submit() po.submit()
@ERPNextTestSuite.change_settings("Selling Settings", {"blanket_order_allowance": 0})
@ERPNextTestSuite.change_settings("Buying Settings", {"blanket_order_allowance": 0})
@ERPNextTestSuite.change_settings(
"Stock Settings",
{"over_delivery_receipt_allowance": 10, "role_allowed_to_over_deliver_receive": "Stock Manager"},
)
def test_stock_over_delivery_role_does_not_bypass_blanket_order_allowance(self):
test_user = frappe.get_doc("User", "test@example.com")
test_user.add_roles("Stock Manager")
frappe.clear_cache()
for blanket_order_type, doctype, date_field in (
("Selling", "Sales Order", "delivery_date"),
("Purchasing", "Purchase Order", "schedule_date"),
):
bo = make_blanket_order(blanket_order_type=blanket_order_type, quantity=100)
frappe.flags.args.doctype = doctype
order = make_order(bo.name)
order.currency = get_company_currency(order.company)
setattr(order, date_field, today())
order.items[0].qty = 110
with self.set_user("test@example.com"):
order.flags.ignore_permissions = True
self.assertRaises(frappe.ValidationError, order.submit)
def test_blanket_order_over_order_aggregated_across_rows(self): def test_blanket_order_over_order_aggregated_across_rows(self):
# the over-order check should sum the same item across multiple order rows # the over-order check should sum the same item across multiple order rows
frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 0) frappe.db.set_single_value("Selling Settings", "blanket_order_allowance", 0)

View File

@@ -125,7 +125,8 @@
"description": "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.", "description": "The percentage you are allowed to receive or deliver more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed to receive 110 units.",
"fieldname": "over_delivery_receipt_allowance", "fieldname": "over_delivery_receipt_allowance",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Over Delivery/Receipt Allowance (%)" "label": "Over Delivery/Receipt Allowance (%)",
"non_negative": 1
}, },
{ {
"default": "Stop", "default": "Stop",
@@ -276,7 +277,8 @@
"description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.", "description": "The percentage you are allowed to transfer more against the quantity ordered. For example, if you have ordered 100 units, and your Allowance is 10%, then you are allowed transfer 110 units.",
"fieldname": "mr_qty_allowance", "fieldname": "mr_qty_allowance",
"fieldtype": "Float", "fieldtype": "Float",
"label": "Over Transfer Allowance (%)" "label": "Over Transfer Allowance (%)",
"non_negative": 1
}, },
{ {
"default": "0", "default": "0",
@@ -437,7 +439,8 @@
"description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.", "description": "The percentage you are allowed to pick more items in the pick list than the ordered quantity.",
"fieldname": "over_picking_allowance", "fieldname": "over_picking_allowance",
"fieldtype": "Percent", "fieldtype": "Percent",
"label": "Over Picking Allowance (%)" "label": "Over Picking Allowance (%)",
"non_negative": 1
}, },
{ {
"default": "1", "default": "1",
@@ -590,7 +593,7 @@
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"issingle": 1, "issingle": 1,
"links": [], "links": [],
"modified": "2026-07-16 17:00:00.000000", "modified": "2026-08-01 23:35:02.896836",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Stock", "module": "Stock",
"name": "Stock Settings", "name": "Stock Settings",

View File

@@ -68,7 +68,7 @@ class StockSettings(Document):
use_naming_series: DF.Check use_naming_series: DF.Check
use_serial_batch_fields: DF.Check use_serial_batch_fields: DF.Check
validate_material_transfer_warehouses: DF.Check validate_material_transfer_warehouses: DF.Check
valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO"] valuation_method: DF.Literal["FIFO", "Moving Average", "LIFO", "Standard Cost"]
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
@@ -101,6 +101,7 @@ class StockSettings(Document):
validate_fields_for_doctype=False, validate_fields_for_doctype=False,
) )
self.validate_over_delivery_receipt_allowance()
self.validate_serial_and_batch_no_settings() self.validate_serial_and_batch_no_settings()
self.cant_change_valuation_method() self.cant_change_valuation_method()
self.validate_clean_description_html() self.validate_clean_description_html()
@@ -112,6 +113,10 @@ class StockSettings(Document):
self.change_precision_for_stock_entry() self.change_precision_for_stock_entry()
self.validate_do_not_use_batchwise_valuation() self.validate_do_not_use_batchwise_valuation()
def validate_over_delivery_receipt_allowance(self):
if not self.over_delivery_receipt_allowance:
self.role_allowed_to_over_deliver_receive = None
def validate_do_not_use_batchwise_valuation(self): def validate_do_not_use_batchwise_valuation(self):
doc_before_save = self.get_doc_before_save() doc_before_save = self.get_doc_before_save()
if not doc_before_save: if not doc_before_save: