399 lines
17 KiB
Python
399 lines
17 KiB
Python
# -*- 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)
|