From 0e4706b074ad34fc27bd90359d468ebb6292995b Mon Sep 17 00:00:00 2001 From: Khushi Rawat <142375893+khushi8112@users.noreply.github.com> Date: Mon, 5 May 2025 12:04:05 +0530 Subject: [PATCH] fix: conflict in asset repair --- .../doctype/asset_repair/asset_repair.js | 21 +- .../doctype/asset_repair/asset_repair.py | 341 +++++++----------- 2 files changed, 141 insertions(+), 221 deletions(-) diff --git a/erpnext/assets/doctype/asset_repair/asset_repair.js b/erpnext/assets/doctype/asset_repair/asset_repair.js index 167bef414f3..3ce1d5390db 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.js +++ b/erpnext/assets/doctype/asset_repair/asset_repair.js @@ -34,7 +34,16 @@ frappe.ui.form.on("Asset Repair", { query: "erpnext.assets.doctype.asset_repair.asset_repair.get_purchase_invoice", filters: { company: frm.doc.company, - docstatus: 1, + }, + }; + }); + + frm.set_query("expense_account", "invoices", function (doc, cdt, cdn) { + let row = locals[cdt][cdn]; + return { + query: "erpnext.assets.doctype.asset_repair.asset_repair.get_expense_accounts", + filters: { + purchase_invoice: row.purchase_invoice, }, }; }); @@ -59,16 +68,6 @@ 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.py b/erpnext/assets/doctype/asset_repair/asset_repair.py index c6ae3c5d8e4..4b4ff1a5383 100644 --- a/erpnext/assets/doctype/asset_repair/asset_repair.py +++ b/erpnext/assets/doctype/asset_repair/asset_repair.py @@ -40,6 +40,7 @@ class AssetRepair(AccountsController): capitalize_repair_cost: DF.Check company: DF.Link | None completion_date: DF.Datetime | None + consumed_items_cost: DF.Currency cost_center: DF.Link | None description: DF.LongText | None downtime: DF.Data | None @@ -50,7 +51,6 @@ class AssetRepair(AccountsController): project: DF.Link | None repair_cost: DF.Currency repair_status: DF.Literal["Pending", "Completed", "Cancelled"] - stock_consumption: DF.Check stock_items: DF.Table[AssetRepairConsumedItem] total_repair_cost: DF.Currency # end: auto-generated types @@ -58,16 +58,12 @@ 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.validate_purchase_invoices() self.update_status() - - if self.get("stock_items"): - self.set_stock_items_cost() - + self.calculate_consumed_items_cost() self.calculate_repair_cost() self.calculate_total_repair_cost() + self.check_repair_status() def validate_dates(self): if self.completion_date and (self.failure_date > self.completion_date): @@ -75,36 +71,58 @@ 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_invoices(self): + for d in self.invoices: + 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) - 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 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_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) - ) + 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: + 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 + ) + ) 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") - add_asset_activity( - self.asset, + self.add_asset_activity( _("Asset out of order due to Asset Repair {0}").format( get_link_to_form("Asset Repair", self.name) ), @@ -112,147 +130,87 @@ class AssetRepair(AccountsController): else: self.asset_doc.set_status() - def set_stock_items_cost(self): + def calculate_consumed_items_cost(self): + consumed_items_cost = 0.0 for item in self.get("stock_items"): item.total_value = flt(item.valuation_rate) * flt(item.consumed_quantity) + consumed_items_cost += item.total_value + self.consumed_items_cost = consumed_items_cost 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) - - total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() - self.total_repair_cost += total_value_of_stock_consumed - - def before_submit(self): - self.check_repair_status() + self.total_repair_cost = flt(self.repair_cost) + flt(self.consumed_items_cost) + def on_submit(self): self.asset_doc.flags.increase_in_asset_value_due_to_repair = False + self.decrease_stock_quantity() - if self.get("stock_consumption") or self.get("capitalize_repair_cost"): - self.asset_doc.flags.increase_in_asset_value_due_to_repair = True + if self.get("capitalize_repair_cost"): + self.asset_doc.flags.ignore_validate_update_after_submit = True + self.update_asset_value() + self.make_gl_entries() + self.set_increase_in_asset_life() - self.increase_asset_value() + depreciation_note = self.get_depreciation_note() + make_new_active_asset_depr_schedules_and_cancel_current_ones( + self.asset_doc, depreciation_note, ignore_booked_entry=True + ) + self.add_asset_activity() - total_repair_cost = self.get_total_value_of_stock_consumed() - if self.capitalize_repair_cost: - total_repair_cost += self.repair_cost - self.asset_doc.total_asset_cost += total_repair_cost - self.asset_doc.additional_asset_cost += total_repair_cost - - if self.get("stock_consumption"): - self.check_for_stock_items_and_warehouse() - self.decrease_stock_quantity() - if self.get("capitalize_repair_cost"): - self.make_gl_entries() - if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: - self.modify_depreciation_schedule() - - notes = _( - "This schedule was created when Asset {0} was repaired through Asset Repair {1}." - ).format( - get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), - get_link_to_form(self.doctype, self.name), - ) - self.asset_doc.flags.ignore_validate_update_after_submit = True - make_new_active_asset_depr_schedules_and_cancel_current_ones( - self.asset_doc, notes, ignore_booked_entry=True - ) - self.asset_doc.save() - - add_asset_activity( - self.asset, - _("Asset updated after completion of Asset Repair {0}").format( - get_link_to_form("Asset Repair", self.name) - ), - ) - - def before_cancel(self): + def on_cancel(self): self.asset_doc = frappe.get_doc("Asset", self.asset) - self.asset_doc.flags.increase_in_asset_value_due_to_repair = False - - if self.get("stock_consumption") or self.get("capitalize_repair_cost"): + if self.get("capitalize_repair_cost"): self.asset_doc.flags.increase_in_asset_value_due_to_repair = True + self.asset_doc.flags.ignore_validate_update_after_submit = True - self.decrease_asset_value() + self.update_asset_value() + self.make_gl_entries(cancel=True) + self.set_increase_in_asset_life() - total_repair_cost = self.get_total_value_of_stock_consumed() - if self.capitalize_repair_cost: - total_repair_cost += self.repair_cost - self.asset_doc.total_asset_cost -= total_repair_cost - self.asset_doc.additional_asset_cost -= total_repair_cost - - if self.get("capitalize_repair_cost"): - self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") - self.make_gl_entries(cancel=True) - if self.asset_doc.calculate_depreciation and self.increase_in_asset_life: - self.revert_depreciation_schedule_on_cancellation() - - notes = _( - "This schedule was created when Asset {0}'s Asset Repair {1} was cancelled." - ).format( - get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), - get_link_to_form(self.doctype, self.name), - ) - self.asset_doc.flags.ignore_validate_update_after_submit = True - make_new_active_asset_depr_schedules_and_cancel_current_ones( - self.asset_doc, notes, ignore_booked_entry=True - ) - self.asset_doc.save() - - add_asset_activity( - self.asset, - _("Asset updated after cancellation of Asset Repair {0}").format( - get_link_to_form("Asset Repair", self.name) - ), - ) + depreciation_note = self.get_depreciation_note() + make_new_active_asset_depr_schedules_and_cancel_current_ones( + self.asset_doc, depreciation_note, ignore_booked_entry=True + ) + self.add_asset_activity() def after_delete(self): frappe.get_doc("Asset", self.asset).set_status() def check_repair_status(self): - if self.repair_status == "Pending": + if self.repair_status == "Pending" and self.docstatus == 1: frappe.throw(_("Please update Repair Status.")) - def check_for_stock_items_and_warehouse(self): - if not self.get("stock_items"): - frappe.throw(_("Please enter Stock Items consumed during the Repair."), title=_("Missing Items")) + def update_asset_value(self): + if self.docstaus == 2: + self.total_repair_cost *= -1 - def increase_asset_value(self): - total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() + self.asset_doc.total_asset_cost += flt(self.total_repair_cost) + self.asset_doc.additional_asset_cost += flt(self.total_repair_cost) if self.asset_doc.calculate_depreciation: for row in self.asset_doc.finance_books: - row.value_after_depreciation += total_value_of_stock_consumed + row.value_after_depreciation += flt(self.total_repair_cost) - if self.capitalize_repair_cost: - row.value_after_depreciation += self.repair_cost - - def decrease_asset_value(self): - total_value_of_stock_consumed = self.get_total_value_of_stock_consumed() - - if self.asset_doc.calculate_depreciation: - for row in self.asset_doc.finance_books: - row.value_after_depreciation -= total_value_of_stock_consumed - - if self.capitalize_repair_cost: - row.value_after_depreciation -= self.repair_cost + self.asset_doc.save() def get_total_value_of_stock_consumed(self): - total_value_of_stock_consumed = 0 - if self.get("stock_consumption"): - for item in self.get("stock_items"): - total_value_of_stock_consumed += item.total_value - - return total_value_of_stock_consumed + return sum([flt(item.total_value) for item in self.get("stock_items")]) def decrease_stock_quantity(self): + if not self.get("stock_items"): + return + stock_entry = frappe.get_doc( - {"doctype": "Stock Entry", "stock_entry_type": "Material Issue", "company": self.company} + { + "doctype": "Stock Entry", + "stock_entry_type": "Material Issue", + "company": self.company, + "asset_repair": self.name, + } ) - stock_entry.asset_repair = self.name for stock_item in self.get("stock_items"): self.validate_serial_no(stock_item) @@ -278,7 +236,7 @@ class AssetRepair(AccountsController): "Item", stock_item.item_code, "has_serial_no" ): msg = f"Serial No Bundle is mandatory for Item {stock_item.item_code}" - frappe.throw(msg, title=_("Missing Serial No Bundle")) + frappe.throw(_(msg), title=_("Missing Serial No Bundle")) if stock_item.serial_and_batch_bundle: values_to_update = { @@ -291,6 +249,9 @@ class AssetRepair(AccountsController): ) def make_gl_entries(self, cancel=False): + if cancel: + self.ignore_linked_doctypes = ("GL Entry", "Stock Ledger Entry") + if flt(self.total_repair_cost) > 0: gl_entries = self.get_gl_entries() make_gl_entries(gl_entries, cancel) @@ -348,7 +309,7 @@ class AssetRepair(AccountsController): ) def get_gl_entries_for_consumed_items(self, gl_entries, fixed_asset_account): - if not (self.get("stock_consumption") and self.get("stock_items")): + if not self.get("stock_items"): return # creating GL Entries for each row in Stock Items based on the Stock Entry created for it @@ -400,72 +361,28 @@ class AssetRepair(AccountsController): ) ) - def modify_depreciation_schedule(self): - for row in self.asset_doc.finance_books: - row.total_number_of_depreciations += self.increase_in_asset_life / row.frequency_of_depreciation + def set_increase_in_asset_life(self): + if self.asset_doc.calculate_depreciation and cint(self.increase_in_asset_life) > 0: + for row in self.asset_doc.finance_books: + row.increase_in_asset_life = row.increase_in_asset_life + ( + cint(self.increase_in_asset_life) * (1 if self.docstatus == 1 else -1) + ) - self.asset_doc.flags.increase_in_asset_life = False - extra_months = self.increase_in_asset_life % row.frequency_of_depreciation - if extra_months != 0: - self.calculate_last_schedule_date(self.asset_doc, row, extra_months) - - # to help modify depreciation schedule when increase_in_asset_life is not a multiple of frequency_of_depreciation - def calculate_last_schedule_date(self, asset, row, extra_months): - asset.flags.increase_in_asset_life = True - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( - asset.opening_number_of_booked_depreciations + def get_depreciation_note(self): + return _("This schedule was created when Asset {0} was repaired through Asset Repair {1}.").format( + get_link_to_form(self.asset_doc.doctype, self.asset_doc.name), + get_link_to_form(self.doctype, self.name), ) - depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) + def add_asset_activity(self, subject=None): + if not subject: + subject = _("Asset updated due to Asset Repair {0} {1}.").format( + get_link_to_form( + self.doctype, self.name, "submission" if self.docstatus == 1 else "cancellation" + ), + ) - # the Schedule Date in the final row of the old Depreciation Schedule - last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date - - # the Schedule Date in the final row of the new Depreciation Schedule - asset.to_date = add_months(last_schedule_date, extra_months) - - # the latest possible date at which the depreciation can occur, without increasing the Total Number of Depreciations - # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... - schedule_date = add_months( - row.depreciation_start_date, - number_of_pending_depreciations * cint(row.frequency_of_depreciation), - ) - - if asset.to_date > schedule_date: - row.total_number_of_depreciations += 1 - - def revert_depreciation_schedule_on_cancellation(self): - for row in self.asset_doc.finance_books: - row.total_number_of_depreciations -= self.increase_in_asset_life / row.frequency_of_depreciation - - self.asset_doc.flags.increase_in_asset_life = False - extra_months = self.increase_in_asset_life % row.frequency_of_depreciation - if extra_months != 0: - self.calculate_last_schedule_date_before_modification(self.asset_doc, row, extra_months) - - def calculate_last_schedule_date_before_modification(self, asset, row, extra_months): - asset.flags.increase_in_asset_life = True - number_of_pending_depreciations = cint(row.total_number_of_depreciations) - cint( - asset.opening_number_of_booked_depreciations - ) - - depr_schedule = get_depr_schedule(asset.name, "Active", row.finance_book) - - # the Schedule Date in the final row of the modified Depreciation Schedule - last_schedule_date = depr_schedule[len(depr_schedule) - 1].schedule_date - - # the Schedule Date in the final row of the original Depreciation Schedule - asset.to_date = add_months(last_schedule_date, -extra_months) - - # the latest possible date at which the depreciation can occur, without decreasing the Total Number of Depreciations - # if depreciations happen yearly and the Depreciation Posting Date is 01-01-2020, this could be 01-01-2021, 01-01-2022... - schedule_date = add_months( - row.depreciation_start_date, - (number_of_pending_depreciations - 1) * cint(row.frequency_of_depreciation), - ) - - if asset.to_date < schedule_date: - row.total_number_of_depreciations -= 1 + add_asset_activity(self.asset, subject) @frappe.whitelist() @@ -476,16 +393,11 @@ def get_downtime(failure_date, completion_date): @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 = ( + return ( frappe.qb.from_(PurchaseInvoice) .join(PurchaseInvoiceItem) .on(PurchaseInvoiceItem.parent == PurchaseInvoice.name) @@ -495,8 +407,17 @@ def expense_item_pi_query(company): .where( (Item.is_stock_item == 0) & (Item.is_fixed_asset == 0) - & (PurchaseInvoice.company == company) + & (PurchaseInvoice.company == filters.get("company")) & (PurchaseInvoice.docstatus == 1) ) - ) - return query + ).run(as_list=1) + + +@frappe.whitelist() +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) + .where(PurchaseInvoiceItem.parent == filters.get("purchase_invoice")) + ).run(as_list=1)