diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 48bf4ff478d..a3ddd066ebd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -111,15 +111,13 @@ frappe.ui.form.on("Asset Repair", { purchase_invoice: function (frm) { if (frm.doc.purchase_invoice) { frappe.call({ - method: "frappe.client.get_value", + method: "erpnext.assets.doctype.asset_repair.asset_repair.get_repair_cost_for_purchase_invoice", args: { - doctype: "Purchase Invoice", - fieldname: "base_net_total", - filters: { name: frm.doc.purchase_invoice }, + purchase_invoice: frm.doc.purchase_invoice, }, callback: function (r) { if (r.message) { - frm.set_value("repair_cost", r.message.base_net_total); + frm.set_value("repair_cost", r.message); } }, }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index e0d41919c4a..4d59ba5b96c 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -3,6 +3,7 @@ import frappe from frappe import _ +from frappe.query_builder.functions import Sum from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours import erpnext @@ -308,9 +309,14 @@ class AssetRepair(AccountsController): if flt(self.repair_cost) <= 0: return - pi_expense_account = ( - frappe.get_doc("Purchase Invoice", self.purchase_invoice).items[0].expense_account - ) + expense_accounts = _get_expense_accounts_for_purchase_invoice(self.purchase_invoice) + + if not expense_accounts: + frappe.throw( + _("No expense accounts found for Purchase Invoice {0}").format(self.purchase_invoice) + ) + + pi_expense_account = expense_accounts[0] gl_entries.append( self.get_gl_dict( @@ -473,3 +479,82 @@ class AssetRepair(AccountsController): def get_downtime(failure_date, completion_date): downtime = time_diff_in_hours(completion_date, failure_date) return round(downtime, 2) + + +@frappe.whitelist() +def get_repair_cost_for_purchase_invoice(purchase_invoice: str) -> float: + """ + Get the total repair cost from GL entries for a purchase invoice. + Only considers expense accounts for non-stock, non-fixed-asset items. + """ + if not purchase_invoice: + return 0.0 + + expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice) + + if not expense_accounts: + return 0.0 + + return _get_total_expense_amount(purchase_invoice, expense_accounts) + + +def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]: + """ + Get expense accounts for non-stock items from the purchase invoice. + """ + pi_items = frappe.db.get_list( + "Purchase Invoice Item", + filters={"parent": purchase_invoice}, + fields=["item_code", "expense_account", "is_fixed_asset"], + ) + + if not pi_items: + return [] + + # Get list of stock item codes from the invoice + item_codes = {item.item_code for item in pi_items if item.item_code} + stock_items = set() + if item_codes: + stock_items = set( + frappe.db.get_all( + "Item", filters={"name": ["in", list(item_codes)], "is_stock_item": 1}, pluck="name" + ) + ) + + expense_accounts = set() + + for item in pi_items: + # Skip stock items - they use warehouse accounts + if item.item_code and item.item_code in stock_items: + continue + + # Skip fixed assets - they use asset accounts + if item.is_fixed_asset: + continue + + # Use expense account from Purchase Invoice Item + if item.expense_account: + expense_accounts.add(item.expense_account) + + return list(expense_accounts) + + +def _get_total_expense_amount(purchase_invoice: str, expense_accounts: list[str]) -> float: + """Get the total expense amount from GL entries for a purchase invoice and accounts.""" + if not expense_accounts: + return 0.0 + + gl_entry = frappe.qb.DocType("GL Entry") + + result = ( + frappe.qb.from_(gl_entry) + .select((Sum(gl_entry.debit) - Sum(gl_entry.credit)).as_("total")) + .where( + (gl_entry.voucher_type == "Purchase Invoice") + & (gl_entry.voucher_no == purchase_invoice) + & (gl_entry.account.isin(expense_accounts)) + & (gl_entry.is_cancelled == 0) + ) + ).run(as_dict=True) + + return flt(result[0].total) if result else 0.0 diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 4cd304fbfd0..1b5290ca8ea 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -8,6 +8,7 @@ from frappe import qb from frappe.query_builder.functions import Sum from frappe.utils import add_days, add_months, flt, get_first_day, nowdate, nowtime, today +from erpnext.accounts.doctype.purchase_invoice.test_purchase_invoice import make_purchase_invoice from erpnext.assets.doctype.asset.asset import ( get_asset_account, get_asset_value_after_depreciation, @@ -21,6 +22,7 @@ from erpnext.assets.doctype.asset.test_asset import ( from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( get_asset_depr_schedule_doc, ) +from erpnext.assets.doctype.asset_repair.asset_repair import get_repair_cost_for_purchase_invoice from erpnext.stock.doctype.item.test_item import create_item from erpnext.stock.doctype.serial_and_batch_bundle.test_serial_and_batch_bundle import ( get_serial_nos_from_bundle, @@ -321,6 +323,52 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(asset.additional_asset_cost, asset_repair.repair_cost) self.assertEqual(booked_value, asset_repair.repair_cost) + def test_repair_cost_fetches_only_service_item_amount(self): + """Test that repair cost only includes service (non-stock) item amounts from purchase invoice.""" + + service_item = create_item( + "_Test Service Item for Repair", + is_stock_item=0, + company="_Test Company", + ) + + stock_item = create_item( + "_Test Stock Item for Repair", + is_stock_item=1, + company="_Test Company", + ) + + expense_account = frappe.db.get_value("Company", "_Test Company", "default_expense_account") + cost_center = frappe.db.get_value("Company", "_Test Company", "cost_center") + + pi = make_purchase_invoice( + item_code=service_item.name, + qty=1, + rate=500, + expense_account=expense_account, + cost_center=cost_center, + update_stock=0, + do_not_submit=1, + ) + + pi.update_stock = 1 + pi.append( + "items", + { + "item_code": stock_item.name, + "qty": 2, + "rate": 300, + "warehouse": "_Test Warehouse - _TC", + "cost_center": cost_center, + }, + ) + pi.save() + pi.submit() + + repair_cost = get_repair_cost_for_purchase_invoice(pi.name) + + self.assertEqual(repair_cost, 500) + def num_of_depreciations(asset): return asset.finance_books[0].total_number_of_depreciations