fix: Multiple Issues in Pick List to Delivery Note Flow (#48206)
* fix: get items from Pick List to DN even if not linked to Sales Order * refactor: consistently return dn; better place to convert json to doc * fix: update DN if already created instead of creating new DN when SO is not present in pick list location * fix: set correct warehouse,batch no and serial no in packed items and allow multiple customer in a pick list * fix: return 0 for minimum possible bundles if none exist * fix: test cases * test: add tests for product bundle items in pick list and handling pick lists with and without sales orders * fix: minor change to test case * refactor: simplify pick list creation by using create_pick_list function * fix: update delivery note creation logic and remove unused function * test: update pick list test for packed items * fix: add conditional check for sales_order before setting customer in delivery note * test: add test case for packed item multiple times in so --------- Co-authored-by: Smit Vora <smitvora203@gmail.com>
This commit is contained in:
@@ -89,6 +89,10 @@ def make_packing_list(doc):
|
||||
update_packed_item_basic_data(item_row, pi_row, bundle_item, item_data)
|
||||
update_packed_item_stock_data(item_row, pi_row, bundle_item, item_data, doc)
|
||||
update_packed_item_price_data(pi_row, item_data, doc)
|
||||
|
||||
if item_row.get("against_pick_list"):
|
||||
update_packed_item_with_pick_list_info(item_row, pi_row)
|
||||
|
||||
update_packed_item_from_cancelled_doc(item_row, bundle_item, pi_row, doc)
|
||||
|
||||
if set_price_from_children: # create/update bundle item wise price dict
|
||||
@@ -237,6 +241,28 @@ def update_packed_item_stock_data(main_item_row, pi_row, packing_item, item_data
|
||||
pi_row.use_serial_batch_fields = frappe.get_single_value("Stock Settings", "use_serial_batch_fields")
|
||||
|
||||
|
||||
def update_packed_item_with_pick_list_info(main_item_row, pi_row):
|
||||
pl_row = frappe.db.get_value(
|
||||
"Pick List Item",
|
||||
{
|
||||
"item_code": pi_row.item_code,
|
||||
"sales_order": main_item_row.get("against_sales_order"),
|
||||
"sales_order_item": main_item_row.get("so_detail"),
|
||||
"parent": main_item_row.against_pick_list,
|
||||
},
|
||||
["warehouse", "batch_no", "serial_no"],
|
||||
as_dict=True,
|
||||
order_by="qty desc",
|
||||
)
|
||||
|
||||
if not pl_row:
|
||||
return
|
||||
|
||||
pi_row.warehouse = pl_row.warehouse
|
||||
pi_row.batch_no = pl_row.batch_no
|
||||
pi_row.serial_no = pl_row.serial_no
|
||||
|
||||
|
||||
def update_packed_item_price_data(pi_row, item_data, doc):
|
||||
"Set price as per price list or from the Item master."
|
||||
if pi_row.rate:
|
||||
|
||||
@@ -161,7 +161,6 @@ class PickList(TransactionBase):
|
||||
"Sales Order": {
|
||||
"ref_dn_field": "sales_order",
|
||||
"compare_fields": [
|
||||
["customer", "="],
|
||||
["company", "="],
|
||||
],
|
||||
},
|
||||
@@ -776,16 +775,16 @@ class PickList(TransactionBase):
|
||||
def _compute_picked_qty_for_bundle(self, bundle_row, bundle_items) -> int:
|
||||
"""Compute how many full bundles can be created from picked items."""
|
||||
precision = frappe.get_precision("Stock Ledger Entry", "qty_after_transaction")
|
||||
possible_bundles = []
|
||||
possible_bundles = {}
|
||||
for item in self.locations:
|
||||
if item.sales_order_item != bundle_row:
|
||||
continue
|
||||
|
||||
if qty_in_bundle := bundle_items.get(item.item_code):
|
||||
possible_bundles.append(item.picked_qty / qty_in_bundle)
|
||||
else:
|
||||
possible_bundles.append(0)
|
||||
return int(flt(min(possible_bundles), precision or 6))
|
||||
possible_bundles.setdefault(item.product_bundle_item, 0)
|
||||
possible_bundles[item.product_bundle_item] += item.picked_qty / qty_in_bundle
|
||||
|
||||
return int(flt(min(possible_bundles.values()), precision or 6)) if possible_bundles else 0
|
||||
|
||||
def has_unreserved_stock(self):
|
||||
if self.purpose == "Delivery":
|
||||
@@ -1221,8 +1220,10 @@ def create_delivery_note(source_name, target_doc=None):
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_wo_so(pick_list):
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
def create_dn_wo_so(pick_list, delivery_note=None):
|
||||
if not delivery_note:
|
||||
delivery_note = frappe.new_doc("Delivery Note")
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
|
||||
item_table_mapper_without_so = {
|
||||
@@ -1234,6 +1235,8 @@ def create_dn_wo_so(pick_list):
|
||||
},
|
||||
}
|
||||
map_pl_locations(pick_list, item_table_mapper_without_so, delivery_note)
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
delivery_note.save()
|
||||
|
||||
return delivery_note
|
||||
|
||||
@@ -1244,22 +1247,30 @@ def create_dn_for_pick_lists(source_name, target_doc=None, kwargs=None):
|
||||
pick_list = frappe.get_doc("Pick List", source_name)
|
||||
validate_item_locations(pick_list)
|
||||
|
||||
if kwargs and (order := kwargs.get("sales_order")):
|
||||
sales_orders = {order}
|
||||
sales_order_arg = kwargs.get("sales_order") if kwargs else None
|
||||
customer_arg = kwargs.get("customer") if kwargs else None
|
||||
|
||||
if sales_order_arg:
|
||||
sales_orders = {sales_order_arg}
|
||||
else:
|
||||
sales_orders = {row.sales_order for row in pick_list.locations if row.sales_order}
|
||||
|
||||
if kwargs and (customer := kwargs.get("customer")):
|
||||
if customer_arg:
|
||||
sales_orders = frappe.get_all(
|
||||
"Sales Order",
|
||||
filters={"customer": customer, "name": ["in", list(sales_orders)]},
|
||||
filters={"customer": customer_arg, "name": ["in", list(sales_orders)]},
|
||||
pluck="name",
|
||||
)
|
||||
|
||||
if not sales_orders:
|
||||
return
|
||||
delivery_note = create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
|
||||
|
||||
return create_dn_from_so(pick_list, sales_orders, delivery_note=target_doc)
|
||||
if not sales_order_arg and not all(item.sales_order for item in pick_list.locations):
|
||||
if isinstance(delivery_note, str):
|
||||
delivery_note = frappe.get_doc(frappe.parse_json(delivery_note))
|
||||
|
||||
delivery_note = create_dn_wo_so(pick_list, delivery_note)
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_with_so(sales_dict, pick_list):
|
||||
@@ -1268,11 +1279,19 @@ def create_dn_with_so(sales_dict, pick_list):
|
||||
|
||||
for customer in sales_dict:
|
||||
delivery_note = create_dn_from_so(pick_list, sales_dict[customer], None)
|
||||
if delivery_note:
|
||||
delivery_note.flags.ignore_mandatory = True
|
||||
# updates packed_items on save
|
||||
# save as multiple customers are possible
|
||||
delivery_note.save()
|
||||
|
||||
return delivery_note
|
||||
|
||||
|
||||
def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
if not sales_order_list:
|
||||
return delivery_note
|
||||
|
||||
item_table_mapper = {
|
||||
"doctype": "Delivery Note Item",
|
||||
"field_map": {
|
||||
@@ -1284,6 +1303,7 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
}
|
||||
|
||||
kwargs = {"skip_item_mapping": True, "ignore_pricing_rule": pick_list.ignore_pricing_rule}
|
||||
|
||||
delivery_note = create_delivery_note_from_sales_order(
|
||||
next(iter(sales_order_list)), delivery_note, kwargs=kwargs
|
||||
)
|
||||
@@ -1291,11 +1311,8 @@ def create_dn_from_so(pick_list, sales_order_list, delivery_note=None):
|
||||
if not delivery_note:
|
||||
return
|
||||
|
||||
if delivery_note:
|
||||
for so in sales_order_list:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
|
||||
update_packed_item_details(pick_list, delivery_note)
|
||||
for so in sales_order_list:
|
||||
map_pl_locations(pick_list, item_table_mapper, delivery_note, so)
|
||||
|
||||
return delivery_note
|
||||
|
||||
@@ -1331,7 +1348,8 @@ def map_pl_locations(pick_list, item_mapper, delivery_note, sales_order=None):
|
||||
set_delivery_note_missing_values(delivery_note)
|
||||
|
||||
delivery_note.company = pick_list.company
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
if sales_order:
|
||||
delivery_note.customer = frappe.get_value("Sales Order", sales_order, "customer")
|
||||
|
||||
|
||||
def add_product_bundles_to_delivery_note(
|
||||
@@ -1353,34 +1371,10 @@ def add_product_bundles_to_delivery_note(
|
||||
dn_bundle_item.qty = pick_list._compute_picked_qty_for_bundle(
|
||||
so_row, product_bundle_qty_map[item_code]
|
||||
)
|
||||
dn_bundle_item.against_pick_list = pick_list.name
|
||||
update_delivery_note_item(sales_order_item, dn_bundle_item, delivery_note)
|
||||
|
||||
|
||||
def update_packed_item_details(pick_list: "PickList", delivery_note) -> None:
|
||||
"""Update stock details on packed items table of delivery note."""
|
||||
|
||||
def _find_so_row(packed_item):
|
||||
for item in delivery_note.items:
|
||||
if packed_item.parent_detail_docname == item.name:
|
||||
return item.so_detail
|
||||
|
||||
def _find_pick_list_location(bundle_row, packed_item):
|
||||
if not bundle_row:
|
||||
return
|
||||
for loc in pick_list.locations:
|
||||
if loc.product_bundle_item == bundle_row and loc.item_code == packed_item.item_code:
|
||||
return loc
|
||||
|
||||
for packed_item in delivery_note.packed_items:
|
||||
so_row = _find_so_row(packed_item)
|
||||
location = _find_pick_list_location(so_row, packed_item)
|
||||
if not location:
|
||||
continue
|
||||
packed_item.warehouse = location.warehouse
|
||||
packed_item.batch_no = location.batch_no
|
||||
packed_item.serial_no = location.serial_no
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def create_stock_entry(pick_list):
|
||||
pick_list = frappe.get_doc(json.loads(pick_list))
|
||||
@@ -1558,20 +1552,23 @@ def get_pick_list_query(doctype, txt, searchfield, start, page_len, filters):
|
||||
|
||||
PICK_LIST = frappe.qb.DocType("Pick List")
|
||||
PICK_LIST_ITEM = frappe.qb.DocType("Pick List Item")
|
||||
SALES_ORDER = frappe.qb.DocType("Sales Order")
|
||||
|
||||
query = (
|
||||
frappe.qb.from_(PICK_LIST)
|
||||
.join(PICK_LIST_ITEM)
|
||||
.on(PICK_LIST.name == PICK_LIST_ITEM.parent)
|
||||
.join(SALES_ORDER)
|
||||
.on(PICK_LIST_ITEM.sales_order == SALES_ORDER.name)
|
||||
.select(
|
||||
PICK_LIST.name,
|
||||
PICK_LIST.customer,
|
||||
SALES_ORDER.customer,
|
||||
Replace(GROUP_CONCAT(PICK_LIST_ITEM.sales_order).distinct(), ",", "<br>").as_("sales_order"),
|
||||
)
|
||||
.where(PICK_LIST.docstatus == 1)
|
||||
.where(PICK_LIST.status.isin(["Open", "Partly Delivered"]))
|
||||
.where(PICK_LIST.company == filters.get("company"))
|
||||
.where(PICK_LIST.customer == filters.get("customer"))
|
||||
.where(SALES_ORDER.customer == filters.get("customer"))
|
||||
.groupby(PICK_LIST.name)
|
||||
)
|
||||
|
||||
|
||||
@@ -5,11 +5,12 @@ import frappe
|
||||
from frappe import _dict
|
||||
from frappe.tests import IntegrationTestCase
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.test_product_bundle import make_product_bundle
|
||||
from erpnext.selling.doctype.sales_order.sales_order import create_pick_list
|
||||
from erpnext.selling.doctype.sales_order.test_sales_order import make_sales_order
|
||||
from erpnext.stock.doctype.item.test_item import create_item, make_item
|
||||
from erpnext.stock.doctype.packed_item.test_packed_item import create_product_bundle
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
|
||||
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note, create_dn_for_pick_lists
|
||||
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
|
||||
from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import (
|
||||
get_batch_from_bundle,
|
||||
@@ -401,7 +402,7 @@ class TestPickList(IntegrationTestCase):
|
||||
item_code = make_item(
|
||||
uoms=[
|
||||
{"uom": "Nos", "conversion_factor": 1},
|
||||
{"uom": "Box", "conversion_factor": 5},
|
||||
{"uom": "Hand", "conversion_factor": 5},
|
||||
{"uom": "Unit", "conversion_factor": 0.5},
|
||||
]
|
||||
).name
|
||||
@@ -417,7 +418,7 @@ class TestPickList(IntegrationTestCase):
|
||||
{
|
||||
"item_code": item_code,
|
||||
"qty": 1,
|
||||
"uom": "Box",
|
||||
"uom": "Hand",
|
||||
"delivery_date": frappe.utils.today(),
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
},
|
||||
@@ -546,7 +547,7 @@ class TestPickList(IntegrationTestCase):
|
||||
sales_order_2 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Sales Order",
|
||||
"customer": "_Test Customer",
|
||||
"customer": "_Test Customer 1",
|
||||
"company": "_Test Company",
|
||||
"items": [
|
||||
{
|
||||
@@ -565,11 +566,10 @@ class TestPickList(IntegrationTestCase):
|
||||
"company": "_Test Company",
|
||||
"items_based_on": "Sales Order",
|
||||
"purpose": "Delivery",
|
||||
"picker": "P001",
|
||||
"customer": "_Test Customer",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": "_Test Item ",
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
@@ -599,7 +599,7 @@ class TestPickList(IntegrationTestCase):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item")
|
||||
self.assertEqual(dn_item.against_sales_order, sales_order_1.name)
|
||||
self.assertEqual(dn_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[0].name)
|
||||
|
||||
for dn in frappe.get_all(
|
||||
"Delivery Note",
|
||||
@@ -610,17 +610,16 @@ class TestPickList(IntegrationTestCase):
|
||||
self.assertEqual(dn_item.item_code, "_Test Item 2")
|
||||
self.assertEqual(dn_item.against_sales_order, sales_order_2.name)
|
||||
self.assertEqual(dn_item.against_pick_list, pick_list.name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[dn_item.idx - 1].name)
|
||||
self.assertEqual(dn_item.pick_list_item, pick_list.locations[1].name)
|
||||
# test DN creation without so
|
||||
pick_list_1 = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pick List",
|
||||
"company": "_Test Company",
|
||||
"purpose": "Delivery",
|
||||
"picker": "P001",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": "_Test Item ",
|
||||
"item_code": "_Test Item",
|
||||
"qty": 1,
|
||||
"stock_qty": 1,
|
||||
"conversion_factor": 1,
|
||||
@@ -1379,3 +1378,110 @@ class TestPickList(IntegrationTestCase):
|
||||
sales_order.reload()
|
||||
sales_order.cancel()
|
||||
stock_entry.cancel()
|
||||
|
||||
def test_packed_item_in_pick_list(self):
|
||||
warehouse_1 = "RJ Warehouse - _TC"
|
||||
warehouse_2 = "_Test Warehouse 2 - _TC"
|
||||
item_1 = make_item(properties={"is_stock_item": 0}).name
|
||||
item_2 = make_item().name
|
||||
item_3 = make_item().name
|
||||
|
||||
make_product_bundle(item_1, items=[item_2, item_3])
|
||||
|
||||
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=10, basic_rate=100)
|
||||
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=4, basic_rate=100)
|
||||
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=6, basic_rate=100)
|
||||
|
||||
sales_order = make_sales_order(item_code=item_1, qty=10, rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
self.assertEqual(len(pick_list.locations), 3)
|
||||
delivery_note = create_delivery_note(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].qty, 10)
|
||||
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_2)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry_1.cancel()
|
||||
stock_entry_2.cancel()
|
||||
stock_entry_3.cancel()
|
||||
|
||||
def test_packed_item_multiple_times_in_so(self):
|
||||
frappe.db.delete("Item Price")
|
||||
warehouse_1 = "RJ Warehouse - _TC"
|
||||
warehouse_2 = "_Test Warehouse 2 - _TC"
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item_1 = make_item(properties={"is_stock_item": 0}).name
|
||||
item_2 = make_item().name
|
||||
item_3 = make_item().name
|
||||
|
||||
make_product_bundle(item_1, items=[item_2, item_3])
|
||||
|
||||
stock_entry_1 = make_stock_entry(item=item_2, to_warehouse=warehouse_1, qty=20, basic_rate=100)
|
||||
stock_entry_2 = make_stock_entry(item=item_3, to_warehouse=warehouse_1, qty=8, basic_rate=100)
|
||||
stock_entry_3 = make_stock_entry(item=item_3, to_warehouse=warehouse_2, qty=12, basic_rate=100)
|
||||
|
||||
sales_order = make_sales_order(
|
||||
item_list=[
|
||||
{"item_code": item_1, "qty": 8, "rate": 100, "warehouse": warehouse},
|
||||
{"item_code": item_1, "qty": 12, "rate": 100, "warehouse": warehouse},
|
||||
]
|
||||
)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.submit()
|
||||
self.assertEqual(len(pick_list.locations), 4)
|
||||
delivery_note = create_delivery_note(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].qty, 8)
|
||||
self.assertEqual(delivery_note.items[1].qty, 12)
|
||||
|
||||
self.assertEqual(delivery_note.packed_items[0].qty, 8)
|
||||
self.assertEqual(delivery_note.packed_items[2].qty, 12)
|
||||
|
||||
self.assertEqual(delivery_note.packed_items[0].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[1].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[2].warehouse, warehouse_1)
|
||||
self.assertEqual(delivery_note.packed_items[3].warehouse, warehouse_2)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry_1.cancel()
|
||||
stock_entry_2.cancel()
|
||||
stock_entry_3.cancel()
|
||||
|
||||
def test_pick_list_with_and_without_so(self):
|
||||
warehouse = "_Test Warehouse - _TC"
|
||||
item = make_item().name
|
||||
|
||||
sales_order = make_sales_order(item_code=item, qty=20, rate=100)
|
||||
stock_entry = make_stock_entry(item=item, to_warehouse=warehouse, qty=500, basic_rate=100)
|
||||
|
||||
pick_list = create_pick_list(sales_order.name)
|
||||
pick_list.append(
|
||||
"locations",
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 10,
|
||||
"stock_qty": 10,
|
||||
"warehouse": warehouse,
|
||||
"picked_qty": 0,
|
||||
},
|
||||
)
|
||||
pick_list.submit()
|
||||
|
||||
delivery_note = create_dn_for_pick_lists(pick_list.name)
|
||||
|
||||
self.assertEqual(delivery_note.items[0].against_pick_list, pick_list.name)
|
||||
self.assertEqual(delivery_note.items[0].against_sales_order, sales_order.name)
|
||||
self.assertEqual(delivery_note.items[0].qty, 20)
|
||||
|
||||
self.assertEqual(delivery_note.items[1].against_pick_list, pick_list.name)
|
||||
self.assertEqual(delivery_note.items[1].qty, 10)
|
||||
|
||||
pick_list.cancel()
|
||||
sales_order.cancel()
|
||||
stock_entry.cancel()
|
||||
|
||||
Reference in New Issue
Block a user