fix: conflict in asset repair

This commit is contained in:
Khushi Rawat
2025-05-05 12:04:05 +05:30
parent 0eb83a4474
commit 0e4706b074
2 changed files with 141 additions and 221 deletions

View File

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

View File

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