添加功能
This commit is contained in:
@@ -22,6 +22,7 @@
|
||||
'views/yuthon_notice_type_views.xml',
|
||||
'views/yuthon_cs_tree_views.xml',
|
||||
'views/yuthon_confirm_users_views.xml',
|
||||
'views/yuthon_notice_send_log_views.xml',
|
||||
],
|
||||
'assets': {
|
||||
'web.assets_backend': [
|
||||
|
||||
@@ -3,5 +3,6 @@
|
||||
from . import yuthon_notice
|
||||
from . import yuthon_notice_type
|
||||
from . import yuthon_confirm_users
|
||||
from . import yuthon_notice_send_log
|
||||
from . import hr_department
|
||||
|
||||
|
||||
@@ -22,6 +22,23 @@ class YuthonConfirmUsers(models.Model):
|
||||
department_id = fields.Many2one(related='employee_id.department_id', string='部门', store=True)
|
||||
employee_number = fields.Char(related='employee_id.number', string='序号', store=True)
|
||||
|
||||
def view_send_logs(self):
|
||||
"""从确认信息列表跳转查看对应的发送日志"""
|
||||
notice_ids = self.mapped('notice_id').ids
|
||||
if not notice_ids:
|
||||
ctx_notice = self.env.context.get('default_notice_id')
|
||||
if ctx_notice:
|
||||
notice_ids = [ctx_notice]
|
||||
domain = [('notice_id', 'in', notice_ids)] if notice_ids else []
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': '发送日志',
|
||||
'res_model': 'yuthon.notice.send.log',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': domain,
|
||||
'context': {'default_notice_id': notice_ids[0] if notice_ids else False},
|
||||
}
|
||||
|
||||
def withdrawn(self):
|
||||
if self.state == 'to_sent':
|
||||
raise UserError('该通知还未发送,无法撤回')
|
||||
@@ -38,15 +55,23 @@ class YuthonConfirmUsers(models.Model):
|
||||
record.withdrawn()
|
||||
|
||||
def sync_confirm_users_notices(self):
|
||||
SendLog = self.env['yuthon.notice.send.log']
|
||||
for record in self.search([('state', '=', 'no')]):
|
||||
partner_ids = record.employee_id.user_id.partner_id.id
|
||||
record.with_context(lang=record.env.lang)._message_auto_subscribe_notify(partner_ids, 'yuthon_notice.message_yuthon_confirm_users_data')
|
||||
url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id={record.employee_id.id}'
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', touser=record.employee_id.wecom_userid, title="通知公告",
|
||||
try:
|
||||
result_data = self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice', touser=record.employee_id.wecom_userid, title="通知公告",
|
||||
description=f"您有一条【{record.notice_id.name}】的通知公告请尽快确认", url=url, btntxt="详细信息")
|
||||
success = bool(result_data and result_data[0])
|
||||
msg_id = result_data[1].get('msgid') if (result_data and isinstance(result_data[1], dict)) else False
|
||||
SendLog.log_send(record.notice_id, record.employee_id, 'sync', success, msg_id=msg_id, confirm_user=record)
|
||||
except Exception:
|
||||
SendLog.log_send(record.notice_id, record.employee_id, 'sync', False, note='接口调用异常', confirm_user=record)
|
||||
|
||||
|
||||
def reminders_notice(self):
|
||||
SendLog = self.env['yuthon.notice.send.log']
|
||||
confirm_noice_ids = self.env["yuthon.confirm.users"].browse(self._context.get('active_ids', self._context.get('active_id')))
|
||||
i = 0
|
||||
for rec in confirm_noice_ids:
|
||||
@@ -57,16 +82,19 @@ class YuthonConfirmUsers(models.Model):
|
||||
# sync_send_message_textcard 返回 (success, res_dict),失败/地址异常时 success=False
|
||||
if not result_data:
|
||||
rec.write({'msg_id': 'ERROR: 接口未返回结果'})
|
||||
SendLog.log_send(self.notice_id, rec.employee_id, 'reminder', False, note='接口未返回结果', confirm_user=rec)
|
||||
continue
|
||||
success, res_dict = result_data
|
||||
res_dict = res_dict or {}
|
||||
if success and res_dict.get('errmsg') == 'ok':
|
||||
rec.write({'msg_id': res_dict.get('msgid')})
|
||||
i += 1
|
||||
SendLog.log_send(self.notice_id, rec.employee_id, 'reminder', True, msg_id=res_dict.get('msgid'), confirm_user=rec)
|
||||
else:
|
||||
errcode = res_dict.get('errcode')
|
||||
errmsg = res_dict.get('errmsg') or res_dict.get('error') or '未知错误'
|
||||
rec.write({'msg_id': f'ERROR {errcode}: {errmsg}'})
|
||||
SendLog.log_send(self.notice_id, rec.employee_id, 'reminder', False, note=f'ERROR {errcode}: {errmsg}', confirm_user=rec)
|
||||
url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id=298'
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice',
|
||||
touser='18562027762',
|
||||
@@ -75,6 +103,7 @@ class YuthonConfirmUsers(models.Model):
|
||||
url=url, btntxt="详细信息")
|
||||
|
||||
def reminders_notice_send(self):
|
||||
SendLog = self.env['yuthon.notice.send.log']
|
||||
confirm_noice_ids = self.env["yuthon.confirm.users"].browse(
|
||||
self._context.get('active_ids', self._context.get('active_id')))
|
||||
i = 0
|
||||
@@ -86,10 +115,15 @@ class YuthonConfirmUsers(models.Model):
|
||||
description=f"您有一条【{rec.notice_id.name}】待确认的通知公告请尽快确认",
|
||||
url=url, btntxt="详细信息")
|
||||
if result_data:
|
||||
res_dict = result_data[1]
|
||||
res_dict = result_data[1] or {}
|
||||
rec.write({'msg_id': res_dict.get('msgid')})
|
||||
if res_dict.get('errmsg') == 'ok':
|
||||
i = i + 1
|
||||
SendLog.log_send(rec.notice_id, rec.employee_id, 'resend', True, msg_id=res_dict.get('msgid'), confirm_user=rec)
|
||||
else:
|
||||
errcode = res_dict.get('errcode')
|
||||
errmsg = res_dict.get('errmsg') or res_dict.get('error') or '未知错误'
|
||||
SendLog.log_send(rec.notice_id, rec.employee_id, 'resend', False, note=f'ERROR {errcode}: {errmsg}', confirm_user=rec)
|
||||
url = f'https://phone.thtzjt.com/phone/Notice/{self.notice_id.id}?employee_id=298'
|
||||
self.sudo().env["wecom.apps"].sync_send_message_textcard(category='notice',
|
||||
touser='18562027762',
|
||||
|
||||
@@ -72,6 +72,18 @@ class YuthonNotice(models.Model):
|
||||
|
||||
publish_date = fields.Date(string='发布日期')
|
||||
|
||||
def view_send_logs(self):
|
||||
"""从公告跳转查看该公告的发送日志"""
|
||||
self.ensure_one()
|
||||
return {
|
||||
'type': 'ir.actions.act_window',
|
||||
'name': '发送日志',
|
||||
'res_model': 'yuthon.notice.send.log',
|
||||
'view_mode': 'tree,form',
|
||||
'domain': [('notice_id', '=', self.id)],
|
||||
'context': {'default_notice_id': self.id},
|
||||
}
|
||||
|
||||
@api.depends_context('uid', 'company')
|
||||
@api.depends('company_id')
|
||||
def _compute_is_company(self):
|
||||
@@ -225,17 +237,32 @@ class YuthonNotice(models.Model):
|
||||
def no_sequence(self):
|
||||
self.sequence = 0
|
||||
|
||||
def notice_publish(self):
|
||||
"""发布通知公告:给 yuthon.confirm.users 中尚未发送的记录发企微消息。
|
||||
# 单公告单员工最大发送次数(含失败重试),超出不再发送,避免接口异常时无限重发
|
||||
MAX_SEND_PER_EMPLOYEE = 1
|
||||
|
||||
- 每人有且仅发一条:msg_id 为空才发,已发过的不重发
|
||||
- 发送成功才写回 msg_id(企微返回的msgid)并更新状态为"待确认"
|
||||
- 单条失败不影响其他记录,失败记录保持"待发送"状态
|
||||
def notice_publish(self):
|
||||
"""发布通知公告:给 yuthon.confirm.users 中尚未成功发送的记录发企微消息。
|
||||
|
||||
重复发送限制:
|
||||
- 行锁(FOR UPDATE)防止快速重复点击造成并发重复发送
|
||||
- 已发过消息(msg_id 非空)的记录跳过,保证每人有且仅一条成功消息
|
||||
- 单公告单员工发送次数达到 MAX_SEND_PER_EMPLOYEE 后不再发送
|
||||
- 每次发送(成功/失败/跳过)都写入 yuthon.notice.send.log 日志
|
||||
"""
|
||||
self.ensure_one()
|
||||
SendLog = self.env['yuthon.notice.send.log']
|
||||
|
||||
if self.notice_state == 'yes':
|
||||
return
|
||||
# 行锁:序列化并发的发布调用,避免重复发送
|
||||
self.env.cr.execute("SELECT id FROM yuthon_notice WHERE id = %s FOR UPDATE", (self.id,))
|
||||
|
||||
# 已发布且无待发送记录 -> 直接返回,避免重复触发
|
||||
pending = self.env['yuthon.confirm.users'].sudo().search_count([
|
||||
('notice_id', '=', self.id), ('msg_id', '=', False)])
|
||||
if self.notice_state == 'yes' and pending == 0:
|
||||
return {
|
||||
'type': 'ir.actions.client', 'tag': 'display_notification',
|
||||
'params': {'title': '提示', 'message': '公告已发布,无可发送记录', 'type': 'info'},
|
||||
}
|
||||
self.notice_state = 'yes'
|
||||
|
||||
if not self.is_change_notice:
|
||||
@@ -255,11 +282,25 @@ class YuthonNotice(models.Model):
|
||||
|
||||
sent_count = 0
|
||||
fail_count = 0
|
||||
skip_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)
|
||||
skip_count += 1
|
||||
continue
|
||||
|
||||
# 限制重复发送:已达最大发送次数则跳过并记录
|
||||
tried = SendLog.search_count([
|
||||
('notice_id', '=', self.id), ('employee_id', '=', employee.id)])
|
||||
if tried >= self.MAX_SEND_PER_EMPLOYEE:
|
||||
_logger.warning("公告[%s]员工%s已达最大发送次数(%s),跳过",
|
||||
self.name, employee.name, self.MAX_SEND_PER_EMPLOYEE)
|
||||
skip_count += 1
|
||||
SendLog.log_send(self, employee, 'publish', False,
|
||||
note=f'已达最大发送次数 {self.MAX_SEND_PER_EMPLOYEE},跳过', confirm_user=rec)
|
||||
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(
|
||||
@@ -273,6 +314,7 @@ class YuthonNotice(models.Model):
|
||||
except Exception:
|
||||
_logger.exception("公告[%s]给员工%s发送企微消息异常", self.name, employee.name)
|
||||
fail_count += 1
|
||||
SendLog.log_send(self, employee, 'publish', False, note='接口调用异常', confirm_user=rec)
|
||||
continue
|
||||
|
||||
# 发送成功:返回元组第一项为 True 且携带 msgid,才写回 msg_id 并更新状态
|
||||
@@ -282,11 +324,15 @@ class YuthonNotice(models.Model):
|
||||
'state': 'no',
|
||||
})
|
||||
sent_count += 1
|
||||
SendLog.log_send(self, employee, 'publish', True,
|
||||
msg_id=result_data[1].get('msgid'), confirm_user=rec)
|
||||
else:
|
||||
_logger.warning("公告[%s]给员工%s发送失败,返回:%s", self.name, employee.name, result_data)
|
||||
fail_count += 1
|
||||
SendLog.log_send(self, employee, 'publish', False, note=str(result_data), confirm_user=rec)
|
||||
|
||||
_logger.info("公告[%s]发布完成,成功发送%s条,失败%s条", self.name, sent_count, fail_count)
|
||||
_logger.info("公告[%s]发布完成,成功%s,失败%s,跳过%s",
|
||||
self.name, sent_count, fail_count, skip_count)
|
||||
|
||||
# 发布完毕弹窗提醒
|
||||
return {
|
||||
@@ -294,7 +340,9 @@ class YuthonNotice(models.Model):
|
||||
'tag': 'display_notification',
|
||||
'params': {
|
||||
'title': '发布完毕',
|
||||
'message': f'已成功发送 {sent_count} 条消息' + (f',{fail_count} 条失败' if fail_count else ''),
|
||||
'message': f'已成功发送 {sent_count} 条消息'
|
||||
+ (f',{fail_count} 条失败' if fail_count else '')
|
||||
+ (f',{skip_count} 条跳过' if skip_count else ''),
|
||||
'type': 'success',
|
||||
'sticky': False,
|
||||
},
|
||||
|
||||
@@ -0,0 +1,52 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
from odoo import api, fields, models
|
||||
|
||||
|
||||
class YuthonNoticeSendLog(models.Model):
|
||||
_name = 'yuthon.notice.send.log'
|
||||
_description = '通知发送日志'
|
||||
_order = 'send_time desc, id desc'
|
||||
|
||||
notice_id = fields.Many2one('yuthon.notice', string='公告', ondelete='cascade')
|
||||
confirm_user_id = fields.Many2one('yuthon.confirm.users', string='确认记录', ondelete='set null')
|
||||
employee_id = fields.Many2one('hr.employee', string='接收人')
|
||||
send_type = fields.Selection([
|
||||
('publish', '发布'),
|
||||
('reminder', '催办'),
|
||||
('resend', '批量发送'),
|
||||
('sync', '自动同步'),
|
||||
], string='发送类型', default='publish')
|
||||
send_time = fields.Datetime(string='发送时间', default=fields.Datetime.now)
|
||||
msg_id = fields.Char(string='消息ID')
|
||||
state = fields.Selection([('success', '成功'), ('fail', '失败')], string='结果')
|
||||
note = fields.Text(string='详情')
|
||||
send_no = fields.Integer(string='第几次发送', readonly=True)
|
||||
company_id = fields.Many2one(related='employee_id.company_id', string='公司', store=True)
|
||||
|
||||
@api.model
|
||||
def log_send(self, notice, employee, send_type, success, msg_id=False, note=False, confirm_user=False):
|
||||
"""记录一次发送结果,并自动计算该员工在本公告中的第几次发送。
|
||||
|
||||
:param notice: yuthon.notice 记录
|
||||
:param employee: hr.employee 记录
|
||||
:param send_type: publish/reminder/resend/sync
|
||||
:param success: 是否发送成功
|
||||
:param msg_id: 企微返回的 msgid
|
||||
:param note: 失败原因/返回详情
|
||||
:param confirm_user: yuthon.confirm.users 记录(可选)
|
||||
"""
|
||||
existing = self.search_count([
|
||||
('notice_id', '=', notice.id),
|
||||
('employee_id', '=', employee.id),
|
||||
])
|
||||
return self.sudo().create({
|
||||
'notice_id': notice.id,
|
||||
'employee_id': employee.id,
|
||||
'confirm_user_id': confirm_user.id if confirm_user else False,
|
||||
'send_type': send_type,
|
||||
'send_time': fields.Datetime.now(),
|
||||
'msg_id': msg_id or False,
|
||||
'state': 'success' if success else 'fail',
|
||||
'note': note or False,
|
||||
'send_no': existing + 1,
|
||||
})
|
||||
@@ -2,5 +2,6 @@ id,name,model_id:id,group_id:id,perm_read,perm_write,perm_create,perm_unlink
|
||||
access_yuthon_notice,yuthon_notice,model_yuthon_notice,,1,1,1,1
|
||||
access_yuthon_notice_type,yuthon_notice_type,model_yuthon_notice_type,base.group_user,1,1,1,1
|
||||
access_yuthon_confirm_users,yuthon_confirm_users,model_yuthon_confirm_users,base.group_user,1,1,1,1
|
||||
access_yuthon_notice_send_log,yuthon_notice_send_log,model_yuthon_notice_send_log,base.group_user,1,1,1,1
|
||||
access_yuthon_cs_tree,yuthon_cs_tree,model_yuthon_cs_tree,base.group_user,1,1,1,1
|
||||
|
||||
|
||||
|
@@ -9,6 +9,7 @@
|
||||
<button name="reminders_notice" string="批量催办" type="object" class="oe_stat_button" icon="fa-bars"/>
|
||||
<button name="all_withdrawn" string="批量撤回" type="object" icon="fa-bars" class="oe_stat_button" confirm="确认要撤回吗?"/>
|
||||
<button name="reminders_notice_send" string="批量发送" type="object" icon="fa-bars" class="oe_stat_button"/>
|
||||
<button name="view_send_logs" string="发送日志" type="object" class="oe_stat_button" icon="fa-history"/>
|
||||
</header>
|
||||
<field name="employee_id"/>
|
||||
<field name="employee_number" column_invisible="1"/>
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
<?xml version="1.0" encoding="UTF-8" ?>
|
||||
<odoo>
|
||||
<record id="view_yuthon_notice_send_log_tree" model="ir.ui.view">
|
||||
<field name="name">yuthon.notice.send.log.tree</field>
|
||||
<field name="model">yuthon.notice.send.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<tree string="发送日志" decoration-danger="state == 'fail'" decoration-success="state == 'success'">
|
||||
<field name="notice_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="company_id" optional="hide"/>
|
||||
<field name="send_type"/>
|
||||
<field name="send_no"/>
|
||||
<field name="send_time"/>
|
||||
<field name="state"/>
|
||||
<field name="msg_id"/>
|
||||
<field name="note"/>
|
||||
</tree>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_yuthon_notice_send_log_form" model="ir.ui.view">
|
||||
<field name="name">yuthon.notice.send.log.form</field>
|
||||
<field name="model">yuthon.notice.send.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<form string="发送日志">
|
||||
<sheet>
|
||||
<group>
|
||||
<field name="notice_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="company_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="send_type"/>
|
||||
<field name="send_no"/>
|
||||
<field name="send_time"/>
|
||||
<field name="state"/>
|
||||
<field name="msg_id"/>
|
||||
</group>
|
||||
<group>
|
||||
<field name="note" nolabel="1" placeholder="失败详情 / 接口返回"/>
|
||||
</group>
|
||||
</sheet>
|
||||
</form>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="view_yuthon_notice_send_log_search" model="ir.ui.view">
|
||||
<field name="name">yuthon.notice.send.log.search</field>
|
||||
<field name="model">yuthon.notice.send.log</field>
|
||||
<field name="arch" type="xml">
|
||||
<search string="发送日志查询">
|
||||
<field name="notice_id"/>
|
||||
<field name="employee_id"/>
|
||||
<field name="state"/>
|
||||
<filter name="group_by_notice" string="按公告" context="{'group_by': 'notice_id'}"/>
|
||||
<filter name="group_by_employee" string="按员工" context="{'group_by': 'employee_id'}"/>
|
||||
<filter name="group_by_state" string="按结果" context="{'group_by': 'state'}"/>
|
||||
<filter name="group_by_type" string="按类型" context="{'group_by': 'send_type'}"/>
|
||||
</search>
|
||||
</field>
|
||||
</record>
|
||||
|
||||
<record id="yuthon_notice_send_log_action" model="ir.actions.act_window">
|
||||
<field name="name">发送日志</field>
|
||||
<field name="res_model">yuthon.notice.send.log</field>
|
||||
<field name="view_mode">tree,form</field>
|
||||
<field name="search_view_id" ref="view_yuthon_notice_send_log_search"/>
|
||||
</record>
|
||||
</odoo>
|
||||
Reference in New Issue
Block a user