!11837 多的记录被过滤掉了,保安部

Merge pull request !11837 from 李鹏宇/dev
This commit is contained in:
李鹏宇
2026-06-01 14:56:43 +00:00
committed by Gitee
11 changed files with 236 additions and 12 deletions
+44
View File
@@ -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='*')
@@ -92,3 +92,27 @@ class AttendanceIotUser(models.Model):
'device_sn': record.attendance_iot_id.name,
'message_data': json.dumps(update_value, ensure_ascii=False)
}])
def action_batch_sync_to_all_devices(self):
"""
批量同步:将勾选员工的人脸数据,推送到该员工关联的全部设备
"""
if self._context.get('iot_sync'):
return
for record in self:
employee = record.employee_id
# 找到该员工在所有设备上的绑定记录
all_bindings = self.search([('employee_id', '=', employee.id)])
if all_bindings:
# action_update_iot_info 内部会跳过 iot_sync 上下文
all_bindings.with_context(iot_sync=False).action_update_iot_info()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '同步完成',
'message': f'已向 {len(self)} 名员工关联的全部设备推送同步指令',
'sticky': False,
'type': 'success',
}
}
@@ -4,9 +4,11 @@
<field name="name">attendance.iot.user.tree</field>
<field name="model">attendance.iot.user</field>
<field name="arch" type="xml">
<tree editable="bottom">
<tree editable="bottom" multi_edit="1">
<header>
<button name="%(action_attendance_iot_import_user_wizard_form)d" string="批量拷贝" type="action" />
<button name="action_batch_sync_to_all_devices" string="同步到全部设备" type="object"
help="将勾选员工的人脸数据同步到该员工关联的所有设备"/>
</header>
<field name="employee_id"/>
<field name="department_id"/>
@@ -6,7 +6,7 @@
<field name="arch" type="xml">
<tree editable="bottom">
<header>
<button name="action_init_device" string="同步员工" type="object"/>
<button name="action_init_device" string="下发全部员工" type="object"/>
</header>
<field name="name"/>
<field name="attendance_address"/>
@@ -19,7 +19,7 @@
<field name="is_connected"/>
<button name="open_record" type="object" string="详情" class="btn btn-blue"/>
<button name="action_view_logs" type="object" string="日志" class="btn btn-blue"/>
<button name="action_send_sync_sigle" type="object" string="同步数据" class="btn btn-blue"/>
<button name="action_send_sync_sigle" type="object" string="拉取设备数据" class="btn btn-blue"/>
<button name="action_send_open_door_sigle" type="object" string="开门" class="btn btn-blue"/>
<button name="action_send_close_door_sigle" type="object" string="关门" class="btn btn-blue"/>
</tree>
@@ -26,3 +26,26 @@ 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',
}
}
@api.depends('role_id', 'employee_id')
def _compute_template_autocomplete_ids(self):
domain = self._get_domain_template_slots()
templates = self.env['planning.slot.template'].search(domain, order='start_time', limit=15)
self.template_autocomplete_ids = templates + self.template_id
+42 -2
View File
@@ -1,11 +1,51 @@
# -*- coding: utf-8 -*-
from odoo import models, fields
import logging
from odoo import models, fields, api, SUPERUSER_ID
class SystemLogs(models.Model):
_name = 'system.logs'
_description = '系统日志'
_order = 'date desc'
name = fields.Char(string="标题")
date = fields.Datetime(string="时间")
date = fields.Datetime(string="时间", default=fields.Datetime.now)
level = fields.Char(string="级别")
logger_name = fields.Char(string="来源")
content = fields.Text(string="内容")
class OdooDBLogHandler(logging.Handler):
"""将日志写入 system.logs 模型"""
def emit(self, record):
if record.levelno < logging.INFO:
return
try:
import odoo
db_name = getattr(odoo.tools.config, 'db_name', None)
if not db_name:
return
db = odoo.sql_db.db_connect(db_name)
with db.cursor() as cr:
env = api.Environment(cr, SUPERUSER_ID, {})
env['system.logs'].create({
'name': record.getMessage()[:80] if record.getMessage() else '',
'date': fields.Datetime.now(),
'level': record.levelname,
'logger_name': record.name,
'content': record.getMessage(),
})
cr.commit()
except Exception:
pass
# 注册 Handler(只注册一次)
_handler_key = 'odoo_db_log_handler'
_root_logger = logging.getLogger()
if not any(getattr(h, 'name', None) == _handler_key for h in _root_logger.handlers):
handler = OdooDBLogHandler()
handler.name = _handler_key
handler.setLevel(logging.INFO)
_root_logger.addHandler(handler)
+22 -3
View File
@@ -4,17 +4,36 @@
<field name="name">系统日志</field>
<field name="res_model">system.logs</field>
<field name="view_mode">tree</field>
<field name="context">{'search_default_today': 1, 'group_by': 'logger_name'}</field>
</record>
<record id="view_system_logs_tree" model="ir.ui.view">
<field name="name">system.logs.tree</field>
<field name="model">system.logs</field>
<field name="arch" type="xml">
<tree string="系统日志">
<field name="name" string="标题"/>
<tree string="系统日志" default_order="date desc">
<field name="date" string="时间"/>
<field name="content" string="内容"/>
<field name="level" string="级别" decoration-danger="level == 'ERROR'"
decoration-warning="level == 'WARNING'" decoration-info="level == 'INFO'"/>
<field name="logger_name" string="来源"/>
<field name="name" string="标题"/>
</tree>
</field>
</record>
<record id="view_system_logs_search" model="ir.ui.view">
<field name="name">system.logs.search</field>
<field name="model">system.logs</field>
<field name="arch" type="xml">
<search>
<filter name="today" string="今天" date="date"/>
<filter name="yesterday" string="昨天" date="date" date_adjust="-1"/>
<filter name="this_week" string="本周" date="date" date_scope="this_week"/>
<separator/>
<filter name="group_by_logger" string="按来源分组" context="{'group_by': 'logger_name'}"/>
<separator/>
<field name="logger_name" string="来源筛选"/>
</search>
</field>
</record>
</odoo>
@@ -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)
@@ -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'})
@@ -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 = '年假明细'
@@ -27,7 +27,11 @@
<xpath expr="//footer/button[@name='auto_plan_id']" position="replace"></xpath>
<xpath expr="//footer/button[@name='action_unpublish']" position="replace"></xpath>
<!-- 补休按钮 -->
<xpath expr="//footer" position="inside">
<button name="action_compensatory_leave" string="补休" type="object"
class="btn-primary" confirm="确认将此班次转为补休?"/>
</xpath>
</field>
</record>