diff --git a/erpnext/stock/doctype/material_request/mapper.py b/erpnext/stock/doctype/material_request/mapper.py index 14def8afcd7..7d192b30b44 100644 --- a/erpnext/stock/doctype/material_request/mapper.py +++ b/erpnext/stock/doctype/material_request/mapper.py @@ -9,6 +9,10 @@ from frappe.model.document import Document from frappe.model.mapper import get_mapped_doc from frappe.utils import cint, flt, getdate, nowdate +from erpnext.setup.doctype.brand.brand import get_brand_defaults +from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults +from erpnext.stock.doctype.item.item import get_item_defaults +from erpnext.stock.get_item_details import get_default_supplier from erpnext.subcontracting.doctype.subcontracting_bom.subcontracting_bom import ( get_subcontracting_boms_for_finished_goods, ) @@ -52,7 +56,7 @@ def make_purchase_order( source_name: str, target_doc: str | dict | Document | None = None, args: dict | str | None = None ): if args is None: - args = {} + args = frappe.flags.args or {} args = frappe.parse_json(args) is_subcontracted = ( @@ -61,6 +65,8 @@ def make_purchase_order( 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 select_item(d): @@ -140,6 +146,70 @@ def make_request_for_quotation(source_name: str, target_doc: str | dict | Docume return doclist +def get_default_supplier_for_item(item_code: str, company: str) -> str | None: + return get_default_supplier( + frappe._dict(), + get_item_defaults(item_code, company), + get_item_group_defaults(item_code, company), + get_brand_defaults(item_code, company), + ) + + +@frappe.whitelist() +def get_item_default_suppliers(source_name: str, filtered_children: str | list | None = None) -> list[dict]: + """Pending items of the Material Request with their default supplier.""" + filtered_children = frappe.parse_json(filtered_children) if filtered_children else [] + + material_request = frappe.get_doc("Material Request", source_name) + material_request.check_permission("read") + + items = [] + for item in material_request.items: + if filtered_children and item.name not in filtered_children: + continue + + ordered_qty = flt(item.ordered_qty) or flt(item.received_qty) + if ordered_qty >= flt(item.stock_qty): + continue + + items.append( + { + "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), + "uom": item.uom, + "supplier": get_default_supplier_for_item(item.item_code, material_request.company), + } + ) + + return items + + +@frappe.whitelist(methods=["POST"]) +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) + + items_by_supplier = {} + for row in item_suppliers: + row = frappe._dict(row) + 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) + + purchase_orders = [] + for supplier, material_request_items in items_by_supplier.items(): + purchase_order = make_purchase_order( + source_name, args={"supplier": supplier, "filtered_children": material_request_items} + ) + purchase_order.insert() + purchase_orders.append(purchase_order.name) + + return purchase_orders + + @frappe.whitelist() def get_items_based_on_default_supplier(supplier: str): supplier_items = [ diff --git a/erpnext/stock/doctype/material_request/material_request.js b/erpnext/stock/doctype/material_request/material_request.js index b5a8c0560cd..a69faf7ec31 100644 --- a/erpnext/stock/doctype/material_request/material_request.js +++ b/erpnext/stock/doctype/material_request/material_request.js @@ -412,13 +412,127 @@ frappe.ui.form.on("Material Request", { }, make_purchase_order: function (frm) { - frappe.model.open_mapped_doc({ - method: "erpnext.stock.doctype.material_request.mapper.make_purchase_order", - frm: frm, - run_link_triggers: true, + frappe.call({ + method: "erpnext.stock.doctype.material_request.mapper.get_item_default_suppliers", + args: { + source_name: frm.doc.name, + filtered_children: (frm.get_selected() || {}).items || [], + }, + freeze: true, + callback: function (r) { + const items = r.message || []; + const suppliers = new Set(items.map((item) => item.supplier || "")); + + if (suppliers.size > 1) { + frm.events.select_suppliers_for_items(frm, items); + return; + } + + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.material_request.mapper.make_purchase_order", + frm: frm, + args: { supplier: items.length ? items[0].supplier : null }, + run_link_triggers: true, + }); + }, }); }, + select_suppliers_for_items: function (frm, items) { + const dialog = new frappe.ui.Dialog({ + title: __("Select Supplier for Items"), + size: "large", + fields: [ + { + fieldname: "items", + fieldtype: "Table", + cannot_add_rows: true, + cannot_delete_rows: true, + in_place_edit: true, + data: items, + get_data: () => items, + description: __("A separate Purchase Order is created for each Supplier."), + fields: [ + { + fieldtype: "Data", + fieldname: "material_request_item", + hidden: 1, + }, + { + fieldtype: "Link", + fieldname: "item_code", + options: "Item", + label: __("Item Code"), + read_only: 1, + in_list_view: 1, + columns: 3, + }, + { + fieldtype: "Data", + fieldname: "item_name", + label: __("Item Name"), + read_only: 1, + in_list_view: 1, + columns: 2, + }, + { + fieldtype: "Float", + fieldname: "qty", + label: __("Quantity"), + read_only: 1, + in_list_view: 1, + columns: 2, + }, + { + fieldtype: "Link", + fieldname: "supplier", + options: "Supplier", + label: __("Supplier"), + reqd: 1, + in_list_view: 1, + columns: 3, + }, + ], + }, + ], + 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])); + } + + frappe.call({ + method: "erpnext.stock.doctype.material_request.mapper.make_purchase_orders_by_supplier", + args: { source_name: frm.doc.name, item_suppliers: rows }, + freeze: true, + callback: function (r) { + if (r.exc) return; + + dialog.hide(); + + const purchase_orders = r.message || []; + if (purchase_orders.length === 1) { + frappe.set_route("Form", "Purchase Order", purchase_orders[0]); + return; + } + + frappe.msgprint({ + title: __("Purchase Orders Created"), + indicator: "green", + message: purchase_orders + .map((name) => frappe.utils.get_form_link("Purchase Order", name, true)) + .join(", "), + }); + }, + }); + }, + }); + + dialog.show(); + }, + make_request_for_quotation: function (frm) { frappe.model.open_mapped_doc({ method: "erpnext.stock.doctype.material_request.mapper.make_request_for_quotation",