mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-13 22:51:49 +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")
|
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):
|
def update_item(obj, target, source_parent):
|
||||||
target.conversion_factor = obj.conversion_factor
|
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"
|
frappe.db.get_value("Material Request", source_name, "material_request_type") == "Subcontracting"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
requested_qty = args.get("requested_qty") or {}
|
||||||
|
|
||||||
def postprocess(source, target_doc):
|
def postprocess(source, target_doc):
|
||||||
target_doc.is_subcontracted = is_subcontracted
|
target_doc.is_subcontracted = is_subcontracted
|
||||||
if args.get("supplier"):
|
if args.get("supplier"):
|
||||||
target_doc.supplier = args.get("supplier")
|
target_doc.supplier = args.get("supplier")
|
||||||
set_missing_values(source, target_doc)
|
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):
|
def select_item(d):
|
||||||
filtered_items = args.get("filtered_children", [])
|
filtered_items = args.get("filtered_children", [])
|
||||||
child_filter = d.name in filtered_items if filtered_items else True
|
child_filter = d.name in filtered_items if filtered_items else True
|
||||||
@@ -108,7 +125,7 @@ def make_purchase_order(
|
|||||||
"doctype": "Purchase Order Item",
|
"doctype": "Purchase Order Item",
|
||||||
"field_map": generate_field_map(),
|
"field_map": generate_field_map(),
|
||||||
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
|
"field_no_map": ["item_code", "item_name", "qty"] if is_subcontracted else [],
|
||||||
"postprocess": update_item,
|
"postprocess": update_requested_item,
|
||||||
"condition": select_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,
|
"material_request_item": item.name,
|
||||||
"item_code": item.item_code,
|
"item_code": item.item_code,
|
||||||
"item_name": item.item_name,
|
"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,
|
"uom": item.uom,
|
||||||
"supplier": get_default_supplier_for_item(item.item_code, material_request.company),
|
"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]:
|
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."""
|
"""Create one draft Purchase Order per supplier for the given Material Request items."""
|
||||||
item_suppliers = frappe.parse_json(item_suppliers)
|
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 = {}
|
items_by_supplier = {}
|
||||||
for row in item_suppliers:
|
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:
|
if not row.supplier:
|
||||||
frappe.throw(_("Select a Supplier for Item {0}").format(frappe.bold(row.item_code)))
|
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 = []
|
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(
|
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_order.insert()
|
||||||
purchase_orders.append(purchase_order.name)
|
purchase_orders.append(purchase_order.name)
|
||||||
|
|||||||
@@ -445,6 +445,8 @@ frappe.ui.form.on("Material Request", {
|
|||||||
},
|
},
|
||||||
|
|
||||||
select_suppliers_for_items: function (frm, items) {
|
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({
|
const dialog = new frappe.ui.Dialog({
|
||||||
title: __("Select Supplier for Items"),
|
title: __("Select Supplier for Items"),
|
||||||
size: "large",
|
size: "large",
|
||||||
@@ -455,8 +457,8 @@ frappe.ui.form.on("Material Request", {
|
|||||||
cannot_add_rows: true,
|
cannot_add_rows: true,
|
||||||
cannot_delete_rows: true,
|
cannot_delete_rows: true,
|
||||||
in_place_edit: true,
|
in_place_edit: true,
|
||||||
data: items,
|
data: rows,
|
||||||
get_data: () => items,
|
get_data: () => rows,
|
||||||
description: __("A separate Purchase Order is created for each Supplier."),
|
description: __("A separate Purchase Order is created for each Supplier."),
|
||||||
fields: [
|
fields: [
|
||||||
{
|
{
|
||||||
@@ -481,11 +483,16 @@ frappe.ui.form.on("Material Request", {
|
|||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
columns: 2,
|
columns: 2,
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
fieldtype: "Float",
|
||||||
|
fieldname: "pending_qty",
|
||||||
|
hidden: 1,
|
||||||
|
},
|
||||||
{
|
{
|
||||||
fieldtype: "Float",
|
fieldtype: "Float",
|
||||||
fieldname: "qty",
|
fieldname: "qty",
|
||||||
label: __("Quantity"),
|
label: __("Quantity"),
|
||||||
read_only: 1,
|
reqd: 1,
|
||||||
in_list_view: 1,
|
in_list_view: 1,
|
||||||
columns: 2,
|
columns: 2,
|
||||||
},
|
},
|
||||||
@@ -503,15 +510,28 @@ frappe.ui.form.on("Material Request", {
|
|||||||
],
|
],
|
||||||
primary_action_label: __("Create"),
|
primary_action_label: __("Create"),
|
||||||
primary_action: function (values) {
|
primary_action: function (values) {
|
||||||
const rows = values.items || [];
|
const item_suppliers = values.items || [];
|
||||||
const missing = rows.find((row) => !row.supplier);
|
|
||||||
if (missing) {
|
const missing_supplier = item_suppliers.find((row) => !row.supplier);
|
||||||
frappe.throw(__("Select a Supplier for Item {0}", [missing.item_code]));
|
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({
|
frappe.call({
|
||||||
method: "erpnext.stock.doctype.material_request.mapper.make_purchase_orders_by_supplier",
|
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,
|
freeze: true,
|
||||||
callback: function (r) {
|
callback: function (r) {
|
||||||
if (r.exc) return;
|
if (r.exc) return;
|
||||||
|
|||||||
Reference in New Issue
Block a user