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.update_entry_type_for_journal_entry
|
||||
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):
|
||||
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
|
||||
|
@ -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
|
||||
}
|
||||
}
|
@ -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 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
|
||||
|
||||
@ -77,15 +78,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 = []
|
||||
@ -162,6 +180,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
|
||||
@ -180,7 +199,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),
|
||||
),
|
||||
)
|
||||
|
||||
@ -309,6 +332,56 @@ 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()
|
||||
|
||||
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]:
|
||||
# Dict[so_item_row: item_code]
|
||||
product_bundles = {}
|
||||
@ -346,29 +419,30 @@ 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]:
|
||||
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):
|
||||
@ -434,31 +508,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:
|
||||
@ -469,25 +550,60 @@ 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
|
||||
):
|
||||
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:
|
||||
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:
|
||||
@ -501,7 +617,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")
|
||||
@ -521,6 +637,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:
|
||||
@ -530,53 +647,58 @@ 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(
|
||||
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 + total_picked_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
|
||||
location.qty = len(serial_nos)
|
||||
|
||||
return locations
|
||||
|
||||
|
||||
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]}
|
||||
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 + total_picked_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
|
||||
|
||||
|
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()
|
||||
|
||||
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)
|
||||
@ -663,3 +664,147 @@ 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):
|
||||
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.update_subcontract_order_supplied_items()
|
||||
self.update_subcontracting_order_status()
|
||||
self.update_pick_list_status()
|
||||
|
||||
self.make_gl_entries()
|
||||
|
||||
@ -2276,6 +2277,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()
|
||||
|
Loading…
x
Reference in New Issue
Block a user