Skip to content
项目
群组
代码片段
帮助
当前项目
正在载入...
登录 / 注册
切换导航面板
H
hh_ccs
项目
项目
详情
活动
周期分析
仓库
仓库
文件
提交
分支
标签
贡献者
图表
比较
统计图
议题
0
议题
0
列表
看板
标记
里程碑
合并请求
0
合并请求
0
CI / CD
CI / CD
流水线
作业
日程
统计图
Wiki
Wiki
代码片段
代码片段
成员
成员
折叠边栏
关闭边栏
活动
图像
聊天
创建新问题
作业
提交
问题看板
Open sidebar
贺阳
hh_ccs
Commits
0c22b124
提交
0c22b124
authored
1月 15, 2026
作者:
贺阳
浏览文件
操作
浏览文件
下载
电子邮件补丁
差异文件
批量获取尾程pod和获取货站pod的优化
上级
8f37205a
显示空白字符变更
内嵌
并排
正在显示
5 个修改的文件
包含
499 行增加
和
938 行删除
+499
-938
timer.xml
ccs_base/data/timer.xml
+1
-1
common_common.py
ccs_base/models/common_common.py
+445
-2
batch_get_lastmile_pod_info_wizard.py
ccs_base/wizard/batch_get_lastmile_pod_info_wizard.py
+23
-458
batch_get_pod_info_wizard.py
ccs_base/wizard/batch_get_pod_info_wizard.py
+29
-476
mail_push.py
consumers/mail_push.py
+1
-1
没有找到文件。
ccs_base/data/timer.xml
浏览文件 @
0c22b124
...
...
@@ -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>
...
...
ccs_base/models/common_common.py
浏览文件 @
0c22b124
# -*- 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,442 @@ 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
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
)
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
...
...
ccs_base/wizard/batch_get_lastmile_pod_info_wizard.py
浏览文件 @
0c22b124
...
...
@@ -198,59 +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
)
logging
.
info
(
'response:
%
s'
%
response
)
if
response
.
status_code
==
200
:
result
=
response
.
json
()
logging
.
info
(
'result:
%
s'
%
result
)
# 检查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(待大包数量和箱号)'
):
"""
...
...
@@ -258,435 +207,51 @@ class BatchGetLastMilePodInfoWizard(models.TransientModel):
: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
})
_logger
.
info
(
f
"更新清关文件记录: 提单 {bl.bl_no}"
)
file_info
[
'clearance_file'
]
=
clearance_file
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
)
for
clearance_file
,
file_info
in
zip
(
new_records
,
create_infos
):
bl
=
file_info
[
'bl'
]
_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
_logger
.
info
(
f
"开始合并 {len(valid_files)} 个PDF文件"
)
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
(
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
len
(
merged_pdf
)
>
0
:
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: 处理后的文件数组
"""
redis_conn
=
self
.
env
[
'common.common'
]
.
sudo
()
.
get_redis
()
if
not
redis_conn
or
redis_conn
==
'no'
:
raise
ValidationError
(
'未连接redis,无法同步尾程POD,请联系管理员'
)
bl_ids
=
[]
for
file_info
in
processed_files
:
bl
=
file_info
.
get
(
'bl'
)
if
not
bl
:
continue
if
bl
.
bl_type
==
'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'
:
'尾程交接POD(待大包数量和箱号)'
}
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
(
'推送尾程POD同步任务到redis失败,请重试或联系管理员'
)
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
):
...
...
ccs_base/wizard/batch_get_pod_info_wizard.py
浏览文件 @
0c22b124
...
...
@@ -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
)
...
...
@@ -444,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'
):
"""
...
...
@@ -517,242 +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"
# 直接保存到字段
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
:
# 尝试不同的字段名(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: 处理后的文件数组
"""
redis_conn
=
self
.
env
[
'common.common'
]
.
sudo
()
.
get_redis
()
if
not
redis_conn
or
redis_conn
==
'no'
:
raise
ValidationError
(
'未连接redis,无法同步货站提货POD,请联系管理员'
)
bl_ids
=
[]
for
file_info
in
processed_files
:
bl
=
file_info
.
get
(
'bl'
)
if
not
bl
:
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'
:
'货站提货POD'
}
try
:
redis_conn
.
lpush
(
'mail_push_package_list'
,
json
.
dumps
(
payload
))
except
Exception
as
e
:
logging
.
info
(
'sync_last_mile_pod redis error:
%
s'
%
e
)
raise
ValidationError
(
'推送货站提货POD同步任务到redis失败,请重试或联系管理员'
)
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
):
"""
...
...
@@ -2370,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)}"
)
consumers/mail_push.py
浏览文件 @
0c22b124
...
...
@@ -67,7 +67,7 @@ class Order_dispose(object):
if
not
non_temu_ids
:
return
clearance_ids
=
clearance_model
.
search
([
(
'bl_id'
,
'in'
,
non_temu_ids
),
(
'bl_id'
,
'in'
,
non_temu_ids
),
(
'is_upload'
,
'='
,
False
),
(
'file_name'
,
'='
,
data
.
get
(
'file_type'
)),
])
if
not
clearance_ids
:
...
...
编写
预览
Markdown
格式
0%
重试
或
添加新文件
添加附件
取消
您添加了
0
人
到此讨论。请谨慎行事。
请先完成此评论的编辑!
取消
请
注册
或者
登录
后发表评论