提交 8f8ddcb9 authored 作者: 贺阳's avatar 贺阳

1、清关中可操作“完成”,点击“完成”进行提单巡查的逻辑,没有问题的,则可变成已完成。有问题的进行提示。权限归清关员/清关经理

2、已完成可点击“追回”,追回变成清关中 3、清关中到已完成支持批量操作 4、根据小包状态自动变成已完成时,也需检查要进行提单巡查的逻辑,有问题的发邮件
上级 d424a312
......@@ -6,8 +6,8 @@ msgid ""
msgstr ""
"Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-15 02:39+0000\n"
"PO-Revision-Date: 2025-07-15 10:45+0800\n"
"POT-Creation-Date: 2025-09-18 08:29+0000\n"
"PO-Revision-Date: 2025-09-18 16:30+0800\n"
"Last-Translator: \n"
"Language-Team: \n"
"Language: zh_CN\n"
......@@ -531,6 +531,11 @@ msgstr "批量"
msgid "Batch Add Package Exception Information"
msgstr "批量添加异常信息"
#. module: ccs_base
#: model:ir.actions.server,name:ccs_base.bl_complete_server_action
msgid "Batch Complete"
msgstr "批量完成"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard
msgid "Batch Link Transfer B/L No"
......@@ -586,15 +591,6 @@ msgstr "大包"
msgid "Big Package No"
msgstr "大包号"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/associate_pallet_wizard.py:0
#, python-format
msgid ""
"Big Package No :%s ,The same bill of lading, same pallet number, and usage "
"date must be consistent!"
msgstr "大包号:%s ,同一提单、同一托盘号的使用日期必须一致!"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__big_package_no
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_big_package__big_package_no
......@@ -959,6 +955,11 @@ msgstr "公司"
msgid "Company Code"
msgstr "公司编码"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view
msgid "Complete"
msgstr "完成"
#. module: ccs_base
#: model:ir.model,name:ccs_base.model_res_config_settings
msgid "Config Settings"
......@@ -2385,6 +2386,13 @@ msgstr ""
msgid "ONLINE SELLING PLACE"
msgstr "网上销售网站"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0
#, python-format
msgid "Only completed bills of loading can be recalled!"
msgstr "只有已完成状态的提单可以追回!"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
......@@ -2392,6 +2400,13 @@ msgstr "网上销售网站"
msgid "Only excel files can be uploaded!"
msgstr "只能上传excel文件!"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0
#, python-format
msgid "Only the status of the bill of loading is ccing can be completed!"
msgstr "只有清关中状态的提单可以完成!"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__operate_remark
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_package_sync_log_view
......@@ -2692,6 +2707,11 @@ msgstr "收件人邮政编码"
msgid "Real Weight"
msgstr "实际重量"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view
msgid "Recall"
msgstr "追回"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.tree_cc_bl_view
msgid "Receive Big Package"
......@@ -3390,13 +3410,6 @@ msgstr "使用日期不能大于当前日期!"
msgid "This B/L No does not exist in the system"
msgstr "提单号在系统不存在"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
#, python-format
msgid "This Transfer B/L No already exists"
msgstr "转单号已存在"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_big_package_view
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_history_big_package_view
......@@ -3518,6 +3531,7 @@ msgstr "转单号必填"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
#, python-format
msgid "Transfer B/L No. cannot be the same as B/L No."
msgstr "转单号不能与提单号相同。"
......@@ -3525,6 +3539,7 @@ msgstr "转单号不能与提单号相同。"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
#, python-format
msgid "Transfer B/L No. cannot be the same as B/L No. or Transfer B/L No."
msgstr "转单号不能与提单号或转单号相同。"
......
......@@ -810,7 +810,14 @@ class CcBL(models.Model):
if item.state == 'draft':
item.state = 'ccing'
def done_func(self):
def complete_func(self):
"""点完成按钮,状态变为已完成"""
for item in self:
if item.state != 'ccing':
raise ValidationError(_('Only the status of the bill of loading is ccing can be completed!'))#只有清关中状态的提单可以完成
item.done_func()
def done_func(self, is_email=False):
"""
变为已完成
"""
......@@ -818,6 +825,13 @@ class CcBL(models.Model):
if item.state == 'ccing':
item.state = 'done'
def action_recall(self):
"""追回操作,将状态从已完成改为清关中"""
for record in self:
if record.state != 'done':
raise ValidationError(_('Only completed bills of loading can be recalled!'))#只有已完成状态的提单可以追回
record.state = 'ccing'
# 定义3个方法,分别创建显示该提单大包,包裹,商品的action
# 创建显示大包的action
def action_show_big_package(self):
......
......@@ -59,7 +59,19 @@
string="Update Bill Of Loading Status"
context="{'active_id': id,'default_bl_id': active_id,
'default_last_process_time':process_time,'default_current_status':customs_clearance_status}"/>
<field name="state" widget="statusbar" options="{'clickable': '1'}"/>
<!-- 完成按钮 - 仅在清关中状态显示,仅清关员和清关经理可操作 -->
<button name="complete_func" type="object" class="oe_highlight"
string="Complete" attrs="{'invisible': [('state', '!=', 'ccing')]}"
groups="ccs_base.group_clearance_of_customs_user,ccs_base.group_clearance_of_customs_manager"/>
<!-- 追回按钮 - 仅在已完成状态显示,仅清关员和清关经理可操作 -->
<button name="action_recall" type="object" string="Recall" attrs="{'invisible': [('state', '!=', 'done')]}"
groups="ccs_base.group_clearance_of_customs_user,ccs_base.group_clearance_of_customs_manager"/>
<field name="state" widget="statusbar"/>
<!-- options="{'clickable': '1'}" -->
</header>
<header>
<field name="customs_clearance_status" widget="statusbar"/>
......@@ -432,4 +444,18 @@
</field>
</record>
<record id="bl_complete_server_action" model="ir.actions.server">
<field name="name">Batch Complete</field>
<field name="model_id" ref="model_cc_bl"/>
<field name="binding_model_id" ref="model_cc_bl"/>
<field name="state">code</field>
<field name="binding_view_types">list</field>
<field name="groups_id" eval="[(4, ref('ccs_base.group_clearance_of_customs_user'))]"/>
<field name="code">
if records:
action = records.complete_func()
</field>
</record>
</odoo>
\ No newline at end of file
......@@ -2,8 +2,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
from odoo import models, api, fields, _
from odoo.exceptions import Warning, ValidationError
from odoo.exceptions import ValidationError
# 定义一个批量更新小包状态的向导, 用于批量更新小包状态
......@@ -82,6 +83,7 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
state_explain = fields.Text('State Explain', help='State Explain')
node_exception_reason_id = fields.Many2one('cc.node.exception.reason', 'Exception Reason',
domain="[('code_id', '=', update_status)]")
# 批量更新小包状态
def submit(self):
# 确认数据
......@@ -89,7 +91,8 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
raise ValidationError('Please confirm that the above data is correct.') # 请确认以上数据正确
parcels = self.get_process_package()
logging.info(
'更新小包状态的小包:%s,当前状态:%s,更新状态:%s' % (parcels, self.current_status, self.update_status))
if not parcels:
raise ValidationError(_('No package to update found.')) # 没有找到要更新的小包
# 1.若选择的更新节点为是当前节点【清关节点设置,是当前节点字段名称改为初始节点】,当更新节点为初始节点时,无需填写操作时间;
......@@ -106,7 +109,7 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
# 更新状态
parcels.write(
{'state': self.update_status.id, 'node_exception_reason_id': self.node_exception_reason_id.id,
'process_time': self.process_time, 'state_explain': self.state_explain, 'is_sync': is_sync})
'process_time': self.process_time, 'state_explain': self.state_explain, 'is_sync': is_sync})
# if parcels:
# where_sql = " where id={0}".format(parcels[0].id) if len(
# parcels) == 1 else " where id in {0}".format(tuple(parcels.ids))
......@@ -116,9 +119,9 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
# where_sql)
# update_sql = update_sql.replace("'False'", "null").replace("False", "null")
# self._cr.execute(update_sql)
# parcels.write({'state': self.update_status.id})
# for parcel in parcels:
# parcel.message_post(body='%s改为%s' % (self.current_status.name, self.update_status.name))
# parcels.write({'state': self.update_status.id})
# for parcel in parcels:
# parcel.message_post(body='%s改为%s' % (self.current_status.name, self.update_status.name))
# 生成sns日志
# self.bl_id.message_post(body='%s更新为%s' % (self.current_status.name or '', self.update_status.name or ''))
# 跳转显示本次更新状态的小包 更新小包状态
......
# -*- coding: utf-8 -*-
import logging
import re
from datetime import timedelta
from odoo import models, fields, api
......@@ -60,7 +59,7 @@ class BlPatrol(models.Model):
# 如果有问题,发送邮件
if result['issue_count'] > 0:
patrol._send_patrol_email(result)
self.env['cc.bl'].sudo()._send_patrol_email(result,patrol_obj=patrol)
_logger.info(f"提单巡查完成: {patrol.name}, 发现问题: {result['issue_count']}")
......@@ -87,344 +86,7 @@ class BlPatrol(models.Model):
bls = self.env['cc.bl'].sudo().search(
[('bl_date', '>=', start_date), ('bl_date', '<=', end_date), ('state', 'in', ['ccing', 'done'])])
_logger.info(f"开始巡查提单,检查范围: {start_date} 到 {end_date}, 提单数量: {len(bls)}")
error_package_issues = []
error_bl_issues = []
package_issue_counter = 1 # 小包问题独立编号
bl_issue_counter = 1 # 提单问题独立编号
for bl in bls:
# 检查小包轨迹问题
bl_package_issues = self._check_package_tracking_issues(bl)
if bl_package_issues:
# 为小包问题添加独立编号
for i, issue in enumerate(bl_package_issues):
# 使用正则表达式匹配任何数字+顿号的格式
if re.match(r'^\d+、', issue):
# 替换开头的编号
bl_package_issues[i] = re.sub(r'^\d+、', f"{package_issue_counter}、", issue, 1)
else:
# 如果没有编号,添加编号
bl_package_issues[i] = f"{package_issue_counter}、{issue}"
package_issue_counter += 1
error_package_issues.extend(bl_package_issues)
# 检查提单关务节点问题
bl_node_issues = self._check_bl_node_issues(bl)
if bl_node_issues:
# 为提单问题添加独立编号
for i, blissue in enumerate(bl_node_issues):
# 使用正则表达式匹配任何数字+顿号的格式
if re.match(r'^\d+、', blissue):
# 替换开头的编号
bl_node_issues[i] = re.sub(r'^\d+、', f"{bl_issue_counter}、", blissue, 1)
else:
# 如果没有编号,添加编号
bl_node_issues[i] = f"{bl_issue_counter}、{blissue}"
bl_issue_counter += 1
error_bl_issues.extend(bl_node_issues)
return {
'success': True,
'bl_count': len(bls),
'issue_count': len(error_package_issues) + len(error_bl_issues),
'package_issues': '\n'.join(error_package_issues) if error_package_issues else '',
'bl_issues': '\n'.join(error_bl_issues) if error_bl_issues else ''
}
def _check_package_tracking_issues(self, bl):
"""
检查小包轨迹问题 - 只检查真正的小包节点,不检查关务节点
"""
issues = []
# 按问题类型分组统计
reverse_issues = {} # 倒挂问题
missing_issues = {} # 漏推问题
# 获取该提单下所有小包的同步日志
ship_packages = bl.ship_package_ids
for package in ship_packages:
# 按节点编码分组,根据同步时间降序,得到对应的同步日志对象,然后再根据节点编码的顺序排序,得到同步日志对象数组
logs_by_process = {}
for log in package.sync_log_ids:
if log.process_code not in logs_by_process:
logs_by_process[log.process_code] = log
else:
# 如果已有该节点的日志,比较sync_time,保留最新的
if log.sync_time and log.sync_time > logs_by_process[log.process_code].sync_time:
logs_by_process[log.process_code] = log
# 方法1: 根据节点的seq进行排序(推荐)
sync_logs = []
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),('is_must', '=', True)], order='seq')
if bl_nodes:
for node in bl_nodes:
if node.is_default:
continue
if node.tk_code in logs_by_process:
sync_logs.append(logs_by_process[node.tk_code])
sync_logs = [log for log in sync_logs if log is not None]
#根据key排序获取logs
if len(sync_logs) >= 2:
for i in range(1, len(sync_logs)):
current_log = sync_logs[i] # 当前日志
previous_log = sync_logs[i - 1] # 前一个日志
current_time = current_log.operate_time # 当前日志时间
previous_time = previous_log.operate_time # 前一个日志时间
if current_time and previous_time and current_time < previous_time:
try:
# 检查节点类型,只处理小包节点
node = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('tk_code', '=', current_log.process_code)])
if node: # 只处理小包节点
progress_name = node.name or "空"
process_code = node.tk_code or "空"
issue_key = f"{progress_name}({process_code})倒挂"
if issue_key not in reverse_issues:
reverse_issues[issue_key] = []
reverse_issues[issue_key].append(package.logistic_order_no)
except Exception as e:
_logger.warning(f"构建小包倒挂问题描述失败: {str(e)}")
# 如果无法确定节点类型,跳过
continue
# 检查漏推问题 - 只检查小包节点
package_issues = self._check_package_missing_nodes(package, sync_logs)
for issue in package_issues:
# 提取问题类型
if "轨迹漏推" in issue:
try:
# 安全地提取问题类型,避免索引越界
if ",出现" in issue and ",涉及" in issue:
issue_key = issue.split(",出现")[1].split(",涉及")[0]
else:
# 如果格式不匹配,使用整个问题描述作为key
issue_key = issue
if issue_key not in missing_issues:
missing_issues[issue_key] = []
missing_issues[issue_key].append(package.logistic_order_no)
except (IndexError, AttributeError) as e:
_logger.warning(f"解析小包问题失败: {issue}, 错误: {str(e)}")
# 使用整个问题描述作为key
issue_key = issue
if issue_key not in missing_issues:
missing_issues[issue_key] = []
missing_issues[issue_key].append(package.logistic_order_no)
# 格式化倒挂问题
issue_counter = 1
for issue_type, packages in reverse_issues.items():
package_list = self._format_package_list(packages)
issues.append(
f"{issue_counter}、{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
issue_counter += 1
# 格式化漏推问题
for issue_type, packages in missing_issues.items():
package_list = self._format_package_list(packages)
issues.append(
f"{issue_counter}、{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
issue_counter += 1
return issues
def _format_package_list(self, packages):
"""
格式化小包列表,最多显示10个
"""
try:
# 过滤掉None和空字符串
valid_packages = [pkg for pkg in packages if pkg and str(pkg).strip()]
if not valid_packages:
return "无有效追踪号"
if len(valid_packages) <= 10:
return "/".join(valid_packages)
else:
return "/".join(valid_packages[:10]) + "等"
except Exception as e:
_logger.warning(f"格式化小包列表失败: {str(e)}")
return "格式化失败"
def _check_package_missing_nodes(self, package, sync_logs):
"""
检查小包漏推节点
"""
issues = []
# 获取所有小包节点,按顺序排序
package_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),('is_must', '=', True)], order='seq')
if not package_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in package_nodes:
# 跳过当前节点(最后一个节点)
if node == package_nodes[-1]:
continue
# 跳过初始节点(is_default=True)
if getattr(node, 'is_default', False):
continue
# 检查是否有该节点的同步日志
has_node_log = any(log.process_code == node.tk_code for log in sync_logs)
# 检查后续节点是否有日志
next_nodes = package_nodes.filtered(lambda n: n.seq > node.seq)
for next_node in next_nodes:
has_next_log = any(log.process_code == next_node.tk_code for log in sync_logs)
# 如果后续节点有日志但当前节点没有,说明漏推
if has_next_log and not has_node_log:
try:
node_name = node.name or "空"
tk_code = node.tk_code or "空"
issues.append(
f"{node_name}({tk_code})轨迹漏推"
)
except Exception as e:
_logger.warning(f"构建小包漏推问题描述失败: {str(e)}")
issues.append("节点轨迹漏推")
break
return issues
def _check_bl_node_issues(self, bl):
"""
检查提单关务节点问题
"""
issues = []
# 按节点编码分组,根据同步时间降序,得到对应的同步日志对象
logs_by_bl_process = {}
for log in bl.bl_sync_log_ids:
if log.process_code not in logs_by_bl_process:
logs_by_bl_process[log.process_code] = log
else:
# 如果已有该节点的日志,比较sync_time,保留最新的
if log.sync_time and log.sync_time > logs_by_bl_process[log.process_code].sync_time:
logs_by_bl_process[log.process_code] = log
# 根据节点的seq进行排序
sync_logs = []
# 获取所有提单节点,按业务顺序排序
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'bl'),('is_must', '=', True)], order='seq')
if bl_nodes:
for node in bl_nodes:
if node.tk_code in logs_by_bl_process.keys():
sync_logs.append(logs_by_bl_process[node.tk_code])
# 检查倒挂问题
if len(sync_logs) >= 2:
# 收集所有有日志的节点及其时间
node_logs = {} # {node_index: (node, log, time)}
for i, node in enumerate(bl_nodes):
for log in sync_logs:
if log.process_code == node.tk_code:
node_logs[i] = (node, log, log.operate_time)
break
# 遍历每个有日志的节点,与前面所有有日志的节点比较
for current_idx in sorted(node_logs.keys()):
current_node, current_log, current_time = node_logs[current_idx]
# 与前面所有有日志的节点比较
for prev_idx in sorted(node_logs.keys()):
if prev_idx >= current_idx: # 跳过自己和自己后面的节点
continue
prev_node, prev_log, prev_time = node_logs[prev_idx]
# 检查时间顺序
if current_time and prev_time:
if current_time < prev_time:
try:
progress_name = current_log.progress_name or "空"
process_code = current_log.process_code or "空"
issues.append(
f"{bl.bl_no},出现{progress_name}({process_code})倒挂"
)
except Exception as e:
_logger.warning(f"构建倒序问题描述失败: {str(e)}")
issues.append(f"{bl.bl_no},出现节点倒挂")
# 检查漏推问题
missing_issues = self._check_bl_missing_nodes(bl, sync_logs)
if missing_issues:
issues.extend(missing_issues)
return issues
def _check_bl_missing_nodes(self, bl, sync_logs):
"""
检查提单漏推节点
"""
issues = []
# 获取所有提单节点,按顺序排序
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'bl'),('is_must', '=', True)], order='seq')
if not bl_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in bl_nodes:
# 跳过当前节点(最后一个节点)
if node == bl_nodes[-1]:
continue
# 跳过初始节点(is_default=True)
if getattr(node, 'is_default', False):
continue
# 检查是否有该节点的同步日志
has_node_log = any(log.process_code == node.tk_code for log in sync_logs)
# 检查后续节点是否有日志
next_nodes = bl_nodes.filtered(lambda n: n.seq > node.seq)
for next_node in next_nodes:
has_next_log = any(log.process_code == next_node.tk_code for log in sync_logs)
# 如果后续节点有日志但当前节点没有,说明漏推
if has_next_log and not has_node_log:
try:
node_name = node.name or "空"
tk_code = node.tk_code or "空"
issues.append(
f"{bl.bl_no},出现{node_name}({tk_code})轨迹漏推"
)
except Exception as e:
_logger.warning(f"构建提单漏推问题描述失败: {str(e)}")
issues.append(f"{bl.bl_no},出现节点轨迹漏推")
break
return issues
def _send_patrol_email(self, result):
"""
发送巡查邮件
"""
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:
_logger.warning("邮件配置不完整,跳过邮件发送")
return
# 解析接收邮箱
receiver_list = [email.strip() for email in receiver_emails.split(';') if email.strip()]
# 构建邮件内容
subject = f"(重要)推送预警/{fields.Date.today()}系统巡查轨迹"
# 构建邮件内容
package_content = result['package_issues'] if result['package_issues'] else ""
bl_content = result['bl_issues'] if result['bl_issues'] else ""
content = f"""您好,经系统巡查。
%s
%s
%s
%s
请立即处理!!!""" % ("发现以下提单存在小包轨迹倒挂/轨迹漏推" if package_content else "", package_content if package_content else "", "发现以下提单存在提单关务节点轨迹倒挂/轨迹漏推" if bl_content else "", bl_content if bl_content else "")
# 发送邮件
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,
}).send()
# 更新发送记录
self.write({
'email_sent': True,
'email_sent_time': fields.Datetime.now()
})
_logger.info(f"巡查邮件发送成功,接收人: {receiver_list}")
except Exception as e:
_logger.error(f"发送巡查邮件失败: {str(e)}")
return bls.check_bl_patrol()
def action_manual_patrol(self):
"""
......
......@@ -4,12 +4,12 @@ import json
import logging
import ssl
from datetime import timedelta, datetime
import re
import aiohttp
import certifi
import pytz
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
def get_rfc339_time(utc_time=None):
if not utc_time:
......@@ -362,7 +362,7 @@ class CcBl(models.Model):
# 如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
if all(line.state.is_done for line in
self.ship_package_ids) and self.unsync_package_count <= 0 and self.customs_clearance_status.is_done and self.is_bl_sync:
self.done_func()
self.done_func(is_email = True)
def change_customs_state_by_ship_package(self, package_state_obj, user_obj=False):
"""
......@@ -444,6 +444,382 @@ class CcBl(models.Model):
result = self.env.cr.fetchone()
return result and result[0] or False
def done_func(self, is_email=False):
"""
变为已完成.先进行提单巡查,再进行提单状态变更
"""
bls = self.filtered(lambda x: x.state == 'ccing')
# 提单巡查
result = bls.check_bl_patrol()
logging.info(f"提单完成时手动巡查完成, 发现问题: {result['issue_count']}")
if is_email:
# 如果有问题,发送邮件
if result['issue_count'] > 0:
bls._send_patrol_email(result)
else:
#把提示进行提示
content = self.get_patrol_email_content(result)
raise ValidationError(content)
super(CcBl, self).done_func(is_email=is_email)
def check_bl_patrol(self):
"""
执行巡查逻辑
检查对象:提单日期为近x天(默认5天),清关中和完成的提单。
检查内容:符合要求的提单,提单关务节点同步日志是否出现倒挂
(根据配置的清关节点排序,若后一个节点的操作时间,小于前序节点时间,则属于倒挂),是否有漏推(若节点已产生同步日期,但前序节点无同步日期,则属于漏推。是当前节点的节点无需纳入判断);
"""
error_package_issues = []
error_bl_issues = []
package_issue_counter = 1 # 小包问题独立编号
bl_issue_counter = 1 # 提单问题独立编号
for bl in self:
# 检查小包轨迹问题
bl_package_issues = self._check_package_tracking_issues(bl)
if bl_package_issues:
# 为小包问题添加独立编号
for i, issue in enumerate(bl_package_issues):
# 使用正则表达式匹配任何数字+顿号的格式
if re.match(r'^\d+、', issue):
# 替换开头的编号
bl_package_issues[i] = re.sub(r'^\d+、', f"{package_issue_counter}、", issue, 1)
else:
# 如果没有编号,添加编号
bl_package_issues[i] = f"{package_issue_counter}、{issue}"
package_issue_counter += 1
error_package_issues.extend(bl_package_issues)
# 检查提单关务节点问题
bl_node_issues = self._check_bl_node_issues(bl)
if bl_node_issues:
# 为提单问题添加独立编号
for i, blissue in enumerate(bl_node_issues):
# 使用正则表达式匹配任何数字+顿号的格式
if re.match(r'^\d+、', blissue):
# 替换开头的编号
bl_node_issues[i] = re.sub(r'^\d+、', f"{bl_issue_counter}、", blissue, 1)
else:
# 如果没有编号,添加编号
bl_node_issues[i] = f"{bl_issue_counter}、{blissue}"
bl_issue_counter += 1
error_bl_issues.extend(bl_node_issues)
return {
'success': True,
'bl_count': len(self),
'issue_count': len(error_package_issues) + len(error_bl_issues),
'package_issues': '\n'.join(error_package_issues) if error_package_issues else '',
'bl_issues': '\n'.join(error_bl_issues) if error_bl_issues else ''
}
def _check_package_tracking_issues(self, bl):
"""
检查小包轨迹问题 - 只检查真正的小包节点,不检查关务节点
"""
issues = []
# 按问题类型分组统计
reverse_issues = {} # 倒挂问题
missing_issues = {} # 漏推问题
# 获取该提单下所有小包的同步日志
ship_packages = bl.ship_package_ids
for package in ship_packages:
# 按节点编码分组,根据同步时间降序,得到对应的同步日志对象,然后再根据节点编码的顺序排序,得到同步日志对象数组
logs_by_process = {}
for log in package.sync_log_ids:
if log.process_code not in logs_by_process:
logs_by_process[log.process_code] = log
else:
# 如果已有该节点的日志,比较sync_time,保留最新的
if log.sync_time and log.sync_time > logs_by_process[log.process_code].sync_time:
logs_by_process[log.process_code] = log
# 方法1: 根据节点的seq进行排序(推荐)
sync_logs = []
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('is_must', '=', True)],
order='seq')
if bl_nodes:
for node in bl_nodes:
if node.is_default:
continue
if node.tk_code in logs_by_process:
sync_logs.append(logs_by_process[node.tk_code])
sync_logs = [log for log in sync_logs if log is not None]
# 根据key排序获取logs
if len(sync_logs) >= 2:
for i in range(1, len(sync_logs)):
current_log = sync_logs[i] # 当前日志
previous_log = sync_logs[i - 1] # 前一个日志
current_time = current_log.operate_time # 当前日志时间
previous_time = previous_log.operate_time # 前一个日志时间
if current_time and previous_time and current_time < previous_time:
try:
# 检查节点类型,只处理小包节点
node = self.env['cc.node'].sudo().search(
[('node_type', '=', 'package'), ('tk_code', '=', current_log.process_code)])
if node: # 只处理小包节点
progress_name = node.name or "空"
process_code = node.tk_code or "空"
issue_key = f"{progress_name}({process_code})倒挂"
if issue_key not in reverse_issues:
reverse_issues[issue_key] = []
reverse_issues[issue_key].append(package.logistic_order_no)
except Exception as e:
logging.warning(f"构建小包倒挂问题描述失败: {str(e)}")
# 如果无法确定节点类型,跳过
continue
# 检查漏推问题 - 只检查小包节点
package_issues = self._check_package_missing_nodes(package, sync_logs)
for issue in package_issues:
# 提取问题类型
if "轨迹漏推" in issue:
try:
# 安全地提取问题类型,避免索引越界
if ",出现" in issue and ",涉及" in issue:
issue_key = issue.split(",出现")[1].split(",涉及")[0]
else:
# 如果格式不匹配,使用整个问题描述作为key
issue_key = issue
if issue_key not in missing_issues:
missing_issues[issue_key] = []
missing_issues[issue_key].append(package.logistic_order_no)
except (IndexError, AttributeError) as e:
logging.warning(f"解析小包问题失败: {issue}, 错误: {str(e)}")
# 使用整个问题描述作为key
issue_key = issue
if issue_key not in missing_issues:
missing_issues[issue_key] = []
missing_issues[issue_key].append(package.logistic_order_no)
# 格式化倒挂问题
issue_counter = 1
for issue_type, packages in reverse_issues.items():
package_list = self._format_package_list(packages)
issues.append(
f"{issue_counter}、{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
issue_counter += 1
# 格式化漏推问题
for issue_type, packages in missing_issues.items():
package_list = self._format_package_list(packages)
issues.append(
f"{issue_counter}、{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
issue_counter += 1
return issues
def _format_package_list(self, packages):
"""
格式化小包列表,最多显示10个
"""
try:
# 过滤掉None和空字符串
valid_packages = [pkg for pkg in packages if pkg and str(pkg).strip()]
if not valid_packages:
return "无有效追踪号"
if len(valid_packages) <= 10:
return "/".join(valid_packages)
else:
return "/".join(valid_packages[:10]) + "等"
except Exception as e:
logging.warning(f"格式化小包列表失败: {str(e)}")
return "格式化失败"
def _check_package_missing_nodes(self, package, sync_logs):
"""
检查小包漏推节点
"""
issues = []
# 获取所有小包节点,按顺序排序
package_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), ('is_must', '=', True)],
order='seq')
if not package_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in package_nodes:
# 跳过当前节点(最后一个节点)
if node == package_nodes[-1]:
continue
# 跳过初始节点(is_default=True)
if getattr(node, 'is_default', False):
continue
# 检查是否有该节点的同步日志
has_node_log = any(log.process_code == node.tk_code for log in sync_logs)
# 检查后续节点是否有日志
next_nodes = package_nodes.filtered(lambda n: n.seq > node.seq)
for next_node in next_nodes:
has_next_log = any(log.process_code == next_node.tk_code for log in sync_logs)
# 如果后续节点有日志但当前节点没有,说明漏推
if has_next_log and not has_node_log:
try:
node_name = node.name or "空"
tk_code = node.tk_code or "空"
issues.append(
f"{node_name}({tk_code})轨迹漏推"
)
except Exception as e:
logging.warning(f"构建小包漏推问题描述失败: {str(e)}")
issues.append("节点轨迹漏推")
break
return issues
def _check_bl_node_issues(self, bl):
"""
检查提单关务节点问题
"""
issues = []
# 按节点编码分组,根据同步时间降序,得到对应的同步日志对象
logs_by_bl_process = {}
for log in bl.bl_sync_log_ids:
if log.process_code not in logs_by_bl_process:
logs_by_bl_process[log.process_code] = log
else:
# 如果已有该节点的日志,比较sync_time,保留最新的
if log.sync_time and log.sync_time > logs_by_bl_process[log.process_code].sync_time:
logs_by_bl_process[log.process_code] = log
# 根据节点的seq进行排序
sync_logs = []
# 获取所有提单节点,按业务顺序排序
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'bl'), ('is_must', '=', True)], order='seq')
if bl_nodes:
for node in bl_nodes:
if node.tk_code in logs_by_bl_process.keys():
sync_logs.append(logs_by_bl_process[node.tk_code])
# 检查倒挂问题
if len(sync_logs) >= 2:
# 收集所有有日志的节点及其时间
node_logs = {} # {node_index: (node, log, time)}
for i, node in enumerate(bl_nodes):
for log in sync_logs:
if log.process_code == node.tk_code:
node_logs[i] = (node, log, log.operate_time)
break
# 遍历每个有日志的节点,与前面所有有日志的节点比较
for current_idx in sorted(node_logs.keys()):
current_node, current_log, current_time = node_logs[current_idx]
# 与前面所有有日志的节点比较
for prev_idx in sorted(node_logs.keys()):
if prev_idx >= current_idx: # 跳过自己和自己后面的节点
continue
prev_node, prev_log, prev_time = node_logs[prev_idx]
# 检查时间顺序
if current_time and prev_time:
if current_time < prev_time:
try:
progress_name = current_log.progress_name or "空"
process_code = current_log.process_code or "空"
issues.append(
f"{bl.bl_no},出现{progress_name}({process_code})倒挂"
)
except Exception as e:
logging.warning(f"构建倒序问题描述失败: {str(e)}")
issues.append(f"{bl.bl_no},出现节点倒挂")
# 检查漏推问题
missing_issues = self._check_bl_missing_nodes(bl, sync_logs)
if missing_issues:
issues.extend(missing_issues)
return issues
def _check_bl_missing_nodes(self, bl, sync_logs):
"""
检查提单漏推节点
"""
issues = []
# 获取所有提单节点,按顺序排序
bl_nodes = self.env['cc.node'].sudo().search([('node_type', '=', 'bl'), ('is_must', '=', True)], order='seq')
if not bl_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in bl_nodes:
# 跳过当前节点(最后一个节点)
if node == bl_nodes[-1]:
continue
# 跳过初始节点(is_default=True)
if getattr(node, 'is_default', False):
continue
# 检查是否有该节点的同步日志
has_node_log = any(log.process_code == node.tk_code for log in sync_logs)
# 检查后续节点是否有日志
next_nodes = bl_nodes.filtered(lambda n: n.seq > node.seq)
for next_node in next_nodes:
has_next_log = any(log.process_code == next_node.tk_code for log in sync_logs)
# 如果后续节点有日志但当前节点没有,说明漏推
if has_next_log and not has_node_log:
try:
node_name = node.name or "空"
tk_code = node.tk_code or "空"
issues.append(
f"{bl.bl_no},出现{node_name}({tk_code})轨迹漏推"
)
except Exception as e:
logging.warning(f"构建提单漏推问题描述失败: {str(e)}")
issues.append(f"{bl.bl_no},出现节点轨迹漏推")
break
return issues
def _send_patrol_email(self, result,patrol_obj=False):
"""
发送巡查邮件
"""
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"(重要)推送预警/{fields.Date.today()}系统巡查轨迹"
content = self.get_patrol_email_content(result)
# 发送邮件
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,
}).send()
# 更新发送记录
if patrol_obj:
patrol_obj.write({
'email_sent': True,
'email_sent_time': fields.Datetime.now()
})
logging.info(f"巡查邮件发送成功,接收人: {receiver_list}")
except Exception as e:
logging.error(f"发送巡查邮件失败: {str(e)}")
def get_patrol_email_content(self, result,title='您好,经系统巡查。'):
"""
获取巡查邮件内容
"""
# 构建邮件内容
package_content = result['package_issues'] if result['package_issues'] else ""
bl_content = result['bl_issues'] if result['bl_issues'] else ""
content = f"""%s
%s
%s
%s
%s
请立即处理!!!""" % (title,"发现以下提单存在小包轨迹倒挂/轨迹漏推" if package_content else "",
package_content if package_content else "",
"发现以下提单存在提单关务节点轨迹倒挂/轨迹漏推" if bl_content else "",
bl_content if bl_content else "")
return content
# =============同步提单状态==================================
# 定义一个方法, 获取提单,并回传提单状态
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论