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
75 changes: 75 additions & 0 deletions beveren_fsm/field_service_management/api/service_order.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
import frappe
from frappe.utils import getdate


@frappe.whitelist()
def get_service_orders_for_tracking(
status=None,
start_date=None,
end_date=None,
search=None,
limit_page_length: int | None = 50,
):
filters = {"docstatus": ["!=", 2]}

if status and status != "all":
filters["status"] = status

if start_date and end_date:
filters["posting_date"] = ["between", [getdate(start_date), getdate(end_date)]]
elif start_date:
filters["posting_date"] = [">=", getdate(start_date)]
elif end_date:
filters["posting_date"] = ["<=", getdate(end_date)]

or_filters = []
if search:
search_term = f"%{search.strip()}%"
or_filters = [
["Service Order", "name", "like", search_term],
["Service Order", "customer", "like", search_term],
["Service Order", "serial_no", "like", search_term],
["Service Order", "item_code", "like", search_term],
]

fields = [
"name",
"customer",
"status",
"posting_date",
"due_date",
"serial_no",
"item_code",
"priority",
"type",
"product_location",
]

limit = int(limit_page_length) if limit_page_length else None

orders = frappe.get_all(
"Service Order",
filters=filters,
or_filters=or_filters,
fields=fields,
order_by="posting_date desc",
limit_page_length=limit,
)

for order in orders:
doc = frappe.get_doc("Service Order", order.name)
order["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,
"service_order": order.name,
}
for movement in doc.product_movement
]

return orders
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,6 @@
"column_break_llgp",
"posting_date",
"due_date",
"amc_contract",
"column_break_ralr",
"priority",
"company",
Expand Down Expand Up @@ -88,6 +87,7 @@
"column_break_gakd",
"warranty_amc_status",
"warranty_expiry_date",
"amc_contract",
"amc_expiry_date",
"appointment_preference_section",
"preferred_date_1",
Expand Down Expand Up @@ -756,6 +756,7 @@
"options": "Service Area"
},
{
"depends_on": "eval:doc.warranty_amc_status==\"Under AMC\"",
"fieldname": "amc_contract",
"fieldtype": "Link",
"label": "AMC Contract",
Expand Down Expand Up @@ -813,6 +814,7 @@
"fieldtype": "Table",
"ignore_user_permissions": 1,
"label": "Product Tracker",
"no_copy": 1,
"options": "Product Movement",
"read_only": 1
},
Expand All @@ -822,6 +824,7 @@
"fieldname": "product_location",
"fieldtype": "Data",
"label": "Product Location",
"no_copy": 1,
"read_only": 1
}
],
Expand All @@ -837,7 +840,7 @@
"link_fieldname": "custom_reference_service_document"
}
],
"modified": "2025-11-18 03:10:34.985668",
"modified": "2025-11-18 05:19:20.178318",
"modified_by": "Administrator",
"module": "Field Service Management",
"name": "Service Order",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@
from frappe import _
from frappe.model.document import Document
from frappe.model.mapper import get_mapped_doc
from frappe.utils import flt, today
from frappe.utils import flt, getdate, today

LOCATION_STATUS_MAP = {
"delivered to customer": "Review",
Expand Down Expand Up @@ -489,10 +489,12 @@ def make_delivery_note(service_order: str, items=None, product_location: str | N
def make_purchase_receipt(service_order: str, items=None, product_location: str | None = None):
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)
service_request = None
if order.service_request:
try:
service_request = frappe.get_doc("Service Request", order.service_request)
except frappe.DoesNotExistError:
service_request = None

# if not service_request.repair_vendor:
# frappe.throw(
Expand All @@ -504,8 +506,12 @@ def make_purchase_receipt(service_order: str, items=None, product_location: str
purchase_receipt = frappe.new_doc("Purchase Receipt")
purchase_receipt.company = order.company
purchase_receipt.posting_date = today()
purchase_receipt.supplier = service_request.repair_vendor
purchase_receipt.supplier_address = service_request.customer_address
purchase_receipt.supplier = getattr(order, "repair_vendor", None) or getattr(
service_request, "repair_vendor", None
)
purchase_receipt.supplier_address = getattr(order, "supplier_address", None) or getattr(
service_request, "customer_address", None
)
purchase_receipt.tc_name = getattr(order, "tc_name", None)
purchase_receipt.terms = getattr(order, "terms", None)
purchase_receipt.custom_service_order = order.name
Expand Down Expand Up @@ -656,10 +662,12 @@ def get(self, key, default=None):
def make_purchase_order(service_order: str, items=None, product_location: str | None = None):
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)
service_request = None
if order.service_request:
try:
service_request = frappe.get_doc("Service Request", order.service_request)
except frappe.DoesNotExistError:
service_request = None

# if not service_request.repair_vendor:
# frappe.throw(
Expand All @@ -672,7 +680,9 @@ def make_purchase_order(service_order: str, items=None, product_location: str |
purchase_order.company = order.company
purchase_order.transaction_date = today()
purchase_order.schedule_date = today()
purchase_order.supplier = service_request.repair_vendor
purchase_order.supplier = getattr(order, "repair_vendor", None) or getattr(
service_request, "repair_vendor", None
)
purchase_order.tc_name = getattr(order, "tc_name", None)
purchase_order.terms = getattr(order, "terms", None)
purchase_order.custom_service_order = order.name
Expand Down Expand Up @@ -821,10 +831,12 @@ def get(self, key, default=None):
def make_purchase_invoice(service_order: str, items=None, product_location: str | None = None):
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)
service_request = None
if order.service_request:
try:
service_request = frappe.get_doc("Service Request", order.service_request)
except frappe.DoesNotExistError:
service_request = None

# if not service_request.repair_vendor:
# frappe.throw(
Expand All @@ -836,7 +848,9 @@ def make_purchase_invoice(service_order: str, items=None, product_location: str
purchase_invoice = frappe.new_doc("Purchase Invoice")
purchase_invoice.company = order.company
purchase_invoice.posting_date = today()
purchase_invoice.supplier = service_request.repair_vendor
purchase_invoice.supplier = getattr(order, "repair_vendor", None) or getattr(
service_request, "repair_vendor", None
)
purchase_invoice.tc_name = getattr(order, "tc_name", None)
purchase_invoice.terms = getattr(order, "terms", None)
purchase_invoice.custom_service_order = order.name
Expand Down
2 changes: 1 addition & 1 deletion schedule/src/components/layout/sidebar-menu.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ export function SidebarMenu({

const menuItems: MenuItem[] = [
{ icon: Home, label: "Home", key: "home", onClick: onScheduleClick },
{ icon: ClipboardList, label: "Requests", key: "requests", onClick: onRequestsClick },
{ icon: ClipboardList, label: "Product Movement", key: "requests", onClick: onRequestsClick },
{ icon: Users, label: "Technicians", key: "technicians", onClick: onTechniciansClick },
{ icon: Settings, label: "Settings", key: "settings", onClick: onSettingsClick },
];
Expand Down
37 changes: 35 additions & 2 deletions schedule/src/components/schedule/gantt-view.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -76,6 +76,21 @@ export function GanttView({
loadTechnicians();
}, []);

useEffect(() => {
const handler = (event: Event) => {
const detail = (event as CustomEvent<{ service_order: string; customer?: string }>).detail;
if (!detail) return;
setCreateServiceOrder(detail.service_order);
if (detail.customer) {
setCreateCustomer(detail.customer);
}
setCreateOpen(true);
};

window.addEventListener("open-create-appointment", handler);
return () => window.removeEventListener("open-create-appointment", handler);
}, []);

const loadTechnicians = async () => {
try {
const data = await fetchTechnicians();
Expand Down Expand Up @@ -552,11 +567,29 @@ export function GanttView({
</div>
<div>
<label className="text-xs text-muted-foreground">Scheduled Start</label>
<input type="datetime-local" className="w-full border border-input rounded px-2 py-1 text-sm bg-background text-foreground" value={createStart ? `${createStart.getFullYear()}-${String(createStart.getMonth()+1).padStart(2,'0')}-${String(createStart.getDate()).padStart(2,'0')}T${String(createStart.getHours()).padStart(2,'0')}:${String(createStart.getMinutes()).padStart(2,'0')}` : ""} onChange={(e) => setCreateStart(new Date(e.target.value))} />
<input
type="datetime-local"
className="w-full border border-input rounded px-2 py-1 text-sm bg-background text-foreground"
value={
createStart
? `${createStart.getFullYear()}-${String(createStart.getMonth() + 1).padStart(2, "0")}-${String(createStart.getDate()).padStart(2, "0")}T${String(createStart.getHours()).padStart(2, "0")}:${String(createStart.getMinutes()).padStart(2, "0")}`
: ""
}
onChange={(e) => setCreateStart(new Date(e.target.value))}
/>
</div>
<div>
<label className="text-xs text-muted-foreground">Scheduled Finish</label>
<input type="datetime-local" className="w-full border border-input rounded px-2 py-1 text-sm bg-background text-foreground" value={createFinish ? `${createFinish.getFullYear()}-${String(createFinish.getMonth()+1).padStart(2,'0')}-${String(createFinish.getDate()).padStart(2,'0')}T${String(createFinish.getHours()).padStart(2,'0')}:${String(createFinish.getMinutes()).padStart(2,'0')}` : ""} onChange={(e) => setCreateFinish(new Date(e.target.value))} />
<input
type="datetime-local"
className="w-full border border-input rounded px-2 py-1 text-sm bg-background text-foreground"
value={
createFinish
? `${createFinish.getFullYear()}-${String(createFinish.getMonth() + 1).padStart(2, "0")}-${String(createFinish.getDate()).padStart(2, "0")}T${String(createFinish.getHours()).padStart(2, "0")}:${String(createFinish.getMinutes()).padStart(2, "0")}`
: ""
}
onChange={(e) => setCreateFinish(new Date(e.target.value))}
/>
</div>
<div className="sm:col-span-1">
<label className="text-xs text-muted-foreground">Technicians</label>
Expand Down
57 changes: 31 additions & 26 deletions schedule/src/components/schedule/schedule-left-panel.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -25,18 +25,25 @@ import { fetchServiceOrderDetail } from "../../hooks/use-appointments";

const STATUS_OPTIONS: AppointmentStatus[] = [
"Open",
"Scheduled",
"Dispatched",
"In Progress",
"Completed",
"Cancelled"
"Quotation",
"Converted",
"Due Soon",
"Overdue",
"On Hold",
"Closed",
];

const getStatusColor = (status: AppointmentStatus): string => {
const colors: Record<AppointmentStatus, string> = {
Open: "bg-blue-100 text-blue-800 border-blue-300",
Quotation: "bg-indigo-100 text-indigo-800 border-indigo-300",
Converted: "bg-emerald-100 text-emerald-800 border-emerald-300",
"Due Soon": "bg-amber-100 text-amber-800 border-amber-300",
Overdue: "bg-red-100 text-red-800 border-red-300",
"On Hold": "bg-gray-200 text-gray-800 border-gray-300",
Closed: "bg-slate-100 text-slate-800 border-slate-300",
Scheduled: "bg-blue-100 text-blue-800 border-blue-300",
Dispatched: "bg-orange-100 text-orange-800 border-orange-300",
Dispatched: "bg-purple-100 text-purple-800 border-purple-300",
"In Progress": "bg-orange-100 text-orange-800 border-orange-300",
Completed: "bg-green-100 text-green-800 border-green-300",
Cancelled: "bg-gray-100 text-gray-800 border-gray-300",
Expand Down Expand Up @@ -147,23 +154,18 @@ export function ScheduleLeftPanel({
}
};

const getOrderStatusColor = (status?: string) => {
if (!status) return "border-border text-muted-foreground";
const normalized = status.toLowerCase();
if (normalized.includes("open") || normalized.includes("pending")) {
return "bg-blue-50 text-blue-700 border-blue-200";
}
if (normalized.includes("progress") || normalized.includes("dispatch")) {
return "bg-orange-50 text-orange-700 border-orange-200";
}
if (normalized.includes("complete") || normalized.includes("closed")) {
return "bg-green-50 text-green-700 border-green-200";
}
if (normalized.includes("cancel")) {
return "bg-gray-50 text-gray-600 border-gray-200";
}
return "border-border text-muted-foreground";
};
const getOrderStatusColor = (status?: string) => {
if (!status) return "border-border text-muted-foreground";
const normalized = status.toLowerCase();
if (normalized === "open") return "bg-cyan-50 text-cyan-700 border-cyan-200";
if (normalized === "scheduled") return "bg-blue-50 text-blue-700 border-blue-200";
if (normalized === "dispatched") return "bg-purple-50 text-purple-700 border-purple-200";
if (normalized === "in progress") return "bg-orange-50 text-orange-700 border-orange-200";
if (normalized === "review") return "bg-pink-50 text-pink-700 border-pink-200";
if (normalized === "completed") return "bg-green-50 text-green-700 border-green-200";
if (normalized === "cancelled") return "bg-gray-100 text-gray-700 border-gray-200";
return "border-border text-muted-foreground";
};

const getOrderPriorityColor = (priority?: string) => {
if (!priority) return "border-border text-muted-foreground";
Expand Down Expand Up @@ -567,16 +569,19 @@ export function ScheduleLeftPanel({
/>
<div className="flex-1 min-w-0">
<div className="flex items-center justify-between mb-1">
<span className="text-sm font-medium truncate">
{appointment.service_order || appointment.name}
</span>
<span className="text-sm font-medium truncate">{appointment.name}</span>
<Badge
variant="outline"
className={`text-xs ${getStatusColor(appointment.status)}`}
>
{appointment.status}
</Badge>
</div>
{appointment.service_order && (
<p className="text-[11px] text-muted-foreground mb-1">
Order: {appointment.service_order}
</p>
)}
<p className="text-xs text-muted-foreground mb-2 line-clamp-2">
{getShortDescription(appointment)}
</p>
Expand Down
Loading