From 2db09b38406d0db4401bc6cb206fd678593d31f1 Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Dec 2025 09:59:05 +0530 Subject: [PATCH 1/3] fix(asset): prevent creating assets beyond purchased quantity --- erpnext/assets/doctype/asset/asset.py | 62 ++++++++++++++++++++++----- 1 file changed, 51 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7a768d900d5..9df5728fd3e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -7,6 +7,7 @@ import math import frappe from frappe import _ +from frappe.query_builder.functions import IfNull, Sum from frappe.utils import ( cint, flt, @@ -121,7 +122,7 @@ class Asset(AccountsController): def validate(self): self.validate_category() self.validate_precision() - self.validate_linked_purchase_docs() + self.validate_linked_purchase_documents() self.set_purchase_doc_row_item() self.validate_asset_values() self.validate_asset_and_reference() @@ -422,20 +423,59 @@ class Asset(AccountsController): if self.available_for_use_date and getdate(self.available_for_use_date) < getdate(self.purchase_date): frappe.throw(_("Available-for-use Date should be after purchase date")) - def validate_linked_purchase_docs(self): - for doctype_field, doctype_name in [ + def validate_linked_purchase_documents(self): + for fieldname, doctype in [ ("purchase_receipt", "Purchase Receipt"), ("purchase_invoice", "Purchase Invoice"), ]: - linked_doc = getattr(self, doctype_field, None) - if linked_doc: - docstatus = frappe.db.get_value(doctype_name, linked_doc, "docstatus") - if docstatus == 0: - frappe.throw( - _("{0} is still in Draft. Please submit it before saving the Asset.").format( - get_link_to_form(doctype_name, linked_doc) - ) + purchase_doc = getattr(self, fieldname, None) + + if not purchase_doc: + continue + + if frappe.db.get_value(doctype, purchase_doc, "docstatus") == 0: + frappe.throw( + _("{0} is in Draft. Submit it before creating the Asset.").format( + get_link_to_form(doctype, purchase_doc) ) + ) + + self.validate_asset_qty_with_purchase_doc(doctype, purchase_doc) + + def validate_asset_qty_with_purchase_doc(self, doctype, purchase_doc): + Asset = frappe.qb.DocType("Asset") + + if doctype == "Purchase Invoice": + asset_filter = (Asset.purchase_invoice == purchase_doc) & (Asset.name != self.name) + else: + asset_filter = (Asset.purchase_receipt == purchase_doc) & (Asset.name != self.name) + + existing_asset_qty = ( + frappe.qb.from_(Asset).select(IfNull(Sum(Asset.asset_quantity), 0)).where(asset_filter) + ).run()[0][0] + + PurchaseDoc = frappe.qb.DocType(doctype) + PurchaseDocItems = frappe.qb.DocType(f"{doctype} Item") + + purchased_qty = ( + frappe.qb.from_(PurchaseDoc) + .join(PurchaseDocItems) + .on(PurchaseDoc.name == PurchaseDocItems.parent) + .select(IfNull(Sum(PurchaseDocItems.qty), 0)) + .where(PurchaseDoc.name == purchase_doc) + .where(PurchaseDocItems.item_code == self.item_code) + ).run()[0][0] + + if (existing_asset_qty + self.asset_quantity) > purchased_qty: + frappe.throw( + _("Created assets {0} exceed the purchased quantity {1} for item {2} " "in {3} {4}").format( + (existing_asset_qty + self.asset_quantity), + purchased_qty, + self.item_code, + doctype, + get_link_to_form(doctype, purchase_doc), + ) + ) def validate_gross_and_purchase_amount(self): if self.is_existing_asset: From 7bfcdb13b1a8348e67ed26929a267c73b9d46daa Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Dec 2025 12:55:41 +0530 Subject: [PATCH 2/3] fix: better validation message --- erpnext/assets/doctype/asset/asset.py | 20 ++++++++++++++------ 1 file changed, 14 insertions(+), 6 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9df5728fd3e..7ae8b10d9f5 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -446,12 +446,15 @@ class Asset(AccountsController): Asset = frappe.qb.DocType("Asset") if doctype == "Purchase Invoice": - asset_filter = (Asset.purchase_invoice == purchase_doc) & (Asset.name != self.name) + asset_filter = Asset.purchase_invoice == purchase_doc else: - asset_filter = (Asset.purchase_receipt == purchase_doc) & (Asset.name != self.name) + asset_filter = Asset.purchase_receipt == purchase_doc existing_asset_qty = ( - frappe.qb.from_(Asset).select(IfNull(Sum(Asset.asset_quantity), 0)).where(asset_filter) + frappe.qb.from_(Asset) + .select(IfNull(Sum(Asset.asset_quantity), 0)) + .where((Asset.item_code == self.item_code) & (Asset.name != self.name)) + .where(asset_filter) ).run()[0][0] PurchaseDoc = frappe.qb.DocType(doctype) @@ -468,12 +471,17 @@ class Asset(AccountsController): if (existing_asset_qty + self.asset_quantity) > purchased_qty: frappe.throw( - _("Created assets {0} exceed the purchased quantity {1} for item {2} " "in {3} {4}").format( - (existing_asset_qty + self.asset_quantity), + _( + "Cannot create asset.

" + "You're trying to create {0} asset(s) from {2} {3}.
" + "However, only {1} item(s) were purchased and {4} asset(s) already exist against {5}." + ).format( + self.asset_quantity, purchased_qty, - self.item_code, doctype, get_link_to_form(doctype, purchase_doc), + existing_asset_qty, + purchase_doc, ) ) From 7012345968121b49d592794ee5015891532e452d Mon Sep 17 00:00:00 2001 From: khushi8112 Date: Fri, 5 Dec 2025 13:08:29 +0530 Subject: [PATCH 3/3] fix: filter out cancelled asset in the query --- erpnext/assets/doctype/asset/asset.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 7ae8b10d9f5..3b5725db93f 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -453,7 +453,7 @@ class Asset(AccountsController): existing_asset_qty = ( frappe.qb.from_(Asset) .select(IfNull(Sum(Asset.asset_quantity), 0)) - .where((Asset.item_code == self.item_code) & (Asset.name != self.name)) + .where((Asset.item_code == self.item_code) & (Asset.name != self.name) & (Asset.docstatus != 2)) .where(asset_filter) ).run()[0][0]