# -*- 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'}