Asset lite updated changes from RC1
This commit is contained in:
parent
eec1247c9a
commit
eab723d294
@ -1,5 +1,51 @@
|
|||||||
import frappe
|
import frappe
|
||||||
from frappe import _
|
from frappe import _
|
||||||
|
from datetime import datetime, timedelta
|
||||||
|
|
||||||
|
def _expand_list_filters(filters):
|
||||||
|
"""Expand UI filter keys (creation_from, etc.) into Frappe filter dict."""
|
||||||
|
if not filters:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
expanded = dict(filters)
|
||||||
|
|
||||||
|
creation_from = expanded.pop('creation_from', None)
|
||||||
|
creation_to = expanded.pop('creation_to', None)
|
||||||
|
if creation_from and creation_to:
|
||||||
|
expanded['creation'] = [
|
||||||
|
'between',
|
||||||
|
[f"{str(creation_from)[:10]} 00:00:00", f"{str(creation_to)[:10]} 23:59:59"],
|
||||||
|
]
|
||||||
|
elif creation_from:
|
||||||
|
expanded['creation'] = ['>=', f"{str(creation_from)[:10]} 00:00:00"]
|
||||||
|
elif creation_to:
|
||||||
|
end = str(creation_to)[:10]
|
||||||
|
try:
|
||||||
|
d = datetime.strptime(end, '%Y-%m-%d') + timedelta(days=1)
|
||||||
|
end_exclusive = d.strftime('%Y-%m-%d')
|
||||||
|
except Exception:
|
||||||
|
end_exclusive = end
|
||||||
|
expanded['creation'] = ['<', f"{end_exclusive} 00:00:00"]
|
||||||
|
|
||||||
|
modified_from = expanded.pop('modified_from', None)
|
||||||
|
modified_to = expanded.pop('modified_to', None)
|
||||||
|
if modified_from and modified_to:
|
||||||
|
expanded['modified'] = [
|
||||||
|
'between',
|
||||||
|
[f"{str(modified_from)[:10]} 00:00:00", f"{str(modified_to)[:10]} 23:59:59"],
|
||||||
|
]
|
||||||
|
elif modified_from:
|
||||||
|
expanded['modified'] = ['>=', f"{str(modified_from)[:10]} 00:00:00"]
|
||||||
|
elif modified_to:
|
||||||
|
end = str(modified_to)[:10]
|
||||||
|
try:
|
||||||
|
d = datetime.strptime(end, '%Y-%m-%d') + timedelta(days=1)
|
||||||
|
end_exclusive = d.strftime('%Y-%m-%d')
|
||||||
|
except Exception:
|
||||||
|
end_exclusive = end
|
||||||
|
expanded['modified'] = ['<', f"{end_exclusive} 00:00:00"]
|
||||||
|
|
||||||
|
return expanded
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest = True)
|
@frappe.whitelist(allow_guest = True)
|
||||||
def get_assets(filters=None, fields=None, limit=20, offset=0, order_by=None, include_finance_books=True):
|
def get_assets(filters=None, fields=None, limit=20, offset=0, order_by=None, include_finance_books=True):
|
||||||
@ -31,7 +77,12 @@ def get_assets(filters=None, fields=None, limit=20, offset=0, order_by=None, inc
|
|||||||
# Parse filters if provided
|
# Parse filters if provided
|
||||||
if filters and isinstance(filters, str):
|
if filters and isinstance(filters, str):
|
||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
|
filters = _expand_list_filters(filters or {})
|
||||||
|
|
||||||
|
from asset_lite.api.userperm_api import apply_permission_filters
|
||||||
|
filters = apply_permission_filters(filters, "Asset")
|
||||||
|
|
||||||
# Handle tree-based fields (Department is a nested set/tree structure)
|
# Handle tree-based fields (Department is a nested set/tree structure)
|
||||||
if filters.get('department') and isinstance(filters['department'], str):
|
if filters.get('department') and isinstance(filters['department'], str):
|
||||||
filters['department'] = ['descendants of (inclusive)', filters['department']]
|
filters['department'] = ['descendants of (inclusive)', filters['department']]
|
||||||
|
|||||||
@ -29,6 +29,9 @@ def get_asset_maintenance_logs(filters=None, fields=None, limit=20, offset=0, or
|
|||||||
# Parse filters if provided
|
# Parse filters if provided
|
||||||
if filters and isinstance(filters, str):
|
if filters and isinstance(filters, str):
|
||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
|
filters = expand_dashboard_filters(filters or {}, "Asset Maintenance Log")
|
||||||
|
|
||||||
# Parse fields if provided
|
# Parse fields if provided
|
||||||
if fields and isinstance(fields, str):
|
if fields and isinstance(fields, str):
|
||||||
|
|||||||
@ -27,7 +27,12 @@ def get_user_details(user_id=None):
|
|||||||
"last_login": user.last_login,
|
"last_login": user.last_login,
|
||||||
"enabled": user.enabled,
|
"enabled": user.enabled,
|
||||||
"creation": user.creation,
|
"creation": user.creation,
|
||||||
"modified": user.modified
|
"modified": user.modified,
|
||||||
|
"custom_site_name": user.get("custom_site_name") or "",
|
||||||
|
"custom_phcc_site_name": user.get("custom_phcc_site_name") or "",
|
||||||
|
"role_profile_name": user.get("role_profile_name") or "",
|
||||||
|
"custom_department": user.get("custom_department") or "",
|
||||||
|
"csrf_token": frappe.local.session.data.csrf_token or "",
|
||||||
}
|
}
|
||||||
|
|
||||||
frappe.response.message = response_data
|
frappe.response.message = response_data
|
||||||
|
|||||||
@ -15,31 +15,382 @@ def _err(msg, code=500):
|
|||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest = True)
|
@frappe.whitelist(allow_guest = True)
|
||||||
def get_number_cards():
|
def get_number_cards(company=None, site_name=None):
|
||||||
"""
|
"""
|
||||||
Returns counts for Number Cards:
|
Returns counts for Number Cards:
|
||||||
- total_assets
|
- total_assets
|
||||||
- work_orders_open
|
- work_orders_open
|
||||||
- work_orders_in_progress
|
- work_orders_in_progress
|
||||||
- work_orders_completed
|
- work_orders_completed
|
||||||
|
- work_orders_closed
|
||||||
|
|
||||||
|
Optional filters: company (Hospital), site_name (Mobile Team Site)
|
||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
total_assets = frappe.db.count("Asset")
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
work_orders_open = frappe.db.count("Work Order", {"status": ["in", ["Not Started", "Open", "Pending"]]})
|
|
||||||
work_orders_in_progress = frappe.db.count("Work Order", {"status": ["in", ["In Process", "In Progress", "Started"]]})
|
from asset_lite.api.userperm_api import get_permission_filters
|
||||||
work_orders_completed = frappe.db.count("Work Order", {"status": ["in", ["Completed", "Closed", "Finished"]]})
|
|
||||||
|
asset_filters = expand_dashboard_filters({"company": company, "site_name": site_name}, "Asset")
|
||||||
|
wo_base = expand_dashboard_filters({"company": company, "site_name": site_name}, "Work_Order")
|
||||||
|
|
||||||
|
asset_perm = get_permission_filters("Asset").get("filters") or {}
|
||||||
|
wo_perm = get_permission_filters("Work_Order").get("filters") or {}
|
||||||
|
|
||||||
|
asset_filters = {**asset_perm, **(asset_filters or {})}
|
||||||
|
wo_base = {**wo_perm, **(wo_base or {})}
|
||||||
|
|
||||||
|
total_assets = frappe.db.count("Asset", asset_filters or {})
|
||||||
|
|
||||||
|
open_filters = {**wo_base, "repair_status": "Open"}
|
||||||
|
in_progress_filters = {**wo_base, "repair_status": "Work In Progress"}
|
||||||
|
completed_filters = {**wo_base, "repair_status": "Completed"}
|
||||||
|
closed_filters = {**wo_base, "repair_status": "Closed"}
|
||||||
|
|
||||||
|
work_orders_open = frappe.db.count("Work_Order", open_filters)
|
||||||
|
work_orders_in_progress = frappe.db.count("Work_Order", in_progress_filters)
|
||||||
|
work_orders_completed = frappe.db.count("Work_Order", completed_filters)
|
||||||
|
work_orders_closed = frappe.db.count("Work_Order", closed_filters)
|
||||||
|
|
||||||
_ok({
|
_ok({
|
||||||
"total_assets": total_assets,
|
"total_assets": total_assets,
|
||||||
"work_orders_open": work_orders_open,
|
"work_orders_open": work_orders_open,
|
||||||
"work_orders_in_progress": work_orders_in_progress,
|
"work_orders_in_progress": work_orders_in_progress,
|
||||||
"work_orders_completed": work_orders_completed,
|
"work_orders_completed": work_orders_completed,
|
||||||
|
"work_orders_closed": work_orders_closed,
|
||||||
})
|
})
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
frappe.log_error(frappe.get_traceback(), "get_number_cards")
|
frappe.log_error(frappe.get_traceback(), "get_number_cards")
|
||||||
_err(str(e))
|
_err(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
WO_STATUS_SORT_ORDER = [
|
||||||
|
"Open",
|
||||||
|
"Work In Progress",
|
||||||
|
"Pending Review",
|
||||||
|
"Completed",
|
||||||
|
"Closed",
|
||||||
|
"Rejected",
|
||||||
|
"Cancelled",
|
||||||
|
]
|
||||||
|
|
||||||
|
WO_STATUS_COLORS = {
|
||||||
|
"Open": "#F59E0B",
|
||||||
|
"Work In Progress": "#3B82F6",
|
||||||
|
"Pending Review": "#E11D48",
|
||||||
|
"Completed": "#10B981",
|
||||||
|
"Closed": "#A855F7",
|
||||||
|
"Rejected": "#EF4444",
|
||||||
|
"Cancelled": "#6B7280",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _is_null_or_unknown_label(label):
|
||||||
|
key = (label or "").strip().lower()
|
||||||
|
return key in ("", "null", "unknown", "none", "undefined")
|
||||||
|
|
||||||
|
|
||||||
|
def _sort_status_labels(labels):
|
||||||
|
order = {name: idx for idx, name in enumerate(WO_STATUS_SORT_ORDER)}
|
||||||
|
|
||||||
|
def sort_key(label):
|
||||||
|
if label in order:
|
||||||
|
return (0, order[label])
|
||||||
|
return (1, label)
|
||||||
|
|
||||||
|
return sorted(labels, key=sort_key)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_dashboard_work_order_metrics(
|
||||||
|
company=None,
|
||||||
|
site_name=None,
|
||||||
|
work_order_type=None,
|
||||||
|
from_date=None,
|
||||||
|
to_date=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Aggregate Work Orders by repair_status for dashboard pie chart.
|
||||||
|
Optional filters: company, site_name, work_order_type, from_date, to_date (creation).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
|
from asset_lite.api.userperm_api import get_permission_filters
|
||||||
|
|
||||||
|
wo_filters = expand_dashboard_filters(
|
||||||
|
{"company": company, "site_name": site_name}, "Work_Order"
|
||||||
|
)
|
||||||
|
wo_perm = get_permission_filters("Work_Order").get("filters") or {}
|
||||||
|
wo_filters = {**wo_perm, **(wo_filters or {})}
|
||||||
|
|
||||||
|
if work_order_type:
|
||||||
|
wo_filters["work_order_type"] = work_order_type
|
||||||
|
|
||||||
|
if from_date and to_date:
|
||||||
|
wo_filters["creation"] = ["between", [f"{from_date} 00:00:00", f"{to_date} 23:59:59"]]
|
||||||
|
elif from_date:
|
||||||
|
wo_filters["creation"] = [">=", f"{from_date} 00:00:00"]
|
||||||
|
elif to_date:
|
||||||
|
wo_filters["creation"] = ["<=", f"{to_date} 23:59:59"]
|
||||||
|
|
||||||
|
from asset_lite.api.work_order_api import get_work_order_role_query
|
||||||
|
|
||||||
|
role_filters, or_filters = get_work_order_role_query()
|
||||||
|
wo_filters.update(role_filters or {})
|
||||||
|
|
||||||
|
rows = frappe.get_all(
|
||||||
|
"Work_Order",
|
||||||
|
filters=wo_filters or None,
|
||||||
|
or_filters=or_filters,
|
||||||
|
fields=["repair_status", "work_order_type", "workflow_state"],
|
||||||
|
limit=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
status_counts = {}
|
||||||
|
type_stats = {}
|
||||||
|
|
||||||
|
for row in rows:
|
||||||
|
status = (row.get("repair_status") or "Unknown").strip()
|
||||||
|
status_counts[status] = status_counts.get(status, 0) + 1
|
||||||
|
|
||||||
|
wo_type = (row.get("work_order_type") or "").strip() or "Unknown"
|
||||||
|
if _is_null_or_unknown_label(wo_type):
|
||||||
|
continue
|
||||||
|
|
||||||
|
bucket = type_stats.setdefault(
|
||||||
|
wo_type,
|
||||||
|
{
|
||||||
|
"type": wo_type,
|
||||||
|
"total": 0,
|
||||||
|
"open": 0,
|
||||||
|
"inProgress": 0,
|
||||||
|
"pendingReview": 0,
|
||||||
|
"completed": 0,
|
||||||
|
"closed": 0,
|
||||||
|
"rejected": 0,
|
||||||
|
"cancelled": 0,
|
||||||
|
"wfCompleted": 0,
|
||||||
|
"wfInProgress": 0,
|
||||||
|
"wfRejected": 0,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
bucket["total"] += 1
|
||||||
|
|
||||||
|
if status == "Open":
|
||||||
|
bucket["open"] += 1
|
||||||
|
elif status == "Work In Progress":
|
||||||
|
bucket["inProgress"] += 1
|
||||||
|
elif status == "Pending Review":
|
||||||
|
bucket["pendingReview"] += 1
|
||||||
|
elif status == "Completed":
|
||||||
|
bucket["completed"] += 1
|
||||||
|
elif status == "Closed":
|
||||||
|
bucket["closed"] += 1
|
||||||
|
elif status == "Rejected":
|
||||||
|
bucket["rejected"] += 1
|
||||||
|
elif status == "Cancelled":
|
||||||
|
bucket["cancelled"] += 1
|
||||||
|
|
||||||
|
wf_state = (row.get("workflow_state") or "").strip()
|
||||||
|
if wf_state == "Completed":
|
||||||
|
bucket["wfCompleted"] += 1
|
||||||
|
elif wf_state in ("Work In Progress", "In Progress"):
|
||||||
|
bucket["wfInProgress"] += 1
|
||||||
|
elif wf_state == "Rejected":
|
||||||
|
bucket["wfRejected"] += 1
|
||||||
|
|
||||||
|
labels = _sort_status_labels(list(status_counts.keys()))
|
||||||
|
values = [status_counts[label] for label in labels]
|
||||||
|
colors = [WO_STATUS_COLORS.get(label, "#6366F1") for label in labels]
|
||||||
|
|
||||||
|
completed_raw = status_counts.get("Completed", 0)
|
||||||
|
closed_raw = status_counts.get("Closed", 0)
|
||||||
|
completed_combined = completed_raw + closed_raw
|
||||||
|
|
||||||
|
counts = {
|
||||||
|
"open": status_counts.get("Open", 0),
|
||||||
|
"inProgress": status_counts.get("Work In Progress", 0),
|
||||||
|
"completed": completed_raw,
|
||||||
|
"completedCombined": completed_combined,
|
||||||
|
"pendingReview": status_counts.get("Pending Review", 0),
|
||||||
|
"rejected": status_counts.get("Rejected", 0),
|
||||||
|
"closed": closed_raw,
|
||||||
|
"cancelled": status_counts.get("Cancelled", 0),
|
||||||
|
"total": len(rows),
|
||||||
|
}
|
||||||
|
|
||||||
|
type_rates = []
|
||||||
|
for wo_type, bucket in type_stats.items():
|
||||||
|
combined = bucket["completed"] + bucket["closed"]
|
||||||
|
bucket["completedCombined"] = combined
|
||||||
|
type_rates.append(bucket)
|
||||||
|
|
||||||
|
type_rates.sort(key=lambda item: item.get("total", 0), reverse=True)
|
||||||
|
|
||||||
|
type_status_matrix = {}
|
||||||
|
for row in rows:
|
||||||
|
status = (row.get("repair_status") or "Unknown").strip()
|
||||||
|
wo_type = (row.get("work_order_type") or "").strip() or "Unknown"
|
||||||
|
if _is_null_or_unknown_label(wo_type):
|
||||||
|
continue
|
||||||
|
bucket = type_status_matrix.setdefault(wo_type, {})
|
||||||
|
bucket[status] = bucket.get(status, 0) + 1
|
||||||
|
|
||||||
|
type_status_labels = sorted(
|
||||||
|
type_status_matrix.keys(),
|
||||||
|
key=lambda item: sum(type_status_matrix[item].values()),
|
||||||
|
reverse=True,
|
||||||
|
)
|
||||||
|
matrix_statuses = set()
|
||||||
|
for status_map in type_status_matrix.values():
|
||||||
|
matrix_statuses.update(status_map.keys())
|
||||||
|
type_status_columns = _sort_status_labels(list(matrix_statuses))
|
||||||
|
type_status_datasets = [
|
||||||
|
{
|
||||||
|
"name": status,
|
||||||
|
"values": [
|
||||||
|
type_status_matrix.get(wo_type, {}).get(status, 0)
|
||||||
|
for wo_type in type_status_labels
|
||||||
|
],
|
||||||
|
"color": WO_STATUS_COLORS.get(status, "#6366F1"),
|
||||||
|
}
|
||||||
|
for status in type_status_columns
|
||||||
|
]
|
||||||
|
|
||||||
|
_ok({
|
||||||
|
"counts": counts,
|
||||||
|
"completion_by_type": {
|
||||||
|
"labels": [item["type"] for item in type_rates],
|
||||||
|
"rates": type_rates,
|
||||||
|
},
|
||||||
|
"work_order_chart": {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [{
|
||||||
|
"name": "Work Orders",
|
||||||
|
"values": values,
|
||||||
|
"colors": colors,
|
||||||
|
}],
|
||||||
|
"type": "Pie",
|
||||||
|
},
|
||||||
|
"type_status_chart": {
|
||||||
|
"labels": type_status_labels,
|
||||||
|
"datasets": type_status_datasets,
|
||||||
|
"type": "Bar",
|
||||||
|
},
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(frappe.get_traceback(), "get_dashboard_work_order_metrics")
|
||||||
|
_err(str(e))
|
||||||
|
|
||||||
|
|
||||||
|
ASSET_DEVICE_STATUS_COLORS = {
|
||||||
|
"up": "#10B981",
|
||||||
|
"down": "#EF4444",
|
||||||
|
"under maintenance": "#F59E0B",
|
||||||
|
"decommissioned": "#6B7280",
|
||||||
|
}
|
||||||
|
|
||||||
|
ASSET_STATUS_FALLBACK_COLORS = [
|
||||||
|
"#6366F1",
|
||||||
|
"#8B5CF6",
|
||||||
|
"#06B6D4",
|
||||||
|
"#EC4899",
|
||||||
|
"#F59E0B",
|
||||||
|
"#10B981",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _asset_device_status_color(label, fallback_index=0):
|
||||||
|
key = (label or "").strip().lower()
|
||||||
|
if key in ASSET_DEVICE_STATUS_COLORS:
|
||||||
|
return ASSET_DEVICE_STATUS_COLORS[key]
|
||||||
|
if "maintenance" in key:
|
||||||
|
return ASSET_DEVICE_STATUS_COLORS["under maintenance"]
|
||||||
|
if "decommission" in key:
|
||||||
|
return ASSET_DEVICE_STATUS_COLORS["decommissioned"]
|
||||||
|
return ASSET_STATUS_FALLBACK_COLORS[fallback_index % len(ASSET_STATUS_FALLBACK_COLORS)]
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_asset_device_status_summary(
|
||||||
|
company=None,
|
||||||
|
site_name=None,
|
||||||
|
from_date=None,
|
||||||
|
to_date=None,
|
||||||
|
):
|
||||||
|
"""
|
||||||
|
Aggregate Assets by custom_device_status for dashboard pie chart.
|
||||||
|
Optional filters: company, site_name (custom_site on Asset), from_date, to_date (creation).
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
|
from asset_lite.api.userperm_api import get_permission_filters
|
||||||
|
|
||||||
|
asset_filters = expand_dashboard_filters(
|
||||||
|
{"company": company, "site_name": site_name}, "Asset"
|
||||||
|
)
|
||||||
|
asset_perm = get_permission_filters("Asset").get("filters") or {}
|
||||||
|
asset_filters = {**asset_perm, **(asset_filters or {})}
|
||||||
|
|
||||||
|
if frappe.db.has_column("Asset", "custom_delete_status"):
|
||||||
|
asset_filters["custom_delete_status"] = ["!=", "Deleted"]
|
||||||
|
|
||||||
|
if from_date and to_date:
|
||||||
|
asset_filters["creation"] = [
|
||||||
|
"between",
|
||||||
|
[f"{from_date} 00:00:00", f"{to_date} 23:59:59"],
|
||||||
|
]
|
||||||
|
elif from_date:
|
||||||
|
asset_filters["creation"] = [">=", f"{from_date} 00:00:00"]
|
||||||
|
elif to_date:
|
||||||
|
asset_filters["creation"] = ["<=", f"{to_date} 23:59:59"]
|
||||||
|
|
||||||
|
rows = frappe.get_all(
|
||||||
|
"Asset",
|
||||||
|
filters=asset_filters or None,
|
||||||
|
fields=["custom_device_status"],
|
||||||
|
limit=0,
|
||||||
|
)
|
||||||
|
|
||||||
|
status_counts = {}
|
||||||
|
for row in rows:
|
||||||
|
raw = (row.get("custom_device_status") or "").strip()
|
||||||
|
label = raw or "Unknown"
|
||||||
|
status_counts[label] = status_counts.get(label, 0) + 1
|
||||||
|
|
||||||
|
sorted_items = sorted(status_counts.items(), key=lambda item: item[1], reverse=True)
|
||||||
|
labels = [label for label, _count in sorted_items]
|
||||||
|
values = [count for _label, count in sorted_items]
|
||||||
|
colors = [
|
||||||
|
_asset_device_status_color(label, idx) for idx, label in enumerate(labels)
|
||||||
|
]
|
||||||
|
|
||||||
|
assets_down = 0
|
||||||
|
for label, count in status_counts.items():
|
||||||
|
if label.strip().lower() == "down":
|
||||||
|
assets_down += count
|
||||||
|
|
||||||
|
total_assets = len(rows)
|
||||||
|
|
||||||
|
_ok({
|
||||||
|
"total_assets": total_assets,
|
||||||
|
"assets_down": assets_down,
|
||||||
|
"chart": {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [{
|
||||||
|
"name": "Assets",
|
||||||
|
"values": values,
|
||||||
|
"colors": colors,
|
||||||
|
}],
|
||||||
|
"type": "Pie",
|
||||||
|
"total": total_assets,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
frappe.log_error(frappe.get_traceback(), "get_asset_device_status_summary")
|
||||||
|
_err(str(e))
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest = True)
|
@frappe.whitelist(allow_guest = True)
|
||||||
def list_dashboard_charts(search=None, public_only=True, limit=50):
|
def list_dashboard_charts(search=None, public_only=True, limit=50):
|
||||||
"""
|
"""
|
||||||
|
|||||||
37
asset_lite/api/dashboard_filters.py
Normal file
37
asset_lite/api/dashboard_filters.py
Normal file
@ -0,0 +1,37 @@
|
|||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def expand_dashboard_filters(filters=None, doctype=None):
|
||||||
|
"""Normalize dashboard hospital/site filters for list APIs."""
|
||||||
|
if not filters:
|
||||||
|
return {}
|
||||||
|
|
||||||
|
expanded = dict(filters)
|
||||||
|
company = expanded.pop("company", None) or expanded.pop("hospital", None)
|
||||||
|
site_name = expanded.pop("site_name", None)
|
||||||
|
|
||||||
|
if doctype == "Asset Maintenance Log":
|
||||||
|
if company:
|
||||||
|
expanded["custom_hospital_name"] = company
|
||||||
|
if site_name:
|
||||||
|
asset_names = frappe.get_all(
|
||||||
|
"Asset",
|
||||||
|
filters={"custom_site": site_name},
|
||||||
|
pluck="name",
|
||||||
|
)
|
||||||
|
expanded["asset_name"] = ["in", asset_names or ["__no_match__"]]
|
||||||
|
elif doctype == "Asset":
|
||||||
|
if company:
|
||||||
|
expanded["company"] = company
|
||||||
|
if site_name:
|
||||||
|
expanded["custom_site"] = site_name
|
||||||
|
elif doctype == "Work_Order":
|
||||||
|
if company:
|
||||||
|
expanded["company"] = company
|
||||||
|
if site_name:
|
||||||
|
expanded["site_name"] = site_name
|
||||||
|
elif doctype == "PM Schedule Generator":
|
||||||
|
if company:
|
||||||
|
expanded["hospital"] = company
|
||||||
|
|
||||||
|
return expanded
|
||||||
277
asset_lite/api/dashboard_report_queries.py
Normal file
277
asset_lite/api/dashboard_report_queries.py
Normal file
@ -0,0 +1,277 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
def _aml_location_conditions(filters, aml_alias="aml", asset_alias="a"):
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append(f"{aml_alias}.custom_hospital_name = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append(f"{asset_alias}.custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
return " AND ".join(conditions), params
|
||||||
|
|
||||||
|
|
||||||
|
def asset_wise_count_execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
where, params = _aml_location_conditions(filters)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
aml.item_name AS item_name,
|
||||||
|
aml.maintenance_status AS maintenance_status,
|
||||||
|
SUM(CASE WHEN aml.due_date = aml.completion_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_on_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date < aml.due_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_within_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date > aml.due_date THEN 1 ELSE 0 END) AS delay_in_completion,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date > CURRENT_DATE() THEN 1 ELSE 0 END) AS pending,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date < CURRENT_DATE() THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY aml.item_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200},
|
||||||
|
{"label": _("Maintenance Status"), "fieldname": "maintenance_status", "fieldtype": "Data", "width": 150},
|
||||||
|
{"label": _("Completed On Time"), "fieldname": "completed_on_time", "fieldtype": "Int", "width": 150},
|
||||||
|
{"label": _("Completed Within Time"), "fieldname": "completed_within_time", "fieldtype": "Int", "width": 170},
|
||||||
|
{"label": _("Delay In Completion"), "fieldname": "delay_in_completion", "fieldtype": "Int", "width": 150},
|
||||||
|
{"label": _("Pending"), "fieldname": "pending", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Overdue"), "fieldname": "overdue", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Cancelled"), "fieldname": "cancelled", "fieldtype": "Int", "width": 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def assignees_status_count_execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
where, params = _aml_location_conditions(filters)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
aml.item_name AS item_name,
|
||||||
|
aml.maintenance_status AS maintenance_status,
|
||||||
|
aml.assign_to_name AS assigned_to,
|
||||||
|
SUM(CASE WHEN aml.due_date = aml.completion_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_on_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date < aml.due_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_within_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date > aml.due_date THEN 1 ELSE 0 END) AS delay_in_completion,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date > CURRENT_DATE() THEN 1 ELSE 0 END) AS pending,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date < CURRENT_DATE() THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY aml.assign_to_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"label": _("Item Name"), "fieldname": "item_name", "fieldtype": "Data", "width": 200},
|
||||||
|
{"label": _("Maintenance Status"), "fieldname": "maintenance_status", "fieldtype": "Data", "width": 150},
|
||||||
|
{"label": _("Assigned To"), "fieldname": "assigned_to", "fieldtype": "Data", "width": 150},
|
||||||
|
{"label": _("Completed On Time"), "fieldname": "completed_on_time", "fieldtype": "Int", "width": 150},
|
||||||
|
{"label": _("Completed Within Time"), "fieldname": "completed_within_time", "fieldtype": "Int", "width": 170},
|
||||||
|
{"label": _("Delay In Completion"), "fieldname": "delay_in_completion", "fieldtype": "Int", "width": 150},
|
||||||
|
{"label": _("Pending"), "fieldname": "pending", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Overdue"), "fieldname": "overdue", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Cancelled"), "fieldname": "cancelled", "fieldtype": "Int", "width": 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def asset_maintenance_frequency_execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
where, params = _aml_location_conditions(filters)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
aml.custom_asset_names AS item,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' THEN 1 ELSE 0 END) AS planned,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Overdue' THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
COUNT(aml.custom_asset_names) AS total_count
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY aml.custom_asset_names
|
||||||
|
"""
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"label": _("Item"), "fieldname": "item", "fieldtype": "Data", "width": 200},
|
||||||
|
{"label": _("Planned"), "fieldname": "planned", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Completed"), "fieldname": "completed", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Cancelled"), "fieldname": "cancelled", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Overdue"), "fieldname": "overdue", "fieldtype": "Int", "width": 100},
|
||||||
|
{"label": _("Total Count"), "fieldname": "total_count", "fieldtype": "Int", "width": 100},
|
||||||
|
]
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return columns, data
|
||||||
|
|
||||||
|
|
||||||
|
def asset_up_and_down_execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append("company = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append("custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
if filters.get("department"):
|
||||||
|
conditions.append("department = %(department)s")
|
||||||
|
params["department"] = filters.get("department")
|
||||||
|
|
||||||
|
if filters.get("custom_class"):
|
||||||
|
conditions.append("custom_class = %(custom_class)s")
|
||||||
|
params["custom_class"] = filters.get("custom_class")
|
||||||
|
|
||||||
|
if filters.get("name"):
|
||||||
|
conditions.append("name = %(name)s")
|
||||||
|
params["name"] = filters.get("name")
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
result = frappe.db.sql(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
name AS name,
|
||||||
|
asset_name,
|
||||||
|
custom_device_status AS status,
|
||||||
|
department,
|
||||||
|
custom_class
|
||||||
|
FROM `tabAsset`
|
||||||
|
WHERE {where}
|
||||||
|
ORDER BY asset_name
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
status_count = {}
|
||||||
|
for row in result:
|
||||||
|
status = row.get("status")
|
||||||
|
if status:
|
||||||
|
normalized_status = status.strip().lower()
|
||||||
|
status_count[normalized_status] = status_count.get(normalized_status, 0) + 1
|
||||||
|
|
||||||
|
chart_labels = list(status_count.keys())
|
||||||
|
chart_values = list(status_count.values())
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"fieldname": "name", "label": "Asset ID", "fieldtype": "Link", "options": "Asset", "width": 200},
|
||||||
|
{"fieldname": "asset_name", "label": "Asset Name", "fieldtype": "Data", "width": 200},
|
||||||
|
{"fieldname": "status", "label": "Status", "fieldtype": "Data", "width": 100},
|
||||||
|
{"fieldname": "department", "label": "Department", "fieldtype": "Link", "options": "Department", "width": 150},
|
||||||
|
{"fieldname": "custom_class", "label": "Class", "fieldtype": "Data", "width": 200},
|
||||||
|
]
|
||||||
|
|
||||||
|
status_colors = [
|
||||||
|
"#2ba63d" if status == "up" else "#FF0000" if status == "down" else "#0000FF"
|
||||||
|
for status in chart_labels
|
||||||
|
]
|
||||||
|
|
||||||
|
chart = {
|
||||||
|
"data": {
|
||||||
|
"labels": chart_labels,
|
||||||
|
"datasets": [{"name": "Number of Assets", "values": chart_values}],
|
||||||
|
},
|
||||||
|
"type": "pie",
|
||||||
|
"colors": status_colors,
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, result, None, chart
|
||||||
|
|
||||||
|
|
||||||
|
def work_order_status_execute(filters=None):
|
||||||
|
filters = filters or {}
|
||||||
|
conditions = ["repair_status IN ('Open', 'Work In Progress', 'Pending Review', 'Completed', 'Closed', 'Cancelled')"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
filter_fields = ["work_order_type", "repair_status", "asset_type", "company", "site_name"]
|
||||||
|
for field in filter_fields:
|
||||||
|
if filters.get(field):
|
||||||
|
conditions.append(f"{field} = %({field})s")
|
||||||
|
params[field] = filters.get(field)
|
||||||
|
|
||||||
|
if filters.get("wo_name"):
|
||||||
|
conditions.append("name = %(wo_name)s")
|
||||||
|
params["wo_name"] = filters.get("wo_name")
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = f"""
|
||||||
|
SELECT
|
||||||
|
name AS wo_name,
|
||||||
|
asset_type,
|
||||||
|
work_order_type,
|
||||||
|
SUM(CASE WHEN repair_status = 'Open' THEN 1 ELSE 0 END) AS open_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Work In Progress' THEN 1 ELSE 0 END) AS work_in_progress_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Pending Review' THEN 1 ELSE 0 END) AS pending_review_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Completed' THEN 1 ELSE 0 END) AS completed_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Closed' THEN 1 ELSE 0 END) AS closed_count
|
||||||
|
FROM `tabWork_Order`
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY work_order_type
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"label": _("Work Order Type"), "fieldname": "work_order_type", "fieldtype": "Data", "width": 200},
|
||||||
|
{"label": _("Open"), "fieldname": "open_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Work In Progress"), "fieldname": "work_in_progress_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Pending Review"), "fieldname": "pending_review_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Completed"), "fieldname": "completed_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Closed"), "fieldname": "closed_count", "fieldtype": "Int", "width": 200},
|
||||||
|
]
|
||||||
|
|
||||||
|
report_summary = []
|
||||||
|
for status in ["Open", "Work In Progress", "Pending Review", "Completed", "Closed"]:
|
||||||
|
status_conditions = list(conditions)
|
||||||
|
status_conditions.append("repair_status = %(status)s")
|
||||||
|
status_params = {**params, "status": status}
|
||||||
|
count = frappe.db.sql(
|
||||||
|
f"SELECT COUNT(*) FROM `tabWork_Order` WHERE {' AND '.join(status_conditions)}",
|
||||||
|
status_params,
|
||||||
|
)[0][0]
|
||||||
|
report_summary.append({"value": count, "label": status})
|
||||||
|
|
||||||
|
total = frappe.db.sql(
|
||||||
|
f"SELECT COUNT(*) FROM `tabWork_Order` WHERE {where}",
|
||||||
|
params,
|
||||||
|
)[0][0]
|
||||||
|
report_summary.append({"value": total, "label": "Total Work Orders"})
|
||||||
|
|
||||||
|
chart = {
|
||||||
|
"data": {
|
||||||
|
"labels": [row["work_order_type"] for row in result],
|
||||||
|
"datasets": [
|
||||||
|
{"name": "Open", "values": [row["open_count"] for row in result]},
|
||||||
|
{"name": "Work In Progress", "values": [row["work_in_progress_count"] for row in result]},
|
||||||
|
{"name": "Pending Review", "values": [row["pending_review_count"] for row in result]},
|
||||||
|
{"name": "Completed", "values": [row["completed_count"] for row in result]},
|
||||||
|
{"name": "Closed", "values": [row["closed_count"] for row in result]},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"type": "bar",
|
||||||
|
"barOptions": {"stacked": 1, "spaceRatio": 0.6},
|
||||||
|
"colors": ["#CCCCB7", "#52B2BF", "#9EC1A4", "#058D7C", "#A3A5CF"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, result, None, chart, report_summary
|
||||||
@ -119,6 +119,9 @@ def get_pm_schedules(filters=None, fields=None, limit=20, offset=0, order_by=Non
|
|||||||
# Parse filters if provided
|
# Parse filters if provided
|
||||||
if filters and isinstance(filters, str):
|
if filters and isinstance(filters, str):
|
||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
|
filters = expand_dashboard_filters(filters or {}, "PM Schedule Generator")
|
||||||
|
|
||||||
# Parse fields if provided
|
# Parse fields if provided
|
||||||
if fields and isinstance(fields, str):
|
if fields and isinstance(fields, str):
|
||||||
|
|||||||
@ -9,6 +9,8 @@ from frappe import _
|
|||||||
DOCTYPE_PERMISSION_MAPPINGS = {
|
DOCTYPE_PERMISSION_MAPPINGS = {
|
||||||
"Asset": {
|
"Asset": {
|
||||||
"Company": "company",
|
"Company": "company",
|
||||||
|
"Hospital": "company",
|
||||||
|
"Mobile Team Site": "custom_site",
|
||||||
"Location": "location",
|
"Location": "location",
|
||||||
"Department": "department",
|
"Department": "department",
|
||||||
"Manufacturer": "custom_manufacturer",
|
"Manufacturer": "custom_manufacturer",
|
||||||
@ -20,6 +22,8 @@ DOCTYPE_PERMISSION_MAPPINGS = {
|
|||||||
},
|
},
|
||||||
"Work_Order": {
|
"Work_Order": {
|
||||||
"Company": "company",
|
"Company": "company",
|
||||||
|
"Hospital": "company",
|
||||||
|
"Mobile Team Site": "site_name",
|
||||||
"Location": "location",
|
"Location": "location",
|
||||||
"Department": "department"
|
"Department": "department"
|
||||||
},
|
},
|
||||||
@ -29,8 +33,13 @@ DOCTYPE_PERMISSION_MAPPINGS = {
|
|||||||
"Supplier": "supplier"
|
"Supplier": "supplier"
|
||||||
},
|
},
|
||||||
"Asset Maintenance Log": {
|
"Asset Maintenance Log": {
|
||||||
"Company": "company",
|
"Company": "custom_hospital_name",
|
||||||
|
"Hospital": "custom_hospital_name",
|
||||||
"Asset": "asset_name"
|
"Asset": "asset_name"
|
||||||
|
},
|
||||||
|
"Item": {
|
||||||
|
"Company": "custom_hospital_name",
|
||||||
|
"Hospital": "custom_hospital_name",
|
||||||
}
|
}
|
||||||
# Add more doctypes as needed - just add them here!
|
# Add more doctypes as needed - just add them here!
|
||||||
}
|
}
|
||||||
@ -49,6 +58,39 @@ def is_system_user(user):
|
|||||||
return "System Manager" in roles
|
return "System Manager" in roles
|
||||||
|
|
||||||
|
|
||||||
|
def merge_query_filters(user_filters=None, permission_filters=None):
|
||||||
|
"""Merge client filters with permission filters (permissions win on conflict)."""
|
||||||
|
merged = dict(user_filters or {})
|
||||||
|
|
||||||
|
for field, value in (permission_filters or {}).items():
|
||||||
|
if field not in merged or not merged[field]:
|
||||||
|
merged[field] = value
|
||||||
|
elif isinstance(value, list) and value and value[0] == "in":
|
||||||
|
permitted_values = value[1] or []
|
||||||
|
user_value = merged[field]
|
||||||
|
|
||||||
|
if isinstance(user_value, str):
|
||||||
|
if user_value not in permitted_values:
|
||||||
|
merged[field] = ["in", []]
|
||||||
|
elif isinstance(user_value, list) and user_value and user_value[0] == "in":
|
||||||
|
user_values = user_value[1] or []
|
||||||
|
intersection = [v for v in user_values if v in permitted_values]
|
||||||
|
merged[field] = ["in", intersection]
|
||||||
|
else:
|
||||||
|
merged[field] = value
|
||||||
|
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def apply_permission_filters(user_filters, target_doctype, user=None):
|
||||||
|
"""Apply user permission filters to a query filter dict."""
|
||||||
|
perm_result = get_permission_filters(target_doctype, user)
|
||||||
|
if perm_result.get("is_admin"):
|
||||||
|
return user_filters or {}
|
||||||
|
|
||||||
|
return merge_query_filters(user_filters, perm_result.get("filters"))
|
||||||
|
|
||||||
|
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
# CORE API FUNCTIONS - These 4 functions handle everything
|
# CORE API FUNCTIONS - These 4 functions handle everything
|
||||||
# ============================================================================
|
# ============================================================================
|
||||||
|
|||||||
142
asset_lite/api/wo_feedback_api.py
Normal file
142
asset_lite/api/wo_feedback_api.py
Normal file
@ -0,0 +1,142 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe import _
|
||||||
|
|
||||||
|
|
||||||
|
RATING_ORDER = [
|
||||||
|
"Excellent / ممتاز",
|
||||||
|
"Good / جيد",
|
||||||
|
"Average / متوسط",
|
||||||
|
"Poor / ضعيف",
|
||||||
|
]
|
||||||
|
|
||||||
|
REPORT_COLUMNS = [
|
||||||
|
{"label": _("Work Order"), "fieldname": "work_order", "fieldtype": "Link", "options": "Work_Order", "width": 150},
|
||||||
|
{"label": _("Requester Name"), "fieldname": "requester_name", "fieldtype": "Data", "width": 180},
|
||||||
|
{"label": _("Rating"), "fieldname": "rating", "fieldtype": "Data", "width": 160},
|
||||||
|
{"label": _("Comments"), "fieldname": "comments", "fieldtype": "Small Text", "width": 220},
|
||||||
|
{"label": _("Created On"), "fieldname": "creation", "fieldtype": "Datetime", "width": 150},
|
||||||
|
{"label": _("Last Modified"), "fieldname": "modified", "fieldtype": "Datetime", "width": 150},
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _parse_filters(filters=None):
|
||||||
|
if isinstance(filters, str):
|
||||||
|
filters = frappe.parse_json(filters or "{}")
|
||||||
|
return filters or {}
|
||||||
|
|
||||||
|
|
||||||
|
def _build_wo_feedback_conditions(filters):
|
||||||
|
conditions = ["IFNULL(wf.rating, '') != ''"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("work_order"):
|
||||||
|
conditions.append("wf.work_order = %(work_order)s")
|
||||||
|
params["work_order"] = filters.get("work_order")
|
||||||
|
|
||||||
|
if filters.get("requester_name"):
|
||||||
|
conditions.append("wf.requester_name LIKE %(requester_name)s")
|
||||||
|
params["requester_name"] = f"%{filters.get('requester_name')}%"
|
||||||
|
|
||||||
|
if filters.get("rating"):
|
||||||
|
conditions.append("wf.rating = %(rating)s")
|
||||||
|
params["rating"] = filters.get("rating")
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append("wo.company = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append("wo.site_name = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
return conditions, params
|
||||||
|
|
||||||
|
|
||||||
|
def _rating_sort_key(rating):
|
||||||
|
try:
|
||||||
|
return RATING_ORDER.index(rating)
|
||||||
|
except ValueError:
|
||||||
|
return len(RATING_ORDER)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist(allow_guest=True)
|
||||||
|
def get_wo_feedback_for_work_order(work_order: str | None = None):
|
||||||
|
"""Guest-safe read for WO Feedback (autoname = work_order field value)."""
|
||||||
|
if not work_order or not str(work_order).strip():
|
||||||
|
frappe.throw(_("Work Order is required"))
|
||||||
|
|
||||||
|
work_order = str(work_order).strip()
|
||||||
|
|
||||||
|
feedback_name = work_order
|
||||||
|
if not frappe.db.exists("WO Feedback", work_order):
|
||||||
|
feedback_name = frappe.db.get_value("WO Feedback", {"work_order": work_order}, "name")
|
||||||
|
if not feedback_name:
|
||||||
|
return None
|
||||||
|
|
||||||
|
return frappe.db.get_value(
|
||||||
|
"WO Feedback",
|
||||||
|
feedback_name,
|
||||||
|
["name", "work_order", "requester_name", "rating", "comments"],
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def run_wo_feedback_report(filters=None):
|
||||||
|
"""Return WO feedback detail rows for dashboard popup (supports hospital/site filters)."""
|
||||||
|
filters = _parse_filters(filters)
|
||||||
|
conditions, params = _build_wo_feedback_conditions(filters)
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
rows = frappe.db.sql(
|
||||||
|
f"""
|
||||||
|
SELECT
|
||||||
|
wf.work_order,
|
||||||
|
wf.requester_name,
|
||||||
|
wf.rating,
|
||||||
|
wf.comments,
|
||||||
|
wf.creation,
|
||||||
|
wf.modified
|
||||||
|
FROM `tabWO Feedback` wf
|
||||||
|
INNER JOIN `tabWork_Order` wo ON wo.name = wf.work_order
|
||||||
|
WHERE {where}
|
||||||
|
ORDER BY wf.modified DESC
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
"columns": REPORT_COLUMNS,
|
||||||
|
"result": rows,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
@frappe.whitelist()
|
||||||
|
def get_wo_feedback_summary(filters=None):
|
||||||
|
"""Return chart-ready rating counts for dashboard card."""
|
||||||
|
filters = _parse_filters(filters)
|
||||||
|
conditions, params = _build_wo_feedback_conditions(filters)
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
rows = frappe.db.sql(
|
||||||
|
f"""
|
||||||
|
SELECT wf.rating, COUNT(*) AS count
|
||||||
|
FROM `tabWO Feedback` wf
|
||||||
|
INNER JOIN `tabWork_Order` wo ON wo.name = wf.work_order
|
||||||
|
WHERE {where}
|
||||||
|
GROUP BY wf.rating
|
||||||
|
""",
|
||||||
|
params,
|
||||||
|
as_dict=True,
|
||||||
|
)
|
||||||
|
|
||||||
|
count_by_rating = {row.rating: int(row.count or 0) for row in rows if row.rating}
|
||||||
|
labels = RATING_ORDER[:]
|
||||||
|
values = [count_by_rating.get(label, 0) for label in labels]
|
||||||
|
|
||||||
|
return {
|
||||||
|
"labels": labels,
|
||||||
|
"datasets": [{"name": "Responses", "values": values}],
|
||||||
|
"type": "Bar",
|
||||||
|
}
|
||||||
@ -17,6 +17,8 @@ WORK_ORDER_FIELDS = [
|
|||||||
'asset_type',
|
'asset_type',
|
||||||
'manufacturer',
|
'manufacturer',
|
||||||
'serial_number',
|
'serial_number',
|
||||||
|
'custom_local_id',
|
||||||
|
'custom_moh_id',
|
||||||
'custom_priority_',
|
'custom_priority_',
|
||||||
'asset',
|
'asset',
|
||||||
'custom_maintenance_manager',
|
'custom_maintenance_manager',
|
||||||
@ -49,6 +51,7 @@ WORK_ORDER_FIELDS = [
|
|||||||
'custom_diffrence',
|
'custom_diffrence',
|
||||||
'feedback_rating',
|
'feedback_rating',
|
||||||
'first_responded_on',
|
'first_responded_on',
|
||||||
|
'completion_date',
|
||||||
'assigned_manager',
|
'assigned_manager',
|
||||||
'penalty',
|
'penalty',
|
||||||
'custom_assigned_supervisor',
|
'custom_assigned_supervisor',
|
||||||
@ -149,8 +152,44 @@ def get_child_table_data(parent_name, parentfield, child_doctype, fields=None):
|
|||||||
return []
|
return []
|
||||||
|
|
||||||
|
|
||||||
|
def get_work_order_role_query(user=None):
|
||||||
|
"""Match frontend mergeWorkOrderFilters / buildWorkOrderRoleFilters."""
|
||||||
|
user = user or frappe.session.user
|
||||||
|
if not user or user == "Guest":
|
||||||
|
return {}, None
|
||||||
|
|
||||||
|
role_profile = frappe.db.get_value("User", user, "role_profile_name")
|
||||||
|
|
||||||
|
if role_profile in ("End User", "PHCC End User"):
|
||||||
|
return {"owner": user}, None
|
||||||
|
|
||||||
|
if role_profile == "Technician":
|
||||||
|
return {}, [
|
||||||
|
["owner", "=", user],
|
||||||
|
["custom_assign_to_contractor", "=", user],
|
||||||
|
["assigned_technician", "=", user],
|
||||||
|
]
|
||||||
|
|
||||||
|
return {}, None
|
||||||
|
|
||||||
|
|
||||||
|
def _count_work_orders(filters=None, or_filters=None):
|
||||||
|
"""Count work orders, including optional OR filter groups."""
|
||||||
|
if or_filters:
|
||||||
|
return len(
|
||||||
|
frappe.get_all(
|
||||||
|
"Work_Order",
|
||||||
|
filters=filters or {},
|
||||||
|
or_filters=or_filters,
|
||||||
|
pluck="name",
|
||||||
|
limit_page_length=0,
|
||||||
|
)
|
||||||
|
)
|
||||||
|
return frappe.db.count("Work_Order", filters or {})
|
||||||
|
|
||||||
|
|
||||||
@frappe.whitelist(allow_guest=True)
|
@frappe.whitelist(allow_guest=True)
|
||||||
def get_work_orders(filters=None, fields=None, limit=20, offset=0, order_by=None, include_child_tables=False):
|
def get_work_orders(filters=None, fields=None, limit=20, offset=0, order_by=None, include_child_tables=False, or_filters=None):
|
||||||
"""
|
"""
|
||||||
Get list of work orders with filters and pagination
|
Get list of work orders with filters and pagination
|
||||||
|
|
||||||
@ -177,6 +216,20 @@ def get_work_orders(filters=None, fields=None, limit=20, offset=0, order_by=None
|
|||||||
# Parse filters if provided
|
# Parse filters if provided
|
||||||
if filters and isinstance(filters, str):
|
if filters and isinstance(filters, str):
|
||||||
filters = json.loads(filters)
|
filters = json.loads(filters)
|
||||||
|
|
||||||
|
if or_filters and isinstance(or_filters, str):
|
||||||
|
or_filters = json.loads(or_filters)
|
||||||
|
|
||||||
|
from asset_lite.api.dashboard_filters import expand_dashboard_filters
|
||||||
|
if isinstance(filters, dict):
|
||||||
|
filters = expand_dashboard_filters(filters or {}, "Work_Order")
|
||||||
|
from asset_lite.api.asset_api import _expand_list_filters
|
||||||
|
filters = _expand_list_filters(filters or {})
|
||||||
|
elif filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
from asset_lite.api.userperm_api import apply_permission_filters
|
||||||
|
filters = apply_permission_filters(filters, "Work_Order")
|
||||||
|
|
||||||
# Parse fields if provided
|
# Parse fields if provided
|
||||||
if fields and isinstance(fields, str):
|
if fields and isinstance(fields, str):
|
||||||
@ -189,12 +242,13 @@ def get_work_orders(filters=None, fields=None, limit=20, offset=0, order_by=None
|
|||||||
include_child_tables = include_child_tables.lower() in ('true', '1', 'yes')
|
include_child_tables = include_child_tables.lower() in ('true', '1', 'yes')
|
||||||
|
|
||||||
# Get total count
|
# Get total count
|
||||||
total_count = frappe.db.count('Work_Order', filters=filters or {})
|
total_count = _count_work_orders(filters or {}, or_filters or None)
|
||||||
|
|
||||||
# Get work orders
|
# Get work orders
|
||||||
work_orders = frappe.get_all(
|
work_orders = frappe.get_all(
|
||||||
'Work_Order',
|
'Work_Order',
|
||||||
filters=filters or {},
|
filters=filters or {},
|
||||||
|
or_filters=or_filters or None,
|
||||||
fields=fields,
|
fields=fields,
|
||||||
limit_page_length=int(limit),
|
limit_page_length=int(limit),
|
||||||
limit_start=int(offset),
|
limit_start=int(offset),
|
||||||
|
|||||||
@ -688,7 +688,7 @@
|
|||||||
"reqd": 1
|
"reqd": 1
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"depends_on": "eval:doc.asset_type == \"Non Biomedical\" || (doc.company && doc.asset_type == \"Biomedical\" && doc.company.startsWith(\"Mobile\"))",
|
"depends_on": "eval:doc.asset_type == \"Non Biomedical\" || (doc.company && doc.asset_type == \"Biomedical\" && (doc.company.startsWith(\"Mobile\") || doc.company.startsWith(\"Dental\")))",
|
||||||
"fetch_from": "asset.custom_site",
|
"fetch_from": "asset.custom_site",
|
||||||
"fetch_if_empty": 1,
|
"fetch_if_empty": 1,
|
||||||
"fieldname": "site_name",
|
"fieldname": "site_name",
|
||||||
|
|||||||
@ -3,8 +3,36 @@
|
|||||||
|
|
||||||
import frappe
|
import frappe
|
||||||
from frappe.model.document import Document
|
from frappe.model.document import Document
|
||||||
|
from frappe.utils import get_datetime, now_datetime
|
||||||
|
|
||||||
|
|
||||||
class Work_Order(Document):
|
class Work_Order(Document):
|
||||||
|
def validate(self):
|
||||||
|
self.apply_status_timestamps()
|
||||||
|
self.calculate_total_hours_spent()
|
||||||
|
|
||||||
|
def apply_status_timestamps(self):
|
||||||
|
"""Auto-set response/completion timestamps when repair status changes."""
|
||||||
|
if self.repair_status == "Work In Progress" and not self.first_responded_on:
|
||||||
|
self.first_responded_on = now_datetime()
|
||||||
|
|
||||||
|
if self.repair_status == "Completed" and not self.completion_date:
|
||||||
|
self.completion_date = now_datetime()
|
||||||
|
|
||||||
|
def calculate_total_hours_spent(self):
|
||||||
|
"""Total hours = completion_date - first_responded_on."""
|
||||||
|
if not self.first_responded_on or not self.completion_date:
|
||||||
|
return
|
||||||
|
|
||||||
|
start = get_datetime(self.first_responded_on)
|
||||||
|
end = get_datetime(self.completion_date)
|
||||||
|
|
||||||
|
if not start or not end or end < start:
|
||||||
|
self.total_hours_spent = 0
|
||||||
|
return
|
||||||
|
|
||||||
|
self.total_hours_spent = round((end - start).total_seconds() / 3600, 2)
|
||||||
|
|
||||||
@frappe.whitelist()
|
@frappe.whitelist()
|
||||||
def check_site_version(self):
|
def check_site_version(self):
|
||||||
# Fetch the site version type from the site configuration
|
# Fetch the site version type from the site configuration
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -451,88 +451,6 @@
|
|||||||
"x_field": null,
|
"x_field": null,
|
||||||
"y_axis": []
|
"y_axis": []
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"aggregate_function_based_on": null,
|
|
||||||
"based_on": null,
|
|
||||||
"chart_name": "Technicians working Hours on Work orders",
|
|
||||||
"chart_type": "Report",
|
|
||||||
"color": null,
|
|
||||||
"custom_options": null,
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Dashboard Chart",
|
|
||||||
"document_type": null,
|
|
||||||
"dynamic_filters_json": "{}",
|
|
||||||
"filters_json": "{}",
|
|
||||||
"from_date": null,
|
|
||||||
"group_by_based_on": null,
|
|
||||||
"group_by_type": "Count",
|
|
||||||
"heatmap_year": null,
|
|
||||||
"is_public": 1,
|
|
||||||
"is_standard": 0,
|
|
||||||
"last_synced_on": null,
|
|
||||||
"modified": "2025-02-18 13:40:45.427898",
|
|
||||||
"module": null,
|
|
||||||
"name": "Technicians working Hours on Work orders",
|
|
||||||
"number_of_groups": 0,
|
|
||||||
"parent_document_type": null,
|
|
||||||
"report_name": "Technicians working Hours",
|
|
||||||
"roles": [
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "Technician"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "Maintenance Manager"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "End user"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "Finance Manager"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "Finance User"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "roles",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"role": "System Manager"
|
|
||||||
}
|
|
||||||
],
|
|
||||||
"source": null,
|
|
||||||
"time_interval": "Yearly",
|
|
||||||
"timeseries": 0,
|
|
||||||
"timespan": "Last Year",
|
|
||||||
"to_date": null,
|
|
||||||
"type": "Bar",
|
|
||||||
"use_report_chart": 0,
|
|
||||||
"value_based_on": null,
|
|
||||||
"x_field": "technician_name",
|
|
||||||
"y_axis": [
|
|
||||||
{
|
|
||||||
"color": "#85cc29",
|
|
||||||
"parent": "Technicians working Hours on Work orders",
|
|
||||||
"parentfield": "y_axis",
|
|
||||||
"parenttype": "Dashboard Chart",
|
|
||||||
"y_field": "total_hours"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"aggregate_function_based_on": null,
|
"aggregate_function_based_on": null,
|
||||||
"based_on": "creation",
|
"based_on": "creation",
|
||||||
|
|||||||
@ -1262,7 +1262,7 @@
|
|||||||
{
|
{
|
||||||
"attach_print": 0,
|
"attach_print": 0,
|
||||||
"channel": "Email",
|
"channel": "Email",
|
||||||
"condition": "doc.workflow_state == \"Completed\"",
|
"condition": "doc.workflow_state == \"Completed\" and (doc.company ==\"Iman General Hospital\" or doc.company == \"Mobile Team - Iman\")",
|
||||||
"date_changed": null,
|
"date_changed": null,
|
||||||
"days_in_advance": 0,
|
"days_in_advance": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@ -1274,7 +1274,7 @@
|
|||||||
"message": "Add your message here",
|
"message": "Add your message here",
|
||||||
"message_type": "Markdown",
|
"message_type": "Markdown",
|
||||||
"method": null,
|
"method": null,
|
||||||
"modified": "2026-01-12 15:52:50.093133",
|
"modified": "2026-08-12 12:20:47.199206",
|
||||||
"module": "Asset Lite",
|
"module": "Asset Lite",
|
||||||
"name": "Work order Approved",
|
"name": "Work order Approved",
|
||||||
"print_format": null,
|
"print_format": null,
|
||||||
@ -1323,7 +1323,7 @@
|
|||||||
{
|
{
|
||||||
"attach_print": 0,
|
"attach_print": 0,
|
||||||
"channel": "Email",
|
"channel": "Email",
|
||||||
"condition": "doc.workflow_state == \"Pending Approval\"",
|
"condition": "doc.workflow_state == \"Pending Approval\" and (doc.company ==\"Iman General Hospital\" or doc.company == \"Mobile Team - Iman\")",
|
||||||
"date_changed": null,
|
"date_changed": null,
|
||||||
"days_in_advance": 0,
|
"days_in_advance": 0,
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@ -1335,7 +1335,7 @@
|
|||||||
"message": "Add your message here",
|
"message": "Add your message here",
|
||||||
"message_type": "Markdown",
|
"message_type": "Markdown",
|
||||||
"method": null,
|
"method": null,
|
||||||
"modified": "2026-01-12 15:51:25.758528",
|
"modified": "2026-08-12 12:22:47.792157",
|
||||||
"module": "Asset Lite",
|
"module": "Asset Lite",
|
||||||
"name": "Work order Pending Approval",
|
"name": "Work order Pending Approval",
|
||||||
"print_format": null,
|
"print_format": null,
|
||||||
@ -1611,7 +1611,7 @@
|
|||||||
"message": "Add your message here",
|
"message": "Add your message here",
|
||||||
"message_type": "Markdown",
|
"message_type": "Markdown",
|
||||||
"method": null,
|
"method": null,
|
||||||
"modified": "2026-01-12 15:55:50.454012",
|
"modified": "2026-08-12 12:37:14.552101",
|
||||||
"module": "Asset Lite",
|
"module": "Asset Lite",
|
||||||
"name": "WO Close",
|
"name": "WO Close",
|
||||||
"print_format": null,
|
"print_format": null,
|
||||||
@ -1646,5 +1646,87 @@
|
|||||||
"slack_webhook_url": null,
|
"slack_webhook_url": null,
|
||||||
"subject": "Work order {{doc.name }} is Closed. ",
|
"subject": "Work order {{doc.name }} is Closed. ",
|
||||||
"value_changed": "workflow_state"
|
"value_changed": "workflow_state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attach_print": 0,
|
||||||
|
"channel": "Email",
|
||||||
|
"condition": "doc.workflow_state == \"Sent To Project Manager\"",
|
||||||
|
"date_changed": null,
|
||||||
|
"days_in_advance": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Notification",
|
||||||
|
"document_type": "Work_Order",
|
||||||
|
"enabled": 1,
|
||||||
|
"event": "Value Change",
|
||||||
|
"is_standard": 0,
|
||||||
|
"message": "Add your message here",
|
||||||
|
"message_type": "Markdown",
|
||||||
|
"method": null,
|
||||||
|
"modified": "2026-08-13 17:00:32.314840",
|
||||||
|
"module": "Asset Lite",
|
||||||
|
"name": "WO Sent To Project Manager",
|
||||||
|
"print_format": null,
|
||||||
|
"property_value": null,
|
||||||
|
"recipients": [
|
||||||
|
{
|
||||||
|
"bcc": null,
|
||||||
|
"cc": null,
|
||||||
|
"condition": null,
|
||||||
|
"parent": "WO Sent To Project Manager",
|
||||||
|
"parentfield": "recipients",
|
||||||
|
"parenttype": "Notification",
|
||||||
|
"receiver_by_document_field": null,
|
||||||
|
"receiver_by_role": "Projects Manager"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"send_system_notification": 1,
|
||||||
|
"send_to_all_assignees": 0,
|
||||||
|
"sender": "Asset Notifications",
|
||||||
|
"sender_email": "prebiyap@gmail.com",
|
||||||
|
"set_property_after_alert": null,
|
||||||
|
"slack_webhook_url": null,
|
||||||
|
"subject": "Work order {{doc.name }} is Sent To Project Manager. Please Take the necessary action. ",
|
||||||
|
"value_changed": "workflow_state"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"attach_print": 0,
|
||||||
|
"channel": "Email",
|
||||||
|
"condition": "doc.workflow_state == \"Closed\"",
|
||||||
|
"date_changed": null,
|
||||||
|
"days_in_advance": 0,
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Notification",
|
||||||
|
"document_type": "Work_Order",
|
||||||
|
"enabled": 1,
|
||||||
|
"event": "Value Change",
|
||||||
|
"is_standard": 0,
|
||||||
|
"message": "Add your message here",
|
||||||
|
"message_type": "Markdown",
|
||||||
|
"method": null,
|
||||||
|
"modified": "2026-08-18 15:42:44.801598",
|
||||||
|
"module": "Asset Lite",
|
||||||
|
"name": "Feedback Form Created",
|
||||||
|
"print_format": null,
|
||||||
|
"property_value": null,
|
||||||
|
"recipients": [
|
||||||
|
{
|
||||||
|
"bcc": null,
|
||||||
|
"cc": null,
|
||||||
|
"condition": null,
|
||||||
|
"parent": "Feedback Form Created",
|
||||||
|
"parentfield": "recipients",
|
||||||
|
"parenttype": "Notification",
|
||||||
|
"receiver_by_document_field": "owner",
|
||||||
|
"receiver_by_role": null
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"send_system_notification": 1,
|
||||||
|
"send_to_all_assignees": 0,
|
||||||
|
"sender": "Asset Notifications",
|
||||||
|
"sender_email": "prebiyap@gmail.com",
|
||||||
|
"set_property_after_alert": null,
|
||||||
|
"slack_webhook_url": null,
|
||||||
|
"subject": "Work order {{doc.name }} is Closed, Please give the Feedback.",
|
||||||
|
"value_changed": "workflow_state"
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because one or more lines are too long
@ -1,4 +1,20 @@
|
|||||||
[
|
[
|
||||||
|
{
|
||||||
|
"default_value": null,
|
||||||
|
"doc_type": "Asset",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Property Setter",
|
||||||
|
"doctype_or_field": "DocType",
|
||||||
|
"field_name": null,
|
||||||
|
"is_system_generated": 0,
|
||||||
|
"modified": "2025-05-22 14:09:07.502818",
|
||||||
|
"module": "Asset Lite",
|
||||||
|
"name": "Asset-main-image_field",
|
||||||
|
"property": "image_field",
|
||||||
|
"property_type": "Data",
|
||||||
|
"row_name": null,
|
||||||
|
"value": "custom_attach_image"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Asset",
|
"doc_type": "Asset",
|
||||||
@ -1903,22 +1919,6 @@
|
|||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "1"
|
"value": "1"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default_value": null,
|
|
||||||
"doc_type": "Asset",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"field_name": "item_code",
|
|
||||||
"is_system_generated": 0,
|
|
||||||
"modified": "2025-04-22 14:45:40.948870",
|
|
||||||
"module": "Asset Lite",
|
|
||||||
"name": "Asset-item_code-read_only",
|
|
||||||
"property": "read_only",
|
|
||||||
"property_type": "Check",
|
|
||||||
"row_name": null,
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Asset",
|
"doc_type": "Asset",
|
||||||
@ -2063,22 +2063,6 @@
|
|||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "Hospital Name"
|
"value": "Hospital Name"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default_value": null,
|
|
||||||
"doc_type": "Asset",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"field_name": "item_code",
|
|
||||||
"is_system_generated": 0,
|
|
||||||
"modified": "2025-04-22 14:45:37.632724",
|
|
||||||
"module": "Asset Lite",
|
|
||||||
"name": "Asset-item_code-hidden",
|
|
||||||
"property": "hidden",
|
|
||||||
"property_type": "Check",
|
|
||||||
"row_name": null,
|
|
||||||
"value": "1"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Asset",
|
"doc_type": "Asset",
|
||||||
@ -3727,22 +3711,6 @@
|
|||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "Light\nDark\nAutomatic\nModern_ui_theme"
|
"value": "Light\nDark\nAutomatic\nModern_ui_theme"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default_value": null,
|
|
||||||
"doc_type": "Asset",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocType",
|
|
||||||
"field_name": null,
|
|
||||||
"is_system_generated": 0,
|
|
||||||
"modified": "2025-05-22 14:09:07.502818",
|
|
||||||
"module": "Asset Lite",
|
|
||||||
"name": "Asset-main-image_field",
|
|
||||||
"property": "image_field",
|
|
||||||
"property_type": "Data",
|
|
||||||
"row_name": null,
|
|
||||||
"value": "custom_attach_image"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Work_Order",
|
"doc_type": "Work_Order",
|
||||||
@ -4159,22 +4127,6 @@
|
|||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "0"
|
"value": "0"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default_value": null,
|
|
||||||
"doc_type": "Work_Order",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"field_name": "company",
|
|
||||||
"is_system_generated": 0,
|
|
||||||
"modified": "2025-08-08 17:25:29.999119",
|
|
||||||
"module": "Asset Lite",
|
|
||||||
"name": "Work_Order-company-depends_on",
|
|
||||||
"property": "depends_on",
|
|
||||||
"property_type": "Data",
|
|
||||||
"row_name": null,
|
|
||||||
"value": "eval:doc.asset_type == \"Biomedical\""
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Report",
|
"doc_type": "Report",
|
||||||
@ -4303,22 +4255,6 @@
|
|||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "PPM Assets"
|
"value": "PPM Assets"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"default_value": null,
|
|
||||||
"doc_type": "Work_Order",
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Property Setter",
|
|
||||||
"doctype_or_field": "DocField",
|
|
||||||
"field_name": "site_name",
|
|
||||||
"is_system_generated": 0,
|
|
||||||
"modified": "2025-08-08 17:33:53.907942",
|
|
||||||
"module": "Asset Lite",
|
|
||||||
"name": "Work_Order-site_name-depends_on",
|
|
||||||
"property": "depends_on",
|
|
||||||
"property_type": "Data",
|
|
||||||
"row_name": null,
|
|
||||||
"value": "eval:doc.asset_type == \"Non Biomedical\" || (doc.company && doc.asset_type == \"Biomedical\" && doc.company.startsWith(\"Mobile\"))"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"default_value": null,
|
"default_value": null,
|
||||||
"doc_type": "Work_Order",
|
"doc_type": "Work_Order",
|
||||||
@ -4590,5 +4526,37 @@
|
|||||||
"property_type": "Check",
|
"property_type": "Check",
|
||||||
"row_name": null,
|
"row_name": null,
|
||||||
"value": "1"
|
"value": "1"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default_value": null,
|
||||||
|
"doc_type": "Work_Order",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Property Setter",
|
||||||
|
"doctype_or_field": "DocField",
|
||||||
|
"field_name": "company",
|
||||||
|
"is_system_generated": 0,
|
||||||
|
"modified": "2026-08-24 12:23:00.164566",
|
||||||
|
"module": "Asset Lite",
|
||||||
|
"name": "Work_Order-company-depends_on",
|
||||||
|
"property": "depends_on",
|
||||||
|
"property_type": "Data",
|
||||||
|
"row_name": null,
|
||||||
|
"value": ""
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"default_value": null,
|
||||||
|
"doc_type": "Work_Order",
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Property Setter",
|
||||||
|
"doctype_or_field": "DocField",
|
||||||
|
"field_name": "site_name",
|
||||||
|
"is_system_generated": 0,
|
||||||
|
"modified": "2026-08-24 12:23:00.016499",
|
||||||
|
"module": "Asset Lite",
|
||||||
|
"name": "Work_Order-site_name-depends_on",
|
||||||
|
"property": "depends_on",
|
||||||
|
"property_type": "Data",
|
||||||
|
"row_name": null,
|
||||||
|
"value": ""
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
@ -313,7 +313,7 @@
|
|||||||
"doctype_event": "Before Insert",
|
"doctype_event": "Before Insert",
|
||||||
"enable_rate_limit": 0,
|
"enable_rate_limit": 0,
|
||||||
"event_frequency": "All",
|
"event_frequency": "All",
|
||||||
"modified": "2026-01-06 17:11:37.557942",
|
"modified": "2026-08-06 21:09:20.199705",
|
||||||
"module": "Asset Lite",
|
"module": "Asset Lite",
|
||||||
"name": "Create Item on Asset",
|
"name": "Create Item on Asset",
|
||||||
"rate_limit_count": 5,
|
"rate_limit_count": 5,
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@ -27,6 +27,20 @@
|
|||||||
"name": "Approve",
|
"name": "Approve",
|
||||||
"workflow_action_name": "Approve"
|
"workflow_action_name": "Approve"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"modified": "2026-08-11 11:41:34.048015",
|
||||||
|
"name": "Work Complete",
|
||||||
|
"workflow_action_name": "Work Complete"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"modified": "2026-08-13 13:28:13.970367",
|
||||||
|
"name": "Work Done",
|
||||||
|
"workflow_action_name": "Work Done"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow Action Master",
|
"doctype": "Workflow Action Master",
|
||||||
@ -41,13 +55,6 @@
|
|||||||
"name": "Re-Open",
|
"name": "Re-Open",
|
||||||
"workflow_action_name": "Re-Open"
|
"workflow_action_name": "Re-Open"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow Action Master",
|
|
||||||
"modified": "2024-09-13 11:10:00.762984",
|
|
||||||
"name": "Send For Approval",
|
|
||||||
"workflow_action_name": "Send For Approval"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow Action Master",
|
"doctype": "Workflow Action Master",
|
||||||
@ -62,6 +69,20 @@
|
|||||||
"name": "Reject",
|
"name": "Reject",
|
||||||
"workflow_action_name": "Reject"
|
"workflow_action_name": "Reject"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"modified": "2025-08-11 12:02:06.266995",
|
||||||
|
"name": "Close",
|
||||||
|
"workflow_action_name": "Close"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"modified": "2024-09-13 11:10:00.762984",
|
||||||
|
"name": "Send For Approval",
|
||||||
|
"workflow_action_name": "Send For Approval"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow Action Master",
|
"doctype": "Workflow Action Master",
|
||||||
@ -69,6 +90,13 @@
|
|||||||
"name": "Cancel",
|
"name": "Cancel",
|
||||||
"workflow_action_name": "Cancel"
|
"workflow_action_name": "Cancel"
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"modified": "2026-08-11 11:47:24.201964",
|
||||||
|
"name": "Approve & Close",
|
||||||
|
"workflow_action_name": "Approve & Close"
|
||||||
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow Action Master",
|
"doctype": "Workflow Action Master",
|
||||||
@ -76,13 +104,6 @@
|
|||||||
"name": "Send For Repair",
|
"name": "Send For Repair",
|
||||||
"workflow_action_name": "Send For Repair"
|
"workflow_action_name": "Send For Repair"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow Action Master",
|
|
||||||
"modified": "2025-08-11 12:02:06.266995",
|
|
||||||
"name": "Close",
|
|
||||||
"workflow_action_name": "Close"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow Action Master",
|
"doctype": "Workflow Action Master",
|
||||||
|
|||||||
@ -35,15 +35,6 @@
|
|||||||
"style": "",
|
"style": "",
|
||||||
"workflow_state_name": "Sent to Procurement User"
|
"workflow_state_name": "Sent to Procurement User"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow State",
|
|
||||||
"icon": "",
|
|
||||||
"modified": "2025-06-17 15:42:44.566036",
|
|
||||||
"name": "Sent To Site Manager",
|
|
||||||
"style": "",
|
|
||||||
"workflow_state_name": "Sent To Site Manager"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
@ -53,24 +44,6 @@
|
|||||||
"style": "",
|
"style": "",
|
||||||
"workflow_state_name": "Sent to General WOA"
|
"workflow_state_name": "Sent to General WOA"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow State",
|
|
||||||
"icon": "",
|
|
||||||
"modified": "2024-09-17 17:37:02.717022",
|
|
||||||
"name": "Cancelled",
|
|
||||||
"style": "",
|
|
||||||
"workflow_state_name": "Cancelled"
|
|
||||||
},
|
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow State",
|
|
||||||
"icon": "",
|
|
||||||
"modified": "2025-05-22 18:15:15.104122",
|
|
||||||
"name": "Pending Purchase",
|
|
||||||
"style": "",
|
|
||||||
"workflow_state_name": "Pending Purchase"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
@ -80,15 +53,6 @@
|
|||||||
"style": "Success",
|
"style": "Success",
|
||||||
"workflow_state_name": "Approved"
|
"workflow_state_name": "Approved"
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"docstatus": 0,
|
|
||||||
"doctype": "Workflow State",
|
|
||||||
"icon": "remove",
|
|
||||||
"modified": "2024-09-10 18:16:06.113226",
|
|
||||||
"name": "Rejected",
|
|
||||||
"style": "Danger",
|
|
||||||
"workflow_state_name": "Rejected"
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
@ -102,19 +66,64 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"modified": "2026-01-12 15:31:11.081747",
|
"modified": "2025-05-22 18:15:14.987359",
|
||||||
"name": "Sent to Team Leader",
|
"name": "Sent To Maintenance manger",
|
||||||
"style": "",
|
"style": "Info",
|
||||||
"workflow_state_name": "Sent to Team Leader"
|
"workflow_state_name": "Sent To Maintenance manger"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"modified": "2025-08-11 12:01:35.474110",
|
"modified": "2025-06-17 15:42:44.566036",
|
||||||
"name": "Closed",
|
"name": "Sent To Site Manager",
|
||||||
"style": "",
|
"style": "",
|
||||||
"workflow_state_name": "Closed"
|
"workflow_state_name": "Sent To Site Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"icon": "",
|
||||||
|
"modified": "2026-08-11 11:37:18.893891",
|
||||||
|
"name": "Sent To Project Manager",
|
||||||
|
"style": "",
|
||||||
|
"workflow_state_name": "Sent To Project Manager"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"icon": "",
|
||||||
|
"modified": "2024-09-17 17:37:02.717022",
|
||||||
|
"name": "Cancelled",
|
||||||
|
"style": "",
|
||||||
|
"workflow_state_name": "Cancelled"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"icon": "remove",
|
||||||
|
"modified": "2024-09-10 18:16:06.113226",
|
||||||
|
"name": "Rejected",
|
||||||
|
"style": "Danger",
|
||||||
|
"workflow_state_name": "Rejected"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"icon": "",
|
||||||
|
"modified": "2025-05-22 18:15:15.104122",
|
||||||
|
"name": "Pending Purchase",
|
||||||
|
"style": "",
|
||||||
|
"workflow_state_name": "Pending Purchase"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"docstatus": 0,
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"icon": "",
|
||||||
|
"modified": "2026-01-12 15:31:11.081747",
|
||||||
|
"name": "Sent to Team Leader",
|
||||||
|
"style": "",
|
||||||
|
"workflow_state_name": "Sent to Team Leader"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
@ -129,10 +138,10 @@
|
|||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
"doctype": "Workflow State",
|
"doctype": "Workflow State",
|
||||||
"icon": "",
|
"icon": "",
|
||||||
"modified": "2025-05-22 18:15:14.987359",
|
"modified": "2025-08-11 12:01:35.474110",
|
||||||
"name": "Sent To Maintenance manger",
|
"name": "Closed",
|
||||||
"style": "Info",
|
"style": "",
|
||||||
"workflow_state_name": "Sent To Maintenance manger"
|
"workflow_state_name": "Closed"
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"docstatus": 0,
|
"docstatus": 0,
|
||||||
|
|||||||
@ -142,9 +142,13 @@ override_doctype_class = {
|
|||||||
# Hook on document methods and events
|
# Hook on document methods and events
|
||||||
|
|
||||||
doc_events = {
|
doc_events = {
|
||||||
"Asset":{
|
"Asset": {
|
||||||
"before_save": "asset_lite.public.py.asset.generate_asset_qr"
|
"before_save": "asset_lite.public.py.asset.generate_asset_qr",
|
||||||
}
|
},
|
||||||
|
"Work_Order": {
|
||||||
|
"after_insert": "asset_lite.public.py.work_order_qr.generate_work_order_feedback_qr",
|
||||||
|
"on_update": "asset_lite.notifications.work_order_notifications.notify_on_workflow_change",
|
||||||
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
# Scheduled Tasks
|
# Scheduled Tasks
|
||||||
@ -261,12 +265,12 @@ fixtures = [
|
|||||||
"module", "=", "Asset Lite"
|
"module", "=", "Asset Lite"
|
||||||
]
|
]
|
||||||
]},
|
]},
|
||||||
# {
|
{
|
||||||
# "doctype": "Role",
|
"doctype": "Role",
|
||||||
# "filters": [
|
"filters": [
|
||||||
# ["creation", ">", "2024-09-12"]
|
["creation", ">", "2024-09-12"]
|
||||||
# ]
|
]
|
||||||
# },
|
},
|
||||||
{"doctype": "Workspace", "filters": [
|
{"doctype": "Workspace", "filters": [
|
||||||
[
|
[
|
||||||
"module", "=", "Asset Lite"
|
"module", "=", "Asset Lite"
|
||||||
@ -296,16 +300,17 @@ fixtures = [
|
|||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
|
||||||
{
|
# Site-specific fixtures — skip on install to avoid company/account conflicts
|
||||||
"doctype": "Company",
|
# {
|
||||||
"filters": [
|
# "doctype": "Company",
|
||||||
["domain", "=", "Healthcare"]
|
# "filters": [
|
||||||
]
|
# ["domain", "=", "Healthcare"]
|
||||||
},
|
# ]
|
||||||
{
|
# },
|
||||||
"doctype": "User Permission",
|
# {
|
||||||
"filters": [
|
# "doctype": "User Permission",
|
||||||
["allow", "=", "Hospital"]
|
# "filters": [
|
||||||
]
|
# ["allow", "=", "Hospital"]
|
||||||
},
|
# ]
|
||||||
|
# },
|
||||||
]
|
]
|
||||||
|
|||||||
@ -52,6 +52,7 @@ def get_active_map_data(hospital=None):
|
|||||||
"wo_progress": count("Work_Order", {"company": name, "repair_status": "Work In Progress"}),
|
"wo_progress": count("Work_Order", {"company": name, "repair_status": "Work In Progress"}),
|
||||||
"wo_review": count("Work_Order", {"company": name, "repair_status": "Pending Review"}),
|
"wo_review": count("Work_Order", {"company": name, "repair_status": "Pending Review"}),
|
||||||
"wo_completed": count("Work_Order", {"company": name, "repair_status": "Completed"}),
|
"wo_completed": count("Work_Order", {"company": name, "repair_status": "Completed"}),
|
||||||
|
"wo_closed": count("Work_Order", {"company": name, "repair_status": "Closed"}),
|
||||||
|
|
||||||
|
|
||||||
"planned_maintenance": count("Asset Maintenance Log", {
|
"planned_maintenance": count("Asset Maintenance Log", {
|
||||||
|
|||||||
0
asset_lite/notifications/__init__.py
Normal file
0
asset_lite/notifications/__init__.py
Normal file
119
asset_lite/notifications/work_order_notifications.py
Normal file
119
asset_lite/notifications/work_order_notifications.py
Normal file
@ -0,0 +1,119 @@
|
|||||||
|
import frappe
|
||||||
|
from frappe.utils import get_url
|
||||||
|
|
||||||
|
EXCLUDED_HOSPITAL = "Iman General Hospital"
|
||||||
|
|
||||||
|
NOTIFY_STATES = frozenset({
|
||||||
|
"Repair InProgress",
|
||||||
|
"Sent To Project Manager",
|
||||||
|
"Sent To Maintenance manger",
|
||||||
|
"Closed",
|
||||||
|
"Completed",
|
||||||
|
})
|
||||||
|
|
||||||
|
MAINTENANCE_MANAGER_PROFILE = "Maintenace Manager"
|
||||||
|
MOH_SUPERVISOR_PROFILE = "MOH Supervisor"
|
||||||
|
PROJECT_MANAGER_PROFILE = "Project Manager"
|
||||||
|
|
||||||
|
# workflow_state -> role profiles to notify (hospital matched via custom_site_name)
|
||||||
|
STATE_ROLE_PROFILES = {
|
||||||
|
"Sent To Maintenance manger": (MAINTENANCE_MANAGER_PROFILE,),
|
||||||
|
"Sent To Project Manager": (
|
||||||
|
MAINTENANCE_MANAGER_PROFILE,
|
||||||
|
MOH_SUPERVISOR_PROFILE,
|
||||||
|
PROJECT_MANAGER_PROFILE,
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
DEFAULT_ROLE_PROFILES = (MAINTENANCE_MANAGER_PROFILE, MOH_SUPERVISOR_PROFILE)
|
||||||
|
|
||||||
|
|
||||||
|
def notify_on_workflow_change(doc, method=None):
|
||||||
|
"""Send email + Notification Log only when workflow_state changes to a target state."""
|
||||||
|
previous_state = _get_previous_workflow_state(doc)
|
||||||
|
if previous_state == doc.workflow_state:
|
||||||
|
return
|
||||||
|
|
||||||
|
_send_workflow_notifications(doc, previous_state)
|
||||||
|
|
||||||
|
|
||||||
|
def _send_workflow_notifications(doc, previous_state=None):
|
||||||
|
if doc.workflow_state not in NOTIFY_STATES:
|
||||||
|
return
|
||||||
|
|
||||||
|
if not doc.company or doc.company == EXCLUDED_HOSPITAL:
|
||||||
|
return
|
||||||
|
|
||||||
|
recipients = _get_hospital_role_users(doc.company, doc.workflow_state)
|
||||||
|
if not recipients:
|
||||||
|
frappe.log_error(
|
||||||
|
f"No notification recipients for Work Order {doc.name} "
|
||||||
|
f"(company={doc.company}, workflow_state={doc.workflow_state})",
|
||||||
|
"Work Order Workflow Notification",
|
||||||
|
)
|
||||||
|
return
|
||||||
|
|
||||||
|
subject = f"Work Order {doc.name} - {doc.workflow_state}"
|
||||||
|
wo_link = _get_work_order_asm_app_url(doc.name)
|
||||||
|
message = _build_message(doc, previous_state, wo_link)
|
||||||
|
|
||||||
|
for user in recipients:
|
||||||
|
_create_notification_log(doc, user, subject, message)
|
||||||
|
if user.email:
|
||||||
|
frappe.sendmail(
|
||||||
|
recipients=[user.email],
|
||||||
|
subject=subject,
|
||||||
|
message=message,
|
||||||
|
reference_doctype="Work_Order",
|
||||||
|
reference_name=doc.name,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _get_previous_workflow_state(doc):
|
||||||
|
previous = doc.get_doc_before_save()
|
||||||
|
if not previous:
|
||||||
|
return None
|
||||||
|
return previous.get("workflow_state")
|
||||||
|
|
||||||
|
|
||||||
|
def _get_work_order_asm_app_url(work_order_name):
|
||||||
|
base_url = get_url().rstrip("/")
|
||||||
|
app_base = (frappe.conf.get("asm_app_base_path") or "/asm_app").rstrip("/")
|
||||||
|
return f"{base_url}{app_base}/work-orders/{work_order_name}"
|
||||||
|
|
||||||
|
|
||||||
|
def _get_hospital_role_users(company, workflow_state=None):
|
||||||
|
role_profiles = STATE_ROLE_PROFILES.get(workflow_state, DEFAULT_ROLE_PROFILES)
|
||||||
|
return frappe.get_all(
|
||||||
|
"User",
|
||||||
|
filters={
|
||||||
|
"enabled": 1,
|
||||||
|
"custom_site_name": company,
|
||||||
|
"role_profile_name": ["in", list(role_profiles)],
|
||||||
|
},
|
||||||
|
fields=["name", "email", "full_name"],
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
def _build_message(doc, previous_state, wo_link):
|
||||||
|
previous_label = previous_state or "N/A"
|
||||||
|
return f"""
|
||||||
|
<p>Work Order <b>{doc.name}</b> workflow has been updated.</p>
|
||||||
|
<p><b>Previous state:</b> {previous_label}</p>
|
||||||
|
<p><b>Current state:</b> {doc.workflow_state}</p>
|
||||||
|
<p><b>Hospital:</b> {doc.company}</p>
|
||||||
|
<p><a href="{wo_link}">Open Work Order</a></p>
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
def _create_notification_log(doc, user, subject, message):
|
||||||
|
frappe.get_doc({
|
||||||
|
"doctype": "Notification Log",
|
||||||
|
"type": "Alert",
|
||||||
|
"document_type": "Work_Order",
|
||||||
|
"document_name": doc.name,
|
||||||
|
"subject": subject,
|
||||||
|
"email_content": message,
|
||||||
|
"from_user": frappe.session.user,
|
||||||
|
"for_user": user.name,
|
||||||
|
}).insert(ignore_permissions=True)
|
||||||
@ -3,4 +3,6 @@
|
|||||||
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
|
# Read docs to understand patches: https://frappeframework.com/docs/v14/user/en/database-migrations
|
||||||
|
|
||||||
[post_model_sync]
|
[post_model_sync]
|
||||||
# Patches added in this section will be executed after doctypes are migrated
|
# Patches added in this section will be executed after doctypes are migrated
|
||||||
|
asset_lite.patches.sync_dashboard_reports.execute
|
||||||
|
asset_lite.patches.sync_work_order_workflow.execute
|
||||||
303
asset_lite/patches/dashboard_report_inline_scripts.py
Normal file
303
asset_lite/patches/dashboard_report_inline_scripts.py
Normal file
@ -0,0 +1,303 @@
|
|||||||
|
"""Full inline Script Report bodies stored in Report.report_script (no imports, no .format())."""
|
||||||
|
|
||||||
|
ASSET_UP_AND_DOWN = r'''def execute(filters=None):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
result = []
|
||||||
|
status_count = {}
|
||||||
|
|
||||||
|
company = filters.get("company")
|
||||||
|
site_name = filters.get("site_name")
|
||||||
|
department = filters.get("department")
|
||||||
|
name = filters.get("name")
|
||||||
|
custom_class = filters.get("custom_class")
|
||||||
|
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if company:
|
||||||
|
conditions.append("company = %(company)s")
|
||||||
|
params["company"] = company
|
||||||
|
if site_name:
|
||||||
|
conditions.append("custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = site_name
|
||||||
|
if department:
|
||||||
|
conditions.append("department = %(department)s")
|
||||||
|
params["department"] = department
|
||||||
|
if custom_class:
|
||||||
|
conditions.append("custom_class = %(custom_class)s")
|
||||||
|
params["custom_class"] = custom_class
|
||||||
|
if name:
|
||||||
|
conditions.append("name = %(name)s")
|
||||||
|
params["name"] = name
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
name AS name,
|
||||||
|
asset_name,
|
||||||
|
custom_device_status AS status,
|
||||||
|
department,
|
||||||
|
custom_class
|
||||||
|
FROM
|
||||||
|
`tabAsset`
|
||||||
|
WHERE
|
||||||
|
""" + where + """
|
||||||
|
ORDER BY
|
||||||
|
asset_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
|
||||||
|
for row in result:
|
||||||
|
status = row.get('status')
|
||||||
|
if status:
|
||||||
|
normalized_status = status.strip().lower()
|
||||||
|
status_count[normalized_status] = status_count.get(normalized_status, 0) + 1
|
||||||
|
|
||||||
|
chart_labels = list(status_count.keys())
|
||||||
|
chart_values = list(status_count.values())
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"fieldname": "name", "label": "Asset ID", "fieldtype": "Link", "options": "Asset", "width": 200},
|
||||||
|
{"fieldname": "asset_name", "label": "Asset Name", "fieldtype": "Data", "width": 200},
|
||||||
|
{"fieldname": "status", "label": "Status", "fieldtype": "Data", "width": 100},
|
||||||
|
{"fieldname": "department", "label": "Department", "fieldtype": "Link", "options": "Department", "width": 150},
|
||||||
|
{"fieldname": "custom_class", "label": "Class", "fieldtype": "Data", "width": 200},
|
||||||
|
]
|
||||||
|
|
||||||
|
status_colors = ["#2ba63d" if status == 'up' else "#FF0000" if status == 'down' else "#0000FF" for status in chart_labels]
|
||||||
|
|
||||||
|
chart = {
|
||||||
|
"data": {
|
||||||
|
"labels": chart_labels,
|
||||||
|
"datasets": [{"name": "Number of Assets", "values": chart_values}]
|
||||||
|
},
|
||||||
|
"type": "pie",
|
||||||
|
"colors": status_colors
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, result, None, chart
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
'''
|
||||||
|
|
||||||
|
WORK_ORDER_STATUS = r'''def execute(filters=None):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
columns = [
|
||||||
|
{"label": _("Work Order Type"), "fieldname": "work_order_type", "fieldtype": "Data", "width": 200},
|
||||||
|
{"label": _("Open"), "fieldname": "open_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Work In Progress"), "fieldname": "work_in_progress_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Pending Review"), "fieldname": "pending_review_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Completed"), "fieldname": "completed_count", "fieldtype": "Int", "width": 200},
|
||||||
|
{"label": _("Closed"), "fieldname": "closed_count", "fieldtype": "Int", "width": 200}
|
||||||
|
]
|
||||||
|
|
||||||
|
conditions = ["repair_status IN ('Open', 'Work In Progress', 'Pending Review', 'Completed', 'Closed', 'Cancelled')"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get('work_order_type'):
|
||||||
|
conditions.append("work_order_type = %(work_order_type)s")
|
||||||
|
params['work_order_type'] = filters.get('work_order_type')
|
||||||
|
if filters.get('repair_status'):
|
||||||
|
conditions.append("repair_status = %(repair_status)s")
|
||||||
|
params['repair_status'] = filters.get('repair_status')
|
||||||
|
if filters.get('asset_type'):
|
||||||
|
conditions.append("asset_type = %(asset_type)s")
|
||||||
|
params['asset_type'] = filters.get('asset_type')
|
||||||
|
if filters.get('company'):
|
||||||
|
conditions.append("company = %(company)s")
|
||||||
|
params['company'] = filters.get('company')
|
||||||
|
if filters.get('site_name'):
|
||||||
|
conditions.append("site_name = %(site_name)s")
|
||||||
|
params['site_name'] = filters.get('site_name')
|
||||||
|
if filters.get('wo_name'):
|
||||||
|
conditions.append("name = %(wo_name)s")
|
||||||
|
params['wo_name'] = filters.get('wo_name')
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
name as wo_name,
|
||||||
|
asset_type,
|
||||||
|
work_order_type,
|
||||||
|
SUM(CASE WHEN repair_status = 'Open' THEN 1 ELSE 0 END) AS open_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Work In Progress' THEN 1 ELSE 0 END) AS work_in_progress_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Pending Review' THEN 1 ELSE 0 END) AS pending_review_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Completed' THEN 1 ELSE 0 END) AS completed_count,
|
||||||
|
SUM(CASE WHEN repair_status = 'Closed' THEN 1 ELSE 0 END) AS closed_count
|
||||||
|
FROM
|
||||||
|
`tabWork_Order`
|
||||||
|
WHERE
|
||||||
|
""" + where + """
|
||||||
|
GROUP BY
|
||||||
|
work_order_type
|
||||||
|
"""
|
||||||
|
|
||||||
|
result = frappe.db.sql(query, params, as_dict=1)
|
||||||
|
|
||||||
|
report_summary = []
|
||||||
|
for status in ['Open', 'Work In Progress', 'Pending Review', 'Completed', 'Closed']:
|
||||||
|
status_conditions = list(conditions)
|
||||||
|
status_conditions.append("repair_status = %(status)s")
|
||||||
|
status_params = dict(params)
|
||||||
|
status_params['status'] = status
|
||||||
|
status_where = " AND ".join(status_conditions)
|
||||||
|
count = frappe.db.sql(
|
||||||
|
"SELECT COUNT(*) FROM `tabWork_Order` WHERE " + status_where,
|
||||||
|
status_params
|
||||||
|
)[0][0]
|
||||||
|
report_summary.append({"value": count, "label": status})
|
||||||
|
|
||||||
|
total = frappe.db.sql(
|
||||||
|
"SELECT COUNT(*) FROM `tabWork_Order` WHERE " + where,
|
||||||
|
params
|
||||||
|
)[0][0]
|
||||||
|
report_summary.append({"value": total, "label": "Total Work Orders"})
|
||||||
|
|
||||||
|
chart = {
|
||||||
|
"data": {
|
||||||
|
"labels": [row['work_order_type'] for row in result],
|
||||||
|
"datasets": [
|
||||||
|
{"name": "Open", "values": [row['open_count'] for row in result]},
|
||||||
|
{"name": "Work In Progress", "values": [row['work_in_progress_count'] for row in result]},
|
||||||
|
{"name": "Pending Review", "values": [row['pending_review_count'] for row in result]},
|
||||||
|
{"name": "Completed", "values": [row['completed_count'] for row in result]},
|
||||||
|
{"name": "Closed", "values": [row['closed_count'] for row in result]},
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"type": "bar",
|
||||||
|
"barOptions": {"stacked": 1, "spaceRatio": 0.6},
|
||||||
|
"colors": ["#CCCCB7", "#52B2BF", "#9EC1A4", "#058D7C", "#A3A5CF"],
|
||||||
|
}
|
||||||
|
|
||||||
|
return columns, result, None, chart, report_summary
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
'''
|
||||||
|
|
||||||
|
ASSET_WISE_COUNT = r'''def execute(filters=None):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append("aml.custom_hospital_name = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append("a.custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
aml.item_name AS item_name,
|
||||||
|
aml.maintenance_status AS maintenance_status,
|
||||||
|
SUM(CASE WHEN aml.due_date = aml.completion_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_on_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date < aml.due_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_within_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date > aml.due_date THEN 1 ELSE 0 END) AS delay_in_completion,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date > CURRENT_DATE() THEN 1 ELSE 0 END) AS pending,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date < CURRENT_DATE() THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE """ + where + """
|
||||||
|
GROUP BY aml.item_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
'''
|
||||||
|
|
||||||
|
ASSIGNEES_STATUS_COUNT = r'''def execute(filters=None):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append("aml.custom_hospital_name = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append("a.custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
aml.item_name AS item_name,
|
||||||
|
aml.maintenance_status AS maintenance_status,
|
||||||
|
aml.assign_to_name AS assigned_to,
|
||||||
|
SUM(CASE WHEN aml.due_date = aml.completion_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_on_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date < aml.due_date AND aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed_within_time,
|
||||||
|
SUM(CASE WHEN aml.completion_date > aml.due_date THEN 1 ELSE 0 END) AS delay_in_completion,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date > CURRENT_DATE() THEN 1 ELSE 0 END) AS pending,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' AND aml.completion_date IS NULL AND aml.due_date < CURRENT_DATE() THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE """ + where + """
|
||||||
|
GROUP BY aml.assign_to_name
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
'''
|
||||||
|
|
||||||
|
ASSET_MAINTENANCE_FREQUENCY = r'''def execute(filters=None):
|
||||||
|
if filters is None:
|
||||||
|
filters = {}
|
||||||
|
|
||||||
|
conditions = ["1=1"]
|
||||||
|
params = {}
|
||||||
|
|
||||||
|
if filters.get("company"):
|
||||||
|
conditions.append("aml.custom_hospital_name = %(company)s")
|
||||||
|
params["company"] = filters.get("company")
|
||||||
|
if filters.get("site_name"):
|
||||||
|
conditions.append("a.custom_site = %(site_name)s")
|
||||||
|
params["site_name"] = filters.get("site_name")
|
||||||
|
|
||||||
|
where = " AND ".join(conditions)
|
||||||
|
|
||||||
|
query = """
|
||||||
|
SELECT
|
||||||
|
aml.custom_asset_names AS item,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Planned' THEN 1 ELSE 0 END) AS planned,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Completed' THEN 1 ELSE 0 END) AS completed,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Cancelled' THEN 1 ELSE 0 END) AS cancelled,
|
||||||
|
SUM(CASE WHEN aml.maintenance_status = 'Overdue' THEN 1 ELSE 0 END) AS overdue,
|
||||||
|
COUNT(aml.custom_asset_names) AS total_count
|
||||||
|
FROM `tabAsset Maintenance Log` aml
|
||||||
|
LEFT JOIN `tabAsset` a ON aml.asset_name = a.name
|
||||||
|
WHERE """ + where + """
|
||||||
|
GROUP BY aml.custom_asset_names
|
||||||
|
"""
|
||||||
|
|
||||||
|
data = frappe.db.sql(query, params, as_dict=True)
|
||||||
|
return data
|
||||||
|
|
||||||
|
data = execute(filters)
|
||||||
|
'''
|
||||||
|
|
||||||
|
REPORT_SCRIPTS = {
|
||||||
|
"Asset Up and Down": ASSET_UP_AND_DOWN,
|
||||||
|
"Work Order Status": WORK_ORDER_STATUS,
|
||||||
|
"Asset wise Count": ASSET_WISE_COUNT,
|
||||||
|
"Asset Maintenance Assignees Status Count": ASSIGNEES_STATUS_COUNT,
|
||||||
|
"Asset Maintenance Frequency": ASSET_MAINTENANCE_FREQUENCY,
|
||||||
|
}
|
||||||
173
asset_lite/patches/sync_dashboard_reports.py
Normal file
173
asset_lite/patches/sync_dashboard_reports.py
Normal file
@ -0,0 +1,173 @@
|
|||||||
|
"""Sync dashboard report scripts and Hospital/Site filters into the live database."""
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
from asset_lite.patches.dashboard_report_inline_scripts import REPORT_SCRIPTS
|
||||||
|
|
||||||
|
LOCATION_FILTERS = [
|
||||||
|
{
|
||||||
|
"fieldname": "company",
|
||||||
|
"label": "Hospital",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Company",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "site_name",
|
||||||
|
"label": "Site Name",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Mobile Team Site",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
DASHBOARD_REPORTS = {
|
||||||
|
"Asset Up and Down": {
|
||||||
|
"javascript": (
|
||||||
|
'frappe.query_reports["Asset Up and Down"] = {\n'
|
||||||
|
" filters: [\n"
|
||||||
|
" {\n"
|
||||||
|
' fieldname: "company",\n'
|
||||||
|
' label: __("Hospital"),\n'
|
||||||
|
' fieldtype: "Link",\n'
|
||||||
|
' options: "Company"\n'
|
||||||
|
" },\n"
|
||||||
|
" {\n"
|
||||||
|
' fieldname: "site_name",\n'
|
||||||
|
' label: __("Site Name"),\n'
|
||||||
|
' fieldtype: "Link",\n'
|
||||||
|
' options: "Mobile Team Site"\n'
|
||||||
|
" },\n"
|
||||||
|
" {\n"
|
||||||
|
' fieldname: "department",\n'
|
||||||
|
' label: __("Department"),\n'
|
||||||
|
' fieldtype: "Link",\n'
|
||||||
|
' options: "Department"\n'
|
||||||
|
" },\n"
|
||||||
|
" {\n"
|
||||||
|
' fieldname: "custom_class",\n'
|
||||||
|
' label: __("Class"),\n'
|
||||||
|
' fieldtype: "Select",\n'
|
||||||
|
' options: "\\nClass A\\nClass B\\nClass C"\n'
|
||||||
|
" },\n"
|
||||||
|
" {\n"
|
||||||
|
' fieldname: "name",\n'
|
||||||
|
' label: __("Asset"),\n'
|
||||||
|
' fieldtype: "Link",\n'
|
||||||
|
' options: "Asset"\n'
|
||||||
|
" }\n"
|
||||||
|
" ]\n"
|
||||||
|
"};\n"
|
||||||
|
),
|
||||||
|
"extra_filters": [
|
||||||
|
{
|
||||||
|
"fieldname": "department",
|
||||||
|
"label": "Department",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Department",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "custom_class",
|
||||||
|
"label": "Class",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": "\nClass A\nClass B\nClass C",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "name",
|
||||||
|
"label": "Asset",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Asset",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Work Order Status": {
|
||||||
|
"extra_filters": [
|
||||||
|
{
|
||||||
|
"fieldname": "repair_status",
|
||||||
|
"label": "Status",
|
||||||
|
"fieldtype": "Select",
|
||||||
|
"options": "\nOpen\nWork In Progress\nPending Review\nCompleted\nClosed",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "work_order_type",
|
||||||
|
"label": "Work Order Type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Issue Type",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "wo_name",
|
||||||
|
"label": "Work Order",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Work_Order",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"fieldname": "asset_type",
|
||||||
|
"label": "Asset Type",
|
||||||
|
"fieldtype": "Link",
|
||||||
|
"options": "Asset Type",
|
||||||
|
"mandatory": 0,
|
||||||
|
"wildcard_filter": 0,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
"Asset wise Count": {"extra_filters": []},
|
||||||
|
"Asset Maintenance Assignees Status Count": {"extra_filters": []},
|
||||||
|
"Asset Maintenance Frequency": {"extra_filters": []},
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def _merge_filters(report_name, extra_filters):
|
||||||
|
merged = []
|
||||||
|
seen = set()
|
||||||
|
for row in LOCATION_FILTERS + extra_filters:
|
||||||
|
fieldname = row["fieldname"]
|
||||||
|
if fieldname in seen:
|
||||||
|
continue
|
||||||
|
seen.add(fieldname)
|
||||||
|
merged.append({**row, "parent": report_name, "parenttype": "Report", "parentfield": "filters"})
|
||||||
|
return merged
|
||||||
|
|
||||||
|
|
||||||
|
def sync_dashboard_reports():
|
||||||
|
updated = []
|
||||||
|
for report_name, config in DASHBOARD_REPORTS.items():
|
||||||
|
if not frappe.db.exists("Report", report_name):
|
||||||
|
continue
|
||||||
|
|
||||||
|
inline_script = REPORT_SCRIPTS.get(report_name)
|
||||||
|
if not inline_script:
|
||||||
|
continue
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Report", report_name)
|
||||||
|
doc.report_script = inline_script.strip()
|
||||||
|
doc.report_type = "Script Report"
|
||||||
|
doc.query = None
|
||||||
|
|
||||||
|
if config.get("javascript") is not None:
|
||||||
|
doc.javascript = config.get("javascript")
|
||||||
|
|
||||||
|
doc.set("filters", _merge_filters(report_name, config.get("extra_filters", [])))
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.save()
|
||||||
|
updated.append(report_name)
|
||||||
|
|
||||||
|
frappe.db.commit()
|
||||||
|
return updated
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
return sync_dashboard_reports()
|
||||||
90
asset_lite/patches/sync_work_order_workflow.py
Normal file
90
asset_lite/patches/sync_work_order_workflow.py
Normal file
@ -0,0 +1,90 @@
|
|||||||
|
"""Sync Work Order workflow states/transitions from fixtures into the live database."""
|
||||||
|
|
||||||
|
import json
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
|
||||||
|
|
||||||
|
def _ensure_workflow_masters():
|
||||||
|
for action_name in ("Work Done", "Approve & Close"):
|
||||||
|
if not frappe.db.exists("Workflow Action Master", action_name):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Workflow Action Master",
|
||||||
|
"workflow_action_name": action_name,
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
for state_name in ("Sent To Project Manager", "Sent To Maintenance manger"):
|
||||||
|
if not frappe.db.exists("Workflow State", state_name):
|
||||||
|
frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "Workflow State",
|
||||||
|
"workflow_state_name": state_name,
|
||||||
|
"style": "Info",
|
||||||
|
}
|
||||||
|
).insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
|
||||||
|
def _load_work_order_fixture():
|
||||||
|
fixture_path = Path(frappe.get_app_path("asset_lite")) / "fixtures" / "workflow.json"
|
||||||
|
workflows = json.loads(fixture_path.read_text())
|
||||||
|
for workflow in workflows:
|
||||||
|
if workflow.get("name") == "Work Order":
|
||||||
|
return workflow
|
||||||
|
raise frappe.ValidationError("Work Order workflow not found in fixtures/workflow.json")
|
||||||
|
|
||||||
|
|
||||||
|
def sync_work_order_workflow():
|
||||||
|
_ensure_workflow_masters()
|
||||||
|
workflow_data = _load_work_order_fixture()
|
||||||
|
|
||||||
|
if not frappe.db.exists("Workflow", "Work Order"):
|
||||||
|
return "Work Order workflow not found"
|
||||||
|
|
||||||
|
doc = frappe.get_doc("Workflow", "Work Order")
|
||||||
|
doc.is_active = workflow_data.get("is_active", 1)
|
||||||
|
doc.override_status = workflow_data.get("override_status", 0)
|
||||||
|
doc.send_email_alert = workflow_data.get("send_email_alert", 0)
|
||||||
|
doc.workflow_state_field = workflow_data.get("workflow_state_field", "workflow_state")
|
||||||
|
|
||||||
|
doc.states = []
|
||||||
|
for row in workflow_data.get("states", []):
|
||||||
|
doc.append(
|
||||||
|
"states",
|
||||||
|
{
|
||||||
|
"state": row.get("state"),
|
||||||
|
"doc_status": row.get("doc_status"),
|
||||||
|
"update_field": row.get("update_field"),
|
||||||
|
"update_value": row.get("update_value"),
|
||||||
|
"is_optional_state": row.get("is_optional_state", 0),
|
||||||
|
"avoid_status_override": row.get("avoid_status_override", 0),
|
||||||
|
"allow_edit": row.get("allow_edit"),
|
||||||
|
"message": row.get("message"),
|
||||||
|
"next_action_email_template": row.get("next_action_email_template"),
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.transitions = []
|
||||||
|
for row in workflow_data.get("transitions", []):
|
||||||
|
doc.append(
|
||||||
|
"transitions",
|
||||||
|
{
|
||||||
|
"state": row.get("state"),
|
||||||
|
"action": row.get("action"),
|
||||||
|
"next_state": row.get("next_state"),
|
||||||
|
"allowed": row.get("allowed"),
|
||||||
|
"allow_self_approval": row.get("allow_self_approval", 1),
|
||||||
|
"condition": row.get("condition") or "",
|
||||||
|
},
|
||||||
|
)
|
||||||
|
|
||||||
|
doc.flags.ignore_permissions = True
|
||||||
|
doc.save()
|
||||||
|
frappe.db.commit()
|
||||||
|
return "Work Order workflow synced"
|
||||||
|
|
||||||
|
|
||||||
|
def execute():
|
||||||
|
return sync_work_order_workflow()
|
||||||
64
asset_lite/public/py/work_order_qr.py
Normal file
64
asset_lite/public/py/work_order_qr.py
Normal file
@ -0,0 +1,64 @@
|
|||||||
|
import base64
|
||||||
|
import io
|
||||||
|
|
||||||
|
import frappe
|
||||||
|
import pyqrcode
|
||||||
|
|
||||||
|
|
||||||
|
def generate_work_order_feedback_qr(doc, method=None):
|
||||||
|
"""Attach a small QR code for the public WO feedback page to custom_wo_qr."""
|
||||||
|
if not doc.name:
|
||||||
|
return
|
||||||
|
|
||||||
|
meta = frappe.get_meta("Work_Order")
|
||||||
|
if not meta.has_field("custom_wo_qr"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if doc.get("custom_wo_qr"):
|
||||||
|
return
|
||||||
|
|
||||||
|
if frappe.db.exists(
|
||||||
|
"File",
|
||||||
|
{
|
||||||
|
"attached_to_doctype": "Work_Order",
|
||||||
|
"attached_to_name": doc.name,
|
||||||
|
"attached_to_field": "custom_wo_qr",
|
||||||
|
},
|
||||||
|
):
|
||||||
|
return
|
||||||
|
|
||||||
|
try:
|
||||||
|
site_url = frappe.utils.get_url().rstrip("/")
|
||||||
|
feedback_url = f"{site_url}/asm_app/wo-feedback?work_order={doc.name}"
|
||||||
|
|
||||||
|
qr_obj = pyqrcode.create(feedback_url, error="H")
|
||||||
|
buffer = io.BytesIO()
|
||||||
|
qr_obj.png(buffer, scale=6)
|
||||||
|
encoded_content = base64.b64encode(buffer.getvalue()).decode("utf-8")
|
||||||
|
|
||||||
|
file_doc = frappe.get_doc(
|
||||||
|
{
|
||||||
|
"doctype": "File",
|
||||||
|
"file_name": f"{doc.name}-wo-feedback-qr.png",
|
||||||
|
"attached_to_doctype": "Work_Order",
|
||||||
|
"attached_to_name": doc.name,
|
||||||
|
"attached_to_field": "custom_wo_qr",
|
||||||
|
"content": encoded_content,
|
||||||
|
"decode": True,
|
||||||
|
"is_private": 0,
|
||||||
|
}
|
||||||
|
)
|
||||||
|
file_doc.insert(ignore_permissions=True)
|
||||||
|
|
||||||
|
frappe.db.set_value(
|
||||||
|
"Work_Order",
|
||||||
|
doc.name,
|
||||||
|
"custom_wo_qr",
|
||||||
|
file_doc.file_url,
|
||||||
|
update_modified=False,
|
||||||
|
)
|
||||||
|
except Exception:
|
||||||
|
frappe.log_error(
|
||||||
|
f"Error generating WO feedback QR for {doc.name}",
|
||||||
|
"Work Order QR Generation Error",
|
||||||
|
)
|
||||||
Loading…
x
Reference in New Issue
Block a user