Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 51 additions & 12 deletions beveren_fsm/field_service_management/api/service_request.py
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
from collections import defaultdict

import frappe
from frappe.utils import getdate

Expand Down Expand Up @@ -58,20 +60,57 @@ def get_service_requests(
)

results = []
request_names = [req.name for req in requests]
order_movements_by_request = defaultdict(list)

if request_names:
related_orders = frappe.get_all(
"Service Order",
filters={"service_request": ["in", request_names]},
fields=["name", "service_request"],
limit_page_length=0,
)

for order_meta in related_orders:
try:
order_doc = frappe.get_doc("Service Order", order_meta.name)
except frappe.DoesNotExistError:
continue

for movement in order_doc.product_movement or []:
order_movements_by_request[order_meta.service_request].append(
{
"name": movement.name,
"movement_type": movement.movement_type,
"destination": movement.destination,
"movement_date": movement.movement_date,
"linked_document_type": movement.linked_document_type,
"linked_document": movement.linked_document,
"handled_by": movement.handled_by,
"service_order": order_meta.name,
}
)

for req in requests:
doc = frappe.get_doc("Service Request", req.name)
req["product_movement"] = [
{
"name": movement.name,
"movement_type": movement.movement_type,
"destination": movement.destination,
"movement_date": movement.movement_date,
"linked_document_type": movement.linked_document_type,
"linked_document": movement.linked_document,
"handled_by": movement.handled_by,
}
for movement in doc.product_movement
]
movements = order_movements_by_request.get(req.name, [])

# Fallback to legacy data if Service Order movements are unavailable
if not movements and hasattr(doc, "product_movement"):
movements = [
{
"name": movement.name,
"movement_type": movement.movement_type,
"destination": movement.destination,
"movement_date": movement.movement_date,
"linked_document_type": movement.linked_document_type,
"linked_document": movement.linked_document,
"handled_by": movement.handled_by,
}
for movement in doc.product_movement
]

req["product_movement"] = movements
results.append(req)

return results
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Product Location",
"label": "Movement Type",
"reqd": 1,
"unique": 1
},
Expand All @@ -24,13 +24,13 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Destination"
"label": "Product Destination"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-11-12 04:07:46.697596",
"modified": "2025-11-18 02:28:42.446026",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Product Location",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -6,9 +6,9 @@
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"destination",
"movement_type",
"movement_date",
"destination",
"column_break_cmtq",
"linked_document_type",
"linked_document",
Expand All @@ -21,7 +21,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Current Location",
"label": "Latest Movement",
"options": "Product Location",
"reqd": 1
},
Expand Down Expand Up @@ -70,7 +70,7 @@
"fieldtype": "Link",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Current Location",
"label": "Latest Movement",
"options": "Product Location",
"reqd": 1
},
Expand All @@ -80,14 +80,14 @@
"fieldtype": "Data",
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Destination"
"label": "Product Location"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-11-17 04:21:43.163670",
"modified": "2025-11-18 02:32:15.727185",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Product Movement",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -582,7 +582,7 @@ frappe.ui.form.on("Service Order", {
{
fieldname: "product_location",
fieldtype: "Link",
label: __("Product Location"),
label: __("Product Movement Type"),
options: "Product Location",
reqd: 1,
default: defaultProductLocation,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@
"priority",
"company",
"amended_from",
"current_product_location",
"product_location",
"accounting_dimensions_section",
"cost_center",
"column_break_ekul",
Expand Down Expand Up @@ -96,7 +96,9 @@
"column_break_gtsg",
"preference_note",
"status_section",
"per_billed"
"per_billed",
"product_tracking_tab",
"product_movement"
],
"fields": [
{
Expand Down Expand Up @@ -800,12 +802,26 @@
"label": "SpareParts Total",
"read_only": 1
},
{
"fieldname": "product_tracking_tab",
"fieldtype": "Tab Break",
"label": "Product Tracking"
},
{
"allow_on_submit": 1,
"fieldname": "current_product_location",
"fieldtype": "Link",
"label": "Current Product Location",
"options": "Product Location",
"fieldname": "product_movement",
"fieldtype": "Table",
"ignore_user_permissions": 1,
"label": "Product Tracker",
"options": "Product Movement",
"read_only": 1
},
{
"allow_on_submit": 1,
"fetch_from": "current_product_location.destination",
"fieldname": "product_location",
"fieldtype": "Data",
"label": "Product Location",
"read_only": 1
}
],
Expand All @@ -821,7 +837,7 @@
"link_fieldname": "custom_reference_service_document"
}
],
"modified": "2025-11-17 04:08:15.600873",
"modified": "2025-11-18 03:10:34.985668",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Service Order",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ def _set_status_from_location(order, location):

class ServiceOrder(Document):
def validate(self):
self.ensure_default_product_location()
self.set_in_words()
self.validate_items()
self.calculate_service_totals()
Expand Down Expand Up @@ -104,6 +105,14 @@ def cancel_linked_request(self):
self.service_request = ""
request.save()

def ensure_default_product_location(self):
if not self.product_location:
default_location = (
frappe.db.get_value("Product Location", {"name": "Customer Site"}, "destination")
or "Customer Site"
)
self.product_location = default_location

@frappe.whitelist()
def create_appointment(self, service_order):
appointment = frappe.new_doc("Service Appointment")
Expand Down Expand Up @@ -993,11 +1002,6 @@ def record_product_movement(

order = frappe.get_doc("Service Order", service_order)

if not order.service_request:
frappe.throw(_("Service Order {0} is not linked to a Service Request").format(order.name))

service_request = frappe.get_doc("Service Request", order.service_request)

row = {
"movement_type": location,
"movement_date": movement_date or today(),
Expand All @@ -1008,19 +1012,36 @@ def record_product_movement(
row["linked_document_type"] = linked_document_type
row["linked_document"] = linked_document

entry = service_request.append("product_movement", row)

# Update current_product_location on Service Request
service_request.current_product_location = location
entry = order.append("product_movement", row)
row_destination = entry.destination or location

# Update current_product_location on Service Order
# Update Service Order fields
order.current_product_location = location

_set_status_from_location(order, location)

service_request.save(ignore_permissions=True)
order.product_location = row_destination
_set_status_from_location(order, row_destination)
order.save(ignore_permissions=True)

# Keep Service Request in sync when available
if order.service_request:
try:
service_request = frappe.get_doc("Service Request", order.service_request)
service_request.current_product_location = row_destination
if hasattr(service_request, "product_movement"):
# legacy compatibility: mirror entry if table still exists
service_request.append(
"product_movement",
{
"movement_type": row_destination,
"movement_date": entry.movement_date,
"linked_document_type": entry.linked_document_type,
"linked_document": entry.linked_document,
"handled_by": entry.handled_by,
},
)
service_request.save(ignore_permissions=True)
except frappe.DoesNotExistError:
pass

return entry.name


Expand All @@ -1042,34 +1063,68 @@ def update_product_movement_on_submit(doc, method):
except frappe.DoesNotExistError:
return

# Get Service Request
if not order.service_request:
return

try:
service_request = frappe.get_doc("Service Request", order.service_request)
except frappe.DoesNotExistError:
return

# Find the product movement entry that matches the location and doesn't have linked_document
product_location = doc.custom_current_product_location

# Find matching entry without linked_document
matching_entry = None
for entry in service_request.product_movement:
for entry in order.product_movement:
if entry.movement_type == product_location and not entry.linked_document:
matching_entry = entry
break

if matching_entry:
# Update the entry with linked document information
matching_entry.linked_document_type = doc.doctype
matching_entry.linked_document = doc.name
if not matching_entry.movement_date:
matching_entry.movement_date = getattr(doc, "posting_date", None) or today()
if not matching_entry.handled_by:
matching_entry.handled_by = frappe.session.user
else:
order.append(
"product_movement",
{
"movement_type": product_location,
"movement_date": getattr(doc, "posting_date", None) or today(),
"linked_document_type": doc.doctype,
"linked_document": doc.name,
"handled_by": frappe.session.user,
},
)

service_request.save(ignore_permissions=True)
order.current_product_location = product_location
order.product_location = (
(getattr(matching_entry, "destination", None) if matching_entry else None)
or getattr(order.product_movement[-1], "destination", None)
or product_location
)
_set_status_from_location(order, order.product_location or product_location)
order.save(ignore_permissions=True)

if _set_status_from_location(order, product_location):
order.save(ignore_permissions=True)
# Keep Service Request in sync when available
if order.service_request:
try:
service_request = frappe.get_doc("Service Request", order.service_request)
except frappe.DoesNotExistError:
service_request = None

if service_request:
service_request.current_product_location = product_location
if hasattr(service_request, "product_movement"):
sr_entry = None
for entry in service_request.product_movement:
if entry.movement_type == product_location and not entry.linked_document:
sr_entry = entry
break
if not sr_entry:
sr_entry = service_request.append(
"product_movement",
{
"movement_type": product_location,
"movement_date": getattr(doc, "posting_date", None) or today(),
"handled_by": frappe.session.user,
},
)
sr_entry.linked_document_type = doc.doctype
sr_entry.linked_document = doc.name
service_request.save(ignore_permissions=True)


@frappe.whitelist()
Expand Down
Loading