refactor: creation of purchase order from sales order

This commit is contained in:
Mihir Kandoi
2025-11-25 12:16:48 +05:30
parent 33135899ab
commit a436c6a503
2 changed files with 56 additions and 192 deletions

View File

@@ -1584,12 +1584,6 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
title: __("Select Items"),
size: "large",
fields: [
{
fieldtype: "Check",
label: __("Against Default Supplier"),
fieldname: "against_default_supplier",
default: 0,
},
{
fieldname: "items_for_po",
fieldtype: "Table",
@@ -1625,10 +1619,11 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
in_list_view: 1,
},
{
fieldtype: "Data",
fieldtype: "Link",
fieldname: "supplier",
label: __("Supplier"),
read_only: 1,
reqd: 1,
options: "Supplier",
in_list_view: 1,
},
],
@@ -1647,13 +1642,17 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
});
}
dialog.hide();
if (selected_items.some((item) => !item.supplier)) {
frappe.throw({
message: "Supplier is required for all selected Items",
title: __("Supplier Required"),
indicator: "blue",
});
}
var method = args.against_default_supplier
? "make_purchase_order_for_default_supplier"
: "make_purchase_order";
dialog.hide();
return frappe.call({
method: "erpnext.selling.doctype.sales_order.sales_order." + method,
method: "erpnext.selling.doctype.sales_order.sales_order.make_purchase_order",
freeze_message: __("Creating Purchase Order ..."),
args: {
source_name: me.frm.doc.name,
@@ -1662,9 +1661,9 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
freeze: true,
callback: function (r) {
if (!r.exc) {
if (!args.against_default_supplier) {
frappe.model.sync(r.message);
frappe.set_route("Form", r.message.doctype, r.message.name);
if (r.message.length == 1) {
frappe.model.sync(r.message[0]);
frappe.set_route("Form", r.message[0].doctype, r.message[0].name);
} else {
frappe.route_options = {
sales_order: me.frm.doc.name,
@@ -1677,37 +1676,25 @@ erpnext.selling.SalesOrderController = class SalesOrderController extends erpnex
},
});
dialog.fields_dict["against_default_supplier"].df.onchange = () => set_po_items_data(dialog);
function set_po_items_data(dialog) {
var against_default_supplier = dialog.get_value("against_default_supplier");
var items_for_po = dialog.get_value("items_for_po");
let po_items = [];
me.frm.doc.items.forEach((d) => {
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
name: d.name,
item_name: d.item_name,
item_code: d.item_code,
pending_qty: pending_qty,
uom: d.uom,
supplier: d.supplier,
});
}
});
if (against_default_supplier) {
let items_with_supplier = items_for_po.filter((item) => item.supplier);
dialog.fields_dict["items_for_po"].df.data = items_with_supplier;
dialog.get_field("items_for_po").refresh();
} else {
let po_items = [];
me.frm.doc.items.forEach((d) => {
let ordered_qty = me.get_ordered_qty(d, me.frm.doc);
let pending_qty = (flt(d.stock_qty) - ordered_qty) / flt(d.conversion_factor);
if (pending_qty > 0) {
po_items.push({
name: d.name,
item_name: d.item_name,
item_code: d.item_code,
pending_qty: pending_qty,
uom: d.uom,
supplier: d.supplier,
});
}
});
dialog.fields_dict["items_for_po"].df.data = po_items;
dialog.get_field("items_for_po").refresh();
}
dialog.fields_dict["items_for_po"].df.data = po_items;
dialog.get_field("items_for_po").refresh();
}
set_po_items_data(dialog);

View File

@@ -1546,7 +1546,7 @@ def get_events(start, end, filters=None):
@frappe.whitelist()
def make_purchase_order_for_default_supplier(source_name, selected_items=None, target_doc=None):
def make_purchase_order(source_name, selected_items=None, target_doc=None):
"""Creates Purchase Order for each Supplier. Returns a list of doc objects."""
from erpnext.setup.utils import get_exchange_rate
@@ -1615,11 +1615,28 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
def update_item_for_packed_item(source, target, source_parent):
target.qty = flt(source.qty) - flt(source.ordered_qty)
def filter_items(item, supplier):
if (
item.ordered_qty < item.stock_qty
and item.item_code in item_codes
and not is_product_bundle(item.item_code)
):
item_supp_comb = next(iter(d for d in items_to_map if d["item_code"] == item.item_code), None)
if item_supp_comb and item_supp_comb.get("supplier") == supplier:
items_to_map.remove(item_supp_comb)
return True
return False
suppliers = [item.get("supplier") for item in selected_items if item.get("supplier")]
suppliers = list(dict.fromkeys(suppliers)) # remove duplicates while preserving order
items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")]
items_to_map = list(set(items_to_map))
item_codes = [item.get("item_code") for item in selected_items if item.get("item_code")]
items_to_map = [
{"item_code": item.get("item_code"), "supplier": item.get("supplier")}
for item in selected_items
if item.get("item_code")
]
if not suppliers:
frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order."))
@@ -1665,10 +1682,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"margin_rate_or_amount",
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
and doc.supplier == supplier
and doc.item_code in items_to_map
and not is_product_bundle(doc.item_code),
"condition": lambda doc: filter_items(doc, supplier),
},
"Packed Item": {
"doctype": "Purchase Order Item",
@@ -1689,7 +1703,7 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
"pricing_rules",
],
"postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
"condition": lambda doc: doc.parent_item in item_codes
and flt(doc.ordered_qty) < flt(doc.qty),
},
},
@@ -1698,150 +1712,13 @@ def make_purchase_order_for_default_supplier(source_name, selected_items=None, t
)
set_delivery_date(doc.items, source_name)
doc.insert()
frappe.db.commit()
if len(suppliers) > 1:
doc.insert()
purchase_orders.append(doc)
return purchase_orders
@frappe.whitelist()
def make_purchase_order(source_name, selected_items=None, target_doc=None):
if not selected_items:
return
if isinstance(selected_items, str):
selected_items = json.loads(selected_items)
items_to_map = [item.get("item_code") for item in selected_items if item.get("item_code")]
items_to_map = list(set(items_to_map))
def is_drop_ship_order(target):
drop_ship = True
for item in target.items:
if not item.delivered_by_supplier:
drop_ship = False
break
return drop_ship
def set_missing_values(source, target):
target.supplier = ""
target.apply_discount_on = ""
target.additional_discount_percentage = 0.0
target.discount_amount = 0.0
target.inter_company_order_reference = ""
target.shipping_rule = ""
target.tc_name = ""
target.terms = ""
target.payment_terms_template = ""
target.payment_schedule = []
if is_drop_ship_order(target):
if source.shipping_address_name:
target.shipping_address = source.shipping_address_name
target.shipping_address_display = source.shipping_address
else:
target.shipping_address = source.customer_address
target.shipping_address_display = source.address_display
target.customer_contact_person = source.contact_person
target.customer_contact_display = source.contact_display
target.customer_contact_mobile = source.contact_mobile
target.customer_contact_email = source.contact_email
else:
target.customer = target.customer_name = target.shipping_address = None
target.run_method("set_missing_values")
if not target.taxes:
target.append_taxes_from_item_tax_template()
target.run_method("calculate_taxes_and_totals")
def update_item(source, target, source_parent):
target.schedule_date = source.delivery_date
target.qty = flt(source.qty) - (flt(source.ordered_qty) / flt(source.conversion_factor))
target.stock_qty = flt(source.stock_qty) - flt(source.ordered_qty)
target.project = source_parent.project
def update_item_for_packed_item(source, target, source_parent):
target.qty = flt(source.qty) - flt(source.ordered_qty)
# po = frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")})
doc = get_mapped_doc(
"Sales Order",
source_name,
{
"Sales Order": {
"doctype": "Purchase Order",
"field_map": {"dispatch_address_name": "dispatch_address"},
"field_no_map": [
"address_display",
"contact_display",
"contact_mobile",
"contact_email",
"contact_person",
"taxes_and_charges",
"shipping_address",
],
"validation": {"docstatus": ["=", 1]},
},
"Sales Order Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_item"],
["parent", "sales_order"],
["stock_uom", "stock_uom"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["delivery_date", "schedule_date"],
],
"field_no_map": [
"rate",
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"supplier",
"pricing_rules",
],
"postprocess": update_item,
"condition": lambda doc: doc.ordered_qty < doc.stock_qty
and doc.item_code in items_to_map
and not is_product_bundle(doc.item_code),
},
"Packed Item": {
"doctype": "Purchase Order Item",
"field_map": [
["name", "sales_order_packed_item"],
["parent", "sales_order"],
["uom", "uom"],
["conversion_factor", "conversion_factor"],
["parent_item", "product_bundle"],
["rate", "rate"],
],
"field_no_map": [
"price_list_rate",
"item_tax_template",
"discount_percentage",
"discount_amount",
"supplier",
"pricing_rules",
],
"postprocess": update_item_for_packed_item,
"condition": lambda doc: doc.parent_item in items_to_map
and flt(doc.ordered_qty) < flt(doc.qty),
},
},
target_doc,
set_missing_values,
)
set_delivery_date(doc.items, source_name)
doc.set_onload("load_after_mapping", False)
return doc
def set_delivery_date(items, sales_order):
delivery_dates = frappe.get_all(
"Sales Order Item", filters={"parent": sales_order}, fields=["delivery_date", "item_code"]