feat: Create single PL/DN from several SO. New PR from latest develop to avoid rebase
diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py
index b2eaecb..3ec0324 100644
--- a/erpnext/stock/doctype/pick_list/pick_list.py
+++ b/erpnext/stock/doctype/pick_list/pick_list.py
@@ -24,8 +24,21 @@
def before_save(self):
self.set_item_locations()
+ # set percentage picked in SO
+ for i in self.get('locations'):
+ if i.sales_order and frappe.db.get_value("Sales Order",i.sales_order,"per_picked") == 100:
+ frappe.throw("Row " + str(i.idx) + " has been picked already!")
+
def before_submit(self):
for item in self.locations:
+ # if the user has not entered any picked qty, set it to stock_qty, before submit
+ if item.picked_qty == 0:
+ item.picked_qty = item.stock_qty
+
+ if item.sales_order_item:
+ # update the picked_qty in SO Item
+ self.update_so(item.sales_order_item,item.picked_qty,item.item_code)
+
if not frappe.get_cached_value('Item', item.item_code, 'has_serial_no'):
continue
if not item.serial_no:
@@ -37,6 +50,28 @@
frappe.throw(_('For item {0} at row {1}, count of serial numbers does not match with the picked quantity')
.format(frappe.bold(item.item_code), frappe.bold(item.idx)), title=_("Quantity Mismatch"))
+ def before_cancel(self):
+ #update picked_qty in SO Item on cancel of PL
+ for i in self.get('locations'):
+ if i.sales_order_item:
+ self.update_so(i.sales_order_item,0,i.item_code)
+
+ def update_so(self,so_item,picked_qty,item_code):
+ so_doc = frappe.get_doc("Sales Order",frappe.db.get_value("Sales Order Item",so_item,"parent"))
+ already_picked,actual_qty = frappe.db.get_value("Sales Order Item",so_item,["picked_qty","qty"])
+
+ if self.docstatus == 1:
+ if (((already_picked + picked_qty)/ actual_qty)*100) > (100 + flt(frappe.db.get_single_value('Stock Settings', 'over_delivery_receipt_allowance'))):
+ frappe.throw('You are picking more than required quantity for ' + item_code + '. Check if there is any other pick list created for '+so_doc.name)
+
+ frappe.db.set_value("Sales Order Item",so_item,"picked_qty",already_picked+picked_qty)
+
+ total_picked_qty = sum(flt(d.picked_qty) for d in so_doc.get('items')) + picked_qty
+ total_so_qty = sum(flt(d.stock_qty) for d in so_doc.get('items'))
+ per_picked = total_picked_qty/total_so_qty * 100
+
+ so_doc.db_set("per_picked", flt(per_picked) ,update_modified=False)
+
@frappe.whitelist()
def set_item_locations(self, save=False):
self.validate_for_qty()
@@ -64,10 +99,6 @@
item_doc.name = None
for row in locations:
- row.update({
- 'picked_qty': row.stock_qty
- })
-
location = item_doc.as_dict()
location.update(row)
self.append('locations', location)
@@ -340,63 +371,106 @@
def create_delivery_note(source_name, target_doc=None):
pick_list = frappe.get_doc('Pick List', source_name)
validate_item_locations(pick_list)
-
- sales_orders = [d.sales_order for d in pick_list.locations if d.sales_order]
- sales_orders = set(sales_orders)
-
+ sales_dict = dict()
+ sales_orders = []
delivery_note = None
- for sales_order in sales_orders:
- delivery_note = create_delivery_note_from_sales_order(sales_order,
- delivery_note, skip_item_mapping=True)
+ for d in pick_list.locations:
+ if d.sales_order:
+ sales_orders.append([frappe.db.get_value("Sales Order",d.sales_order,'customer'),d.sales_order])
+
+ from itertools import groupby
+ from operator import itemgetter
- # map rows without sales orders as well
- if not delivery_note:
+ # Group sales orders by customer
+ for key,keydata in groupby(sales_orders,key=itemgetter(0)):
+ sales_dict[key] = set([d[1] for d in keydata])
+
+ if sales_dict:
+ delivery_note = create_dn_with_so(sales_dict,pick_list)
+
+ is_item_wo_so = 0
+ for n in pick_list.locations :
+ if not n.sales_order:
+ is_item_wo_so = 1
+ break;
+ if is_item_wo_so == 1:
+ # Create a DN for items without sales orders as well
+ delivery_note = create_dn_wo_so(pick_list)
+
+ frappe.msgprint(_('Delivery Note(s) created for the Pick List'))
+ return delivery_note
+
+def create_dn_wo_so(pick_list):
delivery_note = frappe.new_doc("Delivery Note")
- item_table_mapper = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'so_detail',
- 'parent': 'against_sales_order',
- },
- 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
- }
-
- item_table_mapper_without_so = {
- 'doctype': 'Delivery Note Item',
- 'field_map': {
- 'rate': 'rate',
- 'name': 'name',
- 'parent': '',
+ item_table_mapper_without_so = {
+ 'doctype': 'Delivery Note Item',
+ 'field_map': {
+ 'rate': 'rate',
+ 'name': 'name',
+ 'parent': '',
+ }
}
- }
+ map_pl_locations(pick_list,item_table_mapper_without_so,delivery_note)
+ delivery_note.insert(ignore_mandatory = True)
+
+ return delivery_note
+
+
+def create_dn_with_so(sales_dict,pick_list):
+ delivery_note = None
+
+ for customer in sales_dict:
+ for so in sales_dict[customer]:
+ delivery_note = None
+ delivery_note = create_delivery_note_from_sales_order(so,
+ delivery_note, skip_item_mapping=True)
+
+ item_table_mapper = {
+ 'doctype': 'Delivery Note Item',
+ 'field_map': {
+ 'rate': 'rate',
+ 'name': 'so_detail',
+ 'parent': 'against_sales_order',
+ },
+ 'condition': lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
+ }
+ break;
+ if delivery_note:
+ # map all items of all sales orders of that customer
+ for so in sales_dict[customer]:
+ map_pl_locations(pick_list,item_table_mapper,delivery_note,so)
+ delivery_note.insert(ignore_mandatory = True)
+
+ return delivery_note
+
+def map_pl_locations(pick_list,item_mapper,delivery_note,sales_order = None):
for location in pick_list.locations:
- if location.sales_order_item:
- sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
- else:
- sales_order_item = None
+ if location.sales_order == sales_order:
+ if location.sales_order_item:
+ sales_order_item = frappe.get_cached_doc('Sales Order Item', {'name':location.sales_order_item})
+ else:
+ sales_order_item = None
- source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \
- else [location, item_table_mapper_without_so]
+ source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \
+ else [location, item_mapper]
- dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
+ dn_item = map_child_doc(source_doc, delivery_note, table_mapper)
- if dn_item:
- dn_item.warehouse = location.warehouse
- dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
- dn_item.batch_no = location.batch_no
- dn_item.serial_no = location.serial_no
+ if dn_item:
+ dn_item.warehouse = location.warehouse
+ dn_item.qty = flt(location.picked_qty) / (flt(location.conversion_factor) or 1)
+ dn_item.batch_no = location.batch_no
+ dn_item.serial_no = location.serial_no
- update_delivery_note_item(source_doc, dn_item, delivery_note)
-
+ update_delivery_note_item(source_doc, dn_item, delivery_note)
set_delivery_note_missing_values(delivery_note)
delivery_note.pick_list = pick_list.name
- delivery_note.customer = pick_list.customer if pick_list.customer else None
+ delivery_note.company = pick_list.company
+ delivery_note.customer = frappe.get_value("Sales Order",sales_order,"customer")
- return delivery_note
@frappe.whitelist()
def create_stock_entry(pick_list):
@@ -561,4 +635,4 @@
item.material_request = location.material_request
item.serial_no = location.serial_no
item.batch_no = location.batch_no
- item.material_request_item = location.material_request_item
+ item.material_request_item = location.material_request_item
\ No newline at end of file
diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py
index f3b6b89..82d8fe9 100644
--- a/erpnext/stock/doctype/pick_list/test_pick_list.py
+++ b/erpnext/stock/doctype/pick_list/test_pick_list.py
@@ -7,7 +7,6 @@
test_dependencies = ['Item', 'Sales Invoice', 'Stock Entry', 'Batch']
from frappe.tests.utils import FrappeTestCase
-
from erpnext.stock.doctype.item.test_item import create_item
from erpnext.stock.doctype.pick_list.pick_list import create_delivery_note
from erpnext.stock.doctype.purchase_receipt.test_purchase_receipt import make_purchase_receipt
@@ -188,7 +187,6 @@
}]
})
pick_list.set_item_locations()
-
self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no)
pr1.cancel()
@@ -311,6 +309,7 @@
'item_code': '_Test Item',
'qty': 1,
'conversion_factor': 5,
+ 'stock_qty':5,
'delivery_date': frappe.utils.today()
}, {
'item_code': '_Test Item',
@@ -329,9 +328,9 @@
'purpose': 'Delivery',
'locations': [{
'item_code': '_Test Item',
- 'qty': 1,
- 'stock_qty': 5,
- 'conversion_factor': 5,
+ 'qty': 2,
+ 'stock_qty': 1,
+ 'conversion_factor': 0.5,
'sales_order': sales_order.name,
'sales_order_item': sales_order.items[0].name ,
}, {
@@ -389,6 +388,95 @@
for expected_item, created_item in zip(expected_items, pl.locations):
_compare_dicts(expected_item, created_item)
+ def test_multiple_dn_creation(self):
+ sales_order_1 = frappe.get_doc({
+ 'doctype': 'Sales Order',
+ 'customer': '_Test Customer',
+ 'company': '_Test Company',
+ 'items': [{
+ 'item_code': '_Test Item',
+ 'qty': 1,
+ 'conversion_factor': 1,
+ 'delivery_date': frappe.utils.today()
+ }],
+ }).insert()
+ sales_order_1.submit()
+ sales_order_2 = frappe.get_doc({
+ 'doctype': 'Sales Order',
+ 'customer': '_Test Customer 1',
+ 'company': '_Test Company',
+ 'items': [{
+ 'item_code': '_Test Item 2',
+ 'qty': 1,
+ 'conversion_factor': 1,
+ 'delivery_date': frappe.utils.today()
+ },
+ ],
+ }).insert()
+ sales_order_2.submit()
+ pick_list = frappe.get_doc({
+ 'doctype': 'Pick List',
+ 'company': '_Test Company',
+ 'items_based_on': 'Sales Order',
+ 'purpose': 'Delivery',
+ 'picker':'P001',
+ 'locations': [{
+ 'item_code': '_Test Item ',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ 'sales_order': sales_order_1.name,
+ 'sales_order_item': sales_order_1.items[0].name ,
+ }, {
+ 'item_code': '_Test Item 2',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ 'sales_order': sales_order_2.name,
+ 'sales_order_item': sales_order_2.items[0].name ,
+ }
+ ]
+ })
+ pick_list.set_item_locations()
+ pick_list.submit()
+ create_delivery_note(pick_list.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer"},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, '_Test Item')
+ self.assertEqual(dn_item.against_sales_order,sales_order_1.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list.name,"customer":"_Test Customer 1"},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ self.assertEqual(dn_item.item_code, '_Test Item 2')
+ self.assertEqual(dn_item.against_sales_order,sales_order_2.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 ',
+ 'qty': 1,
+ 'stock_qty': 1,
+ 'conversion_factor': 1,
+ }, {
+ 'item_code': '_Test Item 2',
+ 'qty': 2,
+ 'stock_qty': 2,
+ 'conversion_factor': 1,
+ }
+ ]
+ })
+ pick_list_1.set_item_locations()
+ pick_list_1.submit()
+ create_delivery_note(pick_list_1.name)
+ for dn in frappe.get_all("Delivery Note",filters={"pick_list":pick_list_1.name},fields={"name"}):
+ for dn_item in frappe.get_doc("Delivery Note",dn.name).get("items"):
+ if dn_item.item_code == '_Test Item':
+ self.assertEqual(dn_item.qty,1)
+ if dn_item.item_code == '_Test Item 2':
+ self.assertEqual(dn_item.qty,2)
+
# def test_pick_list_skips_items_in_expired_batch(self):
# pass