From e6160d1b634871ff37ea27cebc64b4d3575cc171 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 18:30:51 +0530 Subject: [PATCH 01/10] fix: correct logic for repair cost in asset repair --- .../doctype/asset_repair/asset_repair.js | 55 ++-- .../doctype/asset_repair/asset_repair.py | 239 +++++++++++++----- .../doctype/asset_repair/test_asset_repair.py | 33 +++ 3 files changed, 249 insertions(+), 78 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 5dc32d363d4..05a1aaa4e43 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -124,25 +124,46 @@ frappe.ui.form.on("Asset Repair", { frm.refresh_field("stock_items"); } }, +}); - purchase_invoice: function (frm) { - if (frm.doc.purchase_invoice) { - frappe.call({ - method: "frappe.client.get_value", - args: { - doctype: "Purchase Invoice", - fieldname: "base_net_total", - filters: { name: frm.doc.purchase_invoice }, - }, - callback: function (r) { - if (r.message) { - frm.set_value("repair_cost", r.message.base_net_total); - } - }, - }); - } else { - frm.set_value("repair_cost", 0); +frappe.ui.form.on("Asset Repair Purchase Invoice", { + purchase_invoice: function (frm, cdt, cdn) { + frappe.model.set_value(cdt, cdn, { + expense_account: "", + repair_cost: 0, + }); + }, + + expense_account: function (frm, cdt, cdn) { + var row = locals[cdt][cdn]; + + if (!row.purchase_invoice || !row.expense_account) { + frappe.model.set_value(cdt, cdn, "repair_cost", 0); + return; } + + frappe.call({ + method: "erpnext.assets.doctype.asset_repair.asset_repair.get_unallocated_repair_cost", + args: { + purchase_invoice: row.purchase_invoice, + expense_account: row.expense_account, + }, + callback: function (r) { + if (r.message !== undefined) { + frappe.model.set_value(cdt, cdn, "repair_cost", r.message); + + if (r.message <= 0) { + frappe.msgprint({ + title: __("No Available Amount"), + message: __( + "There is no available repair cost for this Purchase Invoice and Expense Account combination." + ), + indicator: "orange", + }); + } + } + }, + }); }, }); diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index dc74da28db4..401bff6beea 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -4,6 +4,7 @@ import frappe from frappe import _ from frappe.query_builder import DocType +from frappe.query_builder.functions import Sum from frappe.utils import cint, flt, get_link_to_form, getdate, time_diff_in_hours import erpnext @@ -83,10 +84,8 @@ class AssetRepair(AccountsController): def validate_purchase_invoices(self): for d in self.invoices: self.validate_purchase_invoice_status(d.purchase_invoice) - invoice_items = self.get_invoice_items(d.purchase_invoice) - self.validate_service_purchase_invoice(d.purchase_invoice, invoice_items) - self.validate_expense_account(d, invoice_items) - self.validate_purchase_invoice_repair_cost(d, invoice_items) + self.validate_expense_account(d) + self.validate_purchase_invoice_repair_cost(d) def validate_purchase_invoice_status(self, purchase_invoice): docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus") @@ -97,44 +96,37 @@ class AssetRepair(AccountsController): ) ) - def get_invoice_items(self, pi): - invoice_items = frappe.get_all( - "Purchase Invoice Item", - filters={"parent": pi}, - fields=["item_code", "expense_account", "base_net_amount"], + def validate_expense_account(self, row): + """Validate that the expense account exists in the purchase invoice for non-stock items.""" + valid_accounts = _get_expense_accounts_for_purchase_invoice(row.purchase_invoice) + if row.expense_account not in valid_accounts: + frappe.throw( + _( + "Row {0}: Expense account {1} is not valid for Purchase Invoice {2}. " + "Only expense accounts from non-stock items are allowed." + ).format( + row.idx, + frappe.bold(row.expense_account), + get_link_to_form("Purchase Invoice", row.purchase_invoice), + ) + ) + + def validate_purchase_invoice_repair_cost(self, row): + """Validate that repair cost doesn't exceed available amount.""" + available_amount = get_unallocated_repair_cost( + row.purchase_invoice, row.expense_account, exclude_asset_repair=self.name ) - return invoice_items - - def validate_service_purchase_invoice(self, purchase_invoice, invoice_items): - service_item_exists = False - for item in invoice_items: - if frappe.db.get_value("Item", item.item_code, "is_stock_item") == 0: - service_item_exists = True - break - - if not service_item_exists: + if flt(row.repair_cost) > available_amount: frappe.throw( - _("Service item not present in Purchase Invoice {0}").format( - get_link_to_form("Purchase Invoice", purchase_invoice) - ) - ) - - def validate_expense_account(self, row, invoice_items): - pi_expense_accounts = set([item.expense_account for item in invoice_items]) - if row.expense_account not in pi_expense_accounts: - frappe.throw( - _("Expense account {0} not present in Purchase Invoice {1}").format( - row.expense_account, get_link_to_form("Purchase Invoice", row.purchase_invoice) - ) - ) - - def validate_purchase_invoice_repair_cost(self, row, invoice_items): - pi_net_total = sum([flt(item.base_net_amount) for item in invoice_items]) - if flt(row.repair_cost) > pi_net_total: - frappe.throw( - _("Repair cost cannot be greater than purchase invoice base net total {0}").format( - pi_net_total + _( + "Row {0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}" + ).format( + row.idx, + frappe.bold(frappe.format_value(row.repair_cost, {"fieldtype": "Currency"})), + frappe.bold(frappe.format_value(available_amount, {"fieldtype": "Currency"})), + get_link_to_form("Purchase Invoice", row.purchase_invoice), + frappe.bold(row.expense_account), ) ) @@ -411,33 +403,158 @@ def get_downtime(failure_date, completion_date): @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_purchase_invoice(doctype, txt, searchfield, start, page_len, filters): - PurchaseInvoice = DocType("Purchase Invoice") - PurchaseInvoiceItem = DocType("Purchase Invoice Item") - Item = DocType("Item") + """ + Get Purchase Invoices that have expense accounts for non-stock items. + Only returns invoices with at least one non-stock, non-fixed-asset item with an expense account. + """ + pi = DocType("Purchase Invoice") + pi_item = DocType("Purchase Invoice Item") + item = DocType("Item") - return ( - frappe.qb.from_(PurchaseInvoice) - .join(PurchaseInvoiceItem) - .on(PurchaseInvoiceItem.parent == PurchaseInvoice.name) - .join(Item) - .on(Item.name == PurchaseInvoiceItem.item_code) - .select(PurchaseInvoice.name) + query = ( + frappe.qb.from_(pi) + .join(pi_item) + .on(pi_item.parent == pi.name) + .left_join(item) + .on(item.name == pi_item.item_code) + .select(pi.name) + .distinct() .where( - (Item.is_stock_item == 0) - & (Item.is_fixed_asset == 0) - & (PurchaseInvoice.company == filters.get("company")) - & (PurchaseInvoice.docstatus == 1) + (pi.company == filters.get("company")) + & (pi.docstatus == 1) + & (pi_item.is_fixed_asset == 0) + & (pi_item.expense_account.isnotnull()) + & (pi_item.expense_account != "") + & ((pi_item.item_code.isnull()) | (item.is_stock_item == 0)) ) - ).run(as_list=1) + ) + + if txt: + query = query.where(pi.name.like(f"%{txt}%")) + + return query.run(as_list=1) @frappe.whitelist() +@frappe.validate_and_sanitize_search_inputs def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters): - PurchaseInvoiceItem = DocType("Purchase Invoice Item") - return ( - frappe.qb.from_(PurchaseInvoiceItem) - .select(PurchaseInvoiceItem.expense_account) - .distinct() - .where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice")) - ).run(as_list=1) + """ + Get expense accounts for non-stock (service) items from the purchase invoice. + Used as a query function for link fields. + """ + purchase_invoice = filters.get("purchase_invoice") + if not purchase_invoice: + return [] + + expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice) + + # Filter by search text if provided + if txt: + txt = txt.lower() + expense_accounts = [acc for acc in expense_accounts if txt in acc.lower()] + + return [[account] for account in expense_accounts] + + +def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]: + """ + Internal function to get expense accounts for non-stock items from the purchase invoice. + + Args: + purchase_invoice: The Purchase Invoice name + + Returns: + List of expense account names + """ + pi_items = frappe.db.get_all( + "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) + + +@frappe.whitelist() +def get_unallocated_repair_cost( + purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None +) -> float: + """ + Calculate the unused repair cost for a purchase invoice and expense account. + """ + used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair) + total_amount = get_total_expense_amount(purchase_invoice, expense_account) + + return flt(total_amount - used_amount) + + +def get_allocated_repair_cost( + purchase_invoice: str, expense_account: str, exclude_asset_repair: str | None = None +) -> float: + """Get the total repair cost already allocated from submitted Asset Repairs.""" + asset_repair_pi = DocType("Asset Repair Purchase Invoice") + + query = ( + frappe.qb.from_(asset_repair_pi) + .select(Sum(asset_repair_pi.repair_cost).as_("total")) + .where( + (asset_repair_pi.purchase_invoice == purchase_invoice) + & (asset_repair_pi.expense_account == expense_account) + & (asset_repair_pi.docstatus == 1) + ) + ) + + if exclude_asset_repair: + query = query.where(asset_repair_pi.parent != exclude_asset_repair) + + result = query.run(as_dict=True) + + return flt(result[0].total) if result else 0.0 + + +def get_total_expense_amount(purchase_invoice: str, expense_account: str) -> float: + """Get the total expense amount from GL entries for a purchase invoice and account.""" + gl_entry = 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 == expense_account) + & (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 0e4ea84df86..b66dafc2a0f 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -173,6 +173,39 @@ class TestAssetRepair(IntegrationTestCase): ) self.assertTrue(asset_repair.invoices) + def test_repair_cost_exceeds_available_amount(self): + """Test that repair cost cannot exceed available amount from Purchase Invoice.""" + asset_repair1 = create_asset_repair( + capitalize_repair_cost=1, + item="_Test Non Stock Item", + submit=1, + ) + + pi_name = asset_repair1.invoices[0].purchase_invoice + expense_account = asset_repair1.invoices[0].expense_account + + asset_repair2 = frappe.new_doc("Asset Repair") + asset_repair2.update( + { + "asset": asset_repair1.asset, + "asset_name": asset_repair1.asset_name, + "failure_date": nowdate(), + "description": "Second Repair", + "company": asset_repair1.company, + "capitalize_repair_cost": 1, + } + ) + asset_repair2.append( + "invoices", + { + "purchase_invoice": pi_name, + "expense_account": expense_account, + "repair_cost": 10, # PI already fully used, so this should fail + }, + ) + + self.assertRaises(frappe.ValidationError, asset_repair2.save) + def test_gl_entries_with_perpetual_inventory(self): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") From b9aaae63435b289eaf54438c2a910c9858be2b5b Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 18:36:13 +0530 Subject: [PATCH 02/10] fix: remove unnecessary filtering by search text in get_expense_accounts --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 ----- 1 file changed, 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 401bff6beea..066083a3b0f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -450,11 +450,6 @@ def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters): expense_accounts = _get_expense_accounts_for_purchase_invoice(purchase_invoice) - # Filter by search text if provided - if txt: - txt = txt.lower() - expense_accounts = [acc for acc in expense_accounts if txt in acc.lower()] - return [[account] for account in expense_accounts] From 00ffdee92875948ab76c0ccbfcc372c7b55a48b5 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 18:45:07 +0530 Subject: [PATCH 03/10] fix: update repair cost logic to set value only for positive amounts --- .../doctype/asset_repair/asset_repair.js | 18 ++++++++++++------ 1 file changed, 12 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 05a1aaa4e43..fbbf16120a7 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -135,7 +135,7 @@ frappe.ui.form.on("Asset Repair Purchase Invoice", { }, expense_account: function (frm, cdt, cdn) { - var row = locals[cdt][cdn]; + let row = locals[cdt][cdn]; if (!row.purchase_invoice || !row.expense_account) { frappe.model.set_value(cdt, cdn, "repair_cost", 0); @@ -150,13 +150,19 @@ frappe.ui.form.on("Asset Repair Purchase Invoice", { }, callback: function (r) { if (r.message !== undefined) { - frappe.model.set_value(cdt, cdn, "repair_cost", r.message); - - if (r.message <= 0) { + if (r.message > 0) { + frappe.model.set_value(cdt, cdn, "repair_cost", r.message); + } else { + frappe.model.set_value(cdt, cdn, "repair_cost", 0); + let pi_link = frappe.utils.get_form_link( + "Purchase Invoice", + row.purchase_invoice, + true + ); frappe.msgprint({ - title: __("No Available Amount"), message: __( - "There is no available repair cost for this Purchase Invoice and Expense Account combination." + "Row {0}: The entire expense amount for account {1} in {2} has already been allocated.", + [row.idx, row.expense_account.bold(), pi_link] ), indicator: "orange", }); From c2810ea799eeb56333eb64afe48ff50ca80606d3 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 18:54:37 +0530 Subject: [PATCH 04/10] perf: replace get_doc with get_lazy_doc for asset retrieval and optimize stock entry fetching --- erpnext/assets/doctype/asset_repair/asset_repair.py | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 066083a3b0f..79e417200fd 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -57,7 +57,7 @@ class AssetRepair(AccountsController): # end: auto-generated types def validate(self): - self.asset_doc = frappe.get_doc("Asset", self.asset) + self.asset_doc = frappe.get_lazy_doc("Asset", self.asset) self.validate_asset() self.validate_dates() self.validate_purchase_invoices() @@ -188,7 +188,7 @@ class AssetRepair(AccountsController): self.cancel_sabb() def after_delete(self): - frappe.get_doc("Asset", self.asset).set_status() + frappe.get_lazy_doc("Asset", self.asset).set_status() def check_repair_status(self): if self.repair_status == "Pending" and self.docstatus == 1: @@ -324,7 +324,10 @@ class AssetRepair(AccountsController): return # creating GL Entries for each row in Stock Items based on the Stock Entry created for it - stock_entry = frappe.get_doc("Stock Entry", {"asset_repair": self.name}) + stock_entry_name = frappe.db.get_value("Stock Entry", {"asset_repair": self.name}, "name") + stock_entry_items = frappe.get_all( + "Stock Entry Detail", filters={"parent": stock_entry_name}, fields=["expense_account", "amount"] + ) default_expense_account = None if not erpnext.is_perpetual_inventory_enabled(self.company): @@ -334,7 +337,7 @@ class AssetRepair(AccountsController): if not default_expense_account: frappe.throw(_("Please set default Expense Account in Company {0}").format(self.company)) - for item in stock_entry.items: + for item in stock_entry_items: if flt(item.amount) > 0: gl_entries.append( self.get_gl_dict( @@ -365,7 +368,7 @@ class AssetRepair(AccountsController): "cost_center": self.cost_center, "posting_date": self.completion_date, "against_voucher_type": "Stock Entry", - "against_voucher": stock_entry.name, + "against_voucher": stock_entry_name, "company": self.company, }, item=self, From 0b84d116005f7ca81b0e04c405cc43c1bfe9c3e0 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 19:09:54 +0530 Subject: [PATCH 05/10] perf: enhance validation for purchase invoices to check submission status for all invoices --- .../doctype/asset_repair/asset_repair.py | 34 ++++++++++++++----- 1 file changed, 26 insertions(+), 8 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 79e417200fd..ac172b9c42f 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -82,19 +82,37 @@ class AssetRepair(AccountsController): ) def validate_purchase_invoices(self): + self.validate_purchase_invoice_status() + for d in self.invoices: - self.validate_purchase_invoice_status(d.purchase_invoice) self.validate_expense_account(d) self.validate_purchase_invoice_repair_cost(d) - def validate_purchase_invoice_status(self, purchase_invoice): - docstatus = frappe.db.get_value("Purchase Invoice", purchase_invoice, "docstatus") - if docstatus == 0: - frappe.throw( - _("{0} is still in Draft. Please submit it before saving the Asset Repair.").format( - get_link_to_form("Purchase Invoice", purchase_invoice) - ) + def validate_purchase_invoice_status(self): + pi_names = [row.purchase_invoice for row in self.invoices] + docstatus = frappe._dict( + frappe.db.get_all( + "Purchase Invoice", + filters={"name": ["in", pi_names]}, + fields=["name", "docstatus"], + as_list=True, ) + ) + + invalid_invoice = [] + for row in self.invoices: + if docstatus.get(row.purchase_invoice) != 1: + invalid_invoice.append((row.idx, row.purchase_invoice)) + + if invalid_invoice: + invoice_links = "".join( + [ + f"
  • {_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)}
  • " + for idx, pi in invalid_invoice + ] + ) + msg = _("The following Purchase Invoices are not submitted:") + f"
      {invoice_links}
    " + frappe.throw(msg) def validate_expense_account(self, row): """Validate that the expense account exists in the purchase invoice for non-stock items.""" From ff9b39202447d18f84dc4173c4982e587b1cd927 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Fri, 28 Nov 2025 19:13:31 +0530 Subject: [PATCH 06/10] fix: add duplicate purchase invoice validation in asset repair --- .../doctype/asset_repair/asset_repair.py | 26 +++++++++++++++++-- 1 file changed, 24 insertions(+), 2 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index ac172b9c42f..7686d72959b 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -82,12 +82,34 @@ class AssetRepair(AccountsController): ) def validate_purchase_invoices(self): + self.validate_duplicate_purchase_invoices() self.validate_purchase_invoice_status() for d in self.invoices: self.validate_expense_account(d) self.validate_purchase_invoice_repair_cost(d) + def validate_duplicate_purchase_invoices(self): + # account wise duplicate check + purchase_invoices = set() + duplicates = [] + for row in self.invoices: + key = (row.purchase_invoice, row.expense_account) + if key in purchase_invoices: + duplicates.append((row.idx, row.purchase_invoice, row.expense_account)) + else: + purchase_invoices.add(key) + + if duplicates: + duplicate_links = "".join( + [ + f"
  • {_('Row #{0}:').format(idx)} {get_link_to_form('Purchase Invoice', pi)} - {frappe.bold(account)}
  • " + for idx, pi, account in duplicates + ] + ) + msg = _("The following rows are duplicates:") + f"
      {duplicate_links}
    " + frappe.throw(msg) + def validate_purchase_invoice_status(self): pi_names = [row.purchase_invoice for row in self.invoices] docstatus = frappe._dict( @@ -120,7 +142,7 @@ class AssetRepair(AccountsController): if row.expense_account not in valid_accounts: frappe.throw( _( - "Row {0}: Expense account {1} is not valid for Purchase Invoice {2}. " + "Row #{0}: Expense account {1} is not valid for Purchase Invoice {2}. " "Only expense accounts from non-stock items are allowed." ).format( row.idx, @@ -138,7 +160,7 @@ class AssetRepair(AccountsController): if flt(row.repair_cost) > available_amount: frappe.throw( _( - "Row {0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}" + "Row #{0}: Repair cost {1} exceeds available amount {2} for Purchase Invoice {3} and Account {4}" ).format( row.idx, frappe.bold(frappe.format_value(row.repair_cost, {"fieldtype": "Currency"})), From 0c1df307718d92e1b50258d9bf4625a95268ef39 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 1 Dec 2025 13:13:55 +0530 Subject: [PATCH 07/10] fix: add permission check --- erpnext/assets/doctype/asset_repair/asset_repair.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 7686d72959b..9a58ce286c1 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -550,6 +550,11 @@ def get_unallocated_repair_cost( """ Calculate the unused repair cost for a purchase invoice and expense account. """ + if not purchase_invoice or not expense_account: + return 0.0 + + frappe.has_permission("Purchase Invoice","read", purchase_invoice, throw=True) + used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair) total_amount = get_total_expense_amount(purchase_invoice, expense_account) From 2a0ba84f693c33a3b6c8ee6d684f205bacfae576 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 1 Dec 2025 13:17:47 +0530 Subject: [PATCH 08/10] refactor: linters --- erpnext/assets/doctype/asset_repair/asset_repair.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 9a58ce286c1..d9a6742a59a 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -553,7 +553,7 @@ def get_unallocated_repair_cost( if not purchase_invoice or not expense_account: return 0.0 - frappe.has_permission("Purchase Invoice","read", purchase_invoice, throw=True) + frappe.has_permission("Purchase Invoice", "read", purchase_invoice, throw=True) used_amount = get_allocated_repair_cost(purchase_invoice, expense_account, exclude_asset_repair) total_amount = get_total_expense_amount(purchase_invoice, expense_account) From 8ee2cbf259abf9d1357f28ebd2b0bf8945b2c0d8 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 1 Dec 2025 17:05:00 +0530 Subject: [PATCH 09/10] chore: remove unwanted strings --- erpnext/assets/doctype/asset_repair/asset_repair.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index d9a6742a59a..0869f80dc79 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -498,13 +498,7 @@ def get_expense_accounts(doctype, txt, searchfield, start, page_len, filters): def _get_expense_accounts_for_purchase_invoice(purchase_invoice: str) -> list[str]: """ - Internal function to get expense accounts for non-stock items from the purchase invoice. - - Args: - purchase_invoice: The Purchase Invoice name - - Returns: - List of expense account names + Get expense accounts for non-stock items from the purchase invoice. """ pi_items = frappe.db.get_all( "Purchase Invoice Item", From e1fd90f731c48780929b7938fb40297e1b9a63b0 Mon Sep 17 00:00:00 2001 From: ljain112 Date: Mon, 1 Dec 2025 17:06:42 +0530 Subject: [PATCH 10/10] chore: remove unused import for depreciation schedule --- erpnext/assets/doctype/asset_repair/asset_repair.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 0869f80dc79..cbe0f711f15 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -12,7 +12,6 @@ from erpnext.accounts.general_ledger import make_gl_entries from erpnext.assets.doctype.asset.asset import get_asset_account from erpnext.assets.doctype.asset_activity.asset_activity import add_asset_activity from erpnext.assets.doctype.asset_depreciation_schedule.asset_depreciation_schedule import ( - get_depr_schedule, reschedule_depreciation, ) from erpnext.controllers.accounts_controller import AccountsController