mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-08 20:21:47 +00:00
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:
@@ -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)
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user