odex30_standard/odex30_base/system_dashboard_classic/models/dashboard.py

1067 lines
44 KiB
Python

# -*- coding: utf-8 -*-
from odoo import models, fields, api, _
from odoo.exceptions import AccessError, UserError
import ast
import json
from datetime import datetime, date
from dateutil.relativedelta import relativedelta, SA, SU, MO
from math import radians, sin, cos, sqrt, asin
import logging
_logger = logging.getLogger(__name__)
class SystemDashboard(models.Model):
_name = 'system_dashboard_classic.dashboard'
_description = 'System Dashboard'
name = fields.Char("")
# ======================================================================
# HELPER METHODS
# ======================================================================
def is_user(self, groups, user):
"""Check whether current user is in a certain group"""
for group in groups:
if user.id in group.users.ids:
return True
return False
def _get_bool_param(self, key, default=True):
"""
Helper to safely get boolean value from ir.config_parameter.
Odoo 18 saves boolean config_parameter as 'True'/'False' strings.
"""
ICP = self.env['ir.config_parameter'].sudo()
value = ICP.get_param(key, None)
# If never set, use default
if value is None or value == '':
return default
# Handle string 'False' and 'True'
if isinstance(value, str):
return value.lower() not in ('false', '0', 'no')
# Handle actual boolean
return bool(value)
def _check_module_installed(self, module_name):
"""Check if a module is installed"""
module = self.env['ir.module.module'].sudo().search(
[('name', '=', module_name)], limit=1
)
return bool(module and module.state == 'installed')
def _get_user_timezone(self):
"""Get user's timezone with fallback"""
tz = self.env.context.get('tz') or self.env.user.tz or 'UTC'
try:
import pytz
return pytz.timezone(tz)
except Exception:
import pytz
return pytz.UTC
# ======================================================================
# MAIN DATA METHOD (Called by JavaScript)
# ======================================================================
@api.model
def get_data(self):
"""
Main RPC method called from JavaScript on dashboard load.
Returns user data, employee info, statistics, and card configurations.
"""
# Initialize return data structure
values = {
'user': [],
'employee': [],
'timesheet': [],
'leaves': [],
'payroll': [],
'attendance': [],
'attendance_hours': [],
'cards': [],
'chart_types': {},
'chart_colors': {},
'card_orders': {},
'stats_visibility': {},
'celebration': {},
'gender_info': {},
'refresh_settings': {},
'enable_attendance_button': False,
'job_english': '',
}
ICP = self.env['ir.config_parameter'].sudo()
# Load chart type settings
values['chart_types'] = {
'annual_leave': ICP.get_param('system_dashboard_classic.annual_leave_chart_type', 'donut'),
'salary_slips': ICP.get_param('system_dashboard_classic.salary_slips_chart_type', 'donut'),
'timesheet': ICP.get_param('system_dashboard_classic.timesheet_chart_type', 'donut'),
'attendance_hours': ICP.get_param('system_dashboard_classic.attendance_hours_chart_type', 'donut'),
}
# Load chart colors from settings
values['chart_colors'] = {
'primary': ICP.get_param('system_dashboard_classic.primary_color', '#0891b2'),
'warning': ICP.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
'secondary': ICP.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
'success': ICP.get_param('system_dashboard_classic.success_color', '#10b981'),
}
# Load attendance button setting
values['enable_attendance_button'] = self._get_bool_param(
'system_dashboard_classic.enable_attendance_button', 'False'
)
# Load stats visibility settings
values['stats_visibility'] = {
'show_annual_leave': self._get_bool_param('system_dashboard_classic.show_annual_leave'),
'show_salary_slips': self._get_bool_param('system_dashboard_classic.show_salary_slips'),
'show_timesheet': self._get_bool_param('system_dashboard_classic.show_timesheet'),
'show_attendance_hours': self._get_bool_param('system_dashboard_classic.show_attendance_hours'),
'show_attendance_section': self._get_bool_param('system_dashboard_classic.show_attendance_section'),
}
# Initialize celebration data (birthday and work anniversary)
values['celebration'] = {
'is_birthday': False,
'is_anniversary': False,
'anniversary_years': 0,
}
# Initialize gender-aware data for personalized experience
values['gender_info'] = {
'gender': 'male',
'honorific': 'أستاذ',
'pronoun_you': 'ك',
'verb_suffix': 'تَ',
}
# Get current user and employee
user = self.env.user
user_id = self.env['res.users'].sudo().search_read(
[('id', '=', user.id)], limit=1
)
employee_id = self.env['hr.employee'].sudo().search_read(
[('user_id', '=', user.id)], limit=1
)
employee_object = self.env['hr.employee'].sudo().search(
[('user_id', '=', user.id)], limit=1
)
# Get job title in English
if employee_object and employee_object.job_id:
if hasattr(employee_object.job_id, 'english_name') and employee_object.job_id.english_name:
job_english = employee_object.job_id.english_name
else:
job_english = employee_object.job_id.name or ''
else:
job_english = ''
values['job_english'] = job_english
# ==========================================
# CELEBRATION & GENDER DETECTION
# ==========================================
today = date.today()
if employee_object:
# Gender-aware personalization
employee_gender = getattr(employee_object, 'gender', 'male') or 'male'
if employee_gender == 'female':
values['gender_info'] = {
'gender': 'female',
'honorific': 'أستاذة',
'pronoun_you': 'كِ',
'verb_suffix': 'تِ',
}
# Check for BIRTHDAY
birthday = getattr(employee_object, 'birthday', None)
if birthday:
if birthday.month == today.month and birthday.day == today.day:
values['celebration']['is_birthday'] = True
# Check for WORK ANNIVERSARY
joining_date = None
if employee_object.contract_id and employee_object.contract_id.date_start:
joining_date = employee_object.contract_id.date_start
if joining_date:
if joining_date.month == today.month and joining_date.day == today.day:
years = today.year - joining_date.year
if years > 0:
values['celebration']['is_anniversary'] = True
values['celebration']['anniversary_years'] = years
# ==========================================
# ATTENDANCE DATA
# ==========================================
attendance_date = {'is_attendance': False, 'time': False}
if self._check_module_installed('hr_attendance'):
self._load_attendance_data(employee_object, attendance_date)
# ==========================================
# LEAVES DATA
# ==========================================
leaves_data = {'taken': 0, 'remaining_leaves': 0, 'is_module_installed': False}
if self._check_module_installed('hr_holidays'):
self._load_leaves_data(employee_object, leaves_data)
# ==========================================
# PAYROLL DATA
# ==========================================
payroll_data = {'taken': 0, 'payslip_remaining': 0, 'is_module_installed': False}
if self._check_module_installed('hr_payroll'):
self._load_payroll_data(employee_object, payroll_data)
# ==========================================
# TIMESHEET DATA
# ==========================================
timesheet_data = {'taken': 0, 'timesheet_remaining': 0, 'is_module_installed': False}
if self._check_module_installed('hr_timesheet'):
self._load_timesheet_data(employee_object, timesheet_data)
# ==========================================
# ATTENDANCE HOURS DATA
# ==========================================
attendance_hours_data = {'plan_hours': 0, 'official_hours': 0, 'is_module_installed': False}
self._load_attendance_hours_data(employee_object, attendance_hours_data, today)
# ==========================================
# DASHBOARD CARDS
# ==========================================
self._load_dashboard_cards(user, values)
# ==========================================
# FINALIZE RETURN DATA
# ==========================================
values['user'].append(user_id)
values['employee'].append(employee_id)
values['leaves'].append(leaves_data)
values['payroll'].append(payroll_data)
values['attendance'].append(attendance_date)
values['timesheet'].append(timesheet_data)
values['attendance_hours'].append(attendance_hours_data)
# Load user's saved card order preferences
try:
card_orders_json = self.env.user.dashboard_card_orders or '{}'
values['card_orders'] = json.loads(card_orders_json)
except (json.JSONDecodeError, TypeError, AttributeError):
values['card_orders'] = {}
# Load periodic refresh settings
values['refresh_settings'] = {
'enabled': self._get_bool_param('system_dashboard_classic.refresh_enabled', 'False'),
'interval': int(ICP.get_param('system_dashboard_classic.refresh_interval', '60'))
}
return values
# ======================================================================
# DATA LOADING HELPER METHODS
# ======================================================================
def _load_attendance_data(self, employee, data):
"""Load attendance check-in/out status"""
import pytz
if not employee:
return
try:
# Try custom attendance model first (attendance.attendance)
if 'attendance.attendance' in self.env:
last_attendance = self.env['attendance.attendance'].sudo().search(
[('employee_id', '=', employee.id)], limit=1, order="name desc"
)
if last_attendance:
data['is_attendance'] = (last_attendance.action == 'sign_in')
user_tz = self._get_user_timezone()
# Odoo 18: Use to_datetime instead of from_string
if last_attendance.name:
time_object = fields.Datetime.to_datetime(last_attendance.name)
if time_object:
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
data['time'] = time_in_timezone
# Fallback to standard hr.attendance model
elif 'hr.attendance' in self.env:
last_attendance = self.env['hr.attendance'].sudo().search(
[('employee_id', '=', employee.id)], limit=1, order="check_in desc"
)
if last_attendance:
data['is_attendance'] = not last_attendance.check_out
user_tz = self._get_user_timezone()
check_time = last_attendance.check_out or last_attendance.check_in
if check_time:
time_object = fields.Datetime.to_datetime(check_time)
if time_object:
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
data['time'] = time_in_timezone
except Exception as e:
_logger.warning(f"Error loading attendance data: {e}")
def _load_leaves_data(self, employee, data):
"""Load annual leave statistics"""
if not employee:
return
try:
data['is_module_installed'] = True
allocations = self.env['hr.holidays'].sudo().search(
[('employee_id', '=', employee.id), ('holiday_status_id.leave_type', '=', 'annual'),
('type', '=', 'add'),('check_allocation_view', '=', 'balance') ])
if allocations:
taken = sum(alloc.leaves_taken for alloc in allocations)
remaining = sum(alloc.remaining_leaves for alloc in allocations)
data['taken'] = taken
data['remaining_leaves'] = remaining
except Exception as e:
_logger.warning(f"Error loading leaves data: {e}")
def _load_payroll_data(self, employee, data):
"""Load payslip statistics for current year"""
if not employee:
return
try:
data['is_module_installed'] = True
first_day = date(date.today().year, 1, 1)
last_day = date(date.today().year, 12, 31)
payslip_count = self.env['hr.payslip'].sudo().search_count([
('employee_id', '=', employee.id),
('date_from', '>=', first_day),
('date_to', '<=', last_day)
])
# Calculate expected slips based on contract start date
contract = self.env['hr.contract'].sudo().search([
('employee_id', '=', employee.id),
('state', '=', 'open')
], limit=1)
expected_slips = 12
if contract and contract.date_start and contract.date_start.year == date.today().year:
expected_slips = 12 - contract.date_start.month + 1
remaining_slips = max(0, expected_slips - payslip_count)
data['taken'] = payslip_count
data['payslip_remaining'] = remaining_slips
except Exception as e:
_logger.warning(f"Error loading payroll data: {e}")
def _load_timesheet_data(self, employee, data):
"""Load weekly timesheet statistics"""
if not employee:
return
try:
data['is_module_installed'] = True
# Determine week boundaries
calendar = employee.resource_calendar_id
today = date.today()
# Default week start (Saturday)
start_date = today + relativedelta(weeks=-1, days=1, weekday=SA)
end_date = start_date + relativedelta(days=6)
# Calculate total working hours
total_working_hours = 0
if calendar:
total_working_hours = calendar.get_work_hours_count(
datetime.combine(start_date, datetime.min.time()),
datetime.combine(end_date, datetime.max.time()),
compute_leaves=True
)
else:
total_working_hours = 40.0 # Default fallback
# Get actual timesheet hours
timesheet = self.env['account.analytic.line'].sudo().search([
('employee_id', '=', employee.id),
('date', '>=', start_date),
('date', '<=', end_date)
])
done_hours = sum(sheet.unit_amount for sheet in timesheet)
data['taken'] = done_hours
data['timesheet_remaining'] = max(0, total_working_hours - done_hours)
except Exception as e:
_logger.warning(f"Error loading timesheet data: {e}")
def _load_attendance_hours_data(self, employee, data, today):
"""Load monthly attendance hours statistics"""
if not employee:
return
try:
# Try to use hr.attendance.transaction if available
if 'hr.attendance.transaction' in self.env:
first_day_of_month = date(today.year, today.month, 1)
attendance_txns = self.env['hr.attendance.transaction'].sudo().search([
('employee_id', '=', employee.id),
('date', '>=', first_day_of_month),
('date', '<=', today)
])
if employee.resource_calendar_id:
plan_hours_total = employee.resource_calendar_id.get_work_hours_count(
datetime.combine(first_day_of_month, datetime.min.time()),
datetime.combine(today, datetime.max.time()),
compute_leaves=True
)
else:
plan_hours_total = sum(txn.plan_hours for txn in attendance_txns)
official_hours_total = sum(txn.official_hours for txn in attendance_txns)
data['plan_hours'] = round(plan_hours_total, 2)
data['official_hours'] = round(official_hours_total, 2)
data['is_module_installed'] = True
# Fallback to standard hr.attendance
elif 'hr.attendance' in self.env:
first_day_of_month = date(today.year, today.month, 1)
attendances = self.env['hr.attendance'].sudo().search([
('employee_id', '=', employee.id),
('check_in', '>=', datetime.combine(first_day_of_month, datetime.min.time())),
('check_in', '<=', datetime.combine(today, datetime.max.time()))
])
if employee.resource_calendar_id:
plan_hours_total = employee.resource_calendar_id.get_work_hours_count(
datetime.combine(first_day_of_month, datetime.min.time()),
datetime.combine(today, datetime.max.time()),
compute_leaves=True
)
else:
plan_hours_total = 8 * 22 # Default 8 hours * 22 work days
official_hours_total = sum(att.worked_hours for att in attendances if att.worked_hours)
data['plan_hours'] = round(plan_hours_total, 2)
data['official_hours'] = round(official_hours_total, 2)
data['is_module_installed'] = True
except Exception as e:
_logger.warning(f"Error loading attendance hours data: {e}")
def _load_dashboard_cards(self, user, values):
"""Load dashboard card configurations"""
base = self.env['base.dashbord'].search([])
for models in base:
for model in models:
try:
model.with_user(user).check_access('read')
mod = {
'name': '',
'model': '',
'icon': '',
'lines': [],
'type': 'approve',
'domain_to_follow': []
}
for line in model.line_ids:
if self.is_user(line.group_ids, user):
# Get card names with translations
card_name_ar = model.with_context(lang='ar_001').name or model.name
card_name_en = model.with_context(lang='en_US').name or model.name
if not card_name_ar:
card_name_ar = model.model_id.with_context(lang='ar_001').name or model.model_id.name
if not card_name_en:
card_name_en = model.model_id.with_context(lang='en_US').name or model.model_id.name
mod['name'] = card_name_ar if self.env.user.lang in ['ar_001', 'ar_SY'] else card_name_en
mod['name_arabic'] = card_name_ar
mod['name_english'] = card_name_en
mod['model'] = model.model_name
mod['image'] = model.card_image
mod['icon_type'] = model.icon_type
mod['icon_name'] = model.icon_name
# Default Icon Logic
if not mod['image'] and not mod['icon_name']:
mod['icon_type'] = 'icon'
mod['icon_name'] = 'fa-th-large'
# Build domain and state/stage info
self._build_card_line_data(model, line, mod, user)
mod['lines'] = sorted(mod['lines'], key=lambda i: i['id'])
if mod['name']:
values['cards'].append(mod)
# Add self-service cards
if model.is_self_service:
self._add_self_service_card(model, user, values)
except AccessError:
continue
def _build_card_line_data(self, model, line, mod, user):
"""Build domain and count data for a card line"""
state_or_stage = 'state' if line.state_id else 'stage_id'
state_click = line.state_id.state if line.state_id else int(line.stage_id.stage_id)
state_follow = line.state_id.state if line.state_id else int(line.stage_id.stage_id)
action_domain_click = []
action_domain_follow = []
if model.action_domain:
try:
dom_act = ast.literal_eval(model.action_domain)
for i in dom_act:
if i[0] != 'state':
action_domain_click.append(i)
action_domain_follow.append(i)
except (ValueError, SyntaxError):
pass
# Handle hr.holidays workflow special case
if model.model_name == 'hr.holidays':
if self._check_module_installed('hr_holidays_workflow'):
action_domain_click.append('|')
action_domain_click.append(('stage_id.name', '=', line.state_id.state))
action_domain_click.append(
('state', '=', line.state_id.state) if line.state_id else
('stage_id', '=', int(line.stage_id.stage_id))
)
domain_click = str(action_domain_click).replace('(', '[').replace(')', ']')
domain_click = ast.literal_eval(domain_click)
domain_follow = str(action_domain_follow).replace('(', '[').replace(')', ']')
domain_follow = ast.literal_eval(domain_follow)
try:
state_to_click = self.env[model.model_name].search_count(action_domain_click)
except Exception:
state_to_click = 0
mod['domain_to_follow'].append(state_follow)
action_domain_follow.append((state_or_stage, 'not in', mod['domain_to_follow']))
try:
state_to_follow = self.env[model.model_name].search_count(action_domain_follow)
except Exception:
state_to_follow = 0
domain_follow_js = str(action_domain_follow).replace('(', '[').replace(')', ']')
domain_follow_js = ast.literal_eval(domain_follow_js)
# State name translation
state = ""
if line.state_id:
state = line.state_id.state if self.env.user.lang == 'en_US' else line.state_id.name
elif line.stage_id:
state = line.stage_id.name if self.env.user.lang == 'en_US' else line.stage_id.value
mod['lines'].append({
'id': line.state_id.id if line.state_id else line.stage_id.id,
'count_state_click': state_to_click,
'count_state_follow': state_to_follow,
'state_approval': _('') + '' + _(line.state_id.name) if line.state_id else '',
'state_folow': _('All Records'),
'domain_to_follow': state_follow,
'form_view': model.form_view_id.id,
'list_view': model.list_view_id.id,
'domain_click': domain_click,
'domain_follow': domain_follow_js,
'context': model.action_context,
})
def _add_self_service_card(self, model, user, values):
"""Add a self-service card to the values"""
card_name = model.name or model.model_id.name
service_action_domain = []
if model.action_domain:
try:
service_action = ast.literal_eval(model.action_domain)
for i in service_action:
if i[0] != 'state':
service_action_domain.append(i)
except (ValueError, SyntaxError):
pass
if model.search_field:
service_action_domain.append((model.search_field, '=', user.id))
# Icon configuration
if not model.card_image and not model.icon_name:
model_icon_type = 'icon'
model_icon_name = 'fa-th-large'
else:
model_icon_type = model.icon_type
model_icon_name = model.icon_name
try:
state_count = self.env[model.model_name].search_count(service_action_domain)
except Exception:
state_count = 0
values['cards'].append({
'type': 'selfs',
'name': card_name,
'name_english': card_name,
'name_arabic': card_name,
'model': model.model_name,
'state_count': state_count,
'image': model.card_image,
'icon_type': model_icon_type,
'icon_name': model_icon_name,
'form_view': model.form_view_id.id,
'list_view': model.list_view_id.id,
'js_domain': service_action_domain,
'context': model.action_context or {},
})
# ======================================================================
# PUBLIC API METHODS
# ======================================================================
@api.model
def get_public_dashboard_colors(self):
"""Get dashboard colors for JavaScript theming"""
ICP = self.env['ir.config_parameter'].sudo()
return {
'primary': ICP.get_param('system_dashboard_classic.primary_color', '#0891b2'),
'secondary': ICP.get_param('system_dashboard_classic.secondary_color', '#1e293b'),
'success': ICP.get_param('system_dashboard_classic.success_color', '#10b981'),
'warning': ICP.get_param('system_dashboard_classic.warning_color', '#f59e0b'),
}
# ======================================================================
# GEOLOCATION HELPER
# ======================================================================
def _haversine_distance(self, lat1, lon1, lat2, lon2):
"""
Calculate the great-circle distance between two GPS points using Haversine formula.
Returns distance in meters.
"""
R = 6371000 # Earth's radius in meters
lat1_rad, lon1_rad = radians(lat1), radians(lon1)
lat2_rad, lon2_rad = radians(lat2), radians(lon2)
dlat = lat2_rad - lat1_rad
dlon = lon2_rad - lon1_rad
a = sin(dlat / 2) ** 2 + cos(lat1_rad) * cos(lat2_rad) * sin(dlon / 2) ** 2
c = 2 * asin(sqrt(a))
return R * c
# ======================================================================
# CHECK-IN/CHECK-OUT
# ======================================================================
@api.model
def checkin_checkout(self, latitude=None, longitude=None):
"""
Check-in or Check-out with zone-based geolocation validation.
"""
import pytz
_logger = logging.getLogger(__name__)
_logger.info("=== checkin_checkout called ===")
_logger.info(f"User: {self.env.user.name}")
_logger.info(f"Lat/Long: {latitude}, {longitude}")
user = self.env.user
employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1)
if not employee:
is_arabic = 'ar' in self._context.get('lang', 'en_US')
msg = ("عذراً، لم يتم العثور على ملف وظيفي مرتبط بحسابك.\nيرجى مراجعة إدارة الموارد البشرية."
if is_arabic else
"Sorry, no employee profile found linked to your account.\nPlease contact HR department.")
return {'error': True, 'message': msg}
# Zone validation (if attendance.zone model exists)
if 'attendance.zone' in self.env:
validation_result = self._validate_attendance_zone(employee, latitude, longitude)
if validation_result.get('error'):
return validation_result
# Perform attendance action
return self._perform_attendance_action(employee, latitude, longitude)
def _validate_attendance_zone(self, employee, latitude, longitude):
"""Validate if employee is within allowed attendance zone"""
AttendanceZone = self.env['attendance.zone'].sudo()
is_arabic = 'ar' in self._context.get('lang', 'en_US')
employee_zones = AttendanceZone.search([('employee_ids', 'in', employee.id)])
general_zone = employee_zones.filtered(lambda z: z.general)
if general_zone:
return {} # General zone allows from anywhere
if not employee_zones:
msg = ("لم يتم تعيين منطقة حضور خاصة بك.\nيرجى التواصل مع إدارة النظام."
if is_arabic else
"No specific attendance zone assigned to you.\nPlease contact system administration.")
return {'error': True, 'message': msg}
if not latitude or not longitude:
msg = ("يتطلب النظام الوصول إلى موقعك الجغرافي لتسجيل الحضور.\nيرجى تفعيل صلاحية الموقع في المتصفح والمحاولة مرة أخرى."
if is_arabic else
"System requires access to your location for attendance.\nPlease enable location permission in your browser and try again.")
return {'error': True, 'message': msg}
# Check if within any zone
is_in_zone = False
closest_distance = float('inf')
allowed_range = 0
valid_zones = 0
for zone in employee_zones:
if not zone.latitude or not zone.longitude or not zone.allowed_range:
continue
try:
zone_lat = float(zone.latitude)
zone_lon = float(zone.longitude)
zone_range = float(zone.allowed_range)
except (ValueError, TypeError):
continue
valid_zones += 1
distance = self._haversine_distance(latitude, longitude, zone_lat, zone_lon)
if distance <= zone_range:
is_in_zone = True
break
if distance < closest_distance:
closest_distance = distance
allowed_range = zone_range
if not is_in_zone:
if valid_zones == 0:
msg = ("بيانات منطقة الحضور غير مكتملة.\nيرجى التواصل مع إدارة النظام لتحديث الإحداثيات."
if is_arabic else
"Attendance zone data is incomplete.\nPlease contact system administration to update coordinates.")
return {'error': True, 'message': msg}
distance_outside = int(closest_distance - allowed_range)
if distance_outside >= 1000:
distance_km = round(distance_outside / 1000, 1)
dist_str = f"{distance_km} كيلو متر" if is_arabic else f"{distance_km} kilometers"
else:
dist_str = f"{distance_outside} متر" if is_arabic else f"{distance_outside} meters"
gender = getattr(employee, 'gender', 'male') or 'male'
if is_arabic:
if gender == 'female':
msg = f"عذراً، أنتِ تتواجدين خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {dist_str} تقريباً أو أكثر لتتمكني من التسجيل."
else:
msg = f"عذراً، أنت تتواجد خارج نطاق الحضور المسموح.\nيرجى الاقتراب مسافة {dist_str} تقريباً أو أكثر لتتمكن من التسجيل."
else:
msg = f"Sorry, you are outside the allowed attendance zone.\nPlease move approximately {dist_str} or more closer to be able to check in."
return {'error': True, 'message': msg}
return {}
def _perform_attendance_action(self, employee, latitude, longitude):
"""Perform the actual attendance check-in/out action"""
import pytz
user_tz = self._get_user_timezone()
# Try custom attendance model first
if 'attendance.attendance' in self.env:
Attendance = self.env['attendance.attendance']
last_attendance = Attendance.sudo().search(
[('employee_id', '=', employee.id)], limit=1, order="name desc"
)
vals = {
'employee_id': employee.id,
'action_type': 'manual',
'latitude': str(latitude) if latitude else False,
'longitude': str(longitude) if longitude else False,
}
if last_attendance and last_attendance.action == 'sign_in':
vals['action'] = 'sign_out'
is_attendance = False
else:
vals['action'] = 'sign_in'
is_attendance = True
Attendance.create(vals)
# Fetch updated record
last_attendance = Attendance.sudo().search(
[('employee_id', '=', employee.id)], limit=1, order="name desc"
)
time_in_timezone = False
if last_attendance and last_attendance.name:
time_object = fields.Datetime.to_datetime(last_attendance.name)
if time_object:
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
return {'is_attendance': is_attendance, 'time': time_in_timezone}
# Fallback to standard hr.attendance
elif 'hr.attendance' in self.env:
HrAttendance = self.env['hr.attendance']
last_attendance = HrAttendance.sudo().search(
[('employee_id', '=', employee.id)], limit=1, order="check_in desc"
)
if last_attendance and not last_attendance.check_out:
# Check out
last_attendance.write({'check_out': fields.Datetime.now()})
is_attendance = False
check_time = last_attendance.check_out
else:
# Check in
new_attendance = HrAttendance.create({'employee_id': employee.id})
is_attendance = True
check_time = new_attendance.check_in
time_in_timezone = False
if check_time:
time_object = fields.Datetime.to_datetime(check_time)
if time_object:
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
return {'is_attendance': is_attendance, 'time': time_in_timezone}
return {'error': True, 'message': 'Attendance module not configured'}
# ======================================================================
# CARD ORDER MANAGEMENT
# ======================================================================
@api.model
def save_card_order(self, order_type, card_ids):
"""Save card order preference for current user"""
user = self.env.user
try:
current_orders = json.loads(user.dashboard_card_orders or '{}')
except (json.JSONDecodeError, TypeError):
current_orders = {}
current_orders[order_type] = card_ids
user.sudo().write({
'dashboard_card_orders': json.dumps(current_orders)
})
return True
@api.model
def get_card_order(self, order_type=None):
"""Get card order preferences for current user"""
user = self.env.user
try:
all_orders = json.loads(user.dashboard_card_orders or '{}')
except (json.JSONDecodeError, TypeError):
all_orders = {}
if order_type:
return all_orders.get(order_type, [])
return all_orders
# ======================================================================
# REFRESH DATA METHOD (Lightweight)
# ======================================================================
@api.model
def get_refresh_data(self):
"""
Lightweight method for periodic refresh.
Returns only attendance status and approval count.
"""
import pytz
result = {'attendance': [], 'approval_count': 0}
user = self.env.user
employee = self.env['hr.employee'].sudo().search([('user_id', '=', user.id)], limit=1)
if not employee:
return result
# Attendance status
if 'attendance.attendance' in self.env:
last_attendance = self.env['attendance.attendance'].sudo().search(
[('employee_id', '=', employee.id)], order='name desc', limit=1
)
if last_attendance:
is_attendance = (last_attendance.action == 'sign_in')
user_tz = self._get_user_timezone()
time_str = ''
if last_attendance.name:
time_object = fields.Datetime.to_datetime(last_attendance.name)
if time_object:
time_in_timezone = pytz.utc.localize(time_object).astimezone(user_tz)
time_str = time_in_timezone.strftime('%Y-%m-%d %H:%M:%S')
result['attendance'] = [{'is_attendance': is_attendance, 'time': time_str}]
# Calculate approval count
config_lines = self.env['base.dashbord.line'].sudo().search([])
approval_count = 0
for line in config_lines:
if not self.is_user(line.group_ids, user):
continue
dashboard = line.board_id
if not dashboard or not dashboard.model_id:
continue
domain = []
if dashboard.action_domain:
try:
dom_list = ast.literal_eval(dashboard.action_domain)
for item in dom_list:
if isinstance(item, (list, tuple)) and len(item) > 0 and item[0] != 'state':
domain.append(item)
except Exception:
pass
if line.state_id:
domain.append(('state', '=', line.state_id.state))
elif line.stage_id:
try:
target_stage_id = int(line.stage_id.stage_id)
domain.append(('stage_id', '=', target_stage_id))
except (ValueError, TypeError):
pass
try:
if dashboard.model_name in self.env:
line_count = self.env[dashboard.model_name].sudo().search_count(domain)
approval_count += line_count
except Exception:
pass
result['approval_count'] = approval_count
return result
# ======================================================================
# WORK TIMER DATA
# ======================================================================
@api.model
def get_work_timer_data(self):
"""Returns live work timer data for countdown display"""
employee = self.env.user.employee_id
if not employee:
return {'enabled': False, 'reason': 'no_employee'}
if not self._get_bool_param('system_dashboard_classic.show_work_timer', 'True'):
return {'enabled': False, 'reason': 'disabled_in_settings'}
today = fields.Date.today()
# Get calendar for planned hours
calendar = employee.resource_calendar_id
if calendar:
if hasattr(calendar, 'working_hours') and calendar.working_hours:
planned_hours = calendar.working_hours
elif hasattr(calendar, 'hours_per_day') and calendar.hours_per_day:
planned_hours = calendar.hours_per_day
else:
planned_hours = 8.0
else:
planned_hours = 8.0
# Check attendance status
if 'attendance.attendance' in self.env:
last_att = self.env['attendance.attendance'].sudo().search([
('employee_id', '=', employee.id),
('action_date', '=', today)
], order='name DESC', limit=1)
if not last_att:
return {
'enabled': True,
'status': 'not_checked_in',
'message': _('لم تسجل دخول بعد')
}
if last_att.action == 'sign_out':
return {
'enabled': True,
'status': 'checked_out',
'hours_worked': getattr(last_att, 'attendance_duration', 0),
'hours_worked_formatted': getattr(last_att, 'attendance_duration_hhmmss', '00:00:00'),
'planned_hours': planned_hours
}
# Currently checked in
last_sign_out = self.env['attendance.attendance'].sudo().search([
('employee_id', '=', employee.id),
('action_date', '=', today),
('action', '=', 'sign_out'),
('id', '<', last_att.id)
], order='name DESC', limit=1)
previous_duration = getattr(last_sign_out, 'attendance_duration', 0) if last_sign_out else 0
sign_in_utc = last_att.name
sign_in_local = fields.Datetime.context_timestamp(self, sign_in_utc)
return {
'enabled': True,
'status': 'checked_in',
'sign_in_time': sign_in_local.isoformat(),
'planned_hours': planned_hours,
'planned_seconds': int(planned_hours * 3600),
'previous_duration': previous_duration,
'previous_seconds': int(previous_duration * 3600)
}
return {'enabled': False, 'reason': 'attendance_module_not_available'}