From 2c9d9577e3fe78b8b5850625e3ee3771a36121cf Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 23 Sep 2022 13:52:26 +0530 Subject: [PATCH 01/28] refactor: rewrite `Batch Item Expiry Status Report` queries in `QB` --- .../batch_item_expiry_status.py | 36 +++++++++---------- 1 file changed, 17 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py index 3d9b0461977..ef7d6e6816c 100644 --- a/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py +++ b/erpnext/stock/report/batch_item_expiry_status/batch_item_expiry_status.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import cint, getdate @@ -54,31 +55,28 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" +def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) - if filters.get("to_date"): - conditions += " and posting_date <= '%s'" % filters["to_date"] - else: + if not filters.get("to_date"): frappe.throw(_("'To Date' is required")) - return conditions - - -def get_stock_ledger_entries(filters): - conditions = get_conditions(filters) - return frappe.db.sql( - """select item_code, batch_no, warehouse, - posting_date, actual_qty - from `tabStock Ledger Entry` - where is_cancelled = 0 - and docstatus < 2 and ifnull(batch_no, '') != '' %s order by item_code, warehouse""" - % conditions, - as_dict=1, + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select(sle.item_code, sle.batch_no, sle.warehouse, sle.posting_date, sle.actual_qty) + .where( + (sle.is_cancelled == 0) + & (sle.docstatus < 2) + & (IfNull(sle.batch_no, "") != "") + & (sle.posting_date <= filters["to_date"]) + ) + .orderby(sle.item_code, sle.warehouse) ) + return query.run(as_dict=True) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -112,7 +110,7 @@ def get_item_warehouse_batch_map(filters, float_precision): def get_item_details(filters): item_map = {} - for d in frappe.db.sql("select name, item_name, description from tabItem", as_dict=1): + for d in (frappe.qb.from_("Item").select("name", "item_name", "description")).run(as_dict=True): item_map.setdefault(d.name, d) return item_map From 1c1f991d2f2d798dba51631abfdd6579e4671b1b Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 23 Sep 2022 14:10:07 +0530 Subject: [PATCH 02/28] refactor: rewrite `Batch-Wise Balance History Report` queries in `QB` --- .../batch_wise_balance_history.py | 51 ++++++++++--------- 1 file changed, 26 insertions(+), 25 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 8a13300dc83..2f54325a011 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import cint, flt, getdate @@ -64,37 +65,35 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" +# get all details +def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) - if filters.get("to_date"): - conditions += " and posting_date <= '%s'" % filters["to_date"] - else: + if not filters.get("to_date"): frappe.throw(_("'To Date' is required")) - for field in ["item_code", "warehouse", "batch_no", "company"]: - if filters.get(field): - conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field))) - - return conditions - - -# get all details -def get_stock_ledger_entries(filters): - conditions = get_conditions(filters) - return frappe.db.sql( - """ - select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s - group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" - % conditions, - as_dict=1, + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + sle.batch_no, + sle.warehouse, + sle.posting_date, + Sum(sle.actual_qty).as_("actual_qty"), + ) + .where((sle.is_cancelled == 0) & (sle.docstatus < 2) & (IfNull(sle.batch_no, "") != "")) + .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) + .orderby(sle.item_code, sle.warehouse) ) + for field in ("item_code", "warehouse", "batch_no", "company"): + if filters.get(field): + query = query.where(sle[field] == filters[field]) + + return query.run(as_dict=True) + def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) @@ -127,7 +126,9 @@ def get_item_warehouse_batch_map(filters, float_precision): def get_item_details(filters): item_map = {} - for d in frappe.db.sql("select name, item_name, description, stock_uom from tabItem", as_dict=1): + for d in (frappe.qb.from_("Item").select("name", "item_name", "description", "stock_uom")).run( + as_dict=1 + ): item_map.setdefault(d.name, d) return item_map From bb59346651fb4cd99fb1a23655a9b0a72d682f9f Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 23 Sep 2022 15:47:26 +0530 Subject: [PATCH 03/28] refactor: rewrite `Delayed Item Report` queries in `QB` --- .../delayed_item_report.py | 82 +++++++++---------- 1 file changed, 40 insertions(+), 42 deletions(-) diff --git a/erpnext/stock/report/delayed_item_report/delayed_item_report.py b/erpnext/stock/report/delayed_item_report/delayed_item_report.py index 9df24d65596..546a834da8c 100644 --- a/erpnext/stock/report/delayed_item_report/delayed_item_report.py +++ b/erpnext/stock/report/delayed_item_report/delayed_item_report.py @@ -21,56 +21,54 @@ class DelayedItemReport(object): return self.get_columns(), self.get_data() or [] def get_data(self, consolidated=False): - conditions = "" - doctype = self.filters.get("based_on") - child_doc = "%s Item" % doctype + sales_order_field = "sales_order" if doctype == "Sales Invoice" else "against_sales_order" + + parent = frappe.qb.DocType(doctype) + child = frappe.qb.DocType(f"{doctype} Item") + + query = ( + frappe.qb.from_(child) + .from_(parent) + .select( + child.item_code, + child.item_name, + child.item_group, + child.qty, + child.rate, + child.amount, + child.so_detail, + child[sales_order_field].as_("sales_order"), + parent.shipping_address_name, + parent.po_no, + parent.customer, + parent.posting_date, + parent.name, + parent.grand_total, + ) + .where( + (child.parent == parent.name) + & (parent.docstatus == 1) + & (parent.posting_date.between(self.filters.get("from_date"), self.filters.get("to_date"))) + & (child[sales_order_field].notnull()) + & (child[sales_order_field] != "") + ) + ) if doctype == "Sales Invoice": - conditions = " and `tabSales Invoice`.update_stock = 1 and `tabSales Invoice`.is_pos = 0" + query = query.where((parent.update_stock == 1) & (parent.is_pos == 0)) if self.filters.get("item_group"): - conditions += " and `tab%s`.item_group = %s" % ( - child_doc, - frappe.db.escape(self.filters.get("item_group")), - ) - - for field in ["customer", "customer_group", "company"]: - if self.filters.get(field): - conditions += " and `tab%s`.%s = %s" % ( - doctype, - field, - frappe.db.escape(self.filters.get(field)), - ) - - sales_order_field = "against_sales_order" - if doctype == "Sales Invoice": - sales_order_field = "sales_order" + query = query.where(child.item_group == self.filters.get("item_group")) if self.filters.get("sales_order"): - conditions = " and `tab%s`.%s = '%s'" % ( - child_doc, - sales_order_field, - self.filters.get("sales_order"), - ) + query = query.where(child[sales_order_field] == self.filters.get("sales_order")) - self.transactions = frappe.db.sql( - """ SELECT `tab{child_doc}`.item_code, `tab{child_doc}`.item_name, - `tab{child_doc}`.item_group, `tab{child_doc}`.qty, `tab{child_doc}`.rate, `tab{child_doc}`.amount, - `tab{child_doc}`.so_detail, `tab{child_doc}`.{so_field} as sales_order, - `tab{doctype}`.shipping_address_name, `tab{doctype}`.po_no, `tab{doctype}`.customer, - `tab{doctype}`.posting_date, `tab{doctype}`.name, `tab{doctype}`.grand_total - FROM `tab{child_doc}`, `tab{doctype}` - WHERE - `tab{child_doc}`.parent = `tab{doctype}`.name and `tab{doctype}`.docstatus = 1 and - `tab{doctype}`.posting_date between %(from_date)s and %(to_date)s and - `tab{child_doc}`.{so_field} is not null and `tab{child_doc}`.{so_field} != '' {cond} - """.format( - cond=conditions, doctype=doctype, child_doc=child_doc, so_field=sales_order_field - ), - {"from_date": self.filters.get("from_date"), "to_date": self.filters.get("to_date")}, - as_dict=1, - ) + for field in ("customer", "customer_group", "company"): + if self.filters.get(field): + query = query.where(parent[field] == self.filters.get(field)) + + self.transactions = query.run(as_dict=True) if self.transactions: self.filter_transactions_data(consolidated) From e312d17eae2a957b71c3a11480cad1d5dd848077 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Mon, 26 Sep 2022 12:46:52 +0530 Subject: [PATCH 04/28] refactor: rewrite `Item Prices Report` queries in `QB` --- .../stock/report/item_prices/item_prices.py | 167 ++++++++++-------- 1 file changed, 98 insertions(+), 69 deletions(-) diff --git a/erpnext/stock/report/item_prices/item_prices.py b/erpnext/stock/report/item_prices/item_prices.py index 87f1a42e2b2..ab47b4a8b99 100644 --- a/erpnext/stock/report/item_prices/item_prices.py +++ b/erpnext/stock/report/item_prices/item_prices.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import flt @@ -12,8 +13,7 @@ def execute(filters=None): filters = {} columns = get_columns(filters) - conditions = get_condition(filters) - item_map = get_item_details(conditions) + item_map = get_item_details(filters) pl = get_price_list() last_purchase_rate = get_last_purchase_rate() bom_rate = get_item_bom_rate() @@ -63,18 +63,24 @@ def get_columns(filters): return columns -def get_item_details(conditions): +def get_item_details(filters): """returns all items details""" item_map = {} - for i in frappe.db.sql( - """select name, item_group, item_name, description, - brand, stock_uom from tabItem %s - order by item_code, item_group""" - % (conditions), - as_dict=1, - ): + item = frappe.qb.DocType("Item") + query = ( + frappe.qb.from_(item) + .select(item.name, item.item_group, item.item_name, item.description, item.brand, item.stock_uom) + .orderby(item.item_code, item.item_group) + ) + + if filters.get("items") == "Enabled Items only": + query = query.where(item.disabled == 0) + elif filters.get("items") == "Disabled Items only": + query = query.where(item.disabled == 1) + + for i in query.run(as_dict=True): item_map.setdefault(i.name, i) return item_map @@ -85,19 +91,38 @@ def get_price_list(): rate = {} - price_list = frappe.db.sql( - """select ip.item_code, ip.buying, ip.selling, - concat(ifnull(cu.symbol,ip.currency), " ", round(ip.price_list_rate,2), " - ", ip.price_list) as price - from `tabItem Price` ip, `tabPrice List` pl, `tabCurrency` cu - where ip.price_list=pl.name and pl.currency=cu.name and pl.enabled=1""", - as_dict=1, - ) + ip = frappe.qb.DocType("Item Price") + pl = frappe.qb.DocType("Price List") + cu = frappe.qb.DocType("Currency") - for j in price_list: - if j.price: - rate.setdefault(j.item_code, {}).setdefault("Buying" if j.buying else "Selling", []).append( - j.price + price_list = ( + frappe.qb.from_(ip) + .from_(pl) + .from_(cu) + .select( + ip.item_code, + ip.buying, + ip.selling, + (IfNull(cu.symbol, ip.currency)).as_("currency"), + ip.price_list_rate, + ip.price_list, + ) + .where((ip.price_list == pl.name) & (pl.currency == cu.name) & (pl.enabled == 1)) + ).run(as_dict=True) + + for d in price_list: + d.update( + {"price": "{0} {1} - {2}".format(d.currency, round(d.price_list_rate, 2), d.price_list)} + ) + d.pop("currency") + d.pop("price_list_rate") + d.pop("price_list") + + if d.price: + rate.setdefault(d.item_code, {}).setdefault("Buying" if d.buying else "Selling", []).append( + d.price ) + item_rate_map = {} for item in rate: @@ -112,30 +137,39 @@ def get_price_list(): def get_last_purchase_rate(): item_last_purchase_rate_map = {} - query = """select * from ( - (select - po_item.item_code, - po.transaction_date as posting_date, - po_item.base_rate - from `tabPurchase Order` po, `tabPurchase Order Item` po_item - where po.name = po_item.parent and po.docstatus = 1) - union - (select - pr_item.item_code, - pr.posting_date, - pr_item.base_rate - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - where pr.name = pr_item.parent and pr.docstatus = 1) - union - (select - pi_item.item_code, - pi.posting_date, - pi_item.base_rate - from `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pi_item - where pi.name = pi_item.parent and pi.docstatus = 1 and pi.update_stock = 1) - ) result order by result.item_code asc, result.posting_date asc""" + po = frappe.qb.DocType("Purchase Order") + pr = frappe.qb.DocType("Purchase Receipt") + pi = frappe.qb.DocType("Purchase Invoice") + po_item = frappe.qb.DocType("Purchase Order Item") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + pi_item = frappe.qb.DocType("Purchase Invoice Item") - for d in frappe.db.sql(query, as_dict=1): + query = ( + frappe.qb.from_( + ( + frappe.qb.from_(po) + .from_(po_item) + .select(po_item.item_code, po.transaction_date.as_("posting_date"), po_item.base_rate) + .where((po.name == po_item.parent) & (po.docstatus == 1)) + ) + + ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr_item.item_code, pr.posting_date, pr_item.base_rate) + .where((pr.name == pr_item.parent) & (pr.docstatus == 1)) + ) + + ( + frappe.qb.from_(pi) + .from_(pi_item) + .select(pi_item.item_code, pi.posting_date, pi_item.base_rate) + .where((pi.name == pi_item.parent) & (pi.docstatus == 1) & (pi.update_stock == 1)) + ) + ) + .select("*") + .orderby("item_code", "posting_date") + ) + + for d in query.run(as_dict=True): item_last_purchase_rate_map[d.item_code] = d.base_rate return item_last_purchase_rate_map @@ -146,12 +180,15 @@ def get_item_bom_rate(): item_bom_map = {} - for b in frappe.db.sql( - """select item, (total_cost/quantity) as bom_rate - from `tabBOM` where is_active=1 and is_default=1""", - as_dict=1, - ): - item_bom_map.setdefault(b.item, flt(b.bom_rate)) + bom = frappe.qb.DocType("BOM") + bom_data = ( + frappe.qb.from_(bom) + .select(bom.item, (bom.total_cost / bom.quantity).as_("bom_rate")) + .where((bom.is_active == 1) & (bom.is_default == 1)) + ).run(as_dict=True) + + for d in bom_data: + item_bom_map.setdefault(d.item, flt(d.bom_rate)) return item_bom_map @@ -161,25 +198,17 @@ def get_valuation_rate(): item_val_rate_map = {} - for d in frappe.db.sql( - """select item_code, - sum(actual_qty*valuation_rate)/sum(actual_qty) as val_rate - from tabBin where actual_qty > 0 group by item_code""", - as_dict=1, - ): + bin = frappe.qb.DocType("Bin") + bin_data = ( + frappe.qb.from_(bin) + .select( + bin.item_code, Sum(bin.actual_qty * bin.valuation_rate) / Sum(bin.actual_qty).as_("val_rate") + ) + .where(bin.actual_qty > 0) + .groupby(bin.item_code) + ).run(as_dict=True) + + for d in bin_data: item_val_rate_map.setdefault(d.item_code, d.val_rate) return item_val_rate_map - - -def get_condition(filters): - """Get Filter Items""" - - if filters.get("items") == "Enabled Items only": - conditions = " where disabled=0 " - elif filters.get("items") == "Disabled Items only": - conditions = " where disabled=1 " - else: - conditions = "" - - return conditions From c18f13a45ba6498245ba0429e61205a0df2434a6 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 6 Oct 2022 13:35:11 +0530 Subject: [PATCH 05/28] refactor: rewrite `Stock Projected Qty Report` queries in `QB` --- .../stock_projected_qty.py | 106 ++++++++++-------- 1 file changed, 60 insertions(+), 46 deletions(-) diff --git a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py index 49e797d6a30..f477d8f08fd 100644 --- a/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py +++ b/erpnext/stock/report/stock_projected_qty/stock_projected_qty.py @@ -5,6 +5,7 @@ import frappe from frappe import _ from frappe.utils import flt, today +from pypika.terms import ExistsCriterion from erpnext.accounts.doctype.pos_invoice.pos_invoice import get_pos_reserved_qty from erpnext.stock.utils import ( @@ -218,10 +219,26 @@ def get_columns(): def get_bin_list(filters): - conditions = [] + bin = frappe.qb.DocType("Bin") + query = ( + frappe.qb.from_(bin) + .select( + bin.item_code, + bin.warehouse, + bin.actual_qty, + bin.planned_qty, + bin.indented_qty, + bin.ordered_qty, + bin.reserved_qty, + bin.reserved_qty_for_production, + bin.reserved_qty_for_sub_contract, + bin.projected_qty, + ) + .orderby(bin.item_code, bin.warehouse) + ) if filters.item_code: - conditions.append("item_code = '%s' " % filters.item_code) + query = query.where(bin.item_code == filters.item_code) if filters.warehouse: warehouse_details = frappe.db.get_value( @@ -229,21 +246,20 @@ def get_bin_list(filters): ) if warehouse_details: - conditions.append( - " exists (select name from `tabWarehouse` wh \ - where wh.lft >= %s and wh.rgt <= %s and bin.warehouse = wh.name)" - % (warehouse_details.lft, warehouse_details.rgt) + wh = frappe.qb.DocType("Warehouse") + query = query.where( + ExistsCriterion( + frappe.qb.from_(wh) + .select(wh.name) + .where( + (wh.lft >= warehouse_details.lft) + & (wh.rgt <= warehouse_details.rgt) + & (bin.warehouse == wh.name) + ) + ) ) - bin_list = frappe.db.sql( - """select item_code, warehouse, actual_qty, planned_qty, indented_qty, - ordered_qty, reserved_qty, reserved_qty_for_production, reserved_qty_for_sub_contract, projected_qty - from tabBin bin {conditions} order by item_code, warehouse - """.format( - conditions=" where " + " and ".join(conditions) if conditions else "" - ), - as_dict=1, - ) + bin_list = query.run(as_dict=True) return bin_list @@ -251,45 +267,43 @@ def get_bin_list(filters): def get_item_map(item_code, include_uom): """Optimization: get only the item doc and re_order_levels table""" - condition = "" - if item_code: - condition = "and item_code = {0}".format(frappe.db.escape(item_code, percent=False)) + bin = frappe.qb.DocType("Bin") + item = frappe.qb.DocType("Item") - cf_field = cf_join = "" - if include_uom: - cf_field = ", ucd.conversion_factor" - cf_join = ( - "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%(include_uom)s" + query = ( + frappe.qb.from_(item) + .select(item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom) + .where( + (item.is_stock_item == 1) + & (item.disabled == 0) + & ( + (item.end_of_life > today()) | (item.end_of_life.isnull()) | (item.end_of_life == "0000-00-00") + ) + & (ExistsCriterion(frappe.qb.from_(bin).select(bin.name).where(bin.item_code == item.name))) ) - - items = frappe.db.sql( - """ - select item.name, item.item_name, item.description, item.item_group, item.brand, item.stock_uom{cf_field} - from `tabItem` item - {cf_join} - where item.is_stock_item = 1 - and item.disabled=0 - {condition} - and (item.end_of_life > %(today)s or item.end_of_life is null or item.end_of_life='0000-00-00') - and exists (select name from `tabBin` bin where bin.item_code=item.name)""".format( - cf_field=cf_field, cf_join=cf_join, condition=condition - ), - {"today": today(), "include_uom": include_uom}, - as_dict=True, ) - condition = "" if item_code: - condition = "where parent={0}".format(frappe.db.escape(item_code, percent=False)) + query = query.where(item.item_code == item_code) + + if include_uom: + ucd = frappe.qb.DocType("UOM Conversion Detail") + query = query.left_join(ucd).on((ucd.parent == item.name) & (ucd.uom == include_uom)) + + items = query.run(as_dict=True) + + ir = frappe.qb.DocType("Item Reorder") + query = frappe.qb.from_(ir).select("*") + + if item_code: + query = query.where(ir.parent == item_code) reorder_levels = frappe._dict() - for ir in frappe.db.sql( - """select * from `tabItem Reorder` {condition}""".format(condition=condition), as_dict=1 - ): - if ir.parent not in reorder_levels: - reorder_levels[ir.parent] = [] + for d in query.run(as_dict=True): + if d.parent not in reorder_levels: + reorder_levels[d.parent] = [] - reorder_levels[ir.parent].append(ir) + reorder_levels[d.parent].append(d) item_map = frappe._dict() for item in items: From d3c073dc25c6b86abef3f96560a5ed19d4c2f42a Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 6 Oct 2022 17:19:29 +0530 Subject: [PATCH 06/28] refactor: rewrite `Supplier-Wise Sales Analytics Report` queries in `QB` --- .../supplier_wise_sales_analytics.py | 117 ++++++++++++------ 1 file changed, 77 insertions(+), 40 deletions(-) diff --git a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py index 5430fe6969d..8c76908cc3c 100644 --- a/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py +++ b/erpnext/stock/report/supplier_wise_sales_analytics/supplier_wise_sales_analytics.py @@ -4,6 +4,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import flt @@ -70,31 +71,33 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" - values = [] +def get_consumed_details(filters): + item = frappe.qb.DocType("Item") + sle = frappe.qb.DocType("Stock Ledger Entry") + + query = ( + frappe.qb.from_(sle) + .from_(item) + .select( + sle.item_code, + item.item_name, + item.description, + item.stock_uom, + sle.actual_qty, + sle.stock_value_difference, + sle.voucher_no, + sle.voucher_type, + ) + .where((sle.is_cancelled == 0) & (sle.item_code == item.name) & (sle.actual_qty < 0)) + ) if filters.get("from_date") and filters.get("to_date"): - conditions = "and sle.posting_date>=%s and sle.posting_date<=%s" - values = [filters.get("from_date"), filters.get("to_date")] + query = query.where( + (sle.posting_date >= filters.get("from_date")) & (sle.posting_date <= filters.get("to_date")) + ) - return conditions, values - - -def get_consumed_details(filters): - conditions, values = get_conditions(filters) consumed_details = {} - - for d in frappe.db.sql( - """select sle.item_code, i.item_name, i.description, - i.stock_uom, sle.actual_qty, sle.stock_value_difference, - sle.voucher_no, sle.voucher_type - from `tabStock Ledger Entry` sle, `tabItem` i - where sle.is_cancelled = 0 and sle.item_code=i.name and sle.actual_qty < 0 %s""" - % conditions, - values, - as_dict=1, - ): + for d in query.run(as_dict=True): consumed_details.setdefault(d.item_code, []).append(d) return consumed_details @@ -104,24 +107,54 @@ def get_suppliers_details(filters): item_supplier_map = {} supplier = filters.get("supplier") - for d in frappe.db.sql( - """select pr.supplier, pri.item_code from - `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri - where pr.name=pri.parent and pr.docstatus=1 and - pri.item_code=(select name from `tabItem` where - is_stock_item=1 and name=pri.item_code)""", - as_dict=1, - ): + item = frappe.qb.DocType("Item") + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + + query = ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr.supplier, pr_item.item_code) + .where( + (pr.name == pr_item.parent) + & (pr.docstatus == 1) + & ( + pr_item.item_code + == ( + frappe.qb.from_(item) + .select(item.name) + .where((item.is_stock_item == 1) & (item.name == pr_item.item_code)) + ) + ) + ) + ) + + for d in query.run(as_dict=True): item_supplier_map.setdefault(d.item_code, []).append(d.supplier) - for d in frappe.db.sql( - """select pr.supplier, pri.item_code from - `tabPurchase Invoice` pr, `tabPurchase Invoice Item` pri - where pr.name=pri.parent and pr.docstatus=1 and - ifnull(pr.update_stock, 0) = 1 and pri.item_code=(select name from `tabItem` - where is_stock_item=1 and name=pri.item_code)""", - as_dict=1, - ): + pi = frappe.qb.DocType("Purchase Invoice") + pi_item = frappe.qb.DocType("Purchase Invoice Item") + + query = ( + frappe.qb.from_(pi) + .from_(pi_item) + .select(pi.supplier, pi_item.item_code) + .where( + (pi.name == pi_item.parent) + & (pi.docstatus == 1) + & (IfNull(pi.update_stock, 0) == 1) + & ( + pi_item.item_code + == ( + frappe.qb.from_(item) + .select(item.name) + .where((item.is_stock_item == 1) & (item.name == pi_item.item_code)) + ) + ) + ) + ) + + for d in query.run(as_dict=True): if d.item_code not in item_supplier_map: item_supplier_map.setdefault(d.item_code, []).append(d.supplier) @@ -138,7 +171,11 @@ def get_suppliers_details(filters): def get_material_transfer_vouchers(): - return frappe.db.sql_list( - """select name from `tabStock Entry` where - purpose='Material Transfer' and docstatus=1""" + se = frappe.qb.DocType("Stock Entry") + query = ( + frappe.qb.from_(se) + .select(se.name) + .where((se.purpose == "Material Transfer") & (se.docstatus == 1)) ) + + return [r[0] for r in query.run()] From 7c759b193ca957d039165209c9e9237bf93fd5e6 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 7 Oct 2022 09:59:28 +0530 Subject: [PATCH 07/28] refactor: rewrite `Total Stock Summary Report` queries in `QB` --- .../total_stock_summary.py | 58 +++++++++---------- 1 file changed, 28 insertions(+), 30 deletions(-) diff --git a/erpnext/stock/report/total_stock_summary/total_stock_summary.py b/erpnext/stock/report/total_stock_summary/total_stock_summary.py index 21529da2a12..c3155bd1a5e 100644 --- a/erpnext/stock/report/total_stock_summary/total_stock_summary.py +++ b/erpnext/stock/report/total_stock_summary/total_stock_summary.py @@ -4,60 +4,58 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum def execute(filters=None): if not filters: filters = {} - columns = get_columns() + columns = get_columns(filters) stock = get_total_stock(filters) return columns, stock -def get_columns(): +def get_columns(filters): columns = [ - _("Company") + ":Link/Company:250", - _("Warehouse") + ":Link/Warehouse:150", _("Item") + ":Link/Item:150", _("Description") + "::300", _("Current Qty") + ":Float:100", ] + if filters.get("group_by") == "Warehouse": + columns.insert(0, _("Warehouse") + ":Link/Warehouse:150") + else: + columns.insert(0, _("Company") + ":Link/Company:250") + return columns def get_total_stock(filters): - conditions = "" - columns = "" + bin = frappe.qb.DocType("Bin") + item = frappe.qb.DocType("Item") + wh = frappe.qb.DocType("Warehouse") + + query = ( + frappe.qb.from_(bin) + .inner_join(item) + .on(bin.item_code == item.item_code) + .inner_join(wh) + .on(wh.name == bin.warehouse) + .where(bin.actual_qty != 0) + ) if filters.get("group_by") == "Warehouse": if filters.get("company"): - conditions += " AND warehouse.company = %s" % frappe.db.escape( - filters.get("company"), percent=False - ) + query = query.where(wh.company == filters.get("company")) - conditions += " GROUP BY ledger.warehouse, item.item_code" - columns += "'' as company, ledger.warehouse" + query = query.select(bin.warehouse).groupby(bin.warehouse) else: - conditions += " GROUP BY warehouse.company, item.item_code" - columns += " warehouse.company, '' as warehouse" + query = query.select(wh.company).groupby(wh.company) - return frappe.db.sql( - """ - SELECT - %s, - item.item_code, - item.description, - sum(ledger.actual_qty) as actual_qty - FROM - `tabBin` AS ledger - INNER JOIN `tabItem` AS item - ON ledger.item_code = item.item_code - INNER JOIN `tabWarehouse` warehouse - ON warehouse.name = ledger.warehouse - WHERE - ledger.actual_qty != 0 %s""" - % (columns, conditions) - ) + query = query.select( + item.item_code, item.description, Sum(bin.actual_qty).as_("actual_qty") + ).groupby(item.item_code) + + return query.run() From 8103856a41279ac3278d560a99f603604b0fd956 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Thu, 6 Oct 2022 20:58:00 +0530 Subject: [PATCH 08/28] refactor: rewrite `Warehouse wise Item Balance Age and Value Report` queries in `QB` --- ...rehouse_wise_item_balance_age_and_value.py | 30 +++++++------------ 1 file changed, 11 insertions(+), 19 deletions(-) diff --git a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py index a54373f364c..eedf1a04601 100644 --- a/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py +++ b/erpnext/stock/report/warehouse_wise_item_balance_age_and_value/warehouse_wise_item_balance_age_and_value.py @@ -7,6 +7,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Count from frappe.utils import flt from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age @@ -98,7 +99,7 @@ def get_columns(filters): def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): - sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) + sle_count = flt(frappe.qb.from_("Stock Ledger Entry").select(Count("name")).run()[0][0]) if sle_count > 500000: frappe.throw(_("Please set filter based on Item or Warehouse")) if not filters.get("company"): @@ -108,25 +109,16 @@ def validate_filters(filters): def get_warehouse_list(filters): from frappe.core.doctype.user_permission.user_permission import get_permitted_documents - condition = "" - user_permitted_warehouse = get_permitted_documents("Warehouse") - value = () - if user_permitted_warehouse: - condition = "and name in %s" - value = set(user_permitted_warehouse) - elif not user_permitted_warehouse and filters.get("warehouse"): - condition = "and name = %s" - value = filters.get("warehouse") + wh = frappe.qb.DocType("Warehouse") + query = frappe.qb.from_(wh).select(wh.name).where(wh.is_group == 0) - return frappe.db.sql( - """select name - from `tabWarehouse` where is_group = 0 - {condition}""".format( - condition=condition - ), - value, - as_dict=1, - ) + user_permitted_warehouse = get_permitted_documents("Warehouse") + if user_permitted_warehouse: + query = query.where(wh.name.isin(set(user_permitted_warehouse))) + elif filters.get("warehouse"): + query = query.where(wh.name == filters.get("warehouse")) + + return query.run(as_dict=True) def add_warehouse_column(columns, warehouse_list): From a5b3f8cae9b028a9614f66cab6ba43ffc260b365 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 7 Oct 2022 17:23:33 +0530 Subject: [PATCH 09/28] refactor: rewrite `Purchase Order Analysis Report` queries in `QB` --- .../purchase_order_analysis.py | 100 +++++++++--------- 1 file changed, 50 insertions(+), 50 deletions(-) diff --git a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py index a5c464910de..e10c0e2fccf 100644 --- a/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py +++ b/erpnext/buying/report/purchase_order_analysis/purchase_order_analysis.py @@ -6,6 +6,7 @@ import copy import frappe from frappe import _ +from frappe.query_builder.functions import IfNull from frappe.utils import date_diff, flt, getdate @@ -16,9 +17,7 @@ def execute(filters=None): validate_filters(filters) columns = get_columns(filters) - conditions = get_conditions(filters) - - data = get_data(conditions, filters) + data = get_data(filters) if not data: return [], [], None, [] @@ -37,60 +36,61 @@ def validate_filters(filters): frappe.throw(_("To Date cannot be before From Date.")) -def get_conditions(filters): - conditions = "" - if filters.get("from_date") and filters.get("to_date"): - conditions += " and po.transaction_date between %(from_date)s and %(to_date)s" +def get_data(filters): + po = frappe.qb.DocType("Purchase Order") + po_item = frappe.qb.DocType("Purchase Order Item") + pi_item = frappe.qb.DocType("Purchase Invoice Item") - for field in ["company", "name"]: + query = ( + frappe.qb.from_(po) + .from_(po_item) + .left_join(pi_item) + .on(pi_item.po_detail == po_item.name) + .select( + po.transaction_date.as_("date"), + po_item.schedule_date.as_("required_date"), + po_item.project, + po.name.as_("purchase_order"), + po.status, + po.supplier, + po_item.item_code, + po_item.qty, + po_item.received_qty, + (po_item.qty - po_item.received_qty).as_("pending_qty"), + IfNull(pi_item.qty, 0).as_("billed_qty"), + po_item.base_amount.as_("amount"), + (po_item.received_qty * po_item.base_rate).as_("received_qty_amount"), + (po_item.billed_amt * IfNull(po.conversion_rate, 1)).as_("billed_amount"), + (po_item.base_amount - (po_item.billed_amt * IfNull(po.conversion_rate, 1))).as_( + "pending_amount" + ), + po.set_warehouse.as_("warehouse"), + po.company, + po_item.name, + ) + .where( + (po_item.parent == po.name) & (po.status.notin(("Stopped", "Closed"))) & (po.docstatus == 1) + ) + .groupby(po_item.name) + .orderby(po.transaction_date) + ) + + for field in ("company", "name"): if filters.get(field): - conditions += f" and po.{field} = %({field})s" + query = query.where(po[field] == filters.get(field)) + + if filters.get("from_date") and filters.get("to_date"): + query = query.where( + po.transaction_date.between(filters.get("from_date"), filters.get("to_date")) + ) if filters.get("status"): - conditions += " and po.status in %(status)s" + query = query.where(po.status.isin(filters.get("status"))) if filters.get("project"): - conditions += " and poi.project = %(project)s" + query = query.where(po_item.project == filters.get("project")) - return conditions - - -def get_data(conditions, filters): - data = frappe.db.sql( - """ - SELECT - po.transaction_date as date, - poi.schedule_date as required_date, - poi.project, - po.name as purchase_order, - po.status, po.supplier, poi.item_code, - poi.qty, poi.received_qty, - (poi.qty - poi.received_qty) AS pending_qty, - IFNULL(pii.qty, 0) as billed_qty, - poi.base_amount as amount, - (poi.received_qty * poi.base_rate) as received_qty_amount, - (poi.billed_amt * IFNULL(po.conversion_rate, 1)) as billed_amount, - (poi.base_amount - (poi.billed_amt * IFNULL(po.conversion_rate, 1))) as pending_amount, - po.set_warehouse as warehouse, - po.company, poi.name - FROM - `tabPurchase Order` po, - `tabPurchase Order Item` poi - LEFT JOIN `tabPurchase Invoice Item` pii - ON pii.po_detail = poi.name - WHERE - poi.parent = po.name - and po.status not in ('Stopped', 'Closed') - and po.docstatus = 1 - {0} - GROUP BY poi.name - ORDER BY po.transaction_date ASC - """.format( - conditions - ), - filters, - as_dict=1, - ) + data = query.run(as_dict=True) return data From a14b9c7baceb45f0cff43c9b5717896d37d13137 Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 7 Oct 2022 18:01:09 +0530 Subject: [PATCH 10/28] refactor: rewrite `Supplier Quotation Comparison Report` queries in `QB` --- .../supplier_quotation_comparison.py | 72 +++++++++---------- 1 file changed, 36 insertions(+), 36 deletions(-) diff --git a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py index 3013b6d1607..a7282909618 100644 --- a/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py +++ b/erpnext/buying/report/supplier_quotation_comparison/supplier_quotation_comparison.py @@ -16,8 +16,7 @@ def execute(filters=None): return [], [] columns = get_columns(filters) - conditions = get_conditions(filters) - supplier_quotation_data = get_data(filters, conditions) + supplier_quotation_data = get_data(filters) data, chart_data = prepare_data(supplier_quotation_data, filters) message = get_message() @@ -25,50 +24,51 @@ def execute(filters=None): return columns, data, message, chart_data -def get_conditions(filters): - conditions = "" +def get_data(filters): + sq = frappe.qb.DocType("Supplier Quotation") + sq_item = frappe.qb.DocType("Supplier Quotation Item") + + query = ( + frappe.qb.from_(sq_item) + .from_(sq) + .select( + sq_item.parent, + sq_item.item_code, + sq_item.qty, + sq_item.stock_qty, + sq_item.amount, + sq_item.uom, + sq_item.stock_uom, + sq_item.request_for_quotation, + sq_item.lead_time_days, + sq.supplier.as_("supplier_name"), + sq.valid_till, + ) + .where( + (sq_item.parent == sq.name) + & (sq_item.docstatus < 2) + & (sq.company == filters.get("company")) + & (sq.transaction_date.between(filters.get("from_date"), filters.get("to_date"))) + ) + .orderby(sq.transaction_date, sq_item.item_code) + ) + if filters.get("item_code"): - conditions += " AND sqi.item_code = %(item_code)s" + query = query.where(sq_item.item_code == filters.get("item_code")) if filters.get("supplier_quotation"): - conditions += " AND sqi.parent in %(supplier_quotation)s" + query = query.where(sq_item.parent.isin(filters.get("supplier_quotation"))) if filters.get("request_for_quotation"): - conditions += " AND sqi.request_for_quotation = %(request_for_quotation)s" + query = query.where(sq_item.request_for_quotation == filters.get("request_for_quotation")) if filters.get("supplier"): - conditions += " AND sq.supplier in %(supplier)s" + query = query.where(sq.supplier.isin(filters.get("supplier"))) if not filters.get("include_expired"): - conditions += " AND sq.status != 'Expired'" + query = query.where(sq.status != "Expired") - return conditions - - -def get_data(filters, conditions): - supplier_quotation_data = frappe.db.sql( - """ - SELECT - sqi.parent, sqi.item_code, - sqi.qty, sqi.stock_qty, sqi.amount, - sqi.uom, sqi.stock_uom, - sqi.request_for_quotation, - sqi.lead_time_days, sq.supplier as supplier_name, sq.valid_till - FROM - `tabSupplier Quotation Item` sqi, - `tabSupplier Quotation` sq - WHERE - sqi.parent = sq.name - AND sqi.docstatus < 2 - AND sq.company = %(company)s - AND sq.transaction_date between %(from_date)s and %(to_date)s - {0} - order by sq.transaction_date, sqi.item_code""".format( - conditions - ), - filters, - as_dict=1, - ) + supplier_quotation_data = query.run(as_dict=True) return supplier_quotation_data From e78a70699423eb2552e77b62b3af33e5e22290da Mon Sep 17 00:00:00 2001 From: Sagar Sharma Date: Fri, 7 Oct 2022 18:13:33 +0530 Subject: [PATCH 11/28] refactor: rewrite `Procurement Tracker Report` queries in `QB` --- .../procurement_tracker.py | 151 +++++++++--------- 1 file changed, 73 insertions(+), 78 deletions(-) diff --git a/erpnext/buying/report/procurement_tracker/procurement_tracker.py b/erpnext/buying/report/procurement_tracker/procurement_tracker.py index d70ac46ce33..71019e80377 100644 --- a/erpnext/buying/report/procurement_tracker/procurement_tracker.py +++ b/erpnext/buying/report/procurement_tracker/procurement_tracker.py @@ -127,32 +127,27 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" - +def apply_filters_on_query(filters, parent, child, query): if filters.get("company"): - conditions += " AND parent.company=%s" % frappe.db.escape(filters.get("company")) + query = query.where(parent.company == filters.get("company")) if filters.get("cost_center") or filters.get("project"): - conditions += """ - AND (child.`cost_center`=%s OR child.`project`=%s) - """ % ( - frappe.db.escape(filters.get("cost_center")), - frappe.db.escape(filters.get("project")), + query = query.where( + (child.cost_center == filters.get("cost_center")) | (child.project == filters.get("project")) ) if filters.get("from_date"): - conditions += " AND parent.transaction_date>='%s'" % filters.get("from_date") + query = query.where(parent.transaction_date >= filters.get("from_date")) if filters.get("to_date"): - conditions += " AND parent.transaction_date<='%s'" % filters.get("to_date") - return conditions + query = query.where(parent.transaction_date <= filters.get("to_date")) + + return query def get_data(filters): - conditions = get_conditions(filters) - purchase_order_entry = get_po_entries(conditions) - mr_records, procurement_record_against_mr = get_mapped_mr_details(conditions) + purchase_order_entry = get_po_entries(filters) + mr_records, procurement_record_against_mr = get_mapped_mr_details(filters) pr_records = get_mapped_pr_records() pi_records = get_mapped_pi_records() @@ -187,11 +182,15 @@ def get_data(filters): return procurement_record -def get_mapped_mr_details(conditions): +def get_mapped_mr_details(filters): mr_records = {} - mr_details = frappe.db.sql( - """ - SELECT + parent = frappe.qb.DocType("Material Request") + child = frappe.qb.DocType("Material Request Item") + + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( parent.transaction_date, parent.per_ordered, parent.owner, @@ -203,18 +202,13 @@ def get_mapped_mr_details(conditions): child.uom, parent.status, child.project, - child.cost_center - FROM `tabMaterial Request` parent, `tabMaterial Request Item` child - WHERE - parent.per_ordered>=0 - AND parent.name=child.parent - AND parent.docstatus=1 - {conditions} - """.format( - conditions=conditions - ), - as_dict=1, - ) # nosec + child.cost_center, + ) + .where((parent.per_ordered >= 0) & (parent.name == child.parent) & (parent.docstatus == 1)) + ) + query = apply_filters_on_query(filters, parent, child, query) + + mr_details = query.run(as_dict=True) procurement_record_against_mr = [] for record in mr_details: @@ -241,46 +235,49 @@ def get_mapped_mr_details(conditions): def get_mapped_pi_records(): - return frappe._dict( - frappe.db.sql( - """ - SELECT - pi_item.po_detail, - pi_item.base_amount - FROM `tabPurchase Invoice Item` as pi_item - INNER JOIN `tabPurchase Order` as po - ON pi_item.`purchase_order` = po.`name` - WHERE - pi_item.docstatus = 1 - AND po.status not in ('Closed','Completed','Cancelled') - AND pi_item.po_detail IS NOT NULL - """ + po = frappe.qb.DocType("Purchase Order") + pi_item = frappe.qb.DocType("Purchase Invoice Item") + pi_records = ( + frappe.qb.from_(pi_item) + .inner_join(po) + .on(pi_item.purchase_order == po.name) + .select(pi_item.po_detail, pi_item.base_amount) + .where( + (pi_item.docstatus == 1) + & (po.status.notin(("Closed", "Completed", "Cancelled"))) + & (pi_item.po_detail.isnotnull()) ) - ) + ).run() + + return frappe._dict(pi_records) def get_mapped_pr_records(): - return frappe._dict( - frappe.db.sql( - """ - SELECT - pr_item.purchase_order_item, - pr.posting_date - FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item - WHERE - pr.docstatus=1 - AND pr.name=pr_item.parent - AND pr_item.purchase_order_item IS NOT NULL - AND pr.status not in ('Closed','Completed','Cancelled') - """ + pr = frappe.qb.DocType("Purchase Receipt") + pr_item = frappe.qb.DocType("Purchase Receipt Item") + pr_records = ( + frappe.qb.from_(pr) + .from_(pr_item) + .select(pr_item.purchase_order_item, pr.posting_date) + .where( + (pr.docstatus == 1) + & (pr.name == pr_item.parent) + & (pr_item.purchase_order_item.isnotnull()) + & (pr.status.notin(("Closed", "Completed", "Cancelled"))) ) - ) + ).run() + + return frappe._dict(pr_records) -def get_po_entries(conditions): - return frappe.db.sql( - """ - SELECT +def get_po_entries(filters): + parent = frappe.qb.DocType("Purchase Order") + child = frappe.qb.DocType("Purchase Order Item") + + query = ( + frappe.qb.from_(parent) + .from_(child) + .select( child.name, child.parent, child.cost_center, @@ -297,17 +294,15 @@ def get_po_entries(conditions): parent.transaction_date, parent.supplier, parent.status, - parent.owner - FROM `tabPurchase Order` parent, `tabPurchase Order Item` child - WHERE - parent.docstatus = 1 - AND parent.name = child.parent - AND parent.status not in ('Closed','Completed','Cancelled') - {conditions} - GROUP BY - parent.name, child.item_code - """.format( - conditions=conditions - ), - as_dict=1, - ) # nosec + parent.owner, + ) + .where( + (parent.docstatus == 1) + & (parent.name == child.parent) + & (parent.status.notin(("Closed", "Completed", "Cancelled"))) + ) + .groupby(parent.name, child.item_code) + ) + query = apply_filters_on_query(filters, parent, child, query) + + return query.run(as_dict=True) From 65992304bc259a44c60198b3b6e3be0f5b107b3e Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 4 Oct 2022 16:17:56 +0530 Subject: [PATCH 12/28] fix: delete old ple's on item value repost --- erpnext/accounts/utils.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 95ba3d86cef..02dcd683fa2 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1171,6 +1171,10 @@ def _delete_gl_entries(voucher_type, voucher_no): where voucher_type=%s and voucher_no=%s""", (voucher_type, voucher_no), ) + ple = qb.DocType("Payment Ledger Entry") + qb.from_(ple).delete().where( + (ple.voucher_type == voucher_type) & (ple.voucher_no == voucher_no) + ).run() def sort_stock_vouchers_by_posting_date( From eb819368aa9a8700ee868cbb8375aa893e9de72f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 11 Oct 2022 15:27:28 +0530 Subject: [PATCH 13/28] test: dedeplication on payment ledger upon repost --- .../test_repost_item_valuation.py | 55 +++++++++++++++++++ 1 file changed, 55 insertions(+) diff --git a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py index edd2553d5d1..e0f24797100 100644 --- a/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py +++ b/erpnext/stock/doctype/repost_item_valuation/test_repost_item_valuation.py @@ -9,6 +9,7 @@ from frappe.tests.utils import FrappeTestCase from frappe.utils import nowdate from frappe.utils.data import add_to_date, today +from erpnext.accounts.doctype.sales_invoice.test_sales_invoice import create_sales_invoice from erpnext.accounts.utils import repost_gle_for_stock_vouchers from erpnext.controllers.stock_controller import create_item_wise_repost_entries from erpnext.stock.doctype.item.test_item import make_item @@ -272,3 +273,57 @@ class TestRepostItemValuation(FrappeTestCase, StockTestMixin): [{"credit": 50, "debit": 0}], gle_filters={"account": "Stock In Hand - TCP1"}, ) + + def test_duplicate_ple_on_repost(self): + from erpnext.accounts import utils + + # lower numbers to simplify test + orig_chunk_size = utils.GL_REPOSTING_CHUNK + utils.GL_REPOSTING_CHUNK = 2 + self.addCleanup(setattr, utils, "GL_REPOSTING_CHUNK", orig_chunk_size) + + rate = 100 + item = self.make_item() + item.valuation_rate = 90 + item.allow_negative_stock = 1 + item.save() + + company = "_Test Company with perpetual inventory" + + # consume non-existing stock + sinv = create_sales_invoice( + company=company, + posting_date=today(), + debit_to="Debtors - TCP1", + income_account="Sales - TCP1", + expense_account="Cost of Goods Sold - TCP1", + warehouse="Stores - TCP1", + update_stock=1, + currency="INR", + item_code=item.name, + cost_center="Main - TCP1", + qty=1, + rate=rate, + ) + + # backdated receipt triggers repost + make_stock_entry( + item=item.name, + company=company, + qty=5, + rate=rate, + target="Stores - TCP1", + posting_date=add_to_date(today(), days=-1), + ) + + ple_entries = frappe.db.get_list( + "Payment Ledger Entry", + filters={"voucher_type": sinv.doctype, "voucher_no": sinv.name, "delinked": 0}, + ) + + # assert successful deduplication on PLE + self.assertEqual(len(ple_entries), 1) + + # outstanding should not be affected + sinv.reload() + self.assertEqual(sinv.outstanding_amount, 100) From e543dca6a0220b3e77560c6c2beddc32c6672f00 Mon Sep 17 00:00:00 2001 From: Raffael Meyer <14891507+barredterra@users.noreply.github.com> Date: Thu, 13 Oct 2022 08:23:52 +0200 Subject: [PATCH 14/28] fix: don't try to update youtube data if disabled in settings (#32588) fix: cast value from db [skip ci] --- erpnext/utilities/doctype/video/video.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/utilities/doctype/video/video.py b/erpnext/utilities/doctype/video/video.py index 15dbccde89d..13b7877b21d 100644 --- a/erpnext/utilities/doctype/video/video.py +++ b/erpnext/utilities/doctype/video/video.py @@ -59,7 +59,7 @@ def update_youtube_data(): "Video Settings", "Video Settings", ["enable_youtube_tracking", "frequency"] ) - if not enable_youtube_tracking: + if not cint(enable_youtube_tracking): return frequency = get_frequency(frequency) From 56a4a7739882259ecce97896dbf626adc6177502 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Oct 2022 12:31:30 +0530 Subject: [PATCH 15/28] fix: Renamed Notes section to Comments --- erpnext/crm/doctype/lead/lead.json | 4 ++-- erpnext/crm/doctype/opportunity/opportunity.json | 4 ++-- erpnext/crm/doctype/prospect/prospect.json | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 99c00ad6e69..6c8cb2f0800 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -375,7 +375,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "notes_tab", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "collapsible": 1, @@ -514,7 +514,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2022-08-09 18:26:17.101521", + "modified": "2022-10-13 12:28:47.241586", "modified_by": "Administrator", "module": "CRM", "name": "Lead", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index fed0c7c79a2..36c98dc279f 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -551,7 +551,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "notes_tab", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "fieldname": "notes_html", @@ -622,7 +622,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-08-09 18:26:37.235964", + "modified": "2022-10-13 12:29:18.233542", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", diff --git a/erpnext/crm/doctype/prospect/prospect.json b/erpnext/crm/doctype/prospect/prospect.json index 820a6c72ea1..d32311bc4e1 100644 --- a/erpnext/crm/doctype/prospect/prospect.json +++ b/erpnext/crm/doctype/prospect/prospect.json @@ -128,7 +128,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "notes_section", "fieldtype": "Tab Break", - "label": "Notes" + "label": "Comments" }, { "depends_on": "eval: !doc.__islocal", @@ -218,7 +218,7 @@ ], "index_web_pages_for_search": 1, "links": [], - "modified": "2022-08-09 18:26:56.950185", + "modified": "2022-10-13 12:29:33.674561", "modified_by": "Administrator", "module": "CRM", "name": "Prospect", From f561d8f6895a07573b9fef134945da6c9739e04f Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Thu, 13 Oct 2022 12:43:08 +0530 Subject: [PATCH 16/28] fix: Renamed Dashboard tab label to Connections --- erpnext/crm/doctype/lead/lead.json | 4 ++-- erpnext/crm/doctype/opportunity/opportunity.json | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 6c8cb2f0800..8f8a086d99e 100644 --- a/erpnext/crm/doctype/lead/lead.json +++ b/erpnext/crm/doctype/lead/lead.json @@ -506,7 +506,7 @@ { "fieldname": "dashboard_tab", "fieldtype": "Tab Break", - "label": "Dashboard", + "label": "Connections", "show_dashboard": 1 } ], @@ -514,7 +514,7 @@ "idx": 5, "image_field": "image", "links": [], - "modified": "2022-10-13 12:28:47.241586", + "modified": "2022-10-13 12:42:04.277879", "modified_by": "Administrator", "module": "CRM", "name": "Lead", diff --git a/erpnext/crm/doctype/opportunity/opportunity.json b/erpnext/crm/doctype/opportunity/opportunity.json index 36c98dc279f..07641d20c33 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.json +++ b/erpnext/crm/doctype/opportunity/opportunity.json @@ -544,7 +544,7 @@ "depends_on": "eval:!doc.__islocal", "fieldname": "dashboard_tab", "fieldtype": "Tab Break", - "label": "Dashboard", + "label": "Connections", "show_dashboard": 1 }, { @@ -622,7 +622,7 @@ "icon": "fa fa-info-sign", "idx": 195, "links": [], - "modified": "2022-10-13 12:29:18.233542", + "modified": "2022-10-13 12:42:21.545636", "modified_by": "Administrator", "module": "CRM", "name": "Opportunity", From fd49503ba2dc19f07a3a8dce36812be753587e08 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Thu, 13 Oct 2022 15:08:34 +0530 Subject: [PATCH 17/28] fix: Party account for multi-order invoices --- erpnext/buying/doctype/purchase_order/purchase_order.py | 3 ++- erpnext/selling/doctype/sales_order/sales_order.py | 3 +++ 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index bcedd4d0a11..c224b611e52 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -18,7 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( from erpnext.accounts.doctype.tax_withholding_category.tax_withholding_category import ( get_party_tax_withholding_details, ) -from erpnext.accounts.party import get_party_account_currency +from erpnext.accounts.party import get_party_account, get_party_account_currency from erpnext.buying.utils import check_on_hold_or_closed_status, validate_for_items from erpnext.controllers.buying_controller import BuyingController from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults @@ -558,6 +558,7 @@ def get_mapped_purchase_invoice(source_name, target_doc=None, ignore_permissions target.set_advances() target.set_payment_schedule() + target.credit_to = get_party_account("Supplier", source.supplier, source.company) def update_item(obj, target, source_parent): target.amount = flt(obj.amount) - flt(obj.billed_amt) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index 25806d6ed86..f0e9e4b7d92 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -18,6 +18,7 @@ from erpnext.accounts.doctype.sales_invoice.sales_invoice import ( update_linked_doc, validate_inter_company_party, ) +from erpnext.accounts.party import get_party_account from erpnext.controllers.selling_controller import SellingController from erpnext.manufacturing.doctype.production_plan.production_plan import ( get_items_for_material_requests, @@ -727,6 +728,8 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): if source.loyalty_points and source.order_type == "Shopping Cart": target.redeem_loyalty_points = 1 + target.debit_to = get_party_account("Customer", source.customer, source.company) + def update_item(source, target, source_parent): target.amount = flt(source.amount) - flt(source.billed_amt) target.base_amount = target.amount * flt(source_parent.conversion_rate) From 50e96989324f475fc386882eb8a8dc1df4a8c975 Mon Sep 17 00:00:00 2001 From: Ankush Menat Date: Thu, 13 Oct 2022 15:25:22 +0530 Subject: [PATCH 18/28] chore: drop dead code (#32595) [skip ci] --- erpnext/hooks.py | 2 - erpnext/startup/report_data_map.py | 327 ----------------------------- 2 files changed, 329 deletions(-) delete mode 100644 erpnext/startup/report_data_map.py diff --git a/erpnext/hooks.py b/erpnext/hooks.py index b8f51f839ce..6bc17a3675a 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -274,8 +274,6 @@ has_website_permission = { "Timesheet": "erpnext.controllers.website_list_for_contact.has_website_permission", } -dump_report_map = "erpnext.startup.report_data_map.data_map" - before_tests = "erpnext.setup.utils.before_tests" standard_queries = { diff --git a/erpnext/startup/report_data_map.py b/erpnext/startup/report_data_map.py deleted file mode 100644 index f8c1b6cca07..00000000000 --- a/erpnext/startup/report_data_map.py +++ /dev/null @@ -1,327 +0,0 @@ -# Copyright (c) 2015, Frappe Technologies Pvt. Ltd. and Contributors -# License: GNU General Public License v3. See license.txt - - -# mappings for table dumps -# "remember to add indexes!" - -data_map = { - "Company": {"columns": ["name"], "conditions": ["docstatus < 2"]}, - "Fiscal Year": { - "columns": ["name", "year_start_date", "year_end_date"], - "conditions": ["docstatus < 2"], - }, - # Accounts - "Account": { - "columns": ["name", "parent_account", "lft", "rgt", "report_type", "company", "is_group"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - "links": { - "company": ["Company", "name"], - }, - }, - "Cost Center": { - "columns": ["name", "lft", "rgt"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "GL Entry": { - "columns": [ - "name", - "account", - "posting_date", - "cost_center", - "debit", - "credit", - "is_opening", - "company", - "voucher_type", - "voucher_no", - "remarks", - ], - "order_by": "posting_date, account", - "links": { - "account": ["Account", "name"], - "company": ["Company", "name"], - "cost_center": ["Cost Center", "name"], - }, - }, - # Stock - "Item": { - "columns": [ - "name", - "if(item_name=name, '', item_name) as item_name", - "description", - "item_group as parent_item_group", - "stock_uom", - "brand", - "valuation_method", - ], - # "conditions": ["docstatus < 2"], - "order_by": "name", - "links": {"parent_item_group": ["Item Group", "name"], "brand": ["Brand", "name"]}, - }, - "Item Group": { - "columns": ["name", "parent_item_group"], - # "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Brand": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Project": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Warehouse": {"columns": ["name"], "conditions": ["docstatus < 2"], "order_by": "name"}, - "Stock Ledger Entry": { - "columns": [ - "name", - "posting_date", - "posting_time", - "item_code", - "warehouse", - "actual_qty as qty", - "voucher_type", - "voucher_no", - "project", - "incoming_rate as incoming_rate", - "stock_uom", - "serial_no", - "qty_after_transaction", - "valuation_rate", - ], - "order_by": "posting_date, posting_time, creation", - "links": { - "item_code": ["Item", "name"], - "warehouse": ["Warehouse", "name"], - "project": ["Project", "name"], - }, - "force_index": "posting_sort_index", - }, - "Serial No": { - "columns": ["name", "purchase_rate as incoming_rate"], - "conditions": ["docstatus < 2"], - "order_by": "name", - }, - "Stock Entry": { - "columns": ["name", "purpose"], - "conditions": ["docstatus=1"], - "order_by": "posting_date, posting_time, name", - }, - "Material Request Item": { - "columns": ["item.name as name", "item_code", "warehouse", "(qty - ordered_qty) as qty"], - "from": "`tabMaterial Request Item` item, `tabMaterial Request` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > ordered_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - "Purchase Order Item": { - "columns": [ - "item.name as name", - "item_code", - "warehouse", - "(qty - received_qty)*conversion_factor as qty", - ], - "from": "`tabPurchase Order Item` item, `tabPurchase Order` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > received_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - "Sales Order Item": { - "columns": [ - "item.name as name", - "item_code", - "(qty - delivered_qty)*conversion_factor as qty", - "warehouse", - ], - "from": "`tabSales Order Item` item, `tabSales Order` main", - "conditions": [ - "item.parent = main.name", - "main.docstatus=1", - "main.status != 'Stopped'", - "ifnull(warehouse, '')!=''", - "qty > delivered_qty", - ], - "links": {"item_code": ["Item", "name"], "warehouse": ["Warehouse", "name"]}, - }, - # Sales - "Customer": { - "columns": [ - "name", - "if(customer_name=name, '', customer_name) as customer_name", - "customer_group as parent_customer_group", - "territory as parent_territory", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": { - "parent_customer_group": ["Customer Group", "name"], - "parent_territory": ["Territory", "name"], - }, - }, - "Customer Group": { - "columns": ["name", "parent_customer_group"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Territory": { - "columns": ["name", "parent_territory"], - "conditions": ["docstatus < 2"], - "order_by": "lft", - }, - "Sales Invoice": { - "columns": ["name", "customer", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Sales Invoice Item": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Sales Invoice", "name"], "item_code": ["Item", "name"]}, - }, - "Sales Order": { - "columns": ["name", "customer", "transaction_date as posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "transaction_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Sales Order Item[Sales Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Sales Order", "name"], "item_code": ["Item", "name"]}, - }, - "Delivery Note": { - "columns": ["name", "customer", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"customer": ["Customer", "name"], "company": ["Company", "name"]}, - }, - "Delivery Note Item[Sales Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Delivery Note", "name"], "item_code": ["Item", "name"]}, - }, - "Supplier": { - "columns": [ - "name", - "if(supplier_name=name, '', supplier_name) as supplier_name", - "supplier_group as parent_supplier_group", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": { - "parent_supplier_group": ["Supplier Group", "name"], - }, - }, - "Supplier Group": { - "columns": ["name", "parent_supplier_group"], - "conditions": ["docstatus < 2"], - "order_by": "name", - }, - "Purchase Invoice": { - "columns": ["name", "supplier", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Invoice Item": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Invoice", "name"], "item_code": ["Item", "name"]}, - }, - "Purchase Order": { - "columns": ["name", "supplier", "transaction_date as posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Order Item[Purchase Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Order", "name"], "item_code": ["Item", "name"]}, - }, - "Purchase Receipt": { - "columns": ["name", "supplier", "posting_date", "company"], - "conditions": ["docstatus=1"], - "order_by": "posting_date", - "links": {"supplier": ["Supplier", "name"], "company": ["Company", "name"]}, - }, - "Purchase Receipt Item[Purchase Analytics]": { - "columns": ["name", "parent", "item_code", "stock_qty as qty", "base_net_amount"], - "conditions": ["docstatus=1", "ifnull(parent, '')!=''"], - "order_by": "parent", - "links": {"parent": ["Purchase Receipt", "name"], "item_code": ["Item", "name"]}, - }, - # Support - "Issue": { - "columns": ["name", "status", "creation", "resolution_date", "first_responded_on"], - "conditions": ["docstatus < 2"], - "order_by": "creation", - }, - # Manufacturing - "Work Order": { - "columns": [ - "name", - "status", - "creation", - "planned_start_date", - "planned_end_date", - "status", - "actual_start_date", - "actual_end_date", - "modified", - ], - "conditions": ["docstatus = 1"], - "order_by": "creation", - }, - # Medical - "Patient": { - "columns": [ - "name", - "creation", - "owner", - "if(patient_name=name, '', patient_name) as patient_name", - ], - "conditions": ["docstatus < 2"], - "order_by": "name", - "links": {"owner": ["User", "name"]}, - }, - "Patient Appointment": { - "columns": [ - "name", - "appointment_type", - "patient", - "practitioner", - "appointment_date", - "department", - "status", - "company", - ], - "order_by": "name", - "links": { - "practitioner": ["Healthcare Practitioner", "name"], - "appointment_type": ["Appointment Type", "name"], - }, - }, - "Healthcare Practitioner": { - "columns": ["name", "department"], - "order_by": "name", - "links": { - "department": ["Department", "name"], - }, - }, - "Appointment Type": {"columns": ["name"], "order_by": "name"}, - "Medical Department": {"columns": ["name"], "order_by": "name"}, -} From 9127b960333a6e81ec6c81978b3e8ada7fc2b327 Mon Sep 17 00:00:00 2001 From: Anand Baburajan Date: Thu, 13 Oct 2022 18:27:11 +0530 Subject: [PATCH 19/28] fix: add project settings to projects workspace (#32568) --- .../projects/workspace/projects/projects.json | 22 +++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/workspace/projects/projects.json b/erpnext/projects/workspace/projects/projects.json index 1253649e493..4bdb1db387d 100644 --- a/erpnext/projects/workspace/projects/projects.json +++ b/erpnext/projects/workspace/projects/projects.json @@ -5,7 +5,7 @@ "label": "Open Projects" } ], - "content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Open Projects\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Task\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Timesheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project Billing Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}}]", + "content": "[{\"type\":\"chart\",\"data\":{\"chart_name\":\"Open Projects\",\"col\":12}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Your Shortcuts\",\"col\":12}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Task\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Timesheet\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Project Billing Summary\",\"col\":3}},{\"type\":\"shortcut\",\"data\":{\"shortcut_name\":\"Dashboard\",\"col\":3}},{\"type\":\"spacer\",\"data\":{\"col\":12}},{\"type\":\"header\",\"data\":{\"text\":\"Reports & Masters\",\"col\":12}},{\"type\":\"card\",\"data\":{\"card_name\":\"Projects\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Time Tracking\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Reports\",\"col\":4}},{\"type\":\"card\",\"data\":{\"card_name\":\"Settings\",\"col\":4}}]", "creation": "2020-03-02 15:46:04.874669", "docstatus": 0, "doctype": "Workspace", @@ -170,9 +170,27 @@ "link_type": "Report", "onboard": 0, "type": "Link" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Settings", + "link_count": 1, + "onboard": 0, + "type": "Card Break" + }, + { + "hidden": 0, + "is_query_report": 0, + "label": "Projects Settings", + "link_count": 0, + "link_to": "Projects Settings", + "link_type": "DocType", + "onboard": 0, + "type": "Link" } ], - "modified": "2022-06-28 12:31:30.167740", + "modified": "2022-10-11 22:39:10.436311", "modified_by": "Administrator", "module": "Projects", "name": "Projects", From dc3fe8592187bf543eae628c639868de84ad9006 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 17 Oct 2022 11:37:50 +0530 Subject: [PATCH 20/28] chore: Remove HRMS related code (#32607) Co-authored-by: Rucha Mahabal --- .../journal_entry_account/journal_entry_account.json | 12 +++++++++--- erpnext/accounts/utils.py | 4 ---- 2 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json index dff883aef98..a0ea43332c9 100644 --- a/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json +++ b/erpnext/accounts/doctype/journal_entry_account/journal_entry_account.json @@ -202,6 +202,7 @@ "fieldname": "reference_type", "fieldtype": "Select", "label": "Reference Type", + "no_copy": 1, "options": "\nSales Invoice\nPurchase Invoice\nJournal Entry\nSales Order\nPurchase Order\nExpense Claim\nAsset\nLoan\nPayroll Entry\nEmployee Advance\nExchange Rate Revaluation\nInvoice Discounting\nFees\nFull and Final Statement" }, { @@ -209,13 +210,15 @@ "fieldtype": "Dynamic Link", "in_list_view": 1, "label": "Reference Name", + "no_copy": 1, "options": "reference_type" }, { "depends_on": "eval:doc.reference_type&&!in_list(doc.reference_type, ['Expense Claim', 'Asset', 'Employee Loan', 'Employee Advance'])", "fieldname": "reference_due_date", "fieldtype": "Select", - "label": "Reference Due Date" + "label": "Reference Due Date", + "no_copy": 1 }, { "fieldname": "project", @@ -274,19 +277,22 @@ "fieldname": "reference_detail_no", "fieldtype": "Data", "hidden": 1, - "label": "Reference Detail No" + "label": "Reference Detail No", + "no_copy": 1 } ], "idx": 1, "istable": 1, "links": [], - "modified": "2021-08-30 21:27:32.200299", + "modified": "2022-10-13 17:07:17.999191", "modified_by": "Administrator", "module": "Accounts", "name": "Journal Entry Account", + "naming_rule": "Random", "owner": "Administrator", "permissions": [], "sort_field": "modified", "sort_order": "DESC", + "states": [], "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 02dcd683fa2..67574ca9926 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -460,10 +460,6 @@ def reconcile_against_document(args): # nosemgrep frappe.flags.ignore_party_validation = False - if entry.voucher_type in ("Payment Entry", "Journal Entry"): - if hasattr(doc, "update_expense_claim"): - doc.update_expense_claim() - def check_if_advance_entry_modified(args): """ From 6381e75fa5b402a678c94320eba14b19a3c11012 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Oct 2022 12:17:05 +0530 Subject: [PATCH 21/28] fix: group warehouse filter not working for Batchwise Balance history report --- .../batch_wise_balance_history.py | 67 +++++++++++++------ 1 file changed, 47 insertions(+), 20 deletions(-) diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 8a13300dc83..911f4c31673 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -5,6 +5,8 @@ import frappe from frappe import _ from frappe.utils import cint, flt, getdate +from pypika import functions as fn +from pypika.terms import ExistsCriterion def execute(filters=None): @@ -64,36 +66,61 @@ def get_columns(filters): return columns -def get_conditions(filters): - conditions = "" +# get all details +def get_stock_ledger_entries(filters): if not filters.get("from_date"): frappe.throw(_("'From Date' is required")) - if filters.get("to_date"): - conditions += " and posting_date <= '%s'" % filters["to_date"] + sle = frappe.qb.DocType("Stock Ledger Entry") + query = ( + frappe.qb.from_(sle) + .select( + sle.item_code, + sle.warehouse, + sle.batch_no, + sle.posting_date, + fn.Sum(sle.actual_qty).as_("actual_qty"), + ) + .where( + (sle.docstatus < 2) + & (sle.is_cancelled == 0) + & (sle.batch_no.isnotnull()) + & (sle.batch_no != "") + ) + .groupby(sle.voucher_no, sle.batch_no, sle.item_code, sle.warehouse) + .orderby(sle.item_code, sle.warehouse) + ) + + if to_date := filters.get("to_date"): + query = query.where(sle.posting_date <= to_date) else: frappe.throw(_("'To Date' is required")) - for field in ["item_code", "warehouse", "batch_no", "company"]: + query = apply_warehouse_filter(query, sle, filters) + for field in ["item_code", "batch_no", "company"]: if filters.get(field): - conditions += " and {0} = {1}".format(field, frappe.db.escape(filters.get(field))) + query = query.where(sle[field] == filters.get(field)) - return conditions + return query.run(as_dict=True) -# get all details -def get_stock_ledger_entries(filters): - conditions = get_conditions(filters) - return frappe.db.sql( - """ - select item_code, batch_no, warehouse, posting_date, sum(actual_qty) as actual_qty - from `tabStock Ledger Entry` - where is_cancelled = 0 and docstatus < 2 and ifnull(batch_no, '') != '' %s - group by voucher_no, batch_no, item_code, warehouse - order by item_code, warehouse""" - % conditions, - as_dict=1, - ) +def apply_warehouse_filter(query, sle, filters): + if warehouse := filters.get("warehouse"): + warehouse_table = frappe.qb.DocType("Warehouse") + + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + chilren_subquery = ( + frappe.qb.from_(warehouse_table) + .select(warehouse_table.name) + .where( + (warehouse_table.lft >= lft) + & (warehouse_table.rgt <= rgt) + & (warehouse_table.name == sle.warehouse) + ) + ) + query = query.where(ExistsCriterion(chilren_subquery)) + + return query def get_item_warehouse_batch_map(filters, float_precision): From 2481574a2808e888acc0b59492642e0ffd5d9376 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 17 Oct 2022 14:18:55 +0530 Subject: [PATCH 22/28] chore: seperate function to apply filter for warehouse in case of QB --- erpnext/stock/doctype/warehouse/warehouse.py | 21 ++++++++++++++++++ .../batch_wise_balance_history.py | 22 ++----------------- .../report/stock_balance/stock_balance.py | 16 +++----------- .../stock/report/stock_ledger/stock_ledger.py | 17 ++------------ 4 files changed, 28 insertions(+), 48 deletions(-) diff --git a/erpnext/stock/doctype/warehouse/warehouse.py b/erpnext/stock/doctype/warehouse/warehouse.py index ab784ca1070..6e06d236173 100644 --- a/erpnext/stock/doctype/warehouse/warehouse.py +++ b/erpnext/stock/doctype/warehouse/warehouse.py @@ -9,6 +9,7 @@ from frappe import _, throw from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils import cint, flt from frappe.utils.nestedset import NestedSet +from pypika.terms import ExistsCriterion from erpnext.stock import get_warehouse_account @@ -266,3 +267,23 @@ def get_warehouses_based_on_account(account, company=None): frappe.throw(_("Warehouse not found against the account {0}").format(account)) return warehouses + + +# Will be use for frappe.qb +def apply_warehouse_filter(query, sle, filters): + if warehouse := filters.get("warehouse"): + warehouse_table = frappe.qb.DocType("Warehouse") + + lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) + chilren_subquery = ( + frappe.qb.from_(warehouse_table) + .select(warehouse_table.name) + .where( + (warehouse_table.lft >= lft) + & (warehouse_table.rgt <= rgt) + & (warehouse_table.name == sle.warehouse) + ) + ) + query = query.where(ExistsCriterion(chilren_subquery)) + + return query diff --git a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py index 911f4c31673..291c6b5dab5 100644 --- a/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py +++ b/erpnext/stock/report/batch_wise_balance_history/batch_wise_balance_history.py @@ -6,7 +6,8 @@ import frappe from frappe import _ from frappe.utils import cint, flt, getdate from pypika import functions as fn -from pypika.terms import ExistsCriterion + +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter def execute(filters=None): @@ -104,25 +105,6 @@ def get_stock_ledger_entries(filters): return query.run(as_dict=True) -def apply_warehouse_filter(query, sle, filters): - if warehouse := filters.get("warehouse"): - warehouse_table = frappe.qb.DocType("Warehouse") - - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) - - return query - - def get_item_warehouse_batch_map(filters, float_precision): sle = get_stock_ledger_entries(filters) iwb_map = {} diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 679d234c9f6..0fc642ef207 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -10,10 +10,10 @@ from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, date_diff, flt, getdate from frappe.utils.nestedset import get_descendants_of -from pypika.terms import ExistsCriterion import erpnext from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.report.stock_ageing.stock_ageing import FIFOSlots, get_average_age from erpnext.stock.utils import add_additional_uom_columns, is_reposting_item_valuation_in_progress @@ -270,18 +270,8 @@ def apply_conditions(query, filters): if company := filters.get("company"): query = query.where(sle.company == company) - if warehouse := filters.get("warehouse"): - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + if filters.get("warehouse"): + query = apply_warehouse_filter(query, sle, filters) elif warehouse_type := filters.get("warehouse_type"): query = ( query.join(warehouse_table) diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index e18d4c75229..a95119736a0 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -6,11 +6,11 @@ import frappe from frappe import _ from frappe.query_builder.functions import CombineDatetime from frappe.utils import cint, flt -from pypika.terms import ExistsCriterion from erpnext.stock.doctype.inventory_dimension.inventory_dimension import get_inventory_dimensions from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import get_stock_balance_for +from erpnext.stock.doctype.warehouse.warehouse import apply_warehouse_filter from erpnext.stock.utils import ( is_reposting_item_valuation_in_progress, update_included_uom_in_report, @@ -295,20 +295,7 @@ def get_stock_ledger_entries(filters, items): if filters.get(field): query = query.where(sle[field] == filters.get(field)) - if warehouse := filters.get("warehouse"): - lft, rgt = frappe.db.get_value("Warehouse", warehouse, ["lft", "rgt"]) - - warehouse_table = frappe.qb.DocType("Warehouse") - chilren_subquery = ( - frappe.qb.from_(warehouse_table) - .select(warehouse_table.name) - .where( - (warehouse_table.lft >= lft) - & (warehouse_table.rgt <= rgt) - & (warehouse_table.name == sle.warehouse) - ) - ) - query = query.where(ExistsCriterion(chilren_subquery)) + query = apply_warehouse_filter(query, sle, filters) return query.run(as_dict=True) From ced8d2a5376e5b95e6b798a2b3b8975dbd88f1c5 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Mon, 17 Oct 2022 16:21:54 +0530 Subject: [PATCH 23/28] test: lead creation and deletion restricted to dummy company --- .../test_service_level_agreement.py | 37 +++++++++++++++---- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py index 4e00138906d..472f6bc24eb 100644 --- a/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py +++ b/erpnext/support/doctype/service_level_agreement/test_service_level_agreement.py @@ -15,8 +15,30 @@ from erpnext.support.doctype.service_level_agreement.service_level_agreement imp class TestServiceLevelAgreement(unittest.TestCase): def setUp(self): + self.create_company() frappe.db.set_value("Support Settings", None, "track_service_level_agreement", 1) - frappe.db.sql("delete from `tabLead`") + lead = frappe.qb.DocType("Lead") + frappe.qb.from_(lead).delete().where(lead.company == self.company).run() + + def create_company(self): + name = "_Test Support SLA" + company = None + if frappe.db.exists("Company", name): + company = frappe.get_doc("Company", name) + else: + company = frappe.get_doc( + { + "doctype": "Company", + "company_name": name, + "country": "India", + "default_currency": "INR", + "create_chart_of_accounts_based_on": "Standard Template", + "chart_of_accounts": "Standard", + } + ) + company = company.save() + + self.company = company.name def test_service_level_agreement(self): # Default Service Level Agreement @@ -205,7 +227,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # make lead with default SLA creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1) + lead = make_lead(creation=creation, index=1, company=self.company) self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -233,7 +255,7 @@ class TestServiceLevelAgreement(unittest.TestCase): ) creation = datetime.datetime(2020, 3, 4, 4, 0) - lead = make_lead(creation, index=2) + lead = make_lead(creation, index=2, company=self.company) frappe.flags.current_time = datetime.datetime(2020, 3, 4, 4, 15) lead.reload() @@ -267,7 +289,7 @@ class TestServiceLevelAgreement(unittest.TestCase): ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=1) + lead = make_lead(creation=creation, index=1, company=self.company) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) # failed with response time only @@ -294,7 +316,7 @@ class TestServiceLevelAgreement(unittest.TestCase): # fulfilled with response time only creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=2) + lead = make_lead(creation=creation, index=2, company=self.company) self.assertEqual(lead.service_level_agreement, lead_sla.name) self.assertEqual(lead.response_by, datetime.datetime(2019, 3, 4, 16, 0)) @@ -321,7 +343,7 @@ class TestServiceLevelAgreement(unittest.TestCase): apply_sla_for_resolution=0, ) creation = datetime.datetime(2019, 3, 4, 12, 0) - lead = make_lead(creation=creation, index=4) + lead = make_lead(creation=creation, index=4, company=self.company) applied_sla = frappe.db.get_value("Lead", lead.name, "service_level_agreement") self.assertFalse(applied_sla) @@ -611,7 +633,7 @@ def create_custom_doctype(): return frappe.get_doc("DocType", "Test SLA on Custom Dt") -def make_lead(creation=None, index=0): +def make_lead(creation=None, index=0, company=None): return frappe.get_doc( { "doctype": "Lead", @@ -621,5 +643,6 @@ def make_lead(creation=None, index=0): "creation": creation, "service_level_agreement_creation": creation, "priority": "Medium", + "company": company, } ).insert(ignore_permissions=True) From 9b50221bf05d1dec20d37dc1e3f4e9f0d4b9945f Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Thu, 13 Oct 2022 14:13:48 +0530 Subject: [PATCH 24/28] refactor: split ple creation function into two refactor create_payment_ledger_entry function into 2. one for generating ple map and one for DB entry creation --- erpnext/accounts/utils.py | 82 ++++++++++++++++++++++----------------- 1 file changed, 47 insertions(+), 35 deletions(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 67574ca9926..6667454c482 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1368,9 +1368,8 @@ def check_and_delete_linked_reports(report): frappe.delete_doc("Desktop Icon", icon) -def create_payment_ledger_entry( - gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 -): +def get_payment_ledger_entries(gl_entries, cancel=0): + ple_map = [] if gl_entries: ple = None @@ -1410,44 +1409,57 @@ def create_payment_ledger_entry( dr_or_cr *= -1 dr_or_cr_account_currency *= -1 - ple = frappe.get_doc( - { - "doctype": "Payment Ledger Entry", - "posting_date": gle.posting_date, - "company": gle.company, - "account_type": account_type, - "account": gle.account, - "party_type": gle.party_type, - "party": gle.party, - "cost_center": gle.cost_center, - "finance_book": gle.finance_book, - "due_date": gle.due_date, - "voucher_type": gle.voucher_type, - "voucher_no": gle.voucher_no, - "against_voucher_type": gle.against_voucher_type - if gle.against_voucher_type - else gle.voucher_type, - "against_voucher_no": gle.against_voucher if gle.against_voucher else gle.voucher_no, - "account_currency": gle.account_currency, - "amount": dr_or_cr, - "amount_in_account_currency": dr_or_cr_account_currency, - "delinked": True if cancel else False, - "remarks": gle.remarks, - } + ple = frappe._dict( + doctype="Payment Ledger Entry", + posting_date=gle.posting_date, + company=gle.company, + account_type=account_type, + account=gle.account, + party_type=gle.party_type, + party=gle.party, + cost_center=gle.cost_center, + finance_book=gle.finance_book, + due_date=gle.due_date, + voucher_type=gle.voucher_type, + voucher_no=gle.voucher_no, + against_voucher_type=gle.against_voucher_type + if gle.against_voucher_type + else gle.voucher_type, + against_voucher_no=gle.against_voucher if gle.against_voucher else gle.voucher_no, + account_currency=gle.account_currency, + amount=dr_or_cr, + amount_in_account_currency=dr_or_cr_account_currency, + delinked=True if cancel else False, + remarks=gle.remarks, ) dimensions_and_defaults = get_dimensions() if dimensions_and_defaults: for dimension in dimensions_and_defaults[0]: - ple.set(dimension.fieldname, gle.get(dimension.fieldname)) + ple[dimension.fieldname] = gle.get(dimension.fieldname) - if cancel: - delink_original_entry(ple) - ple.flags.ignore_permissions = 1 - ple.flags.adv_adj = adv_adj - ple.flags.from_repost = from_repost - ple.flags.update_outstanding = update_outstanding - ple.submit() + ple_map.append(ple) + return ple_map + + +def create_payment_ledger_entry( + gl_entries, cancel=0, adv_adj=0, update_outstanding="Yes", from_repost=0 +): + if gl_entries: + ple_map = get_payment_ledger_entries(gl_entries, cancel=cancel) + + for entry in ple_map: + + ple = frappe.get_doc(entry) + + if cancel: + delink_original_entry(ple) + + ple.flags.ignore_permissions = 1 + ple.flags.adv_adj = adv_adj + ple.flags.from_repost = from_repost + ple.flags.update_outstanding = update_outstanding + ple.submit() def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, party): From faadf78332a813b69ddb075820cf1693c51d0d48 Mon Sep 17 00:00:00 2001 From: Deepesh Garg Date: Mon, 17 Oct 2022 18:50:54 +0530 Subject: [PATCH 25/28] fix: Ignore linked purchase invoice on cancel --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js | 2 +- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index ec861a27879..c3a9855ff44 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -31,7 +31,7 @@ erpnext.accounts.PurchaseInvoice = class PurchaseInvoice extends erpnext.buying. super.onload(); // Ignore linked advances - this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry']; + this.frm.ignore_doctypes_on_cancel_all = ['Journal Entry', 'Payment Entry', 'Purchase Invoice']; if(!this.frm.doc.__islocal) { // show credit_to in print format diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5dbe7ebc863..3d74b8f139b 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -1415,6 +1415,7 @@ class PurchaseInvoice(BuyingController): "Stock Ledger Entry", "Repost Item Valuation", "Payment Ledger Entry", + "Purchase Invoice", ) self.update_advance_tax_references(cancel=1) From 43b80683ebd6a902fdb0ea0c4e11a8b2bee9841a Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 18 Oct 2022 09:33:37 +0530 Subject: [PATCH 26/28] fix: party type and party mandatory on updating outstanding --- erpnext/accounts/utils.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 67574ca9926..ae4ab64dd85 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -1467,7 +1467,12 @@ def update_voucher_outstanding(voucher_type, voucher_no, account, party_type, pa # on cancellation outstanding can be an empty list voucher_outstanding = ple_query.get_voucher_outstandings(vouchers, common_filter=common_filter) - if voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] and voucher_outstanding: + if ( + voucher_type in ["Sales Invoice", "Purchase Invoice", "Fees"] + and party_type + and party + and voucher_outstanding + ): outstanding = voucher_outstanding[0] ref_doc = frappe.get_doc(voucher_type, voucher_no) From 8f42e7f7034c6ba63b4eb3a6a122295f98cd04f8 Mon Sep 17 00:00:00 2001 From: ruthra kumar Date: Tue, 18 Oct 2022 17:24:36 +0530 Subject: [PATCH 27/28] test: use payable account in tax and to trigger party validation --- .../doctype/sales_invoice/test_sales_invoice.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 301d3e136ec..46c777d5f18 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3228,6 +3228,22 @@ class TestSalesInvoice(unittest.TestCase): self.assertTrue(return_si.docstatus == 1) + def test_sales_invoice_with_payable_tax_account(self): + si = create_sales_invoice(do_not_submit=True) + si.append( + "taxes", + { + "charge_type": "Actual", + "account_head": "Creditors - _TC", + "description": "Test", + "cost_center": "Main - _TC", + "tax_amount": 10, + "total": 10, + "dont_recompute_tax": 0, + }, + ) + self.assertRaises(frappe.ValidationError, si.submit) + def get_sales_invoice_for_e_invoice(): si = make_sales_invoice_for_ewaybill() From 96b4211ea1cfc9a10ba79b54b5645105f0925943 Mon Sep 17 00:00:00 2001 From: Maharshi Patel <39730881+maharshivpatel@users.noreply.github.com> Date: Tue, 18 Oct 2022 22:26:11 +0530 Subject: [PATCH 28/28] fix: pricing rule for non stock UOM and conversions * fix: pricing rule for non stock UOM and conversions --- .../doctype/pricing_rule/pricing_rule.py | 18 ++- .../doctype/pricing_rule/test_pricing_rule.py | 115 ++++++++++++++++++ .../accounts/doctype/pricing_rule/utils.py | 6 + .../sales_invoice/test_sales_invoice.py | 2 +- erpnext/public/js/controllers/transaction.js | 3 + 5 files changed, 142 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 9af3188e476..826d71b12e9 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -268,6 +268,18 @@ def get_serial_no_for_item(args): return item_details +def update_pricing_rule_uom(pricing_rule, args): + child_doc = {"Item Code": "items", "Item Group": "item_groups", "Brand": "brands"}.get( + pricing_rule.apply_on + ) + + apply_on_field = frappe.scrub(pricing_rule.apply_on) + + for row in pricing_rule.get(child_doc): + if row.get(apply_on_field) == args.get(apply_on_field): + pricing_rule.uom = row.uom + + def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import ( get_applied_pricing_rules, @@ -324,6 +336,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if isinstance(pricing_rule, str): pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) + update_pricing_rule_uom(pricing_rule, args) pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) or [] if pricing_rule.get("suggestion"): @@ -440,12 +453,15 @@ def apply_price_discount_rule(pricing_rule, item_details, args): if pricing_rule.currency == args.currency: pricing_rule_rate = pricing_rule.rate + # TODO https://github.com/frappe/erpnext/pull/23636 solve this in some other way. if pricing_rule_rate: + is_blank_uom = pricing_rule.get("uom") != args.get("uom") # Override already set price list rate (from item price) # if pricing_rule_rate > 0 item_details.update( { - "price_list_rate": pricing_rule_rate * args.get("conversion_factor", 1), + "price_list_rate": pricing_rule_rate + * (args.get("conversion_factor", 1) if is_blank_uom else 1), } ) item_details.update({"discount_percentage": 0.0}) diff --git a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py index 0a9db6b0f59..fbe567824f2 100644 --- a/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/test_pricing_rule.py @@ -595,6 +595,121 @@ class TestPricingRule(unittest.TestCase): frappe.get_doc("Item Price", {"item_code": "Water Flask"}).delete() item.delete() + def test_item_price_with_blank_uom_pricing_rule(self): + properties = { + "item_code": "Item Blank UOM", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item Blank UOM", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item Blank UOM Rule", + "apply_on": "Item Code", + "items": [ + { + "item_code": "Item Blank UOM", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, item_code="Item Blank UOM", uom="Box", conversion_factor=10 + ) + si.selling_price_list = "_Test Price List" + si.save() + + # If UOM is blank consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Box that have conversion_factor of 10 so 101 * 10 = 1010 + self.assertEqual(si.items[0].price_list_rate, 1010) + self.assertEqual(si.items[0].rate, 1010) + + si.delete() + + si = create_sales_invoice(do_not_save=True, item_code="Item Blank UOM", uom="Nos") + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is blank so consider it as stock UOM and apply pricing_rule on all UOM. + # rate is 101, Selling UOM is Nos that have conversion_factor of 1 so 101 * 1 = 101 + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item Blank UOM"}).delete() + + item.delete() + + def test_item_price_with_selling_uom_pricing_rule(self): + properties = { + "item_code": "Item UOM other than Stock", + "stock_uom": "Nos", + "sales_uom": "Box", + "uoms": [dict(uom="Box", conversion_factor=10)], + } + item = make_item(properties=properties) + + make_item_price("Item UOM other than Stock", "_Test Price List", 100) + + pricing_rule_record = { + "doctype": "Pricing Rule", + "title": "_Test Item UOM other than Stock Rule", + "apply_on": "Item Code", + "items": [ + { + "item_code": "Item UOM other than Stock", + "uom": "Box", + } + ], + "selling": 1, + "currency": "INR", + "rate_or_discount": "Rate", + "rate": 101, + "company": "_Test Company", + } + rule = frappe.get_doc(pricing_rule_record) + rule.insert() + + si = create_sales_invoice( + do_not_save=True, item_code="Item UOM other than Stock", uom="Box", conversion_factor=10 + ) + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so apply pricing_rule only on Box UOM. + # Selling UOM is Box and as both UOM are same no need to multiply by conversion_factor. + self.assertEqual(si.items[0].price_list_rate, 101) + self.assertEqual(si.items[0].rate, 101) + + si.delete() + + si = create_sales_invoice(do_not_save=True, item_code="Item UOM other than Stock", uom="Nos") + si.selling_price_list = "_Test Price List" + si.save() + + # UOM is Box so pricing_rule won't apply as selling_uom is Nos. + # As Pricing Rule is not applied price of 100 will be fetched from Item Price List. + self.assertEqual(si.items[0].price_list_rate, 100) + self.assertEqual(si.items[0].rate, 100) + + si.delete() + rule.delete() + frappe.get_doc("Item Price", {"item_code": "Item UOM other than Stock"}).delete() + + item.delete() + def test_pricing_rule_for_different_currency(self): make_item("Test Sanitizer Item") diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 1f29d732ba5..4c78d7261d0 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -111,6 +111,12 @@ def _get_pricing_rules(apply_on, args, values): ) if apply_on_field == "item_code": + if args.get("uom", None): + item_conditions += ( + " and ({child_doc}.uom='{item_uom}' or IFNULL({child_doc}.uom, '')='')".format( + child_doc=child_doc, item_uom=args.get("uom") + ) + ) if "variant_of" not in args: args.variant_of = frappe.get_cached_value("Item", args.item_code, "variant_of") diff --git a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py index 301d3e136ec..8d4ec38c81a 100644 --- a/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/test_sales_invoice.py @@ -3320,7 +3320,7 @@ def create_sales_invoice(**args): "asset": args.asset or None, "cost_center": args.cost_center or "_Test Cost Center - _TC", "serial_no": args.serial_no, - "conversion_factor": 1, + "conversion_factor": args.get("conversion_factor", 1), "incoming_rate": args.incoming_rate or 0, "batch_no": args.batch_no or None, }, diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index c17610b58a2..7fecb18fad4 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -426,6 +426,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe if(!this.validate_company_and_party()) { this.frm.fields_dict["items"].grid.grid_rows[item.idx - 1].remove(); } else { + item.pricing_rules = '' return this.frm.call({ method: "erpnext.stock.get_item_details.get_item_details", child: item, @@ -1045,6 +1046,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe uom(doc, cdt, cdn) { var me = this; var item = frappe.get_doc(cdt, cdn); + item.pricing_rules = '' if(item.item_code && item.uom) { return this.frm.call({ method: "erpnext.stock.get_item_details.get_conversion_factor", @@ -1121,6 +1123,7 @@ erpnext.TransactionController = class TransactionController extends erpnext.taxe qty(doc, cdt, cdn) { let item = frappe.get_doc(cdt, cdn); + item.pricing_rules = '' this.conversion_factor(doc, cdt, cdn, true); this.calculate_stock_uom_rate(doc, cdt, cdn); this.apply_pricing_rule(item, true);