@@ -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
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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>
|
||||
|
||||
|
||||
Reference in New Issue
Block a user