Initial commit of Asset Lite app

This commit is contained in:
Duradundi 2026-08-24 13:30:41 +05:30
commit 9b104a3cf4
297 changed files with 86709 additions and 0 deletions

7
.gitignore vendored Normal file
View File

@ -0,0 +1,7 @@
.DS_Store
*.pyc
*.egg-info
*.swp
tags
node_modules
__pycache__

7
README.md Normal file
View File

@ -0,0 +1,7 @@
## Asset Lite
Asset Management System
#### License
mit

1
asset_lite/__init__.py Normal file
View File

@ -0,0 +1 @@
__version__ = "0.0.1"

View File

@ -0,0 +1 @@
from . import asset_api

30
asset_lite/api/api.py Normal file
View File

@ -0,0 +1,30 @@
import frappe
def set_default_homepage():
"""
Set the default workspace based on the user's role.
"""
# Get the current user
current_user = frappe.session.user
# Skip for system users
if current_user in ("Administrator", "Guest"):
return
# Define role-based workspaces
role_based_workspaces = {
"Maintenance Manager": "asset-management",
#"Maintenance User": "asset-management",
#"Technician": "asset-management"
}
# Get the user's roles
user_roles = frappe.get_roles(current_user)
# Determine the default workspace
for role, workspace in role_based_workspaces.items():
if role in user_roles:
# Set the session home page
frappe.local.response["home_page"] = f"/app/{workspace}"
return

1150
asset_lite/api/asset_api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,715 @@
import frappe
from frappe import _
@frappe.whitelist(allow_guest = True)
def get_asset_maintenance_logs(filters=None, fields=None, limit=20, offset=0, order_by=None, include_child_tables=False):
"""
Get list of asset maintenance logs with filters and pagination
Args:
filters: JSON string of filters (e.g., '{"maintenance_status": "Planned"}')
fields: JSON string of fields to return (e.g., '["asset_name", "due_date"]')
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
order_by: Sort order (e.g., "creation desc")
include_child_tables: Whether to include child table data (default: False)
Returns:
{
"asset_maintenance_logs": [...],
"total_count": int,
"limit": int,
"offset": int,
"has_more": bool
}
"""
try:
import json
# Parse filters if provided
if filters and isinstance(filters, str):
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
if fields and isinstance(fields, str):
fields = json.loads(fields)
else:
# Default fields to return
fields = [
'name',
'asset_maintenance',
'naming_series',
'asset_name',
'custom_asset_type',
'item_code',
'item_name',
'custom_asset_names',
'custom_hospital_name',
'task',
'task_name',
'maintenance_type',
'periodicity',
'has_certificate',
'custom_early_completion',
'maintenance_status',
'custom_pm_overdue_reason',
'custom_accepted_by_moh',
'assign_to_name',
'due_date',
'completion_date',
'custom_early_completion_reason',
'custom_accepted_by_moh_',
'custom_template',
'workflow_state',
'creation',
'modified',
'owner',
'modified_by',
'docstatus',
'idx'
]
# Get total count
total_count = frappe.db.count('Asset Maintenance Log', filters=filters or {})
# Get asset maintenance logs
asset_maintenance_logs = frappe.get_all(
'Asset Maintenance Log',
filters=filters or {},
fields=fields,
limit_page_length=int(limit),
limit_start=int(offset),
order_by=order_by or 'creation desc'
)
# Include child tables if requested
if include_child_tables and include_child_tables != 'false':
for log in asset_maintenance_logs:
log['custom_table'] = frappe.get_all(
'PPM Table',
filters={'parent': log['name']},
fields=['name', 'idx', 'maintenance_name', 'working', 'defect_found', 'not_working'],
order_by='idx asc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenance_logs': asset_maintenance_logs,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Asset Maintenance Logs API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenance_logs': [],
'total_count': 0
}
@frappe.whitelist(allow_guest = True)
def get_asset_maintenance_log_details(log_name, include_child_tables=True):
"""
Get detailed information about a specific asset maintenance log
Args:
log_name: Name/ID of the asset maintenance log
include_child_tables: Whether to include child table data (default: True)
Returns:
Asset Maintenance Log document with all fields including child tables
"""
try:
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
# Check if user has permission to read this log
if not frappe.has_permission('Asset Maintenance Log', 'read', log_name):
frappe.throw(_('Not permitted to access this asset maintenance log'))
# Get asset maintenance log details
log = frappe.get_doc('Asset Maintenance Log', log_name)
frappe.response['message'] = log.as_dict()
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Asset Maintenance Log Details API Error')
frappe.response['message'] = {
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def create_asset_maintenance_log(log_data):
"""
Create a new asset maintenance log
Args:
log_data: JSON string containing asset maintenance log fields
Returns:
Created asset maintenance log document
"""
try:
import json
# Parse log data
if isinstance(log_data, str):
log_data = json.loads(log_data)
# Check if user has permission to create asset maintenance log
if not frappe.has_permission('Asset Maintenance Log', 'create'):
frappe.throw(_('Not permitted to create asset maintenance log'))
# Extract child table data
custom_table_data = log_data.pop('custom_table', [])
# Create new asset maintenance log
log = frappe.get_doc({
'doctype': 'Asset Maintenance Log',
**log_data
})
# Add child table rows
if custom_table_data:
for row_data in custom_table_data:
log.append('custom_table', {
'maintenance_name': row_data.get('maintenance_name', ''),
'working': row_data.get('working', 0),
'defect_found': row_data.get('defect_found', 0),
'not_working': row_data.get('not_working', 0)
})
log.insert()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance_log': log.as_dict(),
'message': _('Asset Maintenance Log created successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Create Asset Maintenance Log API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def update_asset_maintenance_log(log_name, log_data):
"""
Update an existing asset maintenance log
Args:
log_name: Name/ID of the asset maintenance log
log_data: JSON string containing fields to update
Returns:
Updated asset maintenance log document
"""
try:
import json
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
# Parse log data
if isinstance(log_data, str):
log_data = json.loads(log_data)
# Check if user has permission to update this log
if not frappe.has_permission('Asset Maintenance Log', 'write', log_name):
frappe.throw(_('Not permitted to update this asset maintenance log'))
# Get asset maintenance log
log = frappe.get_doc('Asset Maintenance Log', log_name)
# Extract child table data before processing other fields
custom_table_data = log_data.pop('custom_table', None)
# List of child table fields to skip in regular update
child_table_fields = ['custom_table', 'table']
# Update regular fields (not child tables)
for key, value in log_data.items():
if key not in child_table_fields and hasattr(log, key):
setattr(log, key, value)
# Handle child table update if provided
if custom_table_data is not None:
# Clear existing child table rows
log.custom_table = []
# Add new child table rows
for row_data in custom_table_data:
log.append('custom_table', {
'maintenance_name': row_data.get('maintenance_name', ''),
'working': row_data.get('working', 0),
'defect_found': row_data.get('defect_found', 0),
'not_working': row_data.get('not_working', 0)
})
log.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance_log': log.as_dict(),
'message': _('Asset Maintenance Log updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Asset Maintenance Log API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def delete_asset_maintenance_log(log_name):
"""
Delete an asset maintenance log
Args:
log_name: Name/ID of the asset maintenance log
Returns:
Success message
"""
try:
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
# Check if user has permission to delete this log
if not frappe.has_permission('Asset Maintenance Log', 'delete', log_name):
frappe.throw(_('Not permitted to delete this asset maintenance log'))
# Delete asset maintenance log
frappe.delete_doc('Asset Maintenance Log', log_name)
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'message': _('Asset Maintenance Log deleted successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Delete Asset Maintenance Log API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def update_maintenance_status(log_name, maintenance_status=None, workflow_state=None):
"""
Update asset maintenance log status
Args:
log_name: Name/ID of the asset maintenance log
maintenance_status: New maintenance status (e.g., 'Planned', 'Completed', 'Overdue')
workflow_state: New workflow state
Returns:
Updated asset maintenance log document
"""
try:
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
# Check if user has permission to update this log
if not frappe.has_permission('Asset Maintenance Log', 'write', log_name):
frappe.throw(_('Not permitted to update this asset maintenance log'))
# Get asset maintenance log
log = frappe.get_doc('Asset Maintenance Log', log_name)
# Update status fields
if maintenance_status:
log.maintenance_status = maintenance_status
if workflow_state:
log.workflow_state = workflow_state
log.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance_log': log.as_dict(),
'message': _('Asset Maintenance Log status updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Maintenance Status API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def get_maintenance_logs_by_asset(asset_name, filters=None, limit=20, offset=0, include_child_tables=False):
"""
Get all maintenance logs for a specific asset
Args:
asset_name: Name/ID of the asset
filters: Additional JSON string of filters
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
include_child_tables: Whether to include child table data (default: False)
Returns:
List of maintenance logs for the asset
"""
try:
import json
if not asset_name:
frappe.throw(_('Asset name is required'))
# Parse additional filters if provided
additional_filters = {}
if filters and isinstance(filters, str):
additional_filters = json.loads(filters)
# Combine filters
combined_filters = {'asset_name': asset_name, **additional_filters}
# Get total count
total_count = frappe.db.count('Asset Maintenance Log', filters=combined_filters)
# Get maintenance logs
logs = frappe.get_all(
'Asset Maintenance Log',
filters=combined_filters,
fields=['*'],
limit_page_length=int(limit),
limit_start=int(offset),
order_by='due_date desc'
)
# Include child tables if requested
if include_child_tables and include_child_tables != 'false':
for log in logs:
log['custom_table'] = frappe.get_all(
'PPM Table',
filters={'parent': log['name']},
fields=['name', 'idx', 'maintenance_name', 'working', 'defect_found', 'not_working'],
order_by='idx asc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenance_logs': logs,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Maintenance Logs By Asset API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenance_logs': [],
'total_count': 0
}
@frappe.whitelist(allow_guest = True)
def get_overdue_maintenance_logs(filters=None, limit=20, offset=0, include_child_tables=False):
"""
Get all overdue maintenance logs
Args:
filters: Additional JSON string of filters
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
include_child_tables: Whether to include child table data (default: False)
Returns:
List of overdue maintenance logs
"""
try:
import json
from frappe.utils import today
# Parse additional filters if provided
additional_filters = {}
if filters and isinstance(filters, str):
additional_filters = json.loads(filters)
# Combine filters - get logs with due_date less than today and status not completed
combined_filters = {
'due_date': ['<', today()],
'maintenance_status': ['!=', 'Completed'],
**additional_filters
}
# Get total count
total_count = frappe.db.count('Asset Maintenance Log', filters=combined_filters)
# Get overdue logs
logs = frappe.get_all(
'Asset Maintenance Log',
filters=combined_filters,
fields=['*'],
limit_page_length=int(limit),
limit_start=int(offset),
order_by='due_date asc'
)
# Include child tables if requested
if include_child_tables and include_child_tables != 'false':
for log in logs:
log['custom_table'] = frappe.get_all(
'PPM Table',
filters={'parent': log['name']},
fields=['name', 'idx', 'maintenance_name', 'working', 'defect_found', 'not_working'],
order_by='idx asc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenance_logs': logs,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Overdue Maintenance Logs API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenance_logs': [],
'total_count': 0
}
@frappe.whitelist(allow_guest = True)
def add_ppm_table_row(log_name, row_data):
"""
Add a PPM table row to a maintenance log
Args:
log_name: Name/ID of the asset maintenance log
row_data: JSON string containing row fields
Returns:
Updated custom_table array
"""
try:
import json
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
# Parse row data
if isinstance(row_data, str):
row_data = json.loads(row_data)
# Check permission
if not frappe.has_permission('Asset Maintenance Log', 'write', log_name):
frappe.throw(_('Not permitted to update this asset maintenance log'))
# Get log and add row
log = frappe.get_doc('Asset Maintenance Log', log_name)
log.append('custom_table', {
'maintenance_name': row_data.get('maintenance_name', ''),
'working': row_data.get('working', 0),
'defect_found': row_data.get('defect_found', 0),
'not_working': row_data.get('not_working', 0)
})
log.save()
frappe.db.commit()
# Return updated child table
custom_table = []
for row in log.custom_table:
custom_table.append({
'name': row.name,
'idx': row.idx,
'maintenance_name': row.maintenance_name,
'working': row.working,
'defect_found': row.defect_found,
'not_working': row.not_working
})
frappe.response['message'] = {
'success': True,
'custom_table': custom_table,
'message': _('PPM table row added successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Add PPM Table Row API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def remove_ppm_table_row(log_name, row_name):
"""
Remove a PPM table row from a maintenance log
Args:
log_name: Name/ID of the asset maintenance log
row_name: Name/ID of the row to remove
Returns:
Updated custom_table array
"""
try:
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
if not row_name:
frappe.throw(_('Row name is required'))
# Check permission
if not frappe.has_permission('Asset Maintenance Log', 'write', log_name):
frappe.throw(_('Not permitted to update this asset maintenance log'))
# Get log and remove row
log = frappe.get_doc('Asset Maintenance Log', log_name)
# Find and remove the row
row_to_remove = None
for row in log.custom_table:
if row.name == row_name:
row_to_remove = row
break
if row_to_remove:
log.remove(row_to_remove)
log.save()
frappe.db.commit()
# Return updated child table
custom_table = []
for row in log.custom_table:
custom_table.append({
'name': row.name,
'idx': row.idx,
'maintenance_name': row.maintenance_name,
'working': row.working,
'defect_found': row.defect_found,
'not_working': row.not_working
})
frappe.response['message'] = {
'success': True,
'custom_table': custom_table,
'message': _('PPM table row removed successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Remove PPM Table Row API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def update_ppm_table_row(log_name, row_name, row_data):
"""
Update a PPM table row in a maintenance log
Args:
log_name: Name/ID of the asset maintenance log
row_name: Name/ID of the row to update
row_data: JSON string containing fields to update
Returns:
Updated custom_table array
"""
try:
import json
if not log_name:
frappe.throw(_('Asset Maintenance Log name is required'))
if not row_name:
frappe.throw(_('Row name is required'))
# Parse row data
if isinstance(row_data, str):
row_data = json.loads(row_data)
# Check permission
if not frappe.has_permission('Asset Maintenance Log', 'write', log_name):
frappe.throw(_('Not permitted to update this asset maintenance log'))
# Get log and update row
log = frappe.get_doc('Asset Maintenance Log', log_name)
# Find and update the row
for row in log.custom_table:
if row.name == row_name:
if 'maintenance_name' in row_data:
row.maintenance_name = row_data['maintenance_name']
if 'working' in row_data:
row.working = row_data['working']
if 'defect_found' in row_data:
row.defect_found = row_data['defect_found']
if 'not_working' in row_data:
row.not_working = row_data['not_working']
break
log.save()
frappe.db.commit()
# Return updated child table
custom_table = []
for row in log.custom_table:
custom_table.append({
'name': row.name,
'idx': row.idx,
'maintenance_name': row.maintenance_name,
'working': row.working,
'defect_found': row.defect_found,
'not_working': row.not_working
})
frappe.response['message'] = {
'success': True,
'custom_table': custom_table,
'message': _('PPM table row updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update PPM Table Row API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}

View File

@ -0,0 +1,168 @@
import frappe
from frappe import _
from frappe.utils import now, today, get_datetime
import json
@frappe.whitelist(allow_guest=False)
def get_user_details(user_id=None):
"""
Get detailed user information
Usage: /api/method/asset_lite.api.custom_api.get_user_details
"""
try:
if not user_id:
user_id = frappe.session.user
user = frappe.get_doc("User", user_id)
# Get user roles
roles = frappe.get_roles(user_id)
response_data = {
"user_id": user_id,
"full_name": user.full_name,
"email": user.email,
"user_image": user.user_image,
"roles": roles,
"last_login": user.last_login,
"enabled": user.enabled,
"creation": user.creation,
"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.status_code = 200
except Exception as e:
frappe.log_error(f"Error in get_user_details: {str(e)}")
frappe.response.message = {"error": str(e)}
frappe.response.status_code = 500
@frappe.whitelist(allow_guest=False)
def get_doctype_records(doctype, filters=None, fields=None, limit=20, offset=0):
"""
Get records from any DocType with filtering and pagination
Usage: /api/method/asset_lite.api.custom_api.get_doctype_records
"""
try:
# Parse filters and fields if provided as JSON strings
if isinstance(filters, str):
filters = json.loads(filters)
if isinstance(fields, str):
fields = json.loads(fields)
# Build the query
query_filters = filters or {}
# Get records
records = frappe.get_list(
doctype,
filters=query_filters,
fields=fields or ["*"],
limit=limit,
start=offset,
order_by="creation desc"
)
# Get total count for pagination
total_count = frappe.db.count(doctype, query_filters)
response_data = {
"records": records,
"total_count": total_count,
"limit": limit,
"offset": offset,
"has_more": (offset + limit) < total_count
}
frappe.response.message = response_data
frappe.response.status_code = 200
except Exception as e:
frappe.log_error(f"Error in get_doctype_records: {str(e)}")
frappe.response.message = {"error": str(e)}
frappe.response.status_code = 500
@frappe.whitelist(allow_guest=False)
def get_dashboard_stats():
"""
Get dashboard statistics
Usage: /api/method/asset_lite.api.custom_api.get_dashboard_stats
"""
try:
# Example: Get counts for different DocTypes
stats = {
"total_users": frappe.db.count("User", {"enabled": 1}),
"total_customers": frappe.db.count("Customer"),
"total_items": frappe.db.count("Item"),
"total_orders": frappe.db.count("Sales Order"),
"recent_activities": []
}
# Get recent activities (example)
recent_users = frappe.get_list(
"User",
fields=["name", "full_name", "creation"],
limit=5,
order_by="creation desc"
)
stats["recent_activities"] = recent_users
frappe.response.message = stats
frappe.response.status_code = 200
except Exception as e:
frappe.log_error(f"Error in get_dashboard_stats: {str(e)}")
frappe.response.message = {"error": str(e)}
frappe.response.status_code = 500
# Example KYC API for your KYCDetails component
@frappe.whitelist(allow_guest=False)
def get_kyc_details():
"""
Get KYC details - customize this based on your actual KYC DocType
Usage: /api/method/asset_lite.api.custom_api.get_kyc_details
"""
try:
# Replace 'KYC' with your actual DocType name
kyc_records = frappe.get_list(
"KYC", # Change this to your actual DocType
fields=["name", "kyc_status", "kyc_type", "creation"],
limit=50,
order_by="creation desc"
)
frappe.response.message = kyc_records
frappe.response.status_code = 200
except Exception as e:
frappe.log_error(f"Error in get_kyc_details: {str(e)}")
frappe.response.message = {"error": str(e)}
frappe.response.status_code = 500
# Simple test endpoint to verify API is working
@frappe.whitelist(allow_guest=False)
def test_api():
"""
Simple test endpoint to verify the API is working
Usage: /api/method/asset_lite.api.custom_api.test_api
"""
try:
frappe.response.message = {
"status": "success",
"message": "API is working!",
"user": frappe.session.user,
"timestamp": now()
}
frappe.response.status_code = 200
except Exception as e:
frappe.log_error(f"Error in test_api: {str(e)}")
frappe.response.message = {"error": str(e)}
frappe.response.status_code = 500

View File

@ -0,0 +1,598 @@
import frappe
from frappe import _
from frappe.utils import nowdate
import json
def _ok(payload, code=200):
frappe.response.status_code = code
frappe.response.message = payload
def _err(msg, code=500):
frappe.response.status_code = code
frappe.response.message = {"error": msg}
@frappe.whitelist(allow_guest = True)
def get_number_cards(company=None, site_name=None):
"""
Returns counts for Number Cards:
- total_assets
- work_orders_open
- work_orders_in_progress
- work_orders_completed
- work_orders_closed
Optional filters: company (Hospital), site_name (Mobile Team Site)
"""
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")
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({
"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": work_orders_closed,
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_number_cards")
_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)
def list_dashboard_charts(search=None, public_only=True, limit=50):
"""
List available Dashboard Chart docs and their y-axis rows.
"""
try:
filters = {}
if str(public_only) in ("1", "true", "True"): # tolerate string flags
filters["is_public"] = 1
charts = frappe.get_all(
"Dashboard Chart",
filters=filters,
fields=[
"name",
"chart_name",
"type",
"is_public",
"chart_type",
"report_name",
"use_report_chart",
"x_field",
"time_interval",
"timespan",
"custom_options",
],
limit=int(limit or 50),
order_by="modified desc",
)
for c in charts:
y_rows = frappe.get_all(
"Dashboard Chart Field",
filters={"parenttype": "Dashboard Chart", "parent": c["name"]},
fields=["y_field", "color"],
order_by="idx asc",
)
c["y_axes"] = y_rows
_ok({"charts": charts})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "list_dashboard_charts")
_err(str(e))
@frappe.whitelist(allow_guest = True)
def get_dashboard_chart_data(chart_name, report_filters=None):
"""
Return chart-ready JSON for any Dashboard Chart (Report-based or Custom).
"""
try:
if isinstance(report_filters, str):
report_filters = json.loads(report_filters or "{}")
report_filters = report_filters or {}
chart = frappe.get_doc("Dashboard Chart", chart_name)
# Handle Custom charts (non-Report based)
if chart.chart_type != "Report" or not chart.use_report_chart:
# For Custom charts, query the source doctype directly
try:
# Get chart configuration
source = chart.document_type
based_on = chart.based_on
value_based_on = chart.value_based_on or "name"
# Build aggregation query
if chart.type == "Pie":
# Group by based_on field and count
data = frappe.db.sql(f"""
SELECT {based_on} as label, COUNT({value_based_on}) as value
FROM `tab{source}`
GROUP BY {based_on}
ORDER BY value DESC
""", as_dict=True)
labels = [str(d.get("label")) for d in data]
values = [float(d.get("value") or 0) for d in data]
_ok({
"labels": labels,
"datasets": [{"name": "count", "values": values}],
"type": "Pie",
"options": _parse_custom_options(chart.custom_options),
"source": {"doctype": source},
})
else:
# Bar chart: group by based_on
data = frappe.db.sql(f"""
SELECT {based_on} as label, COUNT({value_based_on}) as value
FROM `tab{source}`
GROUP BY {based_on}
ORDER BY value DESC
LIMIT 20
""", as_dict=True)
labels = [str(d.get("label")) for d in data]
values = [float(d.get("value") or 0) for d in data]
_ok({
"labels": labels,
"datasets": [{"name": "count", "values": values, "color": "#4F46E5"}],
"type": "Bar",
"options": _parse_custom_options(chart.custom_options),
"source": {"doctype": source},
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), f"Custom Chart Error: {chart_name}")
_err(f"Error processing custom chart: {str(e)}")
return
y_axes = frappe.get_all(
"Dashboard Chart Field",
filters={"parenttype": "Dashboard Chart", "parent": chart.name},
fields=["y_field", "color"],
order_by="idx asc",
)
run = frappe.get_attr("frappe.desk.query_report.run")
report_result = run(chart.report_name, filters=report_filters)
rows = report_result.get("result", []) or []
data_rows = [r for r in rows if not r.get("is_total_row")]
x_key = chart.x_field
labels = [str(r.get(x_key)) for r in data_rows if r.get(x_key) is not None]
datasets = []
for y in y_axes:
series_name = y.get("y_field") # Use field name as series name
values = []
for r in data_rows:
val = r.get(y.get("y_field"))
try:
values.append(float(val) if val is not None else 0)
except Exception:
values.append(0)
datasets.append({"name": series_name, "values": values, "color": y.get("color")})
chart_type = (chart.type or "Bar").title()
if chart_type.lower() == "pie":
ds = datasets[0] if datasets else {"name": "value", "values": []}
_ok({
"labels": labels,
"datasets": [ds],
"type": "Pie",
"options": _parse_custom_options(chart.custom_options),
"source": {"report": chart.report_name},
})
return
_ok({
"labels": labels,
"datasets": datasets,
"type": chart_type,
"options": _parse_custom_options(chart.custom_options),
"source": {"report": chart.report_name},
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_dashboard_chart_data")
_err(str(e))
def _parse_custom_options(raw):
if not raw:
return {}
try:
if isinstance(raw, dict):
return raw
return json.loads(raw)
except Exception:
return {}
@frappe.whitelist(allow_guest = True)
def get_repair_cost_by_item(year=None):
"""
Example specialized endpoint for 'Repair Cost' report style chart
(X: item_code, Y: amount, Filter: Year)
"""
try:
year = int(year or frappe.utils.getdate(nowdate()).year)
rows = frappe.db.sql(
"""
SELECT item_code, SUM(amount) as amount
FROM `tabWork Order` wo
WHERE YEAR(wo.posting_date) = %(year)s
GROUP BY item_code
ORDER BY amount DESC
""",
{"year": year},
as_dict=True,
)
labels = [r.item_code or "Unknown" for r in rows]
values = [float(r.amount or 0) for r in rows]
_ok({
"labels": labels,
"datasets": [{"name": f"Repair Cost {year}", "values": values}],
"type": "Bar",
"options": {},
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_repair_cost_by_item")
_err(str(e))

View 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

View 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

565
asset_lite/api/ppm_api.py Normal file
View File

@ -0,0 +1,565 @@
import frappe
from frappe import _
@frappe.whitelist(allow_guest = True)
def get_asset_maintenances(filters=None, fields=None, limit=20, offset=0, order_by=None):
"""
Get list of asset maintenances (PPM schedules) with filters and pagination
Args:
filters: JSON string of filters (e.g., '{"company": "Al Jouf Hospital"}')
fields: JSON string of fields to return (e.g., '["asset_name", "maintenance_team"]')
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
order_by: Sort order (e.g., "creation desc")
Returns:
{
"asset_maintenances": [...],
"total_count": int,
"limit": int,
"offset": int,
"has_more": bool
}
"""
try:
# Parse filters if provided
if filters and isinstance(filters, str):
import json
filters = json.loads(filters)
# Parse fields if provided
if fields and isinstance(fields, str):
import json
fields = json.loads(fields)
else:
# Default fields to return
fields = [
'name',
'company',
'asset_name',
'custom_asset_type',
'asset_category',
'custom_type_of_maintenance',
'custom_asset_name',
'item_code',
'item_name',
'maintenance_team',
'custom_pm_schedule',
'maintenance_manager',
'maintenance_manager_name',
'custom_warranty',
'custom_warranty_status',
'custom_service_contract',
'custom_service_contract_status',
'custom_frequency',
'custom_total_amount',
'custom_no_of_pms',
'custom_price_per_pm',
'creation',
'modified',
'owner',
'modified_by',
'docstatus',
'idx'
]
# Get total count
total_count = frappe.db.count('Asset Maintenance', filters=filters or {})
# Get asset maintenances
asset_maintenances = frappe.get_all(
'Asset Maintenance',
filters=filters or {},
fields=fields,
limit_page_length=int(limit),
limit_start=int(offset),
order_by=order_by or 'creation desc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenances': asset_maintenances,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Asset Maintenances API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenances': [],
'total_count': 0
}
@frappe.whitelist(allow_guest = True)
def get_asset_maintenance_details(maintenance_name):
"""
Get detailed information about a specific asset maintenance (PPM schedule)
Args:
maintenance_name: Name/ID of the asset maintenance
Returns:
Asset Maintenance document with all fields including child tables
"""
try:
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Check if user has permission to read this maintenance
if not frappe.has_permission('Asset Maintenance', 'read', maintenance_name):
frappe.throw(_('Not permitted to access this asset maintenance'))
# Get asset maintenance details
maintenance = frappe.get_doc('Asset Maintenance', maintenance_name)
frappe.response['message'] = maintenance.as_dict()
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Asset Maintenance Details API Error')
frappe.response['message'] = {
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def create_asset_maintenance(maintenance_data):
"""
Create a new asset maintenance (PPM schedule)
Args:
maintenance_data: JSON string containing asset maintenance fields
Returns:
Created asset maintenance document
"""
try:
import json
# Parse maintenance data
if isinstance(maintenance_data, str):
maintenance_data = json.loads(maintenance_data)
# Check if user has permission to create asset maintenance
if not frappe.has_permission('Asset Maintenance', 'create'):
frappe.throw(_('Not permitted to create asset maintenance'))
# Create new asset maintenance
maintenance = frappe.get_doc({
'doctype': 'Asset Maintenance',
**maintenance_data
})
maintenance.insert()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance': maintenance.as_dict(),
'message': _('Asset Maintenance created successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Create Asset Maintenance API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def update_asset_maintenance(maintenance_name, maintenance_data):
"""
Update an existing asset maintenance (PPM schedule)
Args:
maintenance_name: Name/ID of the asset maintenance
maintenance_data: JSON string containing fields to update
Returns:
Updated asset maintenance document
"""
try:
import json
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Parse maintenance data
if isinstance(maintenance_data, str):
maintenance_data = json.loads(maintenance_data)
# Check if user has permission to update this maintenance
if not frappe.has_permission('Asset Maintenance', 'write', maintenance_name):
frappe.throw(_('Not permitted to update this asset maintenance'))
# Get and update asset maintenance
maintenance = frappe.get_doc('Asset Maintenance', maintenance_name)
# Update fields
for key, value in maintenance_data.items():
if hasattr(maintenance, key):
setattr(maintenance, key, value)
maintenance.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance': maintenance.as_dict(),
'message': _('Asset Maintenance updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Asset Maintenance API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def delete_asset_maintenance(maintenance_name):
"""
Delete an asset maintenance (PPM schedule)
Args:
maintenance_name: Name/ID of the asset maintenance
Returns:
Success message
"""
try:
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Check if user has permission to delete this maintenance
if not frappe.has_permission('Asset Maintenance', 'delete', maintenance_name):
frappe.throw(_('Not permitted to delete this asset maintenance'))
# Delete asset maintenance
frappe.delete_doc('Asset Maintenance', maintenance_name)
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'message': _('Asset Maintenance deleted successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Delete Asset Maintenance API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def get_maintenance_tasks(maintenance_name):
"""
Get all maintenance tasks for a specific asset maintenance
Args:
maintenance_name: Name/ID of the asset maintenance
Returns:
List of maintenance tasks (asset_maintenance_tasks child table)
"""
try:
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Check if user has permission to read this maintenance
if not frappe.has_permission('Asset Maintenance', 'read', maintenance_name):
frappe.throw(_('Not permitted to access this asset maintenance'))
# Get maintenance tasks
tasks = frappe.get_all(
'Asset Maintenance Task',
filters={'parent': maintenance_name},
fields=['*'],
order_by='idx asc'
)
frappe.response['message'] = {
'maintenance_tasks': tasks,
'total_count': len(tasks)
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Maintenance Tasks API Error')
frappe.response['message'] = {
'error': str(e),
'maintenance_tasks': []
}
@frappe.whitelist(allow_guest = True)
def get_service_coverage(maintenance_name):
"""
Get service coverage details for a specific asset maintenance
Args:
maintenance_name: Name/ID of the asset maintenance
Returns:
List of service coverage (custom_service_coverage_table child table)
"""
try:
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Check if user has permission to read this maintenance
if not frappe.has_permission('Asset Maintenance', 'read', maintenance_name):
frappe.throw(_('Not permitted to access this asset maintenance'))
# Get service coverage
coverage = frappe.get_all(
'Service Coverage',
filters={'parent': maintenance_name},
fields=['*'],
order_by='idx asc'
)
frappe.response['message'] = {
'service_coverage': coverage,
'total_count': len(coverage)
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Service Coverage API Error')
frappe.response['message'] = {
'error': str(e),
'service_coverage': []
}
@frappe.whitelist(allow_guest = True)
def add_maintenance_task(maintenance_name, task_data):
"""
Add a new maintenance task to an asset maintenance
Args:
maintenance_name: Name/ID of the asset maintenance
task_data: JSON string containing task fields
Returns:
Updated asset maintenance document
"""
try:
import json
if not maintenance_name:
frappe.throw(_('Asset Maintenance name is required'))
# Parse task data
if isinstance(task_data, str):
task_data = json.loads(task_data)
# Check if user has permission to update this maintenance
if not frappe.has_permission('Asset Maintenance', 'write', maintenance_name):
frappe.throw(_('Not permitted to update this asset maintenance'))
# Get asset maintenance
maintenance = frappe.get_doc('Asset Maintenance', maintenance_name)
# Add new task
maintenance.append('asset_maintenance_tasks', task_data)
maintenance.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'asset_maintenance': maintenance.as_dict(),
'message': _('Maintenance task added successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Add Maintenance Task API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def update_maintenance_task(task_name, task_data):
"""
Update a specific maintenance task
Args:
task_name: Name/ID of the maintenance task
task_data: JSON string containing fields to update
Returns:
Updated task details
"""
try:
import json
if not task_name:
frappe.throw(_('Maintenance task name is required'))
# Parse task data
if isinstance(task_data, str):
task_data = json.loads(task_data)
# Get the task to find parent
task = frappe.get_doc('Asset Maintenance Task', task_name)
# Check if user has permission to update parent maintenance
if not frappe.has_permission('Asset Maintenance', 'write', task.parent):
frappe.throw(_('Not permitted to update this maintenance task'))
# Update task fields
for key, value in task_data.items():
if hasattr(task, key):
setattr(task, key, value)
task.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'maintenance_task': task.as_dict(),
'message': _('Maintenance task updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Maintenance Task API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest = True)
def get_maintenances_by_asset(asset_name, filters=None, limit=20, offset=0):
"""
Get all maintenance schedules for a specific asset
Args:
asset_name: Name/ID of the asset
filters: Additional JSON string of filters
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
Returns:
List of maintenance schedules for the asset
"""
try:
import json
if not asset_name:
frappe.throw(_('Asset name is required'))
# Parse additional filters if provided
additional_filters = {}
if filters and isinstance(filters, str):
additional_filters = json.loads(filters)
# Combine filters
combined_filters = {'asset_name': asset_name, **additional_filters}
# Get total count
total_count = frappe.db.count('Asset Maintenance', filters=combined_filters)
# Get maintenances
maintenances = frappe.get_all(
'Asset Maintenance',
filters=combined_filters,
fields=['*'],
limit_page_length=int(limit),
limit_start=int(offset),
order_by='creation desc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenances': maintenances,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Maintenances By Asset API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenances': [],
'total_count': 0
}
@frappe.whitelist(allow_guest = True)
def get_active_service_contracts(filters=None, limit=20, offset=0):
"""
Get all asset maintenances with active service contracts
Args:
filters: Additional JSON string of filters
limit: Number of records to return (default: 20)
offset: Number of records to skip (default: 0)
Returns:
List of asset maintenances with active contracts
"""
try:
import json
# Parse additional filters if provided
additional_filters = {}
if filters and isinstance(filters, str):
additional_filters = json.loads(filters)
# Combine filters - get maintenances with service contract = 1
combined_filters = {
'custom_service_contract': 1,
**additional_filters
}
# Get total count
total_count = frappe.db.count('Asset Maintenance', filters=combined_filters)
# Get maintenances
maintenances = frappe.get_all(
'Asset Maintenance',
filters=combined_filters,
fields=['*'],
limit_page_length=int(limit),
limit_start=int(offset),
order_by='creation desc'
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'asset_maintenances': maintenances,
'total_count': total_count,
'limit': int(limit),
'offset': int(offset),
'has_more': has_more
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Active Service Contracts API Error')
frappe.response['message'] = {
'error': str(e),
'asset_maintenances': [],
'total_count': 0
}

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,139 @@
import frappe
from frappe import _
@frappe.whitelist(allow_guest = True)
def get_translations(language='en'):
"""
Get all translations for a specific language from Frappe's Translation doctype
This returns a dictionary of source text -> translated text
Usage: /api/method/asset_lite.api.translation_api.get_translations?language=ar
Args:
language: Language code (e.g., 'en', 'ar')
Returns:
Dictionary mapping source text to translated text
"""
try:
# Validate language parameter
if not language:
language = 'en'
# Get all translations for the specified language
translations = frappe.get_all(
'Translation',
filters={
'language': language
},
fields=['source_text', 'translated_text'],
limit_page_length=0 # 0 means no limit (get all)
)
# Convert to dictionary format: {source_text: translated_text}
translation_dict = {}
for trans in translations:
source = trans.get('source_text')
translated = trans.get('translated_text')
if source and translated:
translation_dict[source] = translated
return {
"success": True,
"language": language,
"count": len(translation_dict),
"translations": translation_dict
}
except Exception as e:
frappe.log_error(f"Error in get_translations: {str(e)}", "Translation API Error")
return {
"success": False,
"error": str(e),
"translations": {}
}
@frappe.whitelist(allow_guest=False)
def get_available_languages():
"""
Get list of all available languages that have translations
Usage: /api/method/asset_lite.api.translation_api.get_available_languages
Returns:
List of language codes (e.g., ['en', 'ar'])
"""
try:
# Get distinct languages from Translation doctype
languages = frappe.db.sql("""
SELECT DISTINCT language
FROM `tabTranslation`
WHERE language IS NOT NULL AND language != ''
ORDER BY language
""", as_dict=True)
language_list = [lang['language'] for lang in languages if lang.get('language')]
# Always include English as default
if 'en' not in language_list:
language_list.insert(0, 'en')
return {
"success": True,
"languages": language_list
}
except Exception as e:
frappe.log_error(f"Error in get_available_languages: {str(e)}", "Translation API Error")
return {
"success": False,
"error": str(e),
"languages": ['en']
}
@frappe.whitelist(allow_guest=False)
def get_translation(source_text, language='ar'):
"""
Get a single translation for a specific text
Usage: /api/method/asset_lite.api.translation_api.get_translation?source_text=Comprehensive&language=ar
Args:
source_text: The text to translate
language: Target language code (default: 'ar')
Returns:
Translated text or original if not found
"""
try:
if not source_text:
return {
"success": False,
"error": "source_text is required"
}
translation = frappe.db.get_value(
'Translation',
filters={
'language': language,
'source_text': source_text
},
fieldname='translated_text'
)
return {
"success": True,
"source_text": source_text,
"translated_text": translation or source_text, # Return original if not found
"found": bool(translation)
}
except Exception as e:
frappe.log_error(f"Error in get_translation: {str(e)}", "Translation API Error")
return {
"success": False,
"error": str(e),
"translated_text": source_text
}

View File

@ -0,0 +1,105 @@
import frappe
@frappe.whitelist()
def get_user_roles():
"""Get roles for the current logged-in user - no permission check needed"""
user = frappe.session.user
if not user or user == "Guest":
return []
# Get roles using ignore_permissions
roles = frappe.get_roles(user)
return roles
@frappe.whitelist()
def get_user_info_with_roles():
"""Get current user info along with their roles"""
user = frappe.session.user
if not user or user == "Guest":
return {"user": None, "roles": []}
roles = frappe.get_roles(user)
return {
"user": user,
"roles": roles,
"full_name": frappe.db.get_value("User", user, "full_name")
}
@frappe.whitelist(allow_guest=False)
def check_has_role(roles):
"""Check if current user has any of the specified roles
Args:
roles: comma-separated string or list of role names
Returns:
dict with has_role (bool) and matching_roles (list)
"""
user = frappe.session.user
if not user or user == "Guest":
return {"has_role": False, "matching_roles": [], "user_roles": []}
# Handle both string and list input
if isinstance(roles, str):
check_roles = [r.strip() for r in roles.split(",")]
else:
check_roles = roles
user_roles = frappe.get_roles(user)
matching_roles = [r for r in check_roles if r in user_roles]
return {
"has_role": len(matching_roles) > 0,
"matching_roles": matching_roles,
"user_roles": user_roles
}
@frappe.whitelist()
def get_users_with_role(role):
"""
Get all enabled users who have a specific role
Args:
role: Role name (e.g., 'Technician')
Returns:
List of users with name and full_name
"""
if not role:
return []
# Get all users who have this role from Has Role child table
users_with_role = frappe.get_all(
"Has Role",
filters={
"role": role,
"parenttype": "User"
},
fields=["parent"],
distinct=True
)
if not users_with_role:
return []
user_names = [u.parent for u in users_with_role]
# Get user details for enabled users only
user_details = frappe.get_all(
"User",
filters={
"name": ["in", user_names],
"enabled": 1
},
fields=["name", "full_name"],
order_by="full_name asc"
)
return user_details

View File

@ -0,0 +1,350 @@
import frappe
from frappe import _
# ============================================================================
# CONFIGURATION: Define field mappings for each doctype
# Add new doctypes here as needed - this is the ONLY place you need to update
# ============================================================================
DOCTYPE_PERMISSION_MAPPINGS = {
"Asset": {
"Company": "company",
"Hospital": "company",
"Mobile Team Site": "custom_site",
"Location": "location",
"Department": "department",
"Manufacturer": "custom_manufacturer",
"Supplier": "supplier",
"Modality": "custom_modality",
"Cost Center": "cost_center",
"Asset Type":"custom_asset_type",
"Asset Category": "asset_category"
},
"Work_Order": {
"Company": "company",
"Hospital": "company",
"Mobile Team Site": "site_name",
"Location": "location",
"Department": "department"
},
"Asset Maintenance": {
"Company": "company",
"Asset": "asset_name",
"Supplier": "supplier"
},
"Asset Maintenance Log": {
"Company": "custom_hospital_name",
"Hospital": "custom_hospital_name",
"Asset": "asset_name"
},
"Item": {
"Company": "custom_hospital_name",
"Hospital": "custom_hospital_name",
}
# Add more doctypes as needed - just add them here!
}
# ============================================================================
# HELPER FUNCTION
# ============================================================================
def is_system_user(user):
"""Check if user is Administrator or has System Manager role."""
if user == "Administrator":
return True
roles = frappe.get_roles(user)
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
# ============================================================================
@frappe.whitelist(allow_guest = True)
def get_user_permissions(user=None):
"""
Get all user permissions for the logged-in user.
Returns:
dict: User permissions grouped by 'allow' doctype
"""
if not user:
user = frappe.session.user
if is_system_user(user):
return {
"is_admin": True,
"permissions": {},
"user": user,
"total_permissions": 0
}
permissions = frappe.get_all(
"User Permission",
filters={"user": user},
fields=["name", "allow", "for_value", "is_default", "apply_to_all_doctypes", "applicable_for"],
order_by="allow asc"
)
# Group by 'allow' doctype
grouped = {}
for perm in permissions:
allow_doctype = perm.get("allow")
if allow_doctype not in grouped:
grouped[allow_doctype] = []
grouped[allow_doctype].append({
"for_value": perm.get("for_value"),
"is_default": perm.get("is_default"),
"apply_to_all_doctypes": perm.get("apply_to_all_doctypes"),
"applicable_for": perm.get("applicable_for")
})
return {
"is_admin": False,
"permissions": grouped,
"user": user,
"total_permissions": len(permissions),
"permission_types": list(grouped.keys())
}
@frappe.whitelist(allow_guest = True)
def get_permission_filters(target_doctype, user=None):
"""
Get permission filters for ANY doctype.
This is the MAIN function - use this for all doctypes.
Args:
target_doctype: The doctype (e.g., "Asset", "Work Order", "Project")
user: Optional user email
Returns:
dict: Filters to apply for queries
"""
if not user:
user = frappe.session.user
# System users have full access
if is_system_user(user):
return {
"is_admin": True,
"filters": {},
"restrictions": {},
"target_doctype": target_doctype,
"user": user
}
# Get field mapping for this doctype
field_mapping = DOCTYPE_PERMISSION_MAPPINGS.get(target_doctype, {})
if not field_mapping:
return {
"is_admin": False,
"filters": {},
"restrictions": {},
"target_doctype": target_doctype,
"user": user,
"warning": f"No permission mapping defined for {target_doctype}"
}
filters = {}
restrictions = {}
for allow_doctype, target_field in field_mapping.items():
permissions = frappe.get_all(
"User Permission",
filters={
"user": user,
"allow": allow_doctype
},
fields=["for_value", "applicable_for", "apply_to_all_doctypes"]
)
if permissions:
# Filter permissions that apply to this doctype
applicable = [
p for p in permissions
if p.get("apply_to_all_doctypes") == 1
or not p.get("applicable_for")
or p.get("applicable_for") == target_doctype
]
if applicable:
allowed_values = list(set([p.get("for_value") for p in applicable]))
filters[target_field] = ["in", allowed_values]
restrictions[allow_doctype] = {
"field": target_field,
"values": allowed_values,
"count": len(allowed_values)
}
return {
"is_admin": False,
"filters": filters,
"restrictions": restrictions,
"target_doctype": target_doctype,
"user": user,
"total_restrictions": len(restrictions)
}
@frappe.whitelist(allow_guest = True)
def get_allowed_values(allow_doctype, user=None):
"""
Get allowed values for a specific permission type.
Args:
allow_doctype: e.g., "Company", "Location", "Department"
user: Optional user email
Returns:
dict: List of allowed values
"""
if not user:
user = frappe.session.user
if is_system_user(user):
return {
"is_admin": True,
"allowed_values": [],
"has_restriction": False
}
permissions = frappe.get_all(
"User Permission",
filters={"user": user, "allow": allow_doctype},
fields=["for_value", "is_default"]
)
allowed_values = list(set([p.get("for_value") for p in permissions]))
default_value = next((p.get("for_value") for p in permissions if p.get("is_default")), None)
return {
"is_admin": False,
"allowed_values": sorted(allowed_values),
"default_value": default_value,
"has_restriction": len(allowed_values) > 0,
"allow_doctype": allow_doctype
}
@frappe.whitelist(allow_guest = True)
def check_document_access(doctype, docname, user=None):
"""
Check if user has access to a specific document.
Args:
doctype: e.g., "Asset", "Work Order"
docname: The document name/ID
user: Optional user email
Returns:
dict: Access status
"""
if not user:
user = frappe.session.user
if is_system_user(user):
return {"has_access": True, "is_admin": True}
try:
doc = frappe.get_doc(doctype, docname)
except frappe.DoesNotExistError:
return {"has_access": False, "error": f"{doctype} '{docname}' not found"}
except frappe.PermissionError:
return {"has_access": False, "error": "Permission denied"}
# Get permission filters
perm_result = get_permission_filters(doctype, user)
if perm_result.get("is_admin"):
return {"has_access": True, "is_admin": True}
restrictions = perm_result.get("restrictions", {})
if not restrictions:
return {"has_access": True, "no_restrictions": True}
# Check each restriction
for allow_doctype, info in restrictions.items():
field = info.get("field")
allowed_values = info.get("values", [])
doc_value = getattr(doc, field, None)
if doc_value and doc_value not in allowed_values:
return {
"has_access": False,
"denied_by": allow_doctype,
"field": field,
"document_value": doc_value,
"allowed_values": allowed_values
}
return {"has_access": True}
@frappe.whitelist(allow_guest = True)
def get_configured_doctypes():
"""Get list of doctypes that have permission mappings configured."""
return {
"doctypes": list(DOCTYPE_PERMISSION_MAPPINGS.keys()),
"mappings": {
dt: list(mapping.keys())
for dt, mapping in DOCTYPE_PERMISSION_MAPPINGS.items()
}
}
@frappe.whitelist(allow_guest = True)
def get_user_defaults(user=None):
"""Get default values from user permissions (where is_default=1)."""
if not user:
user = frappe.session.user
if is_system_user(user):
return {"is_admin": True, "defaults": {}}
permissions = frappe.get_all(
"User Permission",
filters={"user": user, "is_default": 1},
fields=["allow", "for_value"]
)
defaults = {p.get("allow"): p.get("for_value") for p in permissions}
return {"is_admin": False, "defaults": defaults}

View 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",
}

File diff suppressed because it is too large Load Diff

View File

View File

@ -0,0 +1,47 @@
frappe.dashboards.chart_sources["Active Map Widget"] = {
method: "asset_lite.map.get_custom_html_data",
filters: []
};
// Override the chart rendering after data is loaded
frappe.provide('frappe.dashboards');
$(document).on('app_ready', function() {
// Override the render method for custom HTML charts
const original_render = frappe.ui.Dashboard.prototype.render_chart;
frappe.ui.Dashboard.prototype.render_chart = function(chart_data, chart_container) {
if (chart_data.custom_html) {
// Clear the container and add custom HTML
chart_container.empty();
const custom_html = `
<div style="padding: 20px; background: #f8f9fa; border-radius: 8px; margin: 10px;">
<h4 style="margin-bottom: 15px; color: #333;">Custom Dashboard Content</h4>
<div class="row">
<div class="col-md-6">
<div class="card" style="border: 1px solid #ddd; padding: 15px;">
<h5>Card Title 1</h5>
<p>Your custom content here</p>
<button class="btn btn-primary btn-sm">Action Button</button>
</div>
</div>
<div class="col-md-6">
<div class="card" style="border: 1px solid #ddd; padding: 15px;">
<h5>Card Title 2</h5>
<p>More custom content</p>
<div class="progress" style="height: 20px;">
<div class="progress-bar" style="width: 75%">75%</div>
</div>
</div>
</div>
</div>
</div>
`;
chart_container.html(custom_html);
return;
}
// Call original render method for other charts
return original_render.call(this, chart_data, chart_container);
};
});

View File

@ -0,0 +1,13 @@
{
"creation": "2025-06-25 18:03:16.704998",
"docstatus": 0,
"doctype": "Dashboard Chart Source",
"idx": 0,
"modified": "2025-06-25 18:03:16.704998",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Active Map Widget",
"owner": "Administrator",
"source_name": "Active Map Widget",
"timeseries": 0
}

View File

@ -0,0 +1,8 @@
// Copyright (c) 2024, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Agent", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,45 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:agent_name",
"creation": "2024-12-13 14:37:38.780730",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"agent_name"
],
"fields": [
{
"fieldname": "agent_name",
"fieldtype": "Data",
"label": "Agent Name",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-12-13 15:15:47.447599",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Agent",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Agent(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestAgent(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Arabic Names", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,50 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:name1",
"creation": "2025-02-07 16:56:53.486694",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"name1",
"arabic_name"
],
"fields": [
{
"fieldname": "name1",
"fieldtype": "Data",
"label": "Name",
"unique": 1
},
{
"fieldname": "arabic_name",
"fieldtype": "Data",
"label": "Arabic Name"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-02-07 16:57:50.823039",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Arabic Names",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ArabicNames(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestArabicNames(FrappeTestCase):
pass

View File

@ -0,0 +1,59 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-26 21:00:34.676325",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"asset",
"asset_name",
"qty",
"column_break_kbew",
"return_inspection_committee"
],
"fields": [
{
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
"options": "Asset"
},
{
"fetch_from": "asset.asset_name",
"fetch_if_empty": 1,
"fieldname": "asset_name",
"fieldtype": "Data",
"label": "Asset Name"
},
{
"default": "1",
"fieldname": "qty",
"fieldtype": "Float",
"label": "Qty"
},
{
"fieldname": "column_break_kbew",
"fieldtype": "Column Break"
},
{
"fieldname": "return_inspection_committee",
"fieldtype": "Select",
"label": "Return Inspection Committee",
"options": "\nFor Repair\nFor Sale\nFor Disposal"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-26 21:00:34.676325",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Asset Item Transfer",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class AssetItemTransfer(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2024, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Asset Type", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,47 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:asset_type",
"creation": "2024-09-23 14:16:49.149093",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"asset_type"
],
"fields": [
{
"fieldname": "asset_type",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Asset Type",
"reqd": 1,
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-23 14:17:58.073254",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Asset Type",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"quick_entry": 1,
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class AssetType(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestAssetType(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("City", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,44 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:city",
"creation": "2025-08-26 15:20:17.013649",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"city"
],
"fields": [
{
"fieldname": "city",
"fieldtype": "Data",
"label": "City",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-26 20:43:50.686274",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "City",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class City(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestCity(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2024, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("ECRI UMDNS", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,51 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:ecri",
"creation": "2024-12-03 14:43:40.910282",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"ecri",
"device_name"
],
"fields": [
{
"fieldname": "ecri",
"fieldtype": "Data",
"label": "ECRI",
"unique": 1
},
{
"fieldname": "device_name",
"fieldtype": "Data",
"label": "Device Name"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-12-03 18:43:19.609779",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "ECRI UMDNS",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ECRIUMDNS(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestECRIUMDNS(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Feedback", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,77 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-03-05 19:53:51.796362",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"work_order",
"column_break_eytf",
"feedback_by",
"section_break_rvtb",
"parameters",
"section_break_zmrx",
"overall"
],
"fields": [
{
"fieldname": "work_order",
"fieldtype": "Link",
"label": "Work Order",
"options": "Work_Order"
},
{
"fieldname": "column_break_eytf",
"fieldtype": "Column Break"
},
{
"fieldname": "feedback_by",
"fieldtype": "Link",
"label": "Feedback by",
"options": "User"
},
{
"fieldname": "section_break_rvtb",
"fieldtype": "Section Break"
},
{
"fieldname": "parameters",
"fieldtype": "Table",
"label": "Parameters",
"options": "Feedback Table"
},
{
"fieldname": "section_break_zmrx",
"fieldtype": "Section Break"
},
{
"fieldname": "overall",
"fieldtype": "Rating",
"label": "Overall"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-03-05 19:56:58.056262",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Feedback",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Feedback(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestFeedback(FrappeTestCase):
pass

View File

@ -0,0 +1,44 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-03-05 20:36:14.651301",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"parameter",
"rating",
"feedback"
],
"fields": [
{
"fieldname": "parameter",
"fieldtype": "Data",
"label": "Parameter"
},
{
"fieldname": "rating",
"fieldtype": "Rating",
"in_list_view": 1,
"label": "Rating",
"reqd": 1
},
{
"fieldname": "feedback",
"fieldtype": "Text",
"label": "Feedback"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-03-05 20:49:43.458939",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Feedback Table",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class FeedbackTable(Document):
pass

View File

@ -0,0 +1,64 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-08-26 21:01:28.397119",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"item",
"qty",
"uom",
"column_break_gcjw",
"item_name",
"return_inspection_committee"
],
"fields": [
{
"fieldname": "item",
"fieldtype": "Link",
"label": "Item",
"options": "Item"
},
{
"fieldname": "qty",
"fieldtype": "Float",
"label": "Qty"
},
{
"fetch_from": "item.stock_uom",
"fetch_if_empty": 1,
"fieldname": "uom",
"fieldtype": "Link",
"label": "UOM",
"options": "UOM"
},
{
"fieldname": "column_break_gcjw",
"fieldtype": "Column Break"
},
{
"fieldname": "item_name",
"fieldtype": "Data",
"label": "Item Name"
},
{
"fieldname": "return_inspection_committee",
"fieldtype": "Select",
"label": "Return Inspection Committee",
"options": "\nFor Repair\nFor Sale\nFor Disposal"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-08-26 21:01:28.397119",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Item Transfer Table",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class ItemTransferTable(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Material Transfer", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,128 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:Material-Transfer-{####}",
"creation": "2025-08-26 21:02:44.817236",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"hospital",
"purpose",
"source_warehouse",
"asset",
"column_break_irlz",
"date",
"reason_for_return",
"transfer_type",
"target_warehouse",
"item",
"section_break_bunw",
"item_table",
"asset_transfer"
],
"fields": [
{
"fieldname": "hospital",
"fieldtype": "Link",
"label": "Hospital",
"options": "Company"
},
{
"fieldname": "purpose",
"fieldtype": "Select",
"label": "Purpose",
"options": "Transfer"
},
{
"fieldname": "source_warehouse",
"fieldtype": "Link",
"label": "Source Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:doc.transfer_type=='Asset'",
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
"options": "Asset"
},
{
"fieldname": "column_break_irlz",
"fieldtype": "Column Break"
},
{
"default": "Today",
"fieldname": "date",
"fieldtype": "Date",
"label": "Date"
},
{
"fieldname": "reason_for_return",
"fieldtype": "Select",
"label": "Reason for Return",
"options": "\nPurpose Completed\nSurplus\nUnusable\nDamaged"
},
{
"fieldname": "transfer_type",
"fieldtype": "Select",
"label": "Transfer Type",
"options": "\nAsset\nItem"
},
{
"fieldname": "target_warehouse",
"fieldtype": "Link",
"label": "Target Warehouse",
"options": "Warehouse"
},
{
"depends_on": "eval:doc.transfer_type=='Item'",
"fieldname": "item",
"fieldtype": "Link",
"label": "Item",
"options": "Item"
},
{
"fieldname": "section_break_bunw",
"fieldtype": "Section Break"
},
{
"depends_on": "eval:doc.transfer_type=='Item'",
"fieldname": "item_table",
"fieldtype": "Table",
"label": "Item Table",
"options": "Item Transfer Table"
},
{
"depends_on": "eval:doc.transfer_type=='Asset'",
"fieldname": "asset_transfer",
"fieldtype": "Table",
"label": "Asset Transfer",
"options": "Asset Item Transfer"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-26 21:02:44.817236",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Material Transfer",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class MaterialTransfer(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestMaterialTransfer(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Mobile Team Site", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,58 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:site_name",
"creation": "2025-08-08 15:36:41.575671",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"site_name",
"mobile_team",
"city"
],
"fields": [
{
"fieldname": "site_name",
"fieldtype": "Data",
"label": "Site Name",
"unique": 1
},
{
"fieldname": "mobile_team",
"fieldtype": "Link",
"label": "Mobile Team",
"options": "Company"
},
{
"fieldname": "city",
"fieldtype": "Link",
"label": "City",
"options": "City"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-08-26 15:26:09.017274",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Mobile Team Site",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class MobileTeamSite(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestMobileTeamSite(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Modality", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,44 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "field:modality",
"creation": "2025-09-08 12:40:28.438645",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"modality"
],
"fields": [
{
"fieldname": "modality",
"fieldtype": "Data",
"label": "Modality",
"unique": 1
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2025-09-08 12:41:21.725442",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Modality",
"naming_rule": "By fieldname",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class Modality(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestModality(FrappeTestCase):
pass

View File

@ -0,0 +1,38 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-09-20 13:15:43.666184",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"purchase_invoice",
"cost"
],
"fields": [
{
"fieldname": "purchase_invoice",
"fieldtype": "Link",
"label": "Purchase Invoice",
"options": "Purchase Invoice"
},
{
"fetch_from": "purchase_invoice.grand_total",
"fieldname": "cost",
"fieldtype": "Currency",
"label": "Cost"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-09-20 13:19:33.308381",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PI Table",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PITable(Document):
pass

View File

@ -0,0 +1,70 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2025-09-18 13:07:33.907967",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"asset",
"asset_name",
"start_date",
"end_date",
"manufacturer",
"model"
],
"fields": [
{
"fieldname": "asset",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"label": "Asset",
"options": "Asset"
},
{
"fieldname": "asset_name",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Asset Name",
"read_only": 1
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "Start Date"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"in_list_view": 1,
"label": "End Date"
},
{
"fieldname": "manufacturer",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Manufacturer",
"options": "Manufacturer"
},
{
"fieldname": "model",
"fieldtype": "Data",
"in_list_view": 1,
"label": "Model"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2025-09-18 13:08:35.075783",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PM Entry Line",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PMEntryLine(Document):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2025, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("PM Schedule Generator", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,232 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:PMSG-{DD}-{MM}-{YY}-{####}",
"creation": "2025-09-08 13:29:07.175342",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"asset_details_section",
"hospital",
"manufacturer",
"model",
"column_break_qtoh",
"pm_for",
"asset_name",
"modality",
"device_status",
"maintenance_details_section",
"start_date",
"no_of_pms",
"maintenance_team",
"maintenance_manager",
"column_break_kapm",
"periodicity",
"end_date",
"assign_to",
"due_date",
"next_pm_date",
"section_break_hkrb",
"maintenance_entries",
"amended_from"
],
"fields": [
{
"fieldname": "asset_details_section",
"fieldtype": "Section Break",
"label": "Asset Details"
},
{
"fieldname": "hospital",
"fieldtype": "Link",
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Hospital",
"options": "Company",
"reqd": 1
},
{
"fieldname": "manufacturer",
"fieldtype": "Link",
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Manufacturer",
"options": "Manufacturer"
},
{
"fieldname": "model",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"label": "Model"
},
{
"fieldname": "column_break_qtoh",
"fieldtype": "Column Break"
},
{
"fieldname": "asset_name",
"fieldtype": "Data",
"label": "Asset Name",
"read_only": 1
},
{
"fieldname": "device_status",
"fieldtype": "Select",
"label": "Device Status",
"options": "\nUp\nDown"
},
{
"fieldname": "maintenance_details_section",
"fieldtype": "Section Break",
"label": "Maintenance Details"
},
{
"fieldname": "start_date",
"fieldtype": "Date",
"label": "Start Date",
"reqd": 1
},
{
"fieldname": "maintenance_team",
"fieldtype": "Link",
"label": "Maintenance Team",
"mandatory_depends_on": "eval:doc.maintenance_entries && doc.maintenance_entries.length > 0",
"options": "Asset Maintenance Team"
},
{
"fetch_from": "maintenance_team.maintenance_manager",
"fetch_if_empty": 1,
"fieldname": "maintenance_manager",
"fieldtype": "Data",
"label": "Maintenance Manager"
},
{
"fieldname": "column_break_kapm",
"fieldtype": "Column Break"
},
{
"fieldname": "end_date",
"fieldtype": "Date",
"label": "End Date",
"reqd": 1
},
{
"fieldname": "periodicity",
"fieldtype": "Select",
"label": "Periodicity",
"mandatory_depends_on": "eval:doc.maintenance_entries && doc.maintenance_entries.length > 0",
"options": "\nDaily\nWeekly\nMonthly\nQuarterly\nHalf-yearly\nYearly\n2 Yearly\n3 Yearly",
"reqd": 1
},
{
"fieldname": "assign_to",
"fieldtype": "Link",
"label": "Assign To",
"mandatory_depends_on": "eval:doc.maintenance_entries && doc.maintenance_entries.length > 0",
"options": "User"
},
{
"fieldname": "due_date",
"fieldtype": "Date",
"label": "First PM Date",
"read_only": 1
},
{
"fieldname": "section_break_hkrb",
"fieldtype": "Section Break"
},
{
"fieldname": "maintenance_entries",
"fieldtype": "Table",
"label": "Maintenance Entries",
"options": "PM Entry Line"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "PM Schedule Generator",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "PM Schedule Generator",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "modality",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Modality",
"options": "Modality"
},
{
"allow_on_submit": 1,
"fieldname": "pm_for",
"fieldtype": "Data",
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "PM Name",
"reqd": 1
},
{
"fieldname": "no_of_pms",
"fieldtype": "Data",
"label": "No of PMs"
},
{
"allow_on_submit": 1,
"fieldname": "next_pm_date",
"fieldtype": "Date",
"label": "Next PM Date"
}
],
"index_web_pages_for_search": 1,
"is_calendar_and_gantt": 1,
"is_submittable": 1,
"links": [
{
"link_doctype": "Asset Maintenance",
"link_fieldname": "custom_pm_schedule"
}
],
"modified": "2025-12-31 20:40:50.189002",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PM Schedule Generator",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PMScheduleGenerator(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2025, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPMScheduleGenerator(FrappeTestCase):
pass

View File

@ -0,0 +1,32 @@
// Copyright (c) 2024, seyfert and contributors
// For license information, please see license.txt
frappe.ui.form.on('PPM', {
refresh: function(frm) {
// Hide the default print icon
frm.page.hide_icon_group('print');
// Add custom button for PPM Sticker with a print icon
frm.add_custom_button(
`<i class="fa fa-print"></i> ${__('PPM Sticker')}`,
function() {
// Set PPM Sticker as the default print format and open print preview
const customLink = `/printview?doctype=PPM&name=${frm.doc.name}&trigger_print=1&format=PPM%20Sticker&no_letterhead=0`;
window.open(customLink);
}
);
// Add custom button for PPM Service Report with a print icon
frm.add_custom_button(
`<i class="fa fa-print"></i> ${__('Service Report')}`,
function() {
// Set Service Report as the default print format and open print preview
const customLink = `/printview?doctype=PPM&name=${frm.doc.name}&trigger_print=1&format=PPM%20Service%20Report&no_letterhead=0`;
window.open(customLink);
}
);
}
});

View File

@ -0,0 +1,112 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:PPM-{data}-{####}",
"creation": "2024-09-23 17:32:15.127002",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"month",
"year",
"column_break_phdw",
"asset",
"asset_name",
"asset_maintenance_log",
"section_break_3",
"data",
"table",
"amended_from"
],
"fields": [
{
"fieldname": "month",
"fieldtype": "Select",
"label": "Month",
"options": "\nJanuary\nFebruary\nMarch\nApril\nMay\nJune\nJuly\nAugust\nSeptember\nOctober\nNovember\nDecember"
},
{
"fieldname": "year",
"fieldtype": "Data",
"label": "Year"
},
{
"fieldname": "column_break_phdw",
"fieldtype": "Column Break"
},
{
"fieldname": "asset_maintenance_log",
"fieldtype": "Link",
"label": "Asset Maintenance Log",
"options": "Asset Maintenance Log",
"read_only": 1
},
{
"fieldname": "section_break_3",
"fieldtype": "Section Break"
},
{
"default": "CT Scan",
"fieldname": "data",
"fieldtype": "Link",
"label": "Template",
"options": "PPM Templates"
},
{
"fieldname": "table",
"fieldtype": "Table",
"options": "PPM Table"
},
{
"fetch_from": "asset_maintenance_log.asset_name",
"fieldname": "asset",
"fieldtype": "Link",
"label": "Asset",
"options": "Asset"
},
{
"fetch_from": "asset.asset_name",
"fieldname": "asset_name",
"fieldtype": "Data",
"label": "Asset Name",
"read_only": 1
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "PPM",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2024-09-23 18:16:03.998515",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PPM",
"naming_rule": "Expression",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PPM(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPPM(FrappeTestCase):
pass

View File

@ -0,0 +1,8 @@
// Copyright (c) 2024, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("PPM CT SCAN MACHINE TEMPLATE", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,43 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2024-09-13 15:46:54.680689",
"default_view": "List",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"table_1"
],
"fields": [
{
"fieldname": "table_1",
"fieldtype": "Table",
"options": "PPM Table For CT Scan Machine"
}
],
"index_web_pages_for_search": 1,
"links": [],
"modified": "2024-09-13 17:45:05.011562",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PPM CT SCAN MACHINE TEMPLATE",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PPMCTSCANMACHINETEMPLATE(Document):
pass

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestPPMCTSCANMACHINETEMPLATE(FrappeTestCase):
pass

View File

@ -0,0 +1,78 @@
{
"actions": [],
"autoname": "autoincrement",
"creation": "2024-09-13 09:25:03.360604",
"doctype": "DocType",
"editable_grid": 1,
"engine": "InnoDB",
"field_order": [
"room_numbers",
"electrical_switches",
"electrical_sockets_outlets",
"fluorescent_tubes",
"column_break_5",
"ballasts",
"starters",
"glass_cover_of_tube_lights"
],
"fields": [
{
"fieldname": "room_numbers",
"fieldtype": "Data",
"label": "Room Numbers"
},
{
"default": "0",
"fieldname": "electrical_switches",
"fieldtype": "Check",
"label": "Electrical switches"
},
{
"default": "0",
"fieldname": "electrical_sockets_outlets",
"fieldtype": "Check",
"label": "Electrical sockets/outlets"
},
{
"default": "0",
"fieldname": "fluorescent_tubes",
"fieldtype": "Check",
"label": "Fluorescent tubes"
},
{
"fieldname": "column_break_5",
"fieldtype": "Column Break"
},
{
"default": "0",
"fieldname": "ballasts",
"fieldtype": "Check",
"label": "Ballasts"
},
{
"default": "0",
"fieldname": "starters",
"fieldtype": "Check",
"label": "Starters"
},
{
"default": "0",
"fieldname": "glass_cover_of_tube_lights",
"fieldtype": "Check",
"label": "Glass cover of tube lights"
}
],
"index_web_pages_for_search": 1,
"istable": 1,
"links": [],
"modified": "2024-09-13 17:45:12.282804",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "PPM Electrical Fixtures Inside Rooms Table",
"naming_rule": "Autoincrement",
"owner": "Administrator",
"permissions": [],
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

@ -0,0 +1,9 @@
# Copyright (c) 2024, seyfert and contributors
# For license information, please see license.txt
# import frappe
from frappe.model.document import Document
class PPMElectricalFixturesInsideRoomsTable(Document):
pass

Some files were not shown because too many files have changed in this diff Show More