提交 c6b93053 authored 作者: 贺阳's avatar 贺阳

增加提单巡查

上级 afef8d8f
...@@ -32,6 +32,7 @@ ...@@ -32,6 +32,7 @@
'views/cc_ship_package_view.xml', 'views/cc_ship_package_view.xml',
'views/cc_bl_view.xml', 'views/cc_bl_view.xml',
'views/pda_scan_record_views.xml', 'views/pda_scan_record_views.xml',
'views/bl_patrol_views.xml',
], ],
'demo': [ 'demo': [
......
...@@ -12,5 +12,17 @@ ...@@ -12,5 +12,17 @@
<field name="active" eval="False"/> <field name="active" eval="False"/>
</record> </record>
<record id="cron_bl_patrol" model="ir.cron">
<field name="name">提单巡查</field>
<field name="model_id" ref="ccs_connect_tiktok.model_bl_patrol"/>
<field name="state">code</field>
<field name="code">model.cron_bl_patrol()</field>
<field name='interval_number'>1</field>
<field name='interval_type'>days</field>
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
<field name="nextcall" eval="(DateTime.now() + timedelta(hours=8)).strftime('%Y-%m-%d %H:%M:%S')"/>
</record>
</data> </data>
</odoo> </odoo>
\ No newline at end of file
...@@ -9,6 +9,7 @@ from . import cc_bill_loading ...@@ -9,6 +9,7 @@ from . import cc_bill_loading
from . import ir_attachment from . import ir_attachment
from . import http from . import http
from . import pda_scan_record from . import pda_scan_record
from . import bl_patrol
# -*- coding: utf-8 -*-
import logging
from datetime import datetime, timedelta
from odoo import models, fields, api, _
from odoo.exceptions import UserError
_logger = logging.getLogger(__name__)
class BlPatrol(models.Model):
_name = 'bl.patrol'
_description = '提单巡查'
_order = 'create_date desc'
name = fields.Char('巡查名称', required=True)
check_date = fields.Date('巡查日期', required=True, default=fields.Date.today)
bl_count = fields.Integer('检查提单数量', default=0)
issue_count = fields.Integer('发现问题数量', default=0)
state = fields.Selection([
('draft', '草稿'),
('running', '执行中'),
('done', '完成'),
('failed', '失败')
], string='状态', default='draft', required=True)
# 问题详情
package_issues = fields.Text('小包轨迹问题')
bl_issues = fields.Text('提单关务节点问题')
# 邮件发送记录
email_sent = fields.Boolean('邮件已发送', default=False)
email_sent_time = fields.Datetime('邮件发送时间')
@api.model
def cron_bl_patrol(self):
"""
定时巡查提单
"""
try:
# 创建巡查记录
patrol = self.create({'name': f'提单巡查_{fields.Date.today()}','check_date': fields.Date.today(),'state': 'running'})
# 执行巡查
result = patrol._execute_patrol()
# 更新巡查记录
patrol.write({
'state': 'done' if result['success'] else 'failed',
'bl_count': result['bl_count'],
'issue_count': result['issue_count'],
'package_issues': result['package_issues'],
'bl_issues': result['bl_issues']
})
# 如果有问题,发送邮件
if result['issue_count'] > 0:
patrol._send_patrol_email(result)
_logger.info(f"提单巡查完成: {patrol.name}, 发现问题: {result['issue_count']}")
except Exception as e:
_logger.error(f"提单巡查失败: {str(e)}")
raise UserError(f"巡查失败: {str(e)}")
def _execute_patrol(self):
"""
执行巡查逻辑
定时巡查,每天1次,默认巡查时间为北京时间8点开始;
检查对象:提单日期为近x天(默认5天),清关中和完成的提单。
检查内容:符合要求的提单,提单关务节点同步日志是否出现倒叙
(根据配置的清关节点排序,若后一个节点的操作时间,小于前序节点时间,则属于倒叙),
是否有漏推(若节点已产生同步日期,但前序节点无同步日期,则属于漏推。是当前节点的节点无需纳入判断);
"""
# 获取配置
config = self.env['ir.config_parameter'].sudo()
check_days = int(config.get_param('patrol_check_days', default=5))#巡查天数
# 计算检查日期范围
end_date = fields.Date.today()
start_date = end_date - timedelta(days=check_days)
# 查找符合条件的提单 提单日期为近x天(默认5天),清关中和完成的提单。
bls = self.env['cc.bl'].sudo().search([('create_date', '>=', start_date),('create_date', '<=', end_date),('state', 'in', ['ccing', 'completed'])])
_logger.info(f"开始巡查提单,检查范围: {start_date} 到 {end_date}, 提单数量: {len(bls)}")
package_issues = []
bl_issues = []
for bl in bls:
# 检查小包轨迹问题
bl_package_issues = self._check_package_tracking_issues(bl)
if bl_package_issues:
package_issues.extend(bl_package_issues)
# 检查提单关务节点问题
bl_node_issues = self._check_bl_node_issues(bl)
if bl_node_issues:
bl_issues.extend(bl_node_issues)
return {
'success': True,
'bl_count': len(bls),
'issue_count': len(package_issues) + len(bl_issues),
'package_issues': '\n'.join(package_issues) if package_issues else '',
'bl_issues': '\n'.join(bl_issues) if 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:
sync_logs = package.sync_log_ids.sorted('operate_time')
if len(sync_logs) < 2:
continue
# 检查倒叙问题
for i in range(1, len(sync_logs)):
current_log = sync_logs[i]
previous_log = sync_logs[i-1]
if current_log.operate_time < previous_log.operate_time:
issue_key = f"{current_log.progress_name}({current_log.process_code})倒叙"
if issue_key not in reverse_issues:
reverse_issues[issue_key] = []
reverse_issues[issue_key].append(package.logistic_order_no)
break
# 检查漏推问题 - 根据节点配置检查
package_issues = self._check_package_missing_nodes(package, sync_logs)
for issue in package_issues:
# 提取问题类型
if "轨迹漏推" in issue:
issue_key = issue.split(",出现")[1].split(",涉及")[0]
if issue_key not in missing_issues:
missing_issues[issue_key] = []
missing_issues[issue_key].append(package.logistic_order_no)
# 格式化倒叙问题
for issue_type, packages in reverse_issues.items():
package_list = self._format_package_list(packages)
issues.append(f"1.{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
# 格式化漏推问题
for issue_type, packages in missing_issues.items():
package_list = self._format_package_list(packages)
issues.append(f"1.{bl.bl_no},出现{issue_type},涉及小包{len(packages)},小包追踪号包括{package_list}")
return issues
def _format_package_list(self, packages):
"""
格式化小包列表,最多显示10个
"""
if len(packages) <= 10:
return "/".join(packages)
else:
return "/".join(packages[:10]) + "等"
def _check_package_missing_nodes(self, package, sync_logs):
"""
检查小包漏推节点
"""
issues = []
# 获取所有小包节点,按顺序排序
package_nodes = self.env['cc.node'].sudo().search([
('node_type', '=', 'package')
], order='seq')
if not package_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in package_nodes:
# 跳过当前节点(最后一个节点)
if node == package_nodes[-1]:
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:
issues.append(
f"{node.name}({node.tk_code})轨迹漏推"
)
break
return issues
def _check_bl_node_issues(self, bl):
"""
检查提单关务节点问题
"""
issues = []
# 获取提单同步日志
sync_logs = bl.bl_sync_log_ids.sorted('operate_time')
if len(sync_logs) < 2:
return issues
# 检查倒叙问题
for i in range(1, len(sync_logs)):
current_log = sync_logs[i]
previous_log = sync_logs[i-1]
if current_log.operate_time < previous_log.operate_time:
issues.append(
f"1.{bl.bl_no},出现{current_log.progress_name}({current_log.process_code})倒叙"
)
break
# 检查漏推问题
bl_issues = self._check_bl_missing_nodes(bl, sync_logs)
if bl_issues:
issues.extend(bl_issues)
return issues
def _check_bl_missing_nodes(self, bl, sync_logs):
"""
检查提单漏推节点
"""
issues = []
# 获取所有提单节点,按顺序排序
bl_nodes = self.env['cc.node'].sudo().search([
('node_type', '=', 'bl')
], order='seq')
if not bl_nodes:
return issues
# 检查每个节点是否有对应的同步日志
for node in bl_nodes:
# 跳过当前节点(最后一个节点)
if node == bl_nodes[-1]:
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:
issues.append(
f"1.{bl.bl_no},出现{node.name}({node.tk_code})轨迹漏推"
)
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('\n') 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"""您好,经系统巡查。
发现以下提单存在小包轨迹倒叙/轨迹漏推
{package_content}
发现以下提单存在提单关务节点轨迹倒叙/轨迹漏推
{bl_content}
请立即处理!!!"""
# 发送邮件
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):
"""
手动执行巡查
"""
self.ensure_one()
if self.state != 'draft':
raise UserError('只能对草稿状态的巡查进行手动执行')
self.cron_bl_patrol()
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '巡查完成',
'message': f'巡查已完成,发现问题: {self.issue_count}',
'type': 'success',
}
}
\ No newline at end of file
...@@ -17,6 +17,12 @@ class ResConfigSettings(models.TransientModel): ...@@ -17,6 +17,12 @@ class ResConfigSettings(models.TransientModel):
tt_customer_id = fields.Many2one('res.partner', string='客户') tt_customer_id = fields.Many2one('res.partner', string='客户')
#交货操作晚于提货操作X分钟【默认80分钟】 #交货操作晚于提货操作X分钟【默认80分钟】
delivery_time = fields.Integer('交货操作晚于提货操作X分钟', default=80,config_parameter='delivery_time') delivery_time = fields.Integer('交货操作晚于提货操作X分钟', default=80,config_parameter='delivery_time')
# 巡查配置
patrol_receiver_emails = fields.Char('接收邮件地址', help='多个邮箱地址,用逗号分隔', config_parameter='patrol_receiver_emails')
patrol_sender_email = fields.Char('发送邮箱地址', config_parameter='patrol_sender_email')
patrol_check_days = fields.Integer('巡查天数', default=5, help='检查近几天的提单', config_parameter='patrol_check_days')
patrol_start_hour = fields.Integer('巡查开始时间(小时)', default=8, help='北京时间,24小时制', config_parameter='patrol_start_hour')
@api.model @api.model
def get_values(self): def get_values(self):
...@@ -32,6 +38,10 @@ class ResConfigSettings(models.TransientModel): ...@@ -32,6 +38,10 @@ class ResConfigSettings(models.TransientModel):
tt_version = config.get_param('tt_version', default='') tt_version = config.get_param('tt_version', default='')
tt_customer_id = config.get_param('tt_customer_id', default=False) tt_customer_id = config.get_param('tt_customer_id', default=False)
delivery_time = config.get_param('delivery_time', default=80) delivery_time = config.get_param('delivery_time', default=80)
patrol_receiver_emails = config.get_param('patrol_receiver_emails', default='')
patrol_sender_email = config.get_param('patrol_sender_email', default='')
patrol_check_days = config.get_param('patrol_check_days', default=5)
patrol_start_hour = config.get_param('patrol_start_hour', default=8)
customer = self.env['res.partner'].sudo().search([('id', '=', tt_customer_id)]) customer = self.env['res.partner'].sudo().search([('id', '=', tt_customer_id)])
values.update( values.update(
tt_url=tt_url, tt_url=tt_url,
...@@ -39,7 +49,11 @@ class ResConfigSettings(models.TransientModel): ...@@ -39,7 +49,11 @@ class ResConfigSettings(models.TransientModel):
tt_app_secret=tt_app_secret, tt_app_secret=tt_app_secret,
tt_version=tt_version, tt_version=tt_version,
tt_customer_id=customer, tt_customer_id=customer,
delivery_time=delivery_time delivery_time=delivery_time,
patrol_receiver_emails=patrol_receiver_emails,
patrol_sender_email=patrol_sender_email,
patrol_check_days=patrol_check_days,
patrol_start_hour=patrol_start_hour
) )
return values return values
...@@ -51,4 +65,8 @@ class ResConfigSettings(models.TransientModel): ...@@ -51,4 +65,8 @@ class ResConfigSettings(models.TransientModel):
ir_config.set_param("tt_app_secret", self.tt_app_secret or "") ir_config.set_param("tt_app_secret", self.tt_app_secret or "")
ir_config.set_param("tt_version", self.tt_version or "") ir_config.set_param("tt_version", self.tt_version or "")
ir_config.set_param("tt_customer_id", self.tt_customer_id.id or False) ir_config.set_param("tt_customer_id", self.tt_customer_id.id or False)
ir_config.set_param("delivery_time", self.delivery_time or 80) ir_config.set_param("delivery_time", self.delivery_time or 80)
\ No newline at end of file ir_config.set_param("patrol_receiver_emails", self.patrol_receiver_emails or "")
ir_config.set_param("patrol_sender_email", self.patrol_sender_email or "")
ir_config.set_param("patrol_check_days", self.patrol_check_days or 5)
ir_config.set_param("patrol_start_hour", self.patrol_start_hour or 8)
\ No newline at end of file
...@@ -15,3 +15,6 @@ access_cc_bl_sync_log_ccs_base.group_clearance_of_customs_user,cc_bl_sync_log cc ...@@ -15,3 +15,6 @@ access_cc_bl_sync_log_ccs_base.group_clearance_of_customs_user,cc_bl_sync_log cc
access_pda_scan_record_user,pda.scan.record.user,model_pda_scan_record,base.group_user,1,1,1,0 access_pda_scan_record_user,pda.scan.record.user,model_pda_scan_record,base.group_user,1,1,1,0
access_pda_scan_record_manager,pda.scan.record.manager,model_pda_scan_record,base.group_system,1,1,1,1 access_pda_scan_record_manager,pda.scan.record.manager,model_pda_scan_record,base.group_system,1,1,1,1
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
# 根据空运提单和大包以及小包和产品的关系,生成一组测试数据,每个提单包括2个大包, 每个大包包括2个小包, 每个小包包括2个产品
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- 巡查列表视图 -->
<record id="view_bl_patrol_tree" model="ir.ui.view">
<field name="name">bl.patrol.tree</field>
<field name="model">bl.patrol</field>
<field name="arch" type="xml">
<tree string="提单巡查">
<field name="name"/>
<field name="check_date"/>
<field name="bl_count"/>
<field name="issue_count"/>
<field name="state"/>
<field name="email_sent"/>
<field name="email_sent_time"/>
<field name="create_date"/>
</tree>
</field>
</record>
<!-- 巡查表单视图 -->
<record id="view_bl_patrol_form" model="ir.ui.view">
<field name="name">bl.patrol.form</field>
<field name="model">bl.patrol</field>
<field name="arch" type="xml">
<form string="提单巡查">
<header>
<button name="action_manual_patrol"
type="object"
string="手动执行巡查"
class="oe_highlight"
attrs="{'invisible': [('state', '!=', 'draft')]}"/>
<field name="state" widget="statusbar"/>
</header>
<sheet>
<group>
<group>
<field name="name"/>
<field name="check_date"/>
<field name="bl_count"/>
<field name="issue_count"/>
</group>
<group>
<field name="email_sent"/>
<field name="email_sent_time"/>
<field name="create_date"/>
</group>
</group>
<notebook>
<page string="小包轨迹问题" attrs="{'invisible': [('package_issues', '=', False)]}">
<field name="package_issues" readonly="1"/>
</page>
<page string="提单关务节点问题" attrs="{'invisible': [('bl_issues', '=', False)]}">
<field name="bl_issues" readonly="1"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<!-- 巡查搜索视图 -->
<record id="view_bl_patrol_search" model="ir.ui.view">
<field name="name">bl.patrol.search</field>
<field name="model">bl.patrol</field>
<field name="arch" type="xml">
<search string="提单巡查">
<field name="name"/>
<field name="check_date"/>
<filter string="草稿" name="draft" domain="[('state', '=', 'draft')]"/>
<filter string="执行中" name="running" domain="[('state', '=', 'running')]"/>
<filter string="完成" name="done" domain="[('state', '=', 'done')]"/>
<filter string="失败" name="failed" domain="[('state', '=', 'failed')]"/>
<filter string="有问题" name="has_issues" domain="[('issue_count', '>', 0)]"/>
<group expand="0" string="分组">
<filter string="状态" name="group_state" context="{'group_by': 'state'}"/>
<filter string="巡查日期" name="group_check_date" context="{'group_by': 'check_date'}"/>
</group>
</search>
</field>
</record>
<!-- 巡查动作 -->
<record id="action_bl_patrol" model="ir.actions.act_window">
<field name="name">提单巡查</field>
<field name="res_model">bl.patrol</field>
<field name="view_mode">tree,form</field>
<field name="context">{'search_default_done': 1}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
创建第一个巡查记录
</p>
<p>
系统将自动巡查提单的轨迹问题,包括小包轨迹倒叙和提单关务节点倒叙。
</p>
</field>
</record>
<!-- 菜单项 -->
<menuitem id="menu_bl_patrol"
name="提单巡查"
action="action_bl_patrol"
sequence="22"/>
</data>
</odoo>
\ No newline at end of file
...@@ -31,6 +31,27 @@ ...@@ -31,6 +31,27 @@
</div> </div>
</div> </div>
</div> </div>
<div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/>
<div class="o_setting_right_pane">
<div class="text-muted">
<label for="patrol_receiver_emails"/>
<field name="patrol_receiver_emails"/>
</div>
<div class="text-muted">
<label for="patrol_sender_email"/>
<field name="patrol_sender_email"/>
</div>
<div class="text-muted">
<label for="patrol_check_days"/>
<field name="patrol_check_days"/>
</div>
<div class="text-muted">
<label for="patrol_start_hour"/>
<field name="patrol_start_hour"/>
</div>
</div>
</div>
<div class="col-12 col-lg-6 o_setting_box"> <div class="col-12 col-lg-6 o_setting_box">
<div class="o_setting_left_pane"/> <div class="o_setting_left_pane"/>
<div class="o_setting_right_pane"> <div class="o_setting_right_pane">
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论