mirror of
https://github.com/frappe/erpnext.git
synced 2026-03-22 19:55:29 +00:00
fix: conflict in asset repair
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user