mirror of
https://github.com/frappe/erpnext.git
synced 2026-08-18 00:48:42 +00:00
Pick list stock availability (#58243)
* feat: stock availability insight on pick list * fix: show holding pick lists inline in stock availability dialog * fix: dashboard layout for stock availability dialog * fix: reword stock release hint in availability dialog * fix: tree layout for stock held by section * fix: escape values in blocking pick lists table
This commit is contained in:
@@ -124,6 +124,10 @@ frappe.ui.form.on("Pick List", {
|
||||
frm.trigger("update_warehouse_property");
|
||||
erpnext.toggle_serial_batch_fields(frm);
|
||||
|
||||
if ((frm.doc.locations || []).length && !["Completed", "Cancelled"].includes(frm.doc.status)) {
|
||||
frm.add_custom_button(__("Stock Availability"), () => frm.events.show_stock_availability(frm));
|
||||
}
|
||||
|
||||
if (frm.doc.docstatus === 1) {
|
||||
const status_completed = frm.doc.status === "Completed";
|
||||
|
||||
@@ -326,6 +330,43 @@ frappe.ui.form.on("Pick List", {
|
||||
},
|
||||
});
|
||||
},
|
||||
show_stock_availability(frm) {
|
||||
const seen = new Set();
|
||||
const items = [];
|
||||
|
||||
(frm.doc.locations || []).forEach((row) => {
|
||||
if (!row.item_code || !row.warehouse) return;
|
||||
|
||||
const key = `${row.item_code}||${row.warehouse}`;
|
||||
if (seen.has(key)) return;
|
||||
|
||||
seen.add(key);
|
||||
items.push({ item_code: row.item_code, warehouse: row.warehouse });
|
||||
});
|
||||
|
||||
if (!items.length) {
|
||||
frappe.msgprint(__("Add items with a warehouse in the Item Locations table"));
|
||||
return;
|
||||
}
|
||||
|
||||
frappe
|
||||
.xcall("erpnext.stock.doctype.pick_list.pick_list.get_stock_availability", {
|
||||
items: items,
|
||||
pick_list: frm.doc.name,
|
||||
})
|
||||
.then((rows) => frm.events.render_stock_availability(rows));
|
||||
},
|
||||
|
||||
render_stock_availability(rows) {
|
||||
const dialog = new frappe.ui.Dialog({
|
||||
title: __("Stock Availability"),
|
||||
size: "extra-large",
|
||||
});
|
||||
|
||||
dialog.$body.html(get_availability_html(rows));
|
||||
dialog.show();
|
||||
},
|
||||
|
||||
show_reserved_stock(frm) {
|
||||
// Get the latest modified date from the locations table.
|
||||
var to_date = moment(
|
||||
@@ -417,6 +458,146 @@ frappe.ui.form.on("Pick List Item", {
|
||||
},
|
||||
});
|
||||
|
||||
function format_float(qty) {
|
||||
return frappe.format(qty, { fieldtype: "Float" });
|
||||
}
|
||||
|
||||
function get_availability_html(rows) {
|
||||
return `
|
||||
${get_availability_cards_html(rows)}
|
||||
${get_availability_summary_html(rows)}
|
||||
${get_holding_documents_html(rows)}`;
|
||||
}
|
||||
|
||||
function get_availability_cards_html(rows) {
|
||||
const blocked = rows.filter((row) => row.free_qty <= 0);
|
||||
const held = rows.filter((row) => row.pick_lists.length || row.reservations.length);
|
||||
|
||||
const cards = [
|
||||
{ label: __("Items"), value: rows.length, color: "var(--text-color)" },
|
||||
{
|
||||
label: __("Held by Other Documents"),
|
||||
value: held.length,
|
||||
color: held.length ? "var(--orange-500)" : "var(--green-500)",
|
||||
},
|
||||
{
|
||||
label: __("Not Free to Pick"),
|
||||
value: blocked.length,
|
||||
color: blocked.length ? "var(--red-500)" : "var(--green-500)",
|
||||
},
|
||||
];
|
||||
|
||||
const card_html = cards
|
||||
.map(
|
||||
(card) => `
|
||||
<div style="flex: 1; border: 1px solid var(--border-color); border-radius: var(--border-radius-md); padding: 12px 15px;">
|
||||
<div class="text-muted" style="font-size: var(--text-sm); margin-bottom: 4px;">${card.label}</div>
|
||||
<div style="font-size: var(--text-2xl); font-weight: 600; color: ${card.color};">${card.value}</div>
|
||||
</div>`
|
||||
)
|
||||
.join("");
|
||||
|
||||
return `<div style="display: flex; gap: 15px; margin-bottom: 20px;">${card_html}</div>`;
|
||||
}
|
||||
|
||||
function get_availability_summary_html(rows) {
|
||||
const header = `
|
||||
<tr>
|
||||
<th>${__("Item")}</th>
|
||||
<th>${__("Warehouse")}</th>
|
||||
<th class="text-right">${__("Actual Qty")}</th>
|
||||
<th class="text-right">${__("Held by Pick Lists")}</th>
|
||||
<th class="text-right">${__("Reserved Qty")}</th>
|
||||
<th class="text-right">${__("Free to Pick")}</th>
|
||||
</tr>`;
|
||||
|
||||
const body = rows
|
||||
.map((row) => {
|
||||
const has_detail = row.pick_lists.length || row.reservations.length;
|
||||
const color = row.free_qty <= 0 ? "red" : has_detail ? "orange" : "green";
|
||||
|
||||
return `
|
||||
<tr>
|
||||
<td><span class="indicator ${color}"></span> ${frappe.utils.escape_html(row.item_code)}</td>
|
||||
<td>${frappe.utils.escape_html(row.warehouse)}</td>
|
||||
<td class="text-right">${format_float(row.actual_qty)}</td>
|
||||
<td class="text-right">${format_float(row.picked_qty)}</td>
|
||||
<td class="text-right">${format_float(row.reserved_qty)}</td>
|
||||
<td class="text-right"><b>${format_float(row.free_qty)}</b></td>
|
||||
</tr>`;
|
||||
})
|
||||
.join("");
|
||||
|
||||
return `
|
||||
<h5 style="margin-bottom: 10px;">${__("Availability")}</h5>
|
||||
<table class="table table-bordered">${header}${body}</table>`;
|
||||
}
|
||||
|
||||
function get_holding_documents_html(rows) {
|
||||
const with_holders = rows.filter((row) => row.pick_lists.length || row.reservations.length);
|
||||
if (!with_holders.length) return "";
|
||||
|
||||
const header = `
|
||||
<tr>
|
||||
<th style="width: 45%">${__("Item / Document")}</th>
|
||||
<th>${__("Status")}</th>
|
||||
<th>${__("Batch No")}</th>
|
||||
<th class="text-right">${__("Qty")}</th>
|
||||
</tr>`;
|
||||
|
||||
const body = with_holders.map((row) => get_holding_tree_rows_html(row)).join("");
|
||||
|
||||
return `
|
||||
<h5 style="margin: 20px 0 10px;">${__("Stock Held By")}</h5>
|
||||
<div class="text-muted" style="font-size: var(--text-sm); margin-bottom: 10px;">
|
||||
${__("Cancel or delete these documents to release the stock.")}
|
||||
</div>
|
||||
<table class="table table-bordered">${header}${body}</table>`;
|
||||
}
|
||||
|
||||
function get_holding_tree_rows_html(row) {
|
||||
const total = row.picked_qty + row.reserved_qty;
|
||||
|
||||
let html = `
|
||||
<tr style="background-color: var(--control-bg);">
|
||||
<td colspan="3">
|
||||
<span class="text-muted">${__("Item")}:</span>
|
||||
<b>${frappe.utils.escape_html(row.item_code)}</b>
|
||||
<span class="text-muted" style="margin: 0 8px;">·</span>
|
||||
<span class="text-muted">${__("Warehouse")}:</span>
|
||||
<b>${frappe.utils.escape_html(row.warehouse)}</b>
|
||||
</td>
|
||||
<td class="text-right"><b>${format_float(total)}</b></td>
|
||||
</tr>`;
|
||||
|
||||
const child_cell = (content) =>
|
||||
`<td style="padding-left: 30px;"><span class="text-muted">└─</span> ${content}</td>`;
|
||||
|
||||
row.pick_lists.forEach((d) => {
|
||||
html += `
|
||||
<tr>
|
||||
${child_cell(frappe.utils.get_form_link("Pick List", d.pick_list, true))}
|
||||
<td>${__(d.status)}</td>
|
||||
<td>${frappe.utils.escape_html(d.batch_no || "")}</td>
|
||||
<td class="text-right">${format_float(d.holding_qty)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
row.reservations.forEach((d) => {
|
||||
const against = frappe.utils.get_form_link(d.voucher_type, d.voucher_no, true);
|
||||
const sre_link = frappe.utils.get_form_link("Stock Reservation Entry", d.name, true);
|
||||
html += `
|
||||
<tr>
|
||||
${child_cell(`${sre_link} · ${__("Reserved for {0}", [against])}`)}
|
||||
<td>${__(d.status)}</td>
|
||||
<td></td>
|
||||
<td class="text-right">${format_float(d.reserved_qty)}</td>
|
||||
</tr>`;
|
||||
});
|
||||
|
||||
return html;
|
||||
}
|
||||
|
||||
function get_item_details(item_code, uom = null, warehouse = null, company = null) {
|
||||
if (item_code) {
|
||||
return frappe.xcall("erpnext.stock.doctype.pick_list.pick_list.get_item_details", {
|
||||
|
||||
@@ -10,7 +10,7 @@ from frappe import _, bold
|
||||
from frappe.model.document import Document
|
||||
from frappe.query_builder import Case
|
||||
from frappe.query_builder.functions import Coalesce, GroupConcat, Locate, Lower, Max, Replace, Sum
|
||||
from frappe.utils import cint, floor, flt, get_link_to_form
|
||||
from frappe.utils import cint, escape_html, floor, flt, get_link_to_form
|
||||
from frappe.utils.nestedset import get_descendants_of
|
||||
|
||||
from erpnext.selling.doctype.product_bundle.product_bundle import get_active_product_bundle
|
||||
@@ -670,6 +670,7 @@ class PickList(TransactionBase):
|
||||
picked_item_details=picked_items_details.get(item_code),
|
||||
consider_rejected_warehouses=self.consider_rejected_warehouses,
|
||||
priority_warehouses=priority_warehouses,
|
||||
pick_list=self.name,
|
||||
),
|
||||
)
|
||||
|
||||
@@ -880,39 +881,18 @@ class PickList(TransactionBase):
|
||||
picked_items[row.item_code][key]["serial_no"].extend(serial_no)
|
||||
|
||||
def _get_pick_list_items(self, 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,
|
||||
pi_item.serial_and_batch_bundle,
|
||||
pi_item.serial_no,
|
||||
(
|
||||
Case()
|
||||
.when(
|
||||
(pi_item.picked_qty > 0) & (pi_item.docstatus == 1),
|
||||
pi_item.picked_qty - pi_item.delivered_qty,
|
||||
)
|
||||
.else_(pi_item.stock_qty)
|
||||
).as_("picked_qty"),
|
||||
)
|
||||
.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.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
query = get_open_pick_list_items_query(
|
||||
[x.item_code for x in items], exclude_pick_list=self.name
|
||||
).select(
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
pi_item.serial_and_batch_bundle,
|
||||
pi_item.serial_no,
|
||||
get_holding_qty_case(pi_item).as_("picked_qty"),
|
||||
)
|
||||
|
||||
if self.name:
|
||||
query = query.where(pi_item.parent != self.name)
|
||||
|
||||
query = query.for_update()
|
||||
|
||||
return query.run(as_dict=True)
|
||||
@@ -1042,6 +1022,143 @@ def get_picked_items_qty(items, contains_packed_items=False) -> list[dict]:
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_open_pick_list_items_query(item_codes, exclude_pick_list=None):
|
||||
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)
|
||||
.where(
|
||||
(pi_item.item_code.isin(item_codes))
|
||||
& ((pi_item.picked_qty > 0) | (pi_item.stock_qty > 0))
|
||||
& (pi.status != "Completed")
|
||||
& (pi.status != "Cancelled")
|
||||
& (pi_item.docstatus != 2)
|
||||
)
|
||||
)
|
||||
|
||||
if exclude_pick_list:
|
||||
query = query.where(pi_item.parent != exclude_pick_list)
|
||||
|
||||
return query
|
||||
|
||||
|
||||
def get_holding_qty_case(pi_item):
|
||||
return (
|
||||
Case()
|
||||
.when(
|
||||
(pi_item.picked_qty > 0) & (pi_item.docstatus == 1),
|
||||
pi_item.picked_qty - pi_item.delivered_qty,
|
||||
)
|
||||
.else_(pi_item.stock_qty)
|
||||
)
|
||||
|
||||
|
||||
def get_pick_list_holders(item_codes, warehouses=None, exclude_pick_list=None):
|
||||
pi = frappe.qb.DocType("Pick List")
|
||||
pi_item = frappe.qb.DocType("Pick List Item")
|
||||
|
||||
query = (
|
||||
get_open_pick_list_items_query(item_codes, exclude_pick_list=exclude_pick_list)
|
||||
.select(
|
||||
pi.name.as_("pick_list"),
|
||||
pi.status,
|
||||
pi_item.item_code,
|
||||
pi_item.warehouse,
|
||||
pi_item.batch_no,
|
||||
Sum(get_holding_qty_case(pi_item)).as_("holding_qty"),
|
||||
)
|
||||
.groupby(pi.name, pi.status, pi_item.item_code, pi_item.warehouse, pi_item.batch_no)
|
||||
)
|
||||
|
||||
if warehouses:
|
||||
query = query.where(pi_item.warehouse.isin(warehouses))
|
||||
|
||||
return query.run(as_dict=True)
|
||||
|
||||
|
||||
def get_reservation_holders(item_codes, warehouses):
|
||||
sre = frappe.qb.DocType("Stock Reservation Entry")
|
||||
|
||||
return (
|
||||
frappe.qb.from_(sre)
|
||||
.select(
|
||||
sre.name,
|
||||
sre.status,
|
||||
sre.item_code,
|
||||
sre.warehouse,
|
||||
sre.voucher_type,
|
||||
sre.voucher_no,
|
||||
(sre.reserved_qty - sre.delivered_qty - sre.transferred_qty - sre.consumed_qty).as_(
|
||||
"reserved_qty"
|
||||
),
|
||||
)
|
||||
.where(
|
||||
(sre.docstatus == 1)
|
||||
& (sre.item_code.isin(item_codes))
|
||||
& (sre.warehouse.isin(warehouses))
|
||||
& (sre.delivered_qty < sre.reserved_qty)
|
||||
& (sre.status.notin(["Closed", "Delivered"]))
|
||||
& (Coalesce(sre.from_voucher_type, "") != "Pick List")
|
||||
)
|
||||
).run(as_dict=True)
|
||||
|
||||
|
||||
def get_bin_qty_map(item_codes, warehouses):
|
||||
bin = frappe.qb.DocType("Bin")
|
||||
|
||||
data = (
|
||||
frappe.qb.from_(bin)
|
||||
.select(bin.item_code, bin.warehouse, bin.actual_qty)
|
||||
.where((bin.item_code.isin(item_codes)) & (bin.warehouse.isin(warehouses)))
|
||||
).run(as_dict=True)
|
||||
|
||||
return {(d.item_code, d.warehouse): flt(d.actual_qty) for d in data}
|
||||
|
||||
|
||||
@frappe.whitelist()
|
||||
def get_stock_availability(items: str | list, pick_list: str | None = None) -> list[dict]:
|
||||
frappe.has_permission("Pick List", throw=True)
|
||||
|
||||
items = frappe.parse_json(items)
|
||||
keys = {(d.get("item_code"), d.get("warehouse")) for d in items}
|
||||
keys = {key for key in keys if all(key)}
|
||||
if not keys:
|
||||
return []
|
||||
|
||||
item_codes = list({key[0] for key in keys})
|
||||
warehouses = list({key[1] for key in keys})
|
||||
|
||||
holders = get_pick_list_holders(item_codes, warehouses=warehouses, exclude_pick_list=pick_list)
|
||||
reservations = get_reservation_holders(item_codes, warehouses)
|
||||
bin_qty_map = get_bin_qty_map(item_codes, warehouses)
|
||||
|
||||
return [get_availability_row(key, bin_qty_map, holders, reservations) for key in sorted(keys)]
|
||||
|
||||
|
||||
def get_availability_row(key, bin_qty_map, holders, reservations):
|
||||
item_code, warehouse = key
|
||||
row_holders = [d for d in holders if (d.item_code, d.warehouse) == key]
|
||||
row_reservations = [d for d in reservations if (d.item_code, d.warehouse) == key]
|
||||
|
||||
actual_qty = flt(bin_qty_map.get(key))
|
||||
picked_qty = flt(sum(flt(d.holding_qty) for d in row_holders))
|
||||
reserved_qty = flt(sum(flt(d.reserved_qty) for d in row_reservations))
|
||||
|
||||
return frappe._dict(
|
||||
item_code=item_code,
|
||||
warehouse=warehouse,
|
||||
actual_qty=actual_qty,
|
||||
picked_qty=picked_qty,
|
||||
reserved_qty=reserved_qty,
|
||||
free_qty=actual_qty - picked_qty - reserved_qty,
|
||||
pick_lists=row_holders,
|
||||
reservations=row_reservations,
|
||||
)
|
||||
|
||||
|
||||
def get_items_with_location_and_quantity(item_doc, item_location_map, docstatus):
|
||||
available_locations = item_location_map.get(item_doc.item_code)
|
||||
locations = []
|
||||
@@ -1106,6 +1223,7 @@ def get_available_item_locations(
|
||||
picked_item_details=None,
|
||||
consider_rejected_warehouses=False,
|
||||
priority_warehouses=None,
|
||||
pick_list=None,
|
||||
):
|
||||
locations = []
|
||||
|
||||
@@ -1149,7 +1267,7 @@ def get_available_item_locations(
|
||||
locations = get_locations_based_on_required_qty(locations, required_qty, priority_warehouses)
|
||||
|
||||
if not ignore_validation:
|
||||
validate_picked_materials(item_code, required_qty, locations, picked_item_details)
|
||||
validate_picked_materials(item_code, required_qty, locations, picked_item_details, pick_list)
|
||||
|
||||
return locations
|
||||
|
||||
@@ -1174,7 +1292,7 @@ def get_locations_based_on_required_qty(locations, required_qty, priority_wareho
|
||||
return filtered_locations
|
||||
|
||||
|
||||
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None):
|
||||
def validate_picked_materials(item_code, required_qty, locations, picked_item_details=None, pick_list=None):
|
||||
for location in list(locations):
|
||||
if location["qty"] < 0:
|
||||
locations.remove(location)
|
||||
@@ -1182,21 +1300,41 @@ def validate_picked_materials(item_code, required_qty, locations, picked_item_de
|
||||
total_qty_available = sum(location.get("qty") for location in locations)
|
||||
remaining_qty = required_qty - total_qty_available
|
||||
|
||||
if remaining_qty > 0:
|
||||
if picked_item_details:
|
||||
frappe.msgprint(
|
||||
_(
|
||||
"{0} units of Item {1} is not available in any of the warehouses. Other Pick Lists exist for this item."
|
||||
).format(remaining_qty, get_link_to_form("Item", item_code)),
|
||||
title=_("Already Picked"),
|
||||
)
|
||||
else:
|
||||
frappe.msgprint(
|
||||
_("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
),
|
||||
title=_("Insufficient Stock"),
|
||||
)
|
||||
if remaining_qty <= 0:
|
||||
return
|
||||
|
||||
msg = _("{0} units of Item {1} is not available in any of the warehouses.").format(
|
||||
remaining_qty, get_link_to_form("Item", item_code)
|
||||
)
|
||||
|
||||
if picked_item_details:
|
||||
blockers = get_blocking_pick_lists_html(item_code, exclude_pick_list=pick_list)
|
||||
if blockers:
|
||||
msg += "<br><br>" + _("The stock is held by the following Pick Lists:") + blockers
|
||||
frappe.msgprint(msg, title=_("Stock Held by Other Pick Lists"))
|
||||
else:
|
||||
frappe.msgprint(msg, title=_("Insufficient Stock"))
|
||||
|
||||
|
||||
def get_blocking_pick_lists_html(item_code, exclude_pick_list=None):
|
||||
holders = get_pick_list_holders([item_code], exclude_pick_list=exclude_pick_list)
|
||||
if not holders:
|
||||
return ""
|
||||
|
||||
header = "<tr><th>{}</th><th>{}</th><th>{}</th><th style='text-align:right'>{}</th></tr>".format(
|
||||
_("Pick List"), _("Status"), _("Warehouse"), _("Qty")
|
||||
)
|
||||
rows = "".join(
|
||||
"<tr><td>{}</td><td>{}</td><td>{}</td><td style='text-align:right'>{}</td></tr>".format(
|
||||
get_link_to_form("Pick List", d.pick_list),
|
||||
escape_html(_(d.status)),
|
||||
escape_html(d.warehouse),
|
||||
flt(d.holding_qty),
|
||||
)
|
||||
for d in holders
|
||||
)
|
||||
|
||||
return f"<table class='table table-bordered'>{header}{rows}</table>"
|
||||
|
||||
|
||||
def filter_locations_by_picked_materials(locations, picked_item_details) -> list[dict]:
|
||||
|
||||
@@ -93,6 +93,50 @@ class TestPickList(ERPNextTestSuite):
|
||||
self.assertEqual(pick_list.locations[0].warehouse, "_Test Warehouse - _TC")
|
||||
self.assertEqual(pick_list.locations[0].qty, 5)
|
||||
|
||||
def test_get_stock_availability(self):
|
||||
from erpnext.stock.doctype.pick_list.pick_list import get_stock_availability
|
||||
|
||||
item = make_item(properties={"is_stock_item": 1}).name
|
||||
make_stock_entry(item=item, to_warehouse="_Test Warehouse - _TC", qty=100, basic_rate=100)
|
||||
|
||||
pick_list = frappe.get_doc(
|
||||
{
|
||||
"doctype": "Pick List",
|
||||
"company": "_Test Company",
|
||||
"purpose": "Material Transfer",
|
||||
"locations": [
|
||||
{
|
||||
"item_code": item,
|
||||
"qty": 40,
|
||||
"stock_qty": 40,
|
||||
"picked_qty": 40,
|
||||
"conversion_factor": 1,
|
||||
"warehouse": "_Test Warehouse - _TC",
|
||||
}
|
||||
],
|
||||
}
|
||||
).save()
|
||||
|
||||
items = frappe.as_json([{"item_code": item, "warehouse": "_Test Warehouse - _TC"}])
|
||||
rows = get_stock_availability(items)
|
||||
|
||||
self.assertEqual(len(rows), 1)
|
||||
self.assertEqual(rows[0].actual_qty, 100.0)
|
||||
self.assertEqual(rows[0].picked_qty, 40.0)
|
||||
self.assertEqual(rows[0].reserved_qty, 0.0)
|
||||
self.assertEqual(rows[0].free_qty, 60.0)
|
||||
self.assertEqual([d.pick_list for d in rows[0].pick_lists], [pick_list.name])
|
||||
self.assertEqual(rows[0].pick_lists[0].status, "Draft")
|
||||
|
||||
rows = get_stock_availability(items, pick_list=pick_list.name)
|
||||
self.assertEqual(rows[0].picked_qty, 0.0)
|
||||
self.assertEqual(rows[0].free_qty, 100.0)
|
||||
|
||||
pick_list.submit()
|
||||
rows = get_stock_availability(items)
|
||||
self.assertEqual(rows[0].picked_qty, 40.0)
|
||||
self.assertEqual(rows[0].pick_lists[0].status, "Open")
|
||||
|
||||
def test_pick_list_splits_row_according_to_warehouse_availability(self):
|
||||
try:
|
||||
frappe.get_doc(
|
||||
|
||||
Reference in New Issue
Block a user