yun_product/yuthon_notice/models/yuthon_notice.py
2026-07-25 14:23:19 +08:00

399 lines
17 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# -*- coding: utf-8 -*-
from odoo import api, fields, models, tools, _
from odoo.exceptions import ValidationError, UserError
import logging
from datetime import datetime, timedelta, date
_logger = logging.getLogger(__name__)
class YuthonNotice(models.Model):
_name = "yuthon.notice"
_description = '通知公告'
_inherit = ['mail.thread.main.attachment', 'mail.activity.mixin']
_rec_name = 'name'
_order = "sequence desc"
name = fields.Char(string='标题')
code = fields.Char(string="编号", default=lambda self: self.env['ir.sequence'].next_by_code('notice_code'))
users_id = fields.Many2one('res.users', string="发布人", default=lambda self: self.env.user)
company_id = fields.Many2one(related="users_id.employee_id.company_id", string="公司", store=True)
department_id = fields.Many2one(related='users_id.employee_id.department_id', string="部门")
notice_type_id = fields.Many2one('yuthon.notice.type', string="类型")
create_date1 = fields.Date(string="发布日期", default=fields.Date.today())
start_date = fields.Date(string="生效日期", default=fields.Date.today())
end_date = fields.Date(string="终止日期")
sequence = fields.Integer(string="顺序", default=1, compute='_compute_sequence')
employee_ids = fields.Many2many('hr.employee', string="人员范围")
department_ids = fields.Many2many('hr.department', string='部门范围')
notice_state = fields.Selection([('no', '未发布'), ('yes', '已发布'), ('stop', '已终止')], default='no', string="公告状态")
is_notice_roger = fields.Selection([('yes', '已读'), ('no', '未读')], default='no', string="确认状态")
is_change_notice = fields.Boolean(string="是否发送通知", default=True)
attn_notice = fields.Html(string="经办人意见")
notice_text = fields.Html(string="正文")
notice_text1 = fields.Html(string="正文")
read_number = fields.Integer(string='阅读量', compute="_compute_read_number", store=True)
is_this_company = fields.Boolean(string='本公司', default=True)
approval_user_id = fields.Many2one('res.users', string="审核人")
approval_date = fields.Datetime(string="审核日期")
is_sticky = fields.Boolean(string="是否置顶")
urgency_level = fields.Selection([('normal', '普件'), ('urgent', '急件'), ('emergency', '特急件')], string='紧急度', default='normal')
active = fields.Boolean('Active', default=True, tracking=True)
work_end = fields.Boolean(string="结束")
users_ids = fields.Many2many(
'res.users',
relation='yuthon_notice_users_rel',
column1='notice_id',
column2='user_id',
string='用户'
)
self_users_ids = fields.Many2many(
'res.users',
relation='yuthon_notice_current_users_rel',
column1='notice_id',
column2='user_id',
string="当前经办人",
default=lambda self: [(6, 0, [self.env.user.id])]
)
# @api.onchange('users_ids')
# def _onchange_users_ids(self):
# today = date.today()
# self.start_date = today
# self.create_date1 = today
is_company = fields.Boolean(string="本公司", default=True, compute='_compute_is_company',
search='_search_part_of_company')
publish_date = fields.Date(string='发布日期')
@api.depends_context('uid', 'company')
@api.depends('company_id')
def _compute_is_company(self):
active_company_ids = self.env.companies
for employee in self:
employee.is_company = employee.company_id in active_company_ids
def _search_part_of_company(self, operator, value):
company_ids = self.env.companies.ids
if not value:
operator = '!=' if operator == '=' else '='
if operator == '=':
return [('company_id', 'in', company_ids)]
else:
return [('company_id', 'not in', company_ids)]
@api.model_create_multi
def create(self, vals_list):
for i in vals_list:
if i['department_ids'] == i['employee_ids'] == i['users_role_ids'] == i['users_role_group_ids']:
raise UserError('请填写发布的范围')
return super().create(vals_list)
def _get_default_name_title(self):
return self._description if self._description else self._name
name_title = fields.Char(default=_get_default_name_title)
@api.constrains('employee_ids', 'department_ids', 'users_role_ids', 'users_role_group_ids')
def _constrains_employee_related_fields(self):
"""
约束验证当关联字段变化时更新users_ids
"""
def get_department_employees(department_id):
"""递归获取部门及其子部门的员工对应的user_id"""
valid_user_ids = set()
# 查找当前部门的员工使用id替代id.origin
employee_ids = self.env['hr.employee'].search([('department_id', '=', department_id.id)])
# 收集有效的user_id
valid_user_ids.update([emp.user_id.id for emp in employee_ids if emp.user_id])
# 递归处理子部门
child_ids = self.env['hr.department'].search([('parent_id', '=', department_id.id)])
for child in child_ids:
valid_user_ids.update(get_department_employees(child))
return valid_user_ids
for notice in self:
# 避免重复触发约束更新users_ids时跳过本次约束检查
if notice.env.context.get('skip_constrains'):
continue
# 获取部门关联的用户ID
department_user_ids = set()
for department_id in notice.department_ids:
department_user_ids.update(get_department_employees(department_id))
group_role_ids = notice.users_role_group_ids.user_role_ids
all_users = set(
notice.employee_ids.mapped('user_id').ids +
group_role_ids.mapped('personnel_ids.user_id').ids +
notice.users_role_ids.mapped('personnel_ids.user_id').ids +
list(department_user_ids)
)
notice.with_context(skip_constrains=True).users_ids = [(6, 0, list(all_users))]
if notice.users_ids and notice.is_change_notice:
conf_old = self.env['yuthon.confirm.users'].sudo().search([('notice_id', '=', notice.id)])
old_employee_ids = conf_old.mapped('employee_id.id')
new_employee_ids = self.env['hr.employee'].sudo().search([
('user_id', 'in', notice.users_ids.ids)
]).ids
add_employee_ids = list(set(new_employee_ids) - set(old_employee_ids))
del_employee_ids = list(set(old_employee_ids) - set(new_employee_ids))
# 3. 处理删除:
if del_employee_ids:
del_conf = conf_old.filtered(lambda c: c.employee_id.id in del_employee_ids)
del_conf.unlink()
if add_employee_ids:
for emp_id in add_employee_ids:
employee = self.env['hr.employee'].sudo().browse(emp_id)
if not employee:
continue
# 防重复:如果已经存在该员工的确认记录,跳过
exist = self.env['yuthon.confirm.users'].sudo().search([
('notice_id', '=', notice.id),
('employee_id', '=', emp_id),
], limit=1)
if exist:
continue
self.env['yuthon.confirm.users'].sudo().create({
'notice_id': notice.id,
'employee_id': emp_id,
'notice_code': notice.code,
'notice_name': notice.name,
'release_date': notice.create_date1,
'state': 'to_sent',
})
def get_no_read_count(self):
# 只统计当前用户发布范围内的未读公告,与列表 domain 保持一致
return self.search_count([
('users_ids', 'in', self.env.uid),
('is_notice_roger', '=', 'no'),
('notice_state', '=', 'yes'),
])
def read(self, fields=None, load='_classic_read'):
# 标记是否真的有未读变已读,避免没变化也触发菜单刷新
changed = False
for con in self:
# 只有全局状态仍是未读时才处理
if con.is_notice_roger == 'no':
# 查找当前用户对应的公告确认记录
con_id = self.env['yuthon.confirm.users'].search([
('notice_id', '=', con.id),
('employee_id', '=', con.env.user.employee_id.id)
])
# 确认是当前员工本人,再标记为已读
if con_id.employee_id == con.env.user.employee_id:
con.is_notice_roger = 'yes'
con_id.write({'state': 'yes'})
changed = True
res = super(YuthonNotice, self).read(fields=fields, load=load)
# 如果有记录从 未读 变为 已读,清除菜单缓存并通知前端刷新角标
if changed:
self.env.registry.clear_cache()
self.env['sun.badge.menu.mixin'].sudo().trigger_menu_reload()
return res
@api.depends('read_number')
def _compute_read_number(self):
notice_confirm_ids = self.env['yuthon.confirm.users'].search(
[('notice_id.id', '=', self.id), ('state', '=', 'yes')])
for read in self:
if notice_confirm_ids:
read.read_number = len(notice_confirm_ids)
else:
read.read_number = 0
def on_sequence(self):
self.sequence = 1
@api.onchange('is_sticky')
def _compute_sequence(self):
for i in self:
if i.is_sticky:
i.sequence = 1
else:
i.sequence = 0
def no_sequence(self):
self.sequence = 0
def notice_publish(self):
"""发布通知公告:给 yuthon.confirm.users 中尚未发送的记录发企微消息。
- 每人有且仅发一条msg_id 为空才发,已发过的不重发
- 发送成功才写回 msg_id企微返回的msgid并更新状态为"待确认"
- 单条失败不影响其他记录,失败记录保持"待发送"状态
"""
self.ensure_one()
if self.notice_state == 'yes':
return
self.notice_state = 'yes'
if not self.is_change_notice:
return
# 重新发布时,已发送过消息的记录重置为"待确认",等待用户重新确认(不重发消息)
self.env['yuthon.confirm.users'].sudo().search([
('notice_id', '=', self.id),
('msg_id', '!=', False),
]).write({'state': 'no'})
# 只发送尚未收到消息的记录msg_id 为空),保证每人有且仅一条
confirm_records = self.env['yuthon.confirm.users'].sudo().search([
('notice_id', '=', self.id),
('msg_id', '=', False),
])
sent_count = 0
fail_count = 0
for rec in confirm_records:
employee = rec.employee_id
if not employee or not employee.wecom_userid:
_logger.warning("公告[%s]确认记录%s跳过员工或企微userid为空", self.name, rec.id)
continue
url = f'https://phone.thtzjt.com/phone/Notice/{self.id}?employee_id={employee.id}'
try:
result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard(
category='notice',
touser=employee.wecom_userid,
title="通知公告",
description=f"{self.name} 发布人: {self.users_id.name or ''}",
url=url,
btntxt="详细信息"
)
except Exception:
_logger.exception("公告[%s]给员工%s发送企微消息异常", self.name, employee.name)
fail_count += 1
continue
# 发送成功:返回元组第一项为 True 且携带 msgid才写回 msg_id 并更新状态
if result_data and result_data[0] and isinstance(result_data[1], dict) and result_data[1].get('msgid'):
rec.sudo().write({
'msg_id': result_data[1].get('msgid'),
'state': 'no',
})
sent_count += 1
else:
_logger.warning("公告[%s]给员工%s发送失败,返回:%s", self.name, employee.name, result_data)
fail_count += 1
_logger.info("公告[%s]发布完成,成功发送%s条,失败%s", self.name, sent_count, fail_count)
# 发布完毕弹窗提醒
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '发布完毕',
'message': f'已成功发送 {sent_count} 条消息' + (f'{fail_count} 条失败' if fail_count else ''),
'type': 'success',
'sticky': False,
},
}
def look_up_notice_users(self):
return {
"type": "ir.actions.act_window",
"name": "通知公告确认信息",
"domain": [("notice_id", "=", self.id)],
"res_model": "yuthon.confirm.users",
"view_mode": "tree",
"context": {
"search_default_group_state": True,
},
}
def write(self, vals):
"""重写write方法检测work_end字段变化触发AI主动感知"""
result = super().write(vals)
# 检测work_end从False变为True
if 'work_end' in vals and vals['work_end']:
for record in self:
if record.work_end: # 确保当前值确实是True
self._trigger_ai_guide(record)
return result
def _trigger_ai_guide(self, record):
"""触发AI主动感知查找匹配的规则推送消息到聊天框"""
# 查找匹配的规则
rules = self.env['ai.rule'].sudo().search([
('rule_type', '=', 'active_guide'),
('guide_model', '=', self._name),
('guide_field', '=', 'work_end'),
('guide_field_value', '=', 'True'),
('active', '=', True),
])
for rule in rules:
# 生成提醒内容(替换变量)
message = rule.guide_message or f'{record.name} 流程已结束'
message = message.replace('{record_name}', record.name or '')
message = message.replace('{user_name}', self.env.user.name or '')
# 自动组装按钮动作(后台自动注入 record_id
action_config = {}
if rule.guide_method_name:
action_config = {
'type': 'execute_tool',
'tool': rule.guide_method_name,
'params': {
'record_id': record.id,
'model': self._name,
}
}
# 查找或创建AI对话用当前用户的默认对话
conversation = self.env['ai.conversation'].sudo().search([
('create_uid', '=', self.env.user.id),
], limit=1, order='write_date desc')
if not conversation:
conversation = self.env['ai.conversation'].sudo().create({
'name': 'AI 对话',
})
# 创建AI消息带action
self.env['ai.message'].sudo().create({
'conversation_id': conversation.id,
'role': 'assistant',
'content': message,
'action': json.dumps({
'type': 'guide_button',
'label': rule.guide_button_label or '确认执行',
'action': action_config,
}, ensure_ascii=False) if action_config else None,
})
# TODO: 通过bus.bus推送到前端让聊天框自动显示新消息
# self.env['bus.bus'].sudo()._sendone(self.env.user.partner_id, 'ai_guide_notification', {
# 'conversation_id': conversation.id,
# 'message': message,
# })
def stop_notice(self):
self.write({
'notice_state': 'stop',
'end_date': fields.Date.today(),
})
def action_print_notice(self):
"""打印通知公告"""
self.ensure_one()
report = self.env.ref('yuthon_notice.action_notice_print_report')
return report.report_action(self)
class YuthonCsTree(models.Model):
_name = 'yuthon.cs.tree'
_description = 'Yuthon Customer Service Tree'
name = fields.Char(string='Name', required=True)