Initial commit of Asset Lite app

This commit is contained in:
Duradundi Hadimani 2026-03-23 17:34:51 +05:30
commit 9c41cbaf5b
337 changed files with 162175 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

1122
asset_lite/api/asset_api.py Normal file

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,712 @@
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)
# 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,163 @@
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
}
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,538 @@
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():
"""
Returns counts for Number Cards:
- total_assets
- work_orders_open
- work_orders_in_progress
- work_orders_completed
"""
try:
total_assets = frappe.db.count("Asset")
work_orders_open = frappe.db.count("Work Order", {"status": ["in", ["Not Started", "Open", "Pending"]]})
work_orders_in_progress = frappe.db.count("Work Order", {"status": ["in", ["In Process", "In Progress", "Started"]]})
work_orders_completed = frappe.db.count("Work Order", {"status": ["in", ["Completed", "Closed", "Finished"]]})
_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,
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_number_cards")
_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).
Supports:
- Report-based charts (with or without use_report_chart)
- Custom/Document-type based charts
- Multi-series (stacked) bar charts
"""
try:
if isinstance(report_filters, str):
report_filters = json.loads(report_filters or "{}")
report_filters = report_filters or {}
# Try to find chart by name first, then by chart_name
chart = None
if frappe.db.exists("Dashboard Chart", chart_name):
chart = frappe.get_doc("Dashboard Chart", chart_name)
else:
# Search by chart_name field
chart_doc_name = frappe.db.get_value("Dashboard Chart", {"chart_name": chart_name}, "name")
if chart_doc_name:
chart = frappe.get_doc("Dashboard Chart", chart_doc_name)
else:
# Try case-insensitive search
chart_doc_name = frappe.db.sql("""
SELECT name FROM `tabDashboard Chart`
WHERE LOWER(chart_name) = LOWER(%s) OR LOWER(name) = LOWER(%s)
LIMIT 1
""", (chart_name, chart_name), as_dict=True)
if chart_doc_name:
chart = frappe.get_doc("Dashboard Chart", chart_doc_name[0].name)
if not chart:
_err(f"Dashboard Chart '{chart_name}' not found. Use list_dashboard_charts API to see available charts.")
return
# Get y-axes configuration
y_axes = frappe.get_all(
"Dashboard Chart Field",
filters={"parenttype": "Dashboard Chart", "parent": chart.name},
fields=["y_field", "color"],
order_by="idx asc",
)
# PRIORITY 1: Handle Report-based charts (check report_name first)
if chart.report_name:
return _process_report_chart(chart, y_axes, report_filters)
# PRIORITY 2: Handle Custom/Document-type charts (only if NO report_name)
if chart.chart_type == "Custom" or (chart.document_type and chart.based_on):
return _process_custom_chart(chart, y_axes)
# PRIORITY 3: Handle Group By / Count / Sum charts
if chart.chart_type in ("Group By", "Count", "Sum"):
return _process_group_by_chart(chart)
_err(f"Unsupported chart type: {chart.chart_type} for chart: {chart.name}")
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_dashboard_chart_data")
_err(str(e))
def _process_report_chart(chart, y_axes, report_filters):
"""
Process a Report-based Dashboard Chart.
Runs the report and extracts data based on x_field and y_axes configuration.
"""
try:
# Merge chart's default filters with provided filters
chart_filters = _parse_custom_options(chart.filters_json) if chart.filters_json else {}
merged_filters = {**chart_filters, **report_filters}
# Run the report
run = frappe.get_attr("frappe.desk.query_report.run")
report_result = run(chart.report_name, filters=merged_filters)
rows = report_result.get("result", []) or []
# Filter out total rows
data_rows = [r for r in rows if not (isinstance(r, dict) and r.get("is_total_row"))]
# Get x-axis labels
x_key = chart.x_field
labels = []
for r in data_rows:
if isinstance(r, dict):
val = r.get(x_key)
if val is not None:
labels.append(str(val))
# Build datasets from y-axes
datasets = []
# Color palette for multi-series charts
default_colors = [
"#6366F1", # Indigo
"#10B981", # Green
"#3B82F6", # Blue
"#F59E0B", # Amber
"#EC4899", # Pink
"#8B5CF6", # Purple
"#06B6D4", # Cyan
"#EF4444", # Red
]
for idx, y in enumerate(y_axes):
y_field = y.get("y_field")
series_name = y_field # Use field name as series name
values = []
for r in data_rows:
if isinstance(r, dict):
val = r.get(y_field)
try:
values.append(float(val) if val is not None else 0)
except (ValueError, TypeError):
values.append(0)
# Use provided color or default from palette
color = y.get("color") or default_colors[idx % len(default_colors)]
datasets.append({
"name": series_name,
"values": values,
"color": color
})
chart_type = (chart.type or "Bar").title()
custom_options = _parse_custom_options(chart.custom_options)
# Handle Pie charts (only use first dataset)
if chart_type.lower() == "pie":
ds = datasets[0] if datasets else {"name": "value", "values": []}
_ok({
"labels": labels,
"datasets": [ds],
"type": "Pie",
"options": custom_options,
"source": {"report": chart.report_name},
})
return
# Return data for Bar/Line charts (supports multi-series/stacked)
_ok({
"labels": labels,
"datasets": datasets,
"type": chart_type,
"options": custom_options,
"source": {"report": chart.report_name},
# Include result for frontend transformation if needed
"result": data_rows if len(y_axes) > 1 else None,
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), f"Report Chart Error: {chart.name}")
_err(f"Error processing report chart: {str(e)}")
def _process_custom_chart(chart, y_axes):
"""
Process a Custom/Document-type based Dashboard Chart.
Queries the source doctype directly based on configuration.
"""
try:
source = chart.document_type
based_on = chart.based_on
value_based_on = chart.value_based_on or "name"
if not source or not based_on:
_err(f"Custom chart '{chart.name}' missing document_type or based_on configuration")
return
# Build aggregation query
if chart.type and chart.type.lower() == "pie":
# Pie chart: 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") or "Unknown") 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") or "Unknown") 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)}")
def _process_group_by_chart(chart):
"""
Process a Group By / Count / Sum type Dashboard Chart.
These charts aggregate data from a doctype without using a report.
"""
try:
source = chart.document_type
if not source:
_err(f"Group By chart '{chart.name}' missing document_type")
return
group_by_field = chart.group_by_based_on or chart.based_on
aggregate_function = chart.chart_type # Count, Sum, etc.
value_field = chart.aggregate_function_based_on or "name"
if not group_by_field:
_err(f"Group By chart '{chart.name}' missing group_by field")
return
# Build query based on aggregate function
if aggregate_function == "Count":
query = f"""
SELECT `{group_by_field}` as label, COUNT(*) as value
FROM `tab{source}`
WHERE `{group_by_field}` IS NOT NULL
GROUP BY `{group_by_field}`
ORDER BY value DESC
LIMIT 20
"""
elif aggregate_function == "Sum":
query = f"""
SELECT `{group_by_field}` as label, SUM(`{value_field}`) as value
FROM `tab{source}`
WHERE `{group_by_field}` IS NOT NULL
GROUP BY `{group_by_field}`
ORDER BY value DESC
LIMIT 20
"""
else:
# Default to count
query = f"""
SELECT `{group_by_field}` as label, COUNT(*) as value
FROM `tab{source}`
WHERE `{group_by_field}` IS NOT NULL
GROUP BY `{group_by_field}`
ORDER BY value DESC
LIMIT 20
"""
data = frappe.db.sql(query, as_dict=True)
labels = [str(d.get("label") or "Unknown") for d in data]
values = [float(d.get("value") or 0) for d in data]
chart_type = (chart.type or "Bar").title()
_ok({
"labels": labels,
"datasets": [{"name": aggregate_function.lower(), "values": values, "color": "#6366F1"}],
"type": chart_type,
"options": _parse_custom_options(chart.custom_options),
"source": {"doctype": source},
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), f"Group By Chart Error: {chart.name}")
_err(f"Error processing group by chart: {str(e)}")
def _parse_custom_options(raw):
"""Parse custom_options JSON string to dict."""
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))
@frappe.whitelist(allow_guest=True)
def get_technician_working_hours(filters=None):
"""
Dedicated endpoint for Technician Working Hours chart.
Returns hours worked by each technician.
"""
try:
if isinstance(filters, str):
filters = json.loads(filters or "{}")
filters = filters or {}
# Run the report
run = frappe.get_attr("frappe.desk.query_report.run")
report_result = run("Technicians working Hours", filters=filters)
rows = report_result.get("result", []) or []
# Filter out total rows
data_rows = [r for r in rows if isinstance(r, dict) and not r.get("is_total_row")]
labels = [str(r.get("technician_name") or "Unknown") for r in data_rows]
values = [float(r.get("total_hours") or 0) for r in data_rows]
_ok({
"labels": labels,
"datasets": [{
"name": "total_hours",
"values": values,
"color": "#6366F1"
}],
"type": "Bar",
"options": {"barOptions": {"stacked": False}},
"result": data_rows,
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_technician_working_hours")
_err(str(e))
@frappe.whitelist(allow_guest=True)
def get_technician_work_summary(filters=None):
"""
Dedicated endpoint for Technician Work Order Summary chart.
Returns work order counts (total, completed, in_progress, open) by technician.
"""
try:
if isinstance(filters, str):
filters = json.loads(filters or "{}")
filters = filters or {}
# Run the report
run = frappe.get_attr("frappe.desk.query_report.run")
report_result = run("Technician Work Order Summary", filters=filters)
rows = report_result.get("result", []) or []
# Filter out total rows
data_rows = [r for r in rows if isinstance(r, dict) and not r.get("is_total_row")]
labels = [str(r.get("assigned_technician") or "Unknown") for r in data_rows]
# Build multi-series datasets
datasets = [
{
"name": "total",
"values": [float(r.get("total") or 0) for r in data_rows],
"color": "#6366F1" # Indigo
},
{
"name": "completed",
"values": [float(r.get("completed") or 0) for r in data_rows],
"color": "#10B981" # Green
},
{
"name": "in_progress",
"values": [float(r.get("in_progress") or 0) for r in data_rows],
"color": "#3B82F6" # Blue
},
{
"name": "open",
"values": [float(r.get("open") or 0) for r in data_rows],
"color": "#F59E0B" # Amber
}
]
# Filter out datasets with all zeros
datasets = [ds for ds in datasets if any(v > 0 for v in ds["values"])]
_ok({
"labels": labels,
"datasets": datasets,
"type": "Bar",
"options": {"barOptions": {"stacked": True}},
"result": data_rows,
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "get_technician_work_summary")
_err(str(e))
@frappe.whitelist(allow_guest=True)
def debug_list_charts():
"""
Debug endpoint to list all Dashboard Chart names.
Use this to verify exact chart names in the database.
"""
try:
charts = frappe.db.sql("""
SELECT name, chart_name, chart_type, report_name, is_public, type
FROM `tabDashboard Chart`
ORDER BY modified DESC
""", as_dict=True)
_ok({
"total": len(charts),
"charts": charts
})
except Exception as e:
frappe.log_error(frappe.get_traceback(), "debug_list_charts")
_err(str(e))

View File

@ -0,0 +1,128 @@
import frappe
from frappe import _
def _cancel_and_delete(doctype: str, name: str) -> None:
doc = frappe.get_doc(doctype, name)
doc.flags.ignore_permissions = True
doc.flags.ignore_links = True
if doc.docstatus == 1:
doc.cancel()
frappe.db.commit()
frappe.delete_doc(doctype, name, force=True, ignore_permissions=True, ignore_on_trash=True)
frappe.db.commit()
def _safe_collect(fn, *args) -> dict:
results = {"deleted": [], "errors": []}
try:
results["deleted"] = fn(*args)
except Exception as e:
results["errors"].append(str(e))
return results
def _del_asset_activities(asset_name: str) -> list:
deleted = []
for name in frappe.get_all("Asset Activity", filters={"asset": asset_name}, pluck="name"):
try:
frappe.delete_doc("Asset Activity", name, force=True, ignore_permissions=True, ignore_on_trash=True)
frappe.db.commit()
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Activity delete error [{name}]: {e}")
return deleted
def _del_asset_movements(asset_name: str) -> list:
deleted = []
parent_names = list(set(
frappe.get_all("Asset Movement Item", filters={"asset": asset_name}, pluck="parent")
))
for name in parent_names:
try:
_cancel_and_delete("Asset Movement", name)
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Movement delete error [{name}]: {e}")
return deleted
def _del_asset_value_adjustments(asset_name: str) -> list:
deleted = []
for name in frappe.get_all("Asset Value Adjustment", filters={"asset": asset_name}, pluck="name"):
try:
_cancel_and_delete("Asset Value Adjustment", name)
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Value Adjustment delete error [{name}]: {e}")
return deleted
def _del_asset_depreciation_schedules(asset_name: str) -> list:
deleted = []
for name in frappe.get_all("Asset Depreciation Schedule", filters={"asset": asset_name}, pluck="name"):
try:
_cancel_and_delete("Asset Depreciation Schedule", name)
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Depreciation Schedule delete error [{name}]: {e}")
return deleted
def _del_asset_maintenance(asset_name: str) -> list:
deleted = []
for name in frappe.get_all("Asset Maintenance", filters={"asset_name": asset_name}, pluck="name"):
try:
_cancel_and_delete("Asset Maintenance", name)
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Maintenance delete error [{name}]: {e}")
return deleted
def _del_asset_repair(asset_name: str) -> list:
deleted = []
for name in frappe.get_all("Asset Repair", filters={"asset_name": asset_name}, pluck="name"):
try:
_cancel_and_delete("Asset Repair", name)
deleted.append(name)
except Exception as e:
frappe.log_error(f"Asset Repair delete error [{name}]: {e}")
return deleted
@frappe.whitelist()
def delete_asset(asset_name: str) -> dict:
if not frappe.db.exists("Asset", asset_name):
frappe.throw(_("Asset {0} does not exist.").format(asset_name), frappe.DoesNotExistError)
if not frappe.has_permission("Asset", "delete", asset_name):
frappe.throw(_("You do not have permission to delete Asset {0}.").format(asset_name), frappe.PermissionError)
details = {}
try:
details["asset_activities"] = _safe_collect(_del_asset_activities, asset_name)
details["asset_movements"] = _safe_collect(_del_asset_movements, asset_name)
details["asset_value_adjustments"] = _safe_collect(_del_asset_value_adjustments, asset_name)
details["asset_depreciation_schedules"] = _safe_collect(_del_asset_depreciation_schedules, asset_name)
details["asset_maintenance"] = _safe_collect(_del_asset_maintenance, asset_name)
# details["asset_repair"] = _safe_collect(_del_asset_repair, asset_name)
asset_doc = frappe.get_doc("Asset", asset_name)
asset_doc.flags.ignore_permissions = True
asset_doc.flags.ignore_links = True
if asset_doc.docstatus == 1:
asset_doc.cancel()
frappe.db.commit()
frappe.delete_doc("Asset", asset_name, force=True, ignore_permissions=True, ignore_on_trash=True)
frappe.db.commit()
return {"success": True, "asset_name": asset_name, "details": details}
except Exception as exc:
frappe.db.rollback()
frappe.log_error(title=f"Asset deletion failed: {asset_name}", message=frappe.get_traceback())
frappe.throw(_("Failed to delete asset {0}: {1}").format(asset_name, str(exc)))

View File

@ -0,0 +1,29 @@
import frappe
@frappe.whitelist()
def create_delete_request(target_doctype, target_name, target_display, reason, department):
"""Create a deletion request for any doctype"""
doc = frappe.new_doc("Delete Request")
doc.target_doctype = target_doctype
doc.target_name = target_name
doc.target_display = target_display or target_name
doc.reason = reason or ""
doc.department = department or ""
doc.requested_by = frappe.session.user
doc.status = "Pending"
doc.workflow_state = "Pending Supervisor"
doc.insert(ignore_permissions=False)
return {"name": doc.name, "status": "created"}
@frappe.whitelist()
def get_pending_requests(doctype_filter=None):
"""Get pending requests for current approver"""
filters = {"workflow_state": ["in", ["Pending Supervisor", "Pending Cluster Manager"]]}
if doctype_filter:
filters["target_doctype"] = doctype_filter
return frappe.get_list(
"Delete Request",
filters=filters,
fields=["name", "target_doctype", "target_name", "target_display",
"reason", "department", "requested_by", "workflow_state", "creation"]
)

View File

@ -0,0 +1,107 @@
# asset_lite/api/doctype_fields.py
#
# WHY THIS EXISTS:
# Frappe's /api/resource/DocType and /api/resource/Custom Field endpoints are
# role-filtered — they silently omit fields the current user's role cannot read.
# This causes the export modal to show different field counts per role (6 for
# Contractor Engineer, 45 for admin, etc.).
#
# This whitelisted function runs as the SYSTEM user (via frappe.get_doc /
# frappe.get_all which bypass field-level permission checks), so it always
# returns the complete field list regardless of who is logged in.
# The React hook calls this endpoint instead of the meta APIs.
import frappe
from frappe import _
# Fields that are never useful in an export UI
SKIP_FIELDTYPES = {
"Section Break", "Column Break", "Tab Break",
"HTML", "Button", "Fold", "Heading",
"Image", "Signature", "Geolocation", "Barcode",
"Table", "Table MultiSelect",
}
SKIP_FIELDNAMES = {
"docstatus", "idx", "naming_series", "amended_from",
"amendment_date", "lft", "rgt", "old_parent",
}
# Always include these meta fields first
META_FIELDS = [
{"fieldname": "name", "label": "ID", "fieldtype": "Data"},
{"fieldname": "owner", "label": "Created By", "fieldtype": "Data"},
{"fieldname": "creation", "label": "Created On", "fieldtype": "Datetime"},
{"fieldname": "modified_by", "label": "Modified By", "fieldtype": "Data"},
{"fieldname": "modified", "label": "Modified On", "fieldtype": "Datetime"},
]
@frappe.whitelist()
def get_export_fields(doctype: str) -> list:
"""
Returns the complete list of exportable fields for a DocType.
Ignores field-level role permissions intentionally this endpoint
is for the export column picker UI only, not for data access control.
Data access is still enforced when the actual export fetch runs.
Returns a list of dicts: { fieldname, label, fieldtype }
"""
# Basic validation — ensure the doctype itself exists and user can read it
if not frappe.db.exists("DocType", doctype):
frappe.throw(_("DocType {0} does not exist").format(doctype), frappe.DoesNotExistError)
# Check the user can at least read this doctype (doc-level permission, not field-level)
if not frappe.has_permission(doctype, "read"):
frappe.throw(_("No read permission for {0}").format(doctype), frappe.PermissionError)
meta_fieldnames = {f["fieldname"] for f in META_FIELDS}
seen = set(meta_fieldnames)
result = []
# ── Standard DocType fields ──────────────────────────────────────────────
# frappe.get_meta() loads DocType metadata as SYSTEM — not role-filtered
meta = frappe.get_meta(doctype)
for f in meta.fields:
if f.fieldtype in SKIP_FIELDTYPES:
continue
if f.fieldname in SKIP_FIELDNAMES:
continue
if f.hidden:
continue
if f.fieldname in seen:
continue
seen.add(f.fieldname)
result.append({
"fieldname": f.fieldname,
"label": f.label or f.fieldname,
"fieldtype": f.fieldtype or "Data",
})
# ── Custom fields ────────────────────────────────────────────────────────
# frappe.get_all() with ignore_permissions=True bypasses role filtering
custom_fields = frappe.get_all(
"Custom Field",
filters={"dt": doctype, "hidden": 0},
fields=["fieldname", "label", "fieldtype"],
ignore_permissions=True, # <-- KEY: bypasses role-based field restrictions
limit_page_length=500,
)
for f in custom_fields:
if f.fieldtype in SKIP_FIELDTYPES:
continue
if f.fieldname in SKIP_FIELDNAMES:
continue
if f.fieldname in seen:
continue
seen.add(f.fieldname)
result.append({
"fieldname": f["fieldname"],
"label": f["label"] or f["fieldname"],
"fieldtype": f["fieldtype"] or "Data",
})
# Return meta fields first, then the rest
return META_FIELDS + result

View File

@ -0,0 +1,46 @@
import frappe
import json
@frappe.whitelist()
def get_items(filters=None, fields=None, limit=20, offset=0, order_by="creation desc"):
import json
if isinstance(filters, str):
filters = json.loads(filters)
if isinstance(fields, str):
fields = json.loads(fields)
if isinstance(limit, str):
limit = int(limit)
if isinstance(offset, str):
offset = int(offset)
if not fields:
fields = ["name", "item_code", "item_name", "item_group", "stock_uom",
"custom_hospital_name", "custom_serial_no", "custom_date_in",
"custom_code", "custom_type", "custom_volts", "custom_w",
"custom_delete_status", "creation", "modified", "owner", "docstatus",
"custom_technical_department", "disabled", "is_stock_item"]
data = frappe.get_all(
"Item",
filters=filters or [],
fields=fields,
limit_start=offset,
limit_page_length=limit,
order_by=order_by,
ignore_permissions=False
)
total_count = len(frappe.get_all(
"Item",
filters=filters or [],
fields=["name"],
limit_page_length=0, # 0 = no limit = all
ignore_permissions=False
))
return {
"data": data,
"total": total_count,
"has_more": (offset + limit) < total_count
}

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,214 @@
import frappe
import json
@frappe.whitelist()
def get_filtered_rooms(txt='', doctype='Room', searchfield='name', start=0, page_len=50, filters=None):
"""
Custom query for Room LinkField that filters based on Infrastructure Location mapping.
Returns rooms that are mapped to the specified building/department in Infrastructure Location.
"""
# Parse filters if string
if isinstance(filters, str):
try:
filters = json.loads(filters)
except:
filters = {}
if not filters:
filters = {}
building = filters.get('building', '')
department = filters.get('department', '')
txt = txt or ''
start = int(start)
page_len = int(page_len)
# If building or department is specified, filter via Infrastructure Location
if building or department:
infra_conditions = []
values = {
'txt': f'%{txt}%',
'start': start,
'page_len': page_len
}
if building:
infra_conditions.append("il.building = %(building)s")
values['building'] = building
if department:
infra_conditions.append("il.department = %(department)s")
values['department'] = department
infra_where = " AND ".join(infra_conditions)
# Query rooms that exist in Infrastructure Location with given filters
query = f"""
SELECT DISTINCT r.name, r.name as description
FROM `tabRoom` r
INNER JOIN `tabInfrastructure Location` il ON il.room_no = r.name
WHERE ({infra_where})
AND r.name LIKE %(txt)s
ORDER BY r.name
LIMIT %(start)s, %(page_len)s
"""
results = frappe.db.sql(query, values, as_list=True)
else:
# No building/department filter - return all rooms matching search text
results = frappe.db.sql("""
SELECT name, name as description
FROM `tabRoom`
WHERE name LIKE %(txt)s
ORDER BY name
LIMIT %(start)s, %(page_len)s
""", {
'txt': f'%{txt}%',
'start': start,
'page_len': page_len
}, as_list=True)
return results
@frappe.whitelist()
def get_filtered_departments(txt='', doctype='Department', searchfield='name', start=0, page_len=50, filters=None):
"""
Custom query for Department LinkField that filters based on Infrastructure Location mapping.
Returns departments that are mapped to the specified building in Infrastructure Location.
"""
# Parse filters if string
if isinstance(filters, str):
try:
filters = json.loads(filters)
except:
filters = {}
if not filters:
filters = {}
building = filters.get('building', '')
company = filters.get('company', '')
txt = txt or ''
start = int(start)
page_len = int(page_len)
values = {
'txt': f'%{txt}%',
'start': start,
'page_len': page_len
}
# If building is specified, filter via Infrastructure Location
if building:
query = """
SELECT DISTINCT d.name, d.name as description
FROM `tabDepartment` d
INNER JOIN `tabInfrastructure Location` il ON il.department = d.name
WHERE il.building = %(building)s
AND d.name LIKE %(txt)s
"""
values['building'] = building
# Add company filter if provided
if company:
query += " AND d.company = %(company)s"
values['company'] = company
query += """
ORDER BY d.name
LIMIT %(start)s, %(page_len)s
"""
results = frappe.db.sql(query, values, as_list=True)
else:
# No building filter - return departments with optional company filter
if company:
results = frappe.db.sql("""
SELECT name, name as description
FROM `tabDepartment`
WHERE name LIKE %(txt)s
AND company = %(company)s
ORDER BY name
LIMIT %(start)s, %(page_len)s
""", {
'txt': f'%{txt}%',
'company': company,
'start': start,
'page_len': page_len
}, as_list=True)
else:
results = frappe.db.sql("""
SELECT name, name as description
FROM `tabDepartment`
WHERE name LIKE %(txt)s
ORDER BY name
LIMIT %(start)s, %(page_len)s
""", {
'txt': f'%{txt}%',
'start': start,
'page_len': page_len
}, as_list=True)
return results
@frappe.whitelist()
def get_room_count(building=None, department=None):
"""
Get count of rooms for a building/department combination from Infrastructure Location.
Used to show "X room(s) available" message.
"""
if not building and not department:
return frappe.db.count('Room')
conditions = []
values = {}
if building:
conditions.append("building = %(building)s")
values['building'] = building
if department:
conditions.append("department = %(department)s")
values['department'] = department
where_clause = " AND ".join(conditions)
count = frappe.db.sql(f"""
SELECT COUNT(DISTINCT room_no)
FROM `tabInfrastructure Location`
WHERE {where_clause}
AND room_no IS NOT NULL
AND room_no != ''
""", values)[0][0]
return count or 0
@frappe.whitelist()
def get_department_count(building=None, company=None):
"""
Get count of departments for a building from Infrastructure Location.
Used to show "X department(s) available" message.
"""
if not building:
if company:
return frappe.db.count('Department', {'company': company})
return frappe.db.count('Department')
values = {'building': building}
query = """
SELECT COUNT(DISTINCT department)
FROM `tabInfrastructure Location`
WHERE building = %(building)s
AND department IS NOT NULL
AND department != ''
"""
count = frappe.db.sql(query, values)[0][0]
return count or 0

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,152 @@
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
@frappe.whitelist()
def has_create_permission(doctype):
"""
Check if current user has create permission for a doctype
Uses ignore_permissions to query Custom DocPerm
"""
user = frappe.session.user
if not user or user == "Guest":
return {"has_permission": False, "reason": "Not logged in"}
# Get user's roles
user_roles = frappe.get_roles(user)
# System Manager and Administrator always have permission
if "System Manager" in user_roles or "Administrator" in user_roles:
return {
"has_permission": True,
"reason": "System Manager/Administrator",
"role": "System Manager"
}
# Check Custom DocPerm with ignore_permissions=True
custom_perms = frappe.get_all(
"Custom DocPerm",
filters={
"parent": doctype,
"role": ["in", user_roles],
"create": 1
},
fields=["name", "role"],
ignore_permissions=True, # ← This is the key!
limit=1
)
if custom_perms and len(custom_perms) > 0:
return {
"has_permission": True,
"reason": "Custom DocPerm",
"role": custom_perms[0].get("role")
}
return {
"has_permission": False,
"reason": "No create permission found in Custom DocPerm"
}

View File

@ -0,0 +1,310 @@
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",
"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",
"Location": "location",
"Department": "department",
"Issue Type": "work_order_type"
},
"Asset Maintenance": {
"Company": "company",
"Asset": "asset_name",
"Supplier": "supplier"
},
"Asset Maintenance Log": {
"Company": "company",
"Asset": "asset_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
# ============================================================================
# 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,940 @@
import frappe
from frappe import _
# Default fields for Work_Order doctype
WORK_ORDER_FIELDS = [
'name',
'owner',
'creation',
'modified',
'modified_by',
'docstatus',
'idx',
'workflow_state',
'company',
'naming_series',
'work_order_type',
'asset_type',
'manufacturer',
'serial_number',
'custom_priority_',
'asset',
'custom_maintenance_manager',
'department',
'repair_status',
'asset_name',
'supplier',
'custom_pending_reason',
'make',
'model',
'custom_site_contractor',
'custom_subcontractor',
'custom_service_agreement',
'custom_service_coverage',
'custom_start_date',
'custom_end_date',
'custom_total_amount',
'warranty',
'service_contract',
'covering_spare_parts',
'spare_parts_labour',
'covering_labour',
'ppm_only',
'failure_date',
'total_hours_spent',
'job_completed',
'custom_difference',
'custom_vendors_hrs',
'custom_deadline_date',
'custom_diffrence',
'feedback_rating',
'first_responded_on',
'assigned_manager',
'penalty',
'custom_assigned_supervisor',
'stock_consumption',
'need_procurement',
'repair_cost',
'total_repair_cost',
'capitalize_repair_cost',
'increase_in_asset_life',
'description',
'actions_performed',
'end_user',
'bio_med_dept'
]
# Child table: Asset Repair Consumed Item (stock_items)
STOCK_ITEMS_FIELDS = [
'name',
'owner',
'creation',
'modified',
'modified_by',
'docstatus',
'idx',
'parent',
'parentfield',
'parenttype',
'item_code',
'warehouse',
'valuation_rate',
'total_value',
'custom_available_stock'
]
# Child table: Invoice Table
INVOICE_TABLE_FIELDS = [
'name',
'owner',
'creation',
'modified',
'modified_by',
'docstatus',
'idx',
'parent',
'parentfield',
'parenttype',
'invoice_number',
'invoice_date',
'invoice_amount',
'vendor',
'description'
]
# Child table: CMQP Table
TABLE_CMQP_FIELDS = [
'name',
'owner',
'creation',
'modified',
'modified_by',
'docstatus',
'idx',
'parent',
'parentfield',
'parenttype',
'parameter',
'value',
'status',
'remarks'
]
def get_child_table_data(parent_name, parentfield, child_doctype, fields=None):
"""
Get child table data for a work order
Args:
parent_name: Name of the parent work order
parentfield: Field name of the child table in parent
child_doctype: Doctype of the child table
fields: List of fields to return
Returns:
List of child table records
"""
try:
return frappe.get_all(
child_doctype,
filters={
'parent': parent_name,
'parentfield': parentfield,
'parenttype': 'Work_Order'
},
fields=fields or ['*'],
order_by='idx asc'
)
except Exception:
return []
@frappe.whitelist(allow_guest=True)
def get_work_orders(filters=None, fields=None, limit=20, offset=0, order_by=None, include_child_tables=False):
"""
Get list of work orders with filters and pagination
Args:
filters: JSON string of filters (e.g., '{"company": "ABC Corp"}')
fields: JSON string of fields to return (e.g., '["work_order_type", "asset_name"]')
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:
{
"work_orders": [...],
"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)
# Parse fields if provided
if fields and isinstance(fields, str):
fields = json.loads(fields)
else:
fields = WORK_ORDER_FIELDS.copy()
# Parse include_child_tables
if isinstance(include_child_tables, str):
include_child_tables = include_child_tables.lower() in ('true', '1', 'yes')
# Get total count
total_count = frappe.db.count('Work_Order', filters=filters or {})
# Get work orders
work_orders = frappe.get_all(
'Work_Order',
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:
for work_order in work_orders:
work_order['stock_items'] = get_child_table_data(
work_order['name'],
'stock_items',
'Asset Repair Consumed Item',
STOCK_ITEMS_FIELDS
)
work_order['invoice_table'] = get_child_table_data(
work_order['name'],
'invoice_table',
'PI Table', # Adjust doctype name as needed
INVOICE_TABLE_FIELDS
)
work_order['table_cmqp'] = get_child_table_data(
work_order['name'],
'table_cmqp',
'Spare Parts', # Adjust doctype name as needed
TABLE_CMQP_FIELDS
)
# Calculate has_more
has_more = (int(offset) + int(limit)) < total_count
frappe.response['message'] = {
'work_orders': work_orders,
'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 Work Orders API Error')
frappe.response['message'] = {
'error': str(e),
'work_orders': [],
'total_count': 0
}
@frappe.whitelist(allow_guest=True)
def get_work_order_details(work_order_name):
"""
Get detailed information about a specific work order
Args:
work_order_name: Name/ID of the work order
Returns:
Work Order document with all fields including child tables
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Check if user has permission to read this work order
if not frappe.has_permission('Work_Order', 'read', work_order_name):
frappe.throw(_('Not permitted to access this work order'))
# Get work order details
work_order = frappe.get_doc('Work_Order', work_order_name)
# Convert to dict and include child tables
work_order_dict = work_order.as_dict()
# Ensure child tables are included with all fields
work_order_dict['stock_items'] = [item.as_dict() for item in work_order.get('stock_items', [])]
work_order_dict['invoice_table'] = [item.as_dict() for item in work_order.get('invoice_table', [])]
work_order_dict['table_cmqp'] = [item.as_dict() for item in work_order.get('table_cmqp', [])]
frappe.response['message'] = work_order_dict
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Work Order Details API Error')
frappe.response['message'] = {
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def create_work_order(work_order_data):
"""
Create a new work order
Args:
work_order_data: JSON string containing work order fields including child tables
Example:
{
"company": "ABC Corp",
"work_order_type": "Repair (CM)",
"asset": "ASSET-001",
"stock_items": [
{"item_code": "ITEM-001", "warehouse": "Main Warehouse", "valuation_rate": 100}
],
"invoice_table": [...],
"table_cmqp": [...]
}
Returns:
Created work order document
"""
try:
import json
# Parse work order data
if isinstance(work_order_data, str):
work_order_data = json.loads(work_order_data)
# Check if user has permission to create work order
if not frappe.has_permission('Work_Order', 'create'):
frappe.throw(_('Not permitted to create work order'))
# Create new work order
work_order = frappe.get_doc({
'doctype': 'Work_Order',
**work_order_data
})
work_order.insert()
frappe.db.commit()
# Return created work order with child tables
work_order_dict = work_order.as_dict()
work_order_dict['stock_items'] = [item.as_dict() for item in work_order.get('stock_items', [])]
work_order_dict['invoice_table'] = [item.as_dict() for item in work_order.get('invoice_table', [])]
work_order_dict['table_cmqp'] = [item.as_dict() for item in work_order.get('table_cmqp', [])]
frappe.response['message'] = {
'success': True,
'work_order': work_order_dict,
'message': _('Work Order created successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Create Work Order API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def update_work_order(work_order_name, work_order_data):
"""
Update an existing work order including child tables
Args:
work_order_name: Name/ID of the work order
work_order_data: JSON string containing fields to update
Example:
{
"repair_status": "In Progress",
"stock_items": [
{"item_code": "ITEM-001", "warehouse": "Main Warehouse", "valuation_rate": 100}
]
}
Returns:
Updated work order document
"""
try:
import json
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Parse work order data
if isinstance(work_order_data, str):
work_order_data = json.loads(work_order_data)
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Handle child tables separately
child_tables = ['stock_items', 'invoice_table', 'table_cmqp']
for key, value in work_order_data.items():
if key in child_tables:
# Clear existing child table entries and add new ones
work_order.set(key, [])
for item in value:
work_order.append(key, item)
elif hasattr(work_order, key):
setattr(work_order, key, value)
work_order.save()
frappe.db.commit()
# Return updated work order with child tables
work_order_dict = work_order.as_dict()
work_order_dict['stock_items'] = [item.as_dict() for item in work_order.get('stock_items', [])]
work_order_dict['invoice_table'] = [item.as_dict() for item in work_order.get('invoice_table', [])]
work_order_dict['table_cmqp'] = [item.as_dict() for item in work_order.get('table_cmqp', [])]
frappe.response['message'] = {
'success': True,
'work_order': work_order_dict,
'message': _('Work Order updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Work Order API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def delete_work_order(work_order_name):
"""
Delete a work order
Args:
work_order_name: Name/ID of the work order
Returns:
Success message
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Check if user has permission to delete this work order
if not frappe.has_permission('Work_Order', 'delete', work_order_name):
frappe.throw(_('Not permitted to delete this work order'))
# Delete work order (child tables will be deleted automatically)
frappe.delete_doc('Work_Order', work_order_name)
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'message': _('Work Order deleted successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Delete Work Order API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def update_work_order_status(work_order_name, repair_status=None, workflow_state=None):
"""
Update work order status
Args:
work_order_name: Name/ID of the work order
repair_status: New repair status (e.g., 'Open', 'In Progress', 'Completed')
workflow_state: New workflow state
Returns:
Updated work order document
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Update status fields
if repair_status:
work_order.repair_status = repair_status
if workflow_state:
work_order.workflow_state = workflow_state
work_order.save()
frappe.db.commit()
# Return updated work order with child tables
work_order_dict = work_order.as_dict()
work_order_dict['stock_items'] = [item.as_dict() for item in work_order.get('stock_items', [])]
work_order_dict['invoice_table'] = [item.as_dict() for item in work_order.get('invoice_table', [])]
work_order_dict['table_cmqp'] = [item.as_dict() for item in work_order.get('table_cmqp', [])]
frappe.response['message'] = {
'success': True,
'work_order': work_order_dict,
'message': _('Work Order status updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Update Work Order Status API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def add_stock_item(work_order_name, item_data):
"""
Add a stock item to work order's stock_items child table
Args:
work_order_name: Name/ID of the work order
item_data: JSON string containing stock item fields
Example:
{
"item_code": "ITEM-001",
"warehouse": "Main Warehouse",
"valuation_rate": 100,
"total_value": 500,
"custom_available_stock": 10
}
Returns:
Updated work order with stock items
"""
try:
import json
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Parse item data
if isinstance(item_data, str):
item_data = json.loads(item_data)
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order and add stock item
work_order = frappe.get_doc('Work_Order', work_order_name)
work_order.append('stock_items', item_data)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'stock_items': [item.as_dict() for item in work_order.get('stock_items', [])],
'message': _('Stock item added successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Add Stock Item API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def remove_stock_item(work_order_name, item_name):
"""
Remove a stock item from work order's stock_items child table
Args:
work_order_name: Name/ID of the work order
item_name: Name/ID of the stock item to remove
Returns:
Updated work order with remaining stock items
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
if not item_name:
frappe.throw(_('Stock item name is required'))
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Find and remove the stock item
item_to_remove = None
for item in work_order.stock_items:
if item.name == item_name:
item_to_remove = item
break
if item_to_remove:
work_order.remove(item_to_remove)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'stock_items': [item.as_dict() for item in work_order.get('stock_items', [])],
'message': _('Stock item removed successfully')
}
else:
frappe.response['message'] = {
'success': False,
'error': _('Stock item not found')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Remove Stock Item API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def add_invoice(work_order_name, invoice_data):
"""
Add an invoice to work order's invoice_table child table
Args:
work_order_name: Name/ID of the work order
invoice_data: JSON string containing invoice fields
Returns:
Updated work order with invoices
"""
try:
import json
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Parse invoice data
if isinstance(invoice_data, str):
invoice_data = json.loads(invoice_data)
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order and add invoice
work_order = frappe.get_doc('Work_Order', work_order_name)
work_order.append('invoice_table', invoice_data)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'invoice_table': [item.as_dict() for item in work_order.get('invoice_table', [])],
'message': _('Invoice added successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Add Invoice API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def remove_invoice(work_order_name, invoice_name):
"""
Remove an invoice from work order's invoice_table child table
Args:
work_order_name: Name/ID of the work order
invoice_name: Name/ID of the invoice to remove
Returns:
Updated work order with remaining invoices
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
if not invoice_name:
frappe.throw(_('Invoice name is required'))
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Find and remove the invoice
item_to_remove = None
for item in work_order.invoice_table:
if item.name == invoice_name:
item_to_remove = item
break
if item_to_remove:
work_order.remove(item_to_remove)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'invoice_table': [item.as_dict() for item in work_order.get('invoice_table', [])],
'message': _('Invoice removed successfully')
}
else:
frappe.response['message'] = {
'success': False,
'error': _('Invoice not found')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Remove Invoice API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def add_cmqp_item(work_order_name, cmqp_data):
"""
Add a CMQP item to work order's table_cmqp child table
Args:
work_order_name: Name/ID of the work order
cmqp_data: JSON string containing CMQP item fields
Returns:
Updated work order with CMQP items
"""
try:
import json
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Parse CMQP data
if isinstance(cmqp_data, str):
cmqp_data = json.loads(cmqp_data)
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order and add CMQP item
work_order = frappe.get_doc('Work_Order', work_order_name)
work_order.append('table_cmqp', cmqp_data)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'table_cmqp': [item.as_dict() for item in work_order.get('table_cmqp', [])],
'message': _('CMQP item added successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Add CMQP Item API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def remove_cmqp_item(work_order_name, cmqp_name):
"""
Remove a CMQP item from work order's table_cmqp child table
Args:
work_order_name: Name/ID of the work order
cmqp_name: Name/ID of the CMQP item to remove
Returns:
Updated work order with remaining CMQP items
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
if not cmqp_name:
frappe.throw(_('CMQP item name is required'))
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Find and remove the CMQP item
item_to_remove = None
for item in work_order.table_cmqp:
if item.name == cmqp_name:
item_to_remove = item
break
if item_to_remove:
work_order.remove(item_to_remove)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'table_cmqp': [item.as_dict() for item in work_order.get('table_cmqp', [])],
'message': _('CMQP item removed successfully')
}
else:
frappe.response['message'] = {
'success': False,
'error': _('CMQP item not found')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Remove CMQP Item API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def get_work_order_child_tables(work_order_name, child_table=None):
"""
Get child table data for a work order
Args:
work_order_name: Name/ID of the work order
child_table: Specific child table to return ('stock_items', 'invoice_table', 'table_cmqp')
If None, returns all child tables
Returns:
Child table data
"""
try:
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Check if user has permission to read this work order
if not frappe.has_permission('Work_Order', 'read', work_order_name):
frappe.throw(_('Not permitted to access this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
result = {}
if child_table:
if child_table == 'stock_items':
result['stock_items'] = [item.as_dict() for item in work_order.get('stock_items', [])]
elif child_table == 'invoice_table':
result['invoice_table'] = [item.as_dict() for item in work_order.get('invoice_table', [])]
elif child_table == 'table_cmqp':
result['table_cmqp'] = [item.as_dict() for item in work_order.get('table_cmqp', [])]
else:
frappe.throw(_('Invalid child table name'))
else:
result = {
'stock_items': [item.as_dict() for item in work_order.get('stock_items', [])],
'invoice_table': [item.as_dict() for item in work_order.get('invoice_table', [])],
'table_cmqp': [item.as_dict() for item in work_order.get('table_cmqp', [])]
}
frappe.response['message'] = {
'success': True,
'work_order_name': work_order_name,
**result
}
except Exception as e:
frappe.log_error(frappe.get_traceback(), 'Get Work Order Child Tables API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}
@frappe.whitelist(allow_guest=True)
def bulk_update_stock_items(work_order_name, stock_items):
"""
Bulk update/replace all stock items in a work order
Args:
work_order_name: Name/ID of the work order
stock_items: JSON string containing list of stock items
Returns:
Updated work order with new stock items
"""
try:
import json
if not work_order_name:
frappe.throw(_('Work Order name is required'))
# Parse stock items
if isinstance(stock_items, str):
stock_items = json.loads(stock_items)
# Check if user has permission to update this work order
if not frappe.has_permission('Work_Order', 'write', work_order_name):
frappe.throw(_('Not permitted to update this work order'))
# Get work order
work_order = frappe.get_doc('Work_Order', work_order_name)
# Clear existing stock items and add new ones
work_order.set('stock_items', [])
for item in stock_items:
work_order.append('stock_items', item)
work_order.save()
frappe.db.commit()
frappe.response['message'] = {
'success': True,
'stock_items': [item.as_dict() for item in work_order.get('stock_items', [])],
'message': _('Stock items updated successfully')
}
except Exception as e:
frappe.db.rollback()
frappe.log_error(frappe.get_traceback(), 'Bulk Update Stock Items API Error')
frappe.response['message'] = {
'success': False,
'error': str(e)
}

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) 2026, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Building", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,47 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:building",
"creation": "2026-01-21 14:00:25.681969",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"building"
],
"fields": [
{
"fieldname": "building",
"fieldtype": "Data",
"label": "Building",
"unique": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-10 10:52:11.526339",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Building",
"naming_rule": "By fieldname",
"owner": "support@seeraarabia.com",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestBuilding(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) 2026, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Delete Request", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,125 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:{target_doctype}-{target_name}",
"creation": "2026-02-18 11:23:18.627887",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"target_doctype",
"target_display",
"department",
"status",
"column_break_nhyd",
"target_name",
"reason",
"requested_by",
"section_break_lwex",
"supervisor_comment",
"column_break_wsat",
"cluster_manager_comment",
"amended_from"
],
"fields": [
{
"fieldname": "target_doctype",
"fieldtype": "Link",
"label": "Target Doctype",
"options": "DocType"
},
{
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Issue Type"
},
{
"allow_on_submit": 1,
"fieldname": "status",
"fieldtype": "Select",
"label": "Status",
"options": "Pending\nApproved\nRejected"
},
{
"fieldname": "column_break_nhyd",
"fieldtype": "Column Break"
},
{
"fieldname": "target_name",
"fieldtype": "Data",
"label": "Target Name"
},
{
"fieldname": "reason",
"fieldtype": "Data",
"label": "Reason"
},
{
"fieldname": "requested_by",
"fieldtype": "Link",
"label": "Requested By",
"options": "User"
},
{
"fieldname": "section_break_lwex",
"fieldtype": "Section Break"
},
{
"fieldname": "supervisor_comment",
"fieldtype": "Small Text",
"label": "Supervisor Comment"
},
{
"fieldname": "column_break_wsat",
"fieldtype": "Column Break"
},
{
"fieldname": "cluster_manager_comment",
"fieldtype": "Small Text",
"label": "CM Comment"
},
{
"fieldname": "target_display",
"fieldtype": "Data",
"label": "Target Display"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Delete Request",
"print_hide": 1,
"read_only": 1,
"search_index": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [],
"modified": "2026-02-18 15:22:54.215648",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Delete Request",
"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
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestDeleteRequest(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("Extension Directory", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,47 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "field:extension_number",
"creation": "2025-12-12 13:47:32.707063",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"extension_number"
],
"fields": [
{
"fieldname": "extension_number",
"fieldtype": "Data",
"label": "Extension Number",
"unique": 1
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-01-12 15:40:06.059968",
"modified_by": "support@seeraarabia.com",
"module": "Asset Lite",
"name": "Extension Directory",
"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
}
],
"row_format": "Dynamic",
"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 ExtensionDirectory(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 TestExtensionDirectory(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,8 @@
// Copyright (c) 2026, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Feedback Parameters", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,41 @@
{
"actions": [],
"allow_rename": 1,
"creation": "2026-02-05 16:07:06.424217",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"section_break_et0g"
],
"fields": [
{
"fieldname": "section_break_et0g",
"fieldtype": "Section Break"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"issingle": 1,
"links": [],
"modified": "2026-02-05 16:07:06.424217",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Feedback Parameters",
"owner": "Administrator",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"print": 1,
"read": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": []
}

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestFeedbackParameters(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,8 @@
// Copyright (c) 2026, seyfert and contributors
// For license information, please see license.txt
// frappe.ui.form.on("Infrastructure Location", {
// refresh(frm) {
// },
// });

View File

@ -0,0 +1,97 @@
{
"actions": [],
"allow_import": 1,
"allow_rename": 1,
"autoname": "format:{building}-{department}-{room_no}-{location}",
"creation": "2026-02-02 09:53:38.751717",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"building",
"room_no",
"check_xqxx",
"column_break_jbgn",
"department",
"location"
],
"fields": [
{
"fieldname": "building",
"fieldtype": "Link",
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Building",
"options": "Building"
},
{
"fieldname": "room_no",
"fieldtype": "Link",
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Room No",
"options": "Room"
},
{
"fieldname": "column_break_jbgn",
"fieldtype": "Column Break"
},
{
"fieldname": "department",
"fieldtype": "Link",
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Department",
"options": "Department"
},
{
"fieldname": "location",
"fieldtype": "Link",
"in_filter": 1,
"in_global_search": 1,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Location",
"options": "Location"
},
{
"default": "0",
"fieldname": "check_xqxx",
"fieldtype": "Check",
"hidden": 1,
"label": "enable"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"links": [],
"modified": "2026-03-02 16:28:35.630556",
"modified_by": "support@seeraarabia.com",
"module": "Asset Lite",
"name": "Infrastructure Location",
"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
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

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

View File

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

View File

@ -0,0 +1,215 @@
{
"actions": [],
"allow_rename": 1,
"autoname": "format:INS-{YYYY}-{###}",
"creation": "2026-01-06 12:12:55.013069",
"doctype": "DocType",
"engine": "InnoDB",
"field_order": [
"inspection_type",
"work_order_type",
"inspection_date",
"target_closure_date",
"directed_to_responsible",
"linked_corrective_wo_no",
"status1",
"attachment",
"custom_delete_status",
"column_break_wcyc",
"status",
"requested_by",
"department",
"location",
"assigned_technician",
"technician_name",
"technician_department",
"extension_no",
"attachment_on_close",
"amended_from",
"section_break_ajvn",
"observation_note",
"column_break_sfev",
"technical_response"
],
"fields": [
{
"fieldname": "inspection_type",
"fieldtype": "Select",
"label": "Inspection Type",
"options": "\nSafety Inspection\nInspection"
},
{
"fieldname": "work_order_type",
"fieldtype": "Link",
"label": "Work Order Type",
"options": "Issue Type"
},
{
"fieldname": "inspection_date",
"fieldtype": "Date",
"label": "Inspection Date"
},
{
"fieldname": "target_closure_date",
"fieldtype": "Date",
"label": " Target Closure Date"
},
{
"fieldname": "directed_to_responsible",
"fieldtype": "Data",
"label": "Directed To (Responsible)"
},
{
"fieldname": "linked_corrective_wo_no",
"fieldtype": "Link",
"label": "Linked Corrective WO No",
"options": "Work_Order"
},
{
"fieldname": "status1",
"fieldtype": "Select",
"hidden": 1,
"label": " Status1",
"options": "\nOpen\nIn Progress\nClosed"
},
{
"fieldname": "attachment",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Attachment on Open"
},
{
"allow_on_submit": 1,
"fieldname": "custom_delete_status",
"fieldtype": "Select",
"label": "Delete Status",
"options": "\nDelete Request With Supervisor\nDelete Request With CM\nDeleted"
},
{
"fieldname": "column_break_wcyc",
"fieldtype": "Column Break"
},
{
"fieldname": "status",
"fieldtype": "Select",
"label": " Status",
"options": "\nOpen\nIn Progress\nPending Review\nClosed",
"read_only": 1
},
{
"fieldname": "requested_by",
"fieldtype": "Data",
"label": "Requested By"
},
{
"fieldname": "department",
"fieldtype": "Link",
"label": "Department",
"options": "Department"
},
{
"fieldname": "location",
"fieldtype": "Link",
"label": "Location",
"options": "Location"
},
{
"fieldname": "assigned_technician",
"fieldtype": "Link",
"label": "Assigned Technician",
"options": "User"
},
{
"fetch_from": "assigned_technician.full_name",
"fetch_if_empty": 1,
"fieldname": "technician_name",
"fieldtype": "Data",
"label": "Technician Name",
"read_only": 1
},
{
"fieldname": "technician_department",
"fieldtype": "Link",
"label": "Technician Department",
"options": "Technical Department"
},
{
"fieldname": "extension_no",
"fieldtype": "Link",
"in_list_view": 1,
"label": "Extension No",
"options": "Extension Directory",
"reqd": 1
},
{
"fetch_from": "linked_corrective_wo_no.custom_attachment_on_close",
"fetch_if_empty": 1,
"fieldname": "attachment_on_close",
"fieldtype": "Attach",
"in_list_view": 1,
"label": "Attachment on Close"
},
{
"fieldname": "amended_from",
"fieldtype": "Link",
"label": "Amended From",
"no_copy": 1,
"options": "Inspection",
"print_hide": 1,
"read_only": 1,
"search_index": 1
},
{
"fieldname": "section_break_ajvn",
"fieldtype": "Section Break"
},
{
"fieldname": "observation_note",
"fieldtype": "Small Text",
"label": " Observation / Note"
},
{
"fieldname": "column_break_sfev",
"fieldtype": "Column Break"
},
{
"fieldname": "technical_response",
"fieldtype": "Small Text",
"label": "Technical Response"
}
],
"grid_page_length": 50,
"index_web_pages_for_search": 1,
"is_submittable": 1,
"links": [
{
"link_doctype": "Work_Order",
"link_fieldname": "inspection"
}
],
"modified": "2026-03-10 10:50:50.987891",
"modified_by": "Administrator",
"module": "Asset Lite",
"name": "Inspection",
"naming_rule": "Expression",
"owner": "support@seeraarabia.com",
"permissions": [
{
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
"share": 1,
"write": 1
}
],
"row_format": "Dynamic",
"sort_field": "modified",
"sort_order": "DESC",
"states": [],
"track_changes": 1
}

View File

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

View File

@ -0,0 +1,9 @@
# Copyright (c) 2026, seyfert and Contributors
# See license.txt
# import frappe
from frappe.tests.utils import FrappeTestCase
class TestInspection(FrappeTestCase):
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) {
// },
// });

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