Files
erpnext/erpnext/controllers/queries.py

1185 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.get_item_details import ItemDetailsCtx, _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"
if isinstance(filters, str):
filters = json.loads(filters)
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)
)
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"]))
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:
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 = ItemDetailsCtx(
{
"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)