diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 66d458bf75..aa5c50f30f 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -8,6 +8,7 @@ import json import frappe from frappe import _, msgprint from frappe.model.document import Document +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( add_days, ceil, @@ -20,6 +21,7 @@ from frappe.utils import ( nowdate, ) from frappe.utils.csvutils import build_csv_response +from pypika.terms import ExistsCriterion from erpnext.manufacturing.doctype.bom.bom import get_children as get_bom_children from erpnext.manufacturing.doctype.bom.bom import validate_bom_no @@ -100,39 +102,46 @@ class ProductionPlan(Document): @frappe.whitelist() def get_pending_material_requests(self): """Pull Material Requests that are pending based on criteria selected""" - mr_filter = item_filter = "" + + bom = frappe.qb.DocType("BOM") + mr = frappe.qb.DocType("Material Request") + mr_item = frappe.qb.DocType("Material Request Item") + + pending_mr_query = ( + frappe.qb.from_(mr) + .from_(mr_item) + .select(mr.name, mr.transaction_date) + .distinct() + .where( + (mr_item.parent == mr.name) + & (mr.material_request_type == "Manufacture") + & (mr.docstatus == 1) + & (mr.status != "Stopped") + & (mr.company == self.company) + & (mr_item.qty > IfNull(mr_item.ordered_qty, 0)) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + if self.from_date: - mr_filter += " and mr.transaction_date >= %(from_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date >= self.from_date) + if self.to_date: - mr_filter += " and mr.transaction_date <= %(to_date)s" + pending_mr_query = pending_mr_query.where(mr.transaction_date <= self.to_date) + if self.warehouse: - mr_filter += " and mr_item.warehouse = %(warehouse)s" + pending_mr_query = pending_mr_query.where(mr_item.warehouse == self.warehouse) if self.item_code: - item_filter += " and mr_item.item_code = %(item)s" + pending_mr_query = pending_mr_query.where(mr_item.item_code == self.item_code) - pending_mr = frappe.db.sql( - """ - select distinct mr.name, mr.transaction_date - from `tabMaterial Request` mr, `tabMaterial Request Item` mr_item - where mr_item.parent = mr.name - and mr.material_request_type = "Manufacture" - and mr.docstatus = 1 and mr.status != "Stopped" and mr.company = %(company)s - and mr_item.qty > ifnull(mr_item.ordered_qty,0) {0} {1} - and (exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1)) - """.format( - mr_filter, item_filter - ), - { - "from_date": self.from_date, - "to_date": self.to_date, - "warehouse": self.warehouse, - "item": self.item_code, - "company": self.company, - }, - as_dict=1, - ) + pending_mr = pending_mr_query.run(as_dict=True) self.add_mr_in_table(pending_mr) @@ -160,16 +169,17 @@ class ProductionPlan(Document): so_mr_list = [d.get(field) for d in self.get(table) if d.get(field)] return so_mr_list - def get_bom_item(self): + def get_bom_item_condition(self): """Check if Item or if its Template has a BOM.""" - bom_item = None + bom_item_condition = None has_bom = frappe.db.exists({"doctype": "BOM", "item": self.item_code, "docstatus": 1}) + if not has_bom: + bom = frappe.qb.DocType("BOM") template_item = frappe.db.get_value("Item", self.item_code, ["variant_of"]) - bom_item = ( - "bom.item = {0}".format(frappe.db.escape(template_item)) if template_item else bom_item - ) - return bom_item + bom_item_condition = bom.item == template_item or None + + return bom_item_condition def get_so_items(self): # Check for empty table or empty rows @@ -178,46 +188,73 @@ class ProductionPlan(Document): so_list = self.get_so_mr_list("sales_order", "sales_orders") - item_condition = "" - bom_item = "bom.item = so_item.item_code" - if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + so_item = frappe.qb.DocType("Sales Order Item") - items = frappe.db.sql( - """ - select - distinct parent, item_code, warehouse, - (qty - work_order_qty) * conversion_factor as pending_qty, - description, name - from - `tabSales Order Item` so_item - where - parent in (%s) and docstatus = 1 and qty > work_order_qty - and exists (select name from `tabBOM` bom where %s - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), bom_item, item_condition), - tuple(so_list), - as_dict=1, + items_subquery = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + items_query = ( + frappe.qb.from_(so_item) + .select( + so_item.parent, + so_item.item_code, + so_item.warehouse, + ((so_item.qty - so_item.work_order_qty) * so_item.conversion_factor).as_("pending_qty"), + so_item.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent.isin(so_list)) + & (so_item.docstatus == 1) + & (so_item.qty > so_item.work_order_qty) + ) + ) + + if self.item_code and frappe.db.exists("Item", self.item_code): + items_query = items_query.where(so_item.item_code == self.item_code) + items_subquery = items_subquery.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) + + items_query = items_query.where(ExistsCriterion(items_subquery)) + + items = items_query.run(as_dict=True) + + pi = frappe.qb.DocType("Packed Item") + + packed_items_query = ( + frappe.qb.from_(so_item) + .from_(pi) + .select( + pi.parent, + pi.item_code, + pi.warehouse.as_("warehouse"), + (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty).as_("pending_qty"), + pi.parent_item, + pi.description, + so_item.name, + ) + .distinct() + .where( + (so_item.parent == pi.parent) + & (so_item.docstatus == 1) + & (pi.parent_item == so_item.item_code) + & (so_item.parent.isin(so_list)) + & (so_item.qty > so_item.work_order_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) ) if self.item_code: - item_condition = " and so_item.item_code = {0}".format(frappe.db.escape(self.item_code)) + packed_items_query = packed_items_query.where(so_item.item_code == self.item_code) - packed_items = frappe.db.sql( - """select distinct pi.parent, pi.item_code, pi.warehouse as warehouse, - (((so_item.qty - so_item.work_order_qty) * pi.qty) / so_item.qty) - as pending_qty, pi.parent_item, pi.description, so_item.name - from `tabSales Order Item` so_item, `tabPacked Item` pi - where so_item.parent = pi.parent and so_item.docstatus = 1 - and pi.parent_item = so_item.item_code - and so_item.parent in (%s) and so_item.qty > so_item.work_order_qty - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(so_list)), item_condition), - tuple(so_list), - as_dict=1, - ) + packed_items = packed_items_query.run(as_dict=True) self.add_items(items + packed_items) self.calculate_total_planned_qty() @@ -233,22 +270,39 @@ class ProductionPlan(Document): mr_list = self.get_so_mr_list("material_request", "material_requests") - item_condition = "" - if self.item_code: - item_condition = " and mr_item.item_code ={0}".format(frappe.db.escape(self.item_code)) + bom = frappe.qb.DocType("BOM") + mr_item = frappe.qb.DocType("Material Request Item") - items = frappe.db.sql( - """select distinct parent, name, item_code, warehouse, description, - (qty - ordered_qty) * conversion_factor as pending_qty - from `tabMaterial Request Item` mr_item - where parent in (%s) and docstatus = 1 and qty > ordered_qty - and exists (select name from `tabBOM` bom where bom.item=mr_item.item_code - and bom.is_active = 1) %s""" - % (", ".join(["%s"] * len(mr_list)), item_condition), - tuple(mr_list), - as_dict=1, + items_query = ( + frappe.qb.from_(mr_item) + .select( + mr_item.parent, + mr_item.name, + mr_item.item_code, + mr_item.warehouse, + mr_item.description, + ((mr_item.qty - mr_item.ordered_qty) * mr_item.conversion_factor).as_("pending_qty"), + ) + .distinct() + .where( + (mr_item.parent.isin(mr_list)) + & (mr_item.docstatus == 1) + & (mr_item.qty > mr_item.ordered_qty) + & ( + ExistsCriterion( + frappe.qb.from_(bom) + .select(bom.name) + .where((bom.item == mr_item.item_code) & (bom.is_active == 1)) + ) + ) + ) ) + if self.item_code: + items_query = items_query.where(mr_item.item_code == self.item_code) + + items = items_query.run(as_dict=True) + self.add_items(items) self.calculate_total_planned_qty() @@ -819,29 +873,45 @@ def download_raw_materials(doc, warehouses=None): def get_exploded_items(item_details, company, bom_no, include_non_stock_items, planned_qty=1): - for d in frappe.db.sql( - """select bei.item_code, item.default_bom as bom, - ifnull(sum(bei.stock_qty/ifnull(bom.quantity, 1)), 0)*%s as qty, item.item_name, - bei.description, bei.stock_uom, item.min_order_qty, bei.source_warehouse, - item.default_material_request_type, item.min_order_qty, item_default.default_warehouse, - item.purchase_uom, item_uom.conversion_factor, item.safety_stock - from - `tabBOM Explosion Item` bei - JOIN `tabBOM` bom ON bom.name = bei.parent - JOIN `tabItem` item ON item.name = bei.item_code - LEFT JOIN `tabItem Default` item_default - ON item_default.parent = item.name and item_default.company=%s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bei.docstatus < 2 - and bom.name=%s and item.is_stock_item in (1, {0}) - group by bei.item_code, bei.stock_uom""".format( - 0 if include_non_stock_items else 1 - ), - (planned_qty, company, bom_no), - as_dict=1, - ): + bei = frappe.qb.DocType("BOM Explosion Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + data = ( + frappe.qb.from_(bei) + .join(bom) + .on(bom.name == bei.parent) + .join(item) + .on(item.name == bei.item_code) + .left_join(item_default) + .on((item_default.parent == item.name) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + (IfNull(Sum(bei.stock_qty / IfNull(bom.quantity, 1)), 0) * planned_qty).as_("qty"), + item.item_name, + bei.description, + bei.stock_uom, + item.min_order_qty, + bei.source_warehouse, + item.default_material_request_type, + item.min_order_qty, + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + item.safety_stock, + ) + .where( + (bei.docstatus < 2) + & (bom.name == bom_no) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bei.item_code, bei.stock_uom) + ).run(as_dict=True) + + for d in data: if not d.conversion_factor and d.purchase_uom: d.conversion_factor = get_uom_conversion_factor(d.item_code, d.purchase_uom) item_details.setdefault(d.get("item_code"), d) @@ -866,33 +936,47 @@ def get_subitems( parent_qty, planned_qty=1, ): - items = frappe.db.sql( - """ - SELECT - bom_item.item_code, default_material_request_type, item.item_name, - ifnull(%(parent_qty)s * sum(bom_item.stock_qty/ifnull(bom.quantity, 1)) * %(planned_qty)s, 0) as qty, - item.is_sub_contracted_item as is_sub_contracted, bom_item.source_warehouse, - item.default_bom as default_bom, bom_item.description as description, - bom_item.stock_uom as stock_uom, item.min_order_qty as min_order_qty, item.safety_stock as safety_stock, - item_default.default_warehouse, item.purchase_uom, item_uom.conversion_factor - FROM - `tabBOM Item` bom_item - JOIN `tabBOM` bom ON bom.name = bom_item.parent - JOIN `tabItem` item ON bom_item.item_code = item.name - LEFT JOIN `tabItem Default` item_default - ON item.name = item_default.parent and item_default.company = %(company)s - LEFT JOIN `tabUOM Conversion Detail` item_uom - ON item.name = item_uom.parent and item_uom.uom = item.purchase_uom - where - bom.name = %(bom)s - and bom_item.docstatus < 2 - and item.is_stock_item in (1, {0}) - group by bom_item.item_code""".format( - 0 if include_non_stock_items else 1 - ), - {"bom": bom_no, "parent_qty": parent_qty, "planned_qty": planned_qty, "company": company}, - as_dict=1, - ) + bom_item = frappe.qb.DocType("BOM Item") + bom = frappe.qb.DocType("BOM") + item = frappe.qb.DocType("Item") + item_default = frappe.qb.DocType("Item Default") + item_uom = frappe.qb.DocType("UOM Conversion Detail") + + items = ( + frappe.qb.from_(bom_item) + .join(bom) + .on(bom.name == bom_item.parent) + .join(item) + .on(bom_item.item_code == item.name) + .left_join(item_default) + .on((item.name == item_default.parent) & (item_default.company == company)) + .left_join(item_uom) + .on((item.name == item_uom.parent) & (item_uom.uom == item.purchase_uom)) + .select( + bom_item.item_code, + item.default_material_request_type, + item.item_name, + IfNull(parent_qty * Sum(bom_item.stock_qty / IfNull(bom.quantity, 1)) * planned_qty, 0).as_( + "qty" + ), + item.is_sub_contracted_item.as_("is_sub_contracted"), + bom_item.source_warehouse, + item.default_bom.as_("default_bom"), + bom_item.description.as_("description"), + bom_item.stock_uom.as_("stock_uom"), + item.min_order_qty.as_("min_order_qty"), + item.safety_stock.as_("safety_stock"), + item_default.default_warehouse, + item.purchase_uom, + item_uom.conversion_factor, + ) + .where( + (bom.name == bom_no) + & (bom_item.docstatus < 2) + & (item.is_stock_item.isin([0, 1]) if include_non_stock_items else item.is_stock_item == 1) + ) + .groupby(bom_item.item_code) + ).run(as_dict=True) for d in items: if not data.get("include_exploded_items") or not d.default_bom: @@ -980,48 +1064,69 @@ def get_material_request_items( def get_sales_orders(self): - so_filter = item_filter = "" - bom_item = "bom.item = so_item.item_code" + bom = frappe.qb.DocType("BOM") + pi = frappe.qb.DocType("Packed Item") + so = frappe.qb.DocType("Sales Order") + so_item = frappe.qb.DocType("Sales Order Item") + + open_so_subquery1 = frappe.qb.from_(bom).select(bom.name).where(bom.is_active == 1) + + open_so_subquery2 = ( + frappe.qb.from_(pi) + .select(pi.name) + .where( + (pi.parent == so.name) + & (pi.parent_item == so_item.item_code) + & ( + ExistsCriterion( + frappe.qb.from_(bom).select(bom.name).where((bom.item == pi.item_code) & (bom.is_active == 1)) + ) + ) + ) + ) + + open_so_query = ( + frappe.qb.from_(so) + .from_(so_item) + .select(so.name, so.transaction_date, so.customer, so.base_grand_total) + .distinct() + .where( + (so_item.parent == so.name) + & (so.docstatus == 1) + & (so.status.notin(["Stopped", "Closed"])) + & (so.company == self.company) + & (so_item.qty > so_item.work_order_qty) + ) + ) date_field_mapper = { - "from_date": (">=", "so.transaction_date"), - "to_date": ("<=", "so.transaction_date"), - "from_delivery_date": (">=", "so_item.delivery_date"), - "to_delivery_date": ("<=", "so_item.delivery_date"), + "from_date": self.from_date >= so.transaction_date, + "to_date": self.to_date <= so.transaction_date, + "from_delivery_date": self.from_delivery_date >= so_item.delivery_date, + "to_delivery_date": self.to_delivery_date <= so_item.delivery_date, } for field, value in date_field_mapper.items(): if self.get(field): - so_filter += f" and {value[1]} {value[0]} %({field})s" + open_so_query = open_so_query.where(value) - for field in ["customer", "project", "sales_order_status"]: + for field in ("customer", "project", "sales_order_status"): if self.get(field): so_field = "status" if field == "sales_order_status" else field - so_filter += f" and so.{so_field} = %({field})s" + open_so_query = open_so_query.where(so[so_field] == self.get(field)) if self.item_code and frappe.db.exists("Item", self.item_code): - bom_item = self.get_bom_item() or bom_item - item_filter += " and so_item.item_code = %(item_code)s" + open_so_query = open_so_query.where(so_item.item_code == self.item_code) + open_so_subquery1 = open_so_subquery1.where( + self.get_bom_item_condition() or bom.item == so_item.item_code + ) - open_so = frappe.db.sql( - f""" - select distinct so.name, so.transaction_date, so.customer, so.base_grand_total - from `tabSales Order` so, `tabSales Order Item` so_item - where so_item.parent = so.name - and so.docstatus = 1 and so.status not in ('Stopped', 'Closed') - and so.company = %(company)s - and so_item.qty > so_item.work_order_qty {so_filter} {item_filter} - and (exists (select name from `tabBOM` bom where {bom_item} - and bom.is_active = 1) - or exists (select name from `tabPacked Item` pi - where pi.parent = so.name and pi.parent_item = so_item.item_code - and exists (select name from `tabBOM` bom where bom.item=pi.item_code - and bom.is_active = 1))) - """, - self.as_dict(), - as_dict=1, + open_so_query = open_so_query.where( + (ExistsCriterion(open_so_subquery1) | ExistsCriterion(open_so_subquery2)) ) + open_so = open_so_query.run(as_dict=True) + return open_so @@ -1030,37 +1135,34 @@ def get_bin_details(row, company, for_warehouse=None, all_warehouse=False): if isinstance(row, str): row = frappe._dict(json.loads(row)) - company = frappe.db.escape(company) - conditions, warehouse = "", "" + bin = frappe.qb.DocType("Bin") + wh = frappe.qb.DocType("Warehouse") + + subquery = frappe.qb.from_(wh).select(wh.name).where(wh.company == company) - conditions = " and warehouse in (select name from `tabWarehouse` where company = {0})".format( - company - ) if not all_warehouse: warehouse = for_warehouse or row.get("source_warehouse") or row.get("default_warehouse") if warehouse: lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - conditions = """ and warehouse in (select name from `tabWarehouse` - where lft >= {0} and rgt <= {1} and name=`tabBin`.warehouse and company = {2}) - """.format( - lft, rgt, company - ) + subquery = subquery.where((wh.lft >= lft) & (wh.rgt <= rgt) & (wh.name == bin.warehouse)) - return frappe.db.sql( - """ select ifnull(sum(projected_qty),0) as projected_qty, - ifnull(sum(actual_qty),0) as actual_qty, ifnull(sum(ordered_qty),0) as ordered_qty, - ifnull(sum(reserved_qty_for_production),0) as reserved_qty_for_production, warehouse, - ifnull(sum(planned_qty),0) as planned_qty - from `tabBin` where item_code = %(item_code)s {conditions} - group by item_code, warehouse - """.format( - conditions=conditions - ), - {"item_code": row["item_code"]}, - as_dict=1, + query = ( + frappe.qb.from_(bin) + .select( + bin.warehouse, + IfNull(Sum(bin.projected_qty), 0).as_("projected_qty"), + IfNull(Sum(bin.actual_qty), 0).as_("actual_qty"), + IfNull(Sum(bin.ordered_qty), 0).as_("ordered_qty"), + IfNull(Sum(bin.reserved_qty_for_production), 0).as_("reserved_qty_for_production"), + IfNull(Sum(bin.planned_qty), 0).as_("planned_qty"), + ) + .where((bin.item_code == row["item_code"]) & (bin.warehouse.isin(subquery))) + .groupby(bin.item_code, bin.warehouse) ) + return query.run(as_dict=True) + @frappe.whitelist() def get_so_details(sales_order):