Files
erpnext/erpnext/stock/get_item_details.py
Sudharsanan11 0d9ace7ab8 fix(stock): show qty (company) and qty (warehouse) in sales transactions
company was passed to get_bin_details only for purchase order, so
company_total_stock was never returned for sales order, quotation,
sales invoice and delivery note and the qty (company) column always
read zero. pass ctx.company for every doctype, which also drops the
dependency on doc being supplied.

on the client, set_actual_qty copied only actual_qty out of the
response, so qty (company) never refreshed on a warehouse change. use
frm.call with child so every bin field is applied, pass
include_child_warehouses to match the server, and include quotation.

(cherry picked from commit ab30bab6cb)
2026-07-13 15:34:09 +00:00

1769 lines
54 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import typing
from functools import WRAPPER_ASSIGNMENTS, wraps
import frappe
from frappe import _, throw
from frappe.model import child_table_fields, default_fields
from frappe.model.document import Document
from frappe.model.meta import get_field_precision
from frappe.model.utils import get_fetch_values
from frappe.query_builder.functions import IfNull, Sum
from frappe.utils import add_days, add_months, cint, cstr, flt, get_link_to_form, getdate, parse_json
import erpnext
from erpnext import get_company_currency
from erpnext.accounts.doctype.pricing_rule.pricing_rule import (
get_pricing_rule_for_item,
set_transaction_type,
)
from erpnext.setup.doctype.brand.brand import get_brand_defaults
from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults
from erpnext.setup.utils import get_exchange_rate
from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_factor
from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no
from erpnext.stock.doctype.price_list.price_list import get_price_list_details
ItemDetails = frappe._dict
ItemDetailsCtx = frappe._dict
ItemPriceCtx = frappe._dict
sales_doctypes = ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice", "POS Invoice"]
purchase_doctypes = [
"Material Request",
"Supplier Quotation",
"Purchase Order",
"Purchase Receipt",
"Purchase Invoice",
]
NOT_APPLICABLE_TAX = "N/A"
def _preprocess_ctx(ctx):
if not ctx.price_list:
ctx.price_list = ctx.selling_price_list or ctx.buying_price_list
if not ctx.item_code and ctx.barcode:
ctx.item_code = get_item_code(barcode=ctx.barcode)
elif not ctx.item_code and ctx.serial_no:
ctx.item_code = get_item_code(serial_no=ctx.serial_no)
set_transaction_type(ctx)
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_item_details(ctx, doc=None, for_validate=False, overwrite_warehouse=True) -> ItemDetails:
"""
ctx = {
"item_code": "",
"warehouse": None,
"customer": "",
"conversion_rate": 1.0,
"selling_price_list": None,
"price_list_currency": None,
"plc_conversion_rate": 1.0,
"doctype": "",
"name": "",
"supplier": None,
"transaction_date": None,
"conversion_rate": 1.0,
"buying_price_list": None,
"is_subcontracted": 0/1,
"ignore_pricing_rule": 0/1
"project": ""
"set_warehouse": ""
}
"""
_preprocess_ctx(ctx)
for_validate = parse_json(for_validate)
overwrite_warehouse = parse_json(overwrite_warehouse)
item = frappe.get_cached_doc("Item", ctx.item_code)
validate_item_details(ctx, item)
if isinstance(doc, str):
doc = json.loads(doc)
if doc:
ctx.transaction_date = doc.get("transaction_date") or doc.get("posting_date")
if doc.get("doctype") == "Purchase Invoice":
ctx.bill_date = doc.get("bill_date")
out: ItemDetails = get_basic_details(ctx, item, overwrite_warehouse)
get_item_tax_template(ctx, item, out)
out.item_tax_rate = get_item_tax_map(
doc=doc or ctx,
tax_template=out.item_tax_template or ctx.item_tax_template,
as_json=True,
)
get_party_item_code(ctx, item, out)
if ctx.doctype in ["Sales Invoice", "Purchase Invoice"]:
get_tax_withholding_category(ctx, item, out)
if ctx.doctype in ["Sales Order", "Quotation"]:
set_valuation_rate(out, ctx)
update_party_blanket_order(ctx, out)
# Never try to find a customer price if customer is set in these Doctype
current_customer = ctx.customer
if ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
ctx.customer = None
out.update(get_price_list_rate(ctx, item))
if (
not out.price_list_rate
and ctx.transaction_type == "selling"
and frappe.get_single_value("Selling Settings", "fallback_to_default_price_list")
):
fallback_args = ctx.copy()
fallback_args.price_list = frappe.get_single_value("Selling Settings", "selling_price_list")
out.update(get_price_list_rate(fallback_args, item))
ctx.customer = current_customer
if ctx.customer and cint(ctx.is_pos):
out.update(get_pos_profile_item_details_(ctx, ctx.company, update_data=True))
if item.is_stock_item:
update_bin_details(ctx, out, doc)
# update ctx with out, if key or value not exists
for key, value in out.items():
if ctx.get(key) is None:
ctx[key] = value
data = get_pricing_rule_for_item(ctx, doc=doc, for_validate=for_validate)
out.update(data)
if (
frappe.get_single_value("Stock Settings", "auto_create_serial_and_batch_bundle_for_outward")
and not ctx.get("serial_and_batch_bundle")
and (ctx.get("use_serial_batch_fields") or ctx.get("doctype") == "POS Invoice")
):
update_stock(ctx, out, doc)
if ctx.transaction_date and item.lead_time_days:
out.schedule_date = out.lead_time_date = add_days(ctx.transaction_date, item.lead_time_days)
if ctx.is_subcontracted:
out.bom = ctx.bom or get_default_bom(ctx.item_code)
get_gross_profit(out)
if ctx.doctype == "Material Request":
out.rate = ctx.rate or out.price_list_rate
out.amount = flt(ctx.qty) * flt(out.rate)
out = remove_standard_fields(out)
return out
def remove_standard_fields(out: ItemDetails):
for key in child_table_fields + default_fields:
out.pop(key, None)
return out
def set_valuation_rate(out: ItemDetails | dict, ctx: ItemDetailsCtx):
if frappe.db.exists("Product Bundle", {"name": ctx.item_code, "disabled": 0}, cache=True):
valuation_rate = 0.0
bundled_items = frappe.get_doc("Product Bundle", ctx.item_code)
for bundle_item in bundled_items.items:
valuation_rate += flt(
get_valuation_rate(bundle_item.item_code, ctx.company, out.get("warehouse")).get(
"valuation_rate"
)
* bundle_item.qty
)
out.update({"valuation_rate": valuation_rate})
else:
out.update(get_valuation_rate(ctx.item_code, ctx.company, out.get("warehouse")))
def update_stock(ctx, out, doc=None):
from erpnext.stock.doctype.batch.batch import get_available_batches
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos_for_outward
if (
(
ctx.get("doctype") in ["Delivery Note", "POS Invoice"]
or (ctx.get("doctype") == "Sales Invoice" and ctx.get("update_stock"))
)
and out.warehouse
and out.stock_qty > 0
):
if doc and isinstance(doc, dict):
doc = frappe._dict(doc)
kwargs = frappe._dict(
{
"item_code": ctx.item_code,
"warehouse": ctx.warehouse,
"based_on": frappe.get_single_value("Stock Settings", "pick_serial_and_batch_based_on"),
"sabb_voucher_no": doc.get("name") if doc else None,
"sabb_voucher_detail_no": ctx.child_docname,
"sabb_voucher_type": ctx.doctype,
"pick_reserved_items": True,
"qty": out.stock_qty,
}
)
if ctx.get("doctype") == "Delivery Note":
kwargs["against_sales_order"] = ctx.get("against_sales_order")
if ctx.get("ignore_serial_nos"):
kwargs["ignore_serial_nos"] = ctx.get("ignore_serial_nos")
qty = out.stock_qty
batches = []
if out.has_batch_no and not ctx.get("batch_no"):
batches = get_available_batches(kwargs)
if doc:
filter_batches(batches, doc)
for batch_no, batch_qty in batches.items():
rate = get_batch_based_item_price(
{"price_list": doc.get("selling_price_list"), "uom": out.uom, "batch_no": batch_no},
out.item_code,
)
if batch_qty >= qty:
out.update({"batch_no": batch_no, "actual_batch_qty": qty})
if rate:
out.update({"rate": rate, "price_list_rate": rate})
break
else:
qty -= batch_qty
out.update({"batch_no": batch_no, "actual_batch_qty": batch_qty})
if rate:
out.update({"rate": rate, "price_list_rate": rate})
if out.has_serial_no and out.has_batch_no and has_incorrect_serial_nos(ctx, out):
kwargs["batches"] = [ctx.get("batch_no")] if ctx.get("batch_no") else [out.get("batch_no")]
serial_nos = get_serial_nos_for_outward(kwargs)
serial_nos = get_filtered_serial_nos(serial_nos, doc)
out["serial_no"] = "\n".join(serial_nos[: cint(out.stock_qty)])
elif out.has_serial_no and not ctx.get("serial_no"):
serial_nos = get_serial_nos_for_outward(kwargs)
serial_nos = get_filtered_serial_nos(serial_nos, doc)
out["serial_no"] = "\n".join(serial_nos[: cint(out.stock_qty)])
def has_incorrect_serial_nos(ctx, out):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
if not ctx.get("serial_no"):
return True
serial_nos = get_serial_nos(ctx.get("serial_no"))
if len(serial_nos) != out.get("stock_qty"):
return True
return False
def filter_batches(batches, doc):
for row in doc.get("items"):
if row.get("batch_no") in batches:
batches[row.get("batch_no")] -= row.get("qty")
if batches[row.get("batch_no")] <= 0:
del batches[row.get("batch_no")]
def get_filtered_serial_nos(serial_nos, doc, table=None):
from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos
if not table:
table = "items"
for row in doc.get(table):
if row.get("serial_no"):
for serial_no in get_serial_nos(row.get("serial_no")):
if serial_no in serial_nos:
serial_nos.remove(serial_no)
return serial_nos
def update_bin_details(ctx: ItemDetailsCtx, out: ItemDetails, doc):
if ctx.doctype == "Material Request" and ctx.material_request_type == "Material Transfer":
out.update(get_bin_details(ctx.item_code, ctx.from_warehouse))
elif out.get("warehouse"):
bin_details = get_bin_details(
ctx.item_code, out.warehouse, ctx.company, include_child_warehouses=True
)
out.update(bin_details)
def get_item_code(barcode=None, serial_no=None):
if barcode:
item_code = frappe.db.get_value("Item Barcode", {"barcode": barcode}, fieldname=["parent"])
if not item_code:
frappe.throw(_("No Item with Barcode {0}").format(barcode))
elif serial_no:
item_code = frappe.db.get_value("Serial No", serial_no, "item_code")
if not item_code:
frappe.throw(_("No Item with Serial No {0}").format(serial_no))
return item_code
def validate_item_details(ctx: ItemDetailsCtx, item):
if not ctx.company:
throw(_("Please specify Company"))
from erpnext.stock.doctype.item.item import validate_end_of_life
validate_end_of_life(item.name, item.end_of_life, item.disabled)
if cint(item.has_variants):
msg = f"Item {item.name} is a template, please select one of its variants"
throw(_(msg), title=_("Template Item Selected"))
elif ctx.doctype != "Material Request":
if ctx.is_subcontracted:
if ctx.is_old_subcontracting_flow:
if item.is_sub_contracted_item != 1:
throw(_("Item {0} must be a Sub-contracted Item").format(item.name))
else:
if item.is_stock_item:
throw(_("Item {0} must be a Non-Stock Item").format(item.name))
def get_basic_details(ctx: ItemDetailsCtx, item, overwrite_warehouse=True) -> ItemDetails:
"""
:param ctx: {
"item_code": "",
"warehouse": None,
"customer": "",
"conversion_rate": 1.0,
"selling_price_list": None,
"price_list_currency": None,
"price_list_uom_dependant": None,
"plc_conversion_rate": 1.0,
"doctype": "",
"name": "",
"supplier": None,
"transaction_date": None,
"conversion_rate": 1.0,
"buying_price_list": None,
"is_subcontracted": 0/1,
"ignore_pricing_rule": 0/1
"project": "",
barcode: "",
serial_no: "",
currency: "",
update_stock: "",
price_list: "",
company: "",
order_type: "",
is_pos: "",
project: "",
qty: "",
stock_qty: "",
conversion_factor: "",
against_blanket_order: 0/1
}
:param item: `item_code` of Item object
:return: frappe._dict
"""
if not item:
item = frappe.get_cached_doc("Item", ctx.item_code)
if item.variant_of and not item.taxes and frappe.db.exists("Item Tax", {"parent": item.variant_of}):
item.update_template_tables()
item_defaults = get_item_defaults(item.name, ctx.company)
item_group_defaults = get_item_group_defaults(item.name, ctx.company)
brand_defaults = get_brand_defaults(item.name, ctx.company)
defaults = frappe._dict(
{
"item_defaults": item_defaults,
"item_group_defaults": item_group_defaults,
"brand_defaults": brand_defaults,
}
)
warehouse = get_item_warehouse_(ctx, item, overwrite_warehouse, defaults)
if ctx.doctype == "Material Request" and not ctx.material_request_type:
ctx["material_request_type"] = frappe.db.get_value(
"Material Request", ctx.name, "material_request_type", cache=True
)
expense_account = None
if item.is_fixed_asset:
from erpnext.assets.doctype.asset.asset import get_asset_account, is_cwip_accounting_enabled
if is_cwip_accounting_enabled(item.asset_category):
expense_account = get_asset_account(
"capital_work_in_progress_account",
asset_category=item.asset_category,
company=ctx.company,
)
elif ctx.doctype in (
"Purchase Invoice",
"Purchase Receipt",
"Purchase Order",
"Material Request",
):
from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account
expense_account = get_asset_category_account(
fieldname="fixed_asset_account", item=ctx.item_code, company=ctx.company
)
# Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master
if not ctx.uom:
if ctx.doctype in sales_doctypes:
ctx.uom = item.sales_uom if item.sales_uom else item.stock_uom
elif (
(ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"])
or (ctx.doctype == "Material Request" and ctx.material_request_type == "Purchase")
or (ctx.doctype == "Supplier Quotation")
):
ctx.uom = item.purchase_uom if item.purchase_uom else item.stock_uom
else:
ctx.uom = item.stock_uom
# Set stock UOM in ctx, so that it can be used while fetching item price
ctx.stock_uom = item.stock_uom
if ctx.batch_no and item.name != frappe.get_cached_value("Batch", ctx.batch_no, "item"):
ctx.batch_no = ""
out = ItemDetails(
{
"item_code": item.name,
"item_name": item.item_name,
"description": cstr(item.description).strip(),
"image": cstr(item.image).strip(),
"warehouse": warehouse,
"income_account": get_default_income_account(
ctx, item_defaults, item_group_defaults, brand_defaults
),
"expense_account": expense_account
or get_default_expense_account(ctx, item_defaults, item_group_defaults, brand_defaults),
"discount_account": get_default_discount_account(
ctx, item_defaults, item_group_defaults, brand_defaults
),
"provisional_expense_account": get_provisional_account(
ctx, item_defaults, item_group_defaults, brand_defaults
),
"cost_center": get_default_cost_center(ctx, item_defaults, item_group_defaults, brand_defaults),
"has_serial_no": item.has_serial_no,
"has_batch_no": item.has_batch_no,
"batch_no": ctx.batch_no,
"uom": ctx.uom,
"stock_uom": item.stock_uom,
"min_order_qty": flt(item.min_order_qty) if ctx.doctype == "Material Request" else "",
"qty": flt(ctx.qty) or 1.0,
"stock_qty": flt(ctx.qty) or 1.0,
"price_list_rate": 0.0,
"base_price_list_rate": 0.0,
"rate": 0.0,
"base_rate": 0.0,
"amount": 0.0,
"base_amount": 0.0,
"net_rate": 0.0,
"net_amount": 0.0,
"discount_percentage": 0.0,
"discount_amount": flt(ctx.discount_amount) or 0.0,
"update_stock": ctx.update_stock if ctx.doctype in ["Sales Invoice", "Purchase Invoice"] else 0,
"delivered_by_supplier": item.delivered_by_supplier
if ctx.doctype in ["Sales Order", "Sales Invoice"]
else 0,
"is_fixed_asset": item.is_fixed_asset,
"last_purchase_rate": item.last_purchase_rate if ctx.doctype in ["Purchase Order"] else 0,
"transaction_date": ctx.transaction_date,
"against_blanket_order": ctx.against_blanket_order,
"bom_no": item.get("default_bom"),
"weight_per_unit": ctx.weight_per_unit or item.get("weight_per_unit"),
"weight_uom": ctx.weight_uom or item.get("weight_uom"),
"grant_commission": item.get("grant_commission"),
}
)
if not item.is_stock_item and not out.expense_account:
out.expense_account = frappe.get_cached_value("Company", ctx.company, "service_expense_account")
default_supplier = get_default_supplier(ctx, item_defaults, item_group_defaults, brand_defaults)
if default_supplier:
out.supplier = default_supplier
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
out.update(calculate_service_end_date(ctx, item))
# calculate conversion factor
if item.stock_uom == ctx.uom:
out.conversion_factor = 1.0
else:
out.conversion_factor = ctx.conversion_factor or get_conversion_factor(item.name, ctx.uom).get(
"conversion_factor"
)
ctx.conversion_factor = out.conversion_factor
out.stock_qty = out.qty * out.conversion_factor
ctx.stock_qty = out.stock_qty
# calculate last purchase rate
if ctx.doctype in purchase_doctypes and not frappe.db.get_single_value(
"Buying Settings", "disable_last_purchase_rate"
):
from erpnext.buying.doctype.purchase_order.purchase_order import item_last_purchase_rate
out.last_purchase_rate = item_last_purchase_rate(
ctx.name, ctx.conversion_rate, item.name, out.conversion_factor
)
expense_account_field = "default_expense_account"
if (
item.is_stock_item
and erpnext.is_perpetual_inventory_enabled(ctx.company)
and (
ctx.doctype == "Purchase Receipt"
or (ctx.doctype == "Purchase Invoice" and ctx.get("update_stock"))
)
):
expense_account_field = "stock_received_but_not_billed"
# if default specified in item is for another company, fetch from company
for d in [
["Account", "income_account", "default_income_account"],
["Account", "expense_account", expense_account_field],
["Cost Center", "cost_center", "cost_center"],
["Warehouse", "warehouse", ""],
]:
if not out[d[1]]:
out[d[1]] = frappe.get_cached_value("Company", ctx.company, d[2]) if d[2] else None
for fieldname in ("item_name", "item_group", "brand", "stock_uom"):
out[fieldname] = item.get(fieldname)
if ctx.manufacturer:
part_no = get_item_manufacturer_part_no(ctx.item_code, ctx.manufacturer)
if part_no:
out.manufacturer_part_no = part_no
else:
out.manufacturer_part_no = None
out.manufacturer = None
else:
data = frappe.get_cached_value(
"Item", item.name, ["default_item_manufacturer", "default_manufacturer_part_no"], as_dict=True
)
if data:
out.update(
{
"manufacturer": data.default_item_manufacturer,
"manufacturer_part_no": data.default_manufacturer_part_no,
}
)
child_doctype = ctx.doctype + " Item"
meta = frappe.get_meta(child_doctype)
if meta.get_field("barcode"):
update_barcode_value(out)
if out.weight_per_unit:
out.total_weight = out.weight_per_unit * out.stock_qty
return out
from erpnext.deprecation_dumpster import get_item_warehouse
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_item_warehouse_(ctx: ItemDetailsCtx, item, overwrite_warehouse, defaults=None):
if not defaults:
defaults = frappe._dict(
{
"item_defaults": get_item_defaults(item.name, ctx.company),
"item_group_defaults": get_item_group_defaults(item.name, ctx.company),
"brand_defaults": get_brand_defaults(item.name, ctx.company),
}
)
if overwrite_warehouse or not ctx.warehouse:
warehouse = (
ctx.set_warehouse
or defaults.item_defaults.get("default_warehouse")
or defaults.item_group_defaults.get("default_warehouse")
or defaults.brand_defaults.get("default_warehouse")
or ctx.warehouse
)
else:
warehouse = ctx.warehouse
if not warehouse:
default_warehouse = frappe.get_single_value("Stock Settings", "default_warehouse")
if (
default_warehouse
and frappe.get_cached_value("Warehouse", default_warehouse, "company") == ctx.company
):
return default_warehouse
return warehouse
def update_barcode_value(out):
barcode_data = get_barcode_data([out])
# If item has one barcode then update the value of the barcode field
if barcode_data and len(barcode_data.get(out.item_code)) == 1:
out["barcode"] = barcode_data.get(out.item_code)[0]
def get_barcode_data(items_list=None, item_code=None):
# get item-wise batch no data
# example: {'LED-GRE': [Batch001, Batch002]}
# where LED-GRE is item code, SN0001 is serial no and Pune is warehouse
itemwise_barcode = {}
if not items_list and item_code:
_dict_item_code = frappe._dict(
{
"item_code": item_code,
}
)
items_list = [frappe._dict(_dict_item_code)]
for item in items_list:
barcodes = frappe.db.get_all("Item Barcode", filters={"parent": item.item_code}, fields="barcode")
for barcode in barcodes:
if item.item_code not in itemwise_barcode:
itemwise_barcode.setdefault(item.item_code, [])
itemwise_barcode[item.item_code].append(barcode.get("barcode"))
return itemwise_barcode
@frappe.whitelist()
def get_item_tax_info(doc, tax_category, item_codes, item_rates=None, item_tax_templates=None):
out = {}
if item_tax_templates is None:
item_tax_templates = {}
if item_rates is None:
item_rates = {}
doc = parse_json(doc)
item_codes = parse_json(item_codes)
item_rates = parse_json(item_rates)
item_tax_templates = parse_json(item_tax_templates)
for item_code in item_codes:
if not item_code or item_code[1] in out or not item_tax_templates.get(item_code[1]):
continue
out[item_code[1]] = ItemDetails()
item = frappe.get_cached_doc("Item", item_code[0])
ctx: ItemDetailsCtx = {
"company": doc.company,
"tax_category": tax_category,
"base_net_rate": item_rates.get(item_code[1]),
}
if item_tax_templates:
ctx.update({"item_tax_template": item_tax_templates.get(item_code[1])})
get_item_tax_template(ctx, item, out[item_code[1]])
out[item_code[1]]["item_tax_rate"] = get_item_tax_map(
doc=doc,
tax_template=out[item_code[1]].get("item_tax_template"),
as_json=True,
)
return out
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_item_tax_template(ctx, item=None, out: ItemDetails | None = None):
"""
Determines item_tax template from item or parent item groups.
Accesses:
ctx = {
"child_doctype": str
}
Passes:
ctx = {
"company": str
"bill_date": str
"transaction_date": str
"tax_category": None
"item_tax_template": None
"base_net_rate": float
}
"""
if not item:
if not ctx.get("item_code"):
frappe.throw(_("Item/Item Code required to get Item Tax Template."))
else:
item = frappe.get_cached_doc("Item", ctx.item_code)
item_tax_template = None
if item.taxes:
item_tax_template = _get_item_tax_template(ctx, item.taxes, out)
if not item_tax_template:
item_tax_template = _get_item_tax_template_from_item_group(ctx, item.item_group, out)
if out and ctx.get("child_doctype") and item_tax_template:
out.update(get_fetch_values(ctx.get("child_doctype"), "item_tax_template", item_tax_template))
return item_tax_template
def _get_item_tax_template_from_item_group(ctx, item_group, out=None):
from frappe.utils.nestedset import get_ancestors_of
ancestors = get_ancestors_of("Item Group", item_group)
for group in [item_group, *ancestors]:
group_doc = frappe.get_cached_doc("Item Group", group)
item_tax_template = _get_item_tax_template(ctx, group_doc.taxes, out)
if item_tax_template:
return item_tax_template
return None
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def _get_item_tax_template(
ctx: ItemDetailsCtx, taxes, out: ItemDetails | None = None, for_validate=False
) -> None | str | list[str]:
"""
Accesses:
ctx = {
"company": str
"bill_date": str
"transaction_date": str
"tax_category": None
"item_tax_template": None
}
Passes:
ctx = {
"base_net_rate": float
}
"""
if out is None:
out = ItemDetails()
taxes_with_validity = []
taxes_with_no_validity = []
for tax in taxes:
disabled, tax_company = frappe.get_cached_value(
"Item Tax Template", tax.item_tax_template, ["disabled", "company"]
)
if not disabled and tax_company == ctx["company"]:
if tax.valid_from or tax.maximum_net_rate:
# In purchase Invoice first preference will be given to supplier invoice date
# if supplier date is not present then posting date
validation_date = (
ctx.get("bill_date") or ctx.get("posting_date") or ctx.get("transaction_date")
)
if getdate(tax.valid_from) <= getdate(validation_date) and is_within_valid_range(ctx, tax):
taxes_with_validity.append(tax)
else:
taxes_with_no_validity.append(tax)
if taxes_with_validity:
taxes = sorted(taxes_with_validity, key=lambda i: i.valid_from or tax.maximum_net_rate, reverse=True)
else:
taxes = taxes_with_no_validity
if for_validate:
return [
tax.item_tax_template
for tax in taxes
if (
cstr(tax.tax_category) == cstr(ctx.get("tax_category"))
and (tax.item_tax_template not in taxes)
)
]
# all templates have validity and no template is valid
if not taxes_with_validity and (not taxes_with_no_validity):
return None
# do not change if already a valid template
if ctx.get("item_tax_template") in {t.item_tax_template for t in taxes}:
out.item_tax_template = ctx.get("item_tax_template")
return ctx.get("item_tax_template")
for tax in taxes:
if cstr(tax.tax_category) == cstr(ctx.get("tax_category")):
out.item_tax_template = tax.item_tax_template
return tax.item_tax_template
return None
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def is_within_valid_range(ctx: ItemDetailsCtx, tax) -> bool:
"""
Accesses:
ctx = {
"base_net_rate": float
}
"""
if not flt(tax.maximum_net_rate):
# No range specified, just ignore
return True
elif flt(tax.minimum_net_rate) <= flt(ctx.get("base_net_rate")) <= flt(tax.maximum_net_rate):
return True
return False
@frappe.whitelist()
def get_item_tax_map(*, doc: str | dict | Document, tax_template: str | None = None, as_json=True):
doc = parse_json(doc)
item_tax_map = {}
for t in (t for t in (doc.get("taxes") or []) if not t.get("set_by_item_tax_template")):
item_tax_map[t.get("account_head")] = t.get("rate")
if tax_template:
template = frappe.get_cached_doc("Item Tax Template", tax_template)
for d in template.taxes:
if frappe.get_cached_value("Account", d.tax_type, "company") == doc.get("company"):
if d.get("not_applicable"):
item_tax_map[d.tax_type] = NOT_APPLICABLE_TAX
else:
item_tax_map[d.tax_type] = d.tax_rate
return json.dumps(item_tax_map) if as_json else item_tax_map
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def calculate_service_end_date(ctx: ItemDetailsCtx, item=None):
_preprocess_ctx(ctx)
if not item:
item = frappe.get_cached_doc("Item", ctx.item_code)
doctype = ctx.parenttype or ctx.doctype
if doctype == "Sales Invoice":
enable_deferred = "enable_deferred_revenue"
no_of_months = "no_of_months"
account = "deferred_revenue_account"
else:
enable_deferred = "enable_deferred_expense"
no_of_months = "no_of_months_exp"
account = "deferred_expense_account"
service_start_date = ctx.service_start_date if ctx.service_start_date else ctx.transaction_date
service_end_date = add_months(service_start_date, item.get(no_of_months))
deferred_detail = {"service_start_date": service_start_date, "service_end_date": service_end_date}
deferred_detail[enable_deferred] = item.get(enable_deferred)
deferred_detail[account] = get_default_deferred_account(ctx, item, fieldname=account)
return deferred_detail
def get_default_income_account(ctx: ItemDetailsCtx, item, item_group, brand):
return (
item.get("income_account")
or item_group.get("income_account")
or brand.get("income_account")
or ctx.income_account
)
def get_default_inventory_account(ctx: ItemDetailsCtx, item, item_group, brand):
if not frappe.get_cached_value("Company", ctx.company, "enable_item_wise_inventory_account"):
return None
return (
ctx.inventory_account
or item.get("default_inventory_account")
or item_group.get("default_inventory_account")
or brand.get("default_inventory_account")
)
def get_default_expense_account(ctx: ItemDetailsCtx, item, item_group, brand):
if ctx.get("doctype") in ["Sales Invoice", "Delivery Note"]:
expense_account = (
item.get("default_cogs_account")
or item_group.get("default_cogs_account")
or brand.get("default_cogs_account")
)
if not expense_account:
expense_account = frappe.get_cached_value("Company", ctx.company, "default_expense_account")
if expense_account:
return expense_account
return (
item.get("expense_account")
or item_group.get("expense_account")
or brand.get("expense_account")
or ctx.expense_account
)
def get_provisional_account(ctx: ItemDetailsCtx, item, item_group, brand):
return (
item.get("default_provisional_account")
or item_group.get("default_provisional_account")
or brand.get("default_provisional_account")
or ctx.default_provisional_account
)
def get_default_discount_account(ctx: ItemDetailsCtx, item, item_group, brand):
return (
item.get("default_discount_account")
or item_group.get("default_discount_account")
or brand.get("default_discount_account")
or ctx.discount_account
)
def get_default_deferred_account(ctx: ItemDetailsCtx, item, fieldname=None):
if item.get("enable_deferred_revenue") or item.get("enable_deferred_expense"):
return (
frappe.get_cached_value(
"Item Default",
{"parent": ctx.item_code, "company": ctx.company},
fieldname,
)
or ctx.get(fieldname)
or frappe.get_cached_value("Company", ctx.company, "default_" + fieldname)
)
else:
return None
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_default_cost_center(ctx: ItemDetailsCtx, item=None, item_group=None, brand=None, company=None):
cost_center = None
if not company and ctx.get("company"):
company = ctx.get("company")
if ctx.get("project"):
cost_center = frappe.db.get_value("Project", ctx.get("project"), "cost_center", cache=True)
if not cost_center and (item and item_group and brand):
if ctx.get("customer"):
cost_center = (
item.get("selling_cost_center")
or item_group.get("selling_cost_center")
or brand.get("selling_cost_center")
)
else:
cost_center = (
item.get("buying_cost_center")
or item_group.get("buying_cost_center")
or brand.get("buying_cost_center")
)
elif not cost_center and ctx.get("item_code") and company:
for method in ["get_item_defaults", "get_item_group_defaults", "get_brand_defaults"]:
path = f"erpnext.stock.get_item_details.{method}"
data = frappe.get_attr(path)(ctx.get("item_code"), company)
if data and (data.selling_cost_center or data.buying_cost_center):
if ctx.get("customer") and data.selling_cost_center:
return data.selling_cost_center
elif ctx.get("supplier") and data.buying_cost_center:
return data.buying_cost_center
return data.selling_cost_center or data.buying_cost_center
if not cost_center and ctx.get("cost_center"):
cost_center = ctx.get("cost_center")
if company and cost_center and frappe.get_cached_value("Cost Center", cost_center, "company") != company:
return None
if not cost_center and company:
cost_center = frappe.get_cached_value("Company", company, "cost_center")
return cost_center
def get_default_supplier(_ctx: ItemDetailsCtx, item, item_group, brand):
return item.get("default_supplier") or item_group.get("default_supplier") or brand.get("default_supplier")
def get_price_list_rate(ctx: ItemDetailsCtx, item_doc, out: ItemDetails = None):
if out is None:
out = ItemDetails()
meta = frappe.get_meta(ctx.parenttype or ctx.doctype)
if meta.get_field("currency") or ctx.get("currency"):
if not ctx.price_list_currency or not ctx.plc_conversion_rate:
# if currency and plc_conversion_rate exist then
# `get_price_list_currency_and_exchange_rate` has already been called
pl_details = get_price_list_currency_and_exchange_rate(ctx)
ctx.update(pl_details)
if meta.get_field("currency"):
validate_conversion_rate(ctx, meta)
price_list_rate = get_price_list_rate_for(ctx, item_doc.name)
# variant
if price_list_rate is None and item_doc.variant_of:
price_list_rate = get_price_list_rate_for(ctx, item_doc.variant_of)
# insert in database
if price_list_rate is None or frappe.get_cached_value(
"Stock Settings", "Stock Settings", "update_existing_price_list_rate"
):
insert_item_price(ctx)
if price_list_rate is None:
return out
out.price_list_rate = flt(price_list_rate) * flt(ctx.plc_conversion_rate) / flt(ctx.conversion_rate)
if frappe.db.get_single_value("Buying Settings", "disable_last_purchase_rate"):
return out
if not ctx.is_internal_supplier and not out.price_list_rate and ctx.transaction_type == "buying":
from erpnext.stock.doctype.item.item import get_last_purchase_details
out.update(get_last_purchase_details(item_doc.name, ctx.name, ctx.conversion_rate))
return out
def insert_item_price(ctx: ItemDetailsCtx):
"""Insert Item Price if Price List and Price List Rate are specified and currency is the same"""
if not ctx.price_list or not ctx.rate or ctx.is_internal_supplier or ctx.is_internal_customer:
return
stock_settings = frappe.get_cached_doc("Stock Settings")
if (
not frappe.db.get_value("Price List", ctx.price_list, "currency", cache=True) == ctx.currency
or not stock_settings.auto_insert_price_list_rate_if_missing
or not frappe.has_permission("Item Price", "write")
):
return
transaction_date = (
getdate(ctx.get("posting_date") or ctx.get("transaction_date") or ctx.get("posting_datetime"))
or getdate()
)
item_prices = frappe.get_all(
"Item Price",
filters={
"item_code": ctx.item_code,
"price_list": ctx.price_list,
"currency": ctx.currency,
"uom": ctx.stock_uom,
},
fields=["name", "price_list_rate", "valid_from", "valid_upto"],
order_by="valid_from desc, creation desc",
)
item_price = next(
(
row
for row in item_prices
if (not row.valid_from or getdate(row.valid_from) <= transaction_date)
and (not row.valid_upto or getdate(row.valid_upto) >= transaction_date)
),
item_prices[0] if item_prices else None,
)
update_based_on_price_list_rate = stock_settings.update_price_list_based_on == "Price List Rate"
if item_price and item_price.name:
if not stock_settings.update_existing_price_list_rate:
return
rate_to_consider = flt(ctx.price_list_rate) if update_based_on_price_list_rate else flt(ctx.rate)
price_list_rate = _get_stock_uom_rate(rate_to_consider, ctx)
if not price_list_rate or item_price.price_list_rate == price_list_rate:
return
is_price_valid_for_transaction = (
not item_price.valid_from or getdate(item_price.valid_from) <= transaction_date
) and (not item_price.valid_upto or getdate(item_price.valid_upto) >= transaction_date)
if is_price_valid_for_transaction:
frappe.db.set_value("Item Price", item_price.name, "price_list_rate", price_list_rate)
frappe.msgprint(
_("Item Price updated for {0} in Price List {1}").format(ctx.item_code, ctx.price_list),
alert=True,
)
else:
# if price is not valid for the transaction date, insert a new price list rate with updated price and future validity
item_price = frappe.new_doc(
"Item Price",
item_code=ctx.item_code,
price_list_rate=price_list_rate,
currency=ctx.currency,
uom=ctx.stock_uom,
price_list=ctx.price_list,
valid_from=transaction_date,
)
item_price.insert()
frappe.msgprint(
_("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", ctx.item_code), ctx.price_list
),
alert=True,
)
else:
rate_to_consider = (
(flt(ctx.price_list_rate) or flt(ctx.rate)) if update_based_on_price_list_rate else flt(ctx.rate)
)
price_list_rate = _get_stock_uom_rate(rate_to_consider, ctx)
item_price = frappe.get_doc(
{
"doctype": "Item Price",
"price_list": ctx.price_list,
"item_code": ctx.item_code,
"currency": ctx.currency,
"price_list_rate": price_list_rate,
"uom": ctx.stock_uom,
"valid_from": transaction_date,
}
)
item_price.insert()
frappe.msgprint(
_("Item Price added for {0} in Price List - {1}").format(
get_link_to_form("Item", ctx.item_code), ctx.price_list
),
alert=True,
)
def _get_stock_uom_rate(rate: float, ctx: ItemDetailsCtx):
return rate / ctx.conversion_factor if ctx.conversion_factor else rate
def get_item_price(
pctx: ItemPriceCtx | dict, item_code, ignore_party=False, force_batch_no=False
) -> list[dict]:
"""
Get name, price_list_rate from Item Price based on conditions
Check if the desired qty is within the increment of the packing list.
:param pctx: dict (or frappe._dict) with mandatory fields price_list, uom
optional fields transaction_date, customer, supplier
:param item_code: str, Item Doctype field item_code
"""
pctx: ItemPriceCtx = frappe._dict(pctx)
ip = frappe.qb.DocType("Item Price")
query = (
frappe.qb.from_(ip)
.select(ip.name, ip.price_list_rate, ip.uom)
.where(
(ip.item_code == item_code)
& (ip.price_list == pctx.price_list)
& (IfNull(ip.uom, "").isin(["", pctx.uom]))
)
.orderby(ip.valid_from, order=frappe.qb.desc)
.orderby(IfNull(ip.batch_no, ""), order=frappe.qb.desc)
.orderby(ip.uom, order=frappe.qb.desc)
.limit(1)
)
if force_batch_no:
query = query.where(ip.batch_no == pctx.batch_no)
else:
query = query.where(IfNull(ip.batch_no, "").isin(["", pctx.batch_no]))
if not ignore_party:
if pctx.customer:
query = query.where(
(ip.customer == pctx.customer)
| ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
).orderby(IfNull(ip.customer, ""), order=frappe.qb.desc)
elif pctx.supplier:
query = query.where(
(ip.supplier == pctx.supplier)
| ((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
).orderby(IfNull(ip.supplier, ""), order=frappe.qb.desc)
else:
query = query.where((IfNull(ip.customer, "") == "") & (IfNull(ip.supplier, "") == ""))
if pctx.transaction_date:
query = query.where(
(IfNull(ip.valid_from, "2000-01-01") <= pctx.transaction_date)
& (IfNull(ip.valid_upto, "2500-12-31") >= pctx.transaction_date)
)
return query.run(as_dict=True)
@frappe.whitelist()
def get_batch_based_item_price(pctx: ItemPriceCtx | dict | str, item_code) -> float:
pctx = parse_json(pctx)
item_price = get_item_price(pctx, item_code, force_batch_no=True)
if not item_price:
item_price = get_item_price(pctx, item_code, ignore_party=True, force_batch_no=True)
is_free_item = pctx.get("items", [{}])[0].get("is_free_item")
if item_price and item_price[0].uom == pctx.uom and not is_free_item:
return item_price[0].price_list_rate
return 0.0
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_price_list_rate_for(ctx: ItemDetailsCtx, item_code):
"""
:param customer: link to Customer DocType
:param supplier: link to Supplier DocType
:param price_list: str (Standard Buying or Standard Selling)
:param item_code: str, Item Doctype field item_code
:param qty: Desired Qty
:param transaction_date: Date of the price
"""
pctx = ItemPriceCtx(
{
"item_code": item_code,
"price_list": ctx.get("price_list"),
"customer": ctx.get("customer"),
"supplier": ctx.get("supplier"),
"uom": ctx.get("uom"),
"transaction_date": ctx.get("transaction_date"),
"batch_no": ctx.get("batch_no"),
}
)
item_price_data = 0
price_list_rate = get_item_price(pctx, item_code)
if price_list_rate:
desired_qty = ctx.get("qty")
if desired_qty and check_packing_list(price_list_rate[0].name, desired_qty, item_code):
item_price_data = price_list_rate
else:
general_price_list_rate = get_item_price(pctx, item_code, ignore_party=ctx.get("ignore_party"))
if not general_price_list_rate and ctx.get("uom") != ctx.get("stock_uom"):
pctx.uom = ctx.get("stock_uom")
general_price_list_rate = get_item_price(pctx, item_code, ignore_party=ctx.get("ignore_party"))
if general_price_list_rate:
item_price_data = general_price_list_rate
if item_price_data:
if item_price_data[0].uom == ctx.get("uom"):
return item_price_data[0].price_list_rate
elif not ctx.get("price_list_uom_dependant"):
return flt(item_price_data[0].price_list_rate * flt(ctx.get("conversion_factor", 1)))
else:
return item_price_data[0].price_list_rate
def check_packing_list(price_list_rate_name, desired_qty, item_code):
"""
Check if the desired qty is within the increment of the packing list.
:param price_list_rate_name: Name of Item Price
:param desired_qty: Desired Qt
:param item_code: str, Item Doctype field item_code
:param qty: Desired Qt
"""
flag = True
if packing_unit := frappe.db.get_value("Item Price", price_list_rate_name, "packing_unit", cache=True):
packing_increment = desired_qty % packing_unit
if packing_increment != 0:
flag = False
return flag
def validate_conversion_rate(ctx: ItemDetailsCtx, meta):
from erpnext.controllers.accounts_controller import validate_conversion_rate
company_currency = frappe.get_cached_value("Company", ctx.company, "default_currency")
if not ctx.conversion_rate and ctx.currency == company_currency:
ctx.conversion_rate = 1.0
if not ctx.ignore_conversion_rate and ctx.conversion_rate == 1 and ctx.currency != company_currency:
ctx.conversion_rate = (
get_exchange_rate(ctx.currency, company_currency, ctx.transaction_date, "for_buying") or 1.0
)
# validate currency conversion rate
validate_conversion_rate(
ctx.currency, ctx.conversion_rate, meta.get_label("conversion_rate"), ctx.company
)
ctx.conversion_rate = flt(
ctx.conversion_rate,
get_field_precision(meta.get_field("conversion_rate"), frappe._dict({"fields": ctx})),
)
if ctx.price_list:
if not ctx.plc_conversion_rate and ctx.price_list_currency == frappe.db.get_value(
"Price List", ctx.price_list, "currency", cache=True
):
ctx.plc_conversion_rate = 1.0
# validate price list currency conversion rate
if not ctx.price_list_currency:
throw(_("Price List Currency not selected"))
else:
validate_conversion_rate(
ctx.price_list_currency,
ctx.plc_conversion_rate,
meta.get_label("plc_conversion_rate"),
ctx.company,
)
if meta.get_field("plc_conversion_rate"):
ctx.plc_conversion_rate = flt(
ctx.plc_conversion_rate,
get_field_precision(meta.get_field("plc_conversion_rate"), frappe._dict({"fields": ctx})),
)
def get_party_item_code(ctx: ItemDetailsCtx, item_doc, out: ItemDetails):
if ctx.transaction_type == "selling" and ctx.customer:
out.customer_item_code = None
if ctx.quotation_to and ctx.quotation_to != "Customer":
return
customer_item_code = item_doc.get("customer_items", {"customer_name": ctx.customer})
if customer_item_code:
out.customer_item_code = customer_item_code[0].ref_code
else:
customer_group = frappe.get_cached_value("Customer", ctx.customer, "customer_group")
customer_group_item_code = item_doc.get("customer_items", {"customer_group": customer_group})
if customer_group_item_code and not customer_group_item_code[0].customer_name:
out.customer_item_code = customer_group_item_code[0].ref_code
if ctx.transaction_type == "buying" and ctx.supplier:
item_supplier = item_doc.get("supplier_items", {"supplier": ctx.supplier})
out.supplier_part_no = item_supplier[0].supplier_part_no if item_supplier else None
def get_tax_withholding_category(ctx: ItemDetailsCtx, item_doc, out: ItemDetails):
"""
Get tax withholding category for the item based on the transaction type and party.
"""
tax_withholding_category = None
field = (
"sales_tax_withholding_category"
if ctx.transaction_type == "selling"
else "purchase_tax_withholding_category"
)
if item_doc.get(field):
tax_withholding_category = item_doc.get(field)
elif ctx.transaction_type == "buying" and ctx.supplier:
tax_withholding_category = frappe.get_cached_value(
"Supplier", ctx.supplier, "tax_withholding_category"
)
elif ctx.transaction_type == "selling" and ctx.customer:
tax_withholding_category = frappe.get_cached_value(
"Customer", ctx.customer, "tax_withholding_category"
)
out.tax_withholding_category = tax_withholding_category
from erpnext.deprecation_dumpster import get_pos_profile_item_details
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_pos_profile_item_details_(ctx: ItemDetailsCtx, company, pos_profile=None, update_data=False):
res = frappe._dict()
if not frappe.flags.pos_profile and not pos_profile:
pos_profile = frappe.flags.pos_profile = get_pos_profile(company, ctx.pos_profile)
if pos_profile:
for fieldname in ("income_account", "cost_center", "warehouse", "expense_account"):
if (not ctx.get(fieldname) or update_data) and pos_profile.get(fieldname):
res[fieldname] = pos_profile.get(fieldname)
if res.get("warehouse"):
res.actual_qty = get_bin_details(ctx.item_code, res.warehouse, include_child_warehouses=True).get(
"actual_qty"
)
return res
@frappe.whitelist()
def get_pos_profile(company, pos_profile=None, user=None):
if pos_profile:
return frappe.get_cached_doc("POS Profile", pos_profile)
if not user:
user = frappe.session["user"]
pf = frappe.qb.DocType("POS Profile")
pfu = frappe.qb.DocType("POS Profile User")
query = (
frappe.qb.from_(pf)
.left_join(pfu)
.on(pf.name == pfu.parent)
.select(pf.star)
.where((pfu.user == user) & (pfu.default == 1))
)
if company:
query = query.where(pf.company == company)
pos_profile = query.run(as_dict=True)
if not pos_profile and company:
pos_profile = (
frappe.qb.from_(pf)
.left_join(pfu)
.on(pf.name == pfu.parent)
.select(pf.star)
.where((pf.company == company) & (pf.disabled == 0))
).run(as_dict=True)
return pos_profile and pos_profile[0] or None
@frappe.whitelist()
def get_conversion_factor(item_code, uom):
item = frappe.get_cached_value("Item", item_code, ["variant_of", "stock_uom"], as_dict=True)
if not item_code or not item or uom == item.stock_uom:
return {"conversion_factor": 1.0}
item_codes = [item_code]
if item.variant_of:
item_codes.append(item.variant_of)
parent = frappe.qb.DocType("Item")
child = frappe.qb.DocType("UOM Conversion Detail")
query = (
frappe.qb.from_(parent)
.join(child)
.on(parent.name == child.parent)
.select(child.conversion_factor)
.where((parent.name.isin(item_codes)) & (child.uom == uom))
.orderby(parent.has_variants)
.limit(1)
)
conversion_factor = query.run(pluck="conversion_factor")
if not conversion_factor:
conversion_factor = get_uom_conv_factor(uom, item.stock_uom)
else:
conversion_factor = conversion_factor[0]
return {"conversion_factor": conversion_factor or 1.0}
@frappe.whitelist()
def get_projected_qty(item_code, warehouse):
return {
"projected_qty": frappe.db.get_value(
"Bin", {"item_code": item_code, "warehouse": warehouse}, "projected_qty"
)
}
@frappe.whitelist()
def get_bin_details(item_code, warehouse, company=None, include_child_warehouses=False):
bin_details = {"projected_qty": 0, "actual_qty": 0, "reserved_qty": 0}
if warehouse:
from frappe.query_builder.functions import Coalesce, Sum
from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses
warehouses = get_child_warehouses(warehouse) if include_child_warehouses else [warehouse]
bin = frappe.qb.DocType("Bin")
bin_details = (
frappe.qb.from_(bin)
.select(
Coalesce(Sum(bin.projected_qty), 0).as_("projected_qty"),
Coalesce(Sum(bin.actual_qty), 0).as_("actual_qty"),
Coalesce(Sum(bin.reserved_qty), 0).as_("reserved_qty"),
)
.where((bin.item_code == item_code) & (bin.warehouse.isin(warehouses)))
).run(as_dict=True)[0]
if company:
bin_details["company_total_stock"] = get_company_total_stock(item_code, company)
return bin_details
def get_company_total_stock(item_code, company):
bin = frappe.qb.DocType("Bin")
wh = frappe.qb.DocType("Warehouse")
return (
frappe.qb.from_(bin)
.inner_join(wh)
.on(bin.warehouse == wh.name)
.select(Sum(bin.actual_qty))
.where((wh.company == company) & (bin.item_code == item_code))
).run()[0][0]
@frappe.whitelist()
def get_batch_qty(batch_no, warehouse, item_code):
from erpnext.stock.doctype.batch import batch
if batch_no:
return {"actual_batch_qty": batch.get_batch_qty(batch_no, warehouse)}
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def apply_price_list(ctx, as_doc=False, doc=None):
"""Apply pricelist on a document-like dict object and return as
{'parent': dict, 'children': list}
:param ctx: See below
:param as_doc: Updates value in the passed dict
ctx = {
"doctype": "",
"name": "",
"items": [{"doctype": "", "name": "", "item_code": "", "brand": "", "item_group": ""}, ...],
"conversion_rate": 1.0,
"selling_price_list": None,
"price_list_currency": None,
"price_list_uom_dependant": None,
"plc_conversion_rate": 1.0,
"doctype": "",
"name": "",
"supplier": None,
"transaction_date": None,
"conversion_rate": 1.0,
"buying_price_list": None,
"ignore_pricing_rule": 0/1
}
"""
_preprocess_ctx(ctx)
parent = get_price_list_currency_and_exchange_rate(ctx)
ctx.update(parent)
children = []
if "items" in ctx:
item_list = ctx.get("items")
ctx.update(parent)
for item in item_list:
ctx_copy = ItemDetailsCtx(ctx.copy())
ctx_copy.update(item)
item_details = apply_price_list_on_item(ctx_copy, doc=doc)
children.append(item_details)
if as_doc:
ctx.price_list_currency = (parent.price_list_currency,)
ctx.plc_conversion_rate = parent.plc_conversion_rate
if ctx.get("items"):
for i, item in enumerate(ctx.get("items")):
for fieldname in children[i]:
# if the field exists in the original doc
# update the value
if fieldname in item and fieldname not in ("name", "doctype"):
item[fieldname] = children[i][fieldname]
return ctx
else:
return {"parent": parent, "children": children}
def apply_price_list_on_item(ctx, doc=None):
item_doc = frappe.get_cached_doc("Item", ctx.item_code)
item_details = get_price_list_rate(ctx, item_doc)
ctx.conversion_factor = flt(ctx.conversion_factor) or get_conversion_factor(ctx.item_code, ctx.uom).get(
"conversion_factor", 1
)
ctx.stock_qty = flt(ctx.qty) * flt(ctx.conversion_factor)
item_details.update(get_pricing_rule_for_item(ctx, doc=doc))
return item_details
def get_price_list_currency_and_exchange_rate(ctx: ItemDetailsCtx):
if not ctx.price_list:
return {}
if ctx.doctype in ["Quotation", "Sales Order", "Delivery Note", "Sales Invoice"]:
ctx.update({"exchange_rate": "for_selling"})
elif ctx.doctype in ["Purchase Order", "Purchase Receipt", "Purchase Invoice"]:
ctx.update({"exchange_rate": "for_buying"})
price_list_details = get_price_list_details(ctx.price_list)
price_list_currency = price_list_details.get("currency")
price_list_uom_dependant = price_list_details.get("price_list_uom_dependant")
plc_conversion_rate = ctx.plc_conversion_rate
company_currency = get_company_currency(ctx.company)
if (not plc_conversion_rate) or (
price_list_currency and ctx.price_list_currency and price_list_currency != ctx.price_list_currency
):
# cksgb 19/09/2016: added args.transaction_date as posting_date argument for get_exchange_rate
plc_conversion_rate = (
get_exchange_rate(price_list_currency, company_currency, ctx.transaction_date, ctx.exchange_rate)
or plc_conversion_rate
)
return frappe._dict(
{
"price_list_currency": price_list_currency,
"price_list_uom_dependant": price_list_uom_dependant,
"plc_conversion_rate": plc_conversion_rate or 1,
}
)
@frappe.whitelist()
def get_default_bom(item_code=None):
def _get_bom(item):
bom = frappe.get_all("BOM", dict(item=item, is_active=True, is_default=True, docstatus=1), limit=1)
return bom[0].name if bom else None
if not item_code:
return
bom_name = _get_bom(item_code)
template_item = frappe.db.get_value("Item", item_code, "variant_of")
if not bom_name and template_item:
bom_name = _get_bom(template_item)
return bom_name
@frappe.whitelist()
def get_valuation_rate(item_code, company, warehouse=None):
if frappe.get_cached_value("Warehouse", warehouse, "is_group"):
return {"valuation_rate": 0.0}
item = get_item_defaults(item_code, company)
item_group = get_item_group_defaults(item_code, company)
brand = get_brand_defaults(item_code, company)
if item.get("is_stock_item"):
if not warehouse:
warehouse = (
item.get("default_warehouse")
or item_group.get("default_warehouse")
or brand.get("default_warehouse")
)
return frappe.db.get_value(
"Bin", {"item_code": item_code, "warehouse": warehouse}, ["valuation_rate"], as_dict=True
) or {"valuation_rate": item.get("valuation_rate") or 0}
elif not item.get("is_stock_item"):
pi_item = frappe.qb.DocType("Purchase Invoice Item")
valuation_rate = (
frappe.qb.from_(pi_item)
.select(Sum(pi_item.base_net_amount) / Sum(pi_item.qty * pi_item.conversion_factor))
.where((pi_item.docstatus == 1) & (pi_item.item_code == item_code))
).run()
if valuation_rate:
return {"valuation_rate": valuation_rate[0][0] or 0.0}
else:
return {"valuation_rate": 0.0}
def get_gross_profit(out: ItemDetails):
if out.valuation_rate:
out.update({"gross_profit": ((out.base_rate - out.valuation_rate) * out.stock_qty)})
return out
@frappe.whitelist()
def get_serial_no(_args, serial_nos=None, sales_order=None):
serial_nos = serial_nos or []
return serial_nos
def update_party_blanket_order(ctx: ItemDetailsCtx, out: ItemDetails | dict):
if out["against_blanket_order"]:
blanket_order_details = get_blanket_order_details(ctx)
if blanket_order_details:
out.update(blanket_order_details)
@frappe.whitelist()
@erpnext.normalize_ctx_input(ItemDetailsCtx)
def get_blanket_order_details(ctx: ItemDetailsCtx):
blanket_order_details = None
if ctx.item_code:
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
query = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo_item.rate.as_("blanket_order_rate"), bo.name.as_("blanket_order"))
.where(
(bo.company == ctx.company)
& (bo_item.item_code == ctx.item_code)
& (bo.docstatus == 1)
& (bo.name == bo_item.parent)
)
)
if ctx.customer and ctx.doctype == "Sales Order":
query = query.where(bo.customer == ctx.customer)
elif ctx.supplier and ctx.doctype == "Purchase Order":
query = query.where(bo.supplier == ctx.supplier)
if ctx.blanket_order:
query = query.where(bo.name == ctx.blanket_order)
if ctx.transaction_date:
query = query.where(bo.to_date >= ctx.transaction_date)
blanket_order_details = query.run(as_dict=True)
blanket_order_details = blanket_order_details[0] if blanket_order_details else ""
return blanket_order_details