From f64f871d451666ab77fe2b65a0ef85a997135282 Mon Sep 17 00:00:00 2001 From: "mergify[bot]" <37929162+mergify[bot]@users.noreply.github.com> Date: Sat, 9 May 2026 02:01:42 +0000 Subject: [PATCH] feat: partial delivery in dropshipping (backport #54787) (#54800) * feat: partial delivery in dropshipping (#54787) (cherry picked from commit db7436039609a9e76cd0f9117750723be596b240) # Conflicts: # erpnext/buying/doctype/purchase_order/purchase_order.py # erpnext/buying/doctype/purchase_order_item/purchase_order_item.json * chore: resolve conflicts * chore: resolve conflicts --------- Co-authored-by: Mihir Kandoi --- .../doctype/purchase_order/purchase_order.js | 149 +++++++++++++++++- .../doctype/purchase_order/purchase_order.py | 16 +- .../purchase_order_item.json | 4 +- erpnext/controllers/accounts_controller.py | 4 +- .../doctype/sales_order/sales_order.py | 2 +- 5 files changed, 160 insertions(+), 15 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index a6b0db94638..bb6e261c22d 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -354,9 +354,9 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } } - if (is_drop_ship && doc.status != "Delivered") { + if (is_drop_ship && !["Completed", "Delivered"].includes(doc.status)) { this.frm.add_custom_button( - __("Delivered"), + __("Deliver (Dropship)"), this.delivered_by_supplier.bind(this), __("Status") ); @@ -374,7 +374,12 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } if (doc.status != "Closed") { if (doc.status != "On Hold") { - if (flt(doc.per_received) < 100 && allow_receipt) { + if ( + doc.items + .filter((item) => !item.delivered_by_supplier) + .some((item) => item.received_qty < item.qty) && + allow_receipt + ) { this.frm.add_custom_button( __("Purchase Receipt"), () => { @@ -730,7 +735,143 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends ( } delivered_by_supplier() { - this.frm.cscript.update_status("Deliver", "Delivered"); + const data = this.frm.doc.items + .filter((item) => item.delivered_by_supplier == 1) + .map((item) => { + return { + __checked: item.qty > item.received_qty, + name: item.name, + item_code: item.item_code, + item_name: item.item_name, + qty: item.qty, + uom: item.uom, + delivered_qty: item.received_qty || 0, + qty_change: item.qty - item.received_qty, + }; + }); + const dialog = new frappe.ui.Dialog({ + title: __("Set Dropship Items Delivered Quantity"), + size: "extra-large", + fields: [ + { + fieldname: "items", + fieldtype: "Table", + data: data, + cannot_add_rows: true, + cannot_delete_rows: true, + fields: [ + { + fieldname: "name", + fieldtype: "Data", + read_only: true, + hidden: 1, + }, + { + fieldname: "item_code", + fieldtype: "Link", + options: "Item", + label: __("Item Code"), + in_list_view: 1, + read_only: true, + }, + { + fieldname: "item_name", + fieldtype: "Data", + label: __("Item Name"), + in_list_view: 1, + read_only: true, + }, + { + fieldname: "qty", + fieldtype: "Float", + label: __("Quantity"), + in_list_view: 1, + read_only: true, + }, + { + fieldname: "uom", + fieldtype: "Data", + label: __("UOM"), + in_list_view: 1, + read_only: true, + }, + { + fieldname: "delivered_qty", + fieldtype: "Float", + label: __("Delivered Qty"), + read_only: true, + in_list_view: 1, + }, + { + fieldname: "qty_change", + fieldtype: "Float", + label: __("Qty Change"), + in_list_view: 1, + reqd: 1, + }, + ], + }, + ], + primary_action: (values) => { + const data = values.items.filter((item) => item.__checked); + if (!data.length) { + frappe.throw(__("Please select at least one item to update delivered quantity.")); + } + + data.forEach((item) => { + if (!item.qty_change) { + frappe.throw( + __( + "Item {0} has no changes in delivered quantity. Please unselect the row if you do not wish to update its quantity.", + [item.item_code.bold()] + ) + ); + } + if (item.qty_change < 0 && Math.abs(item.qty_change) > item.delivered_qty) { + frappe.throw( + __("Delivered Qty cannot be reduced by more than {0} for item {1}", [ + item.delivered_qty, + item.item_code.bold(), + ]) + ); + } + if (item.qty_change > 0 && item.delivered_qty + item.qty_change > item.qty) { + frappe.throw( + __("Delivered Qty cannot be increased by more than {0} for item {1}", [ + item.qty - item.delivered_qty, + item.item_code.bold(), + ]) + ); + } + }); + + data.forEach((item) => { + frappe.model.set_value( + "Purchase Order Item", + item.name, + "received_qty", + item.delivered_qty + item.qty_change + ); + }); + + const frm = this.frm; + frm.dirty(); + frm.save("Update", () => { + frappe.call({ + doc: frm.doc, + method: "update_receiving_percentage", + callback: function (r) { + if (!r.exc) { + dialog.hide(); + frappe.toast(__("Quantities updated successfully.")); + frm.reload_doc(); + } + }, + }); + }); + }, + }); + dialog.show(); } items_on_form_rendered() { diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 48ed761829e..140eef0ba38 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -218,7 +218,6 @@ class PurchaseOrder(BuyingController): self.create_raw_materials_supplied() self.validate_fg_item_for_subcontracting() - self.set_received_qty_for_drop_ship_items() if not self.advance_payment_status: self.advance_payment_status = "Not Initiated" @@ -493,6 +492,8 @@ class PurchaseOrder(BuyingController): if self.has_drop_ship_item(): self.update_delivered_qty_in_sales_order() + self.set_received_qty_to_zero_for_drop_ship_items() + self.update_receiving_percentage() self.update_reserved_qty_for_subcontract() self.check_on_hold_or_closed_status() @@ -566,6 +567,11 @@ class PurchaseOrder(BuyingController): so.set_status(update=True) so.notify_update() + def set_received_qty_to_zero_for_drop_ship_items(self): + for item in self.items: + if item.delivered_by_supplier: + item.db_set("received_qty", 0) + def has_drop_ship_item(self): return any(d.delivered_by_supplier for d in self.items) @@ -575,11 +581,6 @@ class PurchaseOrder(BuyingController): def is_against_pp(self): return any(d.production_plan for d in self.items if d.production_plan) - def set_received_qty_for_drop_ship_items(self): - for item in self.items: - if item.delivered_by_supplier == 1: - item.received_qty = item.qty - def update_reserved_qty_for_subcontract(self): if self.is_old_subcontracting_flow: for d in self.supplied_items: @@ -587,12 +588,13 @@ class PurchaseOrder(BuyingController): stock_bin = get_bin(d.rm_item_code, d.reserve_warehouse) stock_bin.update_reserved_qty_for_sub_contracting(subcontract_doctype="Purchase Order") + @frappe.whitelist() def update_receiving_percentage(self): total_qty, received_qty = 0.0, 0.0 for item in self.items: received_qty += min(item.received_qty, item.qty) total_qty += item.qty - if total_qty: + if total_qty and received_qty: self.db_set("per_received", flt(received_qty / total_qty) * 100, update_modified=False) else: self.db_set("per_received", 0, update_modified=False) diff --git a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json index 2337d6a9fb6..7770b3b572d 100644 --- a/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json +++ b/erpnext/buying/doctype/purchase_order_item/purchase_order_item.json @@ -622,11 +622,13 @@ "width": "100px" }, { + "allow_on_submit": 1, "depends_on": "received_qty", "fieldname": "received_qty", "fieldtype": "Float", "label": "Received Qty", "no_copy": 1, + "non_negative": 1, "oldfieldname": "received_qty", "oldfieldtype": "Currency", "print_hide": 1, @@ -950,7 +952,7 @@ "index_web_pages_for_search": 1, "istable": 1, "links": [], - "modified": "2025-11-30 16:51:57.761673", + "modified": "2026-05-08 20:40:10.683023", "modified_by": "Administrator", "module": "Buying", "name": "Purchase Order Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4f931bb0724..580813b91d3 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -3908,8 +3908,8 @@ def update_child_qty_rate(parent_doctype, trans_items, parent_doctype_name, chil ) qty_limits = { - "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity")), - "Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity")), + "Sales Order": ("delivered_qty", _("Cannot set quantity less than delivered quantity.")), + "Purchase Order": ("received_qty", _("Cannot set quantity less than received quantity.")), } if parent_doctype in qty_limits: diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 5b6fe11f4fb..4b38369d048 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -1628,7 +1628,7 @@ def make_purchase_order(source_name, selected_items=None, target_doc=None): if default_payment_terms: target.payment_terms_template = default_payment_terms - if any(item.delivered_by_supplier == 1 for item in source.items): + if any(item.delivered_by_supplier for item in target.items): if source.shipping_address_name: target.shipping_address = source.shipping_address_name target.shipping_address_display = source.shipping_address