Merge pull request #33715 from s-aga-r/fix-pick-list
fix: consider existing pick-list
This commit is contained in:
commit
ddb4396117
@ -325,3 +325,4 @@ erpnext.patches.v14_0.setup_clear_repost_logs
|
|||||||
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
|
erpnext.patches.v14_0.create_accounting_dimensions_for_payment_request
|
||||||
erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
erpnext.patches.v14_0.update_entry_type_for_journal_entry
|
||||||
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
erpnext.patches.v14_0.change_autoname_for_tax_withheld_vouchers
|
||||||
|
erpnext.patches.v14_0.set_pick_list_status
|
||||||
|
40
erpnext/patches/v14_0/set_pick_list_status.py
Normal file
40
erpnext/patches/v14_0/set_pick_list_status.py
Normal file
@ -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()
|
@ -228,6 +228,7 @@ class DeliveryNote(SellingController):
|
|||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
self.validate_packed_qty()
|
self.validate_packed_qty()
|
||||||
|
self.update_pick_list_status()
|
||||||
|
|
||||||
# Check for Approving Authority
|
# Check for Approving Authority
|
||||||
frappe.get_doc("Authorization Control").validate_approving_authority(
|
frappe.get_doc("Authorization Control").validate_approving_authority(
|
||||||
@ -313,6 +314,11 @@ class DeliveryNote(SellingController):
|
|||||||
if has_error:
|
if has_error:
|
||||||
raise frappe.ValidationError
|
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):
|
def check_next_docstatus(self):
|
||||||
submit_rv = frappe.db.sql(
|
submit_rv = frappe.db.sql(
|
||||||
"""select t1.name
|
"""select t1.name
|
||||||
|
@ -26,7 +26,8 @@
|
|||||||
"locations",
|
"locations",
|
||||||
"amended_from",
|
"amended_from",
|
||||||
"print_settings_section",
|
"print_settings_section",
|
||||||
"group_same_items"
|
"group_same_items",
|
||||||
|
"status"
|
||||||
],
|
],
|
||||||
"fields": [
|
"fields": [
|
||||||
{
|
{
|
||||||
@ -168,11 +169,26 @@
|
|||||||
"fieldtype": "Data",
|
"fieldtype": "Data",
|
||||||
"label": "Customer Name",
|
"label": "Customer Name",
|
||||||
"read_only": 1
|
"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,
|
"is_submittable": 1,
|
||||||
"links": [],
|
"links": [],
|
||||||
"modified": "2022-07-19 11:03:04.442174",
|
"modified": "2023-01-24 10:33:43.244476",
|
||||||
"modified_by": "Administrator",
|
"modified_by": "Administrator",
|
||||||
"module": "Stock",
|
"module": "Stock",
|
||||||
"name": "Pick List",
|
"name": "Pick List",
|
||||||
@ -244,4 +260,4 @@
|
|||||||
"sort_order": "DESC",
|
"sort_order": "DESC",
|
||||||
"states": [],
|
"states": [],
|
||||||
"track_changes": 1
|
"track_changes": 1
|
||||||
}
|
}
|
@ -11,7 +11,8 @@ from frappe import _
|
|||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
from frappe.model.mapper import map_child_doc
|
from frappe.model.mapper import map_child_doc
|
||||||
from frappe.query_builder import Case
|
from frappe.query_builder import Case
|
||||||
from frappe.query_builder.functions import 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 import cint, floor, flt, today
|
||||||
from frappe.utils.nestedset import get_descendants_of
|
from frappe.utils.nestedset import get_descendants_of
|
||||||
|
|
||||||
@ -77,15 +78,32 @@ class PickList(Document):
|
|||||||
)
|
)
|
||||||
|
|
||||||
def on_submit(self):
|
def on_submit(self):
|
||||||
|
self.update_status()
|
||||||
self.update_bundle_picked_qty()
|
self.update_bundle_picked_qty()
|
||||||
self.update_reference_qty()
|
self.update_reference_qty()
|
||||||
self.update_sales_order_picking_status()
|
self.update_sales_order_picking_status()
|
||||||
|
|
||||||
def on_cancel(self):
|
def on_cancel(self):
|
||||||
|
self.update_status()
|
||||||
self.update_bundle_picked_qty()
|
self.update_bundle_picked_qty()
|
||||||
self.update_reference_qty()
|
self.update_reference_qty()
|
||||||
self.update_sales_order_picking_status()
|
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):
|
def update_reference_qty(self):
|
||||||
packed_items = []
|
packed_items = []
|
||||||
so_items = []
|
so_items = []
|
||||||
@ -162,6 +180,7 @@ class PickList(Document):
|
|||||||
def set_item_locations(self, save=False):
|
def set_item_locations(self, save=False):
|
||||||
self.validate_for_qty()
|
self.validate_for_qty()
|
||||||
items = self.aggregate_item_qty()
|
items = self.aggregate_item_qty()
|
||||||
|
picked_items_details = self.get_picked_items_details(items)
|
||||||
self.item_location_map = frappe._dict()
|
self.item_location_map = frappe._dict()
|
||||||
|
|
||||||
from_warehouses = None
|
from_warehouses = None
|
||||||
@ -180,7 +199,11 @@ class PickList(Document):
|
|||||||
self.item_location_map.setdefault(
|
self.item_location_map.setdefault(
|
||||||
item_code,
|
item_code,
|
||||||
get_available_item_locations(
|
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),
|
||||||
),
|
),
|
||||||
)
|
)
|
||||||
|
|
||||||
@ -309,6 +332,56 @@ class PickList(Document):
|
|||||||
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
|
already_picked + (picked_qty * (1 if self.docstatus == 1 else -1)),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
def get_picked_items_details(self, items):
|
||||||
|
picked_items = frappe._dict()
|
||||||
|
|
||||||
|
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(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.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||||
|
& (pi.status != "Completed")
|
||||||
|
& (pi_item.docstatus != 2)
|
||||||
|
)
|
||||||
|
.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]:
|
def _get_product_bundles(self) -> Dict[str, str]:
|
||||||
# Dict[so_item_row: item_code]
|
# Dict[so_item_row: item_code]
|
||||||
product_bundles = {}
|
product_bundles = {}
|
||||||
@ -346,29 +419,30 @@ class PickList(Document):
|
|||||||
return int(flt(min(possible_bundles), precision or 6))
|
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]:
|
def get_picked_items_qty(items) -> List[Dict]:
|
||||||
return frappe.db.sql(
|
pi_item = frappe.qb.DocType("Pick List Item")
|
||||||
f"""
|
return (
|
||||||
SELECT
|
frappe.qb.from_(pi_item)
|
||||||
sales_order_item,
|
.select(
|
||||||
item_code,
|
pi_item.sales_order_item,
|
||||||
sales_order,
|
pi_item.item_code,
|
||||||
SUM(stock_qty) AS stock_qty,
|
pi_item.sales_order,
|
||||||
SUM(picked_qty) AS picked_qty
|
Sum(pi_item.stock_qty).as_("stock_qty"),
|
||||||
FROM
|
Sum(pi_item.picked_qty).as_("picked_qty"),
|
||||||
`tabPick List Item`
|
)
|
||||||
WHERE
|
.where((pi_item.docstatus == 1) & (pi_item.sales_order_item.isin(items)))
|
||||||
sales_order_item IN (
|
.groupby(
|
||||||
{", ".join(frappe.db.escape(d) for d in items)}
|
pi_item.sales_order_item,
|
||||||
)
|
pi_item.sales_order,
|
||||||
AND docstatus = 1
|
)
|
||||||
GROUP BY
|
.for_update()
|
||||||
sales_order_item,
|
).run(as_dict=True)
|
||||||
sales_order
|
|
||||||
FOR UPDATE
|
|
||||||
""",
|
|
||||||
as_dict=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
|
|
||||||
def validate_item_locations(pick_list):
|
def validate_item_locations(pick_list):
|
||||||
@ -434,31 +508,38 @@ def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus)
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations(
|
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 = []
|
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_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")
|
has_batch_no = frappe.get_cached_value("Item", item_code, "has_batch_no")
|
||||||
|
|
||||||
if has_batch_no and has_serial_no:
|
if has_batch_no and has_serial_no:
|
||||||
locations = get_available_item_locations_for_serial_and_batched_item(
|
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:
|
elif has_serial_no:
|
||||||
locations = get_available_item_locations_for_serialized_item(
|
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:
|
elif has_batch_no:
|
||||||
locations = get_available_item_locations_for_batched_item(
|
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:
|
else:
|
||||||
locations = get_available_item_locations_for_other_item(
|
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)
|
total_qty_available = sum(location.get("qty") for location in locations)
|
||||||
|
|
||||||
remaining_qty = required_qty - total_qty_available
|
remaining_qty = required_qty - total_qty_available
|
||||||
|
|
||||||
if remaining_qty > 0 and not ignore_validation:
|
if remaining_qty > 0 and not ignore_validation:
|
||||||
@ -469,25 +550,60 @@ def get_available_item_locations(
|
|||||||
title=_("Insufficient Stock"),
|
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
|
return locations
|
||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_serialized_item(
|
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
|
||||||
):
|
):
|
||||||
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 + total_picked_qty))
|
||||||
|
)
|
||||||
|
|
||||||
if from_warehouses:
|
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_nos = query.run(as_list=True)
|
||||||
"Serial No",
|
|
||||||
fields=["name", "warehouse"],
|
|
||||||
filters=filters,
|
|
||||||
limit=required_qty,
|
|
||||||
order_by="purchase_date",
|
|
||||||
as_list=1,
|
|
||||||
)
|
|
||||||
|
|
||||||
warehouse_serial_nos_map = frappe._dict()
|
warehouse_serial_nos_map = frappe._dict()
|
||||||
for serial_no, warehouse in serial_nos:
|
for serial_no, warehouse in serial_nos:
|
||||||
@ -501,7 +617,7 @@ def get_available_item_locations_for_serialized_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_batched_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")
|
sle = frappe.qb.DocType("Stock Ledger Entry")
|
||||||
batch = frappe.qb.DocType("Batch")
|
batch = frappe.qb.DocType("Batch")
|
||||||
@ -521,6 +637,7 @@ def get_available_item_locations_for_batched_item(
|
|||||||
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
|
.groupby(sle.warehouse, sle.batch_no, sle.item_code)
|
||||||
.having(Sum(sle.actual_qty) > 0)
|
.having(Sum(sle.actual_qty) > 0)
|
||||||
.orderby(IfNull(batch.expiry_date, "2200-01-01"), batch.creation, sle.batch_no, sle.warehouse)
|
.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:
|
if from_warehouses:
|
||||||
@ -530,53 +647,58 @@ def get_available_item_locations_for_batched_item(
|
|||||||
|
|
||||||
|
|
||||||
def get_available_item_locations_for_serial_and_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
|
# Get batch nos by FIFO
|
||||||
locations = get_available_item_locations_for_batched_item(
|
locations = get_available_item_locations_for_batched_item(
|
||||||
item_code, from_warehouses, required_qty, company
|
item_code, from_warehouses, required_qty, company
|
||||||
)
|
)
|
||||||
|
|
||||||
filters = frappe._dict(
|
if locations:
|
||||||
{"item_code": item_code, "company": company, "warehouse": ["!=", ""], "batch_no": ""}
|
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:
|
||||||
for location in locations:
|
location.qty = (
|
||||||
filters.batch_no = location.batch_no
|
required_qty if location.qty > required_qty else location.qty
|
||||||
filters.warehouse = location.warehouse
|
) # if extra qty in batch
|
||||||
location.qty = (
|
|
||||||
required_qty if location.qty > required_qty else location.qty
|
|
||||||
) # if extra qty in batch
|
|
||||||
|
|
||||||
serial_nos = frappe.get_list(
|
serial_nos = (
|
||||||
"Serial No", fields=["name"], filters=filters, limit=location.qty, order_by="purchase_date"
|
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 + total_picked_qty))
|
||||||
|
).run(as_dict=True)
|
||||||
|
|
||||||
serial_nos = [sn.name for sn in serial_nos]
|
serial_nos = [sn.name for sn in serial_nos]
|
||||||
location.serial_no = serial_nos
|
location.serial_no = serial_nos
|
||||||
|
location.qty = len(serial_nos)
|
||||||
|
|
||||||
return locations
|
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(
|
||||||
# gets all items available in different warehouses
|
item_code, from_warehouses, required_qty, company, total_picked_qty=0
|
||||||
warehouses = [x.get("name") for x in frappe.get_list("Warehouse", {"company": company}, "name")]
|
):
|
||||||
|
bin = frappe.qb.DocType("Bin")
|
||||||
filters = frappe._dict(
|
query = (
|
||||||
{"item_code": item_code, "warehouse": ["in", warehouses], "actual_qty": [">", 0]}
|
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 + total_picked_qty))
|
||||||
)
|
)
|
||||||
|
|
||||||
if from_warehouses:
|
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(
|
item_locations = query.run(as_dict=True)
|
||||||
"Bin",
|
|
||||||
fields=["warehouse", "actual_qty as qty"],
|
|
||||||
filters=filters,
|
|
||||||
limit=required_qty,
|
|
||||||
order_by="creation",
|
|
||||||
)
|
|
||||||
|
|
||||||
return item_locations
|
return item_locations
|
||||||
|
|
||||||
|
14
erpnext/stock/doctype/pick_list/pick_list_list.js
Normal file
14
erpnext/stock/doctype/pick_list/pick_list_list.js
Normal file
@ -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];
|
||||||
|
},
|
||||||
|
};
|
@ -414,6 +414,7 @@ class TestPickList(FrappeTestCase):
|
|||||||
pick_list.submit()
|
pick_list.submit()
|
||||||
|
|
||||||
delivery_note = create_delivery_note(pick_list.name)
|
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[0].qty, delivery_note.items[0].qty)
|
||||||
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
|
self.assertEqual(pick_list.locations[1].qty, delivery_note.items[1].qty)
|
||||||
@ -663,3 +664,147 @@ class TestPickList(FrappeTestCase):
|
|||||||
self.assertEqual(dn.items[0].rate, 42)
|
self.assertEqual(dn.items[0].rate, 42)
|
||||||
so.reload()
|
so.reload()
|
||||||
self.assertEqual(so.per_delivered, 100)
|
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):
|
||||||
|
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 = [
|
||||||
|
{
|
||||||
|
"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 = create_items(items_properties)
|
||||||
|
create_stock_entries(items)
|
||||||
|
|
||||||
|
# Step - 2: Create Sales Order [1]
|
||||||
|
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]
|
||||||
|
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
|
||||||
|
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 = picked_items_details.get(location.item_code, {}).get(key, {})
|
||||||
|
picked_qty = item_data.get("picked_qty", 0)
|
||||||
|
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"
|
||||||
|
)
|
||||||
|
|
||||||
|
# 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))
|
||||||
|
@ -158,6 +158,7 @@ class StockEntry(StockController):
|
|||||||
self.validate_subcontract_order()
|
self.validate_subcontract_order()
|
||||||
self.update_subcontract_order_supplied_items()
|
self.update_subcontract_order_supplied_items()
|
||||||
self.update_subcontracting_order_status()
|
self.update_subcontracting_order_status()
|
||||||
|
self.update_pick_list_status()
|
||||||
|
|
||||||
self.make_gl_entries()
|
self.make_gl_entries()
|
||||||
|
|
||||||
@ -2276,6 +2277,11 @@ class StockEntry(StockController):
|
|||||||
|
|
||||||
update_subcontracting_order_status(self.subcontracting_order)
|
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):
|
def set_missing_values(self):
|
||||||
"Updates rate and availability of all the items of mapped doc."
|
"Updates rate and availability of all the items of mapped doc."
|
||||||
self.set_transfer_qty()
|
self.set_transfer_qty()
|
||||||
|
Loading…
x
Reference in New Issue
Block a user