diff --git a/beveren_fsm/field_service_management/api/service_request.py b/beveren_fsm/field_service_management/api/service_request.py index e85b1b4..096571d 100644 --- a/beveren_fsm/field_service_management/api/service_request.py +++ b/beveren_fsm/field_service_management/api/service_request.py @@ -1,3 +1,5 @@ +from collections import defaultdict + import frappe from frappe.utils import getdate @@ -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 diff --git a/beveren_fsm/field_service_management/doctype/product_location/product_location.json b/beveren_fsm/field_service_management/doctype/product_location/product_location.json index f5b2341..aacf392 100644 --- a/beveren_fsm/field_service_management/doctype/product_location/product_location.json +++ b/beveren_fsm/field_service_management/doctype/product_location/product_location.json @@ -15,7 +15,7 @@ "fieldtype": "Data", "in_list_view": 1, "in_standard_filter": 1, - "label": "Product Location", + "label": "Movement Type", "reqd": 1, "unique": 1 }, @@ -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", diff --git a/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json b/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json index ba4e42c..da2e73f 100644 --- a/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json +++ b/beveren_fsm/field_service_management/doctype/product_movement/product_movement.json @@ -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", @@ -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 }, @@ -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 }, @@ -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", diff --git a/beveren_fsm/field_service_management/doctype/service_order/service_order.js b/beveren_fsm/field_service_management/doctype/service_order/service_order.js index 4c552fa..ce56733 100644 --- a/beveren_fsm/field_service_management/doctype/service_order/service_order.js +++ b/beveren_fsm/field_service_management/doctype/service_order/service_order.js @@ -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, diff --git a/beveren_fsm/field_service_management/doctype/service_order/service_order.json b/beveren_fsm/field_service_management/doctype/service_order/service_order.json index fde64e1..b8d9d58 100644 --- a/beveren_fsm/field_service_management/doctype/service_order/service_order.json +++ b/beveren_fsm/field_service_management/doctype/service_order/service_order.json @@ -23,7 +23,7 @@ "priority", "company", "amended_from", - "current_product_location", + "product_location", "accounting_dimensions_section", "cost_center", "column_break_ekul", @@ -96,7 +96,9 @@ "column_break_gtsg", "preference_note", "status_section", - "per_billed" + "per_billed", + "product_tracking_tab", + "product_movement" ], "fields": [ { @@ -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 } ], @@ -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", diff --git a/beveren_fsm/field_service_management/doctype/service_order/service_order.py b/beveren_fsm/field_service_management/doctype/service_order/service_order.py index 3d905aa..941af31 100644 --- a/beveren_fsm/field_service_management/doctype/service_order/service_order.py +++ b/beveren_fsm/field_service_management/doctype/service_order/service_order.py @@ -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() @@ -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") @@ -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(), @@ -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 @@ -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() diff --git a/beveren_fsm/field_service_management/doctype/service_request/service_request.json b/beveren_fsm/field_service_management/doctype/service_request/service_request.json index d607288..52f92a6 100644 --- a/beveren_fsm/field_service_management/doctype/service_request/service_request.json +++ b/beveren_fsm/field_service_management/doctype/service_request/service_request.json @@ -49,9 +49,7 @@ "preferred_time", "column_break_extu", "preference_note", - "amended_from", - "section_break_mzqv", - "product_movement" + "amended_from" ], "fields": [ { @@ -347,20 +345,6 @@ "label": "Current Product Location", "no_copy": 1, "options": "Product Location" - }, - { - "fieldname": "section_break_mzqv", - "fieldtype": "Section Break", - "label": "Movement Tracker" - }, - { - "allow_on_submit": 1, - "fieldname": "product_movement", - "fieldtype": "Table", - "label": "Product Movement", - "no_copy": 1, - "options": "Product Movement", - "read_only": 1 } ], "index_web_pages_for_search": 1, @@ -375,7 +359,7 @@ "link_fieldname": "service_request" } ], - "modified": "2025-11-17 04:17:34.797562", + "modified": "2025-11-18 02:43:22.132242", "modified_by": "Administrator", "module": "Field Service Management", "name": "Service Request", diff --git a/schedule/src/pages/schedule/types.ts b/schedule/src/pages/schedule/types.ts index 3487e8a..92188d3 100644 --- a/schedule/src/pages/schedule/types.ts +++ b/schedule/src/pages/schedule/types.ts @@ -46,6 +46,7 @@ export interface ServiceRequestMovement { linked_document_type?: string; linked_document?: string; handled_by?: string; + service_order?: string; } export interface ServiceRequest {