diff --git a/erpnext/selling/doctype/sales_order/sales_order.json b/erpnext/selling/doctype/sales_order/sales_order.json index 7e99a062439e..fe2f14e19a6d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.json +++ b/erpnext/selling/doctype/sales_order/sales_order.json @@ -130,6 +130,7 @@ "per_delivered", "column_break_81", "per_billed", + "per_picked", "billing_status", "sales_team_section_break", "sales_partner", @@ -1514,13 +1515,19 @@ "fieldtype": "Currency", "label": "Amount Eligible for Commission", "read_only": 1 + }, + { + "fieldname": "per_picked", + "fieldtype": "Percent", + "label": "% Picked", + "read_only": 1 } ], "icon": "fa fa-file-text", "idx": 105, "is_submittable": 1, "links": [], - "modified": "2021-10-05 12:16:40.775704", + "modified": "2022-03-15 21:38:31.437586", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order", @@ -1594,6 +1601,7 @@ "show_name_in_global_search": 1, "sort_field": "modified", "sort_order": "DESC", + "states": [], "timeline_field": "customer", "title_field": "customer_name", "track_changes": 1, diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 7e55499533b9..195e96486b31 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -23,6 +23,7 @@ "quantity_and_rate", "qty", "stock_uom", + "picked_qty", "col_break2", "uom", "conversion_factor", @@ -798,12 +799,17 @@ "fieldtype": "Check", "label": "Grant Commission", "read_only": 1 + }, + { + "fieldname": "picked_qty", + "fieldtype": "Float", + "label": "Picked Qty" } ], "idx": 1, "istable": 1, "links": [], - "modified": "2022-02-24 14:41:57.325799", + "modified": "2022-03-15 20:17:33.984799", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", diff --git a/erpnext/stock/doctype/pick_list/pick_list.js b/erpnext/stock/doctype/pick_list/pick_list.js index 730fd7a829cf..13b74b5eb16f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.js +++ b/erpnext/stock/doctype/pick_list/pick_list.js @@ -146,10 +146,6 @@ frappe.ui.form.on('Pick List', { customer: frm.doc.customer }; frm.get_items_btn = frm.add_custom_button(__('Get Items'), () => { - if (!frm.doc.customer) { - frappe.msgprint(__('Please select Customer first')); - return; - } erpnext.utils.map_current_doc({ method: 'erpnext.selling.doctype.sales_order.sales_order.create_pick_list', source_doctype: 'Sales Order', diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index b2eaecb58687..3a496866cf13 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -3,6 +3,8 @@ import json from collections import OrderedDict, defaultdict +from itertools import groupby +from operator import itemgetter import frappe from frappe import _ @@ -24,8 +26,21 @@ def validate(self): def before_save(self): self.set_item_locations() + # set percentage picked in SO + for location in self.get('locations'): + if location.sales_order and frappe.db.get_value("Sales Order",location.sales_order,"per_picked") == 100: + frappe.throw("Row " + str(location.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 +52,32 @@ def before_submit(self): 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 location in self.get('locations'): + if location.sales_order_item: + self.update_so(location.sales_order_item,0,location.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 = 0 + total_so_qty = 0 + for item in so_doc.get('items'): + total_picked_qty += flt(item.picked_qty) + total_so_qty += flt(item.stock_qty) + total_picked_qty=total_picked_qty + picked_qty + 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 +105,6 @@ def set_item_locations(self, save=False): 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 +377,102 @@ def get_available_item_locations_for_other_item(item_code, from_warehouses, requ 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 location in pick_list.locations: + if location.sales_order: + sales_orders.append([frappe.db.get_value("Sales Order",location.sales_order,'customer'),location.sales_order]) + # 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 location in pick_list.locations : + if not location.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 - # map rows without sales orders as well - if not 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) - 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 + 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) - source_doc, table_mapper = [sales_order_item, item_table_mapper] if sales_order_item \ - else [location, item_table_mapper_without_so] + 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 == 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 - dn_item = map_child_doc(source_doc, delivery_note, table_mapper) + source_doc, table_mapper = [sales_order_item, item_mapper] if sales_order_item \ + else [location, item_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 + dn_item = map_child_doc(source_doc, delivery_note, table_mapper) - update_delivery_note_item(source_doc, dn_item, delivery_note) + 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) 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 +637,4 @@ def update_common_item_properties(item, location): 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 f3b6b89784a2..f60104c09acd 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -17,7 +17,6 @@ class TestPickList(FrappeTestCase): - def test_pick_list_picks_warehouse_for_each_item(self): try: frappe.get_doc({ @@ -188,7 +187,6 @@ def test_pick_list_shows_batch_no_for_batched_item(self): }] }) pick_list.set_item_locations() - self.assertEqual(pick_list.locations[0].batch_no, oldest_batch_no) pr1.cancel() @@ -311,6 +309,7 @@ def test_pick_list_for_items_with_multiple_UOM(self): '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 @@ def test_pick_list_for_items_with_multiple_UOM(self): '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 @@ def _compare_dicts(a, b): 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