249 lines
14 KiB
Python
249 lines
14 KiB
Python
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="<ol>%s</ol>" % "".join(["<li>%s</li>" % 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="<ol>%s</ol>" % "".join(["<li>%s</li>" % 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 |