fix: correct logic for repair cost in asset repair

This commit is contained in:
ljain112
2026-03-09 18:03:59 +05:30
parent 3c77653508
commit c71557f432
3 changed files with 139 additions and 8 deletions

View File

@@ -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);
}
},
});

View File

@@ -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

View File

@@ -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