mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-21 14:09:19 +00:00
fix: allow direct drop-ship on Purchase Orders without Sales Order (#54930)
This commit is contained in:
@@ -333,7 +333,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
if (is_drop_ship && !["Completed", "Delivered"].includes(doc.status)) {
|
||||
this.frm.add_custom_button(
|
||||
__("Deliver (Dropship)"),
|
||||
this.delivered_by_supplier.bind(this),
|
||||
this.update_dropship_delivered_qty.bind(this),
|
||||
__("Status")
|
||||
);
|
||||
|
||||
@@ -698,7 +698,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
||||
this.frm.cscript.update_status("Close", "Closed");
|
||||
}
|
||||
|
||||
delivered_by_supplier() {
|
||||
update_dropship_delivered_qty() {
|
||||
const data = this.frm.doc.items
|
||||
.filter((item) => item.delivered_by_supplier == 1)
|
||||
.map((item) => {
|
||||
|
||||
@@ -664,12 +664,21 @@ class PurchaseOrder(BuyingController):
|
||||
if not self.is_against_so():
|
||||
return
|
||||
for item in removed_items:
|
||||
sales_order_item = item.get("sales_order_item")
|
||||
if not sales_order_item:
|
||||
continue
|
||||
|
||||
prev_ordered_qty = flt(
|
||||
frappe.get_cached_value("Sales Order Item", item.get("sales_order_item"), "ordered_qty")
|
||||
frappe.get_cached_value("Sales Order Item", sales_order_item, "ordered_qty")
|
||||
)
|
||||
# `Sales Order Item.ordered_qty` is tracked in stock UOM (see status_updater);
|
||||
# use the row's stock_qty so PO UOMs that differ from stock UOM decrement correctly.
|
||||
qty_in_stock_uom = flt(item.get("stock_qty")) or flt(item.qty) * flt(
|
||||
item.get("conversion_factor") or 1
|
||||
)
|
||||
|
||||
frappe.db.set_value(
|
||||
"Sales Order Item", item.get("sales_order_item"), "ordered_qty", prev_ordered_qty - item.qty
|
||||
"Sales Order Item", sales_order_item, "ordered_qty", prev_ordered_qty - qty_in_stock_uom
|
||||
)
|
||||
|
||||
def auto_create_subcontracting_order(self):
|
||||
|
||||
@@ -1436,6 +1436,88 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
||||
pi2 = make_pi_from_po(po.name)
|
||||
self.assertEqual(len(pi2.items), 2)
|
||||
|
||||
def test_get_item_details_propagates_drop_ship_flag_to_po(self):
|
||||
"""`get_item_details` should propagate the Item master's
|
||||
`delivered_by_supplier` flag to Purchase Orders, not only to Sales
|
||||
Orders/Invoices, so that POs can be created as drop-ship directly
|
||||
(via the standard item lookup the form uses) without going through
|
||||
the Sales Order → Purchase Order mapping pipeline.
|
||||
"""
|
||||
from erpnext.stock.get_item_details import ItemDetailsCtx, get_item_details
|
||||
|
||||
item = make_item("_Test Drop Ship From Master", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||
|
||||
ctx = ItemDetailsCtx(
|
||||
{
|
||||
"item_code": item.item_code,
|
||||
"doctype": "Purchase Order",
|
||||
"company": "_Test Company",
|
||||
"supplier": "_Test Supplier",
|
||||
"transaction_date": nowdate(),
|
||||
"currency": "INR",
|
||||
"conversion_rate": 1.0,
|
||||
"buying_price_list": "Standard Buying",
|
||||
"price_list_currency": "INR",
|
||||
"plc_conversion_rate": 1.0,
|
||||
"qty": 1,
|
||||
}
|
||||
)
|
||||
|
||||
details = get_item_details(ctx, frappe.new_doc("Purchase Order"))
|
||||
self.assertEqual(details.get("delivered_by_supplier"), 1)
|
||||
|
||||
def test_drop_ship_po_allows_non_company_shipping_address_without_so(self):
|
||||
"""A PO with a drop-ship item should save with a non-company shipping
|
||||
address even when there is no linked Sales Order.
|
||||
Regression test for https://github.com/frappe/erpnext/issues/51629.
|
||||
"""
|
||||
from erpnext.crm.doctype.prospect.test_prospect import make_address
|
||||
|
||||
item = make_item("_Test Drop Ship Direct PO", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||
|
||||
customer_shipping = make_address(
|
||||
address_title="Drop Ship Direct PO", address_type="Shipping", address_line1="1"
|
||||
)
|
||||
customer_shipping.append("links", {"link_doctype": "Customer", "link_name": "_Test Customer"})
|
||||
customer_shipping.save()
|
||||
|
||||
po = create_purchase_order(item=item.item_code, qty=1, do_not_save=True)
|
||||
# In the UI, `get_item_details` propagates the master flag to the row when
|
||||
# the item is added; here we simulate that step explicitly.
|
||||
po.items[0].delivered_by_supplier = 1
|
||||
po.items[0].warehouse = ""
|
||||
po.shipping_address = customer_shipping.name
|
||||
po.save()
|
||||
|
||||
self.assertEqual(po.items[0].delivered_by_supplier, 1)
|
||||
self.assertFalse(po.items[0].warehouse)
|
||||
self.assertEqual(po.shipping_address, customer_shipping.name)
|
||||
|
||||
def test_drop_ship_flag_overridable_per_po_line(self):
|
||||
"""The drop-ship default from the Item master should be overridable
|
||||
on individual PO lines (e.g. ordering a normally drop-shipped item
|
||||
into the own warehouse for samples or stock).
|
||||
"""
|
||||
item = make_item("_Test Drop Ship Override", {"is_stock_item": 1, "delivered_by_supplier": 1})
|
||||
|
||||
po = create_purchase_order(item=item.item_code, qty=1, do_not_save=True)
|
||||
po.items[0].delivered_by_supplier = 0
|
||||
po.save()
|
||||
|
||||
self.assertEqual(po.items[0].delivered_by_supplier, 0)
|
||||
self.assertEqual(po.items[0].warehouse, "_Test Warehouse - _TC")
|
||||
|
||||
def test_remove_unlinked_item_from_mixed_po_does_not_crash(self):
|
||||
"""In a PO that mixes SO-linked and freely-added items, removing an
|
||||
item that has no `sales_order_item` via Update Items must not crash
|
||||
on the missing reference.
|
||||
"""
|
||||
po = create_purchase_order(do_not_submit=True)
|
||||
# Force the SO codepath without needing a real linked Sales Order:
|
||||
po.items[0].sales_order = "DUMMY-SO"
|
||||
|
||||
po.update_ordered_qty_in_so_for_removed_items([frappe._dict({"sales_order_item": None, "qty": 1})])
|
||||
|
||||
|
||||
def create_po_for_sc_testing():
|
||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||
|
||||
@@ -539,12 +539,10 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"depends_on": "delivered_by_supplier",
|
||||
"fieldname": "delivered_by_supplier",
|
||||
"fieldtype": "Check",
|
||||
"label": "To be Delivered to Customer",
|
||||
"print_hide": 1,
|
||||
"read_only": 1
|
||||
"print_hide": 1
|
||||
},
|
||||
{
|
||||
"depends_on": "eval:doc.against_blanket_order",
|
||||
@@ -941,7 +939,7 @@
|
||||
"index_web_pages_for_search": 1,
|
||||
"istable": 1,
|
||||
"links": [],
|
||||
"modified": "2026-05-14 12:16:16.192936",
|
||||
"modified": "2026-05-20 00:50:16.192936",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Buying",
|
||||
"name": "Purchase Order Item",
|
||||
|
||||
@@ -648,7 +648,7 @@
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Enable for drop shipping - supplier delivers directly to the customer without passing through your warehouse.",
|
||||
"description": "If checked, this item is treated as drop-shipped by default in Sales Orders, Sales Invoices and Purchase Orders. The flag can be overridden on each transaction line.",
|
||||
"fieldname": "delivered_by_supplier",
|
||||
"fieldtype": "Check",
|
||||
"label": "Delivered by Supplier (Drop Ship)",
|
||||
@@ -1077,7 +1077,7 @@
|
||||
"image_field": "image",
|
||||
"links": [],
|
||||
"make_attachments_public": 1,
|
||||
"modified": "2026-04-28 17:31:47.613279",
|
||||
"modified": "2026-05-14 02:09:33.455292",
|
||||
"modified_by": "Administrator",
|
||||
"module": "Stock",
|
||||
"name": "Item",
|
||||
|
||||
@@ -493,9 +493,7 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It
|
||||
"discount_percentage": 0.0,
|
||||
"discount_amount": flt(ctx.discount_amount) or 0.0,
|
||||
"update_stock": ctx.update_stock if ctx.doctype in ["Sales Invoice", "Purchase Invoice"] else 0,
|
||||
"delivered_by_supplier": item.delivered_by_supplier
|
||||
if ctx.doctype in ["Sales Order", "Sales Invoice"]
|
||||
else 0,
|
||||
"delivered_by_supplier": item.delivered_by_supplier,
|
||||
"is_fixed_asset": item.is_fixed_asset,
|
||||
"last_purchase_rate": item.last_purchase_rate if ctx.doctype in ["Purchase Order"] else 0,
|
||||
"transaction_date": ctx.transaction_date,
|
||||
|
||||
Reference in New Issue
Block a user