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

初始化代码

上级 4fb10d3c
...@@ -25,6 +25,7 @@ ...@@ -25,6 +25,7 @@
'wizard/add_exception_info_wizard_views.xml', 'wizard/add_exception_info_wizard_views.xml',
'wizard/email_template.xml', 'wizard/email_template.xml',
'wizard/bl_done_wizard_views.xml', 'wizard/bl_done_wizard_views.xml',
'wizard/batch_get_pod_info_wizard_views.xml',
'data/data.xml', 'data/data.xml',
'data/timer.xml', 'data/timer.xml',
'data/sequence.xml', 'data/sequence.xml',
......
...@@ -40,5 +40,11 @@ ...@@ -40,5 +40,11 @@
<field name="value">mjgUUgbxXK8UHcRi5MTlPrb4BWM8NrOu</field> <field name="value">mjgUUgbxXK8UHcRi5MTlPrb4BWM8NrOu</field>
</record> </record>
<record id="last_mile_pod_api_url" model="ir.config_parameter">
<field name="key">last_mile_pod_api_url</field>
<field name="value">http://172.104.52.150:7002</field>
</record>
</data> </data>
</odoo> </odoo>
\ No newline at end of file
...@@ -16,3 +16,5 @@ from . import cc_history_package_good ...@@ -16,3 +16,5 @@ from . import cc_history_package_good
from . import cc_history_ship_package from . import cc_history_ship_package
from . import cc_history_package_sync_log from . import cc_history_package_sync_log
from . import history_tt_api_log from . import history_tt_api_log
...@@ -554,6 +554,9 @@ class CcClearanceFile(models.Model): ...@@ -554,6 +554,9 @@ class CcClearanceFile(models.Model):
def action_sync(self): def action_sync(self):
pass pass
def search_clearance_file(self, bl_id, file_name):
"""搜索清关文件"""
return self.env['cc.clearance.file'].search([('bl_id','=',bl_id),('file_name','=',file_name)],limit=1)
# 创建一个业务对象,继承自models.Model, 用于管理业务数据.业务数据包括提单号、提单日期、提单总件数、提单总金额、所属客户、提单明细、清关进度明细、状态[待确认、清关中、已完成] # 创建一个业务对象,继承自models.Model, 用于管理业务数据.业务数据包括提单号、提单日期、提单总件数、提单总金额、所属客户、提单明细、清关进度明细、状态[待确认、清关中、已完成]
class CcBL(models.Model): class CcBL(models.Model):
...@@ -1089,6 +1092,16 @@ class CcBL(models.Model): ...@@ -1089,6 +1092,16 @@ class CcBL(models.Model):
'default_current_status': customs_clearance_status_list[0]} 'default_current_status': customs_clearance_status_list[0]}
} }
def action_batch_get_pod_info(self):
"""批量获取尾程POD信息"""
return {
'name': _('Batch Get POD Info'),
'type': 'ir.actions.act_window',
'view_mode': 'form',
'res_model': 'batch.get.pod.info.wizard',
'target': 'new',
}
# 增加一个清关进度的业务对象,继承自models.Model, 用于管理业务数据.业务数据包括提单号、清关节点(业务对象)、进度日期、进度描述、更新人 # 增加一个清关进度的业务对象,继承自models.Model, 用于管理业务数据.业务数据包括提单号、清关节点(业务对象)、进度日期、进度描述、更新人
class CcProgress(models.Model): class CcProgress(models.Model):
......
...@@ -6,8 +6,7 @@ add_exception_info_wizard_group_user,add_exception_info_wizard_group_user,ccs_ba ...@@ -6,8 +6,7 @@ add_exception_info_wizard_group_user,add_exception_info_wizard_group_user,ccs_ba
update_bl_status_wizard_group_user,update_bl_status_wizard_group_user,ccs_base.model_update_bl_status_wizard,base.group_user,1,1,1,1 update_bl_status_wizard_group_user,update_bl_status_wizard_group_user,ccs_base.model_update_bl_status_wizard,base.group_user,1,1,1,1
batch_update_transfer_bl_no_wizard_group_user,batch_update_transfer_bl_no_wizard_group_user,ccs_base.model_batch_update_transfer_bl_no_wizard,base.group_user,1,1,1,1 batch_update_transfer_bl_no_wizard_group_user,batch_update_transfer_bl_no_wizard_group_user,ccs_base.model_batch_update_transfer_bl_no_wizard,base.group_user,1,1,1,1
bl_done_wizard_group_user,bl_done_wizard_group_user,ccs_base.model_bl_done_wizard,base.group_user,1,1,1,1 bl_done_wizard_group_user,bl_done_wizard_group_user,ccs_base.model_bl_done_wizard,base.group_user,1,1,1,1
batch_get_pod_info_wizard_group_user,batch_get_pod_info_wizard_group_user,ccs_base.model_batch_get_pod_info_wizard,base.group_user,1,1,1,1
access_group_user_common_common,access_group_user_common_common,model_common_common,base.group_user,1,1,1,1 access_group_user_common_common,access_group_user_common_common,model_common_common,base.group_user,1,1,1,1
......
...@@ -465,4 +465,18 @@ ...@@ -465,4 +465,18 @@
</field> </field>
</record> </record>
<!-- 获取尾程POD -->
<record id="bl_get_pod_info_server_action" model="ir.actions.server">
<field name="name">Batch Get 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>
<field name="binding_view_types">list</field>
<field name="groups_id" eval="[(4, ref('ccs_base.group_clearance_of_customs_user'))]"/>
<field name="code">
if records:
action = records.action_batch_get_pod_info()
</field>
</record>
</odoo> </odoo>
\ No newline at end of file
...@@ -7,4 +7,5 @@ from . import add_exception_info_wizard ...@@ -7,4 +7,5 @@ from . import add_exception_info_wizard
from . import update_bl_status_wizard from . import update_bl_status_wizard
from . import batch_update_transfer_bl_no_wizard from . import batch_update_transfer_bl_no_wizard
from . import bl_done_wizard from . import bl_done_wizard
from . import batch_get_pod_info_wizard
# -*- coding: utf-8 -*-
# License AGPL-3.0 or later (http://www.gnu.org/licenses/agpl.html).
import logging
import requests
from odoo import models, fields, _
from odoo.exceptions import ValidationError
_logger = logging.getLogger(__name__)
class BatchGetPodInfoWizard(models.TransientModel):
_name = 'batch.get.pod.info.wizard'
_description = 'Batch Get POD Info Wizard' # 批量获取POD信息向导
def get_order(self):
"""
得到单据
:return:
"""
order_id = self._context.get('active_id')
if type(order_id) != list:
order_id = [self._context.get('active_id')]
return self.env['cc.bl'].browse(order_id)
sync_last_mile_pod = fields.Boolean(
string='Sync Last Mile POD', # 同步尾程POD
default=True,
help='Whether to sync last mile POD information' # 是否同步尾程POD信息
)
remove_specified_text = fields.Boolean(
string='Remove Specified Text', # 涂抹指定文字
default=True,
help='Whether to remove specified text from PDF files' # 是否涂抹PDF中的指定文字
)
def confirm(self):
"""
Confirm operation # 确认操作
"""
try:
bl_objs = self.get_order()
# 调用接口获取提单pdf文件
pdf_file_arr = self._get_pdf_file_arr()
# 处理PDF文件,匹配提单对象
processed_files = self._match_bl_by_file_name(pdf_file_arr)
# 把没有匹配到文件的进行提示
error_bl = []
for bl in bl_objs:
if not bl in [f['bl'] for f in processed_files]:
error_bl.append(bl)
if error_bl:
# 英文提示
raise ValidationError(_('%s bill of loading cannot find release note file') % (
', '.join([bl.bl_no for bl in error_bl]))) # xx提单无法找到release note文件
# 先涂抹指定文字
if self.remove_specified_text:
processed_files = self._remove_specified_text(processed_files)
# 再同步和回写
if self.sync_last_mile_pod:
self._sync_last_mile_pod(processed_files)
# 显示成功消息
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': _('Operation Completed'), # 操作完成
'message': _('Successfully processed %d PDF files for %d bill of loadings') % (len(processed_files), len(bl_objs)), # 成功处理了%d个PDF文件,涉及%d个提单
'type': 'success',
}
}
except Exception as e:
raise ValidationError(_('Operation failed: %s') % str(e)) # 操作失败
# 写一个方法掉接口获取提单pdf文件
def _get_pdf_file_arr(self):
"""
Get PDF file # 获取PDF文件
"""
# 调用接口,接口返回数组[{'bl_no':'','file_name':'','file_data':''}]
# bl_no:提单号
# file_name:文件名
# file_data:文件数据
return [{
'bl_no': '436-10259804',
'file_name': '合并提单_436-10259804_20251008.pdf',
'file_data': 'base64_data'
}]
api_url = self.env['ir.config_parameter'].sudo().get_param('ccs_base.last_mile_pod_api_url')
response = requests.get(api_url + '/get_pdf_file')
if response.status_code == 200:
return response.json()
else:
raise ValidationError(_('Failed to get PDF file: %s') % response.text)
def _write_pdf_file(self, processed_files):
"""
Write PDF file to clearance files # 回写PDF文件到清关文件
:param processed_files: 处理后的文件数组
"""
for file_info in processed_files:
if not file_info['bl']:
continue
bl = file_info['bl']
file_name = file_info['file_name']
file_data = file_info['file_data']
try:
# 查找或创建清关文件记录
clearance_file = self.env['cc.clearance.file'].sudo().search_clearance_file(bl.id,
'尾程交接POD(待大包数量和箱号)')
if not clearance_file:
# 创建新的清关文件记录
clearance_file = self.env['cc.clearance.file'].create({
'bl_id': bl.id,
'file_name': '尾程交接POD(待大包数量和箱号)',
'attachment_name': file_name,
'file': file_data
})
else:
# 更新现有记录
clearance_file.write({
'attachment_name': file_name,
'file': file_data
})
except Exception as e:
raise ValidationError(_('Failed to write PDF file %s: %s') % (file_name, str(e)))
def _match_bl_by_file_name(self, pdf_file_arr):
"""
Match BL by file name and return processed array # 根据文件名匹配提单并返回处理后的数组
:param pdf_file_arr: PDF文件数组 [{'bl_no':'', 'file_name':'', 'file_data':''}]
:return: 处理后的数组 [{'bl': bl_obj, 'file_name': 'xxx.pdf', 'file_data': 'xxx', 'matched': True/False}]
"""
bl_obj = self.get_order() # 获取当前选中的提单对象
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 = pdf_file.get('bl_no', '') # 获取提单号
if not bl_no:
# 从文件名获取提单号 合并提单_436-10259804_20251008.pdf
split_bl_no = file_name.split('_')[1]
bl_no = self.env['common.common'].sudo().process_match_str(split_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,
'original_data': pdf_file # 保留原始数据
}
processed_files.append(processed_file)
break
return processed_files
def _sync_last_mile_pod(self, processed_files):
"""
Sync last mile POD information # 同步尾程POD信息
:param processed_files: 处理后的文件数组
"""
# 回写PDF文件到清关文件
self._write_pdf_file(processed_files)
# 同步尾程POD信息
for file_info in processed_files:
if not file_info['bl']:
continue
bl = file_info['bl']
try:
# 查找清关文件并执行同步
clearance_files = self.env['cc.clearance.file'].sudo().search_clearance_file(bl.id,
'尾程交接POD(待大包数量和箱号)')
for clearance_file in clearance_files:
clearance_file.action_sync() # 同步尾程POD
_logger.info(f"Successfully synced POD for BL {bl.bl_no}")
except Exception as e:
_logger.error(f"Failed to sync POD for BL {bl.bl_no}: {str(e)}")
raise ValidationError(_('Failed to sync POD for BL %s: %s') % (bl.bl_no, str(e)))
def _remove_specified_text(self, processed_files):
"""
Remove specified text from PDF files using OCR recognition # 使用OCR识别涂抹指定文字
:param processed_files: 处理后的文件数组
:return: 处理后的文件数组(包含处理后的PDF数据)
"""
updated_files = []
for file_info in processed_files:
if not file_info['bl']:
updated_files.append(file_info)
continue
bl = file_info['bl']
file_data = file_info['file_data']
processed_file_data = file_data # 默认使用原始数据
try:
# 使用OCR识别和删除指定文字
if file_data and file_data != 'base64_data': # 跳过测试数据
# 将base64数据转换为二进制
import base64
pdf_binary = base64.b64decode(file_data)
# 使用OCR方法处理PDF
processed_pdf = self._process_pdf_with_ocr(
pdf_data=pdf_binary,
bl_no=bl.bl_no
)
# 将处理后的PDF转换回base64
processed_file_data = base64.b64encode(processed_pdf)
_logger.info(f"Successfully removed specified text from PDF for BL {bl.bl_no}")
except Exception as e:
_logger.error(f"Failed to remove text from PDF for BL {bl.bl_no}: {str(e)}")
raise ValidationError(_('Failed to remove text from PDF for BL %s: %s') % (bl.bl_no, str(e)))
# 更新文件信息,使用处理后的PDF数据
updated_file_info = file_info.copy()
updated_file_info['file_data'] = processed_file_data
updated_files.append(updated_file_info)
return updated_files
def _process_pdf_with_ocr(self, pdf_data, bl_no):
"""
Process PDF with OCR recognition and text removal # 使用OCR识别处理PDF并删除文字
:param pdf_data: PDF二进制数据
:param bl_no: 提单号(用于日志)
:return: 处理后的PDF二进制数据
"""
import fitz # PyMuPDF
import cv2
import numpy as np
from PIL import Image
import pytesseract
import base64
import io
# 定义目标文字和排除文字(与HTML文件保持一致)
TARGET_TEXTS = ['AGN', 'UCLINK LOGISITICS LTD', 'UCLINK LOGISITICS', 'UCLINK', 'LOGISITICS', 'LOGISTICS', 'LTD']
EXCLUDE_TEXTS = ['AIR EQK', 'ARN', 'EQK', 'AIR', 'Page 1 of 1', 'Page 2 of 2', 'Page 3 of 3', 'Page 4 of 4', 'Page 5 of 5']
# 打开PDF文档
pdf_document = fitz.open(stream=pdf_data, filetype="pdf")
total_rectangles = 0
processed_pages = 0
detected_texts = []
all_recognized_texts = []
_logger.info(f"开始OCR处理PDF,共{len(pdf_document)}页,提单号: {bl_no}")
# 处理每一页
for page_num in range(len(pdf_document)):
page = pdf_document[page_num]
_logger.info(f"正在OCR识别第{page_num + 1}页")
try:
# 将页面转换为图像(提高分辨率,与HTML文件保持一致)
mat = fitz.Matrix(2.0, 2.0) # 提高分辨率
pix = page.get_pixmap(matrix=mat)
img_data = pix.tobytes("png")
# 转换为OpenCV格式
nparr = np.frombuffer(img_data, np.uint8)
img = cv2.imdecode(nparr, cv2.IMREAD_COLOR)
# 转换为PIL图像
pil_img = Image.fromarray(cv2.cvtColor(img, cv2.COLOR_BGR2RGB))
# 使用Tesseract进行OCR识别(优化配置,与HTML文件保持一致)
ocr_data = pytesseract.image_to_data(
pil_img,
output_type=pytesseract.Output.DICT,
lang='eng',
config='--psm 6 --oem 1 -c tessedit_char_whitelist=ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789.,- '
)
# 处理OCR结果
page_width = page.rect.width
page_height = page.rect.height
viewport_width = pil_img.width
viewport_height = pil_img.height
# 存储所有识别到的文字
page_recognized_texts = []
for i in range(len(ocr_data['text'])):
text = ocr_data['text'][i].strip()
if text:
page_recognized_texts.append({
'text': text,
'confidence': ocr_data['conf'][i],
'bbox': {
'x0': ocr_data['left'][i],
'y0': ocr_data['top'][i],
'x1': ocr_data['left'][i] + ocr_data['width'][i],
'y1': ocr_data['top'][i] + ocr_data['height'][i]
},
'page': page_num
})
all_recognized_texts.extend(page_recognized_texts)
# 查找目标文字
page_texts = self._find_target_texts(
page_recognized_texts,
page_num,
viewport_width,
viewport_height,
page_width,
page_height,
TARGET_TEXTS,
EXCLUDE_TEXTS
)
detected_texts.extend(page_texts)
_logger.info(f"第{page_num + 1}页OCR完成,找到{len(page_texts)}个目标文字")
# 在页面上绘制删除矩形
for text_info in page_texts:
# 超精确删除模式(与HTML文件保持一致)
rect = {
'x': text_info['x'],
'y': text_info['y'],
'width': text_info['width'],
'height': text_info['height']
}
# 绘制白色矩形覆盖文字
page.draw_rect(
fitz.Rect(rect['x'], rect['y'], rect['x'] + rect['width'], rect['y'] + rect['height']),
color=(1, 1, 1),
fill=(1, 1, 1)
)
total_rectangles += 1
processed_pages += 1
except Exception as e:
_logger.warning(f"第{page_num + 1}页OCR失败: {str(e)}")
# 使用回退策略:预设坐标
self._apply_fallback_rectangles(page, page_num)
processed_pages += 1
# 保存处理后的PDF
output_buffer = io.BytesIO()
pdf_document.save(output_buffer)
pdf_document.close()
result_data = output_buffer.getvalue()
output_buffer.close()
_logger.info(f"PDF OCR处理完成,共处理{processed_pages}页,删除{total_rectangles}个文字区域,提单号: {bl_no}")
return result_data
def _find_target_texts(self, words, page_num, viewport_width, viewport_height, page_width, page_height, target_texts, exclude_texts):
"""
Find target texts using OCR results # 使用OCR结果查找目标文字
"""
found_texts = []
for word in words:
text = word['text'].strip().upper()
# 首先检查是否在排除列表中
is_excluded = False
for exclude_text in exclude_texts:
exclude_upper = exclude_text.upper()
if exclude_upper in text or text in exclude_upper:
is_excluded = True
break
# 检查页码模式(Page X of Y)
import re
if not is_excluded and (re.match(r'^PAGE\s+\d+\s+OF\s+\d+$', text) or re.match(r'^\d+\s*/\s*\d+$', text)):
is_excluded = True
if is_excluded:
continue
# 检查目标文字匹配
for target_text in target_texts:
target_upper = target_text.upper()
is_match = False
if target_text == 'AGN':
# AGN使用精确匹配
is_match = text == 'AGN'
elif target_text == 'LTD':
# LTD使用精确匹配
is_match = text == 'LTD'
elif target_text == 'UCLINK LOGISITICS LTD':
# 完整短语匹配
is_match = ('UCLINK' in text and 'LOGISITICS' in text and 'LTD' in text) or \
'UCLINK LOGISITICS LTD' in text or \
text == 'UCLINK LOGISITICS LTD'
elif target_text == 'UCLINK LOGISITICS':
# 部分短语匹配
is_match = ('UCLINK' in text and 'LOGISITICS' in text) or \
text == 'UCLINK LOGISITICS'
elif target_text == 'UCLINK':
# 单独UCLINK匹配
is_match = text == 'UCLINK' or text.startswith('UCLINK ')
elif target_text in ['LOGISITICS', 'LOGISTICS']:
# LOGISITICS/LOGISTICS匹配
is_match = text in ['LOGISITICS', 'LOGISTICS'] or \
text.startswith('LOGISITICS') or text.startswith('LOGISTICS')
else:
# 其他文字使用包含匹配,但更严格
is_match = target_upper in text and \
'AIR' not in text and \
'EQK' not in text and \
'ARN' not in text
if is_match:
# 坐标转换(与HTML文件保持一致)
scale_x = page_width / viewport_width
scale_y = page_height / viewport_height
converted_x = word['bbox']['x0'] * scale_x
converted_y = (viewport_height - word['bbox']['y1']) * scale_y
converted_width = (word['bbox']['x1'] - word['bbox']['x0']) * scale_x
converted_height = (word['bbox']['y1'] - word['bbox']['y0']) * scale_y
found_texts.append({
'text': target_text,
'full_text': word['text'],
'page': page_num,
'x': converted_x,
'y': converted_y,
'width': converted_width,
'height': converted_height,
'confidence': word['confidence'] / 100,
'type': 'agn' if target_text == 'AGN' else 'uclink'
})
break
return found_texts
def _apply_fallback_rectangles(self, page, page_num):
"""
Apply fallback rectangles when OCR fails # OCR失败时应用回退矩形
"""
page_width = page.rect.width
page_height = page.rect.height
# 超精确的预设坐标覆盖(与HTML文件保持一致)
rectangles = [
{'x': 50, 'y': page_height - 200, 'width': 60, 'height': 10}, # AGN
{'x': 50, 'y': page_height - 220, 'width': 100, 'height': 10}, # UCLINK LOGISITICS
{'x': 155, 'y': page_height - 220, 'width': 30, 'height': 10} # LTD
]
for rect in rectangles:
import fitz
page.draw_rect(
fitz.Rect(rect['x'], rect['y'], rect['x'] + rect['width'], rect['y'] + rect['height']),
color=(1, 1, 1),
fill=(1, 1, 1)
)
_logger.info(f"第{page_num + 1}页使用回退策略,应用了{len(rectangles)}个预设矩形")
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<!-- Batch Get POD Info Wizard Form View 批量获取POD信息向导表单视图 -->
<record id="view_batch_get_pod_info_wizard_form" model="ir.ui.view">
<field name="name">batch.get.pod.info.wizard.form</field>
<field name="model">batch.get.pod.info.wizard</field>
<field name="arch" type="xml">
<form string="Batch Get POD Info"> <!-- 批量获取POD信息 -->
<sheet>
<group>
<group>
<field name="sync_last_mile_pod" widget="boolean_toggle"/>
</group>
<group>
<field name="remove_specified_text" widget="boolean_toggle"/>
</group>
</group>
<div class="alert alert-info" role="alert">
<strong>Description:</strong> <!-- 说明: -->
<ul>
<li><strong>Sync Last Mile POD:</strong> Get the latest POD (Proof of Delivery) information from last mile service providers</li> <!-- 同步尾程POD:从尾程服务商获取最新的POD(Proof of Delivery)信息 -->
<li><strong>Remove Specified Text:</strong> Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 -->
</ul>
</div>
<footer>
<button string="Confirm" type="object" name="confirm" class="btn-primary"/>
<button string="Close" special="cancel"/>
</footer>
</sheet>
</form>
</field>
</record>
<!-- Batch Get POD Info Wizard Action 批量获取POD信息向导动作 -->
<record id="action_batch_get_pod_info_wizard" model="ir.actions.act_window">
<field name="name">Batch Get POD Info</field> <!-- 批量获取POD信息 -->
<field name="res_model">batch.get.pod.info.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
<field name="context">{}</field>
</record>
</data>
</odoo>
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论