Merge pull request #47901 from khushi8112/asset-transfer-and-movement-in-single-asset-movement-record

feat: Asset Transfer and Issue in single asset movement record
This commit is contained in:
Khushi Rawat
2025-06-11 01:16:51 +05:30
committed by GitHub
4 changed files with 126 additions and 130 deletions

View File

@@ -62,8 +62,8 @@ frappe.ui.form.on("Asset Movement", {
fieldnames_to_be_altered = { fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 }, target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 1, reqd: 0 }, source_location: { read_only: 1, reqd: 0 },
from_employee: { read_only: 0, reqd: 0 }, from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 1, reqd: 0 }, to_employee: { read_only: 0, reqd: 0 },
}; };
} else if (frm.doc.purpose === "Issue") { } else if (frm.doc.purpose === "Issue") {
fieldnames_to_be_altered = { fieldnames_to_be_altered = {
@@ -72,6 +72,13 @@ frappe.ui.form.on("Asset Movement", {
from_employee: { read_only: 1, reqd: 0 }, from_employee: { read_only: 1, reqd: 0 },
to_employee: { read_only: 0, reqd: 1 }, to_employee: { read_only: 0, reqd: 1 },
}; };
} else if (frm.doc.purpose === "Transfer and Issue") {
fieldnames_to_be_altered = {
target_location: { read_only: 0, reqd: 1 },
source_location: { read_only: 0, reqd: 1 },
from_employee: { read_only: 0, reqd: 1 },
to_employee: { read_only: 0, reqd: 1 },
};
} }
if (fieldnames_to_be_altered) { if (fieldnames_to_be_altered) {
Object.keys(fieldnames_to_be_altered).forEach((fieldname) => { Object.keys(fieldnames_to_be_altered).forEach((fieldname) => {

View File

@@ -33,7 +33,7 @@
"fieldname": "purpose", "fieldname": "purpose",
"fieldtype": "Select", "fieldtype": "Select",
"label": "Purpose", "label": "Purpose",
"options": "\nIssue\nReceipt\nTransfer", "options": "\nIssue\nReceipt\nTransfer\nTransfer and Issue",
"reqd": 1 "reqd": 1
}, },
{ {
@@ -93,10 +93,11 @@
"fieldtype": "Column Break" "fieldtype": "Column Break"
} }
], ],
"grid_page_length": 50,
"index_web_pages_for_search": 1, "index_web_pages_for_search": 1,
"is_submittable": 1, "is_submittable": 1,
"links": [], "links": [],
"modified": "2024-03-27 13:06:35.116228", "modified": "2025-05-30 17:01:55.864353",
"modified_by": "Administrator", "modified_by": "Administrator",
"module": "Assets", "module": "Assets",
"name": "Asset Movement", "name": "Asset Movement",
@@ -149,6 +150,7 @@
"write": 1 "write": 1
} }
], ],
"row_format": "Dynamic",
"sort_field": "creation", "sort_field": "creation",
"sort_order": "DESC", "sort_order": "DESC",
"states": [] "states": []

View File

@@ -24,19 +24,18 @@ class AssetMovement(Document):
amended_from: DF.Link | None amended_from: DF.Link | None
assets: DF.Table[AssetMovementItem] assets: DF.Table[AssetMovementItem]
company: DF.Link company: DF.Link
purpose: DF.Literal["", "Issue", "Receipt", "Transfer"] purpose: DF.Literal["", "Issue", "Receipt", "Transfer", "Transfer and Issue"]
reference_doctype: DF.Link | None reference_doctype: DF.Link | None
reference_name: DF.DynamicLink | None reference_name: DF.DynamicLink | None
transaction_date: DF.Datetime transaction_date: DF.Datetime
# end: auto-generated types # end: auto-generated types
def validate(self): def validate(self):
self.validate_asset()
self.validate_location()
self.validate_employee()
def validate_asset(self):
for d in self.assets: for d in self.assets:
self.validate_asset(d)
self.validate_movement(d)
def validate_asset(self, d):
status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"]) status, company = frappe.db.get_value("Asset", d.asset, ["status", "company"])
if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"): if self.purpose == "Transfer" and status in ("Draft", "Scrapped", "Sold"):
frappe.throw(_("{0} asset cannot be transferred").format(status)) frappe.throw(_("{0} asset cannot be transferred").format(status))
@@ -44,72 +43,47 @@ class AssetMovement(Document):
if company != self.company: if company != self.company:
frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company))
if not (d.source_location or d.target_location or d.from_employee or d.to_employee): def validate_movement(self, d):
frappe.throw(_("Either location or employee must be required")) if self.purpose == "Transfer and Issue":
self.validate_location_and_employee(d)
elif self.purpose in ["Receipt", "Transfer"]:
self.validate_location(d)
else:
self.validate_employee(d)
def validate_location(self): def validate_location_and_employee(self, d):
for d in self.assets: self.validate_location(d)
if self.purpose in ["Transfer", "Issue"]: self.validate_employee(d)
def validate_location(self, d):
if self.purpose in ["Transfer", "Transfer and Issue"]:
current_location = frappe.db.get_value("Asset", d.asset, "location") current_location = frappe.db.get_value("Asset", d.asset, "location")
if d.source_location: if d.source_location:
if current_location != d.source_location: if current_location != d.source_location:
frappe.throw( frappe.throw(
_("Asset {0} does not belongs to the location {1}").format( _("Asset {0} does not belongs to the location {1}").format(d.asset, d.source_location)
d.asset, d.source_location
)
) )
else: else:
d.source_location = current_location d.source_location = current_location
if self.purpose == "Issue":
if d.target_location:
frappe.throw(
_(
"Issuing cannot be done to a location. Please enter employee to issue the Asset {0} to"
).format(d.asset),
title=_("Incorrect Movement Purpose"),
)
if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
if self.purpose == "Transfer":
if d.to_employee:
frappe.throw(
_(
"Transferring cannot be done to an Employee. Please enter location where Asset {0} has to be transferred"
).format(d.asset),
title=_("Incorrect Movement Purpose"),
)
if not d.target_location: if not d.target_location:
frappe.throw( frappe.throw(_("Target Location is required for transferring Asset {0}").format(d.asset))
_("Target Location is required while transferring Asset {0}").format(d.asset)
)
if d.source_location == d.target_location: if d.source_location == d.target_location:
frappe.throw(_("Source and Target Location cannot be same")) frappe.throw(_("Source and Target Location cannot be same"))
if self.purpose == "Receipt": if self.purpose == "Receipt":
if not (d.source_location) and not d.target_location and not d.to_employee: if not d.target_location:
frappe.throw(_("Target Location is required while receiving Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw( frappe.throw(
_("Target Location or To Employee is required while receiving Asset {0}").format( _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
d.asset
)
)
elif d.source_location:
if d.from_employee and not d.target_location:
frappe.throw(
_(
"Target Location is required while receiving Asset {0} from an employee"
).format(d.asset)
)
elif d.to_employee and d.target_location:
frappe.throw(
_(
"Asset {0} cannot be received at a location and given to an employee in a single movement"
).format(d.asset)
) )
def validate_employee(self): def validate_employee(self, d):
for d in self.assets: if self.purpose == "Tranfer and Issue":
if not d.from_employee:
frappe.throw(_("From Employee is required while issuing Asset {0}").format(d.asset))
if d.from_employee: if d.from_employee:
current_custodian = frappe.db.get_value("Asset", d.asset, "custodian") current_custodian = frappe.db.get_value("Asset", d.asset, "custodian")
@@ -118,6 +92,9 @@ class AssetMovement(Document):
_("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee) _("Asset {0} does not belongs to the custodian {1}").format(d.asset, d.from_employee)
) )
if not d.to_employee:
frappe.throw(_("Employee is required while issuing Asset {0}").format(d.asset))
if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company: if d.to_employee and frappe.db.get_value("Employee", d.to_employee, "company") != self.company:
frappe.throw( frappe.throw(
_("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company) _("Employee {0} does not belongs to the company {1}").format(d.to_employee, self.company)
@@ -130,25 +107,29 @@ class AssetMovement(Document):
self.set_latest_location_and_custodian_in_asset() self.set_latest_location_and_custodian_in_asset()
def set_latest_location_and_custodian_in_asset(self): def set_latest_location_and_custodian_in_asset(self):
for d in self.assets:
current_location, current_employee = self.get_latest_location_and_custodian(d.asset)
self.update_asset_location_and_custodian(d.asset, current_location, current_employee)
self.log_asset_activity(d.asset, current_location, current_employee)
def get_latest_location_and_custodian(self, asset):
current_location, current_employee = "", "" current_location, current_employee = "", ""
cond = "1=1" cond = "1=1"
for d in self.assets:
args = {"asset": d.asset, "company": self.company}
# latest entry corresponds to current document's location, employee when transaction date > previous dates # latest entry corresponds to current document's location, employee when transaction date > previous dates
# In case of cancellation it corresponds to previous latest document's location, employee # In case of cancellation it corresponds to previous latest document's location, employee
args = {"asset": asset, "company": self.company}
latest_movement_entry = frappe.db.sql( latest_movement_entry = frappe.db.sql(
f""" f"""
SELECT asm_item.target_location, asm_item.to_employee SELECT asm_item.target_location, asm_item.to_employee
FROM `tabAsset Movement Item` asm_item, `tabAsset Movement` asm FROM `tabAsset Movement Item` asm_item
JOIN `tabAsset Movement` asm ON asm_item.parent = asm.name
WHERE WHERE
asm_item.parent=asm.name and asm_item.asset = %(asset)s AND
asm_item.asset=%(asset)s and asm.company = %(company)s AND
asm.company=%(company)s and asm.docstatus = 1 AND {cond}
asm.docstatus=1 and {cond} ORDER BY asm.transaction_date DESC
ORDER BY LIMIT 1
asm.transaction_date desc limit 1
""", """,
args, args,
) )
@@ -157,26 +138,32 @@ class AssetMovement(Document):
current_location = latest_movement_entry[0][0] current_location = latest_movement_entry[0][0]
current_employee = latest_movement_entry[0][1] current_employee = latest_movement_entry[0][1]
frappe.db.set_value("Asset", d.asset, "location", current_location, update_modified=False) return current_location, current_employee
frappe.db.set_value("Asset", d.asset, "custodian", current_employee, update_modified=False)
if current_location and current_employee: def update_asset_location_and_custodian(self, asset_id, location, employee):
asset = frappe.get_doc("Asset", asset_id)
if employee and employee != asset.custodian:
frappe.db.set_value("Asset", asset_id, "custodian", employee)
if location and location != asset.location:
frappe.db.set_value("Asset", asset_id, "location", location)
def log_asset_activity(self, asset_id, location, employee):
if location and employee:
add_asset_activity( add_asset_activity(
d.asset, asset_id,
_("Asset received at Location {0} and issued to Employee {1}").format( _("Asset received at Location {0} and issued to Employee {1}").format(
get_link_to_form("Location", current_location), get_link_to_form("Location", location),
get_link_to_form("Employee", current_employee), get_link_to_form("Employee", employee),
), ),
) )
elif current_location: elif location:
add_asset_activity( add_asset_activity(
d.asset, asset_id,
_("Asset transferred to Location {0}").format( _("Asset transferred to Location {0}").format(get_link_to_form("Location", location)),
get_link_to_form("Location", current_location)
),
) )
elif current_employee: elif employee:
add_asset_activity( add_asset_activity(
d.asset, asset_id,
_("Asset issued to Employee {0}").format(get_link_to_form("Employee", current_employee)), _("Asset issued to Employee {0}").format(get_link_to_form("Employee", employee)),
) )

View File

@@ -88,7 +88,7 @@ class TestAssetMovement(IntegrationTestCase):
) )
# after issuing, asset should belong to an employee not at a location # after issuing, asset should belong to an employee not at a location
self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), None) self.assertEqual(frappe.db.get_value("Asset", asset.name, "location"), "Test Location 2")
self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee) self.assertEqual(frappe.db.get_value("Asset", asset.name, "custodian"), employee)
create_asset_movement( create_asset_movement(