feat: adjust the ordered quantity in the supplier selection dialog

The dialog prefilled the pending quantity of each Material Request item but
kept it read only, so ordering less than what was requested meant editing the
Purchase Order afterwards.

The quantity is now editable and is validated against the pending quantity of
its Material Request item, both in the dialog and on the server. The requested
quantity is handed to the mapper as the pending quantity of the source row, so
the existing mapping - including the subcontracting conversions - derives the
Purchase Order quantities from it unchanged.

(cherry picked from commit da83370c5c)
This commit is contained in:
Mihir Kandoi
2026-08-01 08:38:10 +05:30
committed by Mergify
parent f4d3b2771b
commit 2c8c375ca6
2 changed files with 65 additions and 13 deletions

View File

@@ -25,6 +25,16 @@ def set_missing_values(source, target_doc):
target_doc.run_method("calculate_taxes_and_totals")
def get_source_item_for_qty(item, qty):
"""Copy of the source row whose pending quantity is the requested quantity."""
source_item = frappe._dict(item.as_dict())
source_item.ordered_qty = 0
source_item.received_qty = 0
source_item.stock_qty = flt(qty) * flt(item.conversion_factor)
return source_item
def update_item(obj, target, source_parent):
target.conversion_factor = obj.conversion_factor
@@ -63,12 +73,19 @@ def make_purchase_order(
frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
)
requested_qty = args.get("requested_qty") or {}
def postprocess(source, target_doc):
target_doc.is_subcontracted = is_subcontracted
if args.get("supplier"):
target_doc.supplier = args.get("supplier")
set_missing_values(source, target_doc)
def update_requested_item(obj, target, source_parent):
if obj.name in requested_qty:
obj = get_source_item_for_qty(obj, requested_qty[obj.name])
update_item(obj, target, source_parent)
def select_item(d):
filtered_items = args.get("filtered_children", [])
child_filter = d.name in filtered_items if filtered_items else True
@@ -108,7 +125,7 @@ def make_purchase_order(
"doctype": "Purchase Order Item",
"field_map": generate_field_map(),
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
"postprocess": update_item,
"postprocess": update_requested_item,
"condition": select_item,
},
},
@@ -177,7 +194,7 @@ def get_item_default_suppliers(source_name: str, filtered_children: str | list |
"material_request_item": item.name,
"item_code": item.item_code,
"item_name": item.item_name,
"qty": (flt(item.stock_qty) - ordered_qty) / (flt(item.conversion_factor) or 1),
"pending_qty": (flt(item.stock_qty) - ordered_qty) / (flt(item.conversion_factor) or 1),
"uom": item.uom,
"supplier": get_default_supplier_for_item(item.item_code, material_request.company),
}
@@ -190,6 +207,9 @@ def get_item_default_suppliers(source_name: str, filtered_children: str | list |
def make_purchase_orders_by_supplier(source_name: str, item_suppliers: str | list) -> list[str]:
"""Create one draft Purchase Order per supplier for the given Material Request items."""
item_suppliers = frappe.parse_json(item_suppliers)
pending_qty = {
d["material_request_item"]: d["pending_qty"] for d in get_item_default_suppliers(source_name)
}
items_by_supplier = {}
for row in item_suppliers:
@@ -197,12 +217,24 @@ def make_purchase_orders_by_supplier(source_name: str, item_suppliers: str | lis
if not row.supplier:
frappe.throw(_("Select a Supplier for Item {0}").format(frappe.bold(row.item_code)))
items_by_supplier.setdefault(row.supplier, []).append(row.material_request_item)
if flt(row.qty) <= 0 or flt(row.qty) > flt(pending_qty.get(row.material_request_item)):
frappe.throw(
_("Quantity for Item {0} must be greater than zero and cannot exceed {1}").format(
frappe.bold(row.item_code), flt(pending_qty.get(row.material_request_item))
)
)
items_by_supplier.setdefault(row.supplier, {})[row.material_request_item] = flt(row.qty)
purchase_orders = []
for supplier, material_request_items in items_by_supplier.items():
for supplier, requested_qty in items_by_supplier.items():
purchase_order = make_purchase_order(
source_name, args={"supplier": supplier, "filtered_children": material_request_items}
source_name,
args={
"supplier": supplier,
"filtered_children": list(requested_qty),
"requested_qty": requested_qty,
},
)
purchase_order.insert()
purchase_orders.append(purchase_order.name)

View File

@@ -445,6 +445,8 @@ frappe.ui.form.on("Material Request", {
},
select_suppliers_for_items: function (frm, items) {
const rows = items.map((item) => Object.assign({}, item, { qty: item.pending_qty }));
const dialog = new frappe.ui.Dialog({
title: __("Select Supplier for Items"),
size: "large",
@@ -455,8 +457,8 @@ frappe.ui.form.on("Material Request", {
cannot_add_rows: true,
cannot_delete_rows: true,
in_place_edit: true,
data: items,
get_data: () => items,
data: rows,
get_data: () => rows,
description: __("A separate Purchase Order is created for each Supplier."),
fields: [
{
@@ -481,11 +483,16 @@ frappe.ui.form.on("Material Request", {
in_list_view: 1,
columns: 2,
},
{
fieldtype: "Float",
fieldname: "pending_qty",
hidden: 1,
},
{
fieldtype: "Float",
fieldname: "qty",
label: __("Quantity"),
read_only: 1,
reqd: 1,
in_list_view: 1,
columns: 2,
},
@@ -503,15 +510,28 @@ frappe.ui.form.on("Material Request", {
],
primary_action_label: __("Create"),
primary_action: function (values) {
const rows = values.items || [];
const missing = rows.find((row) => !row.supplier);
if (missing) {
frappe.throw(__("Select a Supplier for Item {0}", [missing.item_code]));
const item_suppliers = values.items || [];
const missing_supplier = item_suppliers.find((row) => !row.supplier);
if (missing_supplier) {
frappe.throw(__("Select a Supplier for Item {0}", [missing_supplier.item_code]));
}
const invalid_qty = item_suppliers.find(
(row) => flt(row.qty) <= 0 || flt(row.qty) > flt(row.pending_qty)
);
if (invalid_qty) {
frappe.throw(
__("Quantity for Item {0} must be greater than zero and cannot exceed {1}", [
invalid_qty.item_code,
format_number(invalid_qty.pending_qty),
])
);
}
frappe.call({
method: "erpnext.stock.doctype.material_request.mapper.make_purchase_orders_by_supplier",
args: { source_name: frm.doc.name, item_suppliers: rows },
args: { source_name: frm.doc.name, item_suppliers: item_suppliers },
freeze: true,
callback: function (r) {
if (r.exc) return;