From 48eeaf7405927a918557711ab9fb32b458983fb1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Mon, 1 Jun 2026 09:27:21 +0800 Subject: [PATCH 1/7] =?UTF-8?q?=E8=80=83=E5=8B=A4=E6=97=A5=E5=BF=97?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- attendance_iot/controllers/kaoqin_test.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/attendance_iot/controllers/kaoqin_test.py b/attendance_iot/controllers/kaoqin_test.py index 7fb7a43b8..48c7151f2 100644 --- a/attendance_iot/controllers/kaoqin_test.py +++ b/attendance_iot/controllers/kaoqin_test.py @@ -50,7 +50,12 @@ class BusController(Controller): 获取上传识别成功记录 :return: """ - _logger.info("----------------/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") try: data = request.httprequest.json except Exception as e: From 480452dd5816f6b50a315d3b085359464a43fec8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Mon, 1 Jun 2026 22:13:46 +0800 Subject: [PATCH 2/7] =?UTF-8?q?=E8=80=83=E5=8B=A4=E7=A1=AE=E8=AE=A4?= =?UTF-8?q?=E5=8D=95=E9=87=8D=E5=A4=8D=E5=8F=91=E9=80=81=E4=BC=98=E5=8C=96?= =?UTF-8?q?=EF=BC=8C=E8=80=83=E5=8B=A4=E7=A1=AE=E8=AE=A4=E5=8D=95=E7=A1=AE?= =?UTF-8?q?=E8=AE=A4=E9=80=BB=E8=BE=91=E4=BC=98=E5=8C=96=EF=BC=8C=E5=9C=A8?= =?UTF-8?q?=E6=9C=89=E6=B5=81=E7=A8=8B=E6=83=85=E5=86=B5=E4=B8=8B=E4=B8=8D?= =?UTF-8?q?=E5=8F=AF=E4=BB=A5=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- attendance_iot/controllers/kaoqin_test.py | 7 +--- .../controllers/applet_attendance.py | 14 ++++++-- yuthon_hr_attendance/controllers/portal.py | 24 +++++++++++++ .../models/yuthon_vacation_reports.py | 36 +++++++++++++++++++ 4 files changed, 72 insertions(+), 9 deletions(-) diff --git a/attendance_iot/controllers/kaoqin_test.py b/attendance_iot/controllers/kaoqin_test.py index 48c7151f2..7fb7a43b8 100644 --- a/attendance_iot/controllers/kaoqin_test.py +++ b/attendance_iot/controllers/kaoqin_test.py @@ -50,12 +50,7 @@ class BusController(Controller): 获取上传识别成功记录 :return: """ - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") - _logger.info("----------------考勤机打卡/api/v1/record/face---------接口收到响应-------------------------------") + _logger.info("----------------/api/v1/record/face---------接口收到响应-------------------------------") try: data = request.httprequest.json except Exception as e: diff --git a/yuthon_hr_attendance/controllers/applet_attendance.py b/yuthon_hr_attendance/controllers/applet_attendance.py index de5e8979e..1c173b4a0 100644 --- a/yuthon_hr_attendance/controllers/applet_attendance.py +++ b/yuthon_hr_attendance/controllers/applet_attendance.py @@ -670,13 +670,21 @@ class AppletAttendance(Controller): """ id = int(kw['id']) record = request.env['yuthon.vacation.reports'].sudo().browse(id) - record.manage_reject() - # 这里需要确认提交 + # 检查是否有未办结的人力流程 + has_pending, pending_tip = record._has_pending_hr_workflow( + record.employee_id.id, record.vacation_date) + if has_pending: + return json.dumps({ + 'code': 403, + 'message': pending_tip, + 'data': None + }) + record.vacation_state = 'confirmed' return_data = { 'code': '200', - 'message': 'OK', + 'message': '确认成功', 'data': id } return json.dumps(return_data) diff --git a/yuthon_hr_attendance/controllers/portal.py b/yuthon_hr_attendance/controllers/portal.py index 06a2a149a..71cf6794c 100644 --- a/yuthon_hr_attendance/controllers/portal.py +++ b/yuthon_hr_attendance/controllers/portal.py @@ -56,6 +56,7 @@ class CustomerPortal(portal.CustomerPortal): users_id = int(kw.get('users_id')) user = request.env['res.users'].sudo().browse(int(users_id)) employee_id = user.with_company(user.company_id.id).employee_id.id + employee = request.env['hr.employee'].sudo().browse(employee_id) records = request.env['yuthon.vacation.reports'].sudo().search([('employee_id.id', '=', employee_id)]) data = {} for record in records: @@ -64,10 +65,17 @@ class CustomerPortal(portal.CustomerPortal): if key not in data: data[key] = [] vacation_state = '已确认' if record.vacation_state == 'confirmed' else '待确认' + + # 检查是否有未办结的人力流程 + has_pending, pending_tip = record._has_pending_hr_workflow(employee_id, record.vacation_date) + can_confirm = record.vacation_state != 'confirmed' and not has_pending + data[key].append({ 'id': record.id, 'state': vacation_state, 'month': record.vacation_date.month, + 'can_confirm': can_confirm, + 'pending_tip': pending_tip if has_pending else '', }) return json.dumps({'data': data, 'code': 200, 'message': ''}) @@ -83,8 +91,15 @@ class CustomerPortal(portal.CustomerPortal): return f"{round(num, 1)}{unit}" return f"{num}{unit}" + # 检查是否有未办结的人力流程 + employee_id = record.employee_id.id + has_pending, pending_tip = record._has_pending_hr_workflow(employee_id, record.vacation_date) + can_confirm = record.vacation_state != 'confirmed' and not has_pending + data = { 'state': '已确认' if record.vacation_state == 'confirmed' else '待确认', + 'can_confirm': can_confirm, + 'pending_tip': pending_tip if has_pending else '', '日期': record.display_date, '应到': f"{record.should_attendance}天", '实到': f"{record.actual_attendance}天", @@ -118,6 +133,15 @@ class CustomerPortal(portal.CustomerPortal): @http.route('/phone/vacation/self/status', type='http', auth="none", csrf=False, cors='*') def phone_vacation_self_status(self, **kw): record_id = request.env['yuthon.vacation.reports'].sudo().search([('id', '=', kw.get('id'))]) + if not record_id: + return json.dumps({'code': 400, 'message': '记录不存在'}) + + # 检查是否有未办结的人力流程 + has_pending, pending_tip = record_id._has_pending_hr_workflow( + record_id.employee_id.id, record_id.vacation_date) + if has_pending: + return json.dumps({'code': 403, 'message': pending_tip}) + record_id.write({'vacation_state': 'confirmed'}) return json.dumps({'code': 200, 'message': 'OK'}) diff --git a/yuthon_hr_attendance/models/yuthon_vacation_reports.py b/yuthon_hr_attendance/models/yuthon_vacation_reports.py index f8a4e8d2c..a27f8bfef 100644 --- a/yuthon_hr_attendance/models/yuthon_vacation_reports.py +++ b/yuthon_hr_attendance/models/yuthon_vacation_reports.py @@ -1378,7 +1378,13 @@ class YuthonVacationReports(models.Model): ('vacation_date', '=', last_month_first), ('vacation_state', '=', 'not_sent'), ]) + processed_employees = set() for report in report_objs: + if report.employee_id.id in processed_employees: + # 同员工已发送过通知,多余记录只确认不重复发 + report.write({'vacation_state': 'confirmed'}) + continue + processed_employees.add(report.employee_id.id) resource_user_ids = report.employee_id.resource_calendar_id.resource_user_ids.ids if report.employee_id.id in resource_user_ids: report.write({'vacation_state': 'confirmed'}) @@ -1402,7 +1408,12 @@ class YuthonVacationReports(models.Model): ('vacation_date', '=', last_month_first), ('vacation_state', '=', 'not_sent'), ]) + processed_employees = set() for report in report_objs: + if report.employee_id.id in processed_employees: + report.write({'vacation_state': 'confirmed'}) + continue + processed_employees.add(report.employee_id.id) resource_user_ids = report.employee_id.resource_calendar_id.resource_user_ids.ids if report.employee_id.id in resource_user_ids: report.write({'vacation_state': 'confirmed'}) @@ -2711,6 +2722,31 @@ class YuthonVacationReports(models.Model): } } + def _has_pending_hr_workflow(self, employee_id, month_date): + """ + 检查员工在指定月份是否存在未办结的人力资源相关流程(请假、加班、外出等) + :param employee_id: hr.employee.id + :param month_date: 月份任一天(用于计算当月起止) + :return: (bool, str) 是否有未办结流程, 提示文字 + """ + month_start = month_date.replace(day=1) + month_end = (month_start + relativedelta(months=1) - relativedelta(days=1)) + + # 先找到员工对应的 user + user = self.env['hr.employee'].sudo().browse(employee_id).user_id + if not user: + return False, '' + pending = self.env['soong.workflow.record'].sudo().search_count([ + ('users_ids', 'in', user.id), + ('approvals_group_id.name', '=', '考勤'), + ('approve_state', '=', 'pending'), + ('workflow_happen_date', '>=', month_start), + ('workflow_happen_date', '<=', month_end), + ]) + if pending: + return True, '该月考勤相关流程尚未完结,暂不允许确认' + return False, '' + class VacationLeaveLine(models.Model): _name = 'vacation.leave.line' _description = '年假明细' From fb964db243d84cccbc4918e5f916eaf3d6165276 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Mon, 1 Jun 2026 22:18:32 +0800 Subject: [PATCH 3/7] =?UTF-8?q?=E8=80=83=E5=8B=A4=E6=9C=BA=E4=BA=BA?= =?UTF-8?q?=E8=84=B8=E4=BF=A1=E6=81=AF=E5=90=8C=E6=AD=A5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- attendance_iot/controllers/kaoqin_test.py | 44 +++++++++++++++++++++++ 1 file changed, 44 insertions(+) diff --git a/attendance_iot/controllers/kaoqin_test.py b/attendance_iot/controllers/kaoqin_test.py index 7fb7a43b8..b50e7cb47 100644 --- a/attendance_iot/controllers/kaoqin_test.py +++ b/attendance_iot/controllers/kaoqin_test.py @@ -17,6 +17,43 @@ def remove_base64_prefix(base64_str: str) -> str: class BusController(Controller): + def _sync_to_other_devices(self, employee, source_device, photo=None): + """ + 将员工人脸信息同步到同公司的其他设备 + 利用 attendance.iot.user 的 create / action_update_iot_info 自动触发 MQTT 推送 + """ + if not employee or not source_device: + return + # 查找同公司的其他设备 + company = source_device.company_id or employee.company_id + other_devices = request.env(user=SUPERUSER_ID)['attendance.iot'].search([ + ('company_id', '=', company.id if company else False), + ('id', '!=', source_device.id), + ]) + if not other_devices: + return + + # 如果 photo 不为空,顺便更新员工头像 + if photo: + photo = urllib.parse.unquote(photo) if isinstance(photo, str) else photo + photo = remove_base64_prefix(photo) + employee.with_context(iot_sync=True).write({'avatar_image': photo}) + + for device in other_devices: + existed = request.env(user=SUPERUSER_ID)['attendance.iot.user'].search([ + ('employee_id', '=', employee.id), + ('attendance_iot_id', '=', device.id), + ], limit=1) + if existed: + # 已有绑定 → 触发 MQTT editUser 推送到该设备 + existed.with_context(iot_sync=False).action_update_iot_info() + else: + # 未绑定 → 创建绑定,create() 内部会自动触发 MQTT addUser + request.env(user=SUPERUSER_ID)['attendance.iot.user'].with_context(iot_sync=False).create({ + 'employee_id': employee.id, + 'attendance_iot_id': device.id, + }) + @route('/api/v1/addFace', methods=['POST'], type='json', auth='none', csrf=False) def get_model_kaoqin(self): """ @@ -41,6 +78,8 @@ class BusController(Controller): 'attendance_iot_id': attendance_iot_id.id })] }) + # 同步到同公司的其他设备 + self._sync_to_other_devices(employee, attendance_iot_id, data.get('photo')) _logger.info("-------------------------%s创建成功-------------------------------" % (employee.name)) return {"Result": 0, "Msg": "添加成功"} @@ -108,6 +147,8 @@ class BusController(Controller): 'employee_id': employee.id, 'attendance_iot_id': iot_device.id, }) + # 同步到同公司的其他设备 + self._sync_to_other_devices(employee, iot_device, vl_photo) return {"Result": 0, "Msg": "添加成功"} @route('/api/v1/uploadFaceInfo', methods=['POST'], type='json', auth='none', csrf=False) @@ -155,6 +196,9 @@ class BusController(Controller): photo = remove_base64_prefix(photo) employee.with_context(iot_sync=True).write({'avatar_image': photo}) + # 同步到同公司的其他设备(uploadFaceInfo 是设备主动发起的数据同步,同样需要推给其他设备) + self._sync_to_other_devices(employee, iot_device, u.get('photo')) + return {"Result": 0, "Msg": "更新成功"} @http.route('/phone/attendance/list', type='http', auth="none", csrf=False, cors='*') From dfae1fa383c370b690f712a87f905fa421734954 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E6=9D=8E=E9=B9=8F=E5=AE=87?= Date: Mon, 1 Jun 2026 22:24:00 +0800 Subject: [PATCH 4/7] =?UTF-8?q?=E8=A1=A5=E4=BC=91=E6=8C=89=E9=92=AE?= =?UTF-8?q?=E6=B7=BB=E5=8A=A0=EF=BC=8C=E9=80=BB=E8=BE=91=E5=BE=85=E5=AE=8C?= =?UTF-8?q?=E5=96=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- tx_planning_resource/models/planning_slot.py | 17 +++++++++++++++++ .../views/inherit_planning_slot_views.xml | 6 +++++- 2 files changed, 22 insertions(+), 1 deletion(-) diff --git a/tx_planning_resource/models/planning_slot.py b/tx_planning_resource/models/planning_slot.py index b89e21f82..01cc7f890 100644 --- a/tx_planning_resource/models/planning_slot.py +++ b/tx_planning_resource/models/planning_slot.py @@ -26,3 +26,20 @@ class TxPlanningSlot(models.Model): data_list = [r.get('resource_id', {}).get('id') for r in res if r.get('resource_id', {})] self.env['resource.resource'].search([('id', 'in', data_list)], order="sequence,name") return res + + def action_compensatory_leave(self): + """ + 补休按钮 - 将当前排班转为补休记录 + TODO: 具体业务逻辑由客户确认后补充 + """ + self.ensure_one() + return { + 'type': 'ir.actions.client', + 'tag': 'display_notification', + 'params': { + 'title': '补休', + 'message': f'已为 {self.resource_id.name} 创建补休申请(待实现)', + 'sticky': False, + 'type': 'success', + } + } diff --git a/yuthon_hr_attendance/views/inherit_planning_slot_views.xml b/yuthon_hr_attendance/views/inherit_planning_slot_views.xml index 671c8d63e..afaf92c77 100644 --- a/yuthon_hr_attendance/views/inherit_planning_slot_views.xml +++ b/yuthon_hr_attendance/views/inherit_planning_slot_views.xml @@ -27,7 +27,11 @@ - + + +