feat: partial delivery in dropshipping (backport #54787) (#54800)

* feat: partial delivery in dropshipping (#54787)

(cherry picked from commit db74360396)

# 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 <kandoimihir@gmail.com>
This commit is contained in:
mergify[bot]
2026-05-09 02:01:42 +00:00
committed by GitHub
parent f36bdaadae
commit f64f871d45
5 changed files with 160 additions and 15 deletions

View File

@@ -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() {

View File

@@ -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)

View File

@@ -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",

View File

@@ -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:

View File

@@ -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