提交 2656a7c1 authored 作者: 贺阳's avatar 贺阳

提单状态

上级 44d2fe95
...@@ -670,6 +670,12 @@ class CcBL(models.Model): ...@@ -670,6 +670,12 @@ class CcBL(models.Model):
# 通关文件, 关联附件对象(类型限定为image和PDF) # 通关文件, 关联附件对象(类型限定为image和PDF)
cc_attachment_ids = fields.One2many('cc.clearance.file', 'bl_id', string='Clearance Files') cc_attachment_ids = fields.One2many('cc.clearance.file', 'bl_id', string='Clearance Files')
# 提单上新增字段:关务提单状态,用英文:关联节点的配置(cc.node),节点类型过滤提单的节点名称。
customs_clearance_status = fields.Many2one('cc.node', string='Customs Clearance Status',
domain=[('node_type', '=', 'bl')])
# 增加提单状态操作时间
process_time = fields.Datetime(string='Process Time')
# 增加一个can_cancel的方法,用于检查提单当前是否可以取消,返回True表示可以取消, False表示不可以取消,同时返回取消的原因 # 增加一个can_cancel的方法,用于检查提单当前是否可以取消,返回True表示可以取消, False表示不可以取消,同时返回取消的原因
def check_cancel(self): def check_cancel(self):
if self.is_cancel: if self.is_cancel:
......
...@@ -48,3 +48,8 @@ class CcNode(models.Model): ...@@ -48,3 +48,8 @@ class CcNode(models.Model):
('checked_goods', 'Checked goods'), ('checked_goods', 'Checked goods'),
('handover_completed', 'Handover Completed') ('handover_completed', 'Handover Completed')
], default='', string='Corresponding to the status of the big package', index=True) # 对应大包状态 未理货/已理货/尾程交接 ], default='', string='Corresponding to the status of the big package', index=True) # 对应大包状态 未理货/已理货/尾程交接
# 新增字段:对应小包状态。只有类型为提单上才可填写。可选已配置节点类型为小包的节点。单选;
package_state = fields.Many2one('cc.node', string='Corresponding to the status of the package',domain="[('node_type','=','package')]", index=True) # 对应小包状态
...@@ -50,8 +50,12 @@ ...@@ -50,8 +50,12 @@
<!-- # 为action_batch_input_ship_package_wizard添加一个按钮, 上下文中添加bl_id--> <!-- # 为action_batch_input_ship_package_wizard添加一个按钮, 上下文中添加bl_id-->
<button name="%(action_batch_input_ship_package_wizard)d" type="action" class="oe_highlight" <button name="%(action_batch_input_ship_package_wizard)d" type="action" class="oe_highlight"
string="Update Ship Package Status" string="Update Ship Package Status"
context="{'default_bl_id': active_id, 'active_id': id}"/> context="{'default_bl_id': active_id, 'active_id': id,'default_action_type':'小包'}"/>
<button name="%(action_batch_input_ship_package_wizard)d" type="action" class="oe_highlight"
string="Update BL Status"
context="{'active_id': id,'default_action_type':'提单'}"/>
<field name="state" widget="statusbar" options="{'clickable': '1'}"/> <field name="state" widget="statusbar" options="{'clickable': '1'}"/>
</header> </header>
<sheet> <sheet>
......
...@@ -82,12 +82,14 @@ class BatchInputShipPackageStatusWizard(models.TransientModel): ...@@ -82,12 +82,14 @@ 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)]")
action_type = fields.Char(string='Action Type', default='小包')
# 批量更新小包状态 # 批量更新小包状态
def submit(self): def submit(self):
# 确认数据 # 确认数据
if not self.is_ok: if not self.is_ok:
raise ValidationError('Please confirm that the above data is correct.') # 请确认以上数据正确 raise ValidationError('Please confirm that the above data is correct.') # 请确认以上数据正确
if self.action_type == '小包':
parcels = self.get_process_package() parcels = self.get_process_package()
if not parcels: if not parcels:
......
...@@ -24,6 +24,7 @@ ...@@ -24,6 +24,7 @@
'views/config_settings_views.xml', 'views/config_settings_views.xml',
# 'views/flight_order_view.xml', # 'views/flight_order_view.xml',
'views/cc_ship_package_sync_log_view.xml', 'views/cc_ship_package_sync_log_view.xml',
'views/cc_bl_sync_log_view.xml',
'views/ao_tt_api_log_view.xml', 'views/ao_tt_api_log_view.xml',
'views/cc_node_view.xml', 'views/cc_node_view.xml',
'views/cc_ship_package_view.xml', 'views/cc_ship_package_view.xml',
......
# -*- coding: utf-8 -*-
import asyncio import asyncio
import logging import logging
import ssl import ssl
...@@ -251,8 +252,103 @@ class CcBl(models.Model): ...@@ -251,8 +252,103 @@ class CcBl(models.Model):
else: else:
record.unsync_package_count = 0 record.unsync_package_count = 0
# 计算提单状态操作时间
@api.depends('bl_sync_log_ids')
def _compute_process_time(self):
for bl in self:
if bl.bl_sync_log_ids:
bl.process_time = bl.bl_sync_log_ids.mapped('operate_time').sorted(reverse=True)[0]
else:
bl.process_time = False
# 增加未同步小包数量字段 # 增加未同步小包数量字段
unsync_package_count = fields.Integer('Unsync Package Count', compute='_compute_unsync_package_count', store=True) unsync_package_count = fields.Integer('Unsync Package Count', compute='_compute_unsync_package_count', store=True)
is_bl_sync = fields.Boolean('Is BL Synchronized', default=False)
# 关联提单同步日志
bl_sync_log_ids = fields.One2many('cc.bl.sync.log', 'bl_id', string='BL Sync Logs')
# 增加提单状态操作时间:取最新一条提单节点同步信息的操作时间
process_time = fields.Datetime(string='Process Time', compute='_compute_process_time', store=True)
# =============同步提单状态==================================
# 增加同步提单状态的方法
def action_sync_bl_status(self):
self.callback_track_bl()
# 定义一个方法, 获取提单,并回传提单状态
def callback_track_bl(self):
is_ok = True
for item in self:
is_ok = item.bl_callback_func(item.ids)
return is_ok
def bl_callback_func(self, bl_ids):
"""
同步提单状态
"""
bls = self.env['cc.bl'].search([('id', 'in', bl_ids), ('is_bl_sync', '=', False)])
logging.info('bl_callback_func bls:%s' % len(bls))
is_ok = True
tt_api_obj = self.env["ao.tt.api"].sudo()
async def perform_requests():
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context),
timeout=aiohttp.ClientTimeout(total=60)) as session:
tasks = []
for index, bl in enumerate(bls):
if not bl.is_bl_sync and bl.state and bl.state.tk_code:
data = bl.get_bl_callback_track_data()
tasks.append(tt_api_obj.async_bl_callback_track(session, data, bl.id))
responses = await asyncio.gather(*tasks)
return responses
# 在 Odoo 中运行异步任务
responses = asyncio.run(perform_requests())
for response_item in responses:
response_data = response_item[0]
logging.info('bl response_data response:%s' % response_data)
data = response_item[1]
bl_id = response_item[2]
bl_order = self.env['cc.bl'].sudo().browse(bl_id)
if response_data['code'] != 0:
bl_order.is_bl_sync = False
self._cr.commit() # 提交事务
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(
bl_order.bl_no or '', '提单状态回传:' + error_msg, data, code, request_id,
source='推出')
is_ok = False
else:
# 回传成功
bl_order.is_bl_sync = True
self._cr.commit() # 提交事务
self.env['cc.bl.sync.log'].sudo().create_sync_log(
bl_order.id, 'Tiktok', bl_order.state.tk_code, bl_order.state.name, bl_order.state_explain,
bl_order.process_time.strftime('%Y-%m-%d %H:%M:%S'))
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(
bl_order.bl_no or '', '', data, 0, request_id, source='推出')
return is_ok
def get_bl_callback_track_data(self):
"""
获取提单回传数据
"""
return {
"master_waybill_no": self.bl_no or '', # 主提运单号
"customs_waybill_no": self.customs_bl_no or '', # 关务提单号取海关装货单号
"track_detail": [
{
"operate_time": get_utc_time(self.process_time), # 事件发⽣时间,RFC3339格式(实操时间)
"waybill_status_code": self.state.tk_code, # 提单关务状态编码
}
]
}
# =============同步小包状态==================================
# 定义一个方法, 获取提单下的所有未同步的小包,并回传小包状态 # 定义一个方法, 获取提单下的所有未同步的小包,并回传小包状态
def callback_track(self): def callback_track(self):
...@@ -399,7 +495,7 @@ class CcBl(models.Model): ...@@ -399,7 +495,7 @@ class CcBl(models.Model):
def mail_auto_push(self, mail_time=False, ship_packages=[], action_type='tally'): def mail_auto_push(self, mail_time=False, ship_packages=[], action_type='tally'):
self = self.with_context(dict(self._context, is_mail=True)) self = self.with_context(dict(self._context, is_mail=True))
for item in self: for item in self:
# try: try:
if mail_time: if mail_time:
utc_time = datetime.strptime(mail_time, "%Y-%m-%d %H:%M:%S") utc_time = datetime.strptime(mail_time, "%Y-%m-%d %H:%M:%S")
before_min = self.env['ir.config_parameter'].sudo().get_param('before_min') or 10 before_min = self.env['ir.config_parameter'].sudo().get_param('before_min') or 10
...@@ -461,7 +557,8 @@ class CcBl(models.Model): ...@@ -461,7 +557,8 @@ class CcBl(models.Model):
package_id, set()): package_id, set()):
tally_time = ship_packages_dict.get(package_id) tally_time = ship_packages_dict.get(package_id)
if tally_time: if tally_time:
operation_time = (datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') - timedelta( operation_time = (
datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') - timedelta(
minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta( minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta(
minutes=before_minutes) minutes=before_minutes)
update_data.append(( update_data.append((
...@@ -532,9 +629,7 @@ class CcBl(models.Model): ...@@ -532,9 +629,7 @@ class CcBl(models.Model):
node.desc, node.desc,
True if node.is_default else False True if node.is_default else False
)) ))
print('update_data:%s' % update_data)
if update_data: if update_data:
print('11111111111')
# 构建批量更新SQL # 构建批量更新SQL
values_str = ','.join( values_str = ','.join(
self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in update_data) self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in update_data)
...@@ -556,13 +651,11 @@ class CcBl(models.Model): ...@@ -556,13 +651,11 @@ class CcBl(models.Model):
# sql = """UPDATE cc_bl AS bl SET unsync_package_count = ( SELECT COUNT(*) FROM cc_ship_package sp WHERE sp.bl_id = bl.id AND sp.is_sync = false) WHERE bl.id = %s """ # sql = """UPDATE cc_bl AS bl SET unsync_package_count = ( SELECT COUNT(*) FROM cc_ship_package sp WHERE sp.bl_id = bl.id AND sp.is_sync = false) WHERE bl.id = %s """
# self.env.cr.execute(sql, (item.id,)) # self.env.cr.execute(sql, (item.id,))
# self._cr.commit() # 提交事务 # self._cr.commit() # 提交事务
print('222222')
self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids) self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids)
print('33333')
return True return True
# except Exception as err: except Exception as err:
# logging.error('fetch_mail_dlv--error:%s' % str(err)) logging.error('fetch_mail_dlv--error:%s' % str(err))
def change_state_by_ship_package(self): def change_state_by_ship_package(self):
""" """
...@@ -578,6 +671,46 @@ class CcBl(models.Model): ...@@ -578,6 +671,46 @@ class CcBl(models.Model):
self.done_func() self.done_func()
# 提单节点同步日志
class CcBlSyncLog(models.Model):
_name = 'cc.bl.sync.log'
_description = 'CC Bl Sync Log'
# 定义模型字段
# 提单对象
bl_id = fields.Many2one('cc.bl', 'Bill of Loading', required=True)
# 同步时间
sync_time = fields.Datetime('Sync Time', default=fields.Datetime.now)
# 增加接口客户
api_customer = fields.Char('Api Customer')
# 操作状态
process_code = fields.Char('TK Process Code')
# 加一个进度名称
progress_name = fields.Char('Progress Name')
# 操作时间
operate_time = fields.Datetime('Operate Time', default=fields.Datetime.now)
# 操作备注
operate_remark = fields.Text('Operate Remark')
# 同步操作人
operate_user = fields.Many2one('res.users', 'Operate User', default=lambda self: self.env.user)
# 添加一个新增日志的方法,传入小包ID,API客户,操作状态,操作备注,操作时间
@api.model
def create_sync_log(self, package_id, api_customer, process_code, progress_name, operate_remark, operate_time):
vals = {
'package_id': package_id,
'api_customer': api_customer,
'process_code': process_code,
'progress_name': progress_name,
'operate_remark': operate_remark,
'operate_time': operate_time
}
if self._context.get('is_mail'):
public_user = self.env.ref('base.public_user')
vals['operate_user'] = public_user.id
return self.create(vals)
class CcBigPackage(models.Model): class CcBigPackage(models.Model):
# 模型名称 # 模型名称
_inherit = 'cc.big.package' _inherit = 'cc.big.package'
......
import asyncio
import logging
import ssl
from datetime import timedelta, datetime
import aiohttp
import certifi
import pytz
from odoo import models, fields, api, _
def get_rfc339_time(utc_time=None):
if not utc_time:
# 获取当前时间的UTC时间
utc_time = datetime.utcnow()
# 创建+8时区的对象
target_timezone = pytz.timezone('Asia/Shanghai')
# 将UTC时间转换为目标时区时间
local_time = utc_time.replace(tzinfo=pytz.utc).astimezone(target_timezone)
# 格式化为RFC 3339格式
rfc3339_time = local_time.isoformat(timespec='seconds')
return rfc3339_time
# 定义一个方法,将时间的时区转为UTC时间
def get_utc_time(local_time=None):
if not local_time:
# 获取当前时间的UTC时间
local_time = datetime.now()
# 将本地时间转换为UTC时间
utc_time = local_time.astimezone(pytz.utc)
# 格式化为RFC 3339格式
# rfc3339_time = utc_time.isoformat(timespec='seconds')
return utc_time.strftime('%Y-%m-%d %H:%M:%S')
# 继承cc.clearance.file对象,并重载action_upload方法
class CcClearanceFile(models.Model):
_inherit = "cc.clearance.file"
_description = "Clearance File" # 清关文件
def get_clearance_file_feedback_data(self):
"""通关文件上传数据组织"""
happend_time = get_rfc339_time()
push_data = {
"master_waybill_no": self.bl_id.bl_no or '',
"customs_waybill_id": self.bl_id.customs_bl_no or '',
"operate_time": happend_time,
"file_detail": {
"file_url": "",
"file_code": self.file.decode(), # 将文件内容转换为base64编码,
"file_type": "PDF"
}
}
return push_data
def clearance_file_feedback(self):
if not self.is_upload and self.file:
data = self.get_clearance_file_feedback_data()
tt_api_obj = self.env["ao.tt.api"].sudo()
response = tt_api_obj.clearance_file_feedback(data)
response_data = response.json()
if response_data['code'] != 0:
# 清关文件回传错误
self.is_upload = False
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(self.file_name or '', '清关文件回传:' + error_msg, data,
code,
request_id, source='推出')
return error_msg
else:
# 清关文件回传成功
self.is_upload = True
self.upload_time = datetime.now()
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(self.file_name or '', '', data, 0, request_id,
source='推出')
return ''
# 重载action_upload方法
def action_sync(self):
self.clearance_file_feedback()
return super(CcClearanceFile, self).action_sync()
# 定义小包同步纪录对象,用于记录小包同步的纪录,包括小包对象,同步时间,操作状态,操作时间,操行备注,同步操作人
class CcShipPackageSyncLog(models.Model):
_name = 'cc.ship.package.sync.log'
_description = 'CC Ship Package Sync Log'
# 定义模型字段
# 小包对象
package_id = fields.Many2one('cc.ship.package', 'Ship Package', required=True)
# 同步时间
sync_time = fields.Datetime('Sync Time', default=fields.Datetime.now)
# 增加接口客户
api_customer = fields.Char('Api Customer')
# 操作状态
process_code = fields.Char('TK Process Code')
# 操作时间
operate_time = fields.Datetime('Operate Time', default=fields.Datetime.now)
# 操作备注
operate_remark = fields.Text('Operate Remark')
# 同步操作人
operate_user = fields.Many2one('res.users', 'Operate User', default=lambda self: self.env.user)
# 添加一个新增日志的方法,传入小包ID,API客户,操作状态,操作备注,操作时间
@api.model
def create_sync_log(self, package_id, api_customer, process_code, operate_remark, operate_time):
vals = {
'package_id': package_id,
'api_customer': api_customer,
'process_code': process_code,
'operate_remark': operate_remark,
'operate_time': operate_time
}
if self._context.get('is_mail'):
public_user = self.env.ref('base.public_user')
vals['operate_user'] = public_user.id
return self.create(vals)
# 继承小包对象,并重载action_sync方法, 增加is_sync字段
class CcShipPackage(models.Model):
_inherit = "cc.ship.package"
is_sync = fields.Boolean('Is Sync', default=False, index=True)
tk_code = fields.Char(related='state.tk_code', store=True, string='TK Code', help='TK Code')
# 增加同步日志纪录字段
sync_log_ids = fields.One2many('cc.ship.package.sync.log', 'package_id', 'Sync Logs')
def is_next_code(self, next_state_id):
"""
判断更新的节点是否是 小包状态的下级节点
:param next_state_id:
:return:
"""
if self.state:
if next_state_id in self.state.next_code_ids.ids:
return True
return False
@api.model
def create(self, vals_list):
"""
第一个节点的时候 默认已同步
"""
obj = super(CcShipPackage, self).create(vals_list)
if obj.state.is_default:
obj.is_sync = True
return obj
def action_sync(self):
for record in self:
record.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(record.id, 'Tiktok', record.state.tk_code,
record.state_explain,
record.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
def get_callback_track_data(self):
"""小包上传数据."""
# 获取该提单下的所有同步状态为未同步的小包
push_data = {
"provider_order_id": self.logistic_order_no,
"track_list": [
{
"shipping_method_code": self.state.tk_code,
"operate_time": get_utc_time(self.process_time),
"time_zone": "UTC+0",
"action_code": self.state.tk_code,
"operation_desc": self.state.desc,
"reason_code": self.node_exception_reason_id.name or "" # 异常原因
}
]
}
# logging.info('小包轨迹 push_data:%s' % push_data)
return push_data
def callback_track(self, is_push=True):
if not self.is_sync and self.state and self.state.tk_code:
data = self.get_callback_track_data()
if is_push:
tt_api_obj = self.env["ao.tt.api"].sudo()
response = tt_api_obj.callback_track(data)
response_data = response.json()
if response_data['code'] != 0:
self.is_sync = False
self._cr.commit()
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '',
'小包状态轨迹回传:' + error_msg,
data,
code,
request_id, source='推出')
return error_msg
else:
# 回传成功
self.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(self.id, 'Tiktok', self.state.tk_code,
self.state_explain,
self.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
self._cr.commit()
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '', '', data, 0, request_id,
source='推出')
return ''
else:
self.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(self.id, 'Tiktok', self.state.tk_code,
self.state_explain,
self.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
self._cr.commit()
request_id = ''
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '', '', data, 0, request_id,
source='推出')
return ''
def search_ship_package_info(self, pda_lang=False):
"""
查询小包信息
:return:
"""
return {
'logistic_order_no': self.logistic_order_no, # 物流订单号
}
# 继承提单对象t
class CcBl(models.Model):
_inherit = 'cc.bl'
# 计算未同步小包数量
@api.depends('ship_package_ids', 'ship_package_ids.is_sync')
def _compute_unsync_package_count(self):
for record in self:
record_counts = record.ship_package_ids.filtered(lambda r: not r.is_sync)
if record_counts:
record.unsync_package_count = len(record_counts)
else:
record.unsync_package_count = 0
# 增加未同步小包数量字段
unsync_package_count = fields.Integer('Unsync Package Count', compute='_compute_unsync_package_count', store=True)
# 定义一个方法, 获取提单下的所有未同步的小包,并回传小包状态
def callback_track(self):
is_ok = True
for item in self:
ship_packages = self.env['cc.ship.package'].search([('bl_id', '=', item.id), ('is_sync', '=', False)])
is_ok = item.package_callback_func(ship_packages.ids)
return is_ok
def package_callback_func(self, ship_package_ids):
"""
同步小包状态
"""
ship_packages = self.env['cc.ship.package'].search([('id', 'in', ship_package_ids), ('is_sync', '=', False)])
logging.info('package_callback_func ship_packages:%s' % len(ship_packages))
is_ok = True
tt_api_obj = self.env["ao.tt.api"].sudo()
async def perform_requests():
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context),
timeout=aiohttp.ClientTimeout(total=60)) as session:
tasks = []
for index, package in enumerate(ship_packages):
if not package.is_sync and package.state and package.state.tk_code:
data = package.get_callback_track_data()
tasks.append(tt_api_obj.async_callback_track(session, data, package.id))
responses = await asyncio.gather(*tasks)
return responses
# 在 Odoo 中运行异步任务
responses = asyncio.run(perform_requests())
for response_item in responses:
response_data = response_item[0]
logging.info('response_data response:%s' % response_data)
data = response_item[1]
package_id = response_item[2]
package_order = self.env['cc.ship.package'].sudo().browse(package_id)
if response_data['code'] != 0:
package_order.is_sync = False
self._cr.commit() # 提交事务
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(
package_order.tracking_no or '', '小包状态轨迹回传:' + error_msg, data, code, request_id,
source='推出')
is_ok = False
else:
# 回传成功
package_order.is_sync = True
self._cr.commit() # 提交事务
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(
package_order.id, 'Tiktok', package_order.state.tk_code, package_order.state_explain,
package_order.process_time.strftime('%Y-%m-%d %H:%M:%S'))
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(
package_order.tracking_no or '', '', data, 0, request_id, source='推出')
# 如果提单有小包变成了清关开始,提单状态变为清关中;如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
self.change_state_by_ship_package()
return is_ok
def deal_ship_package_state(self):
for item in self:
ship_packages = self.env['cc.ship.package'].search([('bl_id', '=', item.id), ('is_sync', '=', False)])
for package in ship_packages:
package.callback_track(is_push=False)
return True
def batch_action_sync(self):
"""
批量回传通过文件
"""
for item in self:
for line in item.cc_attachment_ids:
line.action_sync()
# 创建显示包裹的action
def action_show_no_sync_ship_package(self):
# 返回一个action,显示包裹
return {
'name': _('Not Sync Ship Packages'),
'type': 'ir.actions.act_window',
'res_model': 'cc.ship.package',
'view_mode': 'tree,form',
'domain': [('bl_id', '=', self.id), ('is_sync', '=', False)],
}
def search_bl_info(self, pda_lang=False, type='tally'):
"""
查询提单信息
"""
vals = {
'bl_no': self.bl_no or '', # 提单号
'scan_big_package_qty': self.tally_big_package_qty + self.delivered_big_package_qty if type == 'tally' else self.delivered_big_package_qty,
# 已扫大包数量
'big_package_arr': [big_package_item.search_big_package_info(pda_lang=pda_lang, type=type) for
big_package_item in
self.big_package_ids],
# 大包信息
'ship_package_arr': [ship_package_item.search_ship_package_info(pda_lang=pda_lang) for ship_package_item in
self.ship_package_ids], # 小包信息
'pallet_arr': self.get_unique_pallet_info(), # 托盘信息
}
return vals
def get_unique_pallet_info(self):
"""获取唯一托盘信息,返回托盘号和最早的使用时间"""
pallet_info = {}
for package in self.big_package_ids:
pallet_number = package.pallet_number
pallet_usage_time = package.pallet_usage_date
if pallet_number and (pallet_number not in pallet_info or pallet_info[pallet_number] > pallet_usage_time):
pallet_info[pallet_number] = pallet_usage_time
return [{'pallet_number': k, 'pallet_usage_time': v} for k, v in pallet_info.items()]
def deal_bl_no(self, bl_no, state_arr=[]):
"""
处理提单号:去掉杠和空格,并转换为小写
:param bl_no:
:return:
"""
processed_bl_no = bl_no.replace('-', '').replace(' ', '').lower()
# 查询所有提单并处理它们的 bl_no
domain = [('state', 'in', state_arr)] if state_arr else []
all_bl_obj = self.env['cc.bl'].sudo().search(domain)
bl_obj = all_bl_obj.filtered(
lambda r: r.bl_no.replace('-', '').replace(' ', '').lower() == processed_bl_no) # 提单
return bl_obj
def try_callback_track(self, max_retries=3, ship_package_ids=[]):
""" 封装的重试逻辑 """
for i in range(max_retries):
if not ship_package_ids:
is_ok = self.callback_track()
else:
is_ok = self.package_callback_func(ship_package_ids)
if is_ok:
return True
logging.warning(f"Attempt {i + 1}/{max_retries} failed. Retrying...")
return False
def mail_auto_push(self, mail_time=False, ship_packages=[], action_type='tally'):
self = self.with_context(dict(self._context, is_mail=True))
for item in self:
# try:
if mail_time:
utc_time = datetime.strptime(mail_time, "%Y-%m-%d %H:%M:%S")
before_min = self.env['ir.config_parameter'].sudo().get_param('before_min') or 10
before_utc_time = utc_time - timedelta(minutes=int(before_min))
item.push_clear_customs_start(before_utc_time)
# 尝试调用 callback_track
if self.try_callback_track():
item.push_clear_customs_end(utc_time)
# 再次尝试调用 callback_track
if not self.try_callback_track():
logging.error(f"Failed to push item after {3} attempts.")
else:
logging.error(f"Failed to start process for item after {3} attempts.")
elif ship_packages:
ship_package_ids = [ship_package_dict for sublist in [d['id'] for d in ship_packages] for
ship_package_dict in sublist]
tally_state = 'checked_goods' if action_type == 'tally' else 'handover_completed'
# 后续节点
node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'),
('tally_state', '=', tally_state) # 检查理货或尾程交接的节点,根据排序进行升序
], order='seq asc')
if node_obj:
all_ship_package_obj = self.env['cc.ship.package'].search(
[('id', 'in', ship_package_ids)]) # 所有小包
# 预先获取所有同步日志 - 批量查询
all_sync_logs = self.env['cc.ship.package.sync.log'].sudo().search([
('package_id', 'in', ship_package_ids)
])
# 构建同步日志字典以加快查找
sync_log_dict = {}
for log in all_sync_logs:
if log.package_id.id not in sync_log_dict:
sync_log_dict[log.package_id.id] = set()
sync_log_dict[log.package_id.id].add(log.process_code)
# 构建ship_packages字典,用于快速查找
ship_packages_dict = {}
for package in ship_packages:
# 如果一个id在多个package中出现,使用最后一个package的tally_time
if package.get('tally_time'):
for single_id in package['id']:
ship_packages_dict[single_id] = package['tally_time']
# 前序节点 理货或尾程交接之前没有生成的节点
before_node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'), ('is_must', '=', True), ('seq', '<', node_obj[0].seq)],
order='seq asc')
# 理货或尾程交接之前没有生成的节点
for before_node in before_node_obj:
print('before_node:%s' % before_node.name)
before_minutes = before_node.calculate_total_interval(node_obj[0])
# 准备批量更新数据
update_data = []
for package in all_ship_package_obj:
package_id = package.id
if package_id not in sync_log_dict or before_node.tk_code not in sync_log_dict.get(package_id, set()):
tally_time = ship_packages_dict.get(package_id)
if tally_time:
operation_time = (datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') - timedelta(
minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta(
minutes=before_minutes)
update_data.append((
package_id,
before_node.id,
operation_time,
before_node.desc,
True if before_node.is_default else False
))
if update_data:
# 构建批量更新SQL
values_str = ','.join(self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in update_data)
sql = """
UPDATE cc_ship_package AS t SET
state = c.state,
process_time = c.process_time,
state_explain = c.state_explain,
is_sync = c.is_sync
FROM (VALUES
{}
) AS c(id, state, process_time, state_explain, is_sync)
WHERE t.id = c.id
""".format(values_str)
self.env.cr.execute(sql)
self._cr.commit() # 提交事务
# # 更新提单的未同步小包数量
# sql = """
# UPDATE cc_bl AS bl SET
# unsync_package_count = (
# SELECT COUNT(*)
# FROM cc_ship_package sp
# WHERE sp.bl_id = bl.id
# AND sp.is_sync = false
# )
# WHERE bl.id = %s
# """
# self.env.cr.execute(sql, (item.id,))
# self._cr.commit() # 提交事务
self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids)
# 理货或尾程交接的节点
# 预先获取所有状态节点
all_state_nodes = self.env['cc.node'].sudo().search([
('node_type', '=', 'package')
])
state_node_dict = {node.name: node for node in all_state_nodes}
next_minutes = int(self.env['ir.config_parameter'].sudo().get_param('next_minutes', default=20))
for index, node in enumerate(node_obj):
print('node:%s' % node.name)
update_data = []
for package in all_ship_package_obj:
if package.state.name in state_node_dict:
current_state_node = state_node_dict[package.state.name]
if current_state_node.seq < node.seq:
tally_time = ship_packages_dict.get(package.id)
if tally_time:
operation_time = (datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') + timedelta(
minutes=next_minutes * index)) if tally_time else fields.Datetime.now() + timedelta(
minutes=next_minutes * index)
update_data.append((
package.id,
node.id,
operation_time,
node.desc,
True if node.is_default else False
))
print('update_data:%s' % update_data)
if update_data:
print('11111111111')
# 构建批量更新SQL
values_str = ','.join(self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in update_data)
sql = """
UPDATE cc_ship_package AS t SET
state = c.state,
process_time = c.process_time,
state_explain = c.state_explain,
is_sync = c.is_sync
FROM (VALUES
{}
) AS c(id, state, process_time, state_explain, is_sync)
WHERE t.id = c.id
""".format(values_str)
self.env.cr.execute(sql)
self._cr.commit() # 提交事务
# # 更新提单的未同步小包数量
# sql = """UPDATE cc_bl AS bl SET unsync_package_count = ( SELECT COUNT(*) FROM cc_ship_package sp WHERE sp.bl_id = bl.id AND sp.is_sync = false) WHERE bl.id = %s """
# self.env.cr.execute(sql, (item.id,))
# self._cr.commit() # 提交事务
print('222222')
self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids)
print('33333')
return True
# except Exception as err:
# logging.error('fetch_mail_dlv--error:%s' % str(err))
def change_state_by_ship_package(self):
"""
根据小包的状态修改提单的状态
:return:
"""
# 如果提单有小包变成了清关开始,提单状态变为清关中
if self.state == 'draft' and self.ship_package_ids.filtered(
lambda line: line.state.tk_code == 'cb_imcustoms_start'):
self.ccing_func()
# 如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
if all(line.state.is_done for line in self.ship_package_ids) and self.unsync_package_count <= 0:
self.done_func()
class CcBigPackage(models.Model):
# 模型名称
_inherit = 'cc.big.package'
# 模型描述
_description = 'Big Package'
def search_big_package_info(self, pda_lang=False, type='tally'):
"""
查询大包信息
"""
unprocessed_goods_msg_dic = {
'en': 'Unprocessed goods',
'zh': '未理货'
}
checked_goods_msg_dic = {
'en': 'Checked goods',
'zh': '已理货'
}
handover_completed_msg_dic = {
'en': 'Handover Completed',
'zh': '尾程交接'
}
state_arr = {'unprocessed_goods': unprocessed_goods_msg_dic[pda_lang],
'checked_goods': checked_goods_msg_dic[pda_lang],
'handover_completed': handover_completed_msg_dic[pda_lang]} # 未理货/已理货/尾程交接
# 根据下一阶段服务商名称获取尾程服务商的记录
provider_obj = self.env['cc.last.mile.provider'].match_provider(self.next_provider_name)
vals = {
'tally_state_label': state_arr[self.tally_state] or '', # 理货状态显示名称
'tally_state': self.tally_state or '', # 理货状态系统KEY
'tally_user_id': (self.tally_user_id.id or 0) if type == 'tally' else (self.delivery_user_id.id or 0),
# 理货人id/交货人id
'tally_user_name': (self.tally_user_id.name or '') if type == 'tally' else (
self.delivery_user_id.name or ''),
# 理货人名称/交货人名称
'tally_time': (self.tally_time or '') if type == 'tally' else (self.delivery_time or ''),
# self.env['common.common'].sudo().get_format_time(str(self.tally_time)) if self.tally_time else '',
# 理货时间/交货时间
'big_package_no': self.big_package_no or '', # 大包号
'next_service_provider_name': self.next_provider_name or '', # 下一个服务商名称
'next_service_provider_tape_color': (provider_obj.tape_color_value or '') if provider_obj else '',
# 下一个服务商胶带对应色值
'pallet_number': self.pallet_number or '', # 托盘号
'pallet_usage_time': self.pallet_usage_date or '' # 托盘使用时间
}
return vals
def update_big_package_info(self, **kwargs):
"""
理货 tally/尾程交接 handover
"""
action_type = kwargs.get('action_type')
for item in self:
if action_type == 'tally' and item.tally_state == 'unprocessed_goods':
# 更新理货信息
self._update_info(item, kwargs, 'tally')
elif action_type == 'handover' and item.tally_state != 'handover_completed':
# 更新交接信息
self._update_info(item, kwargs, 'handover')
def _update_info(self, item, kwargs, action_type):
"""
更新信息的通用方法
"""
if action_type == 'tally':
if kwargs.get('tally_state'):
item.tally_state = kwargs['tally_state']
if kwargs.get('tally_user_id'):
item.tally_user_id = kwargs['tally_user_id']
if kwargs.get('tally_time'):
item.tally_time = datetime.strptime(kwargs['tally_time'], '%Y-%m-%d %H:%M:%S')
elif action_type == 'handover':
if kwargs.get('tally_state'):
item.tally_state = kwargs['tally_state']
if kwargs.get('tally_user_id'):
item.delivery_user_id = kwargs['tally_user_id']
if kwargs.get('tally_time'):
item.delivery_time = datetime.strptime(kwargs['tally_time'], '%Y-%m-%d %H:%M:%S')
import asyncio
import logging
import ssl
from datetime import timedelta, datetime
import aiohttp
import certifi
import pytz
from odoo import models, fields, api, _
def get_rfc339_time(utc_time=None):
if not utc_time:
# 获取当前时间的UTC时间
utc_time = datetime.utcnow()
# 创建+8时区的对象
target_timezone = pytz.timezone('Asia/Shanghai')
# 将UTC时间转换为目标时区时间
local_time = utc_time.replace(tzinfo=pytz.utc).astimezone(target_timezone)
# 格式化为RFC 3339格式
rfc3339_time = local_time.isoformat(timespec='seconds')
return rfc3339_time
# 定义一个方法,将时间的时区转为UTC时间
def get_utc_time(local_time=None):
if not local_time:
# 获取当前时间的UTC时间
local_time = datetime.now()
# 将本地时间转换为UTC时间
utc_time = local_time.astimezone(pytz.utc)
# 格式化为RFC 3339格式
# rfc3339_time = utc_time.isoformat(timespec='seconds')
return utc_time.strftime('%Y-%m-%d %H:%M:%S')
# 继承cc.clearance.file对象,并重载action_upload方法
class CcClearanceFile(models.Model):
_inherit = "cc.clearance.file"
_description = "Clearance File" # 清关文件
def get_clearance_file_feedback_data(self):
"""通关文件上传数据组织"""
happend_time = get_rfc339_time()
push_data = {
"master_waybill_no": self.bl_id.bl_no or '',
"customs_waybill_id": self.bl_id.customs_bl_no or '',
"operate_time": happend_time,
"file_detail": {
"file_url": "",
"file_code": self.file.decode(), # 将文件内容转换为base64编码,
"file_type": "PDF"
}
}
return push_data
def clearance_file_feedback(self):
if not self.is_upload and self.file:
data = self.get_clearance_file_feedback_data()
tt_api_obj = self.env["ao.tt.api"].sudo()
response = tt_api_obj.clearance_file_feedback(data)
response_data = response.json()
if response_data['code'] != 0:
# 清关文件回传错误
self.is_upload = False
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(self.file_name or '', '清关文件回传:' + error_msg, data,
code,
request_id, source='推出')
return error_msg
else:
# 清关文件回传成功
self.is_upload = True
self.upload_time = datetime.now()
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(self.file_name or '', '', data, 0, request_id,
source='推出')
return ''
# 重载action_upload方法
def action_sync(self):
self.clearance_file_feedback()
return super(CcClearanceFile, self).action_sync()
# 定义小包同步纪录对象,用于记录小包同步的纪录,包括小包对象,同步时间,操作状态,操作时间,操行备注,同步操作人
class CcShipPackageSyncLog(models.Model):
_name = 'cc.ship.package.sync.log'
_description = 'CC Ship Package Sync Log'
# 定义模型字段
# 小包对象
package_id = fields.Many2one('cc.ship.package', 'Ship Package', required=True)
# 同步时间
sync_time = fields.Datetime('Sync Time', default=fields.Datetime.now)
# 增加接口客户
api_customer = fields.Char('Api Customer')
# 操作状态
process_code = fields.Char('TK Process Code')
# 操作时间
operate_time = fields.Datetime('Operate Time', default=fields.Datetime.now)
# 操作备注
operate_remark = fields.Text('Operate Remark')
# 同步操作人
operate_user = fields.Many2one('res.users', 'Operate User', default=lambda self: self.env.user)
# 添加一个新增日志的方法,传入小包ID,API客户,操作状态,操作备注,操作时间
@api.model
def create_sync_log(self, package_id, api_customer, process_code, operate_remark, operate_time):
vals = {
'package_id': package_id,
'api_customer': api_customer,
'process_code': process_code,
'operate_remark': operate_remark,
'operate_time': operate_time
}
if self._context.get('is_mail'):
public_user = self.env.ref('base.public_user')
vals['operate_user'] = public_user.id
return self.create(vals)
# 继承小包对象,并重载action_sync方法, 增加is_sync字段
class CcShipPackage(models.Model):
_inherit = "cc.ship.package"
is_sync = fields.Boolean('Is Sync', default=False, index=True)
tk_code = fields.Char(related='state.tk_code', store=True, string='TK Code', help='TK Code')
# 增加同步日志纪录字段
sync_log_ids = fields.One2many('cc.ship.package.sync.log', 'package_id', 'Sync Logs')
def is_next_code(self, next_state_id):
"""
判断更新的节点是否是 小包状态的下级节点
:param next_state_id:
:return:
"""
if self.state:
if next_state_id in self.state.next_code_ids.ids:
return True
return False
@api.model
def create(self, vals_list):
"""
第一个节点的时候 默认已同步
"""
obj = super(CcShipPackage, self).create(vals_list)
if obj.state.is_default:
obj.is_sync = True
return obj
def action_sync(self):
for record in self:
record.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(record.id, 'Tiktok', record.state.tk_code,
record.state_explain,
record.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
def get_callback_track_data(self):
"""小包上传数据."""
# 获取该提单下的所有同步状态为未同步的小包
push_data = {
"provider_order_id": self.logistic_order_no,
"track_list": [
{
"shipping_method_code": self.state.tk_code,
"operate_time": get_utc_time(self.process_time),
"time_zone": "UTC+0",
"action_code": self.state.tk_code,
"operation_desc": self.state.desc,
"reason_code": self.node_exception_reason_id.name or "" # 异常原因
}
]
}
# logging.info('小包轨迹 push_data:%s' % push_data)
return push_data
def callback_track(self, is_push=True):
if not self.is_sync and self.state and self.state.tk_code:
data = self.get_callback_track_data()
if is_push:
tt_api_obj = self.env["ao.tt.api"].sudo()
response = tt_api_obj.callback_track(data)
response_data = response.json()
if response_data['code'] != 0:
self.is_sync = False
self._cr.commit()
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '',
'小包状态轨迹回传:' + error_msg,
data,
code,
request_id, source='推出')
return error_msg
else:
# 回传成功
self.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(self.id, 'Tiktok', self.state.tk_code,
self.state_explain,
self.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
self._cr.commit()
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '', '', data, 0, request_id,
source='推出')
return ''
else:
self.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(self.id, 'Tiktok', self.state.tk_code,
self.state_explain,
self.process_time.strftime(
'%Y-%m-%d %H:%M:%S'))
self._cr.commit()
request_id = ''
self.env['ao.tt.api.log'].sudo().create_api_log(self.tracking_no or '', '', data, 0, request_id,
source='推出')
return ''
def search_ship_package_info(self, pda_lang=False):
"""
查询小包信息
:return:
"""
return {
'logistic_order_no': self.logistic_order_no, # 物流订单号
}
# 继承提单对象t
class CcBl(models.Model):
_inherit = 'cc.bl'
# 计算未同步小包数量
@api.depends('ship_package_ids', 'ship_package_ids.is_sync')
def _compute_unsync_package_count(self):
for record in self:
record_counts = record.ship_package_ids.filtered(lambda r: not r.is_sync)
if record_counts:
record.unsync_package_count = len(record_counts)
else:
record.unsync_package_count = 0
# 增加未同步小包数量字段
unsync_package_count = fields.Integer('Unsync Package Count', compute='_compute_unsync_package_count', store=True)
# 定义一个方法, 获取提单下的所有未同步的小包,并回传小包状态
def callback_track(self):
is_ok = True
for item in self:
ship_packages = self.env['cc.ship.package'].search([('bl_id', '=', item.id), ('is_sync', '=', False)])
is_ok = item.package_callback_func(ship_packages.ids)
return is_ok
def package_callback_func(self, ship_package_ids):
"""
同步小包状态
"""
ship_packages = self.env['cc.ship.package'].search([('id', 'in', ship_package_ids), ('is_sync', '=', False)])
logging.info('package_callback_func ship_packages:%s' % len(ship_packages))
is_ok = True
tt_api_obj = self.env["ao.tt.api"].sudo()
async def perform_requests():
ssl_context = ssl.create_default_context(cafile=certifi.where())
async with aiohttp.ClientSession(connector=aiohttp.TCPConnector(ssl=ssl_context),
timeout=aiohttp.ClientTimeout(total=60)) as session:
tasks = []
for index, package in enumerate(ship_packages):
if not package.is_sync and package.state and package.state.tk_code:
data = package.get_callback_track_data()
tasks.append(tt_api_obj.async_callback_track(session, data, package.id))
responses = await asyncio.gather(*tasks)
return responses
# 在 Odoo 中运行异步任务
responses = asyncio.run(perform_requests())
for response_item in responses:
response_data = response_item[0]
logging.info('response_data response:%s' % response_data)
data = response_item[1]
package_id = response_item[2]
package_order = self.env['cc.ship.package'].sudo().browse(package_id)
if response_data['code'] != 0:
package_order.is_sync = False
error_msg = response_data['msg']
request_id = response_data['requestID']
code = response_data['code']
self.env['ao.tt.api.log'].sudo().create_api_log(
package_order.tracking_no or '', '小包状态轨迹回传:' + error_msg, data, code, request_id,
source='推出')
is_ok = False
else:
# 回传成功
package_order.is_sync = True
self.env['cc.ship.package.sync.log'].sudo().create_sync_log(
package_order.id, 'Tiktok', package_order.state.tk_code, package_order.state_explain,
package_order.process_time.strftime('%Y-%m-%d %H:%M:%S'))
request_id = response_data['requestID']
self.env['ao.tt.api.log'].sudo().create_api_log(
package_order.tracking_no or '', '', data, 0, request_id, source='推出')
# 如果提单有小包变成了清关开始,提单状态变为清关中;如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
self.change_state_by_ship_package()
return is_ok
def deal_ship_package_state(self):
for item in self:
ship_packages = self.env['cc.ship.package'].search([('bl_id', '=', item.id), ('is_sync', '=', False)])
for package in ship_packages:
package.callback_track(is_push=False)
return True
def batch_action_sync(self):
"""
批量回传通过文件
"""
for item in self:
for line in item.cc_attachment_ids:
line.action_sync()
# 创建显示包裹的action
def action_show_no_sync_ship_package(self):
# 返回一个action,显示包裹
return {
'name': _('Not Sync Ship Packages'),
'type': 'ir.actions.act_window',
'res_model': 'cc.ship.package',
'view_mode': 'tree,form',
'domain': [('bl_id', '=', self.id), ('is_sync', '=', False)],
}
def search_bl_info(self, pda_lang=False, type='tally'):
"""
查询提单信息
"""
vals = {
'bl_no': self.bl_no or '', # 提单号
'scan_big_package_qty': self.tally_big_package_qty + self.delivered_big_package_qty if type == 'tally' else self.delivered_big_package_qty,
# 已扫大包数量
'big_package_arr': [big_package_item.search_big_package_info(pda_lang=pda_lang, type=type) for
big_package_item in
self.big_package_ids],
# 大包信息
'ship_package_arr': [ship_package_item.search_ship_package_info(pda_lang=pda_lang) for ship_package_item in
self.ship_package_ids], # 小包信息
'pallet_arr': self.get_unique_pallet_info(), # 托盘信息
}
return vals
def get_unique_pallet_info(self):
"""获取唯一托盘信息,返回托盘号和最早的使用时间"""
pallet_info = {}
for package in self.big_package_ids:
pallet_number = package.pallet_number
pallet_usage_time = package.pallet_usage_date
if pallet_number and (pallet_number not in pallet_info or pallet_info[pallet_number] > pallet_usage_time):
pallet_info[pallet_number] = pallet_usage_time
return [{'pallet_number': k, 'pallet_usage_time': v} for k, v in pallet_info.items()]
def deal_bl_no(self, bl_no, state_arr=[]):
"""
处理提单号:去掉杠和空格,并转换为小写
:param bl_no:
:return:
"""
processed_bl_no = bl_no.replace('-', '').replace(' ', '').lower()
# 查询所有提单并处理它们的 bl_no
domain = [('state', 'in', state_arr)] if state_arr else []
all_bl_obj = self.env['cc.bl'].sudo().search(domain)
bl_obj = all_bl_obj.filtered(
lambda r: r.bl_no.replace('-', '').replace(' ', '').lower() == processed_bl_no) # 提单
return bl_obj
def try_callback_track(self, max_retries=3, ship_package_ids=[]):
""" 封装的重试逻辑 """
for i in range(max_retries):
if not ship_package_ids:
is_ok = self.callback_track()
else:
is_ok = self.package_callback_func(ship_package_ids)
if is_ok:
return True
logging.warning(f"Attempt {i + 1}/{max_retries} failed. Retrying...")
return False
def mail_auto_push(self, mail_time=False, ship_packages=[], action_type='tally'):
self = self.with_context(dict(self._context, is_mail=True))
for item in self:
# try:
if mail_time:
utc_time = datetime.strptime(mail_time, "%Y-%m-%d %H:%M:%S")
before_min = self.env['ir.config_parameter'].sudo().get_param('before_min') or 10
before_utc_time = utc_time - timedelta(minutes=int(before_min))
item.push_clear_customs_start(before_utc_time)
# 尝试调用 callback_track
if self.try_callback_track():
item.push_clear_customs_end(utc_time)
# 再次尝试调用 callback_track
if not self.try_callback_track():
logging.error(f"Failed to push item after {3} attempts.")
else:
logging.error(f"Failed to start process for item after {3} attempts.")
elif ship_packages:
ship_package_ids = [ship_package_dict for sublist in [d['id'] for d in ship_packages] for
ship_package_dict in sublist]
tally_state = 'checked_goods' if action_type == 'tally' else 'handover_completed'
# 后续节点
node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'),
('tally_state', '=', tally_state) # 检查理货或尾程交接的节点,根据排序进行升序
], order='seq asc')
if node_obj:
all_ship_package_obj = self.env['cc.ship.package'].search(
[('id', 'in', ship_package_ids)]) # 所有小包
# 预先获取所有同步日志 - 批量查询
all_sync_logs = self.env['cc.ship.package.sync.log'].sudo().search([
('package_id', 'in', ship_package_ids)
])
# 构建同步日志字典以加快查找
sync_log_dict = {}
for log in all_sync_logs:
if log.package_id.id not in sync_log_dict:
sync_log_dict[log.package_id.id] = set()
sync_log_dict[log.package_id.id].add(log.process_code)
# 构建ship_packages字典,用于快速查找
ship_packages_dict = {}
for package in ship_packages:
# 如果一个id在多个package中出现,使用最后一个package的tally_time
if package.get('tally_time'):
for single_id in package['id']:
ship_packages_dict[single_id] = package['tally_time']
# 前序节点 理货或尾程交接之前没有生成的节点
before_node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'), ('is_must', '=', True), ('seq', '<', node_obj[0].seq)],
order='seq asc')
# 理货或尾程交接之前没有生成的节点
for before_node in before_node_obj:
before_minutes = before_node.calculate_total_interval(node_obj[0])
for package in all_ship_package_obj:
package_id = package.id
if package_id not in sync_log_dict or before_node.tk_code not in sync_log_dict.get(
package_id, set()):
tally_time = ship_packages_dict.get(package_id)
if tally_time:
operation_time = (
datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') - timedelta(
minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta(
minutes=before_minutes)
package.write({
'state': before_node.id,
'process_time': operation_time,
'state_explain': before_node.desc,
'is_sync': True if before_node.is_default else False
})
self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids)
# 理货或尾程交接的节点
# 预先获取所有状态节点
all_state_nodes = self.env['cc.node'].sudo().search([
('node_type', '=', 'package')
])
state_node_dict = {node.name: node for node in all_state_nodes}
next_minutes = int(self.env['ir.config_parameter'].sudo().get_param('next_minutes', default=20))
for index, node in enumerate(node_obj):
for package in all_ship_package_obj:
if package.state.name in state_node_dict:
current_state_node = state_node_dict[package.state.name]
if current_state_node.seq < node.seq:
tally_time = ship_packages_dict.get(package.id)
if tally_time:
operation_time = (
datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') + timedelta(
minutes=next_minutes * index)) if tally_time else fields.Datetime.now() + timedelta(
minutes=next_minutes * index)
package.write({
'state': node.id,
'process_time': operation_time,
'state_explain': node.desc,
'is_sync': True if node.is_default else False
})
self.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids)
return True
# except Exception as err:
# logging.error('fetch_mail_dlv--error:%s' % str(err))
def change_state_by_ship_package(self):
"""
根据小包的状态修改提单的状态
:return:
"""
# 如果提单有小包变成了清关开始,提单状态变为清关中
if self.state == 'draft' and self.ship_package_ids.filtered(
lambda line: line.state.tk_code == 'cb_imcustoms_start'):
self.ccing_func()
# 如果提单所有小包的清关节点变成"是完成节点",则该提单状态变成已完成
if all(line.state.is_done for line in self.ship_package_ids) and self.unsync_package_count <= 0:
self.done_func()
class CcBigPackage(models.Model):
# 模型名称
_inherit = 'cc.big.package'
# 模型描述
_description = 'Big Package'
def search_big_package_info(self, pda_lang=False, type='tally'):
"""
查询大包信息
"""
unprocessed_goods_msg_dic = {
'en': 'Unprocessed goods',
'zh': '未理货'
}
checked_goods_msg_dic = {
'en': 'Checked goods',
'zh': '已理货'
}
handover_completed_msg_dic = {
'en': 'Handover Completed',
'zh': '尾程交接'
}
state_arr = {'unprocessed_goods': unprocessed_goods_msg_dic[pda_lang],
'checked_goods': checked_goods_msg_dic[pda_lang],
'handover_completed': handover_completed_msg_dic[pda_lang]} # 未理货/已理货/尾程交接
# 根据下一阶段服务商名称获取尾程服务商的记录
provider_obj = self.env['cc.last.mile.provider'].match_provider(self.next_provider_name)
vals = {
'tally_state_label': state_arr[self.tally_state] or '', # 理货状态显示名称
'tally_state': self.tally_state or '', # 理货状态系统KEY
'tally_user_id': (self.tally_user_id.id or 0) if type == 'tally' else (self.delivery_user_id.id or 0),
# 理货人id/交货人id
'tally_user_name': (self.tally_user_id.name or '') if type == 'tally' else (
self.delivery_user_id.name or ''),
# 理货人名称/交货人名称
'tally_time': (self.tally_time or '') if type == 'tally' else (self.delivery_time or ''),
# self.env['common.common'].sudo().get_format_time(str(self.tally_time)) if self.tally_time else '',
# 理货时间/交货时间
'big_package_no': self.big_package_no or '', # 大包号
'next_service_provider_name': self.next_provider_name or '', # 下一个服务商名称
'next_service_provider_tape_color': (provider_obj.tape_color_value or '') if provider_obj else '',
# 下一个服务商胶带对应色值
'pallet_number': self.pallet_number or '', # 托盘号
'pallet_usage_time': self.pallet_usage_date or '' # 托盘使用时间
}
return vals
def update_big_package_info(self, **kwargs):
"""
理货 tally/尾程交接 handover
"""
action_type = kwargs.get('action_type')
for item in self:
if action_type == 'tally' and item.tally_state == 'unprocessed_goods':
# 更新理货信息
self._update_info(item, kwargs, 'tally')
elif action_type == 'handover' and item.tally_state != 'handover_completed':
# 更新交接信息
self._update_info(item, kwargs, 'handover')
def _update_info(self, item, kwargs, action_type):
"""
更新信息的通用方法
"""
if action_type == 'tally':
if kwargs.get('tally_state'):
item.tally_state = kwargs['tally_state']
if kwargs.get('tally_user_id'):
item.tally_user_id = kwargs['tally_user_id']
if kwargs.get('tally_time'):
item.tally_time = datetime.strptime(kwargs['tally_time'], '%Y-%m-%d %H:%M:%S')
elif action_type == 'handover':
if kwargs.get('tally_state'):
item.tally_state = kwargs['tally_state']
if kwargs.get('tally_user_id'):
item.delivery_user_id = kwargs['tally_user_id']
if kwargs.get('tally_time'):
item.delivery_time = datetime.strptime(kwargs['tally_time'], '%Y-%m-%d %H:%M:%S')
...@@ -44,7 +44,7 @@ class TT(models.Model): ...@@ -44,7 +44,7 @@ class TT(models.Model):
def callback_track(self, push_data): def callback_track(self, push_data):
"""包裹轨迹回传""" """包裹轨迹/提单状态回传"""
url = '/logistics/provider/cross_border/callback_track?country=GB' url = '/logistics/provider/cross_border/callback_track?country=GB'
# cb_excustoms_finished 出口清关完毕 # cb_excustoms_finished 出口清关完毕
# cb_transport_assigned 干线揽收 # cb_transport_assigned 干线揽收
...@@ -66,7 +66,7 @@ class TT(models.Model): ...@@ -66,7 +66,7 @@ class TT(models.Model):
def mwb_status_update(self, push_data): def mwb_status_update(self, push_data):
"""清关提单状态回传""" """清关提单状态回传"""
url = 'logistics/provider/customs/mwb_status_update' url = 'logistics/provider/customs/mwb_status_update?country=GB'
timestamp = int(time.time()) timestamp = int(time.time())
sign = self.generate_sign(timestamp, push_data) sign = self.generate_sign(timestamp, push_data)
response = self.get_response(url, sign, timestamp, push_data) response = self.get_response(url, sign, timestamp, push_data)
...@@ -126,20 +126,20 @@ class TT(models.Model): ...@@ -126,20 +126,20 @@ class TT(models.Model):
'app_key': app_key 'app_key': app_key
} }
request_url = tt_url + url request_url = tt_url + url
logging.info('request_url: %s' % request_url) # logging.info('request_url: %s' % request_url)
logging.info('request_data: %s' % parameter) # logging.info('request_data: %s' % parameter)
for i in range(3): # 尝试最多3次 for i in range(3): # 尝试最多3次
try: try:
async with session.post(request_url, headers=headers, data=parameter) as response: async with session.post(request_url, headers=headers, data=parameter) as response:
response_data = await response.json() response_data = await response.json()
logging.info('response: %s', response_data) # logging.info('response: %s', response_data)
# print(response.json()) # print(response.json())
return response_data return response_data
except Exception as e: except Exception as e:
if i < 2: # 如果不是最后一次尝试,等待后重试 if i < 2: # 如果不是最后一次尝试,等待后重试
await asyncio.sleep(2 ** i) # 指数退避策略 await asyncio.sleep(2 ** i) # 指数退避策略
else: else:
logging.warning('request error:%s' % str(e)) # logging.warning('request error:%s' % str(e))
return {'code': 500, 'requestID': 'request error timeout', 'msg': '超时,请重试'} # 如果重试次数用尽,抛出异常 return {'code': 500, 'requestID': 'request error timeout', 'msg': '超时,请重试'} # 如果重试次数用尽,抛出异常
async def async_callback_track_callback(self, session, push_data, package_id): async def async_callback_track_callback(self, session, push_data, package_id):
...@@ -157,3 +157,17 @@ class TT(models.Model): ...@@ -157,3 +157,17 @@ class TT(models.Model):
"""异步调用推送接口""" """异步调用推送接口"""
# async with semaphore: # async with semaphore:
return await self.async_callback_track_callback(session, data, package_id) return await self.async_callback_track_callback(session, data, package_id)
async def async_bl_callback_track_callback(self, session, push_data, package_id):
"""提单状态回传"""
url = '/logistics/provider/customs/mwb_status_update?country=GB'
timestamp = int(time.time())
sign = self.generate_sign(timestamp, push_data)
response = await self.async_get_response(session, url, sign, timestamp, push_data)
logging.info('bl_callback_track response:%s' % response)
return response, push_data, package_id
async def async_bl_callback_track(self, session, data, bl_id):
"""异步调用提单推送接口"""
# async with semaphore:
return await self.async_bl_callback_track_callback(session, data, bl_id)
...@@ -7,3 +7,10 @@ access_cc_ship_package_sync_log_base.group_erp_manager,cc_ship_package_sync_log ...@@ -7,3 +7,10 @@ access_cc_ship_package_sync_log_base.group_erp_manager,cc_ship_package_sync_log
access_cc_ship_package_sync_log_ccs_base.group_clearance_of_customs_manager,cc_ship_package_sync_log ccs_base.group_clearance_of_customs_manager,ccs_connect_tiktok.model_cc_ship_package_sync_log,ccs_base.group_clearance_of_customs_manager,1,0,0,0 access_cc_ship_package_sync_log_ccs_base.group_clearance_of_customs_manager,cc_ship_package_sync_log ccs_base.group_clearance_of_customs_manager,ccs_connect_tiktok.model_cc_ship_package_sync_log,ccs_base.group_clearance_of_customs_manager,1,0,0,0
access_cc_ship_package_sync_log_ccs_base.group_clearance_of_customs_user,cc_ship_package_sync_log ccs_base.group_clearance_of_customs_user,ccs_connect_tiktok.model_cc_ship_package_sync_log,ccs_base.group_clearance_of_customs_user,1,0,0,0 access_cc_ship_package_sync_log_ccs_base.group_clearance_of_customs_user,cc_ship_package_sync_log ccs_base.group_clearance_of_customs_user,ccs_connect_tiktok.model_cc_ship_package_sync_log,ccs_base.group_clearance_of_customs_user,1,0,0,0
access_cc_bl_sync_log_base.group_user,cc_bl_sync_log base.group_user,ccs_connect_tiktok.model_cc_bl_sync_log,base.group_user,1,0,0,0
access_cc_bl_sync_log_base.group_erp_manager,cc_bl_sync_log base.group_erp_manager,ccs_connect_tiktok.model_cc_bl_sync_log,base.group_erp_manager,1,1,1,1
access_cc_bl_sync_log_ccs_base.group_clearance_of_customs_manager,cc_bl_sync_log ccs_base.group_clearance_of_customs_manager,ccs_connect_tiktok.model_cc_bl_sync_log,ccs_base.group_clearance_of_customs_manager,1,0,0,0
access_cc_bl_sync_log_ccs_base.group_clearance_of_customs_user,cc_bl_sync_log ccs_base.group_clearance_of_customs_user,ccs_connect_tiktok.model_cc_bl_sync_log,ccs_base.group_clearance_of_customs_user,1,0,0,0
<?xml version="1.0" encoding="utf-8"?>
<odoo>
# ---------- CC Bl Sync Log ------------
<record model="ir.ui.view" id="tree_cc_bl_sync_log_view">
<field name="name">tree.cc.bl.sync.log</field>
<field name="model">cc.bl.sync.log</field>
<field name="arch" type="xml">
<tree string="CC Bl Sync Log">
<field optional="show" name="bl_id" string="Bill of Loading"/>
<field optional="show" name="api_customer" string="Api Customer"/>
<field optional="show" name="process_code" string="TK Process Code"/>
<field optional="show" name="progress_name" string="Progress Name"/>
<field optional="show" name="operate_time" string="Operate Time"/>
<field optional="show" name="operate_user" string="Operate User"/>
<field optional="show" name="sync_time" string="Sync Time"/>
</tree>
</field>
</record>
<record model="ir.ui.view" id="form_cc_bl_sync_log_view">
<field name="name">form.cc.bl.sync.log</field>
<field name="model">cc.bl.sync.log</field>
<field name="arch" type="xml">
<form string="CC Bl Sync Log">
<sheet>
<group>
<group>
<field name="bl_id" string="Bill of Loading"/>
<field name="api_customer" string="Api Customer"/>
<field name="process_code" string="TK Process Code"/>
<field name="progress_name" string="Progress Name"/>
<field name="operate_time" string="Operate Time"/>
<field name="operate_user" string="Operate User"/>
<field name="sync_time" string="Sync Time"/>
</group>
<group>
</group>
</group>
<notebook>
<page string="Operate Remark">
<field name="operate_remark" string="Operate Remark"/>
</page>
</notebook>
</sheet>
</form>
</field>
</record>
<record model="ir.ui.view" id="search_cc_bl_sync_log_view">
<field name="name">search.cc.bl.sync.log</field>
<field name="model">cc.bl.sync.log</field>
<field name="arch" type="xml">
<search string="CC Bl Sync Log">
<field name="bl_id" string="Bill of Loading"/>
<field name="api_customer" string="Api Customer"/>
<field name="process_code" string="TK Process Code"/>
<field name="operate_time" string="Operate Time"/>
<field name="operate_user" string="Operate User"/>
<field name="sync_time" string="Sync Time"/>
<separator/>
<filter name="filter_operate_time" string="Operate Time" date="operate_time"/>
<filter name="filter_sync_time" string="Sync Time" date="sync_time"/>
<separator/>
<group expand="0" string="Group By">
<filter domain="[]" name="groupby_bl_id" string="Bill of Loading"
context="{'group_by': 'bl_id'}"/>
<filter domain="[]" name="groupby_operate_user" string="Operate User"
context="{'group_by': 'operate_user'}"/>
</group>
</search>
</field>
</record>
<record model="ir.actions.act_window" id="action_cc_bl_sync_log">
<field name="name">CC Bl Sync Log</field>
<field name="res_model">cc.bl.sync.log</field>
<field name="view_mode">tree</field>
<field name="domain">[]</field>
<field name="context">{}</field>
<field name="help" type="html">
<p class="o_view_nocontent_smiling_face">
[CC Bl Sync Log] Not yet! Click the Create button in the top left corner and the sofa is yours!
</p>
<p>
</p>
</field>
</record>
</odoo>
\ No newline at end of file
...@@ -24,6 +24,8 @@ ...@@ -24,6 +24,8 @@
<header position="inside"> <header position="inside">
<button name="callback_track" string="Sync Package Status" type="object"/> <button name="callback_track" string="Sync Package Status" type="object"/>
<button name="batch_action_sync" string="Sync CC Attachment" type="object"/> <button name="batch_action_sync" string="Sync CC Attachment" type="object"/>
<!--增加同步提单状态的按钮-->
<button name="action_sync_bl_status" string="Sync Bl Status" type="object"/>
</header> </header>
<button name="action_show_ship_package" position="replace"> <button name="action_show_ship_package" position="replace">
...@@ -45,48 +47,9 @@ ...@@ -45,48 +47,9 @@
</div> </div>
</button> </button>
</button> </button>
</field>
</record>
<!-- # 继承ccs_base模块的cc_ship_package_view.xml视图,增加is_sync字段在列表中-->
<record model="ir.ui.view" id="tree_cc_ship_package_view_inherit">
<field name="name">tree_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.tree_cc_ship_package_view"/>
<field name="arch" type="xml">
<field name="state" position="after">
<field name="is_sync"/>
</field>
<tree position="attributes">
<attribute name="decoration-danger">is_sync == False</attribute>
</tree>
</field>
</record>
<!-- # 继承ccs_base模块的search_cc_ship_package_view视图,装置加未同步的筛选条件-->
<record model="ir.ui.view" id="search_cc_ship_package_view_inherit">
<field name="name">search_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.search_cc_ship_package_view"/>
<field name="arch" type="xml">
<search position="inside">
<filter string="Not Sync" name="filter_is_sync" domain="[('is_sync','=',False)]"/>
</search>
</field>
</record>
# 继承ccs_base模块的form_cc_ship_package_view视图,增加同步日志列表在notebook中
<record model="ir.ui.view" id="form_cc_ship_package_view_inherit">
<field name="name">form_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.form_cc_ship_package_view"/>
<field name="arch" type="xml">
<notebook position="inside"> <notebook position="inside">
<page string="Sync Log"> <page string="Sync Log">
<field name="sync_log_ids" widget="one2many_list"/> <field name="bl_sync_log_ids" widget="one2many_list"/>
</page> </page>
</notebook> </notebook>
</field> </field>
......
<?xml version="1.0" encoding="UTF-8" ?> <?xml version="1.0" encoding="UTF-8" ?>
<odoo> <odoo>
<record model="ir.ui.view" id="tree_cc_ship_package_view">
<field name="name">tree.cc.ship.package</field> <!-- # 继承ccs_base模块的cc_ship_package_view.xml视图,增加is_sync字段在列表中-->
<record model="ir.ui.view" id="tree_cc_ship_package_view_inherit">
<field name="name">tree_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field> <field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.tree_cc_ship_package_view"/> <field name="inherit_id" ref="ccs_base.tree_cc_ship_package_view"/>
<field name="arch" type="xml"> <field name="arch" type="xml">
<field name="state" position="after">
<field name="is_sync"/>
</field>
<field name="state" position="replace"> <field name="state" position="replace">
<field name="tk_code" invisible="1"/> <field name="tk_code" invisible="1"/>
<field optional="show" name="state" string="Progress" widget="badge" <field optional="show" name="state" string="Progress" widget="badge"
...@@ -13,7 +19,35 @@ ...@@ -13,7 +19,35 @@
decoration-warning="tk_code in ('cb_imcustoms_inspection','cb_imcustoms_exception')" decoration-warning="tk_code in ('cb_imcustoms_inspection','cb_imcustoms_exception')"
decoration-muted="tk_code=='cb_import_customs_failure'"/> decoration-muted="tk_code=='cb_import_customs_failure'"/>
</field> </field>
<tree position="attributes">
<attribute name="decoration-danger">is_sync == False</attribute>
</tree>
</field>
</record>
<!-- # 继承ccs_base模块的search_cc_ship_package_view视图,装置加未同步的筛选条件-->
<record model="ir.ui.view" id="search_cc_ship_package_view_inherit">
<field name="name">search_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.search_cc_ship_package_view"/>
<field name="arch" type="xml">
<search position="inside">
<filter string="Not Sync" name="filter_is_sync" domain="[('is_sync','=',False)]"/>
</search>
</field>
</record>
# 继承ccs_base模块的form_cc_ship_package_view视图,增加同步日志列表在notebook中
<record model="ir.ui.view" id="form_cc_ship_package_view_inherit">
<field name="name">form_cc_ship_package_view_inherit</field>
<field name="model">cc.ship.package</field>
<field name="inherit_id" ref="ccs_base.form_cc_ship_package_view"/>
<field name="arch" type="xml">
<notebook position="inside">
<page string="Sync Log">
<field name="sync_log_ids" widget="one2many_list"/>
</page>
</notebook>
</field> </field>
</record> </record>
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论