From a5d09270cbeb118afcea4d362c2d8ac91b1d955b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 21 Jan 2023 11:28:23 +0530 Subject: [PATCH 01/14] refactor: rewrite `get_picked_items_qty` query in `QB` (cherry picked from commit 29bf787313092e7a261479af36110613c7d0357b) --- erpnext/stock/doctype/pick_list/pick_list.py | 39 +++++++++----------- 1 file changed, 17 insertions(+), 22 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 808f19e2740..caafcdda331 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -347,28 +347,23 @@ class PickList(Document): def get_picked_items_qty(items) -> List[Dict]: - return frappe.db.sql( - f""" - SELECT - sales_order_item, - item_code, - sales_order, - SUM(stock_qty) AS stock_qty, - SUM(picked_qty) AS picked_qty - FROM - `tabPick List Item` - WHERE - sales_order_item IN ( - {", ".join(frappe.db.escape(d) for d in items)} - ) - AND docstatus = 1 - GROUP BY - sales_order_item, - sales_order - FOR UPDATE - """, - as_dict=1, - ) + pi_item = frappe.qb.DocType("Pick List Item") + return ( + frappe.qb.from_(pi_item) + .select( + pi_item.sales_order_item, + pi_item.item_code, + pi_item.sales_order, + Sum(pi_item.stock_qty).as_("stock_qty"), + Sum(pi_item.picked_qty).as_("picked_qty"), + ) + .where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items))) + .groupby( + pi_item.sales_order_item, + pi_item.sales_order, + ) + .for_update() + ).run(as_dict=True) def validate_item_locations(pick_list): From 167a5596cbfb485ea0567994765eae2715325c4f Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 21 Jan 2023 12:15:45 +0530 Subject: [PATCH 02/14] refactor: rewrite `get_available_item_locations_for_other_item` query in `QB` (cherry picked from commit 58dd40a2d7915c6b4ae99b72e8d35915394b57f6) --- erpnext/stock/doctype/pick_list/pick_list.py | 25 ++++++++++---------- 1 file changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index caafcdda331..4f111a2aa95 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -555,23 +555,22 @@ def get_available_item_locations_for_serial_and_batched_item( def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): - # gets all items available in different warehouses - warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")] - - filters = frappe._dict( - {"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]} + bin = frappe.qb.DocType("Bin") + query = ( + frappe.qb.from_(bin) + .select(bin.warehouse, bin.actual_qty.as_("qty")) + .where((bin.item_code == item_code) & (bin.actual_qty > 0)) + .orderby(bin.creation) + .limit(cint(required_qty)) ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(bin.warehouse.isin(from_warehouses)) + else: + wh = frappe.qb.DocType("Warehouse") + query = query.from_(wh).where((bin.warehouse == wh.name) & (wh.company == company)) - item_locations = frappe.get_all( - "Bin", - fields=["warehouse", "actual_qty as qty"], - filters=filters, - limit=required_qty, - order_by="creation", - ) + item_locations = query.run(as_dict=True) return item_locations From d9d986a5123aaca2c1afeffd45c38d0739e43d17 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 21 Jan 2023 12:45:39 +0530 Subject: [PATCH 03/14] refactor: rewrite `get_available_item_locations_for_serialized_item` query in `QB` (cherry picked from commit 5b76e8b19370eb76471f6b59873900d9f2977959) --- erpnext/stock/doctype/pick_list/pick_list.py | 24 +++++++++++--------- 1 file changed, 13 insertions(+), 11 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 4f111a2aa95..f053474b28f 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -11,7 +11,7 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case -from frappe.query_builder.functions import IfNull, Locate, Sum +from frappe.query_builder.functions import Coalesce, IfNull, Locate, Sum from frappe.utils import cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of @@ -470,19 +470,21 @@ def get_available_item_locations( def get_available_item_locations_for_serialized_item( item_code, from_warehouses, required_qty, company ): - filters = frappe._dict({"item_code": item_code, "company": company, "warehouse": ["!=", ""]}) + sn = frappe.qb.DocType("Serial No") + query = ( + frappe.qb.from_(sn) + .select(sn.name, sn.warehouse) + .where((sn.item_code == item_code) & (sn.company == company)) + .orderby(sn.purchase_date) + .limit(cint(required_qty)) + ) if from_warehouses: - filters.warehouse = ["in", from_warehouses] + query = query.where(sn.warehouse.isin(from_warehouses)) + else: + query = query.where(Coalesce(sn.warehouse, "") != "") - serial_nos = frappe.get_all( - "Serial No", - fields=["name", "warehouse"], - filters=filters, - limit=required_qty, - order_by="purchase_date", - as_list=1, - ) + serial_nos = query.run(as_list=True) warehouse_serial_nos_map = frappe._dict() for serial_no, warehouse in serial_nos: From 6166a6e64f143d6b4f41b48a91b08d85b11abaea Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 21 Jan 2023 14:05:51 +0530 Subject: [PATCH 04/14] refactor: rewrite `get_available_item_locations_for_serial_and_batched_item` query in `QB` (cherry picked from commit 57c32166831912603b283208ff08c9fdffcb0d95) --- erpnext/stock/doctype/pick_list/pick_list.py | 33 +++++++++++--------- 1 file changed, 18 insertions(+), 15 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index f053474b28f..4e570571005 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -534,24 +534,27 @@ def get_available_item_locations_for_serial_and_batched_item( item_code, from_warehouses, required_qty, company ) - filters = frappe._dict( - {"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""} - ) + if locations: + sn = frappe.qb.DocType("Serial No") + conditions = (sn.item_code == item_code) & (sn.company == company) - # Get Serial Nos by FIFO for Batch No - for location in locations: - filters.batch_no = location.batch_no - filters.warehouse = location.warehouse - location.qty = ( - required_qty if location.qty > required_qty else location.qty - ) # if extra qty in batch + for location in locations: + location.qty = ( + required_qty if location.qty > required_qty else location.qty + ) # if extra qty in batch - serial_nos = frappe.get_list( - "Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date" - ) + serial_nos = ( + frappe.qb.from_(sn) + .select(sn.name) + .where( + (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) + ) + .orderby(sn.purchase_date) + .limit(cint(location.qty)) + ).run(as_dict=True) - serial_nos = [sn.name for sn in serial_nos] - location.serial_no = serial_nos + serial_nos = [sn.name for sn in serial_nos] + location.serial_no = serial_nos return locations From 140be10060a27e299b1bc5738f6a14d8ffbbb049 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 21 Jan 2023 20:58:47 +0530 Subject: [PATCH 05/14] chore: add method `get_picked_items_details()` (cherry picked from commit 9ae3a54ce96e1bce5d32fadb25fbc3da399f838a) --- erpnext/stock/doctype/pick_list/pick_list.py | 44 ++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 4e570571005..e572540a04c 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -162,6 +162,7 @@ class PickList(Document): def set_item_locations(self, save=False): self.validate_for_qty() items = self.aggregate_item_qty() + picked_items_details = self.get_picked_items_details(items) self.item_location_map = frappe._dict() from_warehouses = None @@ -309,6 +310,49 @@ class PickList(Document): already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)), ) + def get_picked_items_details(self, items): + picked_items = frappe._dict() + + pi_item = frappe.qb.DocType("Pick List Item") + query = ( + frappe.qb.from_(pi_item) + .select( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + pi_item.serial_no, + Sum(pi_item.picked_qty).as_("picked_qty"), + ) + .where( + (pi_item.item_code.isin([x.item_code for x in items])) + & (pi_item.docstatus != 2) + & (pi_item.picked_qty > 0) + ) + .groupby( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + ) + ) + + if self.name: + query = query.where(pi_item.parent != self.name) + + items_data = query.run(as_dict=True) + + for item_data in items_data: + key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse + serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None + data = {"picked_qty": item_data.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if item_data.item_code not in picked_items: + picked_items[item_data.item_code] = {key: data} + else: + picked_items[item_data.item_code][key] = data + + return picked_items + def _get_product_bundles(self) -> Dict[str, str]: # Dict[so_item_row: item_code] product_bundles = {} From 466a791f68643cae1d2f1323039e786aace9be77 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Mon, 23 Jan 2023 13:51:07 +0530 Subject: [PATCH 06/14] fix: consider existing pick list (cherry picked from commit b642718f08472294b6cc1cbdd5d344dcc0f5059d) --- erpnext/stock/doctype/pick_list/pick_list.py | 76 ++++++++++++++++---- 1 file changed, 62 insertions(+), 14 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index e572540a04c..7b75bb0ffd3 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -181,7 +181,11 @@ class PickList(Document): self.item_location_map.setdefault( item_code, get_available_item_locations( - item_code, from_warehouses, self.item_count_map.get(item_code), self.company + item_code, + from_warehouses, + self.item_count_map.get(item_code), + self.company, + picked_item_details=picked_items_details.get(item_code), ), ) @@ -473,31 +477,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus) def get_available_item_locations( - item_code, from_warehouses, required_qty, company, ignore_validation=False + item_code, + from_warehouses, + required_qty, + company, + ignore_validation=False, + picked_item_details=None, ): locations = [] + total_picked_qty = ( + sum([v.get("picked_qty") for k, v in picked_item_details.items()]) if picked_item_details else 0 + ) has_serial_no = frappe.get_cached_value("Item", item_code, "has_serial_no") has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no") if has_batch_no and has_serial_no: locations = get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_serial_no: locations = get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) elif has_batch_no: locations = get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) else: locations = get_available_item_locations_for_other_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty ) total_qty_available = sum(location.get("qty") for location in locations) - remaining_qty = required_qty - total_qty_available if remaining_qty > 0 and not ignore_validation: @@ -508,11 +519,44 @@ def get_available_item_locations( title=_("Insufficient Stock"), ) + if picked_item_details: + for location in list(locations): + key = ( + (location["warehouse"], location["batch_no"]) + if location.get("batch_no") + else location["warehouse"] + ) + + if key in picked_item_details: + picked_detail = picked_item_details[key] + + if picked_detail.get("serial_no") and location.get("serial_no"): + location["serial_no"] = list( + set(location["serial_no"]).difference(set(picked_detail["serial_no"])) + ) + location["qty"] = len(location["serial_no"]) + else: + location["qty"] -= picked_detail.get("picked_qty") + + if location["qty"] < 1: + locations.remove(location) + + total_qty_available = sum(location.get("qty") for location in locations) + remaining_qty = required_qty - total_qty_available + + if remaining_qty > 0 and not ignore_validation: + frappe.msgprint( + _("{0} units of Item {1} is picked in another Pick List.").format( + remaining_qty, frappe.get_desk_link("Item", item_code) + ), + title=_("Already Picked"), + ) + return locations def get_available_item_locations_for_serialized_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): sn = frappe.qb.DocType("Serial No") query = ( @@ -520,7 +564,7 @@ def get_available_item_locations_for_serialized_item( .select(sn.name, sn.warehouse) .where((sn.item_code == item_code) & (sn.company == company)) .orderby(sn.purchase_date) - .limit(cint(required_qty)) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: @@ -542,7 +586,7 @@ def get_available_item_locations_for_serialized_item( def get_available_item_locations_for_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): sle = frappe.qb.DocType("Stock Ledger Entry") batch = frappe.qb.DocType("Batch") @@ -562,6 +606,7 @@ def get_available_item_locations_for_batched_item( .groupby(sle.warehouse, sle.batch_no, sle.item_code) .having(Sum(sle.actual_qty) > 0) .orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: @@ -571,7 +616,7 @@ def get_available_item_locations_for_batched_item( def get_available_item_locations_for_serial_and_batched_item( - item_code, from_warehouses, required_qty, company + item_code, from_warehouses, required_qty, company, total_picked_qty=0 ): # Get batch nos by FIFO locations = get_available_item_locations_for_batched_item( @@ -594,23 +639,26 @@ def get_available_item_locations_for_serial_and_batched_item( (conditions) & (sn.batch_no == location.batch_no) & (sn.warehouse == location.warehouse) ) .orderby(sn.purchase_date) - .limit(cint(location.qty)) + .limit(cint(location.qty + total_picked_qty)) ).run(as_dict=True) serial_nos = [sn.name for sn in serial_nos] location.serial_no = serial_nos + location.qty = len(serial_nos) return locations -def get_available_item_locations_for_other_item(item_code, from_warehouses, required_qty, company): +def get_available_item_locations_for_other_item( + item_code, from_warehouses, required_qty, company, total_picked_qty=0 +): bin = frappe.qb.DocType("Bin") query = ( frappe.qb.from_(bin) .select(bin.warehouse, bin.actual_qty.as_("qty")) .where((bin.item_code == item_code) & (bin.actual_qty > 0)) .orderby(bin.creation) - .limit(cint(required_qty)) + .limit(cint(required_qty + total_picked_qty)) ) if from_warehouses: From e8d617ada216dc699e6c68bc75106f3602056e0b Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 24 Jan 2023 11:10:29 +0530 Subject: [PATCH 07/14] chore: add `status` field in `Pick List` (cherry picked from commit be41052dc80e731bc058bcc4ca3ea0632658b3ea) # Conflicts: # erpnext/patches.txt --- erpnext/patches.txt | 4 ++ erpnext/patches/v14_0/set_pick_list_status.py | 40 +++++++++++++++++++ .../doctype/delivery_note/delivery_note.py | 6 +++ .../stock/doctype/pick_list/pick_list.json | 22 ++++++++-- erpnext/stock/doctype/pick_list/pick_list.py | 23 +++++++++++ .../stock/doctype/pick_list/pick_list_list.js | 14 +++++++ .../stock/doctype/stock_entry/stock_entry.py | 6 +++ 7 files changed, 112 insertions(+), 3 deletions(-) create mode 100644 erpnext/patches/v14_0/set_pick_list_status.py create mode 100644 erpnext/stock/doctype/pick_list/pick_list_list.js diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9dcb9c1bcae..d9734c90fe4 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,4 +325,8 @@ erpnext.patches.v14_0.setup_clear_repost_logs erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers +<<<<<<< HEAD erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries +======= +erpnext.patches.v14_0.set_pick_list_status +>>>>>>> be41052dc8 (chore: add `status` field in `Pick List`) diff --git a/erpnext/patches/v14_0/set_pick_list_status.py b/erpnext/patches/v14_0/set_pick_list_status.py new file mode 100644 index 00000000000..eea5745c23a --- /dev/null +++ b/erpnext/patches/v14_0/set_pick_list_status.py @@ -0,0 +1,40 @@ +# Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and Contributors +# License: MIT. See LICENSE + + +import frappe +from pypika.terms import ExistsCriterion + + +def execute(): + pl = frappe.qb.DocType("Pick List") + se = frappe.qb.DocType("Stock Entry") + dn = frappe.qb.DocType("Delivery Note") + + ( + frappe.qb.update(pl).set( + pl.status, + ( + frappe.qb.terms.Case() + .when(pl.docstatus == 0, "Draft") + .when(pl.docstatus == 2, "Cancelled") + .else_("Completed") + ), + ) + ).run() + + ( + frappe.qb.update(pl) + .set(pl.status, "Open") + .where( + ( + ExistsCriterion( + frappe.qb.from_(se).select(se.name).where((se.docstatus == 1) & (se.pick_list == pl.name)) + ) + | ExistsCriterion( + frappe.qb.from_(dn).select(dn.name).where((dn.docstatus == 1) & (dn.pick_list == pl.name)) + ) + ).negate() + & (pl.docstatus == 1) + ) + ).run() diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index a1df764ea9d..9f9f5cbe2a4 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -228,6 +228,7 @@ class DeliveryNote(SellingController): def on_submit(self): self.validate_packed_qty() + self.update_pick_list_status() # Check for Approving Authority frappe.get_doc("Authorization Control").validate_approving_authority( @@ -313,6 +314,11 @@ class DeliveryNote(SellingController): if has_error: raise frappe.ValidationError + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def check_next_docstatus(self): submit_rv = frappe.db.sql( """select t1.name diff --git a/erpnext/stock/doctype/pick_list/pick_list.json b/erpnext/stock/doctype/pick_list/pick_list.json index e1c3f0f5061..7259dc00a81 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.json +++ b/erpnext/stock/doctype/pick_list/pick_list.json @@ -26,7 +26,8 @@ "locations", "amended_from", "print_settings_section", - "group_same_items" + "group_same_items", + "status" ], "fields": [ { @@ -168,11 +169,26 @@ "fieldtype": "Data", "label": "Customer Name", "read_only": 1 + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "hidden": 1, + "in_standard_filter": 1, + "label": "Status", + "no_copy": 1, + "options": "Draft\nOpen\nCompleted\nCancelled", + "print_hide": 1, + "read_only": 1, + "report_hide": 1, + "reqd": 1, + "search_index": 1 } ], "is_submittable": 1, "links": [], - "modified": "2022-07-19 11:03:04.442174", + "modified": "2023-01-24 10:33:43.244476", "modified_by": "Administrator", "module": "Stock", "name": "Pick List", @@ -244,4 +260,4 @@ "sort_order": "DESC", "states": [], "track_changes": 1 -} +} \ No newline at end of file diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 7b75bb0ffd3..07961d03536 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -77,15 +77,32 @@ class PickList(Document): ) def on_submit(self): + self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() def on_cancel(self): + self.update_status() self.update_bundle_picked_qty() self.update_reference_qty() self.update_sales_order_picking_status() + def update_status(self, status=None, update_modified=True): + if not status: + if self.docstatus == 0: + status = "Draft" + elif self.docstatus == 1: + if self.status == "Draft": + status = "Open" + elif target_document_exists(self.name, self.purpose): + status = "Completed" + elif self.docstatus == 2: + status = "Cancelled" + + if status: + frappe.db.set_value("Pick List", self.name, "status", status, update_modified=update_modified) + def update_reference_qty(self): packed_items = [] so_items = [] @@ -394,6 +411,12 @@ class PickList(Document): return int(flt(min(possible_bundles), precision or 6)) +def update_pick_list_status(pick_list): + if pick_list: + doc = frappe.get_doc("Pick List", pick_list) + doc.run_method("update_status") + + def get_picked_items_qty(items) -> List[Dict]: pi_item = frappe.qb.DocType("Pick List Item") return ( diff --git a/erpnext/stock/doctype/pick_list/pick_list_list.js b/erpnext/stock/doctype/pick_list/pick_list_list.js new file mode 100644 index 00000000000..ad88b0a682f --- /dev/null +++ b/erpnext/stock/doctype/pick_list/pick_list_list.js @@ -0,0 +1,14 @@ +// Copyright (c) 2023, Frappe Technologies Pvt. Ltd. and contributors +// For license information, please see license.txt + +frappe.listview_settings['Pick List'] = { + get_indicator: function (doc) { + const status_colors = { + "Draft": "grey", + "Open": "orange", + "Completed": "green", + "Cancelled": "red", + }; + return [__(doc.status), status_colors[doc.status], "status,=," + doc.status]; + }, +}; \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index d90a74f7b4a..9a247317045 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -161,6 +161,7 @@ class StockEntry(StockController): self.validate_subcontract_order() self.update_subcontract_order_supplied_items() self.update_subcontracting_order_status() + self.update_pick_list_status() self.make_gl_entries() @@ -2279,6 +2280,11 @@ class StockEntry(StockController): update_subcontracting_order_status(self.subcontracting_order) + def update_pick_list_status(self): + from erpnext.stock.doctype.pick_list.pick_list import update_pick_list_status + + update_pick_list_status(self.pick_list) + def set_missing_values(self): "Updates rate and availability of all the items of mapped doc." self.set_transfer_qty() From 7afbd9201d328fa13170453bd120c6c04a7cec04 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 24 Jan 2023 14:45:19 +0530 Subject: [PATCH 08/14] fix: `get_picked_items_details` (cherry picked from commit 7b3d496ce0d8e9fba103e2df281709e7aa3c750f) --- erpnext/stock/doctype/pick_list/pick_list.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 07961d03536..38878484495 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -11,7 +11,8 @@ from frappe import _ from frappe.model.document import Document from frappe.model.mapper import map_child_doc from frappe.query_builder import Case -from frappe.query_builder.functions import Coalesce, IfNull, Locate, Sum +from frappe.query_builder.custom import GROUP_CONCAT +from frappe.query_builder.functions import Coalesce, IfNull, Locate, Replace, Sum from frappe.utils import cint, floor, flt, today from frappe.utils.nestedset import get_descendants_of @@ -334,20 +335,24 @@ class PickList(Document): def get_picked_items_details(self, items): picked_items = frappe._dict() + pi = frappe.qb.DocType("Pick List") pi_item = frappe.qb.DocType("Pick List Item") query = ( - frappe.qb.from_(pi_item) + frappe.qb.from_(pi) + .inner_join(pi_item) + .on(pi.name == pi_item.parent) .select( pi_item.item_code, pi_item.warehouse, pi_item.batch_no, - pi_item.serial_no, Sum(pi_item.picked_qty).as_("picked_qty"), + Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), ) .where( (pi_item.item_code.isin([x.item_code for x in items])) & (pi_item.docstatus != 2) & (pi_item.picked_qty > 0) + & (pi.status != "Completed") ) .groupby( pi_item.item_code, From aa3dd33f5626b8cbae54ba4f9ca97e1acefc9779 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Thu, 26 Jan 2023 18:10:05 +0530 Subject: [PATCH 09/14] fix: `pymysql.err.ProgrammingError` (cherry picked from commit 5138ef0160a72e19dfa3b11b4d662c68e79badb1) --- erpnext/stock/doctype/pick_list/pick_list.py | 75 ++++++++++---------- 1 file changed, 38 insertions(+), 37 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 38878484495..79c6891f5d2 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -335,47 +335,48 @@ class PickList(Document): def get_picked_items_details(self, items): picked_items = frappe._dict() - pi = frappe.qb.DocType("Pick List") - pi_item = frappe.qb.DocType("Pick List Item") - query = ( - frappe.qb.from_(pi) - .inner_join(pi_item) - .on(pi.name == pi_item.parent) - .select( - pi_item.item_code, - pi_item.warehouse, - pi_item.batch_no, - Sum(pi_item.picked_qty).as_("picked_qty"), - Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), + if items: + pi = frappe.qb.DocType("Pick List") + pi_item = frappe.qb.DocType("Pick List Item") + query = ( + frappe.qb.from_(pi) + .inner_join(pi_item) + .on(pi.name == pi_item.parent) + .select( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + Sum(pi_item.picked_qty).as_("picked_qty"), + Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), + ) + .where( + (pi_item.item_code.isin([x.item_code for x in items])) + & (pi_item.docstatus != 2) + & (pi_item.picked_qty > 0) + & (pi.status != "Completed") + ) + .groupby( + pi_item.item_code, + pi_item.warehouse, + pi_item.batch_no, + ) ) - .where( - (pi_item.item_code.isin([x.item_code for x in items])) - & (pi_item.docstatus != 2) - & (pi_item.picked_qty > 0) - & (pi.status != "Completed") - ) - .groupby( - pi_item.item_code, - pi_item.warehouse, - pi_item.batch_no, - ) - ) - if self.name: - query = query.where(pi_item.parent != self.name) + if self.name: + query = query.where(pi_item.parent != self.name) - items_data = query.run(as_dict=True) + items_data = query.run(as_dict=True) - for item_data in items_data: - key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse - serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None - data = {"picked_qty": item_data.picked_qty} - if serial_no: - data["serial_no"] = serial_no - if item_data.item_code not in picked_items: - picked_items[item_data.item_code] = {key: data} - else: - picked_items[item_data.item_code][key] = data + for item_data in items_data: + key = (item_data.warehouse, item_data.batch_no) if item_data.batch_no else item_data.warehouse + serial_no = [x for x in item_data.serial_no.split("\n") if x] if item_data.serial_no else None + data = {"picked_qty": item_data.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if item_data.item_code not in picked_items: + picked_items[item_data.item_code] = {key: data} + else: + picked_items[item_data.item_code][key] = data return picked_items From 7124c0ca30f26c715766b6d1363a4f041e082fac Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Fri, 27 Jan 2023 10:09:20 +0530 Subject: [PATCH 10/14] fix(test): `test_pick_list_for_items_with_multiple_UOM()` (cherry picked from commit 207eeefc857a6e4c136c7971d9a637452adcc395) --- erpnext/stock/doctype/pick_list/test_pick_list.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 43acdf08360..c93b8ce87da 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase): pick_list.submit() delivery_note = create_delivery_note(pick_list.name) + pick_list.load_from_db() self.assertEqual(pick_list.locations[0].qty, delivery_note.items[0].qty) self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty) From cdb6abf569b498897dfa56bce9dbe94d2933e167 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 28 Jan 2023 13:22:10 +0530 Subject: [PATCH 11/14] test: add test cases (cherry picked from commit bb7fe795fe0117cf042f22807f4827f6b027772b) --- .../stock/doctype/pick_list/test_pick_list.py | 135 ++++++++++++++++++ 1 file changed, 135 insertions(+) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index c93b8ce87da..9f8d2d71106 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -664,3 +664,138 @@ class TestPickList(FrappeTestCase): self.assertEqual(dn.items[0].rate, 42) so.reload() self.assertEqual(so.per_delivered, 100) + + def test_pick_list_status(self): + warehouse = "_Test Warehouse - _TC" + item = make_item(properties={"maintain_stock": 1}).name + make_stock_entry(item=item, to_warehouse=warehouse, qty=10) + + so = make_sales_order(item_code=item, qty=10, rate=100) + + pl = create_pick_list(so.name) + pl.save() + pl.reload() + self.assertEqual(pl.status, "Draft") + + pl.submit() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn = create_delivery_note(pl.name) + dn.save() + pl.reload() + self.assertEqual(pl.status, "Open") + + dn.submit() + pl.reload() + self.assertEqual(pl.status, "Completed") + + dn.cancel() + pl.reload() + self.assertEqual(pl.status, "Completed") + + pl.cancel() + pl.reload() + self.assertEqual(pl.status, "Cancelled") + + def test_consider_existing_pick_list(self): + # Step - 1: Setup - Create Items and Stock Entries + items_properties = [ + { + "valuation_rate": 100, + }, + { + "valuation_rate": 200, + "has_batch_no": 1, + "create_new_batch": 1, + }, + { + "valuation_rate": 300, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + { + "valuation_rate": 400, + "has_batch_no": 1, + "create_new_batch": 1, + "has_serial_no": 1, + "serial_no_series": "SNO.###", + }, + ] + + items = [] + for properties in items_properties: + properties.update({"maintain_stock": 1}) + item_code = make_item(properties=properties).name + properties.update({"item_code": item_code}) + items.append(properties) + + warehouses = ["Stores - _TC", "Finished Goods - _TC"] + for item in items: + for warehouse in warehouses: + se = make_stock_entry( + item=item.get("item_code"), + to_warehouse=warehouse, + qty=5, + ) + + # Step - 2: Create Sales Order [1] + item_list = [ + { + "item_code": item.get("item_code"), + "qty": 6, + "warehouse": "All Warehouses - _TC", + } + for item in items + ] + so1 = make_sales_order(item_list=item_list) + + # Step - 3: Create and Submit Pick List [1] for Sales Order [1] + pl1 = create_pick_list(so1.name) + pl1.submit() + + # Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1] + item_list = [ + { + "item_code": item.get("item_code"), + "qty": 4, + "warehouse": "All Warehouses - _TC", + } + for item in items + ] + so2 = make_sales_order(item_list=item_list) + + # Step - 5: Create Pick List [2] for Sales Order [2] + pl2 = create_pick_list(so2.name) + pl2.save() + + # Step - 6: Assert + items_data = {} + for location in pl1.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None + data = {"picked_qty": location.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if location.item_code not in items_data: + items_data[location.item_code] = {key: data} + else: + items_data[location.item_code][key] = data + + for location in pl2.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + item_data = items_data.get(location.item_code, {}).get(key, {}) + picked_qty = item_data.get("picked_qty", 0) + picked_serial_no = items_data.get("serial_no", []) + bin_actual_qty = frappe.db.get_value( + "Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty" + ) + + # Available Qty to pick should be equal to [Actual Qty - Picked Qty] + self.assertEqual(location.stock_qty, bin_actual_qty - picked_qty) + + # Serial No should not be in the Picked Serial No list + if location.serial_no: + a = set(picked_serial_no) + b = set([x for x in location.serial_no.split("\n") if x]) + self.assertSetEqual(b, b.difference(a)) From 4f56c72bedafb8e802abfef4a7aaf34db24f9af7 Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Sat, 28 Jan 2023 17:45:08 +0530 Subject: [PATCH 12/14] refactor: `test_consider_existing_pick_list()` (cherry picked from commit 0b76a26c8a660f21f38b2511586a6ea5d816fb0e) --- .../stock/doctype/pick_list/test_pick_list.py | 101 ++++++++++-------- 1 file changed, 55 insertions(+), 46 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/test_pick_list.py b/erpnext/stock/doctype/pick_list/test_pick_list.py index 9f8d2d71106..1254fe3927f 100644 --- a/erpnext/stock/doctype/pick_list/test_pick_list.py +++ b/erpnext/stock/doctype/pick_list/test_pick_list.py @@ -699,6 +699,54 @@ class TestPickList(FrappeTestCase): self.assertEqual(pl.status, "Cancelled") def test_consider_existing_pick_list(self): + def create_items(items_properties): + items = [] + + for properties in items_properties: + properties.update({"maintain_stock": 1}) + item_code = make_item(properties=properties).name + properties.update({"item_code": item_code}) + items.append(properties) + + return items + + def create_stock_entries(items): + warehouses = ["Stores - _TC", "Finished Goods - _TC"] + + for item in items: + for warehouse in warehouses: + se = make_stock_entry( + item=item.get("item_code"), + to_warehouse=warehouse, + qty=5, + ) + + def get_item_list(items, qty, warehouse="All Warehouses - _TC"): + return [ + { + "item_code": item.get("item_code"), + "qty": qty, + "warehouse": warehouse, + } + for item in items + ] + + def get_picked_items_details(pick_list_doc): + items_data = {} + + for location in pick_list_doc.locations: + key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse + serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None + data = {"picked_qty": location.picked_qty} + if serial_no: + data["serial_no"] = serial_no + if location.item_code not in items_data: + items_data[location.item_code] = {key: data} + else: + items_data[location.item_code][key] = data + + return items_data + # Step - 1: Setup - Create Items and Stock Entries items_properties = [ { @@ -723,70 +771,31 @@ class TestPickList(FrappeTestCase): }, ] - items = [] - for properties in items_properties: - properties.update({"maintain_stock": 1}) - item_code = make_item(properties=properties).name - properties.update({"item_code": item_code}) - items.append(properties) - - warehouses = ["Stores - _TC", "Finished Goods - _TC"] - for item in items: - for warehouse in warehouses: - se = make_stock_entry( - item=item.get("item_code"), - to_warehouse=warehouse, - qty=5, - ) + items = create_items(items_properties) + create_stock_entries(items) # Step - 2: Create Sales Order [1] - item_list = [ - { - "item_code": item.get("item_code"), - "qty": 6, - "warehouse": "All Warehouses - _TC", - } - for item in items - ] - so1 = make_sales_order(item_list=item_list) + so1 = make_sales_order(item_list=get_item_list(items, qty=6)) # Step - 3: Create and Submit Pick List [1] for Sales Order [1] pl1 = create_pick_list(so1.name) pl1.submit() # Step - 4: Create Sales Order [2] with same Item(s) as Sales Order [1] - item_list = [ - { - "item_code": item.get("item_code"), - "qty": 4, - "warehouse": "All Warehouses - _TC", - } - for item in items - ] - so2 = make_sales_order(item_list=item_list) + so2 = make_sales_order(item_list=get_item_list(items, qty=4)) # Step - 5: Create Pick List [2] for Sales Order [2] pl2 = create_pick_list(so2.name) pl2.save() # Step - 6: Assert - items_data = {} - for location in pl1.locations: - key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse - serial_no = [x for x in location.serial_no.split("\n") if x] if location.serial_no else None - data = {"picked_qty": location.picked_qty} - if serial_no: - data["serial_no"] = serial_no - if location.item_code not in items_data: - items_data[location.item_code] = {key: data} - else: - items_data[location.item_code][key] = data + picked_items_details = get_picked_items_details(pl1) for location in pl2.locations: key = (location.warehouse, location.batch_no) if location.batch_no else location.warehouse - item_data = items_data.get(location.item_code, {}).get(key, {}) + item_data = picked_items_details.get(location.item_code, {}).get(key, {}) picked_qty = item_data.get("picked_qty", 0) - picked_serial_no = items_data.get("serial_no", []) + picked_serial_no = picked_items_details.get("serial_no", []) bin_actual_qty = frappe.db.get_value( "Bin", {"item_code": location.item_code, "warehouse": location.warehouse}, "actual_qty" ) From df72e4a2217337d3815808a7bf7fba75eab643ea Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 31 Jan 2023 14:33:21 +0530 Subject: [PATCH 13/14] fix: consider `stock_qty` if `picked_qty` is zero (cherry picked from commit 6ffdeb1af8dc9debf63f15d8f1844a7fb1a35d40) --- erpnext/stock/doctype/pick_list/pick_list.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/pick_list/pick_list.py b/erpnext/stock/doctype/pick_list/pick_list.py index 79c6891f5d2..bf3b5ddc54a 100644 --- a/erpnext/stock/doctype/pick_list/pick_list.py +++ b/erpnext/stock/doctype/pick_list/pick_list.py @@ -346,14 +346,16 @@ class PickList(Document): pi_item.item_code, pi_item.warehouse, pi_item.batch_no, - Sum(pi_item.picked_qty).as_("picked_qty"), + Sum(Case().when(pi_item.picked_qty > 0, pi_item.picked_qty).else_(pi_item.stock_qty)).as_( + "picked_qty" + ), Replace(GROUP_CONCAT(pi_item.serial_no), ",", "\n").as_("serial_no"), ) .where( (pi_item.item_code.isin([x.item_code for x in items])) - & (pi_item.docstatus != 2) - & (pi_item.picked_qty > 0) + & ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0)) & (pi.status != "Completed") + & (pi_item.docstatus != 2) ) .groupby( pi_item.item_code, From 075c547184daa2754e2a64a9f45a68cec3f3c39e Mon Sep 17 00:00:00 2001 From: s-aga-r Date: Tue, 31 Jan 2023 16:14:45 +0530 Subject: [PATCH 14/14] chore: conflicts --- erpnext/patches.txt | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/patches.txt b/erpnext/patches.txt index d9734c90fe4..54957cd2228 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -325,8 +325,5 @@ erpnext.patches.v14_0.setup_clear_repost_logs erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request erpnext.patches.v14_0.update_entry_type_for_journal_entry erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers -<<<<<<< HEAD erpnext.patches.v14_0.update_asset_value_for_manual_depr_entries -======= -erpnext.patches.v14_0.set_pick_list_status ->>>>>>> be41052dc8 (chore: add `status` field in `Pick List`) +erpnext.patches.v14_0.set_pick_list_status \ No newline at end of file