import logging from odoo import models, fields, api, _ from odoo import tools _logger = logging.getLogger(__name__) class UserAttendance(models.Model): _name = 'user.attendance' _description = 'User Attendance' _order = 'timestamp DESC, user_id, status, attendance_state_id, device_id' device_id = fields.Many2one('attendance.device', string='Attendance Machine', required=True, ondelete='restrict', index=True) user_id = fields.Many2one('attendance.device.user', string='Machine User', required=True, ondelete='cascade', index=True) timestamp = fields.Datetime(string='Timestamp', required=True, index=True, help="The date and time at which the employee took a check in/out action at the attendance machine") status = fields.Integer(string='Machine Attendance State', required=True, help="The state which is the unique number stored in the machine to" " indicate type of attendance (e.g. 0: Checkin, 1: Checkout, etc)") attendance_state_id = fields.Many2one('attendance.state', string='Software Attendance State', help="This technical field is to map the attendance" " status stored in the machine and the attendance status in System", required=True, index=True) activity_id = fields.Many2one('attendance.activity', related='attendance_state_id.activity_id', store=True, index=True) hr_attendance_id = fields.Many2one('hr.attendance', string='HR Attendance', ondelete='set null', help="The technical field to link Machine Attendance Data with System Attendance Data", index=True) type = fields.Selection(string='Activity Type', related='attendance_state_id.type', store=True) employee_id = fields.Many2one('hr.employee', string='Employee', related='user_id.employee_id', store=True, index=True) synced = fields.Boolean(string='Synced', help="This field is to indicate whether the attendance data is synchronized to System or not") _sql_constraints = [ ('unique_user_id_device_id_timestamp', 'UNIQUE(user_id, device_id, timestamp)', "The Timestamp and User must be unique per machine"), ] @api.model_create_multi def create(self, vals_list): attendances = super(UserAttendance, self).create(vals_list) return attendances @api.constrains('status', 'attendance_state_id') def constrains_status_attendance_state_id(self): for r in self: if r.status != r.attendance_state_id.code: raise(_("Attendance Status conflict! The status number from machine must match the attendance status defined in System.")) # def _prepare_last_hr_attendance_domain(self): # self.ensure_one() # return [ # ('employee_id', '=', self.employee_id.id), # ('check_in', '<=', self.timestamp), # '|', ('activity_id', '=', False), ('activity_id', '=', self.activity_id.id), # ] def _prepare_last_hr_attendance_domain(self): self.ensure_one() return [ ('employee_id', '=', self.employee_id.id), ('check_in', '<=', self.timestamp), '|', ('activity_id', '=', False), ('activity_id', '=', self.activity_id.id), ] # def _get_last_hr_attendance(self, user_attendance, hr_attendances): # last_hr_attendance = hr_attendances.filtered( # lambda hr_att: hr_att.employee_id == user_attendance.employee_id # and hr_att.checkin_device_id # and hr_att.check_in <= user_attendance.timestamp # and (hr_att.activity_id == user_attendance.activity_id # or not hr_att.activity_id)).sorted('check_in') # return last_hr_attendance and last_hr_attendance[-1:] or False def _get_last_hr_attendance(self, user_attendance, hr_attendances): # Filter by same day to ensure check-in and check-out are linked for the same day same_day_attendance = hr_attendances.filtered( lambda hr_att: hr_att.employee_id == user_attendance.employee_id and hr_att.check_in.date() == user_attendance.timestamp.date() and (hr_att.activity_id == user_attendance.activity_id or not hr_att.activity_id) ).sorted('check_in') return same_day_attendance and same_day_attendance[-1:] or False # def _prepare_hr_attendance_vals(self): # return { # 'employee_id': self.employee_id.id, # 'check_in': self.timestamp, # 'checkin_device_id': self.device_id.id, # 'activity_id': self.activity_id.id, # } # # def _create_hr_attendance(self): # vals_list = [] # for r in self: # vals_list.append(r._prepare_hr_attendance_vals()) # return self.env['hr.attendance'].with_context(sync_from_device=True).create(vals_list) def _prepare_hr_attendance_vals(self): return { 'employee_id': self.employee_id.id, 'check_in': self.timestamp, 'checkin_device_id': self.device_id.id, 'activity_id': self.activity_id.id, } def _create_hr_attendance(self): vals_list = [r._prepare_hr_attendance_vals() for r in self] return self.env['hr.attendance'].with_context(sync_from_device=True).create(vals_list) # def _sync_attendance(self): # """ # This method synchronizes `user.attendance` data into `hr.attendance` data # """ # error_msg = {} # all_hr_attendances = self.env['hr.attendance'].search([('employee_id', 'in', self.employee_id.ids)]) # for attendance_activity in self.activity_id: # for employee in self.filtered(lambda r: r.activity_id == attendance_activity).employee_id: # unsync_user_attendances = self.filtered( # lambda uatt: uatt.employee_id == employee # and uatt.activity_id == attendance_activity # ).sorted('timestamp') # hr_attendances = all_hr_attendances.filtered( # lambda hr_att: hr_att.employee_id == employee # ) # for uatt in unsync_user_attendances: # # Do flush manually to avoid magical user error: # # https://github.com/Viindoo/tvtmaaddons/pull/4053 # try: # with self.env.cr.savepoint(flush=False), tools.mute_logger('odoo.sql_db'): # last_hr_attendance = self._get_last_hr_attendance(uatt, hr_attendances) # uatt_update = False # if uatt.type == 'checkin': # if not last_hr_attendance or (last_hr_attendance.check_out and uatt.timestamp > last_hr_attendance.check_out): # last_hr_attendance = uatt._create_hr_attendance() # uatt_update = True # else: # if last_hr_attendance and not last_hr_attendance.check_out and uatt.timestamp >= last_hr_attendance.check_in: # last_hr_attendance.with_context(not_manual_check_out_modification=True, sync_from_device=True).write({ # 'check_out': uatt.timestamp, # 'checkout_device_id': uatt.device_id.id # }) # uatt_update = True # uatt_vals_to_update = {'synced': True} # if uatt_update: # if last_hr_attendance not in hr_attendances: # hr_attendances += last_hr_attendance # uatt_vals_to_update.update({'hr_attendance_id': last_hr_attendance.id}) # uatt.write(uatt_vals_to_update) # self.flush_recordset() # except Exception as e: # all_hr_attendances = self.env['hr.attendance'].search([('employee_id', 'in', self.employee_id.ids)]) # error_msg.setdefault(uatt.device_id, []) # msg = str(e) # if msg not in error_msg[uatt.device_id]: # error_msg[uatt.device_id].append(str(e)) # if bool(error_msg): # for device, msg_list in error_msg.items(): # device.message_post(body="
    %s
" % "".join(["
  • %s
  • " % msg for msg in msg_list])) def _sync_attendance(self): error_msg = {} all_hr_attendances = self.env['hr.attendance'].search([ ('employee_id', 'in', self.employee_id.ids)]) for attendance_activity in self.activity_id: for employee in self.filtered(lambda r: r.activity_id == attendance_activity).employee_id: unsync_user_attendances = self.filtered( lambda uatt: uatt.employee_id == employee and uatt.activity_id == attendance_activity ).sorted('timestamp') hr_attendances = all_hr_attendances.filtered( lambda hr_att: hr_att.employee_id == employee ) for uatt in unsync_user_attendances: try: with self.env.cr.savepoint(flush=False), tools.mute_logger('odoo.sql_db'): last_hr_attendance = self._get_last_hr_attendance(uatt, hr_attendances) uatt_update = False if uatt.type == 'checkin': if last_hr_attendance: # If the last entry has no checkout, update it if not last_hr_attendance.check_out and uatt.timestamp > last_hr_attendance.check_in: last_hr_attendance.with_context( not_manual_check_out_modification=True, sync_from_device=True ).write({'check_out': uatt.timestamp, 'checkout_device_id': uatt.device_id.id}) uatt_update = True else: # Create new check-in if previous one is completed last_hr_attendance = uatt._create_hr_attendance() uatt_update = True else: # No existing attendance, create new last_hr_attendance = uatt._create_hr_attendance() uatt_update = True else: # Handle check-out if last_hr_attendance and not last_hr_attendance.check_out and uatt.timestamp >= last_hr_attendance.check_in: last_hr_attendance.with_context( not_manual_check_out_modification=True, sync_from_device=True ).write({'check_out': uatt.timestamp, 'checkout_device_id': uatt.device_id.id}) uatt_update = True elif not last_hr_attendance: # No check-in exists, create both check-in and check-out last_hr_attendance = uatt._create_hr_attendance() last_hr_attendance.write( {'check_out': uatt.timestamp, 'checkout_device_id': uatt.device_id.id}) uatt_update = True # Update user attendance record uatt_vals_to_update = {'synced': True} if uatt_update: if last_hr_attendance not in hr_attendances: hr_attendances += last_hr_attendance uatt_vals_to_update.update({'hr_attendance_id': last_hr_attendance.id}) uatt.write(uatt_vals_to_update) self.flush_recordset() except Exception as e: all_hr_attendances = self.env['hr.attendance'].search([ ('employee_id', 'in', self.employee_id.ids)]) error_msg.setdefault(uatt.device_id, []) msg = str(e) if msg not in error_msg[uatt.device_id]: error_msg[uatt.device_id].append(msg) if bool(error_msg): for device, msg_list in error_msg.items(): device.message_post(body="
      %s
    " % "".join(["
  • %s
  • " % msg for msg in msg_list])) def action_sync_attendance(self): self._sync_attendance() @api.model def _prepare_unsynch_data_domain(self): return [ ('hr_attendance_id', '=', False), ('employee_id', '!=', False), ('synced', '=', False), ] @api.model def _cron_synch_hr_attendance(self): unsync_data = self.env['user.attendance'].search(self._prepare_unsynch_data_domain()) unsync_data._sync_attendance() def action_unsynic(self): for rec in self: rec.synced = False