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 + + + + \ 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 + + + + + + + + +