diff --git a/soong_workflow/models/soong_workflow_node.py b/soong_workflow/models/soong_workflow_node.py index 5f08a570f..30777f391 100644 --- a/soong_workflow/models/soong_workflow_node.py +++ b/soong_workflow/models/soong_workflow_node.py @@ -347,6 +347,7 @@ class SoongWorkflowNodeButton(models.Model): ], ) is_review_invisible = fields.Boolean(string="使用范围", default=False) + illustrate = fields.Text(string="说明") reviewer_id = fields.Many2one(comodel_name="res.users", string="Reviewer") reviewer_group_id = fields.Many2one(comodel_name="res.groups", string="Reviewer group") users_role_id = fields.Many2one(comodel_name="soong.user.role", string="主角色",) diff --git a/soong_workflow/views/soong_workflow_node_views.xml b/soong_workflow/views/soong_workflow_node_views.xml index 017c41899..cfac60c58 100644 --- a/soong_workflow/views/soong_workflow_node_views.xml +++ b/soong_workflow/views/soong_workflow_node_views.xml @@ -168,7 +168,7 @@ - + + + + - diff --git a/soong_workflow/wizard/soong_workflow_comment_wizard.py b/soong_workflow/wizard/soong_workflow_comment_wizard.py index 57a2651d3..2b5afd493 100644 --- a/soong_workflow/wizard/soong_workflow_comment_wizard.py +++ b/soong_workflow/wizard/soong_workflow_comment_wizard.py @@ -499,7 +499,21 @@ class SoongWorkflowCommentWizard(models.TransientModel): self.env['soong.workflow.record'].search([('code', '=', record_id.code)]).write({'approve_state': 'end', "employee_ids": False, "node_id": False,}) + + # 给申请人发消息 + biz_record = self.env[self.res_model].browse(self.res_id) + if hasattr(biz_record, 'user_id') and biz_record.user_id: + employee = biz_record.user_id.employee_id + elif hasattr(biz_record, 'users_id') and biz_record.users_id: + employee = biz_record.users_id.employee_id action_id = self.env['ir.actions.act_window'].search([('res_model', '=', self.res_model)])[0] + url = f'https://oa.thtzjt.com/web?login={employee.user_id.login}&oauth_uid={employee.wecom_userid}#id={record_id.id}&cids=2&action={action_id.id}&model={record_id._name}&view_type=form&menu_id=10104' + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='oa', + user_id=employee.wecom_userid, + title=record_id._description, + description=f"编号为{record_id.code}的{record_id._description}已结束", + url=url, btntxt="查看详情") + recipients = [] if self.node_btn_id.workflow_id.archive_type == 'applicant': biz_record = self.env[self.res_model].browse(self.res_id) diff --git a/yuthon_holiday/views/hr_leave_batch.xml b/yuthon_holiday/views/hr_leave_batch.xml index 7cf502a27..6b2471a46 100644 --- a/yuthon_holiday/views/hr_leave_batch.xml +++ b/yuthon_holiday/views/hr_leave_batch.xml @@ -60,8 +60,7 @@ - - + @@ -126,13 +125,10 @@ - - - - - diff --git a/yuthon_hr_attendance/data/mothly_report_time.xml b/yuthon_hr_attendance/data/mothly_report_time.xml index 817b2719c..df2868ac1 100644 --- a/yuthon_hr_attendance/data/mothly_report_time.xml +++ b/yuthon_hr_attendance/data/mothly_report_time.xml @@ -27,5 +27,31 @@ -1 + + 定时任务:发送我的考勤确认 + + code + model.attendance_confirmation() + + + 1 + months + + -1 + + + + 定时任务:考勤报表自动确认 + + code + model.automatic_confirmation() + + + 1 + months + + -1 + + diff --git a/yuthon_hr_attendance/models/hr_attendance.py b/yuthon_hr_attendance/models/hr_attendance.py index 6e6240eff..9ee452f0a 100644 --- a/yuthon_hr_attendance/models/hr_attendance.py +++ b/yuthon_hr_attendance/models/hr_attendance.py @@ -939,8 +939,26 @@ class HrAttendance(models.Model): record.shift_name = '' else: record.shift_name = '行政班次' + self._update_vacation_report(record) return record + def write(self, vals): + result = super(HrAttendance, self).write(vals) + for record in self: + self._update_vacation_report(record) + return result + + def _update_vacation_report(self, record): + """更新相关的假期报表日志""" + if record.current_data and record.employee_id: + current_date = record.current_data.replace(day=1) + report = self.env['yuthon.vacation.reports'].search([ + ('employee_id', '=', record.employee_id.id), + ('vacation_date', '=', current_date) + ], limit=1) + if report: + report.compute_attendance_fields() + 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: diff --git a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py index 9dd789721..200136409 100644 --- a/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py +++ b/yuthon_hr_attendance/models/hr_attendance_detail_wechat.py @@ -353,27 +353,44 @@ class HrAttendanceDetail(models.Model): if self._process_shift_days(current_data): return is_early_card, is_late_card, is_abnormal, is_warning, absenteeism_card + is_tianhe_property = employee.company_id.name == "天河物管" # 迟到判断 if check_in and start_datetime and check_in > start_datetime: - if employee.late_allowed > 0 : - is_late_card = False - employee.late_allowed -= 1 - else: - is_late_card = True - late_delta = (check_in - start_datetime).total_seconds() / 60 - # 迟到超过60分钟标记为旷工 - if late_delta > 60: - absenteeism_card = True + if is_tianhe_property: + # 天河物管:2分钟包容时间 + if employee.late_allowed == 0: + if late_delta > 2: + is_late_card = True + else: + # 迟到超过10分钟 扣允许迟到次数 并且也算迟到 + employee.late_allowed -= 1 + if late_delta > 10: + is_late_card = True + # 迟到超过60分钟标记为旷工 + if late_delta > 60: + absenteeism_card = True + else: + if check_in > start_datetime: + is_late_card = True + # 迟到超过60分钟标记为旷工 + if late_delta > 60: + absenteeism_card = True # 早退判断 - if check_out and end_datetime and check_out < end_datetime: - is_early_card = True - # 计算早退时长(分钟) + if check_out and end_datetime: early_delta = (end_datetime - check_out).total_seconds() / 60 - # 早退超过60分钟标记为旷工 - if early_delta > 60: - absenteeism_card = True + if is_tianhe_property: + # 天河物管:超过2分钟才算早退 + if early_delta > 2: + is_early_card = True + if early_delta > 60: + absenteeism_card = True + else: + if check_out < end_datetime: + is_early_card = True + if early_delta > 60: + absenteeism_card = True # 异常和警告判断 if is_early_card or is_late_card: diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index a28577045..154e9cb4e 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -4,6 +4,7 @@ from odoo import models, fields, api, _ from odoo.exceptions import ValidationError import calendar from datetime import date +from dateutil.relativedelta import relativedelta class YuthonVacationReports(models.Model): @@ -15,6 +16,7 @@ class YuthonVacationReports(models.Model): employee_id = fields.Many2one('hr.employee', string='员工') department_id = fields.Many2one(related='employee_id.department_id', string="部门") company_id = fields.Many2one(related="employee_id.company_id", string="公司") + late_allowed = fields.Float(related="employee_id.late_allowed", string="允许迟到次数") leave_ids = fields.One2many('vacation.leave.line', 'vacation_id', string='年假明细') overtime_ids = fields.One2many('vacation.over.line', 'vacation_id', string='加班明细') meal_fee_ids = fields.One2many('vacation.meal.line', 'vacation_id', string='误餐明细') @@ -52,13 +54,13 @@ class YuthonVacationReports(models.Model): internship_days = fields.Float(string='实习天数', group_operator=None) should_attendance = fields.Integer(string="应到", store=True, group_operator=None) - actual_attendance = fields.Integer(string="实到", compute='_compute_attendance_fields', store=True,group_operator=None) + actual_attendance = fields.Integer(string="实到", compute='compute_attendance_fields', store=True,group_operator=None) - is_late_card = fields.Integer(string="迟到", compute='_compute_attendance_fields', store=True, group_operator=None) - is_early_card = fields.Integer(string="早退", compute='_compute_attendance_fields', store=True, group_operator=None) - is_no_card = fields.Integer(string="缺卡", compute='_compute_attendance_fields', store=True, group_operator=None) - is_work = fields.Integer(string='旷工', compute='_compute_attendance_fields', store=True, group_operator=None) - is_abnormal = fields.Integer(string="异常", compute='_compute_attendance_fields', store=True, group_operator=None) + is_late_card = fields.Integer(string="迟到", compute='compute_attendance_fields', store=True, group_operator=None) + is_early_card = fields.Integer(string="早退", compute='compute_attendance_fields', store=True, group_operator=None) + is_no_card = fields.Integer(string="缺卡", compute='compute_attendance_fields', store=True, group_operator=None) + is_work = fields.Integer(string='旷工', compute='compute_attendance_fields', store=True, group_operator=None) + is_abnormal = fields.Integer(string="异常", compute='compute_attendance_fields', store=True, group_operator=None) vacation_state = fields.Selection( selection=[ @@ -71,13 +73,49 @@ class YuthonVacationReports(models.Model): tracking=3, default='unconfirmed') + def confirm_state(self): + for rec in self: + rec.vacation_state = 'confirmed' + # 计算下个月的日期(基于当前记录的vacation_date) + current_date = rec.vacation_date + if current_date.month == 12: + next_month_date = current_date.replace(year=current_date.year + 1, month=1, day=1) + else: + next_month_date = current_date.replace(month=current_date.month + 1, day=1) + + # 查找下个月是否已有该员工的记录 + next_month_record = self.search([ + ('employee_id', '=', rec.employee_id.id), + ('vacation_date', '=', next_month_date) + ], limit=1) + + inherit_vals = { + 'leave': rec.leave, + 'working_overtime': rec.working_overtime, + 'medical_fee': rec.medical_fee, + } + if next_month_record: + # 如果下个月记录已存在,更新继承字段 + next_month_record.write(inherit_vals) + + # 处理年假明细行继承 + if rec.leave: + leave_hours = rec.leave * 8 + self._update_leave_lines(next_month_record, [rec], leave_hours) + + # 处理补休明细行继承 + if rec.working_overtime: + overtime_hours = rec.working_overtime * 8 + self._update_over_lines(next_month_record, [rec], overtime_hours) + + @api.depends('vacation_date') def _compute_display_date(self): for rec in self: rec.display_date = rec.vacation_date.strftime('%Y年%m月') @api.depends('employee_id', 'vacation_date') - def _compute_attendance_fields(self): + def compute_attendance_fields(self): for rec in self: # 初始化字段值 is_late = 0 @@ -131,28 +169,19 @@ class YuthonVacationReports(models.Model): self._context.get('active_ids', self._context.get('active_id', self.ids)) ) for rec in records: - leave_days = 0 - actual_attendance = 0 - if rec.employee_id and rec.vacation_date: - year = rec.vacation_date.year - month = rec.vacation_date.month - days_in_month = calendar.monthrange(year, month)[1] - month_start = date(year, month, 1) - month_end = date(year, month, days_in_month) - leave_days = rec._create_leave_record(month_start, month_end) - for day in range(1, days_in_month + 1): - current_date = date(year, month, day) - attendances = self.env["hr.attendance"].sudo().search([ - ('employee_id', '=', rec.employee_id.id), - ("current_data", '=', current_date) - ]) - if attendances: - has_valid_check = any(att.check_in or att.check_out for att in attendances) - if has_valid_check: - actual_attendance += 1 + rec.compute_attendance_fields() - rec.should_attendance = leave_days - rec.actual_attendance = actual_attendance + def update_data(self): + august_start = fields.Date.from_string('2024-08-01') + august_end = fields.Date.from_string('2024-08-31') + august_records = self.env["yuthon.vacation.reports"].search([ + ('vacation_date', '>=', august_start), + ('vacation_date', '<=', august_end) + ]) + if august_records: + august_records.write({ + 'vacation_date': august_start + }) @api.depends('other_fee_ids', 'other_fee_ids.holiday_type', 'other_fee_ids.lines', @@ -240,8 +269,8 @@ class YuthonVacationReports(models.Model): return f'{month}月{day}日' def button_put(self): - menu_id = self.env['ir.ui.menu'].search([('name', '=', '考勤确认'), ('parent_id', '!=', False)]) - url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=hr.attendance.month.detail&view_type=list&menu_id={menu_id.id}' + menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的考勤确认'), ('parent_id', '!=', False)]) + url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=yuthon.vacation.reports&view_type=list&menu_id={menu_id.id}' we_id = self.env['hr.employee'].search([('id', '=', self.employee_id.id)]).wecom_userid self.sudo().env["wecom.apps"].sync_send_message_textcard(category='default', user_id=we_id, title="考勤确认", description=self.employee_id.name + '请确认你的考勤:', @@ -260,7 +289,7 @@ class YuthonVacationReports(models.Model): detail_ids = self.env["yuthon.vacation.reports"].browse(self._context.get('active_ids', self._context.get('active_id'))) unique_employee_ids = list({detail.employee_id.id for detail in detail_ids if detail.employee_id}) for employee_id in unique_employee_ids: - menu_id = self.env['ir.ui.menu'].search([('name', '=', '考勤确认'), ('parent_id', '!=', False)]) + menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的考勤确认'), ('parent_id', '!=', False)]) url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=hr.attendance.month.detail&view_type=list&menu_id={menu_id.id}' we_id = self.env['hr.employee'].search([('id', '=', employee_id)]).wecom_userid self.sudo().env["wecom.apps"].sync_send_message_textcard(category='attendance', user_id=we_id, title="考勤确认", @@ -276,6 +305,58 @@ class YuthonVacationReports(models.Model): } } + def attendance_confirmation(self): + for record in self: + employee = self.env['hr.employee'].search([('name', '=', '叶华锋')]) + menu_id = self.env['ir.ui.menu'].search([('name', '=', '我的考勤确认'), ('parent_id', '!=', False)]) + url = f'https://oa.thtzjt.com/web#id={self.id}&cids=2&action={menu_id.action.id}&model=yuthon.vacation.reports&view_type=list&menu_id={menu_id.id}' + we_id = self.env['hr.employee'].search([('id', '=', employee.id)]).wecom_userid + self.sudo().env["wecom.apps"].sync_send_message_textcard(category='default', user_id=we_id, + title="考勤确认", + description=employee.name + '请确认你的考勤:', + url=url, btntxt="详细信息") + + def automatic_confirmation(self): + for rec in self: + today = fields.Date.today() + employee = self.env['hr.employee'].search([('name', '=', '叶华锋')]) + rec.vacation_state = 'confirmed' + # 计算上个月报表的日期 + current_date = today + if current_date.month == 12: + last_month_date = current_date.replace(year=current_date.year - 1, month=1, day=1) + else: + last_month_date = current_date.replace(month=current_date.month - 1, day=1) + + last_month_record = self.search([ + ('employee_id', '=', employee.id), + ('vacation_date', '=', last_month_date) + ], limit=1) + + next_month_record = self.search([ + ('employee_id', '=', employee.id), + ('vacation_date', '=', last_month_date + relativedelta(months=1)) + ], limit=1) + + inherit_vals = { + 'leave': last_month_record.leave, + 'working_overtime': last_month_record.working_overtime, + 'medical_fee': last_month_record.medical_fee, + } + if next_month_record: + # 如果下个月记录已存在,更新继承字段 + next_month_record.write(inherit_vals) + + # 处理年假明细行继承 + if rec.leave: + leave_hours = rec.leave * 8 + self._update_leave_lines(next_month_record, [rec], leave_hours) + + # 处理补休明细行继承 + if rec.working_overtime: + overtime_hours = rec.working_overtime * 8 + self._update_over_lines(next_month_record, [rec], overtime_hours) + @api.depends('medical_fee_ids.update_type', 'medical_fee_ids.lines', 'medical_fee_ids.is_void') def _compute_medical_fee(self): @@ -444,13 +525,15 @@ class YuthonVacationReports(models.Model): ], limit=1) if not existing_record and not is_resource_user: - # 获取上月记录 + # 仅查找已确认的上月记录 last_month_record = self.search([ ('employee_id', '=', employee.id), ('vacation_date', '>=', fields.Date.to_string(last_month_first)), - ('vacation_date', '<=', fields.Date.to_string(last_month_last)) + ('vacation_date', '<=', fields.Date.to_string(last_month_last)), + ('vacation_state', '=', 'confirmed') # 关键条件:仅已确认的记录 ], limit=1) - inherit_vals = {} + + # 只有找到已确认的上月记录时才进行继承 if last_month_record: inherit_vals = { 'leave': last_month_record.leave, @@ -458,13 +541,13 @@ class YuthonVacationReports(models.Model): 'medical_fee': last_month_record.medical_fee, } - # 创建本月报告主记录 - new_report = self.create({ - 'employee_id': employee.id, - 'vacation_date': today, - **inherit_vals - }) - if last_month_record: + # 创建本月报告主记录(包含继承的值) + new_report = self.create({ + 'employee_id': employee.id, + 'vacation_date': today, + **inherit_vals + }) + # 处理年假明细行(继承额度) if last_month_record.leave: leave_hours = last_month_record.leave * 8 @@ -475,6 +558,13 @@ class YuthonVacationReports(models.Model): overtime_hours = last_month_record.working_overtime * 8 self._update_over_lines(new_report, [last_month_record], overtime_hours) + else: + # 如果没有已确认的上月记录,创建空的本月报告 + self.create({ + 'employee_id': employee.id, + 'vacation_date': today, + }) + def _update_leave_lines(self, report, allocations, number_of_hours_display): for alloc in allocations: self.env['vacation.leave.line'].create({ @@ -571,6 +661,16 @@ class VacationOverline(models.Model): adjust_date = fields.Date(string='调整至') text = fields.Text(string='备注') + def open_record(self): + if self.workflow_record_id: + return { + "name": "流程详情", + 'type': 'ir.actions.act_window', + 'res_model': f'{self.workflow_record_id.res_model}', + 'res_id': self.workflow_record_id.res_id, + 'view_mode': 'form', + } + # def _sync_to_employee(self, reverse=False): # """将数据同步到关联员工 # :param reverse: 是否反向操作(用于删除场景) diff --git a/yuthon_hr_attendance/views/hr_attendance_views.xml b/yuthon_hr_attendance/views/hr_attendance_views.xml index 779c647ad..6e5c5a599 100644 --- a/yuthon_hr_attendance/views/hr_attendance_views.xml +++ b/yuthon_hr_attendance/views/hr_attendance_views.xml @@ -28,8 +28,8 @@ - - + + @@ -69,8 +69,8 @@ - - + + diff --git a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml index b88d164d2..6b17243d3 100644 --- a/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml +++ b/yuthon_hr_attendance/views/yuthon_vacation_reports_views.xml @@ -14,6 +14,7 @@ + @@ -54,11 +55,13 @@ + + @@ -86,6 +89,7 @@