提交 b6cf9cb8 authored 作者: 伍姿英's avatar 伍姿英

Merge branch 'release/3.8.1'

......@@ -28,7 +28,7 @@
<!-- 清理向导生成的临时附件-->
<record id="cron_cleanup_temp_attachments" model="ir.cron">
<field name="name">清理向导临时附件</field>
<field name="model_id" ref="model_batch_get_pod_info_wizard"/>
<field name="model_id" ref="model_common_common"/>
<field name="state">code</field>
<field name="code">model.cron_cleanup_temp_attachments()</field>
<field name='interval_number'>1</field>
......
# -*- coding: utf-8 -*-
import base64
import datetime
import gc
import json
import logging
import os
import tempfile
import time
import pytz
from odoo import models
import requests
from odoo import models, _
from odoo.exceptions import ValidationError
from .redis_connection import redis_connection
......@@ -16,7 +24,6 @@ class CommonCommon(models.Model):
_name = 'common.common'
_description = u'公用基础类'
def process_num(self, input_str):
"""
处理导入
......@@ -168,6 +175,446 @@ class CommonCommon(models.Model):
"""
return r
def get_pod_pdf_files(self, bill_numbers, api_sub_path):
"""
获取POD PDF文件
:param bill_numbers: 订单号列表
:param api_sub_path: API子路径
:return: PDF文件列表
"""
api_url = self.env['ir.config_parameter'].sudo().get_param(
'last_mile_pod_api_url',
'http://172.104.52.150:7002'
)
if not api_url:
raise ValidationError(_('API URL not configured'))
request_data = {
"bill_numbers": bill_numbers
}
try:
response = requests.post(
f"{api_url}{api_sub_path}",
headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
json=request_data
)
if response.status_code == 200:
result = response.json()
if not result:
raise ValidationError(_('API returned empty response'))
if not result.get('success'):
error_msg = result.get('message', 'Unknown error')
raise ValidationError(_('API returned error: %s') % error_msg)
results = result.get('results', [])
if not results:
raise ValidationError(_('No PDF files found in API response'))
pdf_file_arr = []
for result_item in results:
if result_item.get('success'):
bill_number = result_item.get('bill_number')
filename = result_item.get('filename')
base64_data = result_item.get('base64')
pdf_file_arr.append({
'bl_no': bill_number,
'file_name': filename,
'file_data': base64_data
})
return pdf_file_arr
else:
raise ValidationError(_('Failed to get PDF file from API: %s') % response.text)
except requests.exceptions.RequestException as e:
raise ValidationError(_('API request failed: %s') % str(e))
def push_sync_pod_task(self, processed_files, file_type, pod_desc, filter_temu=False):
"""
推送POD同步任务到redis
:param processed_files: 处理后的文件数组
:param file_type: 清关文件类型名称
:param pod_desc: 用于提示的信息描述,例如“尾程POD”或“货站提货POD”
:param filter_temu: 是否过滤掉bl_type为temu的提单
"""
redis_conn = self.get_redis()
if not redis_conn or redis_conn == 'no':
raise ValidationError('未连接redis,无法同步%s,请联系管理员' % pod_desc)
bl_ids = []
for file_info in processed_files:
bl = file_info.get('bl')
if not bl:
continue
if filter_temu and getattr(bl, 'bl_type', False) == 'temu':
continue
clearance_file = file_info.get('clearance_file')
if not clearance_file:
continue
bl_ids.append(bl.id)
if not bl_ids:
return
payload = {
'ids': bl_ids,
'action_type': 'sync_last_mile_pod',
'user_login': self.env.user.login,
'file_type': file_type
}
try:
redis_conn.lpush('mail_push_package_list', json.dumps(payload))
except Exception as e:
logging.error('sync_last_mile_pod redis error:%s' % e)
raise ValidationError('推送%s同步任务到redis失败,请重试或联系管理员' % pod_desc)
def cleanup_temp_attachments(self, bl_objs):
"""
清理与当前向导相关的临时附件,包括服务器和本地开发环境的物理文件
"""
try:
if not bl_objs:
return
attachments = self.env['ir.attachment'].search([
('res_model', '=', bl_objs._name),
('res_id', 'in', bl_objs.ids),
('name', 'like', 'temp_pod_%')
])
if attachments:
attachments.unlink()
except Exception as e:
_logger.error('清理临时附件失败: %s' % str(e))
def cron_cleanup_temp_attachments(self):
"""
定时清理向导生成的临时附件
每天早上8点执行,删除1天之前创建的temp_pod_开头的附件
"""
try:
today = datetime.datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
cutoff = today - datetime.timedelta(days=1)
_logger.info('开始执行定时清理临时附件任务,清理时间点: %s' % cutoff.strftime('%Y-%m-%d %H:%M:%S'))
sql_query = """
SELECT id, name, res_model, res_id, create_date, store_fname
FROM ir_attachment
WHERE name LIKE 'temp_pod_%%'
AND create_date < '%s'
ORDER BY create_date DESC
""" % (cutoff.strftime('%Y-%m-%d %H:%M:%S'))
self.env.cr.execute(sql_query)
sql_results = self.env.cr.fetchall()
if sql_results:
attachment_ids = [result[0] for result in sql_results]
temp_attachments = self.env['ir.attachment'].sudo().browse(attachment_ids)
attachment_count = len(temp_attachments)
_logger.info(
'找到 %s 个%s之前创建的临时附件,开始清理' % (attachment_count, cutoff.strftime('%Y-%m-%d')))
import os
from odoo.tools import config
data_dir = config.filestore(self.env.cr.dbname)
for attachment in temp_attachments:
try:
if hasattr(attachment, 'store_fname') and attachment.store_fname:
file_path = attachment.store_fname
elif hasattr(attachment, 'datas_fname') and attachment.datas_fname:
file_path = attachment.datas_fname
else:
file_path = attachment.name
if data_dir and file_path:
full_path = os.path.join(data_dir, file_path)
if os.path.exists(full_path):
os.remove(full_path)
except Exception as file_e:
_logger.warning('删除物理文件失败 %s: %s' % (attachment.name, str(file_e)))
temp_attachments.unlink()
except Exception as e:
_logger.error('定时清理临时附件失败: %s' % str(e))
def merge_pod_pdfs(self, processed_files):
"""
合并处理后的POD PDF文件
:param processed_files: 处理后的文件列表,每个元素为字典包含bl_no, file_data, bl
:return: 合并后的PDF文件数据和文件名
"""
import fitz
temp_file_path = None
try:
valid_files = []
for file_info in processed_files:
if file_info.get('bl_no') and file_info.get('file_data') and file_info.get('bl'):
valid_files.append(file_info)
if not valid_files:
_logger.warning("没有有效的PDF文件可以合并")
return None, None
if len(valid_files) == 1:
file_info = valid_files[0]
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"POD文件_{bl_no}_{timestamp}.pdf"
_logger.info("单个PDF文件直接使用: %s", pdf_filename)
return file_data, pdf_filename
_logger.info("开始合并 %s 个PDF文件", len(valid_files))
temp_file_path = tempfile.mktemp(suffix='.pdf')
merged_pdf = fitz.open()
bl_numbers = []
for file_info in valid_files:
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
bl_numbers.append(bl_no)
source_pdf = None
try:
pdf_binary = base64.b64decode(file_data)
source_pdf = fitz.open(stream=pdf_binary, filetype="pdf")
merged_pdf.insert_pdf(source_pdf)
_logger.info("已添加提单 %s 的PDF到合并文档(%s 页)", bl_no, len(source_pdf))
except Exception as e:
_logger.error("合并提单 %s 的PDF失败: %s", bl_no, str(e))
continue
finally:
if source_pdf:
source_pdf.close()
gc.collect()
if len(merged_pdf) > 0:
merged_pdf.save(temp_file_path, garbage=4, deflate=True, clean=True)
merged_pdf.close()
with open(temp_file_path, 'rb') as f:
pdf_data = f.read()
merged_pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
del pdf_data
gc.collect()
bl_numbers_str = '_'.join(bl_numbers[:5])
if len(bl_numbers) > 5:
bl_numbers_str += f'_等{len(bl_numbers)}个'
timestamp = datetime.datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"合并POD文件_{bl_numbers_str}_{timestamp}.pdf"
_logger.info("成功合并 %s 个PDF文件,文件名: %s", len(bl_numbers), pdf_filename)
return merged_pdf_base64, pdf_filename
_logger.warning("没有有效的PDF文件可以合并")
return None, None
except Exception as e:
_logger.error("合并PDF文件失败: %s", str(e))
return None, None
finally:
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
_logger.info("已删除临时文件: %s", temp_file_path)
except Exception as e:
_logger.warning("删除临时文件失败: %s", str(e))
def match_pod_files(self, pdf_file_arr, bl_obj, include_processing_failed=False):
"""
匹配POD文件与提单
:param pdf_file_arr: PDF文件数组,每个元素为字典包含file_name, file_data, bl_no
:param bl_obj: 提单记录集
:param include_processing_failed: 是否包含处理失败的文件
:return: 匹配后的文件数组,每个元素为字典包含bl, file_name, file_data, bl_no
"""
processed_files = []
for bl in bl_obj:
select_bl_no = self.process_match_str(bl.bl_no)
if not select_bl_no:
continue
for pdf_file in pdf_file_arr:
file_name = pdf_file.get('file_name')
file_data = pdf_file.get('file_data')
raw_bl_no = pdf_file.get('bl_no')
bl_no = self.process_match_str(raw_bl_no)
if bl_no and select_bl_no == bl_no:
file_info = {
'bl': bl,
'file_name': file_name,
'file_data': file_data,
'bl_no': raw_bl_no or bl.bl_no,
}
if include_processing_failed:
file_info['processing_failed'] = False
processed_files.append(file_info)
break
return processed_files
def write_pod_pdf_files(self, processed_files, fix_name):
"""
Write PDF file to clearance files # 回写PDF文件到清关文件
:param processed_files: 处理后的文件数组
:param fix_name:
"""
clearance_model = self.env['cc.clearance.file']
valid_entries = []
bl_ids = set()
for file_info in processed_files:
bl = file_info.get('bl')
if not bl:
_logger.warning("跳过没有提单信息的文件")
continue
file_name = file_info.get('file_name', '')
file_data = file_info.get('file_data', '')
if not file_data:
continue
valid_entries.append((file_info, bl, file_name, file_data))
bl_ids.add(bl.id)
if not valid_entries:
return
existing_clearance = clearance_model.search(
[('bl_id', 'in', list(bl_ids)), ('file_name', '=', fix_name), ('file', '=', False)]
)
existing_by_bl = {rec.bl_id.id: rec for rec in existing_clearance}
create_vals_list = []
create_infos = []
for file_info, bl, file_name, file_data in valid_entries:
clearance_file = existing_by_bl.get(bl.id)
if clearance_file:
clearance_file.write({
'attachment_name': file_name,
'file': file_data
})
file_info['clearance_file'] = clearance_file
self.env.cr.commit()
else:
create_vals_list.append({
'bl_id': bl.id,
'file_name': fix_name,
'attachment_name': file_name,
'file': file_data
})
create_infos.append(file_info)
if create_vals_list:
new_records = clearance_model.create(create_vals_list)
_logger.info("已创建新清关文件记录: %s", new_records.ids)
self.env.cr.commit()
for clearance_file, file_info in zip(new_records, create_infos):
bl = file_info['bl']
file_info['clearance_file'] = clearance_file
def serialize_pod_processed_files(self, processed_files):
"""
将processed_files序列化为JSON字符串,文件数据存储到临时附件中
:param processed_files: 处理后的文件数组
:return: JSON字符串(只包含引用信息,不包含文件数据)
注意:不在这里清理临时附件,因为预览时需要保留附件数据,
只有在确认操作完成后才清理临时附件
"""
serialized_data = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
file_data = file_info.get('file_data', '')
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
attachment_id = None
if file_data:
try:
attachment = self.env['ir.attachment'].create({
'name': f"temp_pod_{bl.bl_no}_{int(time.time())}.pdf",
'datas': file_data,
'type': 'binary',
'res_model': bl._name,
'res_id': bl.id,
})
attachment_id = attachment.id
_logger.info("已创建临时附件存储文件: %s, ID: %s", attachment.name, attachment_id)
except Exception as e:
_logger.error("创建临时附件失败: %s", str(e))
else:
_logger.warning("提单 %s 的文件数据为空,无法创建附件", bl.bl_no)
data = {
'bl_id': bl.id,
'bl_no': bl.bl_no,
'file_name': file_name,
'attachment_id': attachment_id,
}
if 'ocr_texts' in file_info:
data['ocr_texts'] = file_info['ocr_texts']
if 'valid_packages' in file_info and file_info['valid_packages']:
valid_packages = file_info['valid_packages']
if hasattr(valid_packages, 'ids'):
data['valid_package_ids'] = valid_packages.ids
elif isinstance(valid_packages, list):
data['valid_package_ids'] = [p.id for p in valid_packages if hasattr(p, 'id')]
else:
data['valid_package_ids'] = []
_logger.info(
"序列化时保存valid_packages: 提单 %s, 满足条件的小包ID: %s",
bl.bl_no, data['valid_package_ids']
)
serialized_data.append(data)
return json.dumps(serialized_data, ensure_ascii=False)
def deserialize_pod_processed_files(self, json_data):
"""
将JSON字符串反序列化为processed_files(从附件中读取文件数据)
:param json_data: JSON字符串
:return: 处理后的文件数组
"""
if not json_data:
return []
try:
serialized_data = json.loads(json_data)
processed_files = []
for data in serialized_data:
bl_id = data.get('bl_id')
attachment_id = data.get('attachment_id')
if not bl_id:
continue
bl = self.env['cc.bl'].browse(bl_id)
if not bl.exists():
continue
file_data = ''
if attachment_id:
try:
attachment = self.env['ir.attachment'].browse(attachment_id)
if attachment.exists():
file_data = attachment.datas
_logger.info(
"从附件读取文件: %s, ID: %s, 数据长度: %s",
attachment.name, attachment_id,
len(file_data) if file_data else 0
)
else:
_logger.warning("附件不存在: %s", attachment_id)
except Exception as e:
_logger.error("读取附件失败: %s", str(e))
else:
_logger.warning("提单 %s 没有附件ID,无法读取文件数据", bl.bl_no)
file_info = {
'bl': bl,
'bl_no': data.get('bl_no', ''),
'file_name': data.get('file_name', ''),
'file_data': file_data,
}
if 'ocr_texts' in data:
file_info['ocr_texts'] = data['ocr_texts']
if 'valid_package_ids' in data and data['valid_package_ids']:
valid_package_ids = data['valid_package_ids']
valid_packages = self.env['cc.ship.package'].browse(valid_package_ids)
file_info['valid_packages'] = valid_packages
_logger.info(
"反序列化时恢复valid_packages: 提单 %s, 满足条件的小包ID: %s, 数量: %s",
bl.bl_no, valid_package_ids, len(valid_packages)
)
processed_files.append(file_info)
return processed_files
except Exception as e:
_logger.error("反序列化processed_files失败: %s", str(e))
return []
def init_timezone_data(self, name):
timezone_data = {}
timezone_data['Africa/Abidjan'] = 0
......
......@@ -426,9 +426,9 @@
</field>
</record>
<!-- 获取尾程快递POD -->
<record id="bl_get_delivery_pod_info_server_action" model="ir.actions.server">
<field name="name">Batch Get Last Mile POD Info</field>
<!-- 下载货站提货POD-->
<record id="bl_download_pod_server_action" model="ir.actions.server">
<field name="name">Batch Download PickUp POD</field>
<field name="model_id" ref="model_cc_bl"/>
<field name="binding_model_id" ref="model_cc_bl"/>
<field name="state">code</field>
......@@ -436,13 +436,13 @@
<field name="groups_id" eval="[(4, ref('ccs_base.group_clearance_of_customs_user'))]"/>
<field name="code">
if records:
action = records.action_batch_get_last_mile_pod_info()
action = records.action_batch_download_pod('货站提货POD')
</field>
</record>
<!-- 下载货站提货POD-->
<record id="bl_download_pod_server_action" model="ir.actions.server">
<field name="name">Batch Download PickUp POD</field>
<!-- 获取尾程快递POD -->
<record id="bl_get_delivery_pod_info_server_action" model="ir.actions.server">
<field name="name">Batch Get Last Mile POD Info</field>
<field name="model_id" ref="model_cc_bl"/>
<field name="binding_model_id" ref="model_cc_bl"/>
<field name="state">code</field>
......@@ -450,7 +450,7 @@
<field name="groups_id" eval="[(4, ref('ccs_base.group_clearance_of_customs_user'))]"/>
<field name="code">
if records:
action = records.action_batch_download_pod('货站提货POD')
action = records.action_batch_get_last_mile_pod_info()
</field>
</record>
......
......@@ -60,10 +60,16 @@ class BatchGetLastMilePodInfoWizard(models.TransientModel):
def _get_bill_numbers(self, bl_objs):
_logger.info(f"开始预览操作,提单数量: {len(bl_objs)}")
start_time = time.time()
logging.info(f"开始预览操作,开始时间{len(bl_objs)}")
# 调用接口获取提单pdf文件
pdf_file_arr = self._get_pdf_file_arr(bl_objs)
end_time = time.time()
logging.info(f"获取PDF文件耗时: {end_time - start_time} 秒")
# 处理PDF文件,匹配提单对象
processed_files = self._match_bl_by_file_name(pdf_file_arr, bl_objs)
file_end_time = time.time()
logging.info(f"处理PDF文件耗时: {file_end_time - end_time} 秒")
# 把没有匹配到文件的进行提示
error_bl = []
matched_bl_ids = [f['bl'].id for f in processed_files if f.get('bl')]
......@@ -192,57 +198,8 @@ class BatchGetLastMilePodInfoWizard(models.TransientModel):
从API获取PDF文件
"""
bill_numbers = [self.env['common.common'].sudo().process_match_str(bl.bl_no) for bl in bl_objs]
# 调用API获取PDF文件
api_url = self.env['ir.config_parameter'].sudo().get_param('last_mile_pod_api_url',
'http://172.104.52.150:7002')
if not api_url:
raise ValidationError(_('API URL not configured'))
# 构建请求数据
request_data = {
"bill_numbers": bill_numbers
}
try:
response = requests.post(
f"{api_url}/api/pod/pdfs",
headers={'Content-Type': 'application/json', 'Accept': 'application/json'},
json=request_data,
timeout=30
)
if response.status_code == 200:
result = response.json()
# 检查API响应结构
if not result:
raise ValidationError(_('API returned empty response'))
if not result.get('success'):
error_msg = result.get('message', 'Unknown error')
raise ValidationError(_('API returned error: %s') % error_msg)
# 处理结果数据
results = result.get('results', [])
if not results:
raise ValidationError(_('No PDF files found in API response'))
# 构建PDF文件数组
pdf_file_arr = []
for result_item in results:
if result_item.get('success'):
# 验证必要字段
bill_number = result_item.get('bill_number')
filename = result_item.get('filename')
base64_data = result_item.get('base64')
pdf_file_arr.append({
'bl_no': bill_number,
'file_name': filename,
'file_data': base64_data
})
return pdf_file_arr
else:
raise ValidationError(_('Failed to get PDF file from API: %s') % response.text)
except requests.exceptions.RequestException as e:
raise ValidationError(_('API request failed: %s') % str(e))
common = self.env['common.common'].sudo()
return common.get_pod_pdf_files(bill_numbers, '/api/pod/pdfs')
def _write_pdf_file(self, processed_files, fix_name='尾程交接POD(待大包数量和箱号)'):
"""
......@@ -250,514 +207,51 @@ class BatchGetLastMilePodInfoWizard(models.TransientModel):
:param processed_files: 处理后的文件数组
:param fix_name:
"""
for file_info in processed_files:
if not file_info.get('bl'):
_logger.warning("跳过没有提单信息的文件")
continue
bl = file_info['bl']
file_name = file_info.get('file_name', '')
file_data = file_info.get('file_data', '')
if not file_data:
continue
# 如果有文件为空的就回写,否则就创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].search(
[('bl_id', '=', bl.id), ('file_name', '=', fix_name), ('file', '=', False)], limit=1)
if clearance_file:
clearance_file.write({
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"更新清关文件记录: 提单 {bl.bl_no}")
else:
# 创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].create({
'bl_id': bl.id,
'file_name': fix_name,
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"创建新的清关文件记录: 提单 {bl.bl_no}")
file_info['clearance_file'] = clearance_file
common = self.env['common.common'].sudo()
common.write_pod_pdf_files(processed_files, fix_name)
def _merge_pdf_files(self, processed_files):
"""
合并所有涂抹后的PDF文件为一个PDF并保存到pdf_file字段
使用临时文件方式减少内存占用
:param processed_files: 处理后的文件数组
"""
import fitz # PyMuPDF
from datetime import datetime
import tempfile
import os
import gc
temp_file_path = None
try:
# 过滤有效的PDF文件
valid_files = []
for file_info in processed_files:
if file_info.get('bl_no') and file_info.get('file_data'):
valid_files.append(file_info)
if not valid_files:
_logger.warning("没有有效的PDF文件可以合并")
return
# 如果只有一个PDF文件,直接使用,不需要合并
if len(valid_files) == 1:
file_info = valid_files[0]
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
file_name = file_info.get('file_name', f"{bl_no}.pdf")
# 生成文件名(包含提单号和日期)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"POD文件_{bl_no}_{timestamp}.pdf"
# 直接保存到字段
self.write({
'pdf_file': file_data,
'pdf_filename': pdf_filename
})
_logger.info(f"单个PDF文件直接保存: {pdf_filename}")
return
# 多个PDF文件需要合并
_logger.info(f"开始合并 {len(valid_files)} 个PDF文件")
# 使用临时文件方式合并,避免内存占用过大
temp_file_path = tempfile.mktemp(suffix='.pdf')
merged_pdf = fitz.open()
bl_numbers = []
# 遍历所有处理后的PDF文件,分批处理以减少内存占用
batch_size = 5 # 每批处理5个PDF
for batch_start in range(0, len(valid_files), batch_size):
batch_files = valid_files[batch_start:batch_start + batch_size]
_logger.info(f"处理第 {batch_start // batch_size + 1} 批,共 {len(batch_files)} 个PDF")
for file_info in batch_files:
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
bl_numbers.append(bl_no)
source_pdf = None
try:
# 将base64数据转换为二进制
pdf_binary = base64.b64decode(file_data)
# 打开PDF文档
source_pdf = fitz.open(stream=pdf_binary, filetype="pdf")
# 将源PDF的所有页面插入到合并的PDF中
merged_pdf.insert_pdf(source_pdf)
_logger.info(f"已添加提单 {bl_no} 的PDF到合并文档({len(source_pdf)} 页)")
except Exception as e:
_logger.error(f"合并提单 {bl_no} 的PDF失败: {str(e)}")
continue
finally:
# 立即释放资源
if source_pdf:
source_pdf.close()
gc.collect() # 强制垃圾回收
# 每批处理完后,保存到临时文件并释放内存
if batch_start + batch_size < len(valid_files):
# 保存当前合并结果到临时文件
merged_pdf.save(temp_file_path, garbage=4, deflate=True, clean=True)
merged_pdf.close()
# 重新打开临时文件继续合并
merged_pdf = fitz.open(temp_file_path)
gc.collect()
# 如果有页面,保存合并后的PDF
if len(merged_pdf) > 0:
# 使用临时文件保存,减少内存占用
if not temp_file_path:
temp_file_path = tempfile.mktemp(suffix='.pdf')
merged_pdf.save(temp_file_path, garbage=4, deflate=True, clean=True)
merged_pdf.close()
# 从临时文件读取并转换为base64
with open(temp_file_path, 'rb') as f:
pdf_data = f.read()
# 转换为base64
merged_pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
# 清理临时数据
del pdf_data
gc.collect()
# 生成文件名(包含提单号和日期)
bl_numbers_str = '_'.join(bl_numbers[:5]) # 最多显示5个提单号
if len(bl_numbers) > 5:
bl_numbers_str += f'_等{len(bl_numbers)}个'
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"合并POD文件_{bl_numbers_str}_{timestamp}.pdf"
# 保存到字段
common = self.env['common.common'].sudo()
pdf_data, pdf_filename = common.merge_pod_pdfs(processed_files)
if pdf_data and pdf_filename:
self.write({
'pdf_file': merged_pdf_base64,
'pdf_file': pdf_data,
'pdf_filename': pdf_filename
})
# 清理base64数据
del merged_pdf_base64
gc.collect()
_logger.info(f"成功合并 {len(bl_numbers)} 个PDF文件,文件名: {pdf_filename}")
else:
_logger.warning("没有有效的PDF文件可以合并")
except Exception as e:
_logger.error(f"合并PDF文件失败: {str(e)}")
finally:
# 清理临时文件
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
_logger.info(f"已删除临时文件: {temp_file_path}")
except Exception as e:
_logger.warning(f"删除临时文件失败: {str(e)}")
def _match_bl_by_file_name(self, pdf_file_arr, bl_obj):
"""
Match BL by file name and return processed array # 根据文件名匹配提单并返回处理后的数组
:param pdf_file_arr: PDF文件数组 [{'bill_number':'', 'filename':'', 'file_data':''}]
:return: 处理后的数组 [{'bl': bl_obj, 'file_name': 'xxx.pdf', 'file_data': 'xxx', 'matched': True/False}]
"""
processed_files = []
for bl in bl_obj:
select_bl_no = self.env['common.common'].sudo().process_match_str(bl.bl_no)
for pdf_file in pdf_file_arr:
file_name = pdf_file.get('file_name') # 获取文件名
file_data = pdf_file.get('file_data') # 获取文件数据
bl_no = self.env['common.common'].sudo().process_match_str(pdf_file.get('bl_no')) # 获取提单号
if bl_no and select_bl_no == bl_no:
# 构建处理后的文件信息
processed_file = {
'bl': bl,
'file_name': file_name,
'file_data': file_data,
'bl_no': bl.bl_no,
'processing_failed': False,
}
processed_files.append(processed_file)
break
return processed_files
common = self.env['common.common'].sudo()
return common.match_pod_files(pdf_file_arr, bl_obj, include_processing_failed=True)
def _sync_last_mile_pod(self, processed_files):
"""
Sync last mile POD information
:param processed_files: 处理后的文件数组
"""
is_fail = [] # 同步失败
for file_info in processed_files:
if not file_info['bl']:
continue
bl = file_info['bl']
if bl.bl_type == 'temu':
continue
# 查找清关文件并执行同步
clearance_file = file_info.get('clearance_file')
if clearance_file:
try:
clearance_file.action_sync() # 同步尾程交接POD
except Exception as e:
logging.info('_sync_last_mile_pod:%s' % e)
is_fail = True
break
_logger.info(f"Successfully synced POD for BL {bl.bl_no}")
if is_fail:
raise ValidationError('本次同步失败,请重试!')
def get_date_sync_match_node(self, processed_files):
"""
Sync matched node based on POD file, extract time from red boxes # 根据POD文件同步匹配节点
:param processed_files: 处理后的文件数组
"""
ship_packages, pod_node_id = self.get_detail_info(processed_files)
self._sync_match_node(ship_packages, pod_node_id)
def get_detail_info(self, processed_files):
"""
获取提单对应的节点以及时间
:param processed_files: 处理后的文件数组(应该已经包含valid_packages字段,只包含满足条件的小包)
:return: 提单对应的节点以及节点操作时间
"""
ship_packages = []
# 查找对应的清关节点(勾选了POD节点匹配的节点)
pod_node = self.env['cc.node'].search([
('is_pod_node', '=', True),
('node_type', '=', 'package')
], limit=1)
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
if not pod_node:
continue
# 只使用满足条件的小包(经过验证的valid_packages)
valid_packages = file_info.get('valid_packages', [])
if not valid_packages:
_logger.warning(f"提单 {bl.bl_no} 没有满足条件的小包,跳过节点推送")
continue
# 从valid_packages中提取小包ID(记录集对象或列表)
if hasattr(valid_packages, 'ids'):
# 如果是记录集对象,直接获取IDs
valid_package_ids = valid_packages.ids
elif isinstance(valid_packages, list):
# 如果是列表,提取每个对象的ID
valid_package_ids = [p.id for p in valid_packages if hasattr(p, 'id')]
else:
_logger.warning(f"提单 {bl.bl_no} valid_packages格式不正确: {type(valid_packages)}")
valid_package_ids = []
_logger.info(f"提单 {bl.bl_no} 满足条件的小包ID: {valid_package_ids} (共 {len(valid_package_ids)} 个)")
if not valid_package_ids:
_logger.warning(f"提单 {bl.bl_no} 满足条件的小包ID为空,跳过节点推送")
continue
# 从PDF文件提取红色框的时间
file_data = file_info.get('file_data')
if not file_data:
logging.info(f"提单 {bl.bl_no} 没有文件数据")
continue
ship_packages.append({
'bl_id': bl.id,
'id': valid_package_ids, # 只包含满足条件的小包ID
'tally_time': str(file_info.get('tally_time'))
})
return ship_packages, pod_node.id
def _sync_match_node(self, ship_packages, pod_node_id):
"""
同步匹配节点
:param ship_packages: 提单对应的小包以及节点信息
:param pod_node_id: 尾程POD节点匹配的节点ID
"""
# 若该提单里已有对应的小包已有节点推送日志,则不再重新推送;
_logger.info(f"同步匹配节点,提单: {ship_packages}, 节点: {pod_node_id}")
if ship_packages:
bl_objs = self.env['cc.bl'].sudo().search(
[('id', 'in', [ship_package.get('bl_id') for ship_package in ship_packages])])
redis_conn = self.env['common.common'].sudo().get_redis()
if redis_conn and redis_conn != 'no' and pod_node_id:
redis_conn.lpush('mail_push_package_list', json.dumps(
{'ids': bl_objs.ids, 'ship_packages': str(ship_packages), 'action_type': 'push_match_node',
'user_login': self.env.user.login,
'pod_node_id': pod_node_id}))
common = self.env['common.common'].sudo()
common.push_sync_pod_task(
processed_files=processed_files,
file_type='尾程交接POD(待大包数量和箱号)',
pod_desc='尾程POD',
filter_temu=True
)
def _cleanup_temp_attachments(self, bl_objs=None):
"""
清理与当前向导相关的临时附件,包括服务器和本地开发环境的物理文件
"""
try:
attachments = self.env['ir.attachment'].search([
('res_model', '=', bl_objs[0]._name),
('res_id', 'in', bl_objs.ids),
('name', 'like', 'temp_pod_%')
])
if attachments:
# 删除数据库记录
attachments.unlink()
except Exception as e:
_logger.error(f"清理临时附件失败: {str(e)}")
common = self.env['common.common'].sudo()
common.cleanup_temp_attachments(bl_objs)
def _serialize_processed_files(self, processed_files):
"""
将processed_files序列化为JSON字符串,文件数据存储到临时附件中
:param processed_files: 处理后的文件数组
:return: JSON字符串(只包含引用信息,不包含文件数据)
"""
# 注意:不在这里清理临时附件,因为预览时需要保留附件数据
# 只有在确认操作完成后才清理临时附件
serialized_data = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
file_data = file_info.get('file_data', '')
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
# 将文件数据存储到临时附件中
attachment_id = None
if file_data:
try:
attachment = self.env['ir.attachment'].create({
'name': f"temp_pod_{bl.bl_no}_{int(time.time())}.pdf",
'datas': file_data,
'type': 'binary',
'res_model': bl._name,
'res_id': bl.id,
})
attachment_id = attachment.id
_logger.info(f"已创建临时附件存储文件: {attachment.name}, ID: {attachment_id}")
except Exception as e:
_logger.error(f"创建临时附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 的文件数据为空,无法创建附件")
data = {
'bl_id': bl.id,
'bl_no': bl.bl_no,
'file_name': file_name,
'attachment_id': attachment_id, # 存储附件ID而不是文件数据
}
# OCR文本数据量小,可以直接存储
if 'ocr_texts' in file_info:
data['ocr_texts'] = file_info['ocr_texts']
# 保存valid_packages的ID列表(记录集对象无法直接序列化)
if 'valid_packages' in file_info and file_info['valid_packages']:
valid_packages = file_info['valid_packages']
# 如果是记录集对象,提取ID列表
if hasattr(valid_packages, 'ids'):
data['valid_package_ids'] = valid_packages.ids
elif isinstance(valid_packages, list):
# 如果是列表,提取每个对象的ID
data['valid_package_ids'] = [p.id for p in valid_packages if hasattr(p, 'id')]
else:
data['valid_package_ids'] = []
_logger.info(
f"序列化时保存valid_packages: 提单 {bl.bl_no}, 满足条件的小包ID: {data['valid_package_ids']}")
serialized_data.append(data)
return json.dumps(serialized_data, ensure_ascii=False)
common = self.env['common.common'].sudo()
return common.serialize_pod_processed_files(processed_files)
def _deserialize_processed_files(self, json_data):
"""
将JSON字符串反序列化为processed_files(从附件中读取文件数据)
:param json_data: JSON字符串
:return: 处理后的文件数组
"""
if not json_data:
return []
try:
serialized_data = json.loads(json_data)
processed_files = []
for data in serialized_data:
bl_id = data.get('bl_id')
attachment_id = data.get('attachment_id')
if bl_id:
bl = self.env['cc.bl'].browse(bl_id)
if bl.exists():
# 从附件中读取文件数据
file_data = ''
if attachment_id:
try:
attachment = self.env['ir.attachment'].browse(attachment_id)
if attachment.exists():
# attachment.datas 已经是 base64 编码的字符串
file_data = attachment.datas
_logger.info(
f"从附件读取文件: {attachment.name}, ID: {attachment_id}, 数据长度: {len(file_data) if file_data else 0}")
else:
_logger.warning(f"附件不存在: {attachment_id}")
except Exception as e:
_logger.error(f"读取附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 没有附件ID,无法读取文件数据")
file_info = {
'bl': bl,
'bl_no': data.get('bl_no', ''),
'file_name': data.get('file_name', ''),
'file_data': file_data,
}
# 如果有OCR文本,也恢复
if 'ocr_texts' in data:
file_info['ocr_texts'] = data['ocr_texts']
# 恢复valid_packages(从ID列表重建记录集对象)
if 'valid_package_ids' in data and data['valid_package_ids']:
valid_package_ids = data['valid_package_ids']
# 重建记录集对象
valid_packages = self.env['cc.ship.package'].browse(valid_package_ids)
file_info['valid_packages'] = valid_packages
_logger.info(
f"反序列化时恢复valid_packages: 提单 {bl.bl_no}, 满足条件的小包ID: {valid_package_ids}, 数量: {len(valid_packages)}")
processed_files.append(file_info)
return processed_files
except Exception as e:
_logger.error(f"反序列化processed_files失败: {str(e)}")
return []
@api.model
def cron_cleanup_temp_attachments(self):
"""
定时清理向导生成的临时附件
每天早上8点执行,删除1天之前创建的temp_pod_开头的附件
"""
try:
# 计算1天前的时间(前一天23:59:59)
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
one_day_ago = today + timedelta(days=2) - timedelta(seconds=1) # 前一天23:59:59
_logger.info(f"开始执行定时清理临时附件任务,清理时间点: {one_day_ago.strftime('%Y-%m-%d %H:%M:%S')}")
# 构建SQL查询
sql_query = """
SELECT id, name, res_model, res_id, create_date, store_fname
FROM ir_attachment
WHERE res_model = 'batch.get.pod.info.wizard'
AND create_date < '%s'
ORDER BY create_date DESC
""" % (one_day_ago.strftime('%Y-%m-%d %H:%M:%S'))
# 执行SQL查询
self.env.cr.execute(sql_query)
sql_results = self.env.cr.fetchall()
# 将SQL结果转换为Odoo记录集
if sql_results:
attachment_ids = [result[0] for result in sql_results]
temp_attachments = self.env['ir.attachment'].sudo().browse(attachment_ids)
attachment_count = len(temp_attachments)
_logger.info(f"找到 {attachment_count} 个{one_day_ago.strftime('%Y-%m-%d')}之前创建的临时附件,开始清理")
# 删除物理文件
for attachment in temp_attachments:
try:
# 获取附件的物理文件路径
if hasattr(attachment, 'store_fname') and attachment.store_fname:
# Odoo 12+ 使用 store_fname
file_path = attachment.store_fname
elif hasattr(attachment, 'datas_fname') and attachment.datas_fname:
# 旧版本使用 datas_fname
file_path = attachment.datas_fname
else:
# 尝试从 name 字段构建路径
file_path = attachment.name
# 构建完整的文件路径
import os
from odoo.tools import config
# 获取 Odoo 数据目录
data_dir = config.filestore(self.env.cr.dbname)
if data_dir and file_path:
full_path = os.path.join(data_dir, file_path)
if os.path.exists(full_path):
os.remove(full_path)
except Exception as file_e:
_logger.warning(f"删除物理文件失败 {attachment.name}: {str(file_e)}")
# 删除数据库记录
temp_attachments.unlink()
except Exception as e:
_logger.error(f"定时清理临时附件失败: {str(e)}")
common = self.env['common.common'].sudo()
return common.deserialize_pod_processed_files(json_data)
@api.depends()
def _compute_show_sync_last_mile_pod(self):
......
......@@ -6,9 +6,6 @@ import io
import json
import logging
import time
from datetime import datetime, timedelta
import requests
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
......@@ -103,6 +100,11 @@ class BatchGetPodInfoWizard(models.TransientModel):
processed_files_data = fields.Text(string='已处理的文件数据', help='存储已处理的文件信息(JSON格式)')
def _get_bill_numbers(self, bl_objs):
"""
获取提单号
:param bl_objs: 提单记录集
:return: 提单号列表
"""
_logger.info(f"开始预览操作,提单数量: {len(bl_objs)}")
# 调用接口获取提单pdf文件
pdf_file_arr = self._get_pdf_file_arr(bl_objs)
......@@ -349,7 +351,6 @@ class BatchGetPodInfoWizard(models.TransientModel):
if action_type == '获取货站提货POD信息':
if self.sync_last_mile_pod and successful_processed_files:
self._sync_last_mile_pod(successful_processed_files)
# 同步推送匹配节点
if self.sync_match_node and successful_processed_files:
# 且需先对比小包当前节点的操作时间是否小于提取时间(同时区对比)若大于则不能推送,
......@@ -445,72 +446,8 @@ class BatchGetPodInfoWizard(models.TransientModel):
从API获取PDF文件
"""
bill_numbers = [self.env['common.common'].sudo().process_match_str(bl.bl_no) for bl in bl_objs]
# 调用API获取PDF文件
api_url = self.env['ir.config_parameter'].sudo().get_param('last_mile_pod_api_url',
'http://172.104.52.150:7002')
if not api_url:
raise ValidationError(_('API URL not configured'))
# 构建请求数据
request_data = {
"bill_numbers": bill_numbers
}
try:
response = requests.post(
f"{api_url}/api/release-notes/pdfs",
headers={'Content-Type': 'application/json'},
json=request_data
)
if response.status_code == 200:
result = response.json()
# 检查API响应结构
if not result:
raise ValidationError(_('API returned empty response'))
if not result.get('success'):
error_msg = result.get('message', 'Unknown error')
raise ValidationError(_('API returned error: %s') % error_msg)
# 处理结果数据
results = result.get('results', [])
if not results:
raise ValidationError(_('No PDF files found in API response')) # 提示:API调用成功,但没有PDF文件
# 构建PDF文件数组
pdf_file_arr = []
for result_item in results:
if result_item.get('success'):
# 验证必要字段
bill_number = result_item.get('bill_number')
filename = result_item.get('filename')
base64_data = result_item.get('base64')
if not all([bill_number, filename, base64_data]):
_logger.warning(f"跳过无效的PDF文件项: {result_item}")
continue
# 验证PDF文件
try:
pdf_binary = base64.b64decode(base64_data)
# 验证PDF文件头
if not pdf_binary.startswith(b'%PDF-'):
_logger.warning(f"API返回的文件不是有效的PDF格式,提单号: {bill_number}")
continue
pdf_file_arr.append({
'bl_no': bill_number,
'file_name': filename,
'file_data': base64_data
})
except Exception as e:
_logger.warning(f"API PDF文件验证失败,提单号: {bill_number}, 错误: {str(e)}")
continue
return pdf_file_arr
else:
raise ValidationError(_('Failed to get PDF file from API: %s') % response.text)
except requests.exceptions.RequestException as e:
raise ValidationError(_('API request failed: %s') % str(e))
common = self.env['common.common'].sudo()
return common.get_pod_pdf_files(bill_numbers, '/api/release-notes/pdfs')
def _write_pdf_file(self, processed_files, fix_name='货站提货POD'):
"""
......@@ -518,233 +455,43 @@ class BatchGetPodInfoWizard(models.TransientModel):
:param processed_files: 处理后的文件数组
:param fix_name:
"""
for file_info in processed_files:
if not file_info.get('bl'):
_logger.warning("跳过没有提单信息的文件")
continue
bl = file_info['bl']
file_name = file_info.get('file_name', '')
file_data = file_info.get('file_data', '')
if not file_data:
continue
# 如果有文件为空的就回写,否则就创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].search(
[('bl_id', '=', bl.id), ('file_name', '=', fix_name), ('file', '=', False)], limit=1)
if clearance_file:
clearance_file.write({
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"更新清关文件记录: 提单 {bl.bl_no}")
else:
# 创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].create({
'bl_id': bl.id,
'file_name': fix_name,
'attachment_name': file_name,
'file': file_data
})
_logger.info(f"创建新的清关文件记录: 提单 {bl.bl_no}")
file_info['clearance_file'] = clearance_file
common = self.env['common.common'].sudo()
common.write_pod_pdf_files(processed_files, fix_name)
def _merge_pdf_files(self, processed_files):
"""
合并所有涂抹后的PDF文件为一个PDF并保存到pdf_file字段
使用临时文件方式减少内存占用
合并处理后的POD PDF文件
:param processed_files: 处理后的文件数组
"""
import fitz # PyMuPDF
from datetime import datetime
import tempfile
import os
import gc
temp_file_path = None
try:
# 过滤有效的PDF文件
valid_files = []
for file_info in processed_files:
if file_info.get('bl_no') and file_info.get('file_data'):
valid_files.append(file_info)
if not valid_files:
_logger.warning("没有有效的PDF文件可以合并")
return
# 如果只有一个PDF文件,直接使用,不需要合并
if len(valid_files) == 1:
file_info = valid_files[0]
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
file_name = file_info.get('file_name', f"{bl_no}.pdf")
# 生成文件名(包含提单号和日期)
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"POD文件_{bl_no}_{timestamp}.pdf"
# 直接保存到字段
common = self.env['common.common'].sudo()
pdf_data, pdf_filename = common.merge_pod_pdfs(processed_files)
if pdf_data and pdf_filename:
self.write({
'pdf_file': file_data,
'pdf_file': pdf_data,
'pdf_filename': pdf_filename
})
_logger.info(f"单个PDF文件直接保存: {pdf_filename}")
return
# 多个PDF文件需要合并
_logger.info(f"开始合并 {len(valid_files)} 个PDF文件")
# 使用临时文件方式合并,避免内存占用过大
temp_file_path = tempfile.mktemp(suffix='.pdf')
merged_pdf = fitz.open()
bl_numbers = []
# 遍历所有处理后的PDF文件,分批处理以减少内存占用
batch_size = 5 # 每批处理5个PDF
for batch_start in range(0, len(valid_files), batch_size):
batch_files = valid_files[batch_start:batch_start + batch_size]
_logger.info(f"处理第 {batch_start // batch_size + 1} 批,共 {len(batch_files)} 个PDF")
for file_info in batch_files:
bl = file_info['bl']
bl_no = bl.bl_no
file_data = file_info['file_data']
bl_numbers.append(bl_no)
source_pdf = None
try:
# 将base64数据转换为二进制
pdf_binary = base64.b64decode(file_data)
# 打开PDF文档
source_pdf = fitz.open(stream=pdf_binary, filetype="pdf")
# 将源PDF的所有页面插入到合并的PDF中
merged_pdf.insert_pdf(source_pdf)
_logger.info(f"已添加提单 {bl_no} 的PDF到合并文档({len(source_pdf)} 页)")
except Exception as e:
_logger.error(f"合并提单 {bl_no} 的PDF失败: {str(e)}")
continue
finally:
# 立即释放资源
if source_pdf:
source_pdf.close()
gc.collect() # 强制垃圾回收
# 每批处理完后,保存到临时文件并释放内存
if batch_start + batch_size < len(valid_files):
# 保存当前合并结果到临时文件
merged_pdf.save(temp_file_path, garbage=4, deflate=True, clean=True)
merged_pdf.close()
# 重新打开临时文件继续合并
merged_pdf = fitz.open(temp_file_path)
gc.collect()
# 如果有页面,保存合并后的PDF
if len(merged_pdf) > 0:
# 使用临时文件保存,减少内存占用
if not temp_file_path:
temp_file_path = tempfile.mktemp(suffix='.pdf')
merged_pdf.save(temp_file_path, garbage=4, deflate=True, clean=True)
merged_pdf.close()
# 从临时文件读取并转换为base64
with open(temp_file_path, 'rb') as f:
pdf_data = f.read()
# 转换为base64
merged_pdf_base64 = base64.b64encode(pdf_data).decode('utf-8')
# 清理临时数据
del pdf_data
gc.collect()
# 生成文件名(包含提单号和日期)
bl_numbers_str = '_'.join(bl_numbers[:5]) # 最多显示5个提单号
if len(bl_numbers) > 5:
bl_numbers_str += f'_等{len(bl_numbers)}个'
timestamp = datetime.now().strftime('%Y%m%d_%H%M%S')
pdf_filename = f"合并POD文件_{bl_numbers_str}_{timestamp}.pdf"
# 保存到字段
self.write({
'pdf_file': merged_pdf_base64,
'pdf_filename': pdf_filename
})
# 清理base64数据
del merged_pdf_base64
gc.collect()
_logger.info(f"成功合并 {len(bl_numbers)} 个PDF文件,文件名: {pdf_filename}")
else:
_logger.warning("没有有效的PDF文件可以合并")
except Exception as e:
_logger.error(f"合并PDF文件失败: {str(e)}")
finally:
# 清理临时文件
if temp_file_path and os.path.exists(temp_file_path):
try:
os.remove(temp_file_path)
_logger.info(f"已删除临时文件: {temp_file_path}")
except Exception as e:
_logger.warning(f"删除临时文件失败: {str(e)}")
def _match_bl_by_file_name(self, pdf_file_arr, bl_obj):
"""
Match BL by file name and return processed array # 根据文件名匹配提单并返回处理后的数组
:param pdf_file_arr: PDF文件数组 [{'bill_number':'', 'filename':'', 'file_data':''}]
:return: 处理后的数组 [{'bl': bl_obj, 'file_name': 'xxx.pdf', 'file_data': 'xxx', 'matched': True/False}]
"""
processed_files = []
for bl in bl_obj:
select_bl_no = self.env['common.common'].sudo().process_match_str(bl.bl_no)
for pdf_file in pdf_file_arr:
# 尝试不同的字段名(API可能使用不同的字段名)
file_name = pdf_file.get('file_name') # 获取文件名
file_data = pdf_file.get('file_data') # 获取文件数据
bl_no = pdf_file.get('bl_no') # 获取提单号
if bl_no and select_bl_no == bl_no:
# 构建处理后的文件信息
processed_file = {
'bl': bl,
'file_name': file_name,
'file_data': file_data,
'bl_no': bl_no,
}
processed_files.append(processed_file)
break
return processed_files
common = self.env['common.common'].sudo()
return common.match_pod_files(pdf_file_arr, bl_obj, include_processing_failed=False)
def _sync_last_mile_pod(self, processed_files):
"""
Sync pickup POD information # 同步货站提货POD信息
:param processed_files: 处理后的文件数组
"""
# return False#测试 先不同步
# 同步货站提货POD信息
is_fail = [] # 同步失败
for file_info in processed_files:
if not file_info['bl']:
continue
bl = file_info['bl']
# 查找清关文件并执行同步
clearance_file = file_info.get('clearance_file')
if clearance_file:
try:
clearance_file.action_sync() # 同步货站提货POD
except Exception as e:
logging.info('_sync_last_mile_pod:%s' % e)
is_fail = True
break
_logger.info(f"Successfully synced POD for BL {bl.bl_no}")
if is_fail:
raise ValidationError('本次同步失败,请重试!')
common = self.env['common.common'].sudo()
common.push_sync_pod_task(
processed_files=processed_files,
file_type='货站提货POD',
pod_desc='货站提货POD',
filter_temu=False
)
def _check_target_texts_exist(self, pdf_binary, bl_no):
"""
......@@ -2362,200 +2109,14 @@ class BatchGetPodInfoWizard(models.TransientModel):
return False, []
def _cleanup_temp_attachments(self, bl_objs=None):
"""
清理与当前向导相关的临时附件,包括服务器和本地开发环境的物理文件
"""
try:
attachments = self.env['ir.attachment'].search([
('res_model', '=', bl_objs[0]._name),
('res_id', 'in', bl_objs.ids),
('name', 'like', 'temp_pod_%')
])
if attachments:
# 删除数据库记录
attachments.unlink()
except Exception as e:
_logger.error(f"清理临时附件失败: {str(e)}")
common = self.env['common.common'].sudo()
common.cleanup_temp_attachments(bl_objs)
def _serialize_processed_files(self, processed_files):
"""
将processed_files序列化为JSON字符串,文件数据存储到临时附件中
:param processed_files: 处理后的文件数组
:return: JSON字符串(只包含引用信息,不包含文件数据)
"""
# 注意:不在这里清理临时附件,因为预览时需要保留附件数据
# 只有在确认操作完成后才清理临时附件
serialized_data = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
file_data = file_info.get('file_data', '')
file_name = file_info.get('file_name', f"{bl.bl_no}.pdf")
# 将文件数据存储到临时附件中
attachment_id = None
if file_data:
try:
attachment = self.env['ir.attachment'].create({
'name': f"temp_pod_{bl.bl_no}_{int(time.time())}.pdf",
'datas': file_data,
'type': 'binary',
'res_model': bl._name,
'res_id': bl.id,
})
attachment_id = attachment.id
_logger.info(f"已创建临时附件存储文件: {attachment.name}, ID: {attachment_id}")
except Exception as e:
_logger.error(f"创建临时附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 的文件数据为空,无法创建附件")
data = {
'bl_id': bl.id,
'bl_no': bl.bl_no,
'file_name': file_name,
'attachment_id': attachment_id, # 存储附件ID而不是文件数据
}
# OCR文本数据量小,可以直接存储
if 'ocr_texts' in file_info:
data['ocr_texts'] = file_info['ocr_texts']
# 保存valid_packages的ID列表(记录集对象无法直接序列化)
if 'valid_packages' in file_info and file_info['valid_packages']:
valid_packages = file_info['valid_packages']
# 如果是记录集对象,提取ID列表
if hasattr(valid_packages, 'ids'):
data['valid_package_ids'] = valid_packages.ids
elif isinstance(valid_packages, list):
# 如果是列表,提取每个对象的ID
data['valid_package_ids'] = [p.id for p in valid_packages if hasattr(p, 'id')]
else:
data['valid_package_ids'] = []
_logger.info(
f"序列化时保存valid_packages: 提单 {bl.bl_no}, 满足条件的小包ID: {data['valid_package_ids']}")
serialized_data.append(data)
return json.dumps(serialized_data, ensure_ascii=False)
common = self.env['common.common'].sudo()
return common.serialize_pod_processed_files(processed_files)
def _deserialize_processed_files(self, json_data):
"""
将JSON字符串反序列化为processed_files(从附件中读取文件数据)
:param json_data: JSON字符串
:return: 处理后的文件数组
"""
if not json_data:
return []
try:
serialized_data = json.loads(json_data)
processed_files = []
for data in serialized_data:
bl_id = data.get('bl_id')
attachment_id = data.get('attachment_id')
if bl_id:
bl = self.env['cc.bl'].browse(bl_id)
if bl.exists():
# 从附件中读取文件数据
file_data = ''
if attachment_id:
try:
attachment = self.env['ir.attachment'].browse(attachment_id)
if attachment.exists():
# attachment.datas 已经是 base64 编码的字符串
file_data = attachment.datas
_logger.info(
f"从附件读取文件: {attachment.name}, ID: {attachment_id}, 数据长度: {len(file_data) if file_data else 0}")
else:
_logger.warning(f"附件不存在: {attachment_id}")
except Exception as e:
_logger.error(f"读取附件失败: {str(e)}")
else:
_logger.warning(f"提单 {bl.bl_no} 没有附件ID,无法读取文件数据")
file_info = {
'bl': bl,
'bl_no': data.get('bl_no', ''),
'file_name': data.get('file_name', ''),
'file_data': file_data,
}
# 如果有OCR文本,也恢复
if 'ocr_texts' in data:
file_info['ocr_texts'] = data['ocr_texts']
# 恢复valid_packages(从ID列表重建记录集对象)
if 'valid_package_ids' in data and data['valid_package_ids']:
valid_package_ids = data['valid_package_ids']
# 重建记录集对象
valid_packages = self.env['cc.ship.package'].browse(valid_package_ids)
file_info['valid_packages'] = valid_packages
_logger.info(
f"反序列化时恢复valid_packages: 提单 {bl.bl_no}, 满足条件的小包ID: {valid_package_ids}, 数量: {len(valid_packages)}")
processed_files.append(file_info)
return processed_files
except Exception as e:
_logger.error(f"反序列化processed_files失败: {str(e)}")
return []
common = self.env['common.common'].sudo()
return common.deserialize_pod_processed_files(json_data)
@api.model
def cron_cleanup_temp_attachments(self):
"""
定时清理向导生成的临时附件
每天早上8点执行,删除1天之前创建的temp_pod_开头的附件
"""
try:
# 计算1天前的时间(前一天23:59:59)
today = datetime.now().replace(hour=0, minute=0, second=0, microsecond=0)
one_day_ago = today + timedelta(days=2) - timedelta(seconds=1) # 前一天23:59:59
_logger.info(f"开始执行定时清理临时附件任务,清理时间点: {one_day_ago.strftime('%Y-%m-%d %H:%M:%S')}")
# 构建SQL查询
sql_query = """
SELECT id, name, res_model, res_id, create_date, store_fname
FROM ir_attachment
WHERE res_model = 'batch.get.pod.info.wizard'
AND create_date < '%s'
ORDER BY create_date DESC
""" % (one_day_ago.strftime('%Y-%m-%d %H:%M:%S'))
# 执行SQL查询
self.env.cr.execute(sql_query)
sql_results = self.env.cr.fetchall()
# 将SQL结果转换为Odoo记录集
if sql_results:
attachment_ids = [result[0] for result in sql_results]
temp_attachments = self.env['ir.attachment'].sudo().browse(attachment_ids)
attachment_count = len(temp_attachments)
_logger.info(f"找到 {attachment_count} 个{one_day_ago.strftime('%Y-%m-%d')}之前创建的临时附件,开始清理")
# 删除物理文件
for attachment in temp_attachments:
try:
# 获取附件的物理文件路径
if hasattr(attachment, 'store_fname') and attachment.store_fname:
# Odoo 12+ 使用 store_fname
file_path = attachment.store_fname
elif hasattr(attachment, 'datas_fname') and attachment.datas_fname:
# 旧版本使用 datas_fname
file_path = attachment.datas_fname
else:
# 尝试从 name 字段构建路径
file_path = attachment.name
# 构建完整的文件路径
import os
from odoo.tools import config
# 获取 Odoo 数据目录
data_dir = config.filestore(self.env.cr.dbname)
if data_dir and file_path:
full_path = os.path.join(data_dir, file_path)
if os.path.exists(full_path):
os.remove(full_path)
except Exception as file_e:
_logger.warning(f"删除物理文件失败 {attachment.name}: {str(file_e)}")
# 删除数据库记录
temp_attachments.unlink()
except Exception as e:
_logger.error(f"定时清理临时附件失败: {str(e)}")
......@@ -31,8 +31,11 @@ class Order_dispose(object):
try:
data = json.loads(data)
logging.info('mail_push_data: %s', data)
action_type = data.get('action_type')
if action_type == 'sync_last_mile_pod':
self._sync_last_mile_pod_from_queue(data)
return res_data
ship_packages = eval(data['ship_packages']) if data.get('ship_packages') else [] # 小包
action_type = data.get('action_type') # 类型
utc_time = data.get('utc_time')
bl_obj = self.odoo_db.env['cc.bl']
if action_type and not utc_time:
......@@ -52,6 +55,33 @@ class Order_dispose(object):
logging.error('mail_auto_push error:%s' % str(ex))
return res_data
def _sync_last_mile_pod_from_queue(self, data):
bl_ids = data.get('ids') or []
if not bl_ids:
return
try:
bl_model = self.odoo_db.env['cc.bl']
clearance_model = self.odoo_db.env['cc.clearance.file']
bl_records = bl_model.browse(bl_ids)
non_temu_ids = [bl.id for bl in bl_records if getattr(bl, 'bl_type', False) != 'temu']
if not non_temu_ids:
return
clearance_ids = clearance_model.search([
('bl_id', 'in', non_temu_ids),
('file_name', '=', data.get('file_type')),
])
logging.info("已查询到需要同步的清关文件记录: %s", clearance_ids)
if not clearance_ids:
return
clearance_records = clearance_model.browse(clearance_ids)
for clearance_file in clearance_records:
try:
clearance_file.action_sync()
except Exception as ex:
logging.error('sync_last_mile_pod action_sync error:%s' % str(ex))
except Exception as ex:
logging.error('sync_last_mile_pod_from_queue error:%s' % str(ex))
try:
pool = redis.ConnectionPool(**config.redis_options)
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论