Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
H
hh_ccs
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
贺阳
hh_ccs
Commits
beeba436
提交
beeba436
authored
3月 26, 2026
作者:
伍姿英
浏览文件
操作
浏览文件
下载
差异文件
Merge branch 'release/3.10.0'
上级
a9d205ab
a25cb2c5
隐藏空白字符变更
内嵌
并排
正在显示
8 个修改的文件
包含
869 行增加
和
5 行删除
+869
-5
__manifest__.py
ccs_connect_tiktok/__manifest__.py
+1
-0
timer.xml
ccs_connect_tiktok/data/timer.xml
+15
-0
__init__.py
ccs_connect_tiktok/models/__init__.py
+1
-1
cc_node.py
ccs_connect_tiktok/models/cc_node.py
+14
-0
warn_config.py
ccs_connect_tiktok/models/warn_config.py
+742
-0
ir.model.access.csv
ccs_connect_tiktok/security/ir.model.access.csv
+4
-2
menu_view.xml
ccs_connect_tiktok/views/menu_view.xml
+2
-2
warn_config_views.xml
ccs_connect_tiktok/views/warn_config_views.xml
+90
-0
没有找到文件。
ccs_connect_tiktok/__manifest__.py
浏览文件 @
beeba436
...
...
@@ -21,6 +21,7 @@
'wizard/batch_input_ship_package_statu_wizard.xml'
,
'wizard/update_bl_status_wizard.xml'
,
'wizard/excel_wizard.xml'
,
'views/warn_config_views.xml'
,
# 'wizard/again_push_wizard.xml',
# 'wizard/batch_push_tiktok.xml',
# view
...
...
ccs_connect_tiktok/data/timer.xml
浏览文件 @
beeba436
...
...
@@ -25,5 +25,19 @@
<field
name=
"doall"
eval=
"False"
/>
</record>
<record
id=
"cron_cron_warn_cc_order"
model=
"ir.cron"
>
<field
name=
"name"
>
节点进度预警
</field>
<field
name=
"model_id"
ref=
"ccs_connect_tiktok.model_warning_config"
/>
<field
name=
"state"
>
code
</field>
<field
name=
"code"
>
model.cron_warn_cc_order()
</field>
<field
name=
'interval_number'
>
30
</field>
<field
name=
'interval_type'
>
minutes
</field>
<field
name=
"numbercall"
>
-1
</field>
<field
name=
"active"
eval=
"True"
/>
<field
name=
"nextcall"
eval=
"(datetime.now() + timedelta(minutes=5)).strftime('%Y-%m-%d %H:%M:%S')"
/>
<field
name=
"doall"
eval=
"False"
/>
</record>
</data>
</odoo>
\ No newline at end of file
ccs_connect_tiktok/models/__init__.py
浏览文件 @
beeba436
...
...
@@ -12,5 +12,5 @@ from . import pda_scan_record
from
.
import
bl_patrol
from
.
import
cc_pallet
from
.
import
warn_config
ccs_connect_tiktok/models/cc_node.py
浏览文件 @
beeba436
...
...
@@ -8,6 +8,20 @@ class CCNode(models.Model):
_name
=
'cc.node'
_inherit
=
'cc.node'
def
name_get
(
self
):
# 1. 如果 XML 视图里没有传这个 context,就走系统默认逻辑(只显示名字)
if
not
self
.
env
.
context
.
get
(
'show_code_in_name'
):
return
super
(
CCNode
,
self
)
.
name_get
()
# 2. 如果传了 context,说明是在预警配置页面,执行拼接逻辑
result
=
[]
for
record
in
self
:
if
record
.
tk_code
:
name
=
f
"{record.name}-{record.tk_code}"
else
:
name
=
record
.
name
result
.
append
((
record
.
id
,
name
))
return
result
tk_code
=
fields
.
Char
(
'TK Code'
,
help
=
'TK Code'
)
interval_minutes
=
fields
.
Integer
(
'Predecessor Node Interval (Minutes)'
,
default
=
20
,
help
=
'Default interval time between predecessor nodes in minutes.'
)
# 前序节点间隔时间,默认20分钟
...
...
ccs_connect_tiktok/models/warn_config.py
0 → 100644
浏览文件 @
beeba436
from
odoo
import
models
,
fields
,
api
from
odoo.exceptions
import
ValidationError
from
datetime
import
datetime
,
timedelta
,
timezone
from
collections
import
defaultdict
import
logging
_logger
=
logging
.
getLogger
(
__name__
)
class
WarningConfig
(
models
.
Model
):
_name
=
'warning.config'
_description
=
'预警配置'
name
=
fields
.
Char
(
string
=
'预警名称'
,
required
=
True
,
copy
=
False
)
time_type
=
fields
.
Selection
([
(
'clearance_node'
,
'清关进度节点'
),
(
'flight_landing'
,
'航班落地'
)
],
string
=
'预警时间类型'
,
required
=
True
,
default
=
'clearance_node'
)
# 当类型为"清关进度节点"时使用的字段
time_point_id
=
fields
.
Many2one
(
'cc.node'
,
string
=
'预警时间点'
)
# 当类型为"航班落地"时使用的占位展示字段
flight_landing_time
=
fields
.
Char
(
string
=
'预警时间点'
,
default
=
'预计到达时间'
,
readonly
=
True
)
remaining_time
=
fields
.
Integer
(
string
=
'剩余时间'
,
help
=
'填正整数或负整数。整数代表距离该时间还剩多长时间,负数代表超过时间多长时间。'
)
unsynced_node_id
=
fields
.
Many2one
(
'cc.node'
,
string
=
'未同步节点状态'
)
warning_reason
=
fields
.
Text
(
string
=
'预警原因'
)
active
=
fields
.
Boolean
(
string
=
'有效'
,
default
=
True
)
# 数据库层面的唯一性约束
_sql_constraints
=
[
(
'name_unique'
,
'UNIQUE(name)'
,
'预警名称必须唯一,不能重复!'
)
]
# Python 层面的逻辑校验
@api.constrains
(
'time_point_id'
,
'unsynced_node_id'
,
'time_type'
)
def
_check_node_difference
(
self
):
for
record
in
self
:
# 只有在时间类型为清关节点时,才去对比两个节点是否相同
if
record
.
time_type
==
'clearance_node'
:
if
record
.
time_point_id
and
record
.
unsynced_node_id
and
record
.
time_point_id
==
record
.
unsynced_node_id
:
raise
ValidationError
(
'未同步节点状态 不能跟 预警时间点 一致!'
)
# 当切换时间类型时,清空不相关的数据
@api.onchange
(
'time_type'
)
def
_onchange_time_type
(
self
):
if
self
.
time_type
==
'flight_landing'
:
self
.
time_point_id
=
False
def
convert_to_utc
(
self
,
time_str
):
"""
将带时区的时间字符串转换为 0 时区 (UTC) 的常规格式
"""
# 1. 解析时间字符串
dt
=
datetime
.
fromisoformat
(
time_str
)
# 2. 转换到 0 时区
utc_dt
=
dt
.
astimezone
(
timezone
.
utc
)
# 3. 返回格式化后的字符串
return
datetime
.
strptime
(
utc_dt
.
strftime
(
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
),
"
%
Y-
%
m-
%
d
%
H:
%
M:
%
S"
)
# def cron_warn_cc_order(self):
# # 获取最近几天的提单 未完成的
# # 根据配置条件去检查
# config_objs = self.env['warning.config'].sudo().search([])
# warn_order_days = self.env['ir.config_parameter'].sudo().get_param('warn_order_days') or 10
# warn_order_days = int(warn_order_days)
# utc_time = datetime.utcnow()
# c_time = utc_time - timedelta(days=warn_order_days)
# domain = [('create_date', '>=', c_time.strftime('%Y-%m-%d %H:%M:%S')), ('state', '!=', 'done')]
# bl_objs = self.env['cc.bl'].sudo().search(domain)
# ship_package_arr = defaultdict(list)
# for config_obj in config_objs:
# # 需要一个时间然后检查同步日志里面有没有这个节点
# if config_obj.time_type == 'clearance_node':
# if config_obj.time_point_id.node_type == 'bl':
# # 找这个提单的节点的这个时间 去查有没有上传
# for bl_obj in bl_objs:
# log_obj = self.env['cc.bl.sync.log'].sudo().search([('bl_id', '=', bl_obj.id),
# ('process_code', '=', config_obj.time_point_id.tk_code)], order='operate_time desc', limit=1)
# not_log_obj = self.env['cc.bl.sync.log'].sudo().search([('bl_id', '=', bl_obj.id),
# ('process_code', '=',
# config_obj.unsynced_node_id.tk_code)],
# order='operate_time desc', limit=1)
# if utc_time > (log_obj.operate_time + timedelta(hours=-int(config_obj.remaining_time))) and not not_log_obj:
# # 发送消息
# content = f"""
# 提单号{bl_obj.bl_no}
# 关务节点{config_obj.unsynced_node_id.name})未同步,{config_obj.warning_reason or ''}
# 请及时操作!
# """
# self.send_email(content)
# pass
# elif config_obj.time_point_id.node_type == 'package':
# for bl_obj in bl_objs:
# package_objs = self.env['cc.ship.package'].sudo().search([('bl_id', '=', bl_obj.id)])
# package_arr = []
# for package_obj in package_objs:
# log_obj = self.env['cc.ship.package.sync.log'].sudo().search([('package_id', '=', package_obj.id),
# ('process_code', '=', config_obj.time_point_id.tk_code)], order='operate_time desc', limit=1)
# not_log_obj = self.env['cc.ship.package.sync.log'].sudo().search([('package_id', '=', package_obj.id),
# ('process_code', '=',
# config_obj.unsynced_node_id.tk_code)],
# order='operate_time desc', limit=1)
# if utc_time > (log_obj.operate_time + timedelta(hours=-int(config_obj.remaining_time))) and not not_log_obj:
# # 发送消息
# package_arr.append(package_obj)
# pass
# if bl_obj.bl_no in ship_package_arr:
# ship_package_arr.append({
# 'name': config_obj.unsynced_node_id.name or '',
# 'arr': package_arr
# })
# else:
# ship_package_arr[bl_obj.bl_no] = [{
# 'name': config_obj.unsynced_node_id.name or '',
# 'arr': package_arr
# }]
# self.send_ship_email(ship_package_arr)
# else:
# if config_obj.unsynced_node_id.node_type == 'bl':
# # 找这个提单的节点的这个时间 去查有没有上传
# for bl_obj in bl_objs:
# not_log_obj = self.env['cc.bl.sync.log'].sudo().search([('bl_id', '=', bl_obj.id),
# ('process_code', '=',
# config_obj.unsynced_node_id.tk_code)],
# order='operate_time desc', limit=1)
# eta = self.convert_to_utc(bl_obj.eta)
# # 输出: 2025-09-30 13:35:00
# if utc_time > (eta + timedelta(hours=-int(config_obj.remaining_time))) and not not_log_obj:
# # 发送消息
# content = f"""
# 提单号{bl_obj.bl_no}
# 关务节点{config_obj.unsynced_node_id.name})未同步,{config_obj.warning_reason or ''}
# 请及时操作!
# """
# self.send_email(content)
# pass
# elif config_obj.unsynced_node_id.node_type == 'package':
# for bl_obj in bl_objs:
# package_objs = self.env['cc.ship.package'].sudo().search([('bl_id', '=', bl_obj.id)])
# package_arr = []
# for package_obj in package_objs:
# not_log_obj = self.env['cc.ship.package.sync.log'].sudo().search([('package_id', '=', package_obj.id),
# ('process_code', '=',
# config_obj.unsynced_node_id.tk_code)],
# order='operate_time desc', limit=1)
# eta = self.convert_to_utc(bl_obj.eta)
# if utc_time > (eta + timedelta(hours=-int(config_obj.remaining_time))) and not not_log_obj:
# # 发送消息
# package_arr.append(package_obj)
# pass
# if bl_obj.bl_no in ship_package_arr:
# ship_package_arr.append({
# 'name': config_obj.unsynced_node_id.name or '',
# 'arr': package_arr
# })
# else:
# ship_package_arr[bl_obj.bl_no] = [{
# 'name': config_obj.unsynced_node_id.name or '',
# 'arr': package_arr
# }]
# self.send_ship_email(ship_package_arr)
# def cron_warn_cc_order(self):
# # 1. 初始化基础参数
# utc_time = datetime.utcnow()
# warn_days = int(self.env['ir.config_parameter'].sudo().get_param('warn_order_days', 10))
# c_time = utc_time - timedelta(days=warn_days)
# config_objs = self.env['warning.config'].sudo().search([])
# bl_objs = self.env['cc.bl'].sudo().search([
# ('create_date', '>=', c_time.strftime('%Y-%m-%d %H:%M:%S')),
# ('state', '!=', 'done')
# ])
# # bl_objs = self.env['cc.bl'].sudo().search([('id', '=', 71)]) # 本地测试提单
# if not config_objs or not bl_objs:
# return
# # 2. 一次性获取所有相关的包裹,并按 bl_id 分组映射到内存中
# all_packages = self.env['cc.ship.package'].sudo().search([('bl_id', 'in', bl_objs.ids)])
# packages_by_bl = defaultdict(list)
# for pkg in all_packages:
# packages_by_bl[pkg.bl_id.id].append(pkg)
# # 3. 核心工具函数:批量获取最新日志字典 {record_id: {process_code: operate_time}}
#
# def get_latest_logs_time(model_name, rel_field, record_ids, process_codes):
# if not record_ids or not process_codes:
# return {}
# logs = self.env[model_name].sudo().search([
# (rel_field, 'in', tuple(record_ids)),
# ('process_code', 'in', tuple(process_codes))
# ], order='operate_time desc')
# log_dict = defaultdict(dict)
# for log in logs:
# rec_id = getattr(log, rel_field).id
# code = log.process_code
# if code not in log_dict[rec_id]: # 只记录最新的那条时间
# log_dict[rec_id][code] = log.operate_time
# return log_dict
#
# ship_package_arr = defaultdict(list)
# bl_arr = defaultdict(list)
# # 4. 遍历配置规则进行校验
# for config in config_objs:
# is_clearance = (config.time_type == 'clearance_node')
# node_type = config.time_point_id.node_type if is_clearance else config.unsynced_node_id.node_type
#
# sync_code = config.time_point_id.tk_code if is_clearance else None
# unsync_code = config.unsynced_node_id.tk_code
# codes_to_fetch = {c for c in [sync_code, unsync_code] if c}
#
# time_offset = timedelta(hours=-int(config.remaining_time or 0))
# node_name = config.unsynced_node_id.name or ''
# warning_reason = config.warning_reason or ''
#
# # 处理提单级别 (BL)
# if node_type == 'bl':
# bl_logs = get_latest_logs_time('cc.bl.sync.log', 'bl_id', bl_objs.ids, codes_to_fetch)
# for bl in bl_objs:
# logs = bl_logs.get(bl.id, {})
# if unsync_code in logs: # 已同步,跳过
# continue
#
# # 确定计算基准时间
# base_time = logs.get(sync_code) if is_clearance else (
# self.convert_to_utc(bl.eta) if bl.eta else None)
#
# if base_time and utc_time > (base_time + time_offset):
# content = f"提单号{bl.bl_no}\n关务节点({node_name})未同步,{warning_reason}\n请及时操作!"
# bl_arr[bl.bl_no].append({
# 'name': node_name,
# 'warning_reason': warning_reason
# })
# # self.send_bl_email(config.name, content)
# # 处理包裹级别 (Package)
# elif node_type == 'package':
# pkg_logs = get_latest_logs_time('cc.ship.package.sync.log', 'package_id', all_packages.ids,
# codes_to_fetch)
# for bl in bl_objs:
# alert_pkgs = []
# # 获取该提单下的所有包裹
# for pkg in packages_by_bl.get(bl.id, []):
# logs = pkg_logs.get(pkg.id, {})
# if unsync_code in logs: # 已同步,跳过
# continue
# # 确定计算基准时间
# base_time = logs.get(sync_code) if is_clearance else (
# self.convert_to_utc(bl.eta) if bl.eta else None)
# if base_time and utc_time > (base_time + time_offset):
# alert_pkgs.append(pkg)
# # 修复 Bug: 正确地向 defaultdict(list) 的子列表中追加数据
# if alert_pkgs:
# ship_package_arr[bl.bl_no].append({
# 'name': node_name,
# 'arr': alert_pkgs,
# 'warning_reason': warning_reason
# })
# # 5. 循环外部:一次性发送包裹邮件汇总!
# if ship_package_arr:
# # print(ship_package_arr)
# self.send_warn_email(self.format_package_warning_email(ship_package_arr))
# if bl_arr:
# # print(bl_arr)
# self.send_warn_email(self.format_email_content_grouped(bl_arr))
# def cron_warn_cc_order(self):
# # 1. 初始化基础参数
# utc_time = datetime.utcnow()
# warn_days = int(self.env['ir.config_parameter'].sudo().get_param('warn_order_days', 7))
# # c_time = utc_time - timedelta(days=warn_days)
# end_date = fields.Date.today()
# start_date = end_date - timedelta(days=warn_days)
#
# config_objs = self.env['warning.config'].sudo().search([])
# bl_objs = self.env['cc.bl'].sudo().search([
# ('bl_date', '>=', start_date),
# ('state', '!=', 'done')
# ])
# # bl_objs = self.env['cc.bl'].sudo().search([
# # ('id', '=', 71)
# # ])
# if not config_objs or not bl_objs:
# return
# # 2. 获取包裹及预计算提单的 ETA (避免重复转换时间)
# all_packages = self.env['cc.ship.package'].sudo().search([('bl_id', 'in', bl_objs.ids)])
# packages_by_bl = defaultdict(list)
# for pkg in all_packages:
# packages_by_bl[pkg.bl_id.id].append(pkg)
# bl_eta_utc_dict = {bl.id: self.convert_to_utc(bl.eta) if bl.eta else None for bl in bl_objs}
# # 3. 核心优化:提前收集所有配置中用到的 process_code,一次性查询!
# bl_codes_needed = set()
# pkg_codes_needed = set()
# for config in config_objs:
# is_clearance = (config.time_type == 'clearance_node')
# node_type = config.time_point_id.node_type if is_clearance else config.unsynced_node_id.node_type
# if node_type == 'bl':
# if is_clearance: bl_codes_needed.add(config.time_point_id.tk_code)
# bl_codes_needed.add(config.unsynced_node_id.tk_code)
# elif node_type == 'package':
# if is_clearance: pkg_codes_needed.add(config.time_point_id.tk_code)
# pkg_codes_needed.add(config.unsynced_node_id.tk_code)
#
# # 4. 超级工具函数:使用 search_read 避开 ORM 实例化开销
# def get_all_logs_dict(model_name, rel_field, record_ids, process_codes):
# if not record_ids or not process_codes:
# return {}
# # search_read 直接返回字典列表,比 search 返回对象快 10 倍以上
# logs_data = self.env[model_name].sudo().search_read(
# [(rel_field, 'in', tuple(record_ids)), ('process_code', 'in', tuple(process_codes))],
# [rel_field, 'process_code', 'operate_time'],
# order='operate_time desc'
# )
# log_dict = defaultdict(dict)
# for data in logs_data:
# # search_read 中的 Many2one 字段会返回 (id, name) 元组
# rec_id = data[rel_field][0] if isinstance(data[rel_field], tuple) else data[rel_field]
# code = data['process_code']
# if code not in log_dict[rec_id]: # 依靠 order='desc',最先遍历到的一定是最新的
# log_dict[rec_id][code] = data['operate_time']
# return log_dict
#
# # 无论有多少条配置规则,查提单日志和包裹日志永远只各查 1 次数据库!
# bl_logs_dict = get_all_logs_dict('cc.bl.sync.log', 'bl_id', bl_objs.ids, bl_codes_needed)
# pkg_logs_dict = get_all_logs_dict('cc.ship.package.sync.log', 'package_id', all_packages.ids, pkg_codes_needed)
#
# ship_package_arr = defaultdict(list)
# bl_arr = defaultdict(list)
#
# # 5. 遍历配置规则进行校验 (此时全是纯内存/字典操作,耗时接近 0)
# for config in config_objs:
# is_clearance = (config.time_type == 'clearance_node')
# node_type = config.time_point_id.node_type if is_clearance else config.unsynced_node_id.node_type
#
# sync_code = config.time_point_id.tk_code if is_clearance else None
# unsync_code = config.unsynced_node_id.tk_code
#
# time_offset = timedelta(hours=-int(config.remaining_time or 0))
# node_name = config.unsynced_node_id.name or ''
# warning_reason = config.warning_reason or ''
#
# if node_type == 'bl':
# for bl in bl_objs:
# logs = bl_logs_dict.get(bl.id, {})
# if unsync_code in logs: # 已同步,跳过
# continue
#
# base_time = logs.get(sync_code) if is_clearance else bl_eta_utc_dict.get(bl.id)
#
# if base_time and utc_time > (base_time + time_offset):
# bl_arr[bl.bl_no].append({
# 'name': node_name,
# 'warning_reason': warning_reason
# })
#
# elif node_type == 'package':
# for bl in bl_objs:
# alert_pkgs = []
# # 获取该提单预计算的基准时间 (非clearance模式下)
# bl_eta = bl_eta_utc_dict.get(bl.id) if not is_clearance else None
#
# for pkg in packages_by_bl.get(bl.id, []):
# logs = pkg_logs_dict.get(pkg.id, {})
# if unsync_code in logs:
# continue
#
# base_time = logs.get(sync_code) if is_clearance else bl_eta
#
# if base_time and utc_time > (base_time + time_offset):
# alert_pkgs.append(pkg)
#
# if alert_pkgs:
# ship_package_arr[bl.bl_no].append({
# 'name': node_name,
# 'arr': alert_pkgs,
# 'warning_reason': warning_reason
# })
#
# # 6. 发送邮件汇总
# if ship_package_arr:
# self.send_warn_email(self.format_package_warning_email(ship_package_arr))
# if bl_arr:
# self.send_warn_email(self.format_email_content_grouped(bl_arr))
def
format_package_warning_email
(
self
,
data
):
"""
将小包预警数据格式化为指定的邮件内容(已移除开头标题)
"""
email_lines
=
[]
for
bl_no
,
node_list
in
data
.
items
():
email_lines
.
append
(
f
"提单号{bl_no}"
)
for
node
in
node_list
:
packages
=
node
.
get
(
'arr'
,
[])
total_count
=
len
(
packages
)
if
total_count
==
0
:
continue
# 提取小包号(最多10个)
# 注意:这里的 pkg.name 假设你的小包号字段叫 name,请根据实际情况替换
pkg_names
=
[
pkg
.
logistic_order_no
for
pkg
in
packages
[:
10
]]
pkg_str
=
"/"
.
join
(
pkg_names
)
node_name
=
node
.
get
(
'name'
,
'未知节点'
)
reason
=
node
.
get
(
'warning_reason'
,
'无预警原因'
)
# 按照要求的格式拼接
email_lines
.
append
(
f
"小包{pkg_str}等{total_count}个小包,节点{node_name}未同步,{reason}"
)
email_lines
.
append
(
"请及时操作!"
)
# 用换行符将所有行连接起来
return
"
\n
"
.
join
(
email_lines
)
def
format_email_content_grouped
(
self
,
data
):
"""
将预警数据格式化为邮件文本(按提单号聚合节点)
"""
email_lines
=
[]
for
bl_no
,
nodes
in
data
.
items
():
email_lines
.
append
(
f
"提单号{bl_no}"
)
# 将该提单下的所有异常节点列出来
for
node
in
nodes
:
node_name
=
node
.
get
(
'name'
,
'未知节点'
)
reason
=
node
.
get
(
'warning_reason'
,
'无'
)
email_lines
.
append
(
f
"关务节点{node_name}未同步,{reason}"
)
email_lines
.
append
(
"请及时操作!
\n
"
)
# \n 用于和下一个提单隔开
return
"
\n
"
.
join
(
email_lines
)
.
strip
()
def
send_warn_email
(
self
,
content
):
"""
发送邮件
"""
try
:
# 获取邮件配置
config
=
self
.
env
[
'ir.config_parameter'
]
.
sudo
()
receiver_emails
=
config
.
get_param
(
'patrol_receiver_emails'
,
default
=
''
)
sender_email
=
config
.
get_param
(
'patrol_sender_email'
,
default
=
''
)
if
not
receiver_emails
or
not
sender_email
:
logging
.
warning
(
"邮件配置不完整,跳过邮件发送"
)
return
# 解析接收邮箱
receiver_list
=
[
email
.
strip
()
for
email
in
receiver_emails
.
split
(
';'
)
if
email
.
strip
()]
# 构建邮件内容
subject
=
f
"节点未及时推送预警通知"
# 标题
# 发送邮件
self
.
env
[
'mail.mail'
]
.
sudo
()
.
create
({
'subject'
:
subject
,
'body_html'
:
content
.
replace
(
'
\n
'
,
'<br/>'
),
'email_from'
:
sender_email
,
'email_to'
:
','
.
join
(
receiver_list
),
'auto_delete'
:
True
,
'date'
:
datetime
.
utcnow
()
})
.
send
()
# 更新发送记录
# if patrol_obj:
# patrol_obj.write({
# 'email_sent': True,
# 'email_sent_time': fields.Datetime.now()
# })
_logger
.
info
(
f
"预警邮件发送成功,接收人: {receiver_list}"
)
except
Exception
as
e
:
_logger
.
error
(
f
"发送预警邮件失败: {str(e)}"
)
# def send_ship_email(self, content):
# """
# 发送邮件
# """
# try:
# # 获取邮件配置
# config = self.env['ir.config_parameter'].sudo()
# receiver_emails = config.get_param('patrol_receiver_emails', default='')
# sender_email = config.get_param('patrol_sender_email', default='')
# if not receiver_emails or not sender_email:
# logging.warning("邮件配置不完整,跳过邮件发送")
# return
# # 解析接收邮箱
# receiver_list = [email.strip() for email in receiver_emails.split(';') if email.strip()]
# # 构建邮件内容
# subject = f"节点未及时推送预警通知" # 标题
# # 发送邮件
# self.env['mail.mail'].sudo().create({
# 'subject': subject,
# 'body_html': content.replace('\n', '<br/>'),
# 'email_from': sender_email,
# 'email_to': ','.join(receiver_list),
# 'auto_delete': True,
# 'date': datetime.utcnow()
# }).send()
#
# # 更新发送记录
# # if patrol_obj:
# # patrol_obj.write({
# # 'email_sent': True,
# # 'email_sent_time': fields.Datetime.now()
# # })
#
# _logger.info(f"预警邮件发送成功,接收人: {receiver_list}")
#
# except Exception as e:
# _logger.error(f"发送预警邮件失败: {str(e)}")
def
cron_warn_cc_order
(
self
):
# 1. 初始化基础参数
utc_time
=
datetime
.
utcnow
()
warn_days
=
int
(
self
.
env
[
'ir.config_parameter'
]
.
sudo
()
.
get_param
(
'warn_order_days'
,
7
))
end_date
=
fields
.
Date
.
today
()
start_date
=
end_date
-
timedelta
(
days
=
warn_days
)
config_objs
=
self
.
env
[
'warning.config'
]
.
sudo
()
.
search
([])
bl_objs
=
self
.
env
[
'cc.bl'
]
.
sudo
()
.
search
([
(
'bl_date'
,
'>='
,
start_date
),
(
'state'
,
'!='
,
'done'
)
])
# bl_objs = self.env['cc.bl'].sudo().search([
# ('id', '=', 71)
# ])
if
not
config_objs
or
not
bl_objs
:
return
# 2. 获取包裹及预计算提单的 ETA
all_packages
=
self
.
env
[
'cc.ship.package'
]
.
sudo
()
.
search
([(
'bl_id'
,
'in'
,
bl_objs
.
ids
)])
packages_by_bl
=
defaultdict
(
list
)
for
pkg
in
all_packages
:
packages_by_bl
[
pkg
.
bl_id
.
id
]
.
append
(
pkg
)
bl_eta_utc_dict
=
{
bl
.
id
:
self
.
convert_to_utc
(
bl
.
eta
)
if
bl
.
eta
else
None
for
bl
in
bl_objs
}
# 3. 核心优化:彻底解耦时间节点和未同步节点的类型,分别收集 process_code!
bl_codes_needed
=
set
()
pkg_codes_needed
=
set
()
for
config
in
config_objs
:
is_clearance
=
(
config
.
time_type
==
'clearance_node'
)
# A. 收集未同步节点 (决定了我们要检查谁)
if
config
.
unsynced_node_id
.
node_type
==
'bl'
:
bl_codes_needed
.
add
(
config
.
unsynced_node_id
.
tk_code
)
elif
config
.
unsynced_node_id
.
node_type
==
'package'
:
pkg_codes_needed
.
add
(
config
.
unsynced_node_id
.
tk_code
)
# B. 收集预警时间点 (决定了基准时间来自谁)
if
is_clearance
and
config
.
time_point_id
:
if
config
.
time_point_id
.
node_type
==
'bl'
:
bl_codes_needed
.
add
(
config
.
time_point_id
.
tk_code
)
elif
config
.
time_point_id
.
node_type
==
'package'
:
pkg_codes_needed
.
add
(
config
.
time_point_id
.
tk_code
)
# 4. 超级工具函数:使用 search_read 避开 ORM 实例化开销
def
get_all_logs_dict
(
model_name
,
rel_field
,
record_ids
,
process_codes
):
if
not
record_ids
or
not
process_codes
:
return
{}
logs_data
=
self
.
env
[
model_name
]
.
sudo
()
.
search_read
(
[(
rel_field
,
'in'
,
tuple
(
record_ids
)),
(
'process_code'
,
'in'
,
tuple
(
process_codes
))],
[
rel_field
,
'process_code'
,
'operate_time'
],
order
=
'operate_time desc'
)
log_dict
=
defaultdict
(
dict
)
for
data
in
logs_data
:
rec_id
=
data
[
rel_field
][
0
]
if
isinstance
(
data
[
rel_field
],
tuple
)
else
data
[
rel_field
]
code
=
data
[
'process_code'
]
if
code
not
in
log_dict
[
rec_id
]:
log_dict
[
rec_id
][
code
]
=
data
[
'operate_time'
]
return
log_dict
bl_logs_dict
=
get_all_logs_dict
(
'cc.bl.sync.log'
,
'bl_id'
,
bl_objs
.
ids
,
bl_codes_needed
)
pkg_logs_dict
=
get_all_logs_dict
(
'cc.ship.package.sync.log'
,
'package_id'
,
all_packages
.
ids
,
pkg_codes_needed
)
# 5. 统一数据结构:按提单维度合并所有预警
# 数据格式: { bl_id: {'bl': bl_record, 'bl_warnings': [...], 'pkg_warnings': [...]} }
warnings_by_bl
=
defaultdict
(
lambda
:
{
'bl_record'
:
None
,
'bl_warnings'
:
[],
'pkg_warnings'
:
[]
})
# 6. 遍历配置规则进行校验 (纯内存/字典操作)
for
config
in
config_objs
:
is_clearance
=
(
config
.
time_type
==
'clearance_node'
)
target_node_type
=
config
.
unsynced_node_id
.
node_type
# 预警主体(查提单还是查包裹)
time_node_type
=
config
.
time_point_id
.
node_type
if
is_clearance
else
None
# 基准时间来源
unsync_code
=
config
.
unsynced_node_id
.
tk_code
sync_code
=
config
.
time_point_id
.
tk_code
if
is_clearance
else
None
time_offset
=
timedelta
(
hours
=-
int
(
config
.
remaining_time
or
0
))
node_name
=
config
.
unsynced_node_id
.
name
or
''
warning_reason
=
config
.
warning_reason
or
''
for
bl
in
bl_objs
:
warnings_by_bl
[
bl
.
id
][
'bl_record'
]
=
bl
bl_log
=
bl_logs_dict
.
get
(
bl
.
id
,
{})
# 统一获取来自提单层级的基准时间 (航班落地 或 提单节点的清关时间)
if
not
is_clearance
:
bl_base_time
=
bl_eta_utc_dict
.
get
(
bl
.
id
)
elif
time_node_type
==
'bl'
:
bl_base_time
=
bl_log
.
get
(
sync_code
)
else
:
bl_base_time
=
None
# --- 情景 A: 检查提单自身预警 ---
if
target_node_type
==
'bl'
:
if
unsync_code
in
bl_log
:
# 该提单已同步目标节点,跳过
continue
if
bl_base_time
and
utc_time
>
(
bl_base_time
+
time_offset
):
warnings_by_bl
[
bl
.
id
][
'bl_warnings'
]
.
append
({
'name'
:
node_name
,
'warning_reason'
:
warning_reason
})
# --- 情景 B: 检查小包预警 (重头戏) ---
elif
target_node_type
==
'package'
:
alert_pkgs
=
[]
for
pkg
in
packages_by_bl
.
get
(
bl
.
id
,
[]):
pkg_log
=
pkg_logs_dict
.
get
(
pkg
.
id
,
{})
if
unsync_code
in
pkg_log
:
# 该小包已同步目标节点,跳过
continue
# 决定该小包的基准时间:如果是以小包节点计算,取小包日志;否则取上方的提单基准时间
if
is_clearance
and
time_node_type
==
'package'
:
base_time
=
pkg_log
.
get
(
sync_code
)
else
:
base_time
=
bl_base_time
if
base_time
and
utc_time
>
(
base_time
+
time_offset
):
alert_pkgs
.
append
(
pkg
)
if
alert_pkgs
:
warnings_by_bl
[
bl
.
id
][
'pkg_warnings'
]
.
append
({
'name'
:
node_name
,
'arr'
:
alert_pkgs
,
'warning_reason'
:
warning_reason
})
# 7. 清理掉没有产生任何预警的提单空壳数据
final_warnings
=
{
bl_id
:
data
for
bl_id
,
data
in
warnings_by_bl
.
items
()
if
data
[
'bl_warnings'
]
or
data
[
'pkg_warnings'
]
}
# 8. 发送邮件汇总 (统一入口)
if
final_warnings
:
# 你需要根据这个新的字典结构,重写/新增一个统一的格式化邮件函数
email_content
=
self
.
format_combined_warning_email
(
final_warnings
)
self
.
send_warn_email
(
email_content
)
# def format_combined_warning_email(self, combined_warnings):
# """
# 接收 combined_warnings 字典,格式化为HTML邮件正文
# """
# html = "<h3>清关异常预警汇总</h3>"
#
# for bl_id, data in combined_warnings.items():
# bl = data['bl_record']
# html += f"<hr/><h4>提单号: {bl.bl_no}</h4>"
#
# # 1. 拼接提单自身预警
# if data['bl_warnings']:
# html += "<b>【提单维度预警】:</b><ul>"
# for w in data['bl_warnings']:
# html += f"<li>节点: {w['name']}, 原因: {w['warning_reason']}</li>"
# html += "</ul>"
#
# # 2. 拼接该提单下的小包预警
# if data['pkg_warnings']:
# html += "<b>【小包维度预警】:</b><ul>"
# for w in data['pkg_warnings']:
# pkg_nos = ", ".join([p.logistic_order_no for p in w['arr']]) # 假设包裹号字段是 name
# html += f"<li>节点: {w['name']}, 原因: {w['warning_reason']}<br/>"
# html += f"异常包裹: {pkg_nos}</li>"
# html += "</ul>"
#
# return html
def
format_combined_warning_email
(
self
,
combined_warnings
):
"""
按照最新文本格式要求格式化合并后的预警邮件
"""
html_content
=
""
for
bl_id
,
data
in
combined_warnings
.
items
():
bl
=
data
[
'bl_record'
]
# 提单号头部
html_content
+=
f
"提单号{bl.bl_no}<br/>"
# 统一序号计数器
counter
=
1
# 1. 拼接提单自身预警
for
w
in
data
[
'bl_warnings'
]:
node_name
=
w
[
'name'
]
or
'未知节点'
reason
=
w
[
'warning_reason'
]
or
'无'
# 格式: 1.关务节点xxxx未同步,xxxxx
html_content
+=
f
"{counter}.关务节点{node_name}未同步,{reason};<br/>"
counter
+=
1
# 2. 拼接该提单下的小包预警
for
w
in
data
[
'pkg_warnings'
]:
node_name
=
w
[
'name'
]
or
'未知节点'
reason
=
w
[
'warning_reason'
]
or
'无'
pkgs
=
w
.
get
(
'arr'
,
[])
total_count
=
len
(
pkgs
)
if
total_count
>
0
:
# 截取前10个小包对象,并提取它们的名字(单号)
display_pkg_names
=
[
p
.
logistic_order_no
for
p
in
pkgs
[:
10
]]
# 用 '/' 将单号拼接起来
pkg_str
=
"/"
.
join
(
display_pkg_names
)
# 格式: 3.小包xxxx/xxx/xxxx等xxx个小包,节点xxxx未同步,xxxxx
html_content
+=
f
"{counter}.小包{pkg_str}等{total_count}个小包,节点{node_name}未同步,{reason};<br/>"
counter
+=
1
# 如果有多个提单,在下一个提单前额外加一个空行,保持排版清爽
html_content
+=
"<br/>"
# 3. 邮件最末尾统一加上提示语(无需加粗,纯文本风格)
html_content
+=
"请及时操作!"
return
html_content
ccs_connect_tiktok/security/ir.model.access.csv
浏览文件 @
beeba436
...
...
@@ -22,4 +22,7 @@ access_pda_scan_record_manager,pda.scan.record.manager,model_pda_scan_record,bas
access_bl_patrol_user,bl.patrol.user,model_bl_patrol,base.group_user,1,0,0,0
access_bl_patrol_manager,bl.patrol.manager,model_bl_patrol,base.group_system,1,1,1,1
access_package_data_wizard_base.group_user,package_data_wizard base.group_user,ccs_connect_tiktok.model_package_data_wizard,base.group_user,1,1,1,1
\ No newline at end of file
access_package_data_wizard_base.group_user,package_data_wizard base.group_user,ccs_connect_tiktok.model_package_data_wizard,base.group_user,1,1,1,1
access_warning_config_user,warning.config.user,model_warning_config,base.group_user,1,1,1,1
ccs_connect_tiktok/views/menu_view.xml
浏览文件 @
beeba436
...
...
@@ -6,14 +6,14 @@
id=
"menu_tt_api_log"
name=
"TIKTOK推送日志"
parent=
""
groups=
"ccs_base.group_clearance_of_customs_user,ccs_base.group_clearance_of_customs_manager"
sequence=
"2
1
"
action=
"action_ao_tt_api_log"
/>
sequence=
"2
2
"
action=
"action_ao_tt_api_log"
/>
<!-- 菜单项 -->
<menuitem
id=
"menu_bl_patrol"
name=
"提单巡查"
action=
"action_bl_patrol"
sequence=
"2
2
"
sequence=
"2
3
"
groups=
"ccs_base.group_clearance_of_customs_user,ccs_base.group_clearance_of_customs_manager"
/>
...
...
ccs_connect_tiktok/views/warn_config_views.xml
0 → 100644
浏览文件 @
beeba436
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<record
id=
"view_warning_config_form"
model=
"ir.ui.view"
>
<field
name=
"name"
>
warning.config.form
</field>
<field
name=
"model"
>
warning.config
</field>
<field
name=
"arch"
type=
"xml"
>
<form
string=
"预警配置"
>
<sheet>
<widget
name=
"web_ribbon"
title=
"归档"
bg_color=
"bg-danger"
attrs=
"{'invisible': [('active', '=', True)]}"
/>
<div
class=
"oe_title"
>
<h1>
<field
name=
"name"
placeholder=
"请输入预警名称..."
/>
</h1>
</div>
<group>
<group>
<field
name=
"time_type"
widget=
"radio"
/>
<field
name=
"time_point_id"
attrs=
"{
'invisible': [('time_type', '=', 'flight_landing')],
'required': [('time_type', '=', 'clearance_node')]
}"
context=
"{'show_code_in_name': True}"
options=
'{"always_reload": True}'
/>
<field
name=
"flight_landing_time"
attrs=
"{'invisible': [('time_type', '=', 'clearance_node')]}"
/>
<field
name=
"remaining_time"
/>
</group>
<group>
<field
name=
"unsynced_node_id"
context=
"{'show_code_in_name': True}"
options=
'{"always_reload": True}'
/>
<field
name=
"active"
invisible=
"1"
/>
</group>
</group>
<notebook>
<page
string=
"预警原因"
name=
"reason"
>
<field
name=
"warning_reason"
placeholder=
"请详细描述预警原因..."
/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record
id=
"view_warning_config_tree"
model=
"ir.ui.view"
>
<field
name=
"name"
>
warning.config.tree
</field>
<field
name=
"model"
>
warning.config
</field>
<field
name=
"arch"
type=
"xml"
>
<tree
string=
"预警配置"
>
<field
name=
"name"
/>
<field
name=
"time_type"
/>
<field
name=
"time_point_id"
attrs=
"{'invisible': [('time_type', '=', 'flight_landing')]}"
/>
<field
name=
"flight_landing_time"
attrs=
"{'invisible': [('time_type', '=', 'clearance_node')]}"
/>
<field
name=
"remaining_time"
/>
<field
name=
"unsynced_node_id"
/>
<field
name=
"active"
widget=
"boolean_toggle"
/>
</tree>
</field>
</record>
<record
id=
"view_warning_config_search"
model=
"ir.ui.view"
>
<field
name=
"name"
>
warning.config.search
</field>
<field
name=
"model"
>
warning.config
</field>
<field
name=
"arch"
type=
"xml"
>
<search
string=
"搜索预警配置"
>
<field
name=
"name"
/>
<field
name=
"time_point_id"
/>
<filter
string=
"航班落地"
name=
"type_flight"
domain=
"[('time_type', '=', 'flight_landing')]"
/>
<filter
string=
"清关进度节点"
name=
"type_clearance"
domain=
"[('time_type', '=', 'clearance_node')]"
/>
<separator/>
<filter
string=
"归档"
name=
"inactive"
domain=
"[('active', '=', False)]"
/>
</search>
</field>
</record>
<record
id=
"action_warning_config"
model=
"ir.actions.act_window"
>
<field
name=
"name"
>
预警配置
</field>
<field
name=
"res_model"
>
warning.config
</field>
<field
name=
"view_mode"
>
tree,form
</field>
<field
name=
"help"
type=
"html"
>
<p
class=
"o_view_nocontent_smiling_face"
>
创建第一条预警配置
</p>
</field>
</record>
<menuitem
id=
"menu_warning_config"
name=
"预警配置"
action=
"action_warning_config"
sequence=
"21"
groups=
"base.group_system"
/>
</odoo>
\ No newline at end of file
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论