115 lines
3.7 KiB
Python
115 lines
3.7 KiB
Python
# -*- coding: utf-8 -*-
|
||
from odoo import fields, models, api
|
||
from datetime import timedelta
|
||
|
||
# 教学用:规定上班时间为 09:00。
|
||
# 注意:Odoo 数据库里的 Datetime 以 UTC 存储,这里直接用字段的“小时”做演示比较;
|
||
# 生产环境应先用 fields.Datetime.context_timestamp() 把时间转换到用户时区再判断。
|
||
WORK_START_HOUR = 9
|
||
|
||
|
||
class Attendance(models.Model):
|
||
"""教职工出勤记录。"""
|
||
_name = 'yuthon.attendance'
|
||
_description = '出勤记录'
|
||
_order = 'date desc, employee_id'
|
||
|
||
employee_id = fields.Many2one(
|
||
'yuthon.employee',
|
||
string='教职工',
|
||
required=True,
|
||
ondelete='cascade',
|
||
)
|
||
date = fields.Date(
|
||
string='日期',
|
||
required=True,
|
||
default=fields.Date.today,
|
||
)
|
||
check_in = fields.Datetime(string='签到时间')
|
||
check_out = fields.Datetime(string='签退时间')
|
||
|
||
# ---- 计算字段 ----
|
||
working_hours = fields.Float(
|
||
string='工时(小时)',
|
||
compute='_compute_working_hours',
|
||
store=True,
|
||
)
|
||
status = fields.Selection(
|
||
[
|
||
('normal', '正常'),
|
||
('late', '迟到'),
|
||
('absent', '缺勤'),
|
||
],
|
||
string='状态',
|
||
compute='_compute_status',
|
||
store=True,
|
||
)
|
||
late_minutes = fields.Integer(
|
||
string='迟到分钟',
|
||
compute='_compute_status',
|
||
store=True,
|
||
)
|
||
|
||
# ---- 约束 ----
|
||
_sql_constraints = [
|
||
('uniq_employee_date', 'unique(employee_id, date)',
|
||
'同一教职工一天只能有一条出勤记录!'),
|
||
]
|
||
|
||
@api.constrains('check_in', 'check_out')
|
||
def _check_time_order(self):
|
||
for rec in self:
|
||
if rec.check_in and rec.check_out and rec.check_out < rec.check_in:
|
||
raise models.ValidationError('签退时间不能早于签到时间。')
|
||
|
||
@api.depends('check_in', 'check_out')
|
||
def _compute_working_hours(self):
|
||
for rec in self:
|
||
if rec.check_in and rec.check_out and rec.check_out >= rec.check_in:
|
||
delta = rec.check_out - rec.check_in
|
||
rec.working_hours = round(delta.total_seconds() / 3600.0, 2)
|
||
else:
|
||
rec.working_hours = 0.0
|
||
|
||
@api.depends('check_in')
|
||
def _compute_status(self):
|
||
for rec in self:
|
||
if not rec.check_in:
|
||
rec.status = 'absent'
|
||
rec.late_minutes = 0
|
||
continue
|
||
check_in_hour = rec.check_in.hour + rec.check_in.minute / 60.0
|
||
if check_in_hour > WORK_START_HOUR:
|
||
rec.status = 'late'
|
||
threshold = rec.check_in.replace(
|
||
hour=WORK_START_HOUR, minute=0, second=0, microsecond=0)
|
||
rec.late_minutes = max(0, int(
|
||
(rec.check_in - threshold).total_seconds() // 60))
|
||
else:
|
||
rec.status = 'normal'
|
||
rec.late_minutes = 0
|
||
|
||
|
||
class Employee(models.Model):
|
||
"""通过 _inherit 反向扩展 yuthon.employee,把出勤记录挂上去。
|
||
|
||
这是「模块组合」的关键示范:yuthon_attendance 不修改 yuthon_employee 的源码,
|
||
而是继承它追加 One2many 反向关联,让两个模块保持解耦。
|
||
"""
|
||
_inherit = 'yuthon.employee'
|
||
|
||
attendance_ids = fields.One2many(
|
||
'yuthon.attendance',
|
||
'employee_id',
|
||
string='出勤记录',
|
||
)
|
||
attendance_count = fields.Integer(
|
||
string='出勤记录数',
|
||
compute='_compute_attendance_count',
|
||
)
|
||
|
||
@api.depends('attendance_ids')
|
||
def _compute_attendance_count(self):
|
||
for rec in self:
|
||
rec.attendance_count = len(rec.attendance_ids)
|