diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 67ce6e6f7ef..167bef414f3 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -29,8 +29,9 @@ frappe.ui.form.on("Asset Repair", { }; }); - frm.set_query("purchase_invoice", function () { + frm.set_query("purchase_invoice", "invoices", function () { return { + query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice", filters: { company: frm.doc.company, docstatus: 1, @@ -58,6 +59,16 @@ frappe.ui.form.on("Asset Repair", { }, }; }); + + frm.set_query("expense_account", "invoices", function () { + return { + filters: { + company: frm.doc.company, + is_group: ["=", 0], + report_type: ["=", "Profit and Loss"], + }, + }; + }); }, refresh: function (frm) { diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.json b/erpnext/assets/doctype/asset_repair/asset_repair.json index 968b5321670..8195c56b431 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.json +++ b/erpnext/assets/doctype/asset_repair/asset_repair.json @@ -22,7 +22,8 @@ "column_break_14", "project", "accounting_details", - "purchase_invoice", + "invoices", + "section_break_y7cc", "capitalize_repair_cost", "stock_consumption", "column_break_8", @@ -229,26 +230,30 @@ "label": "Increase In Asset Life(Months)", "no_copy": 1 }, - { - "fieldname": "purchase_invoice", - "fieldtype": "Link", - "label": "Purchase Invoice", - "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0", - "no_copy": 1, - "options": "Purchase Invoice" - }, { "fetch_from": "asset.company", "fieldname": "company", "fieldtype": "Link", "label": "Company", "options": "Company" + }, + { + "fieldname": "invoices", + "fieldtype": "Table", + "label": "Asset Repair Purchase Invoices", + "mandatory_depends_on": "eval: doc.repair_status == 'Completed' && doc.repair_cost > 0;", + "no_copy": 1, + "options": "Asset Repair Purchase Invoice" + }, + { + "fieldname": "section_break_y7cc", + "fieldtype": "Section Break" } ], "index_web_pages_for_search": 1, "is_submittable": 1, "links": [], - "modified": "2024-06-13 16:14:14.398356", + "modified": "2024-09-30 13:02:06.931188", "modified_by": "Administrator", "module": "Assets", "name": "Asset Repair", diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index 4e73148828d..e42450e83e4 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 import DocType from frappe.utils import add_months, cint, flt, get_link_to_form, getdate, time_diff_in_hours import erpnext @@ -28,6 +29,9 @@ class AssetRepair(AccountsController): from erpnext.assets.doctype.asset_repair_consumed_item.asset_repair_consumed_item import ( AssetRepairConsumedItem, ) + from erpnext.assets.doctype.asset_repair_purchase_invoice.asset_repair_purchase_invoice import ( + AssetRepairPurchaseInvoice, + ) actions_performed: DF.LongText | None amended_from: DF.Link | None @@ -41,9 +45,9 @@ class AssetRepair(AccountsController): downtime: DF.Data | None failure_date: DF.Datetime increase_in_asset_life: DF.Int + invoices: DF.Table[AssetRepairPurchaseInvoice] naming_series: DF.Literal["ACC-ASR-.YYYY.-"] project: DF.Link | None - purchase_invoice: DF.Link | None repair_cost: DF.Currency repair_status: DF.Literal["Pending", "Completed", "Cancelled"] stock_consumption: DF.Check @@ -54,10 +58,15 @@ class AssetRepair(AccountsController): def validate(self): self.asset_doc = frappe.get_doc("Asset", self.asset) self.validate_dates() + self.validate_purchase_invoice() + self.validate_purchase_invoice_repair_cost() + self.validate_purchase_invoice_expense_account() self.update_status() if self.get("stock_items"): self.set_stock_items_cost() + + self.calculate_repair_cost() self.calculate_total_repair_cost() def validate_dates(self): @@ -66,6 +75,31 @@ class AssetRepair(AccountsController): _("Completion Date can not be before Failure Date. Please adjust the dates accordingly.") ) + def validate_purchase_invoice(self): + query = expense_item_pi_query(self.company) + purchase_invoice_list = [item[0] for item in query.run()] + for pi in self.invoices: + if pi.purchase_invoice not in purchase_invoice_list: + frappe.throw(_("Expense item not present in Purchase Invoice")) + + def validate_purchase_invoice_repair_cost(self): + for pi in self.invoices: + if flt(pi.repair_cost) > frappe.db.get_value( + "Purchase Invoice", pi.purchase_invoice, "base_net_total" + ): + frappe.throw(_("Repair cost cannot be greater than purchase invoice base net total")) + + def validate_purchase_invoice_expense_account(self): + for pi in self.invoices: + if pi.expense_account not in frappe.db.get_all( + "Purchase Invoice Item", {"parent": pi.purchase_invoice}, pluck="expense_account" + ): + frappe.throw( + _("Expense account not present in Purchase Invoice {0}").format( + get_link_to_form("Purchase Invoice", pi.purchase_invoice) + ) + ) + def update_status(self): if self.repair_status == "Pending" and self.asset_doc.status != "Out of Order": frappe.db.set_value("Asset", self.asset, "status", "Out of Order") @@ -82,6 +116,9 @@ class AssetRepair(AccountsController): for item in self.get("stock_items"): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + def calculate_repair_cost(self): + self.repair_cost = sum(flt(pi.repair_cost) for pi in self.invoices) + def calculate_total_repair_cost(self): self.total_repair_cost = flt(self.repair_cost) @@ -267,40 +304,39 @@ 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 - ) + debit_against_account = set() + for pi in self.invoices: + debit_against_account.add(pi.expense_account) + gl_entries.append( + self.get_gl_dict( + { + "account": pi.expense_account, + "credit": pi.repair_cost, + "credit_in_account_currency": pi.repair_cost, + "against": fixed_asset_account, + "voucher_type": self.doctype, + "voucher_no": self.name, + "cost_center": self.cost_center, + "posting_date": getdate(), + "company": self.company, + }, + item=self, + ) + ) + debit_against_account = ", ".join(debit_against_account) gl_entries.append( self.get_gl_dict( { "account": fixed_asset_account, "debit": self.repair_cost, "debit_in_account_currency": self.repair_cost, - "against": pi_expense_account, + "against": debit_against_account, "voucher_type": self.doctype, "voucher_no": self.name, "cost_center": self.cost_center, "posting_date": getdate(), "against_voucher_type": "Purchase Invoice", - "against_voucher": self.purchase_invoice, - "company": self.company, - }, - item=self, - ) - ) - - gl_entries.append( - self.get_gl_dict( - { - "account": pi_expense_account, - "credit": self.repair_cost, - "credit_in_account_currency": self.repair_cost, - "against": fixed_asset_account, - "voucher_type": self.doctype, - "voucher_no": self.name, - "cost_center": self.cost_center, - "posting_date": getdate(), "company": self.company, }, item=self, @@ -432,3 +468,31 @@ 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_purchase_invoice(doctype, txt, searchfield, start, page_len, filters): + query = expense_item_pi_query(filters.get("company")) + return query.run(as_list=1) + + +def expense_item_pi_query(company): + PurchaseInvoice = DocType("Purchase Invoice") + PurchaseInvoiceItem = DocType("Purchase Invoice Item") + Item = DocType("Item") + + query = ( + frappe.qb.from_(PurchaseInvoice) + .join(PurchaseInvoiceItem) + .on(PurchaseInvoiceItem.parent == PurchaseInvoice.name) + .join(Item) + .on(Item.name == PurchaseInvoiceItem.item_code) + .select(PurchaseInvoice.name) + .where( + (Item.is_stock_item == 0) + & (Item.is_fixed_asset == 0) + & (PurchaseInvoice.company == company) + & (PurchaseInvoice.docstatus == 1) + ) + ) + return query diff --git a/erpnext/assets/doctype/asset_repair/test_asset_repair.py b/erpnext/assets/doctype/asset_repair/test_asset_repair.py index 44d08869a63..48f67038f4d 100644 --- a/erpnext/assets/doctype/asset_repair/test_asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/test_asset_repair.py @@ -126,15 +126,17 @@ class TestAssetRepair(unittest.TestCase): def test_increase_in_asset_value_due_to_repair_cost_capitalisation(self): asset = create_asset(calculate_depreciation=1, submit=1) initial_asset_value = get_asset_value_after_depreciation(asset.name) - asset_repair = create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) + asset_repair = create_asset_repair( + asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1 + ) asset.reload() increase_in_asset_value = get_asset_value_after_depreciation(asset.name) - initial_asset_value self.assertEqual(asset_repair.repair_cost, increase_in_asset_value) def test_purchase_invoice(self): - asset_repair = create_asset_repair(capitalize_repair_cost=1, submit=1) - self.assertTrue(asset_repair.purchase_invoice) + asset_repair = create_asset_repair(capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1) + self.assertTrue(asset_repair.invoices) def test_gl_entries_with_perpetual_inventory(self): set_depreciation_settings_in_company(company="_Test Company with perpetual inventory") @@ -147,6 +149,7 @@ class TestAssetRepair(unittest.TestCase): "fixed_asset_account": "_Test Fixed Asset - TCP1", "accumulated_depreciation_account": "_Test Accumulated Depreciations - TCP1", "depreciation_expense_account": "_Test Depreciations - TCP1", + "capital_work_in_progress_account": "CWIP Account - TCP1", }, ) asset_category.save() @@ -156,6 +159,9 @@ class TestAssetRepair(unittest.TestCase): stock_consumption=1, warehouse="Stores - TCP1", company="_Test Company with perpetual inventory", + pi_expense_account1="Administrative Expenses - TCP1", + pi_expense_account2="Legal Expenses - TCP1", + item="_Test Non Stock Item", submit=1, ) @@ -181,16 +187,16 @@ class TestAssetRepair(unittest.TestCase): fixed_asset_account = get_asset_account( "fixed_asset_account", asset=asset_repair.asset, company=asset_repair.company ) - pi_expense_account = ( - frappe.get_doc("Purchase Invoice", asset_repair.purchase_invoice).items[0].expense_account - ) + pi_expense_accounts = [pi.expense_account for pi in asset_repair.invoices] + pi_repair_costs = [pi.repair_cost for pi in asset_repair.invoices] stock_entry_expense_account = ( frappe.get_doc("Stock Entry", {"asset_repair": asset_repair.name}).get("items")[0].expense_account ) expected_values = { fixed_asset_account: [asset_repair.total_repair_cost, 0], - pi_expense_account: [0, asset_repair.repair_cost], + pi_expense_accounts[0]: [0, pi_repair_costs[0]], + pi_expense_accounts[1]: [0, pi_repair_costs[1]], stock_entry_expense_account: [0, 100], } @@ -203,6 +209,7 @@ class TestAssetRepair(unittest.TestCase): asset_repair = create_asset_repair( capitalize_repair_cost=1, stock_consumption=1, + item="_Test Non Stock Item", submit=1, ) @@ -231,8 +238,14 @@ class TestAssetRepair(unittest.TestCase): default_expense_account = frappe.get_cached_value( "Company", asset_repair.company, "default_expense_account" ) + pi_expense_accounts = [pi.expense_account for pi in asset_repair.invoices] - expected_values = {fixed_asset_account: [1100, 0], default_expense_account: [0, 1100]} + expected_values = { + fixed_asset_account: [650, 0], + pi_expense_accounts[0]: [0, 250], + default_expense_account: [0, 100], + pi_expense_accounts[1]: [0, 300], + } for d in gl_entries: self.assertEqual(expected_values[d.account][0], d.debit) @@ -245,7 +258,7 @@ class TestAssetRepair(unittest.TestCase): self.assertEqual(first_asset_depr_schedule.status, "Active") initial_num_of_depreciations = num_of_depreciations(asset) - create_asset_repair(asset=asset, capitalize_repair_cost=1, submit=1) + create_asset_repair(asset=asset, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1) asset.reload() first_asset_depr_schedule.load_from_db() @@ -288,7 +301,6 @@ def create_asset_repair(**args): "asset_name": asset.asset_name, "failure_date": nowdate(), "description": "Test Description", - "repair_cost": 0, "company": asset.company, } ) @@ -351,16 +363,38 @@ def create_asset_repair(**args): if args.capitalize_repair_cost: asset_repair.capitalize_repair_cost = 1 - asset_repair.repair_cost = 1000 if asset.calculate_depreciation: asset_repair.increase_in_asset_life = 12 - pi = make_purchase_invoice( + pi1 = make_purchase_invoice( company=asset.company, - expense_account=frappe.db.get_value("Company", asset.company, "default_expense_account"), + item=args.item or "_Test Item", + expense_account=args.pi_expense_account1 or "Administrative Expenses - _TC", cost_center=asset_repair.cost_center, warehouse=args.warehouse or create_warehouse("Test Warehouse", company=asset.company), + rate="50", ) - asset_repair.purchase_invoice = pi.name + pi2 = make_purchase_invoice( + company=asset.company, + item=args.item or "_Test Item", + expense_account=args.pi_expense_account2 or "Legal Expenses - _TC", + cost_center=asset_repair.cost_center, + warehouse=args.warehouse or create_warehouse("Test Warehouse", company=asset.company), + rate="60", + ) + invoices = [ + { + "purchase_invoice": pi1.name, + "expense_account": args.pi_expense_account1 or "Administrative Expenses - _TC", + "repair_cost": args.pi_repair_cost1 or 250, + }, + { + "purchase_invoice": pi2.name, + "expense_account": args.pi_expense_account2 or "Legal Expenses - _TC", + "repair_cost": args.pi_repair_cost2 or 300, + }, + ] + for invoice in invoices: + asset_repair.append("invoices", invoice) asset_repair.submit() return asset_repair diff --git a/erpnext/assets/doctype/asset_repair_purchase_invoice/__init__.py b/erpnext/assets/doctype/asset_repair_purchase_invoice/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.json b/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.json new file mode 100644 index 00000000000..181fcf0278c --- /dev/null +++ b/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.json @@ -0,0 +1,50 @@ +{ + "actions": [], + "allow_rename": 1, + "creation": "2024-09-30 12:52:08.085813", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "purchase_invoice", + "expense_account", + "repair_cost" + ], + "fields": [ + { + "fieldname": "purchase_invoice", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Purchase Invoice", + "options": "Purchase Invoice" + }, + { + "fieldname": "expense_account", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Expense Account", + "options": "Account", + "reqd": 1 + }, + { + "fieldname": "repair_cost", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Repair Cost", + "options": "Company:company:default_currency", + "reqd": 1 + } + ], + "index_web_pages_for_search": 1, + "istable": 1, + "links": [], + "modified": "2024-09-30 13:02:43.040762", + "modified_by": "Administrator", + "module": "Assets", + "name": "Asset Repair Purchase Invoice", + "owner": "Administrator", + "permissions": [], + "sort_field": "creation", + "sort_order": "DESC", + "states": [] +} \ No newline at end of file diff --git a/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.py b/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.py new file mode 100644 index 00000000000..24c5988d6db --- /dev/null +++ b/erpnext/assets/doctype/asset_repair_purchase_invoice/asset_repair_purchase_invoice.py @@ -0,0 +1,25 @@ +# Copyright (c) 2024, Frappe Technologies Pvt. Ltd. and contributors +# For license information, please see license.txt + +# import frappe +from frappe.model.document import Document + + +class AssetRepairPurchaseInvoice(Document): + # begin: auto-generated types + # This code is auto-generated. Do not modify anything in this block. + + from typing import TYPE_CHECKING + + if TYPE_CHECKING: + from frappe.types import DF + + expense_account: DF.Link + parent: DF.Data + parentfield: DF.Data + parenttype: DF.Data + purchase_invoice: DF.Link | None + repair_cost: DF.Currency + # end: auto-generated types + + pass diff --git a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py index 82fa3ba17e9..1280bd929f2 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/test_asset_value_adjustment.py @@ -153,7 +153,9 @@ class TestAssetValueAdjustment(unittest.TestCase): post_depreciation_entries(getdate("2023-08-21")) # create asset repair - asset_repair = create_asset_repair(asset=asset_doc, capitalize_repair_cost=1, submit=1) + asset_repair = create_asset_repair( + asset=asset_doc, capitalize_repair_cost=1, item="_Test Non Stock Item", submit=1 + ) first_asset_depr_schedule = get_asset_depr_schedule_doc(asset_doc.name, "Active") self.assertEqual(first_asset_depr_schedule.status, "Active") @@ -177,8 +179,8 @@ class TestAssetValueAdjustment(unittest.TestCase): # Test gl entry creted from asset value adjustemnet expected_gle = ( - ("_Test Difference Account - _TC", 5625.29, 0.0), - ("_Test Fixed Asset - _TC", 0.0, 5625.29), + ("_Test Difference Account - _TC", 5175.29, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 5175.29), ) gle = frappe.db.sql( @@ -244,12 +246,12 @@ class TestAssetValueAdjustment(unittest.TestCase): ["2023-05-31", 9983.33, 45408.05], ["2023-06-30", 9983.33, 55391.38], ["2023-07-31", 9983.33, 65374.71], - ["2023-08-31", 8133.33, 73508.04], - ["2023-09-30", 8133.33, 81641.37], - ["2023-10-31", 8133.33, 89774.7], - ["2023-11-30", 8133.33, 97908.03], - ["2023-12-31", 8133.33, 106041.36], - ["2024-01-15", 8133.35, 114174.71], + ["2023-08-31", 8208.33, 73583.04], + ["2023-09-30", 8208.33, 81791.37], + ["2023-10-31", 8208.33, 89999.7], + ["2023-11-30", 8208.33, 98208.03], + ["2023-12-31", 8208.33, 106416.36], + ["2024-01-15", 8208.35, 114624.71], ] schedules = [