Files
erpnext/erpnext/buying/utils.py
Loïc Oberle 0729c9a9cd refactor(material-request): replace raw SQL with Frappe Query Builder (#54836)
* refactor(material-request): replace raw SQL with Frappe Query Builder

Replace frappe.db.sql with frappe.qb in get_linked_material_requests
to improve readability and leverage the ORM's built-in SQL injection protection.

* removes unused import
2026-05-11 12:10:04 +00:00

156 lines
4.3 KiB
Python

# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import json
import frappe
from frappe import _
from frappe.utils import cint, cstr, flt, getdate
from erpnext.stock.doctype.item.item import get_last_purchase_details, validate_end_of_life
def update_last_purchase_rate(doc, is_submit) -> None:
"""updates last_purchase_rate in item table for each item"""
if doc.get("is_internal_supplier"):
return
this_purchase_date = getdate(doc.get("posting_date") or doc.get("transaction_date"))
for d in doc.get("items"):
if d.get("is_free_item"):
continue
# get last purchase details
last_purchase_details = get_last_purchase_details(d.item_code, doc.name)
# compare last purchase date and this transaction's date
last_purchase_rate = None
if last_purchase_details and (
doc.get("docstatus") == 2 or last_purchase_details.purchase_date > this_purchase_date
):
last_purchase_rate = last_purchase_details["base_net_rate"]
elif is_submit == 1:
# even if this transaction is the latest one, it should be submitted
# for it to be considered for latest purchase rate
if flt(d.conversion_factor):
last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor)
# Check if item code is present
# Conversion factor should not be mandatory for non itemized items
elif d.item_code:
frappe.throw(_("UOM Conversion factor is required in row {0}").format(d.idx))
# update last purchsae rate
frappe.db.set_value("Item", d.item_code, "last_purchase_rate", flt(last_purchase_rate))
def validate_for_items(doc) -> None:
items = []
for d in doc.get("items"):
set_stock_levels(row=d) # update with latest quantities
item = validate_item_and_get_basic_data(row=d)
validate_stock_item_warehouse(row=d, item=item)
validate_end_of_life(d.item_code, item.end_of_life, item.disabled)
items.append(cstr(d.item_code))
if (
items
and len(items) != len(set(items))
and not cint(frappe.db.get_single_value("Buying Settings", "allow_multiple_items") or 0)
):
frappe.throw(_("Same item cannot be entered multiple times."))
def set_stock_levels(row) -> None:
projected_qty = frappe.db.get_value(
"Bin",
{
"item_code": row.item_code,
"warehouse": row.warehouse,
},
"projected_qty",
)
qty_data = {
"projected_qty": flt(projected_qty),
"ordered_qty": 0,
"received_qty": 0,
}
if row.doctype in ("Purchase Receipt Item", "Purchase Invoice Item"):
qty_data.pop("received_qty")
for field in qty_data:
if row.meta.get_field(field):
row.set(field, qty_data[field])
def validate_item_and_get_basic_data(row) -> dict:
item = frappe.db.get_values(
"Item",
filters={"name": row.item_code},
fieldname=["is_stock_item", "is_sub_contracted_item", "end_of_life", "disabled"],
as_dict=1,
)
if not item:
frappe.throw(_("Row #{0}: Item {1} does not exist").format(row.idx, frappe.bold(row.item_code)))
return item[0]
def validate_stock_item_warehouse(row, item) -> None:
if item.is_stock_item == 1 and row.qty and not row.warehouse and not row.get("delivered_by_supplier"):
frappe.throw(
_("Row #{1}: Warehouse is mandatory for stock Item {0}").format(
frappe.bold(row.item_code), row.idx
)
)
def check_on_hold_or_closed_status(doctype, docname) -> None:
status = frappe.db.get_value(doctype, docname, "status")
if status in ("Closed", "On Hold"):
frappe.throw(_("{0} {1} status is {2}").format(doctype, docname, status), frappe.InvalidStatusError)
@frappe.whitelist()
def get_linked_material_requests(items: str):
"""
Retrieve Material Requests linked to a list of items.
"""
items = json.loads(items)
mr_list = []
mr = frappe.qb.DocType("Material Request")
mr_item = frappe.qb.DocType("Material Request Item")
for item in items:
query = (
frappe.qb.from_(mr)
.join(mr_item)
.on(mr.name == mr_item.parent)
.select(
mr.name.as_("mr_name"),
(mr_item.qty - mr_item.ordered_qty).as_("qty"),
mr_item.item_code,
mr_item.name.as_("mr_item"),
)
.where(mr_item.item_code == item)
.where(mr.material_request_type == "Purchase")
.where(mr.per_ordered < 99.99)
.where(mr.docstatus == 1)
.where(mr.status != "Stopped")
.distinct()
.orderby(mr_item.item_code)
)
result = query.run(as_dict=True)
if result:
mr_list.extend(result)
return mr_list