From e3d12dc50587023fc9898cd8a9c14bc3e643fcae Mon Sep 17 00:00:00 2001
From: lzl <18587483734@163.com>
Date: Tue, 1 Sep 2026 22:56:39 +0800
Subject: [PATCH] =?UTF-8?q?[ADD]=20=E6=B7=BB=E5=8A=A0=E4=BA=86=20yuthon=5F?=
=?UTF-8?q?hr=5Fattendance=20=E6=A8=A1=E5=9D=97?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.idea/custom.iml | 8 +
.../inspectionProfiles/profiles_settings.xml | 6 +
.idea/misc.xml | 4 +
.idea/modules.xml | 8 +
.idea/vcs.xml | 4 +
.idea/workspace.xml | 45 ++
yuthon_hr_attendance/__init__.py | 3 +
yuthon_hr_attendance/__manifest__.py | 26 +
yuthon_hr_attendance/models/__init__.py | 4 +
yuthon_hr_attendance/models/hr_attendance.py | 254 +++++++++
.../models/hr_attendance_month.py | 506 ++++++++++++++++++
.../security/ir.model.access.csv | 6 +
.../views/attendance_menu.xml | 15 +
.../views/card_remind_form.xml | 30 ++
.../views/hr_attendance_month_views.xml | 278 ++++++++++
.../views/hr_attendance_views.xml | 95 ++++
16 files changed, 1292 insertions(+)
create mode 100644 .idea/custom.iml
create mode 100644 .idea/inspectionProfiles/profiles_settings.xml
create mode 100644 .idea/misc.xml
create mode 100644 .idea/modules.xml
create mode 100644 .idea/vcs.xml
create mode 100644 .idea/workspace.xml
create mode 100644 yuthon_hr_attendance/__init__.py
create mode 100644 yuthon_hr_attendance/__manifest__.py
create mode 100644 yuthon_hr_attendance/models/__init__.py
create mode 100644 yuthon_hr_attendance/models/hr_attendance.py
create mode 100644 yuthon_hr_attendance/models/hr_attendance_month.py
create mode 100644 yuthon_hr_attendance/security/ir.model.access.csv
create mode 100644 yuthon_hr_attendance/views/attendance_menu.xml
create mode 100644 yuthon_hr_attendance/views/card_remind_form.xml
create mode 100644 yuthon_hr_attendance/views/hr_attendance_month_views.xml
create mode 100644 yuthon_hr_attendance/views/hr_attendance_views.xml
diff --git a/.idea/custom.iml b/.idea/custom.iml
new file mode 100644
index 0000000..d0876a7
--- /dev/null
+++ b/.idea/custom.iml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/inspectionProfiles/profiles_settings.xml b/.idea/inspectionProfiles/profiles_settings.xml
new file mode 100644
index 0000000..105ce2d
--- /dev/null
+++ b/.idea/inspectionProfiles/profiles_settings.xml
@@ -0,0 +1,6 @@
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/misc.xml b/.idea/misc.xml
new file mode 100644
index 0000000..a971a2c
--- /dev/null
+++ b/.idea/misc.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/modules.xml b/.idea/modules.xml
new file mode 100644
index 0000000..71a3844
--- /dev/null
+++ b/.idea/modules.xml
@@ -0,0 +1,8 @@
+
+
+
+
+
+
+
+
\ No newline at end of file
diff --git a/.idea/vcs.xml b/.idea/vcs.xml
new file mode 100644
index 0000000..d843f34
--- /dev/null
+++ b/.idea/vcs.xml
@@ -0,0 +1,4 @@
+
+
+
+
\ No newline at end of file
diff --git a/.idea/workspace.xml b/.idea/workspace.xml
new file mode 100644
index 0000000..3cc0bb5
--- /dev/null
+++ b/.idea/workspace.xml
@@ -0,0 +1,45 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 1788074028307
+
+
+ 1788074028307
+
+
+
+
\ No newline at end of file
diff --git a/yuthon_hr_attendance/__init__.py b/yuthon_hr_attendance/__init__.py
new file mode 100644
index 0000000..cde864b
--- /dev/null
+++ b/yuthon_hr_attendance/__init__.py
@@ -0,0 +1,3 @@
+# -*- coding: utf-8 -*-
+
+from . import models
diff --git a/yuthon_hr_attendance/__manifest__.py b/yuthon_hr_attendance/__manifest__.py
new file mode 100644
index 0000000..9d47989
--- /dev/null
+++ b/yuthon_hr_attendance/__manifest__.py
@@ -0,0 +1,26 @@
+# -*- coding: utf-8 -*-
+
+# email:pengyuthon@163.com
+
+{
+ 'name': 'Yuthon attendances',
+ 'summary': '',
+ 'description': '''
+ ''',
+ 'author': 'pengyuthon',
+ 'version': '18.0.1.0',
+ 'category': 'Human Resources/Attendances',
+ 'sequence': 240,
+ 'website': 'https://www.pengyuthon.com',
+ 'depends': ['base', 'hr', 'hr_attendance', 'mail', 'planning', 'hr_holidays', 'resource'],
+ 'data': [
+ 'security/ir.model.access.csv',
+ 'views/attendance_menu.xml',
+ 'views/hr_attendance_views.xml',
+ 'views/hr_attendance_month_views.xml',
+ 'views/card_remind_form.xml',
+ ],
+ 'installable': True,
+ 'application': True,
+ 'license': 'LGPL-3',
+}
diff --git a/yuthon_hr_attendance/models/__init__.py b/yuthon_hr_attendance/models/__init__.py
new file mode 100644
index 0000000..4b2a768
--- /dev/null
+++ b/yuthon_hr_attendance/models/__init__.py
@@ -0,0 +1,4 @@
+# -*- coding: utf-8 -*-
+
+from . import hr_attendance
+from . import hr_attendance_month
diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py
new file mode 100644
index 0000000..dc739f9
--- /dev/null
+++ b/yuthon_hr_attendance/models/hr_attendance.py
@@ -0,0 +1,254 @@
+# -*- coding: utf-8 -*-
+
+from datetime import datetime, time
+from re import search
+
+from odoo import models, fields, api, exceptions, _
+from datetime import timedelta, date
+from dateutil.relativedelta import relativedelta
+from odoo.tools import format_datetime, format_duration
+import logging
+from odoo.exceptions import UserError, ValidationError
+import calendar
+
+_logger = logging.getLogger(__name__)
+
+
+class HrAttendance(models.Model):
+ _inherit = "hr.attendance"
+ _description = "Attendance"
+ _rec_name = 'current_data'
+
+ is_clock_card = fields.Boolean(string="外勤")
+ is_no_card = fields.Boolean(string="缺卡", group_operator='count_true')
+ is_early_card = fields.Boolean(string="早退", group_operator='count_true')
+ is_late_card = fields.Boolean(string="迟到", group_operator='count_true')
+ is_abnormal = fields.Boolean(string="异常", group_operator='count_true')
+ is_expatriation = fields.Boolean(string="外派", group_operator='count_true')
+ user_id = fields.Many2one(related='employee_id.user_id', store=True)
+ absenteeism_card = fields.Boolean(string="旷工", group_operator='count_true')
+ work_time_type = fields.Selection([('close_day', '休息日'), ('work_day', '工作日')], string="工作时间类型")
+ company_id = fields.Many2one(related='employee_id.company_id', string='公司', index=True, store=True)
+ on_card_location = fields.Char(string="上班打卡地点")
+ down_card_location = fields.Char(string="下班打卡地点")
+ out_card_time = fields.Datetime(string="外出打卡时间")
+ out_card_location = fields.Char(string="外出打卡地点")
+ department_id = fields.Many2one(related='employee_id.department_id', string="部门", store=True)
+ work_hours = fields.Float(string="工作时长")
+ current_data = fields.Date(string='打卡日期', tracking=True)
+ week = fields.Selection([('one', '一'), ('two', '二'), ('three', '三'),
+ ('four', '四'), ('five', '五'), ('six', '六'),
+ ('seven', '日')], string='星期', compute='_compute_week', store=True)
+ abnormal = fields.Integer(string="异常计算")
+ shift_id = fields.Many2one('planning.slot', string="班次", store=True)
+ shift_name = fields.Char(string="班次", store=True)
+ is_week = fields.Boolean(string="是否为周末")
+ is_holiday = fields.Boolean(string="是否为节假日")
+ noon_duty = fields.Datetime(string="中值")
+ night_duty = fields.Datetime(string="晚值")
+ is_warning = fields.Boolean(compute='_compute_is_warning', string="警告", store=True)
+
+ @api.depends('is_no_card', 'is_early_card', 'is_late_card', 'is_abnormal', 'absenteeism_card')
+ def _compute_is_warning(self):
+ for record in self:
+ record.is_warning = (
+ record.is_no_card or
+ record.is_early_card or
+ record.is_late_card or
+ record.is_abnormal or
+ record.absenteeism_card
+ )
+
+ @api.depends('current_data')
+ def _compute_week(self):
+ for record in self:
+ if record.current_data:
+ weekday = (record.current_data + timedelta(hours=8)).weekday()
+ weekdays_cn = {0: 'one', 1: 'two', 2: 'three',
+ 3: 'four', 4: 'five', 5: 'six',
+ 6: 'seven'}
+ record.week = weekdays_cn[weekday]
+ else:
+ record.week = False
+
+ @api.depends('employee_id', 'check_in', 'check_out')
+ def _compute_display_name(self):
+ for attendance in self:
+ is_early_card = '早退' if attendance.is_early_card else ''
+ is_late_card = '迟到' if attendance.is_late_card else ''
+ if not attendance.check_out:
+ attendance.display_name = _(
+ str(attendance.current_data) + " 上午:%s" + is_late_card + " -下午缺卡",
+ format_datetime(self.env, attendance.check_in, dt_format="HH:mm"),
+ )
+ elif not attendance.check_in:
+ attendance.display_name = _(
+ str(attendance.current_data) + " 上午:缺卡-下午%s" + is_early_card,
+ format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
+ )
+ else:
+ attendance.display_name = _(
+ str(attendance.current_data) + " 上午:%s" + is_late_card + "-下午:%s" + is_early_card,
+ format_datetime(self.env, attendance.check_in, dt_format="HH:mm"),
+ format_datetime(self.env, attendance.check_out, dt_format="HH:mm"),
+ )
+
+ def update_abnormal(self):
+ attendance_ids = self.env["hr.attendance"].browse(
+ self._context.get('active_ids', self._context.get('active_id')))
+ for attendance in attendance_ids:
+ attendance.set_shift_name()
+
+ def set_shift_name(self):
+ """抽取的统一班次名称设置逻辑"""
+ for record in self:
+ # 白名单不做处理
+ if record.shift_id or record.employee_id.id in record.employee_id.resource_calendar_id.resource_user_ids.ids:
+ continue # 已有班次记录,无需自动设置
+
+ shift_days_list = []
+ is_shift_replacement = False
+ is_holiday = False
+ is_weekend = False
+ # 判断排班员工
+ planning_ids = self.env['planning.role'].search([('name', 'in', ['保安部', '工程部', '联安粮库'])])
+ related_resources = set()
+ for role in planning_ids:
+ if role.resource_ids:
+ related_resources.update(role.resource_ids.ids)
+ related_resources_set = self.env['resource.resource'].browse(related_resources)
+ if record.employee_id.resource_id in related_resources_set:
+ record.shift_name = '休息'
+ record.is_week = True
+ record.write({
+ 'is_early_card': False,
+ 'is_late_card': False,
+ 'is_abnormal': False,
+ 'is_warning': False,
+ 'is_no_card': False,
+ 'absenteeism_card': False,
+ })
+ continue
+
+ if record.current_data:
+ current_year = record.current_data.year
+ query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
+ query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
+ leave_records = self.env['resource.calendar.leaves'].search([
+ ("calendar_id.name", "=", "行政考勤工作日"),
+ ("date_from", ">=", query_start),
+ ("date_from", "<=", query_end),
+ ('resource_id', '=', False),
+ ])
+ # 检查是否为节假日
+ for leave in leave_records:
+ if leave.date_from and leave.date_to:
+ from_date = fields.Date.to_date(leave.date_from + timedelta(hours=8))
+ to_date = fields.Date.to_date(leave.date_to + timedelta(hours=8))
+ if from_date <= record.current_data <= to_date:
+ is_holiday = True # 标记为节假日
+
+ # 收集调班补班日期
+ if leave.shift_replacement_day:
+ shift_days_list.extend(leave.shift_replacement_day.split(','))
+ # 检查是否为调班补班日
+ target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
+ if target_day_str in shift_days_list:
+ is_shift_replacement = True
+
+ # 检查是否为周末
+ weekday = record.current_data.isoweekday()
+ if weekday in [6, 7]:
+ is_weekend = True
+
+ if is_shift_replacement:
+ record.shift_name = '行政班次'
+ elif is_weekend or is_holiday:
+ record.shift_name = '休息'
+ else:
+ record.shift_name = '行政班次'
+ if record.is_expatriation:
+ record.shift_name = '外派'
+
+ if record.shift_name == "休息" and not is_holiday:
+ record.is_week = is_weekend
+ else:
+ record.is_week = False
+
+ record.is_holiday = is_holiday
+
+ def button_all(self):
+ attendance_ids = self.env["hr.attendance"].browse(self._context.get('active_ids', self._context.get('active_id')))
+ for record in attendance_ids:
+ if not record.shift_id:
+ shift_days_list = []
+ is_shift_replacement = False
+ is_leave_day = False
+ is_weekend = False
+ if record.current_data:
+ # 查询休假记录
+ current_year = (fields.Date.today() + timedelta(hours=8)).year
+ query_start = datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
+ query_end = datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
+ leave_records = self.env['resource.calendar.leaves'].search(
+ [("calendar_id.name", "=", "行政考勤工作日"),
+ ("date_from", ">=", query_start),
+ ("date_from", "<=", query_end),
+ ('resource_id', '=', False),])
+ # 检查是否存在匹配的休假日期
+ for leave in leave_records:
+ if leave.date_from and leave.date_to:
+ # 提取休假记录的月日范围
+ from_date = fields.Date.to_date(leave.date_from) + timedelta(days=1)
+ to_date = fields.Date.to_date(leave.date_to) + timedelta(days=1)
+ if from_date <= record.current_data <= to_date:
+ is_leave_day = True
+ if leave.shift_replacement_day:
+ shift_days_list.extend(leave.shift_replacement_day.split(',')) # 调班补班
+
+ target_day_str = f"{record.current_data.month}月{record.current_data.day}日"
+ if target_day_str in shift_days_list:
+ is_shift_replacement = True
+
+ weekday = record.current_data.isoweekday()
+ if weekday in [6, 7]:
+ is_weekend = True
+
+ if is_leave_day:
+ record.shift_name = ''
+ elif is_shift_replacement:
+ record.shift_name = '行政班次'
+ elif is_weekend:
+ record.shift_name = ''
+ else:
+ record.shift_name = '行政班次'
+
+ @api.model
+ def create(self, vals):
+ record = super(HrAttendance, self).create(vals)
+ # 未来日期不处理班次(保持原逻辑)
+ if 'current_data' in vals:
+ current_data = fields.Date.to_date(vals['current_data'])
+ today = fields.Date.today()
+ if current_data > today:
+ return record
+ record.set_shift_name()
+ return record
+
+ def write(self, vals):
+ result = super(HrAttendance, self).write(vals)
+ # 当更新考勤日期或班次时,重新计算班次名称
+ if 'current_data' in vals or 'shift_id' in vals:
+ self.set_shift_name()
+ return result
+
+
+class card_remind_form(models.Model):
+ _name = 'card.remind.form'
+ _description = '提醒日志'
+
+ name = fields.Char(string='姓名')
+ start_time = fields.Datetime(string='时间')
+ shift = fields.Char(string='班次')
+ department = fields.Char(string='部门')
+ remarks = fields.Text(string='备注')
diff --git a/yuthon_hr_attendance/models/hr_attendance_month.py b/yuthon_hr_attendance/models/hr_attendance_month.py
new file mode 100644
index 0000000..b322247
--- /dev/null
+++ b/yuthon_hr_attendance/models/hr_attendance_month.py
@@ -0,0 +1,506 @@
+# -*- coding: utf-8 -*-
+
+import datetime
+import calendar
+from odoo import api, fields, models, tools, _
+from odoo.tools.float_utils import float_round
+from dateutil.relativedelta import relativedelta
+from odoo.tools import DEFAULT_SERVER_DATE_FORMAT
+from datetime import timedelta
+from datetime import date
+
+
+class HrAttendanceYear(models.Model):
+ _name = "hr.attendance.year"
+ _description = "出勤年"
+ _rec_name = "year"
+ _order = 'year desc'
+
+ year = fields.Char(string='年份')
+ hr_attendance_month_ids = fields.One2many(comodel_name='hr.attendance.month', inverse_name='attendance_year_id', string="出勤月度汇总")
+ hr_attendance_month_detail_ids = fields.One2many(comodel_name='hr.attendance.month.detail', inverse_name='attendance_year_id', string="出勤月度汇总明细")
+
+
+class HrAttendanceMonth(models.Model):
+ _name = "hr.attendance.month"
+ _description = "出勤月度汇总"
+ _order = "statistics_time desc"
+
+ name = fields.Char(compute='_compute_name')
+ attendance_year_id = fields.Many2one('hr.attendance.year', string="统计年份")
+ company_id = fields.Many2one('res.company', string="公司", default=lambda self: self.env.user.company_id)
+ current_year = fields.Char(string='统计年份', related="attendance_year_id.year")
+ current_month = fields.Selection([
+ ('1', '1月份'),
+ ('2', '2月份'),
+ ('3', '3月份'),
+ ('4', '4月份'),
+ ('5', '5月份'),
+ ('6', '6月份'),
+ ('7', '7月份'),
+ ('8', '8月份'),
+ ('9', '9月份'),
+ ('10', '10月份'),
+ ('11', '11月份'),
+ ('12', '12月份')
+ ], string="考勤月份")
+ is_statistics = fields.Boolean(string="是否已统计", store=True, default=False)
+ statistics_time = fields.Datetime(string="统计时间")
+ attendance_month_detail_ids = fields.One2many(string="出勤月度汇总明细", comodel_name='hr.attendance.month.detail', inverse_name='hr_attendance_month_id')
+ is_state = fields.Boolean(string='是否已全部确认', compute='_compute_is_state', store=True)
+
+ @api.depends('attendance_month_detail_ids.state')
+ def _compute_is_state(self):
+ for record in self:
+ record.is_state = all(detail.state == 'confirmed' for detail in record.attendance_month_detail_ids)
+
+ def _compute_name(self):
+ for record in self:
+ record.write({'name': self.current_year+'年'+self.current_month+'月'})
+
+ def action_show_details(self):
+ self.ensure_one()
+ view = self.env.ref("yuthon_hr_attendance.view_hr_attendance_month_detail_tree")
+ form_view = self.env.ref("yuthon_hr_attendance.view_hr_attendance_month_detail_form")
+ return {
+ "name": _("月度汇总明细"),
+ "type": "ir.actions.act_window",
+ "view_mode": "list,form",
+ "res_model": "hr.attendance.month.detail",
+ "views": [(view.id, "list"), (form_view.id, "form")],
+ "view_id": view.id,
+ "target": "current",
+ "res_id": self.id,
+ "domain": [('hr_attendance_month_id', '=', self.id)],
+ "context": {'hr_attendance_month_id': self.id}
+ }
+
+
+class HrAttendanceMonthDetail(models.Model):
+ _name = "hr.attendance.month.detail"
+ _inherit = ['mail.thread', 'mail.activity.mixin']
+ _description = "考勤确认单"
+
+ name = fields.Char(related='employee_id.name')
+ employee_id = fields.Many2one('hr.employee', string="员工", readonly=True)
+ company_id = fields.Many2one(related="employee_id.company_id", string="公司", store=True)
+ department_id = fields.Many2one('hr.department', string="部门", related="employee_id.department_id", readonly=True, store=True)
+ # userid = fields.Char(string='userid')
+ attendance_year_id = fields.Many2one('hr.attendance.year', string="统计年份")
+ current_year = fields.Char(string='统计年份', related="attendance_year_id.year", store=True)
+ current_month = fields.Selection([
+ ('1', '1月份'),
+ ('2', '2月份'),
+ ('3', '3月份'),
+ ('4', '4月份'),
+ ('5', '5月份'),
+ ('6', '6月份'),
+ ('7', '7月份'),
+ ('8', '8月份'),
+ ('9', '9月份'),
+ ('10', '10月份'),
+ ('11', '11月份'),
+ ('12', '12月份')
+ ], string="考勤月份", store=True)
+ required_attendance_days = fields.Integer(string="应出勤天数")
+ actual_attendance_days = fields.Integer(string="实际出勤天数")
+ compensatory_leave = fields.Integer(string="调休")
+ leave_of_absence = fields.Integer(string="事假", digits=(16, 3))
+ statutory_leave = fields.Integer(string="加班时(法定)")
+ work_overtime = fields.Integer(string="加班")
+ entry_date = fields.Date(related='employee_id.entry_date', string="入职日期")
+ expected_date = fields.Date(string="离职日期")
+ cop_leave = fields.Float(string='补休', digits=(16, 3))
+ annual_leave = fields.Float(string='年假', digits=(16, 3))
+ duty_off_leave = fields.Integer(string='值班调休')
+ marriage_leave = fields.Integer(string='婚假')
+ general_off_leave = fields.Integer(string='调休')
+ maternity_leave = fields.Integer(string='产假')
+ paternity_leave = fields.Integer(string='陪产假')
+ family_planning_leave = fields.Integer(string='计生假')
+ bereavement_leave = fields.Float(string='丧假')
+ home_visit_leave = fields.Integer(string='探亲假')
+ parents_meeting_leave = fields.Integer(string='家长会假')
+ general_sick_leave = fields.Float(string='病假(普通)')
+ hospital_sick_leave = fields.Float(string='病假(住院期)')
+ recovery_sick_leave = fields.Float(string='病假(出院恢复期)')
+ exam_leave = fields.Integer(string='考试假')
+ holiday_id = fields.Many2one('hr.leave.type', string="假期种类")
+ active = fields.Boolean('Active', default=True, tracking=True)
+ # todo 需要什么字段往上加什么
+ # 打卡
+ come_late = fields.Integer(string="迟到")
+ is_early_card = fields.Integer(string="早退")
+ is_no_card = fields.Integer(string="缺卡")
+ is_work = fields.Integer(string='旷工')
+ # 假期
+ unpaid_leave = fields.Integer(string="事假")
+
+ note = fields.Char(string='备注')
+ hr_attendance_month_id = fields.Many2one('hr.attendance.month', string="出勤月度汇总", ondelete="cascade", readonly=True)
+ signature = fields.Image(string="签名", copy=False, attachment=True, max_width=1024, max_height=1024)
+ signed_by = fields.Char(string="签名者", copy=False)
+ signed_on = fields.Datetime(string="签名时间", copy=False)
+ state = fields.Selection(
+ selection=[
+ ('unconfirmed', "未确认"),
+ ('confirmed', "已确认"),
+ ('pending' , "待定"),
+ ],
+ string="状态",
+ readonly=True, copy=False, index=True,
+ tracking=3,
+ default='unconfirmed')
+ # company_area = fields.Selection(related='department_id.company_area', string='* 所属区域')
+ def details_record(self):
+ start_datetime = datetime.datetime.strptime(f"{self.current_year}-{self.current_month}-01", "%Y-%m-%d")
+ start_date = start_datetime.date()
+
+ end_datetime = start_datetime + relativedelta(months=1) - relativedelta(days=1)
+ end_date = end_datetime.date()
+
+ domain = [
+ ('employee_id', '=', self.employee_id.id),
+ ('current_data', '>=', start_date),
+ ('current_data', '<=', end_date),
+ ]
+ return {
+ 'name': '详情',
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'hr.attendance',
+ 'view_mode': 'list',
+ 'domain': domain,
+ }
+
+ def attendance_confirm(self):
+ detail_ids = self.env["hr.attendance.month.detail"].browse(
+ self._context.get('active_ids', self._context.get('active_id')))
+ for record in detail_ids:
+ record.state = 'confirmed'
+
+ def button_pending(self):
+ for record in self:
+ record.write({'state': 'pending'})
+
+ def _get_required_attendance_days(self, employee):
+ """
+ 应出勤天数(按员工考勤日历的工作日统计)
+ :param employee: 员工
+ :return: 应出勤天数
+ """
+ self.ensure_one()
+ days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
+ calendar_attendance = employee.resource_calendar_id.attendance_ids
+ work_days = {str(a.dayofweek) for a in calendar_attendance}
+ required_attendance_days = 0
+ for n in range(days):
+ current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
+ # 0=周一 ... 6=周日
+ if str(current_date.weekday()) in work_days:
+ required_attendance_days += 1
+ return required_attendance_days
+
+ def _get_actual_attendance_days(self, employee):
+ """
+ 实际出勤天数
+ :param employee: 员工
+ :return: 实际出勤天数
+ """
+ self.ensure_one()
+ days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
+ actual_attendance_days = 0
+ for n in range(days):
+ # 根据出勤时间判断是否打卡
+ current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
+ attendance_obj = self.env["hr.attendance"].sudo().search([('employee_id', '=', employee.id), ("current_data", '=', current_date)])
+ is_check_in = False
+ for obj in attendance_obj:
+ if obj.check_in or obj.check_out:
+ # 当天有打卡记录说明已经出勤
+ is_check_in = True
+ if is_check_in is True:
+ actual_attendance_days += 1
+ return actual_attendance_days
+
+ def _get_come_late(self, employee):
+ """
+ 判断是否迟到
+ :param employee: 员工
+ :return: 迟到次数
+ """
+ self.ensure_one()
+ days = calendar.monthrange(int(self.current_year), int(self.current_month))[1]
+ is_late_card = 0
+ is_early_card = 0
+ is_no_card = 0
+ for n in range(days):
+ # 根据出勤时间判断是否打卡
+ current_date = datetime.date(int(self.current_year), int(self.current_month), n + 1)
+ attendance_obj = self.env["hr.attendance"].sudo().search([('employee_id', '=', employee.id), ("current_data", '=', current_date)])
+ for obj in attendance_obj:
+ # 迟到
+ if obj.is_late_card:
+ is_late_card += 1
+ # 早退
+ if obj.is_early_card:
+ is_early_card += 1
+ # 缺卡
+ if obj.is_no_card:
+ is_no_card += 1
+ return is_late_card, is_early_card, is_no_card
+
+ def _hr_attendance_month_detail_action(self, processed_employee_ids):
+ """
+ 返回视图 view_hr_attendance_month_detail_tree
+ :return: ir.actions.act_window 视图 view_hr_attendance_month_detail_tree
+ """
+ action = {
+ 'type': 'ir.actions.act_window',
+ 'res_model': 'hr.attendance.month.detail',
+ 'name': "考勤统计",
+ 'view_type': 'list',
+ 'view_mode': 'list,form',
+ 'domain': [('employee_id', 'in', processed_employee_ids)] if processed_employee_ids else [],
+ 'views': [[self.env.ref('yuthon_hr_attendance.view_hr_attendance_month_detail_tree').id, 'list'],
+ [False, 'form']],
+ }
+ return action
+
+ def _get_hr_leave_type(self, employee_id, type_list):
+ """
+ :param type_list: 请假类型列表
+ :return: 天
+ """
+ self.ensure_one()
+ leave_days = {}
+ year = int(self.current_year)
+ month = int(self.current_month)
+ if month == 12:
+ date_to = date(year, 12, 31)
+ else:
+ next_month = month + 1
+ date_to = date(year, next_month, 1) - datetime.timedelta(days=1)
+
+ date_from = date(year, month, 1)
+ for leave_type in type_list:
+ holiday_status_id = self.env['hr.leave.type'].search([('name', '=', leave_type)])
+ leave_records = self.env['hr.leave'].search([
+ ('employee_id', '=', employee_id.id),
+ ('holiday_status_id', '=', holiday_status_id.id),
+ ('date_from', '<', date_to),
+ ('date_to', '>', date_from),
+ ('state', '=', 'validate')
+ ])
+ leave_days[leave_type] = 0
+ for leave in leave_records:
+ leave_start = leave.date_from.date()
+ leave_end = leave.date_to.date()
+ start_in_month = max(leave_start, date_from)
+ end_in_month = min(leave_end, date_to)
+ # 判断是否跨月
+ is_cross_month = leave_start.year != leave_end.year or leave_start.month != leave_end.month
+ if is_cross_month:
+ if leave_start.year == year and leave_start.month == month:
+ start_date = leave_start
+ if leave_end.year == year and leave_end.month == month:
+ end_date = leave_end
+ else:
+ end_date = date(year, month + 1, 1) if month < 12 else date(year + 1, 1, 1)
+ end_date = end_date - datetime.timedelta(days=1) # 取到当月最后一天
+ elif leave_end.year == year and leave_end.month == month:
+ start_date = date(year, month, 1)
+ end_date = leave_end
+ else:
+ start_date = start_in_month
+ end_date = end_in_month
+ days = self._compute_duration(holiday_status_id, start_date, end_date)
+ else:
+ days = self._compute_duration(holiday_status_id, start_in_month, end_in_month)
+
+ if leave_type in leave_days:
+ leave_days[leave_type] += days
+ else:
+ leave_days[leave_type] = days
+ if leave_type not in leave_days:
+ leave_days[leave_type] = 0
+
+ return leave_days
+
+ def _compute_duration(self, holiday_status_id, start_date, end_date):
+ """
+ 计算请假时长
+ :param holiday_status_id: 请假类型
+ :param start_date: 开始时间
+ :param end_date: 结束时间
+ :return:天
+ """
+ # 如果是自然日:
+ if holiday_status_id.type_of_work == "1":
+ if start_date and end_date:
+ days = (end_date - start_date).days + 1
+ else:
+ days = 0
+ return days
+ if holiday_status_id.type_of_work == 0:
+ current_year = (fields.Date.today() + timedelta(hours=8)).year
+ query_start = datetime.datetime(current_year, 1, 1, 0, 0, 0) - timedelta(hours=8)
+ query_end = datetime.datetime(current_year, 12, 31, 23, 59, 59) - timedelta(hours=8)
+ calendar_leaves = self.env['resource.calendar.leaves'].sudo().search([
+ ("calendar_id.name", "=", "行政考勤工作日"),
+ ("date_from", ">=", query_start),
+ ("date_from", "<=", query_end),
+ ('resource_id', '=', False),
+ ])
+ holidays_list = []
+ shift_days_list = []
+ # 提取节假日和调休日
+ for leave in calendar_leaves:
+ if leave.holidays:
+ holidays_list.extend(leave.holidays.split(','))
+ if leave.alternate_rest_day:
+ holidays_list.extend(leave.alternate_rest_day.split(','))
+ if leave.shift_replacement_day:
+ shift_days_list.extend(leave.shift_replacement_day.split(','))
+
+ holidays_list = [self.remove_leading_zero(date) for date in holidays_list]
+ shift_days_list = [self.remove_leading_zero(date) for date in shift_days_list]
+
+ days = 0 # 初始化工作日计数
+ current_date = start_date
+
+ while current_date <= end_date:
+ current_date_str = current_date.strftime('%m月%d日')
+ current_date_str = self.remove_leading_zero(current_date_str)
+ is_weekend = current_date.weekday() in [5, 6]
+
+ if current_date_str in holidays_list:
+ # 如果是法定节假日,不计为工作日
+ pass
+ elif current_date_str in shift_days_list:
+ # 如果是调休日,无论是否周末都算作工作日
+ days += 1
+ elif not is_weekend:
+ # 如果不是周末且不是节假日,算作工作日
+ days += 1
+
+ current_date += timedelta(days=1)
+ else:
+ delta = end_date - start_date
+ days = delta.days + 1
+ return days
+
+ def remove_leading_zero(self, date_str):
+ month, day = date_str.split('月')
+ month = month.lstrip('0')
+ day = day.rstrip('日')
+ day = day.lstrip('0')
+ return f'{month}月{day}日'
+
+ def analysis_attendance_month_detail(self):
+ """
+ 更新用户的考勤明细
+ :return:
+ """
+ self.ensure_one()
+ if self.state == 'confirmed' and self.signature:
+ return False
+ # 排班的进行统计
+ if self.employee_id.default_planning_role_id:
+ # todo 需要什么字段往上加什么
+ # 迟到, 早退, 缺卡
+ self.come_late, self.is_early_card, self.is_no_card = self._get_come_late(self.employee_id)
+ # 无薪假:hr.leave表
+ days, = self._get_hr_leave_type(self.employee_id, ['事假', '年假'])
+ self.unpaid_leave = days
+
+ # 行政的进行统计
+ if not self.employee_id.default_planning_role_id:
+ # todo 需要什么字段往上加什么
+ # 迟到, 早退, 缺卡
+ self.come_late, self.is_early_card, self.is_no_card = self._get_come_late(self.employee_id)
+ # 无薪假:hr.leave表
+ leave_days = self._get_hr_leave_type(self.employee_id,['事假', '年假', '产假'])
+ self.leave_of_absence = leave_days.get('事假', 0)
+ self.annual_leave = leave_days.get('年假', 0)
+ self.maternity_leave = leave_days.get('产假', 0)
+
+ return True
+
+ def update_attendance_month(self):
+ """
+ 调用用户的考勤明细
+ :return:
+ """
+ for record in self:
+ # 更新用户的考勤明细
+ record.analysis_attendance_month_detail()
+
+ def _get_attendance_month_obj(self):
+ """
+ 获取出勤月度汇总记录结果表
+ :return: obj
+ """
+ attendance_month_obj = self.env["hr.attendance.month"].search([
+ ('current_year', '=', self.current_year),
+ ('current_month', '=', self.current_month)
+ ], limit=1)
+ if not attendance_month_obj:
+ attendance_month_obj = self.env["hr.attendance.month"].sudo().create({
+ "attendance_year_id": self.attendance_year_id.id,
+ "current_year": self.current_year,
+ "current_month": self.current_month,
+ "is_statistics": True,
+ "statistics_time": fields.Datetime.now(),
+ })
+ return attendance_month_obj
+
+ def _get_attendance_month_detail(self, employee, current_year, current_month):
+ """
+ 判断当前是否已经创建了考勤明细
+ :param employee: 员工
+ :return: 考情明细
+ """
+ attendance_month_detail_obj = self.env["hr.attendance.month.detail"].search(
+ [('employee_id', '=', employee.id),
+ ('current_year', '=', current_year),
+ ('current_month', '=', current_month),
+ ], limit=1)
+ if not attendance_month_detail_obj:
+ attendance_month_detail_obj = self.env["hr.attendance.month.detail"].sudo().create({
+ "employee_id": employee.id,
+ "attendance_year_id": self.attendance_year_id.id,
+ "current_year": current_year,
+ "current_month": current_month,
+ })
+ return attendance_month_detail_obj
+
+ def button_compute_all(self):
+ processed_employee_ids = []
+ attendance_month_obj = self._get_attendance_month_obj()
+ # 查询所有的当前员工? 是否需要判断当前员工是否关联企业微信??预留
+ calendars = self.env['resource.calendar'].search([])
+ all_employee_ids = set()
+ for calendar in calendars:
+ all_employee_ids.update(calendar.resource_user_ids.ids)
+ all_employee_ids = list(all_employee_ids)
+ detail_ids = self.env["hr.attendance.month.detail"].browse(self._context.get('active_ids', self._context.get('active_id')))
+ for detail_id in detail_ids:
+ if detail_id.employee_id not in all_employee_ids:
+ # 判断当前是否已经创建了考勤明细
+ attendance_month_detail_obj = self._get_attendance_month_detail(detail_id.employee_id, detail_id.current_year, detail_id.current_month)
+ attendance_month_detail_obj.hr_attendance_month_id = attendance_month_obj.id
+ # 判断当前用户是否已经确认签字,签字后不再更新
+ if attendance_month_detail_obj.state == 'confirmed' and attendance_month_detail_obj.signature:
+ continue
+ # 更新用户的考勤明细
+ attendance_month_detail_obj.analysis_attendance_month_detail()
+ if detail_id.employee_id.id not in processed_employee_ids:
+ processed_employee_ids.append(detail_id.employee_id.id)
+ # 返回包含处理过的员工ID的action动作
+ action = self._hr_attendance_month_detail_action(processed_employee_ids)
+ return action
+
+ def attendance_month_detail(self):
+ attendance_details = self.env['hr.attendance.month.detail'].search([])
+ for detail in attendance_details:
+ detail.write({'state': 'confirmed'})
diff --git a/yuthon_hr_attendance/security/ir.model.access.csv b/yuthon_hr_attendance/security/ir.model.access.csv
new file mode 100644
index 0000000..d1ec5b8
--- /dev/null
+++ b/yuthon_hr_attendance/security/ir.model.access.csv
@@ -0,0 +1,6 @@
+id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
+access_hr_attendance_year_manager,hr.attendance.year.manager,model_hr_attendance_year,base.group_user,1,1,1,1
+access_hr_attendance_month_manager,hr.attendance.month.manager,model_hr_attendance_month,base.group_user,1,1,0,1
+access_hr_attendance_month_detail_user,hr.attendance.month.detail.user,model_hr_attendance_month_detail,base.group_user,1,0,0,0
+access_hr_attendance_month_detail_manager,hr.attendance.month.detail.manager,model_hr_attendance_month_detail,base.group_user,1,1,1,1
+access_card_remind_form,card.remind.form,model_card_remind_form,base.group_user,1,1,1,1
diff --git a/yuthon_hr_attendance/views/attendance_menu.xml b/yuthon_hr_attendance/views/attendance_menu.xml
new file mode 100644
index 0000000..f1f0f2d
--- /dev/null
+++ b/yuthon_hr_attendance/views/attendance_menu.xml
@@ -0,0 +1,15 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/yuthon_hr_attendance/views/card_remind_form.xml b/yuthon_hr_attendance/views/card_remind_form.xml
new file mode 100644
index 0000000..3cbd1a1
--- /dev/null
+++ b/yuthon_hr_attendance/views/card_remind_form.xml
@@ -0,0 +1,30 @@
+
+
+ card.remind.tree
+ card.remind.form
+
+
+
+
+
+
+
+
+
+
+
+ card.remind.search
+ card.remind.form
+
+
+
+
+
+
+
+ 考勤提醒表单
+ card.remind.form
+ list
+ {'group_by': ['department', 'name']}
+
+
diff --git a/yuthon_hr_attendance/views/hr_attendance_month_views.xml b/yuthon_hr_attendance/views/hr_attendance_month_views.xml
new file mode 100644
index 0000000..b1051dc
--- /dev/null
+++ b/yuthon_hr_attendance/views/hr_attendance_month_views.xml
@@ -0,0 +1,278 @@
+
+
+
+ hr.attendance.year.tree
+ hr.attendance.year
+
+
+
+
+
+
+
+
+ 出勤年份维护
+ hr.attendance.year
+ list,form
+
+
+
+ hr.attendance.month.tree
+ hr.attendance.month
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.month.form
+ hr.attendance.month
+
+
+
+
+
+
+ 出勤统计
+ hr.attendance.month
+ list,form
+
+
+
+ hr.attendance.month.detail.tree
+ hr.attendance.month.detail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.month.detail.tree
+ hr.attendance.month.detail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.month.detail.form
+ hr.attendance.month.detail
+
+
+
+
+
+ hr.attendance.month.detail.form
+ hr.attendance.month.detail
+
+
+
+
+
+
+ hr.attendance.month.detail.search
+ hr.attendance.month.detail
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 考勤确认单
+ hr.attendance.month.detail
+ list,form
+
+
+ {
+ "search_default_group_by_current_year_id":1,
+ "search_default_group_by_current_month":1
+ }
+
+
+
+ 考勤确认
+ hr.attendance.month.detail
+ list,form
+ [('employee_id.user_id', '=', uid)]
+
+
+
diff --git a/yuthon_hr_attendance/views/hr_attendance_views.xml b/yuthon_hr_attendance/views/hr_attendance_views.xml
new file mode 100644
index 0000000..7e39ee7
--- /dev/null
+++ b/yuthon_hr_attendance/views/hr_attendance_views.xml
@@ -0,0 +1,95 @@
+
+
+
+ hr.attendance.going.list
+ hr.attendance
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ hr.attendance.search
+ hr.attendance
+
+
+
+
+
+
+
+
+
+ hr.attendance.search
+ hr.attendance
+
+
+
+
+
+
+
+
+
+
+
+
+
+ 我的考勤
+ hr.attendance
+ list
+ [('employee_id.user_id', '=', uid)]
+
+ {'search_default_group_by_current_data': 1}
+
+
+
+ 我的异常
+ hr.attendance
+ list,form
+ [('employee_id.user_id', '=', uid), '|', '|', '|', '|', ('is_abnormal',
+ '=', True), ('is_early_card', '=', True), ('is_late_card', '=', True),
+ ('absenteeism_card', '=', True), ('is_no_card', '=', True)]
+
+ {'search_default_group_by_current_data': 1}
+
+
+
+ 考勤记录
+ hr.attendance
+ list
+
+
+