# -*- 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)