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

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

2、已完成可点击“追回”,追回变成清关中 3、清关中到已完成支持批量操作 4、根据小包状态自动变成已完成时,也需检查要进行提单巡查的逻辑,有问题的发邮件
上级 d424a312
...@@ -6,8 +6,8 @@ msgid "" ...@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 16.0\n" "Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-07-15 02:39+0000\n" "POT-Creation-Date: 2025-09-18 08:29+0000\n"
"PO-Revision-Date: 2025-07-15 10:45+0800\n" "PO-Revision-Date: 2025-09-18 16:30+0800\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: zh_CN\n" "Language: zh_CN\n"
...@@ -531,6 +531,11 @@ msgstr "批量" ...@@ -531,6 +531,11 @@ msgstr "批量"
msgid "Batch Add Package Exception Information" msgid "Batch Add Package Exception Information"
msgstr "批量添加异常信息" msgstr "批量添加异常信息"
#. module: ccs_base
#: model:ir.actions.server,name:ccs_base.bl_complete_server_action
msgid "Batch Complete"
msgstr "批量完成"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard
msgid "Batch Link Transfer B/L No" msgid "Batch Link Transfer B/L No"
...@@ -586,15 +591,6 @@ msgstr "大包" ...@@ -586,15 +591,6 @@ msgstr "大包"
msgid "Big Package No" msgid "Big Package No"
msgstr "大包号" 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 #. 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_big_package__big_package_no
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_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 "公司" ...@@ -959,6 +955,11 @@ msgstr "公司"
msgid "Company Code" msgid "Company Code"
msgstr "公司编码" msgstr "公司编码"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view
msgid "Complete"
msgstr "完成"
#. module: ccs_base #. module: ccs_base
#: model:ir.model,name:ccs_base.model_res_config_settings #: model:ir.model,name:ccs_base.model_res_config_settings
msgid "Config Settings" msgid "Config Settings"
...@@ -2385,6 +2386,13 @@ msgstr "" ...@@ -2385,6 +2386,13 @@ msgstr ""
msgid "ONLINE SELLING PLACE" msgid "ONLINE SELLING PLACE"
msgstr "网上销售网站" 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 #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0 #: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
...@@ -2392,6 +2400,13 @@ msgstr "网上销售网站" ...@@ -2392,6 +2400,13 @@ msgstr "网上销售网站"
msgid "Only excel files can be uploaded!" msgid "Only excel files can be uploaded!"
msgstr "只能上传excel文件!" 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 #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__operate_remark #: 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 #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_package_sync_log_view
...@@ -2692,6 +2707,11 @@ msgstr "收件人邮政编码" ...@@ -2692,6 +2707,11 @@ msgstr "收件人邮政编码"
msgid "Real Weight" msgid "Real Weight"
msgstr "实际重量" msgstr "实际重量"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view
msgid "Recall"
msgstr "追回"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.tree_cc_bl_view #: model_terms:ir.ui.view,arch_db:ccs_base.tree_cc_bl_view
msgid "Receive Big Package" msgid "Receive Big Package"
...@@ -3390,13 +3410,6 @@ msgstr "使用日期不能大于当前日期!" ...@@ -3390,13 +3410,6 @@ msgstr "使用日期不能大于当前日期!"
msgid "This B/L No does not exist in the system" msgid "This B/L No does not exist in the system"
msgstr "提单号在系统不存在" 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 #. 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_big_package_view
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_history_big_package_view #: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_history_big_package_view
...@@ -3518,6 +3531,7 @@ msgstr "转单号必填" ...@@ -3518,6 +3531,7 @@ msgstr "转单号必填"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: 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 #, python-format
msgid "Transfer B/L No. cannot be the same as B/L No." msgid "Transfer B/L No. cannot be the same as B/L No."
msgstr "转单号不能与提单号相同。" msgstr "转单号不能与提单号相同。"
...@@ -3525,6 +3539,7 @@ msgstr "转单号不能与提单号相同。" ...@@ -3525,6 +3539,7 @@ msgstr "转单号不能与提单号相同。"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: 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 #, python-format
msgid "Transfer B/L No. cannot be the same as B/L No. or Transfer B/L No." msgid "Transfer B/L No. cannot be the same as B/L No. or Transfer B/L No."
msgstr "转单号不能与提单号或转单号相同。" msgstr "转单号不能与提单号或转单号相同。"
......
...@@ -810,7 +810,14 @@ class CcBL(models.Model): ...@@ -810,7 +810,14 @@ class CcBL(models.Model):
if item.state == 'draft': if item.state == 'draft':
item.state = 'ccing' 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): ...@@ -818,6 +825,13 @@ class CcBL(models.Model):
if item.state == 'ccing': if item.state == 'ccing':
item.state = 'done' 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 # 定义3个方法,分别创建显示该提单大包,包裹,商品的action
# 创建显示大包的action # 创建显示大包的action
def action_show_big_package(self): def action_show_big_package(self):
......
...@@ -59,7 +59,19 @@ ...@@ -59,7 +59,19 @@
string="Update Bill Of Loading Status" string="Update Bill Of Loading Status"
context="{'active_id': id,'default_bl_id': active_id, context="{'active_id': id,'default_bl_id': active_id,
'default_last_process_time':process_time,'default_current_status':customs_clearance_status}"/> '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>
<header> <header>
<field name="customs_clearance_status" widget="statusbar"/> <field name="customs_clearance_status" widget="statusbar"/>
...@@ -432,4 +444,18 @@ ...@@ -432,4 +444,18 @@
</field> </field>
</record> </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> </odoo>
\ No newline at end of file
...@@ -2,8 +2,9 @@ ...@@ -2,8 +2,9 @@
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html). # License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging import logging
from odoo import models, api, fields, _ 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): ...@@ -82,6 +83,7 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
state_explain = fields.Text('State Explain', help='State Explain') state_explain = fields.Text('State Explain', help='State Explain')
node_exception_reason_id = fields.Many2one('cc.node.exception.reason', 'Exception Reason', node_exception_reason_id = fields.Many2one('cc.node.exception.reason', 'Exception Reason',
domain="[('code_id', '=', update_status)]") domain="[('code_id', '=', update_status)]")
# 批量更新小包状态 # 批量更新小包状态
def submit(self): def submit(self):
# 确认数据 # 确认数据
...@@ -89,7 +91,8 @@ class BatchInputShipPackageStatusWizard(models.TransientModel): ...@@ -89,7 +91,8 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
raise ValidationError('Please confirm that the above data is correct.') # 请确认以上数据正确 raise ValidationError('Please confirm that the above data is correct.') # 请确认以上数据正确
parcels = self.get_process_package() parcels = self.get_process_package()
logging.info(
'更新小包状态的小包:%s,当前状态:%s,更新状态:%s' % (parcels, self.current_status, self.update_status))
if not parcels: if not parcels:
raise ValidationError(_('No package to update found.')) # 没有找到要更新的小包 raise ValidationError(_('No package to update found.')) # 没有找到要更新的小包
# 1.若选择的更新节点为是当前节点【清关节点设置,是当前节点字段名称改为初始节点】,当更新节点为初始节点时,无需填写操作时间; # 1.若选择的更新节点为是当前节点【清关节点设置,是当前节点字段名称改为初始节点】,当更新节点为初始节点时,无需填写操作时间;
...@@ -106,7 +109,7 @@ class BatchInputShipPackageStatusWizard(models.TransientModel): ...@@ -106,7 +109,7 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
# 更新状态 # 更新状态
parcels.write( parcels.write(
{'state': self.update_status.id, 'node_exception_reason_id': self.node_exception_reason_id.id, {'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: # if parcels:
# where_sql = " where id={0}".format(parcels[0].id) if len( # where_sql = " where id={0}".format(parcels[0].id) if len(
# parcels) == 1 else " where id in {0}".format(tuple(parcels.ids)) # parcels) == 1 else " where id in {0}".format(tuple(parcels.ids))
...@@ -116,9 +119,9 @@ class BatchInputShipPackageStatusWizard(models.TransientModel): ...@@ -116,9 +119,9 @@ class BatchInputShipPackageStatusWizard(models.TransientModel):
# where_sql) # where_sql)
# update_sql = update_sql.replace("'False'", "null").replace("False", "null") # update_sql = update_sql.replace("'False'", "null").replace("False", "null")
# self._cr.execute(update_sql) # self._cr.execute(update_sql)
# parcels.write({'state': self.update_status.id}) # parcels.write({'state': self.update_status.id})
# for parcel in parcels: # for parcel in parcels:
# parcel.message_post(body='%s改为%s' % (self.current_status.name, self.update_status.name)) # parcel.message_post(body='%s改为%s' % (self.current_status.name, self.update_status.name))
# 生成sns日志 # 生成sns日志
# self.bl_id.message_post(body='%s更新为%s' % (self.current_status.name or '', self.update_status.name or '')) # self.bl_id.message_post(body='%s更新为%s' % (self.current_status.name or '', self.update_status.name or ''))
# 跳转显示本次更新状态的小包 更新小包状态 # 跳转显示本次更新状态的小包 更新小包状态
......
# -*- coding: utf-8 -*- # -*- coding: utf-8 -*-
import logging import logging
import re
from datetime import timedelta from datetime import timedelta
from odoo import models, fields, api from odoo import models, fields, api
...@@ -60,7 +59,7 @@ class BlPatrol(models.Model): ...@@ -60,7 +59,7 @@ class BlPatrol(models.Model):
# 如果有问题,发送邮件 # 如果有问题,发送邮件
if result['issue_count'] > 0: 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']}") _logger.info(f"提单巡查完成: {patrol.name}, 发现问题: {result['issue_count']}")
...@@ -87,344 +86,7 @@ class BlPatrol(models.Model): ...@@ -87,344 +86,7 @@ class BlPatrol(models.Model):
bls = self.env['cc.bl'].sudo().search( bls = self.env['cc.bl'].sudo().search(
[('bl_date', '>=', start_date), ('bl_date', '<=', end_date), ('state', 'in', ['ccing', 'done'])]) [('bl_date', '>=', start_date), ('bl_date', '<=', end_date), ('state', 'in', ['ccing', 'done'])])
_logger.info(f"开始巡查提单,检查范围: {start_date} 到 {end_date}, 提单数量: {len(bls)}") _logger.info(f"开始巡查提单,检查范围: {start_date} 到 {end_date}, 提单数量: {len(bls)}")
error_package_issues = [] return bls.check_bl_patrol()
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)}")
def action_manual_patrol(self): def action_manual_patrol(self):
""" """
......
...@@ -4,12 +4,12 @@ import json ...@@ -4,12 +4,12 @@ import json
import logging import logging
import ssl import ssl
from datetime import timedelta, datetime from datetime import timedelta, datetime
import re
import aiohttp import aiohttp
import certifi import certifi
import pytz import pytz
from odoo import models, fields, api, _ from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
def get_rfc339_time(utc_time=None): def get_rfc339_time(utc_time=None):
if not utc_time: if not utc_time:
...@@ -362,7 +362,7 @@ class CcBl(models.Model): ...@@ -362,7 +362,7 @@ class CcBl(models.Model):
# 如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成 # 如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
if all(line.state.is_done for line in 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.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): def change_customs_state_by_ship_package(self, package_state_obj, user_obj=False):
""" """
...@@ -444,6 +444,382 @@ class CcBl(models.Model): ...@@ -444,6 +444,382 @@ class CcBl(models.Model):
result = self.env.cr.fetchone() result = self.env.cr.fetchone()
return result and result[0] or False 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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论