refactor(postgres): port Assets module queries to the query builder

Convert the remaining raw frappe.db.sql in the Assets module to frappe.qb / ORM so
the queries run on PostgreSQL as well as MariaDB. Faithful 1:1 conversions -- no
MariaDB behaviour change:

- asset.py (gl-entry / bom-cost fetches), asset_maintenance.py (team members),
  asset_movement.py (latest location/custodian), location.py (get_children)
- fixed_asset_register.py: the depreciation-amount aggregate groups by asset.name
  (the primary key) selecting only asset.name + Sum(gle.debit), which is valid under
  Postgres strict GROUP BY (PK functional dependency)

Tests: existing asset (61), asset_maintenance, asset_movement and location suites
pass on both engines; adds a test for the previously-untested Fixed Asset Register
report (covers the GROUP BY aggregate on both engines).

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Mihir Kandoi
2026-06-19 09:48:35 +05:30
parent facb27c3f4
commit fc9608d14d
6 changed files with 103 additions and 76 deletions

View File

@@ -735,12 +735,10 @@ class Asset(AccountsController):
frappe.throw(_("Asset cannot be cancelled, as it is already {0}").format(self.status))
def cancel_movement_entries(self):
movements = frappe.db.sql(
"""SELECT asm.name, asm.docstatus
FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item
WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""",
self.name,
as_dict=1,
movements = frappe.get_all(
"Asset Movement Item",
filters={"asset": self.name, "docstatus": 1},
fields=["parent as name"],
)
for movement in movements:
@@ -860,15 +858,18 @@ class Asset(AccountsController):
cwip_enabled = is_cwip_accounting_enabled(self.asset_category)
cwip_account = self.get_cwip_account(cwip_enabled=cwip_enabled)
query = """SELECT name FROM `tabGL Entry` WHERE voucher_no = %s and account = %s"""
if asset_bought_with_invoice:
# with invoice purchase either expense or cwip has been booked
expense_booked = frappe.db.sql(query, (purchase_document, fixed_asset_account), as_dict=1)
expense_booked = frappe.db.exists(
"GL Entry", {"voucher_no": purchase_document, "account": fixed_asset_account}
)
if expense_booked:
# if expense is already booked from invoice then do not make gl entries regardless of cwip enabled/disabled
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
cwip_booked = frappe.db.exists(
"GL Entry", {"voucher_no": purchase_document, "account": cwip_account}
)
if cwip_booked:
# if cwip is booked from invoice then make gl entries regardless of cwip enabled/disabled
return True
@@ -878,10 +879,11 @@ class Asset(AccountsController):
# if cwip account isn't available do not make gl entries
return False
cwip_booked = frappe.db.sql(query, (purchase_document, cwip_account), as_dict=1)
# if cwip is not booked from receipt then do not make gl entries
# if cwip is booked from receipt then make gl entries
return cwip_booked
return bool(
frappe.db.exists("GL Entry", {"voucher_no": purchase_document, "account": cwip_account})
)
def get_purchase_document(self):
asset_bought_with_invoice = self.purchase_invoice and frappe.db.get_value(
@@ -1074,11 +1076,15 @@ def make_post_gl_entry():
for asset_category in asset_categories:
if cint(asset_category.enable_cwip_accounting):
assets = frappe.db.sql_list(
""" select name from `tabAsset`
where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0
and available_for_use_date = %s and docstatus = 1""",
(asset_category.name, nowdate()),
assets = frappe.get_all(
"Asset",
filters={
"asset_category": asset_category.name,
"booked_fixed_asset": 0,
"available_for_use_date": nowdate(),
"docstatus": 1,
},
pluck="name",
)
for asset in assets:

View File

@@ -79,11 +79,14 @@ def assign_tasks(asset_maintenance_name, assign_to_member, maintenance_task, nex
"description": maintenance_task,
"date": next_due_date,
}
if not frappe.db.sql(
"""select owner from `tabToDo`
where reference_type=%(doctype)s and reference_name=%(name)s and status='Open'
and owner=%(assign_to)s""",
args,
if not frappe.db.exists(
"ToDo",
{
"reference_type": args["doctype"],
"reference_name": args["name"],
"status": "Open",
"owner": args["assign_to"],
},
):
# assign_to function expects a list
args["assign_to"] = [args["assign_to"]]
@@ -187,13 +190,9 @@ def get_team_members(
@frappe.whitelist()
def get_maintenance_log(asset_name: str):
return frappe.db.sql(
"""
select maintenance_status, count(asset_name) as count, asset_name
from `tabAsset Maintenance Log`
where asset_name=%s
group by maintenance_status
""",
(asset_name,),
as_dict=1,
return frappe.get_all(
"Asset Maintenance Log",
filters={"asset_name": asset_name},
fields=["maintenance_status", {"COUNT": "asset_name", "as": "count"}, "asset_name"],
group_by="maintenance_status, asset_name",
)

View File

@@ -127,24 +127,20 @@ class AssetMovement(Document):
def get_latest_location_and_custodian(self, asset):
current_location, current_employee = "", ""
cond = "1=1"
# latest entry corresponds to current document's location, employee when transaction date > previous dates
# In case of cancellation it corresponds to previous latest document's location, employee
args = {"asset": asset, "company": self.company}
latest_movement_entry = frappe.db.sql(
f"""
SELECT asm_item.target_location, asm_item.to_employee
FROM `tabAsset Movement Item` asm_item
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
WHERE
asm_item.asset = %(asset)s AND
asm.company = %(company)s AND
asm.docstatus = 1 AND {cond}
ORDER BY asm.transaction_date DESC
LIMIT 1
""",
args,
asm = frappe.qb.DocType("Asset Movement")
asm_item = frappe.qb.DocType("Asset Movement Item")
latest_movement_entry = (
frappe.qb.from_(asm_item)
.inner_join(asm)
.on(asm_item.parent == asm.name)
.select(asm_item.target_location, asm_item.to_employee)
.where((asm_item.asset == asset) & (asm.company == self.company) & (asm.docstatus == 1))
.orderby(asm.transaction_date, order=frappe.qb.desc)
.limit(1)
.run()
)
if latest_movement_entry:

View File

@@ -215,17 +215,12 @@ def get_children(doctype: str, parent: str | None = None, location: str | None =
if parent is None or parent == "All Locations":
parent = ""
return frappe.db.sql(
f"""
select
name as value,
is_group as expandable
from
`tabLocation` comp
where
ifnull(parent_location, "")={frappe.db.escape(parent)}
""",
as_dict=1,
filters = {"parent_location": parent} if parent else {"parent_location": ["is", "not set"]}
return frappe.get_all(
"Location",
filters=filters,
fields=["name as value", "is_group as expandable"],
)

View File

@@ -395,32 +395,30 @@ def get_group_by_data(
def get_purchase_receipt_supplier_map():
pr = frappe.qb.DocType("Purchase Receipt")
pri = frappe.qb.DocType("Purchase Receipt Item")
return frappe._dict(
frappe.db.sql(
""" Select
pr.name, pr.supplier
FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri
WHERE
pri.parent = pr.name
AND pri.is_fixed_asset=1
AND pr.docstatus=1
AND pr.is_return=0"""
)
frappe.qb.from_(pr)
.inner_join(pri)
.on(pri.parent == pr.name)
.select(pr.name, pr.supplier)
.distinct()
.where((pri.is_fixed_asset == 1) & (pr.docstatus == 1) & (pr.is_return == 0))
.run()
)
def get_purchase_invoice_supplier_map():
pi = frappe.qb.DocType("Purchase Invoice")
pii = frappe.qb.DocType("Purchase Invoice Item")
return frappe._dict(
frappe.db.sql(
""" Select
pi.name, pi.supplier
FROM `tabPurchase Invoice` pi, `tabPurchase Invoice Item` pii
WHERE
pii.parent = pi.name
AND pii.is_fixed_asset=1
AND pi.docstatus=1
AND pi.is_return=0"""
)
frappe.qb.from_(pi)
.inner_join(pii)
.on(pii.parent == pi.name)
.select(pi.name, pi.supplier)
.distinct()
.where((pii.is_fixed_asset == 1) & (pi.docstatus == 1) & (pi.is_return == 0))
.run()
)

View File

@@ -0,0 +1,33 @@
# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and Contributors
# License: GNU General Public License v3. See license.txt
import frappe
from erpnext.assets.doctype.asset.test_asset import AssetSetup, create_asset
from erpnext.assets.report.fixed_asset_register.fixed_asset_register import execute
class TestFixedAssetRegister(AssetSetup):
def test_report_lists_submitted_asset(self):
"""Exercises the report's converted queries -- including the depreciation aggregate that groups
by asset.name (must be valid on Postgres) -- by asserting a submitted asset is listed."""
asset = create_asset(
item_code="Macbook Pro",
purchase_date="2020-01-01",
available_for_use_date="2020-06-06",
location="Test Location",
submit=1,
)
filters = frappe._dict(
{
"company": "_Test Company",
"status": "In Location",
"filter_based_on": "Date Range",
"from_date": "2020-01-01",
"to_date": "2030-12-31",
"date_based_on": "Purchase Date",
}
)
data = execute(filters)[1]
asset_ids = {row.get("asset_id") for row in data}
self.assertIn(asset.name, asset_ids)