diff --git a/attendance_iot/models/iot_message.py b/attendance_iot/models/iot_message.py
index 09aeed617..9c1b55edd 100644
--- a/attendance_iot/models/iot_message.py
+++ b/attendance_iot/models/iot_message.py
@@ -5,6 +5,30 @@ from odoo import fields, models, api
_logger = logging.getLogger(__name__)
+def _strip_face_template(message_data_str):
+ """移除 message_data JSON 中的 face_template 字段(图片base64太长)"""
+ if not message_data_str:
+ return message_data_str
+ try:
+ data = json.loads(message_data_str)
+ if isinstance(data, dict) and 'face_template' in data:
+ data.pop('face_template')
+ _logger.info("已移除 face_template 字段")
+ return json.dumps(data, ensure_ascii=False)
+ if isinstance(data, list):
+ changed = False
+ for item in data:
+ if isinstance(item, dict) and 'face_template' in item:
+ item.pop('face_template')
+ changed = True
+ if changed:
+ _logger.info("已移除列表中的 face_template 字段")
+ return json.dumps(data, ensure_ascii=False)
+ return message_data_str
+ except (json.JSONDecodeError, TypeError):
+ return message_data_str
+
+
class IotMessage(models.Model):
_name = 'iot.message'
_description = 'mqtt 消息,发送和接收'
@@ -21,6 +45,52 @@ class IotMessage(models.Model):
# 响应信息
res_data = fields.Text(string='发送后的响应信息')
+ @api.model_create_multi
+ def create(self, vals_list):
+ for vals in vals_list:
+ if vals.get('message_data'):
+ vals['message_data'] = _strip_face_template(vals['message_data'])
+ return super().create(vals_list)
+
+ def write(self, vals):
+ if vals.get('message_data'):
+ vals['message_data'] = _strip_face_template(vals['message_data'])
+ return super().write(vals)
+
+ def action_clear_face_template(self):
+ """批量清除已有记录的 face_template 参数"""
+ count = 0
+ for record in self:
+ if not record.message_data:
+ continue
+ try:
+ data = json.loads(record.message_data)
+ changed = False
+ if isinstance(data, dict) and 'face_template' in data:
+ data.pop('face_template')
+ changed = True
+ elif isinstance(data, list):
+ for item in data:
+ if isinstance(item, dict) and 'face_template' in item:
+ item.pop('face_template')
+ changed = True
+ if changed:
+ record.write({'message_data': json.dumps(data, ensure_ascii=False)})
+ count += 1
+ except (json.JSONDecodeError, TypeError):
+ continue
+ _logger.info(f"已清理 {count} 条记录的 face_template")
+ return {
+ 'type': 'ir.actions.client',
+ 'tag': 'display_notification',
+ 'params': {
+ 'title': '完成',
+ 'message': f'已清理 {count} 条记录的 face_template',
+ 'sticky': False,
+ 'type': 'success',
+ }
+ }
+
@api.model
def to_reply(self, data):
_logger.info(f'返回消息:{data}')
diff --git a/attendance_iot/views/iot_message.xml b/attendance_iot/views/iot_message.xml
index 849eb04bd..a61385825 100644
--- a/attendance_iot/views/iot_message.xml
+++ b/attendance_iot/views/iot_message.xml
@@ -8,6 +8,9 @@
iot.message
+
diff --git a/yuthon_base/models/__init__.py b/yuthon_base/models/__init__.py
index 19457e29a..2f7af145a 100644
--- a/yuthon_base/models/__init__.py
+++ b/yuthon_base/models/__init__.py
@@ -9,4 +9,5 @@ from . import res_partner
from . import base_month_year
from . import system_logs
from . import res_users
-from . import mail_thread
\ No newline at end of file
+from . import mail_thread
+from . import ir_ui_menu
diff --git a/yuthon_base/models/ir_ui_menu.py b/yuthon_base/models/ir_ui_menu.py
new file mode 100644
index 000000000..eeaafe667
--- /dev/null
+++ b/yuthon_base/models/ir_ui_menu.py
@@ -0,0 +1,65 @@
+# -*- coding: utf-8 -*-
+from odoo import api, fields, models, _
+import pypinyin
+import re
+
+
+class IrUiMenu(models.Model):
+ _inherit = 'ir.ui.menu'
+
+ name_pinyin = fields.Char(
+ string='菜单名拼音',
+ compute='_compute_name_pinyin',
+ store=True,
+ index=True,
+ )
+
+ @api.depends('name')
+ def _compute_name_pinyin(self):
+ for menu in self:
+ if menu.name:
+ # 全拼:如 "通知公告" → "tongzhigonggao"
+ full_pinyin = ''.join(pypinyin.lazy_pinyin(menu.name)).lower()
+ # 首字母:如 "通知公告" → "tzgg"
+ first_letters = ''.join([
+ p[0] for p in pypinyin.pinyin(menu.name)
+ ]).lower()
+ menu.name_pinyin = f"{full_pinyin} {first_letters}"
+ else:
+ menu.name_pinyin = False
+
+ def name_search(self, name='', args=None, operator='ilike', limit=100):
+ """
+ 重写 name_search 支持全拼/首字母模糊搜索菜单
+ """
+ if args is None:
+ args = []
+ if name and operator in ('ilike', 'like', '=', '=like'):
+ # 清理输入:去掉空格、转小写
+ search_term = name.strip().lower().replace(' ', '')
+ if not search_term:
+ return super().name_search(name=name, args=args, operator=operator, limit=limit)
+
+ # 生成搜索词的拼音和首字母
+ search_pinyin = ''.join(pypinyin.lazy_pinyin(search_term)).lower()
+ search_first = ''.join([p[0] for p in pypinyin.pinyin(search_term)]).lower()
+
+ # 构建拼音搜索域:匹配 name_pinyin 字段
+ pinyin_domain = [
+ '|',
+ ('name_pinyin', 'ilike', search_term), # 直接搜索词
+ ('name_pinyin', 'ilike', search_pinyin), # 全拼搜索
+ ]
+ # 如果首字母不同,也加上
+ if search_first != search_pinyin:
+ pinyin_domain = ['|'] + pinyin_domain + [('name_pinyin', 'ilike', search_first)]
+
+ # 组合:原名搜索 OR 拼音搜索
+ full_domain = ['|'] + expression.AND([args, [('name', operator, name)]]) + expression.AND([args, pinyin_domain])
+
+ # 去重:用 browse 把结果合并(优先按原名匹配排序)
+ name_results = self.search(expression.AND([args, [('name', operator, name)]]), limit=limit)
+ pinyin_results = self.search(expression.AND([args, pinyin_domain]), limit=limit)
+ all_ids = list(dict.fromkeys(name_results.ids + pinyin_results.ids))[:limit]
+ return self.browse(all_ids).name_get()
+ return super().name_search(name=name, args=args, operator=operator, limit=limit)