Files
erpnext/erpnext/controllers/queries.py
Mihir Kandoi 88b8ce3888 fix: enforce company restrictions at transaction level
Restrict to Companies only filtered list views and document reads, and
only for users with Company user permissions. Any user could still use
a master restricted to Company A in a Company B transaction, and users
without Company user permissions bypassed the feature entirely.

Validate on save of transactions that every linked Item, Customer and
Supplier allows the transaction company, and filter item link queries
by the transaction company so restricted items don't show up in the
item selector.
2026-07-22 14:13:22 +05:30

1191 lines
33 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
from collections import OrderedDict, defaultdict
import frappe
from frappe import qb, scrub
from frappe.permissions import has_permission
from frappe.query_builder import Case, Criterion, DocType
from frappe.query_builder.functions import (
Cast_,
Concat,
IfNull,
Length,
Locate,
Lower,
Round,
Substring,
Sum,
)
from frappe.utils import nowdate, today, unique
from pypika import Order
import erpnext
from erpnext.accounts.utils import build_qb_match_conditions
from erpnext.stock.doctype.company_restriction.company_restriction import get_restriction_criterion
from erpnext.stock.get_item_details import _get_item_tax_template
from erpnext.stock.utils import get_combine_datetime
from erpnext.utilities.query import get_filter_conditions_qb
# searches for active employees
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def employee_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict | str | None = None,
reference_doctype: str | None = None,
ignore_user_permissions: bool = False,
):
doctype = "Employee"
fields = get_fields(doctype, ["name", "employee_name"])
ignore_permissions = False
if reference_doctype and ignore_user_permissions:
ignore_permissions = has_ignored_field(reference_doctype, doctype) and has_permission(
doctype,
ptype="select" if frappe.only_has_select_perm(doctype) else "read",
)
Employee = frappe.qb.DocType("Employee")
search_str = f"%{txt}%"
txt_no_percent = txt.replace("%", "")
search_fields = list(dict.fromkeys([searchfield, *fields]))
search_conditions = [Employee[field].like(search_str) for field in search_fields]
query = frappe.qb.get_query(
"Employee",
fields=fields,
filters=filters,
ignore_permissions=ignore_permissions,
)
query = (
query.where(Employee.status.isin(["Active", "Suspended"]))
.where(Employee.docstatus < 2)
.where(Criterion.any(search_conditions))
.orderby(
Case()
.when(Locate(txt_no_percent, Employee.name) > 0, Locate(txt_no_percent, Employee.name))
.else_(99999)
)
.orderby(
Case()
.when(
Locate(txt_no_percent, Employee.employee_name) > 0,
Locate(txt_no_percent, Employee.employee_name),
)
.else_(99999)
)
.orderby(Employee.idx, order=Order.desc)
.orderby(Employee.name)
.orderby(Employee.employee_name)
.limit(page_len)
.offset(start)
)
return query.run()
def has_ignored_field(reference_doctype, doctype):
meta = frappe.get_meta(reference_doctype)
for field in meta.fields:
if not field.ignore_user_permissions:
continue
if field.fieldtype == "Link" and field.options == doctype:
return True
elif field.fieldtype == "Dynamic Link":
options = meta.get_link_doctype(field.fieldname)
if not options:
continue
if isinstance(options, str):
options = options.split("\n")
if doctype in options or "DocType" in options:
return True
return False
# searches for leads which are not converted
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def lead_query(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None
):
doctype = "Lead"
fields = get_fields(doctype, ["name", "lead_name", "company_name"])
Lead = frappe.qb.DocType("Lead")
search_str = f"%{txt}%"
txt_no_percent = txt.replace("%", "")
searchfields = frappe.get_meta(doctype).get_search_fields()
search_fields = list(dict.fromkeys([searchfield, "lead_name", "company_name", *searchfields]))
search_conditions = [Lead[field].like(search_str) for field in search_fields]
query = frappe.qb.get_query("Lead", fields=fields, filters=filters, ignore_permissions=False)
query = (
query.where(Lead.docstatus < 2)
.where(Lead.status.isnull() | (Lead.status != "Converted"))
.where(Criterion.any(search_conditions))
.orderby(
Case().when(Locate(txt_no_percent, Lead.name) > 0, Locate(txt_no_percent, Lead.name)).else_(99999)
)
.orderby(
Case()
.when(Locate(txt_no_percent, Lead.lead_name) > 0, Locate(txt_no_percent, Lead.lead_name))
.else_(99999)
)
.orderby(
Case()
.when(Locate(txt_no_percent, Lead.company_name) > 0, Locate(txt_no_percent, Lead.company_name))
.else_(99999)
)
.orderby(Lead.idx, order=Order.desc)
.orderby(Lead.name)
.orderby(Lead.lead_name)
.limit(page_len)
.offset(start)
)
return query.run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def tax_account_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
company_currency = erpnext.get_company_currency(filters.get("company"))
Account = frappe.qb.DocType("Account")
def get_accounts(with_account_type_filter):
query = frappe.qb.get_query("Account", fields=["name", "parent_account"], ignore_permissions=False)
query = (
query.where(Account.docstatus != 2)
.where(Account.is_group == 0)
.where(Account.company == filters.get("company"))
.where(Account.disabled == filters.get("disabled", 0))
.where(
(Account.account_currency == company_currency)
| Account.account_currency.isnull()
| (Account.account_currency == "")
)
.where(Account[searchfield].like(f"%{txt}%"))
)
if with_account_type_filter:
query = query.where(Account.account_type.isin(filters.get("account_type")))
query = (
query.orderby(Account.idx, order=Order.desc).orderby(Account.name).limit(page_len).offset(start)
)
return query.run()
tax_accounts = get_accounts(True)
if not tax_accounts:
tax_accounts = get_accounts(False)
return tax_accounts
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_query(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict | str | None = None,
as_dict: bool = False,
):
"""
Fetch items for link fields
"""
doctype = "Item"
filters = frappe.parse_json(filters)
company = filters.pop("company", None) if isinstance(filters, dict) else None
if filters and isinstance(filters, dict):
if filters.get("customer") or filters.get("supplier"):
party_type = "Customer" if filters.get("customer") else "Supplier"
party = filters.get("customer") or filters.get("supplier")
group = "Customer Group" if filters.get("customer") else "Supplier Group"
item_rules_list = frappe.get_all(
"Party Specific Item",
filters={"party_type": party_type},
fields=["party", "restrict_based_on", "based_on_value"],
)
party_group_rules_list = frappe.get_all(
"Party Specific Item",
filters={"party_type": group},
fields=["party as party_group", "restrict_based_on", "based_on_value"],
)
current_party_group = frappe.get_value(party_type, party, frappe.scrub(group))
restricted_items = defaultdict(set)
allowed_items = defaultdict(set)
for rule in item_rules_list:
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
if rule.party == party:
allowed_items[restrict_based_on].add(rule.based_on_value)
else:
restricted_items[restrict_based_on].add(rule.based_on_value)
for rule in party_group_rules_list:
restrict_based_on = "name" if rule.restrict_based_on == "Item" else rule.restrict_based_on
if current_party_group == rule.party_group:
allowed_items[restrict_based_on].add(rule.based_on_value)
else:
restricted_items[restrict_based_on].add(rule.based_on_value)
for field, restricted_values in restricted_items.items():
values_to_exclude = restricted_values - allowed_items[field]
if values_to_exclude:
filters[scrub(field)] = ["not in", list(values_to_exclude)]
if filters.get("customer"):
del filters["customer"]
else:
del filters["supplier"]
else:
filters.pop("customer", None)
filters.pop("supplier", None)
item = DocType(doctype)
# Condition for the date
eol = item.end_of_life
date_conditions = [eol > nowdate(), eol.isnull()]
# Add the condition if the db can evaluate it
if frappe.db.db_type not in ["postgres"]:
date_conditions.append(eol == "0000-00-00")
date_condition = Criterion.any(date_conditions)
# Condition for the searchfields
meta = frappe.get_meta("Item", cached=True)
searchfields = meta.get_search_fields()
query_select = []
extra_searchfields = [field for field in searchfields if field not in ["name", "description"]]
for field in extra_searchfields:
query_select.append(item[field])
if "description" in searchfields:
description_col = (
Case()
.when(Length(item.description) > 40, Concat(Substring(item.description, 1, 40), "..."))
.else_(item.description)
).as_("description")
query_select.append(description_col)
fields_to_process = list(
dict.fromkeys(
searchfields
+ [
field
for field in [
searchfield or "name",
"item_code",
"item_group",
"item_name",
]
if field not in searchfields
]
)
)
db_fields = [f.fieldname for f in meta.fields] + ["name"]
search_str = f"%{txt}%"
search_conditions = []
for fieldname in fields_to_process:
if fieldname in db_fields:
search_conditions.append(item[fieldname].like(search_str))
barcode_tbl = DocType("Item Barcode")
barcode_subquery = (
frappe.qb.from_(barcode_tbl).select(barcode_tbl.parent).where(barcode_tbl.barcode.like(search_str))
)
search_conditions.append(item.item_code.isin(barcode_subquery))
# Condition for the description
if frappe.db.estimate_count("Item") < 50000 and "description" not in fields_to_process:
search_conditions.append(item.description.like(search_str))
txt_no_percent = txt.replace("%", "")
# Building the query
query = (
frappe.get_query(doctype, filters=filters, ignore_permissions=False)
.select(*query_select)
.where(item.docstatus < 2)
.where(item.disabled == 0)
.where(item.has_variants == 0)
.where(date_condition)
.where(Criterion.any(search_conditions))
.orderby(
Case()
.when(
Locate(Lower(txt_no_percent), Lower(item.name)) > 0,
Locate(Lower(txt_no_percent), Lower(item.name)),
)
.else_(99999)
)
.orderby(
Case()
.when(
Locate(Lower(txt_no_percent), Lower(item.item_name)) > 0,
Locate(Lower(txt_no_percent), Lower(item.item_name)),
)
.else_(99999)
)
.orderby(item.idx, order=Order.desc)
.orderby(item.name)
.orderby(item.item_name)
.limit(page_len)
.offset(start)
)
if company:
query = query.where(get_restriction_criterion("Item", [company]))
return query.run(as_dict=as_dict)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def bom(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | str | None = None
):
doctype = "BOM"
fields = get_fields(doctype, ["name", "item"])
BOM = frappe.qb.DocType("BOM")
txt_no_percent = txt.replace("%", "")
query = frappe.qb.get_query("BOM", fields=fields, filters=filters, ignore_permissions=False)
query = (
query.where(BOM.docstatus == 1)
.where(BOM.is_active == 1)
.where(BOM[searchfield].like(f"%{txt}%"))
.orderby(
Case().when(Locate(txt_no_percent, BOM.name) > 0, Locate(txt_no_percent, BOM.name)).else_(99999)
)
.orderby(BOM.idx, order=Order.desc)
.orderby(BOM.name)
.limit(page_len or 20)
.offset(start or 0)
)
return query.run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_project_name(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | None = None
):
proj = qb.DocType("Project")
qb_filter_and_conditions = []
qb_filter_or_conditions = []
if filters:
if filters.get("customer"):
qb_filter_and_conditions.append(
(proj.customer == filters.get("customer")) | (proj.customer.isnull()) | (proj.customer == "")
)
if filters.get("company"):
qb_filter_and_conditions.append(proj.company == filters.get("company"))
qb_filter_and_conditions.append(proj.status.notin(["Completed", "Cancelled", "On hold"]))
q = qb.from_(proj)
fields = get_fields(doctype, ["name", "project_name"])
for x in fields:
q = q.select(proj[x])
# don't consider 'customer' and 'status' fields for pattern search, as they must be exactly matched
searchfields = [
x for x in frappe.get_meta(doctype).get_search_fields() if x not in ["customer", "status"]
]
# pattern search
if txt:
for x in searchfields:
qb_filter_or_conditions.append(proj[x].like(f"%{txt}%"))
q = q.where(Criterion.all(qb_filter_and_conditions)).where(Criterion.any(qb_filter_or_conditions))
# ordering
if txt:
# project_name containing search string 'txt' will be given higher precedence
q = q.orderby(
Case()
.when(
Locate(Lower(txt), Lower(proj.project_name)) > 0,
Locate(Lower(txt), Lower(proj.project_name)),
)
.else_(99999)
)
q = q.orderby(proj.idx, order=Order.desc).orderby(proj.name)
if page_len:
q = q.limit(page_len)
if start:
q = q.offset(start)
return q.run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_delivery_notes_to_be_billed(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict, as_dict: bool = False
):
DeliveryNote = frappe.qb.DocType("Delivery Note")
fields = get_fields(doctype, ["name", "customer", "posting_date"])
original_dn = (
frappe.qb.from_(DeliveryNote)
.select(DeliveryNote.name)
.where((DeliveryNote.docstatus == 1) & (DeliveryNote.is_return == 0) & (DeliveryNote.per_billed > 0))
)
query = frappe.qb.get_query(
"Delivery Note",
fields=fields,
filters=filters,
ignore_permissions=False,
)
query = (
query.where(
(DeliveryNote.docstatus == 1)
& (DeliveryNote.status.notin(["Stopped", "Closed"]))
& (DeliveryNote[searchfield].like(f"%{txt}%"))
& (
((DeliveryNote.is_return == 0) & (DeliveryNote.per_billed < 100))
| ((DeliveryNote.grand_total == 0) & (DeliveryNote.per_billed < 100))
| (
(DeliveryNote.is_return == 1)
& (DeliveryNote.per_billed < 100)
& (DeliveryNote.return_against.isin(original_dn))
)
)
)
.orderby(DeliveryNote[searchfield], order=Order.asc)
.limit(page_len)
.offset(start)
)
return query.run(as_dict=as_dict)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_batch_no(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
doctype = "Batch"
meta = frappe.get_meta(doctype, cached=True)
searchfields = meta.get_search_fields()
page_len = 300
batches = get_batches_from_stock_ledger_entries(searchfields, txt, filters, start, page_len)
batches.extend(get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start, page_len))
filtered_batches = get_filterd_batches(batches)
if filters.get("is_inward"):
filtered_batches.extend(get_empty_batches(filters, start, page_len, filtered_batches, txt))
return filtered_batches
def get_empty_batches(filters, start, page_len, filtered_batches=None, txt=None):
query_filter = {"item": filters.get("item_code"), "disabled": 0}
if txt:
query_filter["name"] = ("like", f"%{txt}%")
exclude_batches = [batch[0] for batch in filtered_batches] if filtered_batches else []
if exclude_batches:
query_filter["name"] = ("not in", exclude_batches)
return frappe.get_all(
"Batch",
fields=["name", "batch_qty"],
filters=query_filter,
limit_start=start,
limit_page_length=page_len,
as_list=1,
)
def get_filterd_batches(data):
batches = OrderedDict()
for batch_data in data:
if batch_data[0] not in batches:
batches[batch_data[0]] = list(batch_data)
else:
batches[batch_data[0]][1] += batch_data[1]
filterd_batch = []
for _batch, batch_data in batches.items():
if batch_data[1] > 0:
filterd_batch.append(tuple(batch_data))
return filterd_batch
def get_batches_from_stock_ledger_entries(searchfields, txt, filters, start=0, page_len=100):
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_table = frappe.qb.DocType("Batch")
expiry_date = filters.get("posting_date") or today()
query = (
frappe.qb.from_(stock_ledger_entry)
.inner_join(batch_table)
.on(batch_table.name == stock_ledger_entry.batch_no)
.select(
stock_ledger_entry.batch_no,
Sum(stock_ledger_entry.actual_qty).as_("qty"),
)
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
& (batch_table.disabled == 0)
& (stock_ledger_entry.batch_no.isnotnull())
)
.groupby(stock_ledger_entry.batch_no, stock_ledger_entry.warehouse, batch_table.name)
.having(Sum(stock_ledger_entry.actual_qty) != 0)
.offset(start)
.limit(page_len)
)
if not filters.get("is_inward"):
if filters.get("posting_date") and filters.get("posting_time"):
query = query.where(
stock_ledger_entry.posting_datetime
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
)
if not filters.get("include_expired_batches"):
query = query.where((batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull()))
query = query.select(
Case()
.when(batch_table.manufacturing_date.isnotnull(), Concat("MFG-", batch_table.manufacturing_date))
.as_("manufacturing_date"),
Case()
.when(batch_table.expiry_date.isnotnull(), Concat("EXP-", batch_table.expiry_date))
.as_("expiry_date"),
)
if filters.get("warehouse"):
query = query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
for field in searchfields:
query = query.select(batch_table[field])
if txt:
txt_condition = batch_table.name.like(f"%{txt}%")
for field in [*searchfields, "name"]:
txt_condition |= batch_table[field].like(f"%{txt}%")
query = query.where(txt_condition)
return query.run(as_list=1) or []
def get_batches_from_serial_and_batch_bundle(searchfields, txt, filters, start=0, page_len=100):
bundle = frappe.qb.DocType("Serial and Batch Entry")
stock_ledger_entry = frappe.qb.DocType("Stock Ledger Entry")
batch_table = frappe.qb.DocType("Batch")
expiry_date = filters.get("posting_date") or today()
bundle_query = (
frappe.qb.from_(bundle)
.inner_join(stock_ledger_entry)
.on(bundle.parent == stock_ledger_entry.serial_and_batch_bundle)
.inner_join(batch_table)
.on(batch_table.name == bundle.batch_no)
.select(
bundle.batch_no,
Sum(bundle.qty).as_("qty"),
)
.where(stock_ledger_entry.is_cancelled == 0)
.where(
(stock_ledger_entry.item_code == filters.get("item_code"))
& (batch_table.disabled == 0)
& (stock_ledger_entry.serial_and_batch_bundle.isnotnull())
)
.groupby(bundle.batch_no, bundle.warehouse, batch_table.name)
.having(Sum(bundle.qty) != 0)
.offset(start)
.limit(page_len)
)
if not filters.get("is_inward"):
if filters.get("posting_date") and filters.get("posting_time"):
bundle_query = bundle_query.where(
stock_ledger_entry.posting_datetime
<= get_combine_datetime(filters.get("posting_date"), filters.get("posting_time"))
)
if not filters.get("include_expired_batches"):
bundle_query = bundle_query.where(
(batch_table.expiry_date >= expiry_date) | (batch_table.expiry_date.isnull())
)
bundle_query = bundle_query.select(
Case()
.when(batch_table.manufacturing_date.isnotnull(), Concat("MFG-", batch_table.manufacturing_date))
.as_("manufacturing_date"),
Case()
.when(batch_table.expiry_date.isnotnull(), Concat("EXP-", batch_table.expiry_date))
.as_("expiry_date"),
)
if filters.get("warehouse"):
bundle_query = bundle_query.where(stock_ledger_entry.warehouse == filters.get("warehouse"))
for field in searchfields:
bundle_query = bundle_query.select(batch_table[field])
if txt:
txt_condition = batch_table.name.like(f"%{txt}%")
for field in [*searchfields, "name"]:
txt_condition |= batch_table[field].like(f"%{txt}%")
bundle_query = bundle_query.where(txt_condition)
return bundle_query.run(as_list=1)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_account_list(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict | list
):
doctype = "Account"
filter_list = []
if isinstance(filters, dict):
for key, val in filters.items():
if isinstance(val, list | tuple):
filter_list.append([doctype, key, val[0], val[1]])
else:
filter_list.append([doctype, key, "=", val])
elif isinstance(filters, list):
filter_list.extend(filters)
if "is_group" not in [d[1] for d in filter_list]:
filter_list.append(["Account", "is_group", "=", "0"])
if searchfield and txt:
filter_list.append([doctype, searchfield, "like", "%%%s%%" % txt])
return frappe.desk.reportview.execute(
doctype,
filters=filter_list,
fields=["name", "parent_account"],
limit_start=start,
limit_page_length=page_len,
as_list=True,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_blanket_orders(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
bo = frappe.qb.DocType("Blanket Order")
bo_item = frappe.qb.DocType("Blanket Order Item")
blanket_orders = (
frappe.qb.from_(bo)
.from_(bo_item)
.select(bo.name)
.distinct()
.select(bo.blanket_order_type, bo.to_date)
.where(
(bo_item.parent == bo.name)
& (bo_item.item_code == filters.get("item"))
& (bo.blanket_order_type == filters.get("blanket_order_type"))
& (bo.company == filters.get("company"))
& (bo.docstatus == 1)
)
.run()
)
return blanket_orders
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_income_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
# income account can be any Credit account,
# but can also be a Asset account with account_type='Income Account' in special circumstances.
# Hence the first condition is an "OR"
if not filters:
filters = {}
dt = "Account"
acc = qb.DocType(dt)
condition = [
(acc.report_type.eq("Profit and Loss") | acc.account_type.isin(["Income Account", "Temporary"])),
acc.is_group.eq(0),
acc.disabled.eq(0),
]
if txt:
condition.append(acc.name.like(f"%{txt}%"))
if filters.get("company"):
condition.append(acc.company.eq(filters.get("company")))
user_perms = build_qb_match_conditions(dt)
condition.extend(user_perms)
return (
qb.from_(acc)
.select(acc.name)
.where(Criterion.all(condition))
.orderby(acc.idx, order=Order.desc)
.orderby(acc.name)
.run()
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_dimensions(
doctype: str,
txt: str,
searchfield: str,
start: int,
page_len: int,
filters: dict,
reference_doctype: str | None = None,
):
from erpnext.accounts.doctype.accounting_dimension_filter.accounting_dimension_filter import (
get_dimension_filter_map,
)
dimension_filters = get_dimension_filter_map()
dimension_filters = dimension_filters.get((filters.get("dimension"), filters.get("account")))
query_filters = []
or_filters = []
fields = ["name"]
searchfields = frappe.get_meta(doctype).get_search_fields()
meta = frappe.get_meta(doctype)
if meta.is_tree and meta.has_field("is_group"):
query_filters.append(["is_group", "=", 0])
if meta.has_field("disabled"):
query_filters.append(["disabled", "!=", 1])
if meta.has_field("company"):
query_filters.append(["company", "=", filters.get("company")])
for field in searchfields:
df = meta.get_field(field)
if not df or df.fieldtype != "Check":
or_filters.append([field, "LIKE", "%%%s%%" % txt])
fields.append(field)
if dimension_filters:
if dimension_filters["allow_or_restrict"] == "Allow":
query_selector = "in"
else:
query_selector = "not in"
if len(dimension_filters["allowed_dimensions"]) == 1:
dimensions = tuple(dimension_filters["allowed_dimensions"] * 2)
else:
dimensions = tuple(dimension_filters["allowed_dimensions"])
query_filters.append(["name", query_selector, dimensions])
output = frappe.get_list(
doctype,
fields=fields,
filters=query_filters,
or_filters=or_filters,
as_list=1,
reference_doctype=reference_doctype,
)
return [tuple(d) for d in set(output)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_expense_account(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
if not filters:
filters = {}
dt = "Account"
acc = qb.DocType(dt)
condition = [
(
acc.report_type.eq("Profit and Loss")
| acc.account_type.isin(
[
"Expense Account",
"Fixed Asset",
"Temporary",
"Asset Received But Not Billed",
"Capital Work in Progress",
]
)
),
acc.is_group.eq(0),
acc.disabled.eq(0),
]
if txt:
condition.append(acc.name.like(f"%{txt}%"))
if filters.get("company"):
condition.append(acc.company.eq(filters.get("company")))
user_perms = build_qb_match_conditions(dt)
condition.extend(user_perms)
return qb.from_(acc).select(acc.name).where(Criterion.all(condition)).run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def warehouse_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: list):
# Should be used when item code is passed in filters.
filter_dict = get_doctype_wise_filters(filters)
warehouse_field = "name"
meta = frappe.get_meta("Warehouse")
if meta.get("show_title_field_in_link") and meta.get("title_field"):
searchfield = meta.get("title_field")
warehouse_field = meta.get("title_field")
wh = frappe.qb.DocType("Warehouse")
bin_dt = frappe.qb.DocType("Bin")
# Bin filters go on the LEFT JOIN so warehouses without a matching Bin row are still returned
join_condition = bin_dt.warehouse == wh.name
for condition in get_filter_conditions_qb("Bin", filter_dict.get("Bin")):
join_condition &= condition
# Base the query on Warehouse so get_query applies its user-permission match conditions;
# Bin is left-joined (its filters on the JOIN) so warehouses without a Bin row still match.
query = (
frappe.qb.get_query("Warehouse", fields=[warehouse_field], ignore_permissions=False)
.left_join(bin_dt)
.on(join_condition)
.select(
Concat("Actual Qty", " : ", IfNull(Round(bin_dt.actual_qty, 2), 0)).as_("actual_qty"),
)
.where(wh[searchfield].like(f"%{txt}%"))
)
for condition in get_filter_conditions_qb("Warehouse", filter_dict.get("Warehouse")):
query = query.where(condition)
return (
query.orderby(IfNull(bin_dt.actual_qty, 0), order=Order.desc)
.orderby(wh[warehouse_field], order=Order.asc)
.limit(page_len)
.offset(start)
.run()
)
def get_doctype_wise_filters(filters):
# Helper function to seperate filters doctype_wise
filter_dict = defaultdict(list)
for row in filters:
filter_dict[row[0]].append(row)
return filter_dict
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_batch_numbers(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
batch = frappe.qb.DocType("Batch")
query = (
frappe.qb.from_(batch)
.select(batch.batch_id)
.where(
(batch.disabled == 0)
& (batch.expiry_date.isnull() | (batch.expiry_date >= today()))
& batch.name.like(f"%{txt}%")
)
)
if filters and filters.get("item"):
query = query.where(batch.item == filters.get("item"))
return query.orderby(batch.batch_id).limit(page_len).offset(start).run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def item_manufacturer_query(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
item_filters = [
["manufacturer", "like", "%" + txt + "%"],
["item_code", "=", filters.get("item_code")],
]
item_manufacturers = frappe.get_all(
"Item Manufacturer",
fields=["manufacturer", "manufacturer_part_no"],
filters=item_filters,
limit_start=start,
limit_page_length=page_len,
as_list=1,
)
return item_manufacturers
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_receipts(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
pr = frappe.qb.DocType("Purchase Receipt")
pr_item = frappe.qb.DocType("Purchase Receipt Item")
query = (
frappe.qb.from_(pr)
.inner_join(pr_item)
.on(pr_item.parent == pr.name)
.select(pr.name)
.distinct() # one row per receipt, not per matching item line
.where((pr.docstatus == 1) & pr.name.like(f"%{txt}%"))
)
if filters and filters.get("item_code"):
query = query.where(pr_item.item_code == filters.get("item_code"))
return query.orderby(pr.name).limit(page_len).offset(start).run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_purchase_invoices(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
pi = frappe.qb.DocType("Purchase Invoice")
pi_item = frappe.qb.DocType("Purchase Invoice Item")
query = (
frappe.qb.from_(pi)
.inner_join(pi_item)
.on(pi_item.parent == pi.name)
.select(pi.name)
.distinct() # one row per invoice, not per matching item line
.where((pi.docstatus == 1) & pi.name.like(f"%{txt}%"))
)
if filters and filters.get("item_code"):
query = query.where(pi_item.item_code == filters.get("item_code"))
return query.orderby(pi.name).limit(page_len).offset(start).run()
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_doctypes_for_closing(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
doctypes = frappe.get_hooks("period_closing_doctypes")
if txt:
doctypes = [d for d in doctypes if txt.lower() in d.lower()]
return [(d,) for d in set(doctypes)]
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_tax_template(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
item_doc = frappe.get_cached_doc("Item", filters.get("item_code"))
item_group = filters.get("item_group")
company = filters.get("company")
taxes = item_doc.taxes or []
while item_group:
item_group_doc = frappe.get_cached_doc("Item Group", item_group)
taxes += item_group_doc.taxes or []
item_group = item_group_doc.parent_item_group
if not taxes:
or_filters = []
if txt:
search_fields = ["name"]
tax_template_doc = frappe.get_meta("Item Tax Template")
if title_field := tax_template_doc.title_field:
search_fields.append(title_field)
if tax_template_doc.search_fields:
search_fields.extend(tax_template_doc.get_search_fields())
for f in search_fields:
or_filters.append([doctype, f.strip(), "like", f"%{txt}%"])
return frappe.get_list(
"Item Tax Template",
filters={"disabled": 0, "company": company},
or_filters=or_filters,
as_list=True,
)
else:
valid_from = filters.get("valid_from")
valid_from = valid_from[1] if isinstance(valid_from, list) else valid_from
ctx = frappe._dict(
{
"item_code": filters.get("item_code"),
"posting_date": valid_from,
"tax_category": filters.get("tax_category"),
"company": company,
"base_net_rate": filters.get("base_net_rate"),
}
)
taxes = _get_item_tax_template(ctx, taxes, for_validate=True)
txt = txt.lower()
return [(d,) for d in set(taxes) if not txt or txt in d.lower()]
def get_fields(doctype, fields=None):
if fields is None:
fields = []
meta = frappe.get_meta(doctype)
fields.extend(meta.get_search_fields())
if meta.title_field and meta.title_field.strip() not in fields:
fields.insert(1, meta.title_field.strip())
return unique(fields)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_payment_terms_for_references(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
terms = []
if filters:
terms = frappe.db.get_all(
"Payment Schedule",
filters={"parent": filters.get("reference")},
fields=["payment_term"],
limit=page_len,
as_list=1,
)
return terms
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_filtered_child_rows(
doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict
):
table = frappe.qb.DocType(doctype)
query = (
frappe.qb.from_(table)
.select(
table.name,
Concat("#", table.idx, ", ", table.item_code),
)
.orderby(table.idx)
.offset(start)
.limit(page_len)
)
if filters:
for field, value in filters.items():
query = query.where(table[field] == value)
if txt:
txt += "%"
query = query.where(
((Cast_(table.idx, "varchar").like(txt.replace("#", ""))) | (table.item_code.like(txt)))
| (table.name.like(txt))
)
return query.run(as_dict=False)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_item_uom_query(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
if frappe.get_single_value("Stock Settings", "allow_uom_with_conversion_rate_defined_in_item"):
query_filters = {"parent": filters.get("item_code")}
if txt:
query_filters["uom"] = ["like", f"%{txt}%"]
return frappe.get_all(
"UOM Conversion Detail",
filters=query_filters,
fields=["uom", "conversion_factor"],
limit_start=start,
limit_page_length=page_len,
order_by="idx",
as_list=1,
)
return frappe.get_all(
"UOM",
filters={"name": ["like", f"%{txt}%"], "enabled": 1},
fields=["name"],
limit_start=start,
limit_page_length=page_len,
as_list=1,
)
@frappe.whitelist()
@frappe.validate_and_sanitize_search_inputs
def get_warehouse_address(doctype: str, txt: str, searchfield: str, start: int, page_len: int, filters: dict):
table = frappe.qb.DocType(doctype)
child_table = frappe.qb.DocType("Dynamic Link")
query = (
frappe.qb.from_(table)
.inner_join(child_table)
.on((table.name == child_table.parent) & (child_table.parenttype == doctype))
.select(table.name)
.where(
(child_table.link_name == filters.get("warehouse"))
& (table.disabled == 0)
& (child_table.link_doctype == "Warehouse")
& (table.name.like(f"%{txt}%"))
)
.offset(start)
.limit(page_len)
)
return query.run(as_list=1)