fix: Delivery note creation from pick list

- Changes in serial no and batch no seletion
- Changes in warehouse overrwite logic

Co-authored-by: Nabin Hait <nabinhait@gmail.com>
This commit is contained in:
Suraj Shetty 2019-08-16 08:16:22 +05:30
parent e047de854b
commit 182f4def00
7 changed files with 401 additions and 199 deletions

View File

@ -263,7 +263,7 @@ class AccountsController(TransactionBase):
if self.get("is_subcontracted"):
args["is_subcontracted"] = self.is_subcontracted
ret = get_item_details(args, self)
ret = get_item_details(args, self, overwrite_warehouse=False)
for fieldname, value in ret.items():
if item.meta.get_field(fieldname) and value is not None:

View File

@ -568,7 +568,7 @@ def make_project(source_name, target_doc=None):
return doc
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False):
def set_missing_values(source, target):
target.ignore_pricing_rule = 1
target.run_method("set_missing_values")
@ -593,23 +593,13 @@ def make_delivery_note(source_name, target_doc=None):
or item.get("buying_cost_center") \
or item_group.get("buying_cost_center")
target_doc = get_mapped_doc("Sales Order", source_name, {
mapper = {
"Sales Order": {
"doctype": "Delivery Note",
"validation": {
"docstatus": ["=", 1]
}
},
"Sales Order Item": {
"doctype": "Delivery Note Item",
"field_map": {
"rate": "rate",
"name": "so_detail",
"parent": "against_sales_order",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
},
"Sales Taxes and Charges": {
"doctype": "Sales Taxes and Charges",
"add_if_empty": True
@ -618,7 +608,21 @@ def make_delivery_note(source_name, target_doc=None):
"doctype": "Sales Team",
"add_if_empty": True
}
}, target_doc, set_missing_values)
}
if not skip_item_mapping:
mapper["Sales Order Item"] = {
"doctype": "Delivery Note Item",
"field_map": {
"rate": "rate",
"name": "so_detail",
"parent": "against_sales_order",
},
"postprocess": update_item,
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
}
target_doc = get_mapped_doc("Sales Order", source_name, mapper, target_doc, set_missing_values)
return target_doc
@ -999,9 +1003,16 @@ def make_inter_company_purchase_order(source_name, target_doc=None):
@frappe.whitelist()
def make_pick_list(source_name, target_doc=None):
def update_item_quantity(source, target, source_parent):
target.qty = flt(source.qty) - flt(source.delivered_qty)
target.stock_qty = (flt(source.qty) - flt(source.delivered_qty)) * flt(source.conversion_factor)
doc = get_mapped_doc("Sales Order", source_name, {
"Sales Order": {
"doctype": "Pick List",
"field_map": {
"doctype": "items_based_on"
},
"validation": {
"docstatus": ["=", 1]
}
@ -1009,11 +1020,11 @@ def make_pick_list(source_name, target_doc=None):
"Sales Order Item": {
"doctype": "Pick List Reference Item",
"field_map": {
"item_code": "item",
"parenttype": "reference_doctype",
"parent": "reference_name",
"name": "reference_document_item"
"parent": "sales_order",
"name": "sales_order_item"
},
"postprocess": update_item_quantity,
"conditions": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
},
}, target_doc)

View File

@ -7,6 +7,7 @@
"field_order": [
"company",
"column_break_4",
"items_based_on",
"parent_warehouse",
"section_break_4",
"reference_items",
@ -50,9 +51,16 @@
"fieldtype": "Table",
"label": "Items To Be Picked",
"options": "Pick List Reference Item"
},
{
"default": "Sales Order",
"fieldname": "items_based_on",
"fieldtype": "Select",
"label": "Items Based On",
"options": "\nSales Order\nWork Order"
}
],
"modified": "2019-08-01 10:50:17.055509",
"modified": "2019-08-13 19:30:01.151720",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List",

View File

@ -5,7 +5,10 @@
from __future__ import unicode_literals
import frappe
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from six import iteritems
from frappe.model.mapper import get_mapped_doc, map_child_doc
from frappe.utils import floor, flt, today
from erpnext.selling.doctype.sales_order.sales_order import make_delivery_note as make_delivery_note_from_sales_order
class PickList(Document):
def set_item_locations(self):
@ -17,46 +20,56 @@ class PickList(Document):
# Reset
self.delete_key('item_locations')
for item in reference_items:
data = get_items_with_warehouse_and_quantity(item, from_warehouses)
for item_info in data:
print(self.append('item_locations', item_info))
for item_doc in reference_items:
if frappe.get_cached_value('Item', item_doc.item_code, 'has_serial_no'):
item_locations = get_item_locations_based_on_serial_nos(item_doc)
elif frappe.get_cached_value('Item', item_doc.item_code, 'has_batch_no'):
item_locations = get_item_locations_based_on_batch_nos(item_doc)
else:
item_locations = get_items_with_warehouse_and_quantity(item_doc, from_warehouses)
for item_doc in self.get('item_locations'):
if frappe.get_cached_value('Item', item_doc.item, 'has_serial_no'):
set_serial_nos(item_doc)
elif frappe.get_cached_value('Item', item_doc.item, 'has_batch_no'):
set_batch_no(item_doc, self)
for row in item_locations:
row.update({
'item_code': item_doc.item_code,
'sales_order': item_doc.sales_order,
'sales_order_item': item_doc.sales_order_item,
'uom': item_doc.uom,
'stock_uom': item_doc.stock_uom,
'conversion_factor': item_doc.conversion_factor,
'stock_qty': row.get("qty", 0) * item_doc.conversion_factor,
'picked_qty': row.get("qty", 0) * item_doc.conversion_factor
})
self.append('item_locations', row)
def get_items_with_warehouse_and_quantity(item_doc, from_warehouses):
items = []
item_locations = get_available_items(item_doc.item, from_warehouses)
remaining_qty = item_doc.qty
item_locations = []
item_location_map = get_available_items(item_doc.item_code, from_warehouses)
remaining_stock_qty = item_doc.stock_qty
while remaining_stock_qty > 0 and item_location_map:
item_location = item_location_map.pop(0)
stock_qty = remaining_stock_qty if item_location.qty >= remaining_stock_qty else item_location.qty
qty = stock_qty / (item_doc.conversion_factor or 1)
while remaining_qty > 0 and item_locations:
item_location = item_locations.pop(0)
qty = remaining_qty if item_location.qty >= remaining_qty else item_location.qty
items.append({
'item': item_doc.item,
uom_must_be_whole_number = frappe.db.get_value("UOM", item_doc.uom, "must_be_whole_number")
if uom_must_be_whole_number:
qty = floor(qty)
stock_qty = qty * item_doc.conversion_factor
item_locations.append({
'qty': qty,
'warehouse': item_location.warehouse,
'reference_doctype': item_doc.reference_doctype,
'reference_name': item_doc.reference_name,
'reference_document_item': item_doc.reference_document_item,
'warehouse': item_location.warehouse
})
remaining_qty -= qty
remaining_stock_qty -= stock_qty
if remaining_qty:
frappe.msgprint('{} qty of {} is out of stock. Skipping...'.format(remaining_qty, item_doc.item))
return items
if remaining_stock_qty:
frappe.msgprint('{0} {1} of {2} is not available.'
.format(remaining_stock_qty / item_doc.conversion_factor, item_doc.uom, item_doc.item_code))
return item_locations
return items
def get_available_items(item, from_warehouses):
def get_available_items(item_code, from_warehouses):
# gets all items available in different warehouses
# FIFO
filters = frappe._dict({
'item_code': item,
'item_code': item_code,
'actual_qty': ['>', 0]
})
if from_warehouses:
@ -71,79 +84,170 @@ def get_available_items(item, from_warehouses):
def set_serial_nos(item_doc):
serial_nos = frappe.get_all('Serial No', {
'item_code': item_doc.item,
'item_code': item_doc.item_code,
'warehouse': item_doc.warehouse
}, limit=item_doc.qty, order_by='purchase_date')
}, limit=item_doc.stock_qty, order_by='purchase_date')
item_doc.set('serial_no', '\n'.join([serial_no.name for serial_no in serial_nos]))
# should we assume that all serialized item available in stock will have serial no?
# should we assume that all serialized item_code available in stock will have serial no?
def set_batch_no(item_doc, parent_doc):
batches = frappe.db.sql("""
def get_item_locations_based_on_serial_nos(item_doc):
serial_nos = frappe.get_all('Serial No',
fields = ['name', 'warehouse'],
filters = {
'item_code': item_doc.item_code,
'warehouse': ['!=', '']
}, limit=item_doc.stock_qty, order_by='purchase_date', as_list=1)
remaining_stock_qty = flt(item_doc.stock_qty) - len(serial_nos)
if remaining_stock_qty:
frappe.msgprint('{0} {1} of {2} is not available.'
.format(remaining_stock_qty, item_doc.stock_uom, item_doc.item_code))
warehouse_serial_nos_map = frappe._dict()
for serial_no, warehouse in serial_nos:
warehouse_serial_nos_map.setdefault(warehouse, []).append(serial_no)
item_locations = []
for warehouse, serial_nos in iteritems(warehouse_serial_nos_map):
item_locations.append({
'qty': len(serial_nos),
'warehouse': warehouse,
'serial_no': '\n'.join(serial_nos)
})
return item_locations
def get_item_locations_based_on_batch_nos(item_doc):
batch_qty = frappe.db.sql("""
SELECT
`batch_no`,
SUM(`actual_qty`) AS `qty`
sle.`warehouse`,
sle.`batch_no`,
SUM(sle.`actual_qty`) AS `qty`
FROM
`tabStock Ledger Entry`
`tabStock Ledger Entry` sle, `tabBatch` batch
WHERE
`item_code`=%(item_code)s
AND `warehouse`=%(warehouse)s
sle.batch_no = batch.name
and sle.`item_code`=%(item_code)s
and IFNULL(batch.expiry_date, '2200-01-01') > %(today)s
GROUP BY
`warehouse`,
`batch_no`,
`item_code`
HAVING `qty` > 0
ORDER BY IFNULL(batch.expiry_date, '2200-01-01')
""", {
'item_code': item_doc.item,
'warehouse': item_doc.warehouse,
'item_code': item_doc.item_code,
'today': today()
}, as_dict=1)
item_locations = []
required_qty = item_doc.qty
while required_qty > 0 and batches:
batch = batches.pop()
batch_expiry = frappe.get_value('Batch', batch.batch_no, 'expiry_date')
if batch_expiry and batch_expiry <= frappe.utils.getdate():
frappe.msgprint('Skipping expired Batch {}'.format(batch.batch_no))
continue
item_doc.batch_no = batch.batch_no
if batch.qty >= item_doc.qty:
required_qty = 0
break
for d in batch_qty:
if d.qty > required_qty:
d.qty = required_qty
else:
# split item if quantity of item in batch is less that required
# Look for another batch
required_qty -= d.qty
item_locations.append(d)
if required_qty <= 0:
break
# required_qty = item_doc.qty
# while required_qty > 0 and batches:
# batch = batches.pop()
# batch_expiry = frappe.get_value('Batch', batch.batch_no, 'expiry_date')
# if batch_expiry and batch_expiry <= frappe.utils.getdate():
# frappe.msgprint('Skipping expired Batch {}'.format(batch.batch_no))
# continue
# item_doc.batch_no = batch.batch_no
# if batch.qty >= item_doc.qty:
# required_qty = 0
# break
# else:
# # split item_code if quantity of item_code in batch is less that required
# # Look for another batch
# required_qty -= batch.qty
# # set quantity of current item_code equal to batch quantity
# item_doc.set('qty', batch.qty)
# item_doc = parent_doc.append('items', {
# 'item_code': item_doc.item_code,
# 'qty': required_qty,
# 'warehouse': item_doc.warehouse,
# 'sales_order': item_doc.sales_order,
# 'sales_order_item': item_doc.sales_order_item,
# 'uom': item_doc.uom,
# 'stock_uom': item_doc.stock_uom,
# 'conversion_factor': item_doc.conversion_factor,
# 'stock_qty': qty * item_doc.conversion_factor,
# })
required_qty -= batch.qty
# set quantity of current item equal to batch quantity
item_doc.set('qty', batch.qty)
item_doc = parent_doc.append('items', {
'item': item_doc.item,
'qty': required_qty,
'warehouse': item_doc.warehouse,
'reference_doctype': item_doc.reference_doctype,
'reference_name': item_doc.reference_name,
'reference_document_item': item_doc.reference_document_item,
})
if required_qty:
frappe.msgprint('No batches found for {} qty of {}. Skipping...'.format(required_qty, item_doc.item))
parent_doc.remove(item_doc)
frappe.msgprint('No batches found for {} qty of {}.'.format(required_qty, item_doc.item_code))
return item_locations
@frappe.whitelist()
def make_delivery_note(source_name, target_doc=None):
target_doc = get_mapped_doc("Pick List", source_name, {
"Pick List": {
"doctype": "Delivery Note",
# "validation": {
# "docstatus": ["=", 1]
# }
},
"Pick List Item": {
pick_list = frappe.get_doc('Pick List', source_name)
sales_orders = [d.sales_order for d in pick_list.item_locations]
sales_orders = set(sales_orders)
delivery_note = None
for sales_order in sales_orders:
delivery_note = make_delivery_note_from_sales_order(sales_order,
delivery_note, skip_item_mapping=True)
for location in pick_list.item_locations:
sales_order_item = frappe.get_cached_doc('Sales Order Item', location.sales_order_item)
item_table_mapper = {
"doctype": "Delivery Note Item",
"field_map": {
"item": "item_code",
"reference_docname": "against_sales_order",
"rate": "rate",
"name": "so_detail",
"parent": "against_sales_order",
},
},
}, target_doc)
"condition": lambda doc: abs(doc.delivered_qty) < abs(doc.qty) and doc.delivered_by_supplier!=1
}
return target_doc
dn_item = map_child_doc(sales_order_item, delivery_note, item_table_mapper)
if dn_item:
dn_item.warehouse = location.warehouse
dn_item.qty = location.qty
update_delivery_note_item(sales_order_item, dn_item, delivery_note)
set_delivery_note_missing_values(delivery_note)
return delivery_note
def set_delivery_note_missing_values(target):
target.run_method("set_missing_values")
target.run_method("set_po_nos")
target.run_method("calculate_taxes_and_totals")
def update_delivery_note_item(source, target, delivery_note):
cost_center = frappe.db.get_value("Project", delivery_note.project, "cost_center")
if not cost_center:
cost_center = frappe.db.get_value('Item Default',
fieldname=['buying_cost_center'],
filters={
'parent': source.item_code,
'parenttype': 'Item',
'company': delivery_note.company
})
if not cost_center:
cost_center = frappe.db.get_value('Item Default',
fieldname=['buying_cost_center'],
filters={
'parent': source.item_group,
'parenttype': 'Item Group',
'company': delivery_note.company
})
target.cost_center = cost_center

View File

@ -4,32 +4,29 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item",
"item_code",
"item_name",
"column_break_2",
"description",
"has_batch_no",
"has_serial_no",
"section_break_5",
"warehouse",
"quantity_section",
"qty",
"stock_qty",
"picked_qty",
"column_break_11",
"uom",
"stock_uom",
"conversion_factor",
"serial_no_and_batch_section",
"serial_no",
"column_break_20",
"batch_no",
"reference_section",
"reference_doctype",
"reference_name",
"reference_document_item"
"column_break_15",
"sales_order",
"sales_order_item"
],
"fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item",
"read_only": 1
},
{
"fieldname": "qty",
"fieldtype": "Float",
@ -52,72 +49,31 @@
"read_only": 1
},
{
"fetch_from": "item.item_name",
"fetch_from": "item_code.item_name",
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name",
"read_only": 1
},
{
"fetch_from": "item.description",
"fetch_from": "item_code.description",
"fieldname": "description",
"fieldtype": "Text",
"label": "Description",
"read_only": 1
},
{
"fieldname": "reference_document_item",
"fieldtype": "Data",
"label": "Reference Document Item",
"read_only": 1
},
{
"depends_on": "serial_no",
"fieldname": "serial_no",
"fieldtype": "Small Text",
"label": "Serial No",
"read_only": 1
"label": "Serial No"
},
{
"depends_on": "batch_no",
"fieldname": "batch_no",
"fieldtype": "Link",
"label": "Batch No",
"options": "Batch",
"read_only": 1
},
{
"default": "0",
"fetch_from": "item.has_serial_no",
"fieldname": "has_serial_no",
"fieldtype": "Check",
"label": "Has Serial No",
"read_only": 1
},
{
"default": "0",
"fetch_from": "item.has_batch_no",
"fieldname": "has_batch_no",
"fieldtype": "Check",
"label": "Has Batch No",
"read_only": 1
},
{
"fieldname": "reference_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "reference_doctype",
"fieldtype": "Select",
"label": "Reference Document Type",
"options": "Sales Order\nWork Order",
"read_only": 1
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Document",
"options": "reference_doctype",
"read_only": 1
"options": "Batch"
},
{
"fieldname": "column_break_2",
@ -126,10 +82,80 @@
{
"fieldname": "section_break_5",
"fieldtype": "Section Break"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "column_break_11",
"fieldtype": "Column Break"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM",
"read_only": 1
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor",
"read_only": 1
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Qty as per Stock UOM",
"read_only": 1
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item",
"read_only": 1
},
{
"fieldname": "quantity_section",
"fieldtype": "Section Break",
"label": "Quantity"
},
{
"fieldname": "column_break_15",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order",
"read_only": 1
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"label": "Sales Order Item",
"read_only": 1
},
{
"fieldname": "serial_no_and_batch_section",
"fieldtype": "Section Break",
"label": "Serial No and Batch"
},
{
"fieldname": "column_break_20",
"fieldtype": "Column Break"
}
],
"istable": 1,
"modified": "2019-07-30 23:47:53.566473",
"modified": "2019-08-14 18:41:37.727388",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Item",

View File

@ -4,32 +4,19 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item",
"item_code",
"quantity_section",
"qty",
"reference_doctype",
"reference_name",
"reference_document_item"
"stock_qty",
"column_break_5",
"uom",
"stock_uom",
"conversion_factor",
"reference_section",
"sales_order",
"sales_order_item"
],
"fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fieldname": "reference_doctype",
"fieldtype": "Link",
"label": "Reference Document type",
"options": "DocType"
},
{
"fieldname": "reference_name",
"fieldtype": "Dynamic Link",
"label": "Reference Name",
"options": "reference_doctype"
},
{
"fieldname": "qty",
"fieldtype": "Float",
@ -37,13 +24,62 @@
"label": "Qty"
},
{
"fieldname": "reference_document_item",
"fieldname": "quantity_section",
"fieldtype": "Section Break",
"label": "Quantity"
},
{
"fieldname": "stock_qty",
"fieldtype": "Float",
"label": "Stock Qty"
},
{
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
},
{
"fieldname": "stock_uom",
"fieldtype": "Link",
"label": "Stock UOM",
"options": "UOM"
},
{
"fieldname": "item_code",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Item",
"options": "Item"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"fieldname": "reference_section",
"fieldtype": "Section Break",
"label": "Reference"
},
{
"fieldname": "sales_order",
"fieldtype": "Link",
"label": "Sales Order",
"options": "Sales Order"
},
{
"fieldname": "sales_order_item",
"fieldtype": "Data",
"label": "Reference Document Item"
"label": "Sales Order Item"
},
{
"fieldname": "conversion_factor",
"fieldtype": "Float",
"label": "UOM Conversion Factor"
}
],
"istable": 1,
"modified": "2019-07-30 23:43:30.901151",
"modified": "2019-08-14 18:38:28.867113",
"modified_by": "Administrator",
"module": "Stock",
"name": "Pick List Reference Item",

View File

@ -22,7 +22,7 @@ sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice']
purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice']
@frappe.whitelist()
def get_item_details(args, doc=None):
def get_item_details(args, doc=None, overwrite_warehouse=True):
"""
args = {
"item_code": "",
@ -44,11 +44,15 @@ def get_item_details(args, doc=None):
"set_warehouse": ""
}
"""
args = process_args(args)
print('warehouse', args.warehouse, '========')
item = frappe.get_cached_doc("Item", args.item_code)
validate_item_details(args, item)
out = get_basic_details(args, item)
out = get_basic_details(args, item, overwrite_warehouse)
print('warehouse2', out.warehouse, '========')
get_item_tax_template(args, item, out)
out["item_tax_rate"] = get_item_tax_map(args.company, args.get("item_tax_template") if out.get("item_tax_template") is None \
@ -178,7 +182,7 @@ def validate_item_details(args, item):
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
def get_basic_details(args, item):
def get_basic_details(args, item, overwrite_warehouse=True):
"""
:param args: {
"item_code": "",
@ -225,15 +229,27 @@ def get_basic_details(args, item):
item_group_defaults = get_item_group_defaults(item.name, args.company)
brand_defaults = get_brand_defaults(item.name, args.company)
warehouse = (args.get("set_warehouse") or item_defaults.get("default_warehouse") or
item_group_defaults.get("default_warehouse") or brand_defaults.get("default_warehouse") or args.warehouse)
if overwrite_warehouse or not args.warehouse:
warehouse = (
args.get("set_warehouse") or
item_defaults.get("default_warehouse") or
item_group_defaults.get("default_warehouse") or
brand_defaults.get("default_warehouse") or
args.warehouse
)
if not warehouse:
defaults = frappe.defaults.get_defaults() or {}
if defaults.get("default_warehouse") and frappe.db.exists("Warehouse",
{'name': defaults.default_warehouse, 'company': args.company}):
warehouse_exists = frappe.db.exists("Warehouse", {
'name': defaults.default_warehouse,
'company': args.company
})
if defaults.get("default_warehouse") and warehouse_exists:
warehouse = defaults.default_warehouse
else:
warehouse = args.warehouse
if args.get('doctype') == "Material Request" and not args.get('material_request_type'):
args['material_request_type'] = frappe.db.get_value('Material Request',
args.get('name'), 'material_request_type', cache=True)
@ -784,6 +800,7 @@ def get_projected_qty(item_code, warehouse):
@frappe.whitelist()
def get_bin_details(item_code, warehouse):
print(item_code, warehouse, '---------------------------')
return frappe.db.get_value("Bin", {"item_code": item_code, "warehouse": warehouse},
["projected_qty", "actual_qty", "reserved_qty"], as_dict=True, cache=True) \
or {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}