mirror of
https://github.com/frappe/erpnext.git
synced 2026-05-21 22:19:18 +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)) {
|
if (is_drop_ship && !["Completed", "Delivered"].includes(doc.status)) {
|
||||||
this.frm.add_custom_button(
|
this.frm.add_custom_button(
|
||||||
__("Deliver (Dropship)"),
|
__("Deliver (Dropship)"),
|
||||||
this.delivered_by_supplier.bind(this),
|
this.update_dropship_delivered_qty.bind(this),
|
||||||
__("Status")
|
__("Status")
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -698,7 +698,7 @@ erpnext.buying.PurchaseOrderController = class PurchaseOrderController extends (
|
|||||||
this.frm.cscript.update_status("Close", "Closed");
|
this.frm.cscript.update_status("Close", "Closed");
|
||||||
}
|
}
|
||||||
|
|
||||||
delivered_by_supplier() {
|
update_dropship_delivered_qty() {
|
||||||
const data = this.frm.doc.items
|
const data = this.frm.doc.items
|
||||||
.filter((item) => item.delivered_by_supplier == 1)
|
.filter((item) => item.delivered_by_supplier == 1)
|
||||||
.map((item) => {
|
.map((item) => {
|
||||||
|
|||||||
@@ -664,12 +664,21 @@ class PurchaseOrder(BuyingController):
|
|||||||
if not self.is_against_so():
|
if not self.is_against_so():
|
||||||
return
|
return
|
||||||
for item in removed_items:
|
for item in removed_items:
|
||||||
|
sales_order_item = item.get("sales_order_item")
|
||||||
|
if not sales_order_item:
|
||||||
|
continue
|
||||||
|
|
||||||
prev_ordered_qty = flt(
|
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(
|
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):
|
def auto_create_subcontracting_order(self):
|
||||||
|
|||||||
@@ -1436,6 +1436,88 @@ class TestPurchaseOrder(ERPNextTestSuite):
|
|||||||
pi2 = make_pi_from_po(po.name)
|
pi2 = make_pi_from_po(po.name)
|
||||||
self.assertEqual(len(pi2.items), 2)
|
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():
|
def create_po_for_sc_testing():
|
||||||
from erpnext.controllers.tests.test_subcontracting_controller import (
|
from erpnext.controllers.tests.test_subcontracting_controller import (
|
||||||
|
|||||||
@@ -539,12 +539,10 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"default": "0",
|
||||||
"depends_on": "delivered_by_supplier",
|
|
||||||
"fieldname": "delivered_by_supplier",
|
"fieldname": "delivered_by_supplier",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "To be Delivered to Customer",
|
"label": "To be Delivered to Customer",
|
||||||
"print_hide": 1,
|
"print_hide": 1
|
||||||
"read_only": 1
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.against_blanket_order",
|
"depends_on": "eval:doc.against_blanket_order",
|
||||||
@@ -941,7 +939,7 @@
|
|||||||
"index_web_pages_for_search": 1,
|
"index_web_pages_for_search": 1,
|
||||||
"istable": 1,
|
"istable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2026-05-14 12:16:16.192936",
|
"modified": "2026-05-20 00:50:16.192936",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Buying",
|
"module": "Buying",
|
||||||
"name": "Purchase Order Item",
|
"name": "Purchase Order Item",
|
||||||
|
|||||||
@@ -648,7 +648,7 @@
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
"default": "0",
|
"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",
|
"fieldname": "delivered_by_supplier",
|
||||||
"fieldtype": "Check",
|
"fieldtype": "Check",
|
||||||
"label": "Delivered by Supplier (Drop Ship)",
|
"label": "Delivered by Supplier (Drop Ship)",
|
||||||
@@ -1077,7 +1077,7 @@
|
|||||||
"image_field": "image",
|
"image_field": "image",
|
||||||
"links": [],
|
"links": [],
|
||||||
"make_attachments_public": 1,
|
"make_attachments_public": 1,
|
||||||
"modified": "2026-04-28 17:31:47.613279",
|
"modified": "2026-05-14 02:09:33.455292",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Item",
|
"name": "Item",
|
||||||
|
|||||||
@@ -493,9 +493,7 @@ def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> It
|
|||||||
"discount_percentage": 0.0,
|
"discount_percentage": 0.0,
|
||||||
"discount_amount": flt(ctx.discount_amount) or 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,
|
"update_stock": ctx.update_stock if ctx.doctype in ["Sales Invoice", "Purchase Invoice"] else 0,
|
||||||
"delivered_by_supplier": item.delivered_by_supplier
|
"delivered_by_supplier": item.delivered_by_supplier,
|
||||||
if ctx.doctype in ["Sales Order", "Sales Invoice"]
|
|
||||||
else 0,
|
|
||||||
"is_fixed_asset": item.is_fixed_asset,
|
"is_fixed_asset": item.is_fixed_asset,
|
||||||
"last_purchase_rate": item.last_purchase_rate if ctx.doctype in ["Purchase Order"] else 0,
|
"last_purchase_rate": item.last_purchase_rate if ctx.doctype in ["Purchase Order"] else 0,
|
||||||
"transaction_date": ctx.transaction_date,
|
"transaction_date": ctx.transaction_date,
|
||||||
|
|||||||
Reference in New Issue
Block a user