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

Merge branch 'release/3.6.0'

...@@ -12,5 +12,31 @@ ...@@ -12,5 +12,31 @@
<field name="active" eval="False"/> <field name="active" eval="False"/>
</record> </record>
<!-- 自动获取尾程POD-->
<record id="cron_get_pod" model="ir.cron">
<field name="name">自动获取尾程POD</field>
<field name="model_id" ref="ccs_base.model_cc_bl"/>
<field name="state">code</field>
<field name="code">model.cron_get_pod()</field>
<field name='interval_number'>3</field>
<field name='interval_type'>hours</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=0, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="numbercall">-1</field>
<field name="active" eval="False"/>
</record>
<!-- 清理向导生成的临时附件-->
<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="state">code</field>
<field name="code">model.cron_cleanup_temp_attachments()</field>
<field name='interval_number'>1</field>
<field name='interval_type'>days</field>
<field name="nextcall" eval="(DateTime.now().replace(hour=0, minute=0) + timedelta(days=1)).strftime('%Y-%m-%d %H:%M:%S')" />
<field name="numbercall">-1</field>
<field name="active" eval="True"/>
</record>
</data> </data>
</odoo> </odoo>
\ No newline at end of file
...@@ -6,8 +6,8 @@ msgid "" ...@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 16.0\n" "Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-10-17 07:22+0000\n" "POT-Creation-Date: 2025-10-31 02:13+0000\n"
"PO-Revision-Date: 2025-10-17 15:27+0800\n" "PO-Revision-Date: 2025-10-31 10:14+0800\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: zh_CN\n" "Language: zh_CN\n"
...@@ -35,18 +35,14 @@ msgstr "%s %s关联了托盘%s,托盘使用日期为%s" ...@@ -35,18 +35,14 @@ msgstr "%s %s关联了托盘%s,托盘使用日期为%s"
#. odoo-python #. odoo-python
#: code:addons/ccs_base/wizard/associate_pallet_wizard.py:0 #: code:addons/ccs_base/wizard/associate_pallet_wizard.py:0
#, python-format #, python-format
msgid "" msgid "%s at %s changed the pallet number from %s to %s, and the pallet usage date from %s to %s"
"%s at %s changed the pallet number from %s to %s, and the pallet usage date "
"from %s to %s"
msgstr "%s %s更改了托盘号,由%s变更为%s,托盘使用日期%s变更为%s" msgstr "%s %s更改了托盘号,由%s变更为%s,托盘使用日期%s变更为%s"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/wizard/add_exception_info_wizard.py:0 #: code:addons/ccs_base/wizard/add_exception_info_wizard.py:0
#, python-format #, python-format
msgid "" msgid "%s at %s manipulated abnormal information, the reason for the abnormality is: %s, %s %s email"
"%s at %s manipulated abnormal information, the reason for the abnormality "
"is: %s, %s %s email"
msgstr "%s %s 操作了异常信息,异常原因:%s,%s%s邮件" msgstr "%s %s 操作了异常信息,异常原因:%s,%s%s邮件"
#. module: ccs_base #. module: ccs_base
...@@ -54,8 +50,7 @@ msgstr "%s %s 操作了异常信息,异常原因:%s,%s%s邮件" ...@@ -54,8 +50,7 @@ msgstr "%s %s 操作了异常信息,异常原因:%s,%s%s邮件"
#: code:addons/ccs_base/wizard/add_exception_info_wizard.py:0 #: code:addons/ccs_base/wizard/add_exception_info_wizard.py:0
#, python-format #, python-format
msgid "" msgid ""
"%s at %s manipulated abnormal information, the reason for the abnormality " "%s at %s manipulated abnormal information, the reason for the abnormality is: %s, not sent %s email"
"is: %s, not sent %s email"
msgstr "%s %s 操作了异常信息,异常原因:%s,未发送%s邮件" msgstr "%s %s 操作了异常信息,异常原因:%s,未发送%s邮件"
#. module: ccs_base #. module: ccs_base
...@@ -65,6 +60,13 @@ msgstr "%s %s 操作了异常信息,异常原因:%s,未发送%s邮件" ...@@ -65,6 +60,13 @@ msgstr "%s %s 操作了异常信息,异常原因:%s,未发送%s邮件"
msgid "%s bill of loading cannot find release note file" msgid "%s bill of loading cannot find release note file"
msgstr "%s 提单无法找到release note文件" msgstr "%s 提单无法找到release note文件"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "%s bill of loading cannot get node operation time,please manually upload push tk"
msgstr "%s提单号没有获取到节点操作时间,请手动上传推送tk"
#. module: ccs_base #. module: ccs_base
#: model:mail.template,body_html:ccs_base.email_template_exception_notification_en #: model:mail.template,body_html:ccs_base.email_template_exception_notification_en
msgid "" msgid ""
...@@ -74,29 +76,23 @@ msgid "" ...@@ -74,29 +76,23 @@ msgid ""
" <br>\n" " <br>\n"
" <!-- 检查 big_package_ids 是否有值 -->\n" " <!-- 检查 big_package_ids 是否有值 -->\n"
" <t t-if=\"object.big_package_ids\">\n" " <t t-if=\"object.big_package_ids\">\n"
" <t t-foreach=\"object.big_package_ids\" t-" " <t t-foreach=\"object.big_package_ids\" t-as=\"big_package_id\">\n"
"as=\"big_package_id\">\n"
" <div>Package:\n" " <div>Package:\n"
" <t t-esc=\"big_package_id.big_package_no or " " <t t-esc=\"big_package_id.big_package_no or ''\"></t>\n"
"''\"></t>\n"
" </div>\n" " </div>\n"
" </t>\n" " </t>\n"
" </t>\n" " </t>\n"
" <!-- 检查 ship_package_ids 是否有值 -->\n" " <!-- 检查 ship_package_ids 是否有值 -->\n"
" <t t-if=\"object.ship_package_ids\">\n" " <t t-if=\"object.ship_package_ids\">\n"
" <t t-foreach=\"object.ship_package_ids\" t-" " <t t-foreach=\"object.ship_package_ids\" t-as=\"ship_package_id\">\n"
"as=\"ship_package_id\">\n"
" <div>Package:\n" " <div>Package:\n"
" <t t-esc=\"ship_package_id." " <t t-esc=\"ship_package_id.logistic_order_no or ''\"></t>\n"
"logistic_order_no or ''\"></t>\n"
" </div>\n" " </div>\n"
" </t>\n" " </t>\n"
" </t>\n" " </t>\n"
" <div>Please know that there is an exception and the " " <div>Please know that there is an exception and the cause of the exception is\n"
"cause of the exception is\n"
" <t t-if=\"object.exception_ids\">\n" " <t t-if=\"object.exception_ids\">\n"
" <t t-esc=\"'/'.join([ex.reason for ex in object." " <t t-esc=\"'/'.join([ex.reason for ex in object.exception_ids])\"></t>\n"
"exception_ids])\"></t>\n"
" </t>\n" " </t>\n"
" <t t-if=\"not object.exception_ids\">\n" " <t t-if=\"not object.exception_ids\">\n"
" No Exception\n" " No Exception\n"
...@@ -117,28 +113,24 @@ msgid "" ...@@ -117,28 +113,24 @@ msgid ""
" <br>\n" " <br>\n"
" <!-- 检查 big_package_ids 是否有值 -->\n" " <!-- 检查 big_package_ids 是否有值 -->\n"
" <t t-if=\"object.big_package_ids\">\n" " <t t-if=\"object.big_package_ids\">\n"
" <t t-foreach=\"object.big_package_ids\" t-" " <t t-foreach=\"object.big_package_ids\" t-as=\"big_package_id\">\n"
"as=\"big_package_id\">\n"
" <div>包裹:\n" " <div>包裹:\n"
" <t t-esc=\"big_package_id.big_package_no or " " <t t-esc=\"big_package_id.big_package_no or ''\"></t>\n"
"''\"></t>\n"
" </div>\n" " </div>\n"
" </t>\n" " </t>\n"
" </t>\n" " </t>\n"
" <!-- 检查 ship_package_ids 是否有值 -->\n" " <!-- 检查 ship_package_ids 是否有值 -->\n"
" <t t-if=\"object.ship_package_ids\">\n" " <t t-if=\"object.ship_package_ids\">\n"
" <t t-foreach=\"object.ship_package_ids\" t-" " <t t-foreach=\"object.ship_package_ids\" t-as=\"ship_package_id\">\n"
"as=\"ship_package_id\">\n"
" <div>包裹:\n" " <div>包裹:\n"
" <t t-esc=\"ship_package_id." " <t t-esc=\"ship_package_id.logistic_order_no or ''\"></t>\n"
"logistic_order_no or ''\"></t>\n"
" </div>\n" " </div>\n"
" </t>\n" " </t>\n"
" </t>\n" " </t>\n"
" <div>出现异常,异常原因:\n" " <div>出现异常,异常原因:\n"
" <t t-if=\"object.exception_ids\">\n" " <t t-if=\"object.exception_ids\">\n"
" <t t-esc=\"'/'.join([ex.reason for ex in object." " <t t-esc=\"'/'.join([ex.reason for ex in object.exception_ids])\"></t>,"
"exception_ids])\"></t>,请知晓。\n" "请知晓。\n"
" </t>\n" " </t>\n"
" <t t-if=\"not object.exception_ids\">\n" " <t t-if=\"not object.exception_ids\">\n"
" 无异常,请知晓。\n" " 无异常,请知晓。\n"
...@@ -154,13 +146,12 @@ msgstr "" ...@@ -154,13 +146,12 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard
msgid "" msgid ""
"<span class=\"label label-warning\">\n" "<span class=\"label label-warning\">\n"
" If there is abnormal data, please download " " If there is abnormal data, please download the error file, fix the "
"the error file, fix the data as prompted, and re-import!\n" "data as prompted, and re-import!\n"
" </span>" " </span>"
msgstr "" msgstr ""
"<span class=\"label label-warning\">\n" "<span class=\"label label-warning\">\n"
" 如果数据异常,请下载错误文件,按提示修正数" " 如果数据异常,请下载错误文件,按提示修正数据,然后重新导入!\n"
"据,然后重新导入!\n"
" </span>" " </span>"
#. module: ccs_base #. module: ccs_base
...@@ -200,13 +191,11 @@ msgstr "<span class=\"o_stat_text\">已理货大包</span>" ...@@ -200,13 +191,11 @@ msgstr "<span class=\"o_stat_text\">已理货大包</span>"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_update_transfer_bl_no_wizard
msgid "" msgid ""
"<span style=\"color:red;font-size:15px;\">Tip: Please fill in the " "<span style=\"color:red;font-size:15px;\">Tip: Please fill in the information strictly according to "
"information strictly according to the template, otherwise the system will " "the template, otherwise the system will not recognize it.\n"
"not recognize it.\n"
" </span>" " </span>"
msgstr "" msgstr ""
"<span style=\"color:red;font-size:15px;\">提示:请务必按照模板填写信息,否则" "<span style=\"color:red;font-size:15px;\">提示:请务必按照模板填写信息,否则系统无法识别。\n"
"系统无法识别。\n"
" </span>" " </span>"
#. module: ccs_base #. module: ccs_base
...@@ -217,26 +206,33 @@ msgstr "<strong>说明:</strong>" ...@@ -217,26 +206,33 @@ msgstr "<strong>说明:</strong>"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form
msgid "" msgid ""
"<strong>Remove Specified Text:</strong> Remove specified text (AGN, UCLINK " "<strong>Remove Specified Text:</strong>\n"
"LOGISITICS LTD) from PDF files" " Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files"
msgstr "" msgstr "<strong>删除指定文本:</strong>从 PDF 文件中删除指定文本(AGN、UCLINK LOGISITICS LTD)。"
"<strong>涂抹指定文字:</strong>从 PDF 文件中删除指定文本(AGN、UCLINK "
"LOGISITICS LTD)。"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form
msgid "" msgid ""
"<strong>Sync Last Mile POD:</strong> Synchronize POD (Proof of Delivery) " "<strong>Sync Last Mile POD:</strong>\n"
"attachment information with TK system, including big package quantities and " " Synchronize POD (Proof of Delivery) attachment information with "
"container numbers" "TK system, including\n"
" big package quantities and container numbers"
msgstr "同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息" msgstr "同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form
msgid ""
"<strong>Sync Push Match Node:</strong>\n"
" Synchronize and push matched node information based on POD file, "
"extract time from\n"
" red boxes as node operation time"
msgstr ""
"<strong>同步推送匹配节点:</strong>根据 POD 文件同步推送匹配的节点信息,提取时间作为节点运行时间"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_input_ship_package_status_wizard__exclude_tracking_no #: model:ir.model.fields,help:ccs_base.field_batch_input_ship_package_status_wizard__exclude_tracking_no
#: model:ir.model.fields,help:ccs_base.field_batch_input_ship_package_status_wizard__select_tracking_no #: model:ir.model.fields,help:ccs_base.field_batch_input_ship_package_status_wizard__select_tracking_no
msgid "" msgid "A TrackingNo no is one line,separate multiple tracking numbers with line breaks"
"A TrackingNo no is one line,separate multiple tracking numbers with line "
"breaks"
msgstr "一个跟踪号为一行,多个跟踪号之间用换行符隔开" msgstr "一个跟踪号为一行,多个跟踪号之间用换行符隔开"
#. module: ccs_base #. module: ccs_base
...@@ -492,13 +488,6 @@ msgstr "添加包裹异常信息" ...@@ -492,13 +488,6 @@ msgstr "添加包裹异常信息"
msgid "Add Package Exception Information Wizard" msgid "Add Package Exception Information Wizard"
msgstr "添加包裹异常信息向导" msgstr "添加包裹异常信息向导"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "All API PDF files failed validation"
msgstr "所有API PDF文件验证都失败"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__api_customer #: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__api_customer
msgid "Api Customer" msgid "Api Customer"
...@@ -608,6 +597,7 @@ msgstr "批量完成" ...@@ -608,6 +597,7 @@ msgstr "批量完成"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#: model:ir.actions.act_window,name:ccs_base.action_batch_get_pod_info_wizard #: model:ir.actions.act_window,name:ccs_base.action_batch_get_pod_info_wizard
#: model:ir.actions.server,name:ccs_base.bl_get_pod_info_server_action #: model:ir.actions.server,name:ccs_base.bl_get_pod_info_server_action
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form #: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form
...@@ -718,8 +708,7 @@ msgid "Bill Of Loading Last Process Time" ...@@ -718,8 +708,7 @@ msgid "Bill Of Loading Last Process Time"
msgstr "提单最后推送时间" msgstr "提单最后推送时间"
#. module: ccs_base #. module: ccs_base
#: model:ir.actions.act_window,name:ccs_base.action_cc_bl #: model:ir.actions.act_window,name:ccs_base.action_cc_bl model:ir.model,name:ccs_base.model_cc_bl
#: model:ir.model,name:ccs_base.model_cc_bl
#: model:ir.model.fields,field_description:ccs_base.field_batch_input_ship_package_status_wizard__bl_id #: model:ir.model.fields,field_description:ccs_base.field_batch_input_ship_package_status_wizard__bl_id
#: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__bl_id #: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__bl_id
#: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__bl_id #: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__bl_id
...@@ -779,8 +768,7 @@ msgstr "提单" ...@@ -779,8 +768,7 @@ msgstr "提单"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0 model:ir.model,name:ccs_base.model_bl_done_wizard
#: model:ir.model,name:ccs_base.model_bl_done_wizard
#: model_terms:ir.ui.view,arch_db:ccs_base.view_bl_done_wizard #: model_terms:ir.ui.view,arch_db:ccs_base.view_bl_done_wizard
#, python-format #, python-format
msgid "Bl Done Wizard" msgid "Bl Done Wizard"
...@@ -866,8 +854,7 @@ msgid "CC Manager" ...@@ -866,8 +854,7 @@ msgid "CC Manager"
msgstr "清关经理" msgstr "清关经理"
#. module: ccs_base #. module: ccs_base
#: model:ir.actions.act_window,name:ccs_base.action_cc_node #: model:ir.actions.act_window,name:ccs_base.action_cc_node model:ir.model,name:ccs_base.model_cc_node
#: model:ir.model,name:ccs_base.model_cc_node
#: model:ir.model.fields,field_description:ccs_base.field_cc_progress__cc_node_id #: model:ir.model.fields,field_description:ccs_base.field_cc_progress__cc_node_id
#: model:ir.ui.menu,name:ccs_base.menu_cc_node #: model:ir.ui.menu,name:ccs_base.menu_cc_node
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_progress_view #: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_progress_view
...@@ -1540,13 +1527,6 @@ msgstr "导出报关文件" ...@@ -1540,13 +1527,6 @@ msgstr "导出报关文件"
msgid "Failed to get PDF file from API: %s" msgid "Failed to get PDF file from API: %s"
msgstr "从 API 获取 PDF 文件失败: %s" msgstr "从 API 获取 PDF 文件失败: %s"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "Failed to save PDF attachment: %s"
msgstr "保存 PDF 附件失败:%s"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__file #: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__file
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_clearance_file_view #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_clearance_file_view
...@@ -1879,20 +1859,6 @@ msgstr "个人" ...@@ -1879,20 +1859,6 @@ msgstr "个人"
msgid "Internal Account Number" msgid "Internal Account Number"
msgstr "内部帐号" msgstr "内部帐号"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "Invalid PDF data for saving: cannot open PDF - %s"
msgstr "用于保存的 PDF 数据无效:无法打开 PDF - %s"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "Invalid PDF data for saving: not a valid PDF format"
msgstr "用于保存的 PDF 数据无效:不是有效的 PDF 格式"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_ship_package__invoice_attachment_ids #: model:ir.model.fields,field_description:ccs_base.field_cc_history_ship_package__invoice_attachment_ids
#: model:ir.model.fields,field_description:ccs_base.field_cc_ship_package__invoice_attachment_ids #: model:ir.model.fields,field_description:ccs_base.field_cc_ship_package__invoice_attachment_ids
...@@ -2226,8 +2192,7 @@ msgstr "链接" ...@@ -2226,8 +2192,7 @@ msgstr "链接"
#. module: ccs_base #. module: ccs_base
#. odoo-javascript #. odoo-javascript
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0 code:addons/ccs_base/static/src/views/list.xml:0
#: code:addons/ccs_base/static/src/views/list.xml:0
#, python-format #, python-format
msgid "Link Pallet" msgid "Link Pallet"
msgstr "关联托盘" msgstr "关联托盘"
...@@ -2235,8 +2200,7 @@ msgstr "关联托盘" ...@@ -2235,8 +2200,7 @@ msgstr "关联托盘"
#. module: ccs_base #. module: ccs_base
#. odoo-javascript #. odoo-javascript
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0 code:addons/ccs_base/static/src/views/list.xml:0
#: code:addons/ccs_base/static/src/views/list.xml:0
#, python-format #, python-format
msgid "Link Transfer B/L No" msgid "Link Transfer B/L No"
msgstr "关联转单号" msgstr "关联转单号"
...@@ -2433,13 +2397,6 @@ msgstr "下一阶段服务商名称" ...@@ -2433,13 +2397,6 @@ msgstr "下一阶段服务商名称"
msgid "No Bill of Loading" msgid "No Bill of Loading"
msgstr "提单号" msgstr "提单号"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "No PDF files found"
msgstr "无法获取到pdf文件"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0 #: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
...@@ -2454,13 +2411,6 @@ msgstr "API调用成功,但没有PDF文件" ...@@ -2454,13 +2411,6 @@ msgstr "API调用成功,但没有PDF文件"
msgid "No package to update found." msgid "No package to update found."
msgstr "未找到要更新的小包。" msgstr "未找到要更新的小包。"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "No processed file data available"
msgstr "没有处理后的文件数据"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_node_exception_reason__code_id #: model:ir.model.fields,field_description:ccs_base.field_cc_node_exception_reason__code_id
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_node_exception_reason_view #: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_node_exception_reason_view
...@@ -2569,6 +2519,20 @@ msgstr "只有已完成状态的提单可以追回!" ...@@ -2569,6 +2519,20 @@ msgstr "只有已完成状态的提单可以追回!"
msgid "Only excel files can be uploaded!" msgid "Only excel files can be uploaded!"
msgstr "只能上传excel文件!" msgstr "只能上传excel文件!"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_node.py:0
#, python-format
msgid "Only one package type node can be selected as POD node"
msgstr "只能选择一个小包类型节点作为 POD 节点"
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/models/cc_node.py:0
#, python-format
msgid "Only package type nodes can be selected as POD nodes"
msgstr "只能选择小包类型节点作为 POD 节点"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
...@@ -2622,6 +2586,16 @@ msgstr "其他信息" ...@@ -2622,6 +2586,16 @@ msgstr "其他信息"
msgid "PDF File" msgid "PDF File"
msgstr "PDF文件" msgstr "PDF文件"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__pdf_file
msgid "PDF文件"
msgstr ""
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__pdf_filename
msgid "PDF文件名称"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_ship_package_view #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_ship_package_view
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_ship_package_view #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_ship_package_view
...@@ -2764,17 +2738,14 @@ msgstr "摆放区域必须唯一!" ...@@ -2764,17 +2738,14 @@ msgstr "摆放区域必须唯一!"
#: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0 #: code:addons/ccs_base/wizard/batch_update_transfer_bl_no_wizard.py:0
#, python-format #, python-format
msgid "" msgid ""
"Please check if the import file and content are correct, and import " "Please check if the import file and content are correct, and import according to the template file!"
"according to the template file!"
msgstr "请检查导入文件和内容是否正确,请根据模板文件导入!" msgstr "请检查导入文件和内容是否正确,请根据模板文件导入!"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
#, python-format #, python-format
msgid "" msgid "Please configure the default customs clearance status of the bill of loading node type first."
"Please configure the default customs clearance status of the bill of "
"loading node type first."
msgstr "请先配置默认的提单节点类型的清关节点" msgstr "请先配置默认的提单节点类型的清关节点"
#. module: ccs_base #. module: ccs_base
...@@ -2791,6 +2762,11 @@ msgstr "请选择异常原因!" ...@@ -2791,6 +2762,11 @@ msgstr "请选择异常原因!"
msgid "Please upload the B/L data file to be imported first!" msgid "Please upload the B/L data file to be imported first!"
msgstr "请先上传需要更新的提单数据文件!" msgstr "请先上传需要更新的提单数据文件!"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.view_batch_get_pod_info_wizard_form
msgid "Preview"
msgstr "预览"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_input_ship_package_status_wizard__process_time #: model:ir.model.fields,field_description:ccs_base.field_batch_input_ship_package_status_wizard__process_time
#: model:ir.model.fields,field_description:ccs_base.field_cc_bl__process_time #: model:ir.model.fields,field_description:ccs_base.field_cc_bl__process_time
...@@ -3356,6 +3332,21 @@ msgstr "发件人所在省" ...@@ -3356,6 +3332,21 @@ msgstr "发件人所在省"
msgid "Shipping_fee" msgid "Shipping_fee"
msgstr "运费" msgstr "运费"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__show_error_message
msgid "Show Error Message"
msgstr "显示错误消息"
#. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__show_error_message
msgid "Show error message"
msgstr "显示错误消息"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__skip_ocr_direct_ai
msgid "Skip OCR Direct AI"
msgstr "跳过 OCR 直接 AI"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_bl_view
msgid "Start Port" msgid "Start Port"
...@@ -3430,6 +3421,11 @@ msgstr "同步日志" ...@@ -3430,6 +3421,11 @@ msgstr "同步日志"
msgid "Sync Logs" msgid "Sync Logs"
msgstr "同步日志" msgstr "同步日志"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__sync_match_node
msgid "Sync Push Match Node"
msgstr "同步推送匹配节点"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__upload_time #: model:ir.model.fields,field_description:ccs_base.field_cc_clearance_file__upload_time
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__sync_time #: model:ir.model.fields,field_description:ccs_base.field_cc_history_package_sync_log__sync_time
...@@ -3488,6 +3484,12 @@ msgstr "" ...@@ -3488,6 +3484,12 @@ msgstr ""
msgid "TYPE" msgid "TYPE"
msgstr "类型" msgstr "类型"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_node__is_pod_node
#: model_terms:ir.ui.view,arch_db:ccs_base.tree_cc_node_view
msgid "Tail POD Node Match"
msgstr "尾程POD节点匹配"
#. module: ccs_base #. module: ccs_base
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
...@@ -3568,9 +3570,7 @@ msgstr "转单号必填" ...@@ -3568,9 +3570,7 @@ msgstr "转单号必填"
#. odoo-python #. odoo-python
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
#, python-format #, python-format
msgid "" msgid "The customs clearance status of the selected bill of loading must be the same."
"The customs clearance status of the selected bill of loading must be the "
"same."
msgstr "选择的关务提单状态必须是同一个。" msgstr "选择的关务提单状态必须是同一个。"
#. module: ccs_base #. module: ccs_base
...@@ -3612,9 +3612,7 @@ msgstr "托盘号只能输入数字!" ...@@ -3612,9 +3612,7 @@ msgstr "托盘号只能输入数字!"
#. odoo-python #. odoo-python
#: code:addons/ccs_base/wizard/batch_input_ship_package_statu_wizard.py:0 #: code:addons/ccs_base/wizard/batch_input_ship_package_statu_wizard.py:0
#, python-format #, python-format
msgid "" msgid "The selected operation time exceeds %s hours, please confirm the operation time again"
"The selected operation time exceeds %s hours, please confirm the operation "
"time again"
msgstr "所选操作时间超过 %s 小时,请重新确认操作时间" msgstr "所选操作时间超过 %s 小时,请重新确认操作时间"
#. module: ccs_base #. module: ccs_base
...@@ -3622,8 +3620,8 @@ msgstr "所选操作时间超过 %s 小时,请重新确认操作时间" ...@@ -3622,8 +3620,8 @@ msgstr "所选操作时间超过 %s 小时,请重新确认操作时间"
#: code:addons/ccs_base/models/cc_bill_loading.py:0 #: code:addons/ccs_base/models/cc_bill_loading.py:0
#, python-format #, python-format
msgid "" msgid ""
"The small package node or bill of lading node is not in the completed node, " "The small package node or bill of lading node is not in the completed node, and the bill of lading "
"and the bill of lading cannot be changed to completed!" "cannot be changed to completed!"
msgstr "小包节点或提单节点不在已完成节点,提单不能变为已完成!" msgstr "小包节点或提单节点不在已完成节点,提单不能变为已完成!"
#. module: ccs_base #. module: ccs_base
...@@ -3966,6 +3964,16 @@ msgstr "是否分大包导出" ...@@ -3966,6 +3964,16 @@ msgstr "是否分大包导出"
msgid "Whether to remove specified text from PDF files" msgid "Whether to remove specified text from PDF files"
msgstr "是否涂抹PDF中的指定文字" msgstr "是否涂抹PDF中的指定文字"
#. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__skip_ocr_direct_ai
msgid "Whether to skip OCR processing and directly use AI processing (for testing AI)"
msgstr ""
#. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__sync_match_node
msgid "Whether to sync and push matched node information"
msgstr "是否同步推送匹配节点信息"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__sync_last_mile_pod #: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__sync_last_mile_pod
msgid "Whether to sync last mile POD information" msgid "Whether to sync last mile POD information"
...@@ -3995,86 +4003,68 @@ msgstr "昨日小包" ...@@ -3995,86 +4003,68 @@ msgstr "昨日小包"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_big_package #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_big_package
msgid "" msgid "[Big Package] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[Big Package] Not yet! Click the Create button in the top left corner and "
"the sofa is yours!"
msgstr "【大包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【大包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_package_sync_log #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_package_sync_log
msgid "" msgid ""
"[CC History Package Sync Log] Not yet! Click the Create button in the top " "[CC History Package Sync Log] Not yet! Click the Create button in the top left corner and the sofa "
"left corner and the sofa is yours!" "is yours!"
msgstr "" msgstr ""
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_node #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_node
msgid "" msgid "[CC Node] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[CC Node] Not yet! Click the Create button in the top left corner and the "
"sofa is yours!"
msgstr "【清关节点】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【清关节点】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_progress #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_progress
msgid "" msgid "[CC Progress] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[CC Progress] Not yet! Click the Create button in the top left corner and "
"the sofa is yours!"
msgstr "【清关进度】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【清关进度】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_is_clearance_company #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_is_clearance_company
msgid "" msgid ""
"[Clearance Company] Not yet! Click the Create button in the top left corner " "[Clearance Company] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"and the sofa is yours!"
msgstr "【清关公司】还没!点击左上角的创建按钮,沙发就是你的了!" msgstr "【清关公司】还没!点击左上角的创建按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_clearance_file #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_clearance_file
msgid "" msgid "[Clearance File] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[Clearance File] Not yet! Click the Create button in the top left corner "
"and the sofa is yours!"
msgstr "【清关文件】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【清关文件】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_partner #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_partner
msgid "" msgid "[Customers] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[Customers] Not yet! Click the Create button in the top left corner and the "
"sofa is yours!"
msgstr "【客户】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【客户】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_big_package #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_big_package
msgid "" msgid ""
"[History Big Package] Not yet! Click the Create button in the top left " "[History Big Package] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"corner and the sofa is yours!"
msgstr "【历史大包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【历史大包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_package_good #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_package_good
msgid "" msgid ""
"[History Package Good] Not yet! Click the Create button in the top left " "[History Package Good] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"corner and the sofa is yours!"
msgstr "【货物】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【货物】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_ship_package #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_history_ship_package
msgid "" msgid ""
"[History Ship Package] Not yet! Click the Create button in the top left " "[History Ship Package] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"corner and the sofa is yours!"
msgstr "【小包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【小包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_package_good #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_package_good
msgid "" msgid "[Package Good] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[Package Good] Not yet! Click the Create button in the top left corner and "
"the sofa is yours!"
msgstr "【货物】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【货物】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
#: model_terms:ir.actions.act_window,help:ccs_base.action_cc_ship_package #: model_terms:ir.actions.act_window,help:ccs_base.action_cc_ship_package
msgid "" msgid "[Ship Package] Not yet! Click the Create button in the top left corner and the sofa is yours!"
"[Ship Package] Not yet! Click the Create button in the top left corner and "
"the sofa is yours!"
msgstr "【小包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!" msgstr "【小包】 还没有!点击左上角的“创建”按钮,沙发就是你的了!"
#. module: ccs_base #. module: ccs_base
...@@ -4101,14 +4091,12 @@ msgstr "" ...@@ -4101,14 +4091,12 @@ msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:mail.template,subject:ccs_base.email_template_exception_notification_en #: model:mail.template,subject:ccs_base.email_template_exception_notification_en
msgid "" msgid ""
"{{ ('Big Package Exception' if object.action_type == 'big package' else " "{{ ('Big Package Exception' if object.action_type == 'big package' else 'Ship Package Exception') }}"
"'Ship Package Exception') }}"
msgstr "" msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:mail.template,subject:ccs_base.email_template_exception_notification #: model:mail.template,subject:ccs_base.email_template_exception_notification
msgid "" msgid "{{ ('大包异常' if object.action_type == 'big package' else '小包异常') }}"
"{{ ('大包异常' if object.action_type == 'big package' else '小包异常') }}"
msgstr "" msgstr ""
#. module: ccs_base #. module: ccs_base
...@@ -4198,6 +4186,11 @@ msgstr "" ...@@ -4198,6 +4186,11 @@ msgstr ""
msgid "失败原因" msgid "失败原因"
msgstr "" msgstr ""
#. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__processed_files_data
msgid "存储已处理的文件信息(JSON格式)"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:ir.actions.act_window,name:ccs_base.action_export_bl_big_package_xlsx_wizard #: model:ir.actions.act_window,name:ccs_base.action_export_bl_big_package_xlsx_wizard
#: model:ir.model,name:ccs_base.model_export_bl_big_package_xlsx_wizard #: model:ir.model,name:ccs_base.model_export_bl_big_package_xlsx_wizard
...@@ -4205,6 +4198,11 @@ msgstr "" ...@@ -4205,6 +4198,11 @@ msgstr ""
msgid "导出报关文件" msgid "导出报关文件"
msgstr "" msgstr ""
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_batch_get_pod_info_wizard__processed_files_data
msgid "已处理的文件数据"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:mail.template,name:ccs_base.email_template_exception_notification #: model:mail.template,name:ccs_base.email_template_exception_notification
msgid "异常信息邮件" msgid "异常信息邮件"
...@@ -4251,11 +4249,22 @@ msgstr "" ...@@ -4251,11 +4249,22 @@ msgstr ""
msgid "本周日志" msgid "本周日志"
msgstr "" msgstr ""
#. module: ccs_base
#: model:ir.model.fields,help:ccs_base.field_batch_get_pod_info_wizard__pdf_file
msgid "涂抹后的所有pdf文件合并为一个pdf文件"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_res_config_settings__before_min #: model:ir.model.fields,field_description:ccs_base.field_res_config_settings__before_min
msgid "清关时间取值(早于清关结束)" msgid "清关时间取值(早于清关结束)"
msgstr "" msgstr ""
#. module: ccs_base
#: model:ir.actions.server,name:ccs_base.cron_cleanup_temp_attachments_ir_actions_server
#: model:ir.cron,cron_name:ccs_base.cron_cleanup_temp_attachments
msgid "清理向导临时附件"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_history_tt_api_log__source #: model:ir.model.fields,field_description:ccs_base.field_history_tt_api_log__source
#: model_terms:ir.ui.view,arch_db:ccs_base.search_history_tt_api_log_view #: model_terms:ir.ui.view,arch_db:ccs_base.search_history_tt_api_log_view
...@@ -4267,6 +4276,12 @@ msgstr "" ...@@ -4267,6 +4276,12 @@ msgstr ""
msgid "自动推送配置" msgid "自动推送配置"
msgstr "" msgstr ""
#. module: ccs_base
#: model:ir.actions.server,name:ccs_base.cron_get_pod_ir_actions_server
#: model:ir.cron,cron_name:ccs_base.cron_get_pod
msgid "自动获取尾程POD"
msgstr ""
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_history_tt_api_log__request_id #: model:ir.model.fields,field_description:ccs_base.field_history_tt_api_log__request_id
msgid "请求id" msgid "请求id"
...@@ -4286,3 +4301,10 @@ msgstr "" ...@@ -4286,3 +4301,10 @@ msgstr ""
#: model_terms:ir.ui.view,arch_db:ccs_base.search_history_tt_api_log_view #: model_terms:ir.ui.view,arch_db:ccs_base.search_history_tt_api_log_view
msgid "近7日日志" msgid "近7日日志"
msgstr "" msgstr ""
#. module: ccs_base
#. odoo-python
#: code:addons/ccs_base/wizard/batch_get_pod_info_wizard.py:0
#, python-format
msgid "预览操作失败: %s"
msgstr ""
...@@ -556,7 +556,8 @@ class CcClearanceFile(models.Model): ...@@ -556,7 +556,8 @@ class CcClearanceFile(models.Model):
def search_clearance_file(self, bl_id, file_name): 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) 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):
...@@ -712,6 +713,26 @@ class CcBL(models.Model): ...@@ -712,6 +713,26 @@ class CcBL(models.Model):
# 添加状态说明字段 # 添加状态说明字段
state_explain = fields.Text('State Explain', help='State Explain') state_explain = fields.Text('State Explain', help='State Explain')
def cron_get_pod(self):
"""
状态为清关中且附件信息尾程交接POD(待大包数量和箱号),为空的提单,自动获取尾程POD信息
"""
fix_name = '尾程交接POD(待大包数量和箱号)'
bl_objs = self.env['cc.bl'].search(
[('state', '=', 'ccing'), ('cc_attachment_ids.file_name', '=', fix_name)])
if len(bl_objs) > 0:
line_objs = bl_objs.cc_attachment_ids.filtered(lambda attach: not attach.file and attach.file_name == fix_name)
if len(line_objs) > 0:
bl_objs = line_objs.mapped('bl_id')
logging.info('cron_get_pod bl_objs:%s,%s' % (len(bl_objs), ','.join([bl.bl_no for bl in bl_objs])))
wizard_obj = self.env['batch.get.pod.info.wizard'].sudo().with_context(active_id=bl_objs.ids,
is_skip_raise_error=True).create({
'sync_last_mile_pod': True,
'remove_specified_text': True,
'sync_match_node': True
})
wizard_obj.confirm()
# 增加一个can_cancel的方法,用于检查提单当前是否可以取消,返回True表示可以取消, False表示不可以取消,同时返回取消的原因 # 增加一个can_cancel的方法,用于检查提单当前是否可以取消,返回True表示可以取消, False表示不可以取消,同时返回取消的原因
def check_cancel(self): def check_cancel(self):
if self.is_cancel: if self.is_cancel:
......
...@@ -2,7 +2,8 @@ ...@@ -2,7 +2,8 @@
# 导入日志 # 导入日志
import logging import logging
from odoo import models, fields from odoo import api, models, fields, _
from odoo.exceptions import ValidationError
# 获取日志 # 获取日志
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
...@@ -52,6 +53,31 @@ class CcNode(models.Model): ...@@ -52,6 +53,31 @@ class CcNode(models.Model):
package_state = fields.Many2one('cc.node', string='Corresponding to the status of the package', package_state = fields.Many2one('cc.node', string='Corresponding to the status of the package',
domain="[('node_type','=','package')]", index=True) # 对应小包状态 domain="[('node_type','=','package')]", index=True) # 对应小包状态
# 新增字段:尾程POD节点匹配,只有类型为小包的节点才能勾选
is_pod_node = fields.Boolean(string='Tail POD Node Match', default=False)
@api.constrains('is_pod_node', 'node_type')
def _check_pod_node(self):
"""
Constraints: Only package type nodes can be selected as POD nodes
Only one package type node can be selected as POD node
"""
for record in self:
# 只有小包类型节点才能勾选尾程POD节点匹配
if record.is_pod_node and record.node_type != 'package':
raise ValidationError(_("Only package type nodes can be selected as POD nodes"))
# 如果当前节点勾选了POD节点,检查是否已有其他小包节点勾选了
# Only one package type node can be selected as POD node
if record.is_pod_node and record.node_type == 'package':
existing_pod_nodes = self.search([
('is_pod_node', '=', True),
('node_type', '=', 'package'),
('id', '!=', record.id)
])
if existing_pod_nodes:
raise ValidationError(_("Only one package type node can be selected as POD node"))
def node_is_sync(self): def node_is_sync(self):
# 如果更新节点是 默认节点 同步的标志变为True # 如果更新节点是 默认节点 同步的标志变为True
is_sync = False is_sync = False
......
...@@ -5,15 +5,15 @@ import base64 ...@@ -5,15 +5,15 @@ import base64
import json import json
import logging import logging
import re import re
import tempfile
from datetime import datetime, timedelta from datetime import datetime, timedelta
from io import BytesIO from io import BytesIO
import pdfplumber import pdfplumber
import xlrd import xlrd
from aip.ocr import AipOcr
from odoo import models from odoo import models
from odoo.exceptions import ValidationError from odoo.exceptions import ValidationError
import tempfile
from aip.ocr import AipOcr
from pdf2image import convert_from_path from pdf2image import convert_from_path
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
...@@ -265,7 +265,8 @@ class OrderStateChangeRule(models.Model): ...@@ -265,7 +265,8 @@ class OrderStateChangeRule(models.Model):
ids = [i[0] for i in result] ids = [i[0] for i in result]
bl_objs = self.env['cc.bl'].sudo().search([('id', 'in', ids)]) if result else False bl_objs = self.env['cc.bl'].sudo().search([('id', 'in', ids)]) if result else False
if bl_objs and attachment_tuple_arr: if bl_objs and attachment_tuple_arr:
file_objs = self.env['cc.clearance.file'].sudo().search([('file_name', '=', '尾程交接POD(待大包数量和箱号)'), file_objs = self.env['cc.clearance.file'].sudo().search(
[('file_name', '=', '尾程交接POD(待大包数量和箱号)'),
('bl_id', 'in', bl_objs.ids)]) ('bl_id', 'in', bl_objs.ids)])
file_objs.unlink() file_objs.unlink()
for attachment_tuple in attachment_tuple_arr: for attachment_tuple in attachment_tuple_arr:
...@@ -362,7 +363,7 @@ class OrderStateChangeRule(models.Model): ...@@ -362,7 +363,7 @@ class OrderStateChangeRule(models.Model):
# poppler_path = r"E:\poppler-23.08.0\Library\bin" # poppler_path = r"E:\poppler-23.08.0\Library\bin"
# images = convert_from_path(pdf_path, poppler_path=poppler_path) # images = convert_from_path(pdf_path, poppler_path=poppler_path)
# 非本地代码 # 非本地代码
images = convert_from_path(pdf_path)#如果文件损坏的会报错,需要处理 images = convert_from_path(pdf_path) # 如果文件损坏的会报错,需要处理
# 保存每一页为图片文件 # 保存每一页为图片文件
for i, image in enumerate(images): for i, image in enumerate(images):
if i == 0: if i == 0:
......
...@@ -14,6 +14,7 @@ ...@@ -14,6 +14,7 @@
<field name="file" string="PDF File" required="1" filename="attachment_name"/> <field name="file" string="PDF File" required="1" filename="attachment_name"/>
<field name="is_upload" readonly="1"/> <field name="is_upload" readonly="1"/>
<field name="upload_time"/> <field name="upload_time"/>
<field name="create_date" optional="show"/>
<button name="action_sync" string="SyncTo.." type="object" icon="fa-upload"/> <button name="action_sync" string="SyncTo.." type="object" icon="fa-upload"/>
<field name="attachment_name" invisible="1"/> <field name="attachment_name" invisible="1"/>
</tree> </tree>
......
...@@ -17,6 +17,7 @@ ...@@ -17,6 +17,7 @@
<field optional="show" name="is_must" string="Is Must Node"/> <field optional="show" name="is_must" string="Is Must Node"/>
<field optional="show" name="is_done" string="Is Done Node"/> <field optional="show" name="is_done" string="Is Done Node"/>
<field optional="show" name="is_default" string="Is Current Node"/> <field optional="show" name="is_default" string="Is Current Node"/>
<field optional="show" name="is_pod_node" string="Tail POD Node Match" attrs="{'invisible': [('node_type', '!=', 'package')]}" />
</tree> </tree>
</field> </field>
</record> </record>
......
# -*- coding: utf-8 -*-
import base64
import requests
import json
import logging
from http import HTTPStatus
from openai import OpenAI
from PIL import Image, ImageDraw
import io
_logger = logging.getLogger(__name__)
class AIImageEditService:
"""AI图片编辑服务 - 使用坐标检测方式移除文字"""
def __init__(self, api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1'):
self.api_key = api_key
# 使用OpenAI客户端连接阿里云百炼
self.client = OpenAI(
api_key=api_key,
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
def edit_image_remove_text(self, image_base64, text_to_remove="AGN UCLINK LOGISITICS LTD"):
"""
使用坐标检测方式移除图片中的指定文字(按照image-to-coordinate.py的逻辑)
:param image_base64: 图片的base64编码
:param text_to_remove: 要移除的文字
:return: 处理后的图片base64编码,失败返回None
"""
import time
start_time = time.time()
_logger.info(f"开始AI坐标检测,目标文字: {text_to_remove}")
try:
# 解码图片获取尺寸
image_data = base64.b64decode(image_base64)
image = Image.open(io.BytesIO(image_data))
img_w, img_h = image.size
_logger.info(f"图片尺寸: {img_w}x{img_h} 像素")
# 构建坐标检测提示词(按照image-to-coordinate.py的格式)
prompt_text = f"""(仅归一化坐标,严格 JSON)
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
坐标系与输出要求:
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 目标文字有可能因为被遮挡而无法被准确识别,那么请根据可见部分的位置进行定位。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d。确保bbox_norm中有且仅有x1,y1,x2,y2四个参数,且每个参数都是浮点数,范围[0,1],保留4位小数。
输出 JSON 格式(严格按照示例的格式,实际数值请识别后填充):"""
prompt_text += '[{"text":"AGN","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"UCLINK","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LOGISITICS","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LTD","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}}]'
# 调用坐标检测API
api_start_time = time.time()
completion = self.client.chat.completions.create(
model="qwen3-vl-plus",
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": f"data:image/png;base64,{image_base64}"
},
},
{"type": "text", "text": prompt_text},
],
},
],
temperature=0.1,
)
api_end_time = time.time()
api_processing_time = api_end_time - api_start_time
_logger.info(f"AI坐标检测耗时: {api_processing_time:.2f}秒")
# 解析返回的坐标
raw_text = completion.choices[0].message.content
_logger.info(f"AI返回的坐标信息: {raw_text}")
result = self._safe_extract_json(raw_text)
if result is None or not isinstance(result, dict):
_logger.error("AI返回内容无法解析为JSON坐标")
return None
# 使用convert_ai_json_to_coords_map转换坐标
coords_map = self._convert_ai_json_to_coords_map(result, img_w, img_h)
if not coords_map:
_logger.error("无法从AI返回中提取矩形框坐标")
return None
_logger.info(f"解析并统一后的坐标字典: {coords_map}")
# 使用坐标信息涂抹文字
edited_image_base64 = self._erase_regions_on_image(image, coords_map, img_w, img_h)
total_time = time.time() - start_time
_logger.info(f"AI坐标检测和涂抹总耗时: {total_time:.2f}秒")
return edited_image_base64
except Exception as e:
_logger.error(f"AI坐标检测异常: {str(e)}")
return None
def download_and_convert_to_base64(self, image_url, timeout=300):
"""
下载图片并转换为base64
:param image_url: 图片URL
:param timeout: 超时时间
:return: base64编码的图片数据
"""
try:
response = requests.get(image_url, stream=True, timeout=timeout)
response.raise_for_status()
# 将图片内容转换为base64
image_data = response.content
image_base64 = base64.b64encode(image_data).decode('utf-8')
_logger.info("图片下载并转换为base64成功")
return image_base64
except requests.exceptions.RequestException as e:
_logger.error(f"图片下载失败: {str(e)}")
return None
def _safe_extract_json(self, text: str):
"""从模型返回文本中尽可能鲁棒地提取JSON对象(按照image-to-coordinate.py的逻辑)。"""
# 直接尝试解析
try:
if text.startswith("```json"):
text = text[7:-3].strip()
obj = json.loads(text)
if isinstance(obj, list):
return {'rects': obj}
return obj
except Exception:
pass
# 尝试提取首尾花括号之间的内容
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
candidate = text[start:end+1]
try:
return json.loads(candidate)
except Exception:
# 将单引号替换为双引号再试一次(有些模型会返回单引号JSON样式)
candidate2 = candidate.replace("'", '"')
try:
return json.loads(candidate2)
except Exception:
return None
# 尝试提取数组格式
start = text.find('[')
end = text.rfind(']')
if start != -1 and end != -1 and end > start:
candidate = text[start:end+1]
try:
obj = json.loads(candidate)
if isinstance(obj, list):
return {'rects': obj}
return obj
except Exception:
pass
return None
def _normalize_bbox(self, bbox, img_w, img_h):
"""
规范化bbox为像素坐标 [x1,y1,x2,y2],并保证边界有效。
支持:
- 像素坐标;
- 归一化坐标(0-1);
- [x1,y1,w,h] 形式(若x2<=x1或y2<=y1则按宽高处理)。
"""
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
return None
x1, y1, x2, y2 = bbox
# 如果可能是归一化坐标
if 0 <= x1 <= 1 and 0 <= y1 <= 1 and 0 <= x2 <= 1 and 0 <= y2 <= 1:
x1 = int(round(x1 * img_w))
y1 = int(round(y1 * img_h))
x2 = int(round(x2 * img_w))
y2 = int(round(y2 * img_h))
else:
# 像素坐标或 [x1,y1,w,h]
x1 = int(round(x1))
y1 = int(round(y1))
x2 = int(round(x2))
y2 = int(round(y2))
# 如果x2<=x1或y2<=y1,尝试将其视作宽高
if x2 <= x1 or y2 <= y1:
x2 = x1 + max(1, x2)
y2 = y1 + max(1, y2)
# 修正边界并加上少量边距
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
# 边距按短边的2%(最少5px,最多30px)
short_side = min(img_w, img_h)
margin = max(5, min(30, int(0.02 * short_side)))
x1 = max(0, x1 - margin)
y1 = max(0, y1 - margin)
x2 = min(img_w - 1, x2 + margin)
y2 = min(img_h - 1, y2 + margin)
return [x1, y1, x2, y2]
def _convert_ai_json_to_coords_map(self, result, img_w: int, img_h: int):
"""
将AI返回的JSON统一转换为 {label: [x1,y1,x2,y2]} 形式,兼容多种结构(按照image-to-coordinate.py的逻辑)
1) {"rects":[{"text":"AGN","bbox_norm":{x1,y1,x2,y2},"bbox_px":{x1,y1,x2,y2}}]}
2) {"AGN":[x1,y1,x2,y2], "UCLINK":[...], ...}
3) {"rects":[{"label":"AGN","bbox":[x1,y1,x2,y2]}]}
4) {"rects":[{"text":"AGN","x1":...,"y1":...,"x2":...,"y2":...}]}
返回值可以包含像素或归一化坐标,后续由 normalize_bbox 统一处理。
"""
coords_map = {}
def dict_to_list(b):
if isinstance(b, dict):
return [b.get("x1"), b.get("y1"), b.get("x2"), b.get("y2")]
return b
try:
# 情形A:顶层是dict
if isinstance(result, dict):
# A1:包含 rects 列表
if "rects" in result and isinstance(result["rects"], list):
for i, item in enumerate(result["rects"]):
if not isinstance(item, dict):
continue
label = item.get("text") or item.get("label") or item.get("word") or f"rect_{i}"
idx = item.get("occurrence_index")
key = f"{label}#{idx}" if isinstance(idx, int) and idx > 0 else label
bbox_px = dict_to_list(item.get("bbox_px") or item.get("bbox_pixels"))
bbox_norm = dict_to_list(item.get("bbox_norm"))
bbox_generic = dict_to_list(item.get("bbox"))
chosen = None
# 如果同时存在像素和归一化,做一致性校验
if isinstance(bbox_px, (list, tuple)) and len(bbox_px) == 4 and isinstance(bbox_norm, (list, tuple)) and len(bbox_norm) == 4:
try:
px_from_norm = [int(round(float(bbox_norm[0]) * img_w)),
int(round(float(bbox_norm[1]) * img_h)),
int(round(float(bbox_norm[2]) * img_w)),
int(round(float(bbox_norm[3]) * img_h))]
diff = sum(abs(px_from_norm[j] - int(round(float(bbox_px[j])))) for j in range(4))
chosen = bbox_px if diff <= 4 else bbox_norm
except Exception:
chosen = bbox_px
elif isinstance(bbox_px, (list, tuple)) and len(bbox_px) == 4:
chosen = bbox_px
elif isinstance(bbox_norm, (list, tuple)) and len(bbox_norm) == 4:
chosen = bbox_norm
elif isinstance(bbox_generic, (list, tuple)) and len(bbox_generic) == 4:
chosen = bbox_generic
else:
# 直接字段 x1,y1,x2,y2
if all(k in item for k in ("x1", "y1", "x2", "y2")):
chosen = [item.get("x1"), item.get("y1"), item.get("x2"), item.get("y2")]
if isinstance(chosen, (list, tuple)) and len(chosen) == 4:
coords_map[key] = list(chosen)
else:
_logger.warning(f"跳过无法解析的rect: {item}")
else:
# A2:简单键值对形式
for k, v in result.items():
if isinstance(v, (list, tuple)) and len(v) == 4:
coords_map[k] = list(v)
# 情形B:顶层是list
elif isinstance(result, list):
for i, item in enumerate(result):
if not isinstance(item, dict):
continue
label = item.get("text") or item.get("label") or item.get("word") or f"rect_{i}"
bbox = item.get("bbox_px") or item.get("bbox_norm") or item.get("bbox")
bbox = dict_to_list(bbox)
if isinstance(bbox, (list, tuple)) and len(bbox) == 4:
coords_map[label] = list(bbox)
else:
_logger.warning("AI返回的JSON结构未知,无法解析。")
except Exception as e:
_logger.error(f"解析AI JSON时发生错误: {e}")
return coords_map
def _erase_regions_on_image(self, image, coords_map, img_w, img_h):
"""
在图片上涂抹指定区域(按照image-to-coordinate.py的逻辑)
"""
try:
# 创建图片副本
img = image.convert('RGB')
draw = ImageDraw.Draw(img)
for key, bbox in coords_map.items():
nb = self._normalize_bbox(bbox, img_w, img_h)
if nb is None:
_logger.warning(f"坐标格式错误,跳过 {key}: {bbox}")
continue
x1, y1, x2, y2 = nb
# 用白色矩形覆盖
draw.rectangle([x1, y1, x2, y2], fill=(255, 255, 255))
_logger.info(f"已抹除 {key} 区域: {nb}")
# 将处理后的图片转换为base64
output = io.BytesIO()
img.save(output, format='PNG')
edited_image_base64 = base64.b64encode(output.getvalue()).decode('utf-8')
_logger.info("清理后的图片已处理完成")
return edited_image_base64
except Exception as e:
_logger.error(f"涂抹区域失败: {str(e)}")
return None
This source diff could not be displayed because it is too large. You can view the blob instead.
...@@ -9,24 +9,61 @@ ...@@ -9,24 +9,61 @@
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="Batch Get POD Info"> <!-- 批量获取POD信息 --> <form string="Batch Get POD Info"> <!-- 批量获取POD信息 -->
<sheet> <sheet>
<!-- <group> -->
<group> <group>
<group> <!-- attrs="{'invisible': [('pdf_file', '!=', False)]}" -->
<field name="sync_last_mile_pod" widget="boolean_toggle"/> <field name="remove_specified_text" readonly="1" widget="boolean_toggle"/>
<field name="skip_ocr_direct_ai" readonly="0" widget="boolean_toggle"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
</group> </group>
<group> <group attrs="{'invisible': ['|', ('pdf_file', '=', False), ('show_error_message', '=', False)]}">
<field name="remove_specified_text" widget="boolean_toggle"/> <field name="sync_successful_processed" widget="boolean_toggle"/>
</group> </group>
<group attrs="{'invisible': ['|', ('pdf_file', '=', False), ('sync_successful_processed', '=', False)]}">
<field name="sync_last_mile_pod" widget="boolean_toggle"/>
</group> </group>
<group attrs="{'invisible': ['|', ('pdf_file', '=', False), ('sync_successful_processed', '=', False)]}">
<field name="sync_match_node" widget="boolean_toggle"/>
</group>
<!-- </group> -->
<div class="alert alert-info" role="alert"> <div class="alert alert-info" role="alert">
<strong>Description:</strong> <!-- 说明: --> <strong>Description:</strong> <!-- 说明: -->
<ul> <ul>
<li><strong>Sync Last Mile POD:</strong> Synchronize POD (Proof of Delivery) attachment information with TK system, including big package quantities and container numbers</li> <!-- 同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息 --> <li>
<li><strong>Remove Specified Text:</strong> Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 --> <strong>Remove Specified Text:</strong>
Remove specified text (AGN, UCLINK LOGISITICS LTD) from PDF files
</li> <!-- 涂抹指定文字:对PDF文件中的指定文字进行涂抹处理 -->
<li attrs="{'invisible': [('sync_successful_processed', '=', False)]}">
<strong>Sync Last Mile POD:</strong>
Synchronize POD (Proof of Delivery) attachment information with TK system, including
big package quantities and container numbers
</li> <!-- 同步尾程POD:向TK同步尾程交接POD(待大包数量和箱号)的附件信息 -->
<li attrs="{'invisible': [('sync_successful_processed', '=', False)]}">
<strong>Sync Push Match Node:</strong>
Synchronize and push matched node information based on POD file, extract time from
red boxes as node operation time
</li> <!-- 同步推送匹配节点:根据POD文件获取对应的清关节点,提取红色框时间作为节点操作时间 -->
</ul> </ul>
</div> </div>
<div class="alert alert-danger" role="alert"
attrs="{'invisible': [('show_error_message', '=', False)]}">
<field name="show_error_message"/>
</div>
<div>
<field name="pdf_file" filename="pdf_filename" widget="pdf_viewer" readonly="1"
attrs="{'invisible': [('pdf_file', '=', False)]}"/>
</div>
<footer> <footer>
<button string="Confirm" type="object" name="confirm" class="btn-primary"/> <!-- 预览按钮:处理PDF并显示合并后的文件 -->
<button string="Preview" type="object" name="action_preview" class="btn-primary"
attrs="{'invisible': [('pdf_file', '!=', False)]}"/>
<!-- 确认按钮:使用已处理的文件数据进行回写和同步 -->
<!-- 如果有失败的文件(show_error_message不为空),需要勾选sync_successful_processed才显示;如果全部成功,直接显示 -->
<button string="Confirm" type="object" name="confirm" class="btn-primary"
attrs="{'invisible': ['|', ('pdf_file', '=', False), '&amp;', ('show_error_message', '!=', False), ('sync_successful_processed', '=', False)]}"/>
<button string="Close" special="cancel"/> <button string="Close" special="cancel"/>
</footer> </footer>
</sheet> </sheet>
......
import os
from openai import OpenAI
import requests
import mimetypes
import base64
import fitz # PyMuPDF
import json
from PIL import Image, ImageDraw
import time
begin_time = time.time()
client = OpenAI(
# 若没有配置环境变量,请用阿里云百炼API Key将下行替换为:api_key="sk-xxx",
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1',
# 以下是北京地域base_url,如果使用新加坡地域的模型,需要将base_url替换为:https://dashscope-intl.aliyuncs.com/compatible-mode/v1
base_url="https://dashscope.aliyuncs.com/compatible-mode/v1",
)
pdf_path = "./43610236590 (3).pdf"
def pdf_to_images(pdf_path, output_dir='./pdf_pages', dpi=150):
"""
将PDF文件逐页转为PNG图片
:param pdf_path: PDF文件路径
:param output_dir: 输出目录,默认当前目录下pdf_pages文件夹
:param dpi: 渲染分辨率,默认150
:return: 生成的图片路径列表
"""
os.makedirs(output_dir, exist_ok=True)
doc = fitz.open(pdf_path)
image_paths = []
print(f"PDF总页数: {len(doc)}")
for page_num in range(len(doc)):
page = doc.load_page(page_num)
mat = fitz.Matrix(dpi / 72, dpi / 72)
pix = page.get_pixmap(matrix=mat)
output_path = os.path.join(output_dir, f"page_{page_num + 1}.png")
pix.save(output_path)
image_paths.append(output_path)
doc.close()
return image_paths
def download_image(image_url, save_path='output.png'):
try:
response = requests.get(image_url, stream=True, timeout=300) # 设置超时
response.raise_for_status() # 如果HTTP状态码不是200,则引发异常
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"图像已成功下载到: {save_path}")
except requests.exceptions.RequestException as e:
print(f"图像下载失败: {e}")
def encode_file(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type or not mime_type.startswith("image/"):
raise ValueError("不支持或无法识别的图像格式")
with open(file_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
def safe_extract_json(text: str):
"""从模型返回文本中尽可能鲁棒地提取JSON对象。"""
# 直接尝试解析
try:
if text.startswith("```json"):
text = text[7:-3].strip()
obj = json.loads(text)
if isinstance(obj, list):
return {'rects': obj}
return obj
except Exception:
pass
print(text)
# 尝试提取首尾花括号之间的内容
start = text.find('{')
end = text.rfind('}')
if start != -1 and end != -1 and end > start:
candidate = text[start:end+1]
try:
return json.loads(candidate)
except Exception:
# 将单引号替换为双引号再试一次(有些模型会返回单引号JSON样式)
candidate2 = candidate.replace("'", '"')
try:
return json.loads(candidate2)
except Exception:
return None
return None
def normalize_bbox(bbox, img_w, img_h):
"""
规范化bbox为像素坐标 [x1,y1,x2,y2],并保证边界有效。
支持:
- 像素坐标;
- 归一化坐标(0-1);
- [x1,y1,w,h] 形式(若x2<=x1或y2<=y1则按宽高处理)。
"""
if not isinstance(bbox, (list, tuple)) or len(bbox) != 4:
return None
x1, y1, x2, y2 = bbox
# 如果可能是归一化坐标
if 0 <= x1 <= 1 and 0 <= y1 <= 1 and 0 <= x2 <= 1 and 0 <= y2 <= 1:
x1 = int(round(x1 * img_w))
y1 = int(round(y1 * img_h))
x2 = int(round(x2 * img_w))
y2 = int(round(y2 * img_h))
else:
# 像素坐标或 [x1,y1,w,h]
x1 = int(round(x1))
y1 = int(round(y1))
x2 = int(round(x2))
y2 = int(round(y2))
# 如果x2<=x1或y2<=y1,尝试将其视作宽高
if x2 <= x1 or y2 <= y1:
x2 = x1 + max(1, x2)
y2 = y1 + max(1, y2)
# 修正边界并加上少量边距
if x1 > x2:
x1, x2 = x2, x1
if y1 > y2:
y1, y2 = y2, y1
# 边距按短边的2%(最少5px,最多30px)
short_side = min(img_w, img_h)
margin = max(5, min(30, int(0.02 * short_side)))
x1 = max(0, x1 - margin)
y1 = max(0, y1 - margin)
x2 = min(img_w - 1, x2 + margin)
y2 = min(img_h - 1, y2 + margin)
return [x1, y1, x2, y2]
def erase_regions_on_image(image_path: str, coords_map: dict, save_path: str):
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"坐标格式错误,跳过 {key}: {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], fill=(255, 255, 255))
print(f"已抹除 {key} 区域: {nb}")
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"清理后的图片已保存: {save_path}")
def draw_debug_boxes(image_path: str, coords_map: dict, save_path: str):
"""在原图上绘制预测的矩形框用于人工核对。"""
img = Image.open(image_path).convert('RGB')
draw = ImageDraw.Draw(img)
w, h = img.size
for key, bbox in coords_map.items():
nb = normalize_bbox(bbox, w, h)
if nb is None:
print(f"跳过无法解析的坐标: {key} -> {bbox}")
continue
x1, y1, x2, y2 = nb
draw.rectangle([x1, y1, x2, y2], outline=(255, 0, 0), width=3)
draw.text((x1, max(0, y1 - 16)), key, fill=(255, 0, 0))
os.makedirs(os.path.dirname(save_path), exist_ok=True)
img.save(save_path)
print(f"调试框已生成: {save_path}")
def convert_ai_json_to_coords_map(result, img_w: int, img_h: int) -> dict:
"""
将AI返回的JSON统一转换为 {label: [x1,y1,x2,y2]} 形式,兼容多种结构:
1) {"rects":[{"text":"AGN","bbox_norm":{x1,y1,x2,y2},"bbox_px":{x1,y1,x2,y2}}]}
2) {"AGN":[x1,y1,x2,y2], "UCLINK":[...], ...}
3) {"rects":[{"label":"AGN","bbox":[x1,y1,x2,y2]}]}
4) {"rects":[{"text":"AGN","x1":...,"y1":...,"x2":...,"y2":...}]}
返回值可以包含像素或归一化坐标,后续由 normalize_bbox 统一处理。
"""
coords_map: dict = {}
def dict_to_list(b):
if isinstance(b, dict):
return [b.get("x1"), b.get("y1"), b.get("x2"), b.get("y2")]
return b
try:
# 情形A:顶层是dict
if isinstance(result, dict):
# A1:包含 rects 列表
if "rects" in result and isinstance(result["rects"], list):
for i, item in enumerate(result["rects"]):
if not isinstance(item, dict):
continue
label = item.get("text") or item.get("label") or item.get("word") or f"rect_{i}"
idx = item.get("occurrence_index")
key = f"{label}#{idx}" if isinstance(idx, int) and idx > 0 else label
bbox_px = dict_to_list(item.get("bbox_px") or item.get("bbox_pixels"))
bbox_norm = dict_to_list(item.get("bbox_norm"))
bbox_generic = dict_to_list(item.get("bbox"))
chosen = None
# 如果同时存在像素和归一化,做一致性校验
if isinstance(bbox_px, (list, tuple)) and len(bbox_px) == 4 and isinstance(bbox_norm, (list, tuple)) and len(bbox_norm) == 4:
try:
px_from_norm = [int(round(float(bbox_norm[0]) * img_w)),
int(round(float(bbox_norm[1]) * img_h)),
int(round(float(bbox_norm[2]) * img_w)),
int(round(float(bbox_norm[3]) * img_h))]
diff = sum(abs(px_from_norm[j] - int(round(float(bbox_px[j])))) for j in range(4))
chosen = bbox_px if diff <= 4 else bbox_norm
except Exception:
chosen = bbox_px
elif isinstance(bbox_px, (list, tuple)) and len(bbox_px) == 4:
chosen = bbox_px
elif isinstance(bbox_norm, (list, tuple)) and len(bbox_norm) == 4:
chosen = bbox_norm
elif isinstance(bbox_generic, (list, tuple)) and len(bbox_generic) == 4:
chosen = bbox_generic
else:
# 直接字段 x1,y1,x2,y2
if all(k in item for k in ("x1", "y1", "x2", "y2")):
chosen = [item.get("x1"), item.get("y1"), item.get("x2"), item.get("y2")]
if isinstance(chosen, (list, tuple)) and len(chosen) == 4:
coords_map[key] = list(chosen)
else:
print(f"跳过无法解析的rect: {item}")
else:
# A2:简单键值对形式
for k, v in result.items():
if isinstance(v, (list, tuple)) and len(v) == 4:
coords_map[k] = list(v)
# 情形B:顶层是list
elif isinstance(result, list):
for i, item in enumerate(result):
if not isinstance(item, dict):
continue
label = item.get("text") or item.get("label") or item.get("word") or f"rect_{i}"
bbox = item.get("bbox_px") or item.get("bbox_norm") or item.get("bbox")
bbox = dict_to_list(bbox)
if isinstance(bbox, (list, tuple)) and len(bbox) == 4:
coords_map[label] = list(bbox)
else:
print("AI返回的JSON结构未知,无法解析。")
except Exception as e:
print(f"解析AI JSON时发生错误: {e}")
return coords_map
def images_to_pdf(image_paths, output_pdf):
os.makedirs(os.path.dirname(output_pdf), exist_ok=True)
pil_images = [Image.open(p).convert('RGB') for p in image_paths]
if not pil_images:
raise RuntimeError("没有需要写入PDF的图片")
first = pil_images[0]
rest = pil_images[1:]
first.save(output_pdf, save_all=True, append_images=rest)
print(f"已生成PDF: {output_pdf}")
image_paths = pdf_to_images(pdf_path)
final_images = []
location_map = ''
for index, image_path in enumerate(image_paths):
image_base64 = encode_file(image_path)
# 获取图片分辨率,并在提示中要求模型按比例(相对宽高的0-1浮点数)返回坐标
img_w, img_h = Image.open(image_path).size
print(f"页面尺寸: {img_w}x{img_h} 像素")
text = f"""(仅归一化坐标,严格 JSON)
你是一名版面定位助手。请在下图中定位并分别框出以下四个单词:AGN、UCLINK、LOGISITICS、LTD。
坐标系与输出要求:
- 图像尺寸:宽 {img_w} 像素,高 {img_h} 像素。
- 原点位于图像左上角;x 向右增大,y 向下增大。
- 为每个目标词返回它的最小外接矩形框,边界紧贴字形,不要添加额外边距。
- 返回坐标为相对宽高的归一化浮点数,范围 [0,1],保留 4 位小数;保证 0 ≤ x1 < x2 ≤ 1,0 ≤ y1 < y2 ≤ 1。
- 禁止任何图片预处理(裁剪、缩放、加边距、重采样);坐标必须对应原始图像。
- 目标文字有可能因为被遮挡而无法被准确识别,那么请根据可见部分的位置进行定位。
- 严格只输出下面的压缩的 JSON,不要附加解释或其他文本。
- JSON中不要出现不在实例中的参数,例如bbox_2d。确保bbox_norm中有且仅有x1,y1,x2,y2四个参数,且每个参数都是浮点数,范围[0,1],保留4位小数。
输出 JSON 格式(严格按照示例的格式):"""
text += '[{"text":"AGN","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"UCLINK","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LOGISITICS","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}},{"text":"LTD","bbox_norm":{"x1":0.0000,"y1":0.0000,"x2":0.0000,"y2":0.0000}}]'
completion = client.chat.completions.create(
model="qwen3-vl-plus", # 此处以qwen3-vl-plus为例,可按需更换模型名称。模型列表:https://help.aliyun.com/zh/model-studio/models
messages=[
{
"role": "user",
"content": [
{
"type": "image_url",
"image_url": {
"url": image_base64
},
},
{"type": "text", "text": text},
],
},
],
temperature=0.1,
)
raw_text = completion.choices[0].message.content
# raw_text = '```json[{"bbox_norm": {"x1": 0.1028, "y1": 0.1934, "x2": 0.1325, "y2": 0.2006}, "text": "AGN", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1028, "y1": 0.2057, "x2": 0.1608, "y2": 0.2165}, "text": "UCLINK", "occurrence_index": 0},{"bbox_norm": {"x1": 0.1677, "y1": 0.2057, "x2": 0.2657, "y2": 0.2165}, "text": "LOGISITICS", "occurrence_index": 0},{"bbox_norm": {"x1": 0.2726, "y1": 0.2057, "x2": 0.3023, "y2": 0.2165}, "text": "LTD", "occurrence_index": 0}]```'
print(raw_text)
result = safe_extract_json(raw_text)
if not location_map:
location_map = json.dumps(result['rects'], ensure_ascii=False)
if result is None or not isinstance(result, dict):
raise RuntimeError("模型返回内容无法解析为JSON坐标,请检查返回格式。")
# 只处理第一页:将抹除后的图片写入 output/cleaned_page_1.png,然后重新生成PDF
cleaned_dir = os.path.join("./output")
cleaned_first = os.path.join(cleaned_dir, f"cleaned_page_{index}.png")
debug_first = os.path.join(cleaned_dir, f"debug_page_{index}.png")
coords_map = convert_ai_json_to_coords_map(result, img_w, img_h)
if not coords_map:
raise RuntimeError("无法从AI返回中提取矩形框坐标,请检查输出格式或提示词。")
print(f"解析并统一后的坐标字典: {coords_map}")
draw_debug_boxes(image_path, coords_map, debug_first)
erase_regions_on_image(image_path, coords_map, cleaned_first)
# 合成PDF:第一页使用清理后的图片,其余页沿用原图
final_images.append(cleaned_first)
images_to_pdf(final_images, os.path.join(cleaned_dir, f"cleaned_page.pdf"))
end_time = time.time()
print(f"耗时: {end_time - begin_time} 秒")
import json
import os
from dashscope import MultiModalConversation
import dashscope
import base64
import requests
dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'
image_path = "./图片识别2.png"
def download_image(image_url, save_path='output.png'):
try:
response = requests.get(image_url, stream=True, timeout=300) # 设置超时
response.raise_for_status() # 如果HTTP状态码不是200,则引发异常
with open(save_path, 'wb') as f:
for chunk in response.iter_content(chunk_size=8192):
f.write(chunk)
print(f"图像已成功下载到: {save_path}")
except requests.exceptions.RequestException as e:
print(f"图像下载失败: {e}")
with open(image_path, "rb") as image_file:
image_base64 = base64.b64encode(image_file.read()).decode('utf-8')
# 模型支持输入1-3张图片
messages = [
{
"role": "user",
"content": [
{"image": f"data:image/png;base64,{image_base64}"},
{"text": "将图片中的AGN UCLINK LOGISITICS LTD这一段文字抹去"}
]
}
]
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
# 若没有配置环境变量,请用百炼 API Key 将下行替换为:api_key="sk-xxx"
# api_key = os.getenv("DASHSCOPE_API_KEY")
# 模型仅支持单轮对话,复用了多轮对话的接口
response = MultiModalConversation.call(
api_key='sk-e41914f0d9c94035a5ae1322e9a61fb1',
model="qwen-image-edit",
messages=messages,
stream=False,
watermark=False,
negative_prompt=" "
)
if response.status_code == 200:
# 如需查看完整响应,请取消下行注释
# print(json.dumps(response, ensure_ascii=False))
print("输出图像的URL:", response.output.choices[0].message.content[0]['image'])
image_url = response.output.choices[0].message.content[0]['image']
download_image(image_url, save_path='处理图片.png')
else:
print(f"HTTP返回码:{response.status_code}")
print(f"错误码:{response.code}")
print(f"错误信息:{response.message}")
print("请参考文档:https://help.aliyun.com/zh/model-studio/developer-reference/error-code")
import base64
import mimetypes
from http import HTTPStatus
from urllib.parse import urlparse, unquote
from pathlib import PurePosixPath
import dashscope
import requests
from dashscope import ImageSynthesis
import os
# 以下为北京地域url,若使用新加坡地域的模型,需将url替换为:https://dashscope-intl.aliyuncs.com/api/v1
dashscope.base_http_api_url = 'https://dashscope.aliyuncs.com/api/v1'
# 若没有配置环境变量,请用百炼API Key将下行替换为:api_key="sk-xxx"
# 新加坡和北京地域的API Key不同。获取API Key:https://help.aliyun.com/zh/model-studio/get-api-key
api_key = 'sk-e41914f0d9c94035a5ae1322e9a61fb1'
# --- 输入图片:使用 Base64 编码 ---
# base64编码格式为 data:{MIME_type};base64,{base64_data}
def encode_file(file_path):
mime_type, _ = mimetypes.guess_type(file_path)
if not mime_type or not mime_type.startswith("image/"):
raise ValueError("不支持或无法识别的图像格式")
with open(file_path, "rb") as image_file:
encoded_string = base64.b64encode(image_file.read()).decode('utf-8')
return f"data:{mime_type};base64,{encoded_string}"
"""
图像输入方式说明:
以下提供了三种图片输入方式,三选一即可
1. 使用公网URL - 适合已有公开可访问的图片
2. 使用本地文件 - 适合本地开发测试
3. 使用Base64编码 - 适合私有图片或需要加密传输的场景
"""
# 【方式一】使用公网图片 URL
# image_url_1 = "https://img.alicdn.com/imgextra/i4/O1CN01TlDlJe1LR9zso3xAC_!!6000000001295-2-tps-1104-1472.png"
# image_url_2 = "https://img.alicdn.com/imgextra/i4/O1CN01M9azZ41YdblclkU6Z_!!6000000003082-2-tps-1696-960.png"
# 【方式二】使用本地文件(支持绝对路径和相对路径)
# 格式要求:file:// + 文件路径
# 示例(绝对路径):
# image_url_1 = "file://" + "/path/to/your/image_1.png" # Linux/macOS
# image_url_2 = "file://" + "C:/path/to/your/image_2.png" # Windows
# 示例(相对路径):
# image_url_1 = "file://" + "./image_1.png" # 以实际路径为准
# image_url_2 = "file://" + "./image_1.png" # 以实际路径为准
# 【方式三】使用Base64编码的图片
image_url_1 = encode_file("./图片识别2.png") # 以实际路径为准
# image_url_2 = encode_file("./image_2.png") # 以实际路径为准
print('----sync call, please wait a moment----')
rsp = ImageSynthesis.call(api_key=api_key,
model="wan2.5-i2i-preview",
prompt="将图片中的 AGN 跟 UCLINK LOGISITICS LTD 这两段文字抹去, 确保其他的内容都保留,包括水印等",
images=[image_url_1],
negative_prompt="",
n=1,
watermark=False,
seed=12345)
print('response: %s' % rsp)
if rsp.status_code == HTTPStatus.OK:
# 在当前目录下保存图片
for result in rsp.output.results:
file_name = PurePosixPath(unquote(urlparse(result.url).path)).parts[-1]
with open('./%s' % file_name, 'wb+') as f:
f.write(requests.get(result.url).content)
else:
print('sync_call Failed, status_code: %s, code: %s, message: %s' %
(rsp.status_code, rsp.code, rsp.message))
\ No newline at end of file
...@@ -6,8 +6,8 @@ msgid "" ...@@ -6,8 +6,8 @@ msgid ""
msgstr "" msgstr ""
"Project-Id-Version: Odoo Server 16.0\n" "Project-Id-Version: Odoo Server 16.0\n"
"Report-Msgid-Bugs-To: \n" "Report-Msgid-Bugs-To: \n"
"POT-Creation-Date: 2025-09-23 09:31+0000\n" "POT-Creation-Date: 2025-10-31 02:15+0000\n"
"PO-Revision-Date: 2025-09-23 17:35+0800\n" "PO-Revision-Date: 2025-10-31 10:16+0800\n"
"Last-Translator: \n" "Last-Translator: \n"
"Language-Team: \n" "Language-Team: \n"
"Language: zh_CN\n" "Language: zh_CN\n"
...@@ -54,6 +54,11 @@ msgstr "" ...@@ -54,6 +54,11 @@ msgstr ""
msgid "Attachment" msgid "Attachment"
msgstr "附件" msgstr "附件"
#. module: ccs_connect_tiktok
#: model:ir.model,name:ccs_connect_tiktok.model_batch_get_pod_info_wizard
msgid "Batch Get POD Info Wizard"
msgstr "批量获取尾程POD向导"
#. module: ccs_connect_tiktok #. module: ccs_connect_tiktok
#: model:ir.actions.server,name:ccs_connect_tiktok.action_batch_sync_bl_status #: model:ir.actions.server,name:ccs_connect_tiktok.action_batch_sync_bl_status
msgid "Batch Sync Bill Of Loading Status" msgid "Batch Sync Bill Of Loading Status"
...@@ -396,14 +401,6 @@ msgstr "托盘" ...@@ -396,14 +401,6 @@ msgstr "托盘"
msgid "Pallet Handover" msgid "Pallet Handover"
msgstr "托盘交货" msgstr "托盘交货"
#. module: ccs_connect_tiktok
#. odoo-python
#: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0
#: model:ir.model.fields.selection,name:ccs_connect_tiktok.selection__pda_scan_record__operation__pallet_pickup
#, python-format
msgid "Pallet Pickup"
msgstr "托盘提货"
#. module: ccs_connect_tiktok #. module: ccs_connect_tiktok
#. odoo-python #. odoo-python
#: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0 #: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0
...@@ -442,6 +439,11 @@ msgstr "进度" ...@@ -442,6 +439,11 @@ msgstr "进度"
msgid "Progress Name" msgid "Progress Name"
msgstr "进度名称" msgstr "进度名称"
#. module: ccs_connect_tiktok
#: model_terms:ir.ui.view,arch_db:ccs_connect_tiktok.search_cc_bl_view
msgid "Push Failed"
msgstr "推送失败"
#. module: ccs_connect_tiktok #. module: ccs_connect_tiktok
#: model:ir.model,name:ccs_connect_tiktok.model_cc_ship_package #: model:ir.model,name:ccs_connect_tiktok.model_cc_ship_package
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_cc_ship_package_sync_log__package_id #: model:ir.model.fields,field_description:ccs_connect_tiktok.field_cc_ship_package_sync_log__package_id
...@@ -451,6 +453,11 @@ msgstr "进度名称" ...@@ -451,6 +453,11 @@ msgstr "进度名称"
msgid "Ship Package" msgid "Ship Package"
msgstr "小包" msgstr "小包"
#. module: ccs_connect_tiktok
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_cc_bl__push_remark
msgid "Ship Package Push Remark"
msgstr "小包推送备注"
#. module: ccs_connect_tiktok #. module: ccs_connect_tiktok
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_batch_input_ship_package_status_wizard__is_skip_check #: model:ir.model.fields,field_description:ccs_connect_tiktok.field_batch_input_ship_package_status_wizard__is_skip_check
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_update_bl_status_wizard__is_skip_check #: model:ir.model.fields,field_description:ccs_connect_tiktok.field_update_bl_status_wizard__is_skip_check
...@@ -565,14 +572,6 @@ msgstr "进度编码" ...@@ -565,14 +572,6 @@ msgstr "进度编码"
msgid "Tail Handover" msgid "Tail Handover"
msgstr "按尾程交货" msgstr "按尾程交货"
#. module: ccs_connect_tiktok
#. odoo-python
#: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0
#: model:ir.model.fields.selection,name:ccs_connect_tiktok.selection__pda_scan_record__operation__tail_pickup
#, python-format
msgid "Tail Pickup"
msgstr ""
#. module: ccs_connect_tiktok #. module: ccs_connect_tiktok
#. odoo-python #. odoo-python
#: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0 #: code:addons/ccs_connect_tiktok/models/pda_scan_record.py:0
......
...@@ -351,6 +351,7 @@ class CcBl(models.Model): ...@@ -351,6 +351,7 @@ class CcBl(models.Model):
bl_sync_log_ids = fields.One2many('cc.bl.sync.log', 'bl_id', string='Bill Of Loading Sync Logs') bl_sync_log_ids = fields.One2many('cc.bl.sync.log', 'bl_id', string='Bill Of Loading Sync Logs')
# 增加提单状态操作时间:取最新一条提单节点同步信息的操作时间 # 增加提单状态操作时间:取最新一条提单节点同步信息的操作时间
process_time = fields.Datetime(string='Process Time', compute='_compute_process_time', store=True) process_time = fields.Datetime(string='Process Time', compute='_compute_process_time', store=True)
push_remark = fields.Text('Ship Package Push Remark', default='')
def change_state_by_ship_package(self): def change_state_by_ship_package(self):
""" """
...@@ -457,7 +458,7 @@ class CcBl(models.Model): ...@@ -457,7 +458,7 @@ class CcBl(models.Model):
raise ValidationError( raise ValidationError(
_('The small package node or bill of lading node is not in the completed node, and the bill of lading cannot be changed to completed!')) # 小包节点或提单节点不在已完成节点,提单不能变为已完成! _('The small package node or bill of lading node is not in the completed node, and the bill of lading cannot be changed to completed!')) # 小包节点或提单节点不在已完成节点,提单不能变为已完成!
def done_func(self, is_email=False,**kwargs): def done_func(self, is_email=False, **kwargs):
""" """
变为已完成.先进行提单巡查,再进行提单状态变更 变为已完成.先进行提单巡查,再进行提单状态变更
""" """
...@@ -475,7 +476,7 @@ class CcBl(models.Model): ...@@ -475,7 +476,7 @@ class CcBl(models.Model):
content = self.get_patrol_email_content(result) content = self.get_patrol_email_content(result)
raise ValidationError(content) raise ValidationError(content)
if is_success or kwargs.get('exception_reason'): if is_success or kwargs.get('exception_reason'):
super(CcBl, self).done_func(is_email=is_email,**kwargs) super(CcBl, self).done_func(is_email=is_email, **kwargs)
def check_bl_patrol(self): def check_bl_patrol(self):
""" """
...@@ -994,7 +995,7 @@ class CcBl(models.Model): ...@@ -994,7 +995,7 @@ class CcBl(models.Model):
create_sync_log_value_arr = [] # 创建小包同步日志数据 create_sync_log_value_arr = [] # 创建小包同步日志数据
create_api_log_value_arr = [] # 创建api_log日志数据 create_api_log_value_arr = [] # 创建api_log日志数据
utc_time = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S') utc_time = datetime.utcnow().strftime('%Y-%m-%d %H:%M:%S')
user_id = self.env.user.id user_id = user_obj.id if user_obj else self.env.user.id
for response_item in responses: for response_item in responses:
response_data = response_item[0] response_data = response_item[0]
logging.info('ship response_data:%s' % response_data) logging.info('ship response_data:%s' % response_data)
...@@ -1226,7 +1227,7 @@ class CcBl(models.Model): ...@@ -1226,7 +1227,7 @@ class CcBl(models.Model):
return False return False
def mail_auto_push(self, mail_time=False, tally_ship_packages=[], action_type='tally', mail_db_user='邮件接收', def mail_auto_push(self, mail_time=False, tally_ship_packages=[], action_type='tally', mail_db_user='邮件接收',
pda_db_user='pda'): pda_db_user='pda', pod_node_id=False):
self = self.with_context(dict(self._context, is_mail=True)) self = self.with_context(dict(self._context, is_mail=True))
for item in self: for item in self:
try: try:
...@@ -1249,14 +1250,7 @@ class CcBl(models.Model): ...@@ -1249,14 +1250,7 @@ class CcBl(models.Model):
user_obj = self.env['res.users'].search([('login', '=', pda_db_user)], limit=1) user_obj = self.env['res.users'].search([('login', '=', pda_db_user)], limit=1)
ship_package_ids = [ship_package_dict for sublist in [d['id'] for d in ship_packages] for ship_package_ids = [ship_package_dict for sublist in [d['id'] for d in ship_packages] for
ship_package_dict in sublist] ship_package_dict in sublist]
tally_state = 'checked_goods' if action_type == 'tally' else ( node_obj = False
'picked_up' if action_type == 'pickup' else 'handover_completed')
# 后续节点
node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'),
('tally_state', '=', tally_state) # 检查理货或尾程交接的节点,根据排序进行升序
], order='seq asc')
if node_obj:
all_ship_package_obj = self.env['cc.ship.package'].search( all_ship_package_obj = self.env['cc.ship.package'].search(
[('id', 'in', ship_package_ids)]) # 所有小包 [('id', 'in', ship_package_ids)]) # 所有小包
# 预先获取所有同步日志 - 批量查询 # 预先获取所有同步日志 - 批量查询
...@@ -1279,8 +1273,24 @@ class CcBl(models.Model): ...@@ -1279,8 +1273,24 @@ class CcBl(models.Model):
for single_id in package['id']: for single_id in package['id']:
ship_packages_dict[single_id] = package['tally_time'] ship_packages_dict[single_id] = package['tally_time']
if action_type == 'push_match_node' and pod_node_id:
# 尾程匹配的节点包括前序节点
node_obj = self.env['cc.node'].sudo().search([('id', '=', pod_node_id)])
else:
tally_state = 'checked_goods' if action_type == 'tally' else (
'picked_up' if action_type == 'pickup' else 'handover_completed')
# 检查理货或尾程交接的节点,根据排序进行升序
node_obj = self.env['cc.node'].sudo().search([
('node_type', '=', 'package'),
('tally_state', '=', tally_state)
], order='seq asc')
if not node_obj:
return True
# 前序节点 理货或尾程交接之前没有生成的节点 # 前序节点 理货或尾程交接之前没有生成的节点
before_node_obj = node_obj[0].get_before_node() before_node_obj = node_obj[0].get_before_node()
if pod_node_id:
# 合并前序节点和当前节点,去重
before_node_obj = before_node_obj | node_obj[0]
# 理货或尾程交接之前没有生成的节点 # 理货或尾程交接之前没有生成的节点
for before_node in before_node_obj: for before_node in before_node_obj:
before_minutes = before_node.calculate_total_interval(node_obj[0]) before_minutes = before_node.calculate_total_interval(node_obj[0])
...@@ -1292,10 +1302,11 @@ class CcBl(models.Model): ...@@ -1292,10 +1302,11 @@ class CcBl(models.Model):
package_id, set()): package_id, set()):
tally_time = ship_packages_dict.get(package_id) tally_time = ship_packages_dict.get(package_id)
if tally_time: if tally_time:
operation_time = ( tally_time = datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S')
datetime.strptime(tally_time, '%Y-%m-%d %H:%M:%S') - timedelta( operation_time = tally_time if pod_node_id and pod_node_id == before_node.id else ((
tally_time - timedelta(
minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta( minutes=before_minutes)) if tally_time else fields.Datetime.now() - timedelta(
minutes=before_minutes) minutes=before_minutes))
update_data.append(( update_data.append((
package_id, package_id,
before_node.id, before_node.id,
...@@ -1307,7 +1318,8 @@ class CcBl(models.Model): ...@@ -1307,7 +1318,8 @@ class CcBl(models.Model):
if update_data: if update_data:
# 构建批量更新SQL # 构建批量更新SQL
values_str = ','.join( values_str = ','.join(
self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in update_data) self.env.cr.mogrify("(%s,%s,%s,%s,%s)", row).decode('utf-8') for row in
update_data)
sql = """ sql = """
UPDATE cc_ship_package AS t SET UPDATE cc_ship_package AS t SET
state = c.state, state = c.state,
...@@ -1325,6 +1337,8 @@ class CcBl(models.Model): ...@@ -1325,6 +1337,8 @@ class CcBl(models.Model):
item.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids, item.try_callback_track(max_retries=2, ship_package_ids=ship_package_ids,
user_obj=user_obj) user_obj=user_obj)
# 理货或尾程交接的节点 # 理货或尾程交接的节点
# pda需要生成后续节点
if action_type != 'push_match_node':
# 预先获取所有状态节点 # 预先获取所有状态节点
all_state_nodes = self.env['cc.node'].sudo().search([ all_state_nodes = self.env['cc.node'].sudo().search([
('node_type', '=', 'package') ('node_type', '=', 'package')
......
...@@ -53,6 +53,9 @@ ...@@ -53,6 +53,9 @@
</div> </div>
</button> </button>
</button> </button>
<field name="process_time" position="after">
<field name="push_remark" readonly="1" attrs="{'invisible': [('push_remark', '=', '')]}"/>
</field>
<notebook position="inside"> <notebook position="inside">
<page string="Sync Log"> <page string="Sync Log">
<field name="bl_sync_log_ids" widget="one2many_list"/> <field name="bl_sync_log_ids" widget="one2many_list"/>
...@@ -65,6 +68,20 @@ ...@@ -65,6 +68,20 @@
</field> </field>
</record> </record>
<record model="ir.ui.view" id="search_cc_bl_view">
<field name="name">search.cc.bl</field>
<field name="model">cc.bl</field>
<field name="inherit_id" ref="ccs_base.search_cc_bl_view"/>
<field name="arch" type="xml">
<filter name="filter_state_not_finished" position="after">
<separator/>
<filter string="Push Failed" name="filter_push_failed" domain="[('push_remark', '!=', '')]"/>
</filter>
</field>
</record>
<record id="action_batch_sync_package_status" model="ir.actions.server"> <record id="action_batch_sync_package_status" model="ir.actions.server">
<field name="name">Batch Sync Package Status</field> <field name="name">Batch Sync Package Status</field>
<field name="model_id" ref="model_cc_bl"/> <field name="model_id" ref="model_cc_bl"/>
......
...@@ -3,4 +3,5 @@ ...@@ -3,4 +3,5 @@
from . import batch_input_ship_package_statu_wizard from . import batch_input_ship_package_statu_wizard
from . import update_bl_status_wizard from . import update_bl_status_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
from odoo import models, fields, api, _
import pytz
from datetime import datetime
_logger = logging.getLogger(__name__)
from odoo.exceptions import ValidationError
class BatchGetPodInfoWizard(models.TransientModel):
_inherit = 'batch.get.pod.info.wizard'
_description = 'Batch Get POD Info Wizard' # 批量获取POD信息向导
def _validate_node_push_conditions(self, processed_files):
"""
验证节点推送条件(简化版本)
第一步:对比小包当前节点的操作时间是否小于提取时间(同时区对比)
第二步:如果是需要补推的,提取时间 − 小包当前节点的操作时间 < 前序间隔时间的不能推送
:param processed_files: 处理后的文件数组
:return: 满足条件的文件数组
"""
valid_files = []
for file_info in processed_files:
if not file_info.get('bl'):
continue
bl = file_info['bl']
_logger.info(f"开始验证提单 {bl.bl_no} 的节点推送条件")
try:
# 获取提取时间
extracted_times = self._extract_time_from_pdf(
file_info.get('file_data'),
bl.bl_no,
file_info.get('ocr_texts')
)
if not extracted_times:
_logger.warning(f"提单 {bl.bl_no} 未提取到时间信息,跳过节点推送")
continue
# 取最早时间作为提取时间(用户时区的本地时间)
extract_time_local = min(extracted_times)
_logger.info(f"提单 {bl.bl_no} 最早提取时间(本地): {extract_time_local}")
# 将提取时间从用户时区转换为UTC(0时区)
extract_time = self._convert_local_time_to_utc(extract_time_local)
_logger.info(f"提单 {bl.bl_no} 最早提取时间(UTC): {extract_time}")
# 获取小包信息
ship_packages = bl.ship_package_ids
if not ship_packages:
_logger.warning(f"提单 {bl.bl_no} 没有关联的小包,跳过节点推送")
continue
# 智能验证:先检查是否同一状态
is_same_status = self._check_packages_same_status(ship_packages)
if is_same_status:
# 同一状态,只验证第一个小包
_logger.info(f"提单 {bl.bl_no} 所有小包状态一致,只验证第一个小包")
first_package = ship_packages[0]
if self._validate_package_push_condition(first_package, extract_time, bl.bl_no):
# 第一个小包满足条件,所有小包都满足
file_info['valid_packages'] = ship_packages
valid_files.append(file_info)
_logger.info(f"提单 {bl.bl_no} 所有 {len(ship_packages)} 个小包都满足推送条件")
else:
# 第一个小包不满足条件,所有小包都不满足
self._add_bl_push_remark(bl, len(ship_packages), extract_time)
_logger.warning(f"提单 {bl.bl_no} 所有 {len(ship_packages)} 个小包都不满足推送条件")
else:
# 不同状态,逐个验证
_logger.info(f"提单 {bl.bl_no} 小包状态不一致,逐个验证")
valid_packages = []
invalid_count = 0
for package in ship_packages:
if self._validate_package_push_condition(package, extract_time, bl.bl_no):
valid_packages.append(package)
else:
invalid_count += 1
if valid_packages:
file_info['valid_packages'] = valid_packages
valid_files.append(file_info)
_logger.info(f"提单 {bl.bl_no} 有 {len(valid_packages)} 个小包满足推送条件")
if invalid_count > 0:
self._add_bl_push_remark(bl, invalid_count, extract_time)
_logger.warning(f"提单 {bl.bl_no} 有 {invalid_count} 个小包不满足推送条件")
except Exception as e:
_logger.error(f"验证提单 {bl.bl_no} 节点推送条件失败: {str(e)}")
continue
return valid_files
def _check_packages_same_status(self, ship_packages):
"""
检查小包是否同一状态(批量优化版本)
:param ship_packages: 小包记录集
:return: 是否同一状态
"""
try:
if not ship_packages or len(ship_packages) <= 1:
return True
# 使用SQL批量查询所有小包的不同状态(包括NULL)
package_ids = ship_packages.ids
# 直接查询不同的状态(DISTINCT),更高效
sql_query = """
SELECT DISTINCT state
FROM cc_ship_package
WHERE id IN %s
"""
self.env.cr.execute(sql_query, (tuple(package_ids),))
# 直接解包元组中的state值
distinct_states = [row[0] for row in self.env.cr.fetchall()]
unique_status_count = len(distinct_states)
# 检查是否有NULL状态
has_null = None in distinct_states
# 详细日志
_logger.info(f"提单小包状态检查: {len(package_ids)} 个小包,有 {unique_status_count} 种不同状态" if unique_status_count > 1 else f"提单小包状态检查: {len(package_ids)} 个小包,所有小包都是同一状态")
# 判断是否同一状态
# 如果只有1种状态且不是NULL,说明所有小包都是同一状态
if unique_status_count == 1 and not has_null:
return True
# 如果有多于1种状态,或者有NULL状态,都认为状态不一致
else:
return False
except Exception as e:
_logger.error(f"检查小包状态一致性失败: {str(e)}")
import traceback
_logger.error(traceback.format_exc())
# 即使检查失败,也返回False(状态不一致),继续执行后续的逐个验证逻辑,而不是中断整个流程
_logger.warning(f"状态检查异常,将逐个验证所有小包")
return False
def _get_package_operation_times(self, package_ids):
"""
获取小包当前节点操作时间(支持单个和批量)
根据小包的当前状态,查找同步日志中该状态的最晚时间
:param package_ids: 小包ID列表或单个小包ID
:return: {package_id: operation_time} 字典 或 单个操作时间
"""
try:
# 统一处理单个ID和ID列表
if isinstance(package_ids, int):
package_ids = [package_ids]
single_mode = True
else:
single_mode = False
if not package_ids:
return None if single_mode else {}
# 使用SQL批量查询小包当前节点操作时间
# 根据小包当前状态,查找同步日志中该状态的最晚时间
sql_query = """
SELECT
p.id,
MAX(n.operate_time) as latest_operation_time
FROM cc_ship_package p
LEFT JOIN cc_ship_package_sync_log n ON p.id = n.package_id
AND n.process_code = (SELECT tk_code FROM cc_node WHERE id = p.state ORDER BY operate_time DESC LIMIT 1)
WHERE p.id IN %s
GROUP BY p.id
"""
self.env.cr.execute(sql_query, (tuple(package_ids),))
results = self.env.cr.fetchall()
operation_times = {}
for package_id, operate_time in results:
if operate_time:
operation_times[package_id] = operate_time
_logger.info(f"获取到 {len(operation_times)} 个小包的操作时间")
# 如果是单个模式,返回单个值
if single_mode:
return operation_times.get(package_ids[0])
else:
return operation_times
except Exception as e:
_logger.error(f"获取小包操作时间失败: {str(e)}")
return None if single_mode else {}
def _add_bl_push_remark(self, bl, failed_count, extract_time):
"""
在提单上添加推送备注
:param bl: 提单对象
:param failed_count: 失败的小包数量
:param extract_time: 提取时间
"""
try:
# 构建备注信息
remark = f"获取尾程POD,自动推送节点失败,有风险产生倒挂。失败小包数量: {failed_count},请手动操作{extract_time.strftime('%Y-%m-%d %H:%M:%S')}(获取时间)"
# 更新提单的推送备注字段
# if hasattr(bl, 'push_remark'):
# # 如果已有备注,追加新备注
# existing_remark = bl.push_remark or ""
# new_remark = f"{existing_remark}\n{remark}" if existing_remark else remark
bl.write({'push_remark': remark})
except Exception as e:
_logger.error(f"为提单 {bl.bl_no} 添加推送备注失败: {str(e)}")
def _validate_package_push_condition(self, package, extract_time, bl_no):
"""
验证单个小包的推送条件(简化版本)
第一步:对比小包当前节点的操作时间是否小于提取时间(同时区对比)
第二步:如果是需要补推的,提取时间 − 小包当前节点的操作时间 < 前序间隔时间的不能推送
:param package: 小包对象
:param extract_time: 提取时间(UTC时间)
:param bl_no: 提单号
:return: 是否满足推送条件
"""
try:
# 获取小包当前节点的操作时间(数据库中的时间,应该是UTC时间)
current_node_operation_time = self._get_package_operation_times(package.id)
#没有操作时间,代表没有推送过,可以推送
if not current_node_operation_time:
_logger.info(f"小包 {package.id} 没有操作时间,可以推送")
return True
# 记录详细信息用于调试
_logger.info(f"小包 {package.id} (状态: {package.state.name if package.state else 'None'}) 验证推送条件:")
_logger.info(f" - 提取时间(UTC): {extract_time}")
_logger.info(f" - 操作时间(UTC): {current_node_operation_time}")
# 第一步:对比小包当前节点的操作时间是否小于提取时间(同时区对比)
if current_node_operation_time >= extract_time:
_logger.warning(f"小包 {package.id} 当前节点操作时间 {current_node_operation_time} >= 提取时间 {extract_time},不满足推送条件(倒挂)")
return False
# 计算时间差
time_diff = extract_time - current_node_operation_time
time_diff_minutes = time_diff.total_seconds() / 60
_logger.info(f" - 时间差: {time_diff_minutes:.1f} 分钟")
# 第二步:如果是需要补推的,提取时间 − 小包当前节点的操作时间 < 前序间隔时间的不能推送
flag, match_node = self._need_supplement_push(package)
_logger.info(f" - 是否需要补推: {flag}, 匹配节点: {match_node.name if match_node else 'None'}")
# 如果需要补推,检查前序间隔时间
if flag:
interval_time = self._get_write_node_previous_interval_time(package, match_node)
if interval_time:
# 将间隔时间转换为分钟数
interval_minutes = interval_time.total_seconds() / 60
_logger.info(f" - 前序间隔时间: {interval_minutes:.1f} 分钟")
_logger.info(f" - 时间差 {time_diff_minutes:.1f} 分钟 vs 前序间隔时间 {interval_minutes:.1f} 分钟")
# 检查:提取时间 − 小包当前节点的操作时间 < 前序间隔时间的不能推送
if time_diff_minutes < interval_minutes:
_logger.warning(f"小包 {package.id} 时间差 {time_diff_minutes:.1f} 分钟 < 前序间隔时间 {interval_minutes:.1f} 分钟,不满足补推条件(倒挂)")
return False
else:
_logger.info(f"小包 {package.id} 时间差 {time_diff_minutes:.1f} 分钟 >= 前序间隔时间 {interval_minutes:.1f} 分钟,满足补推条件")
else:
_logger.warning(f"小包 {package.id} 需要补推但未获取到前序间隔时间,跳过补推检查")
else:
_logger.info(f"小包 {package.id} 不需要补推(当前状态: {package.state.name if package.state else 'None'}, 状态ID: {package.state.id if package.state else 'None'}),跳过前序间隔时间检查")
_logger.info(f"小包 {package.id} 满足推送条件")
return True
except Exception as e:
_logger.error(f"验证小包 {package.id} 推送条件失败: {str(e)}")
import traceback
_logger.error(traceback.format_exc())
return False
def _need_supplement_push(self, package):
"""
判断是否需要补推节点
:param package: 小包对象
:return: (是否需要补推, POD节点对象)
"""
pod_node = False
try:
# 查找对应的清关节点(勾选了POD节点匹配的节点)
pod_node = self.env['cc.node'].search([
('is_pod_node', '=', True),
('node_type', '=', 'package')
], limit=1)
if not pod_node:
_logger.info(f"小包 {package.id} 未找到POD节点,不需要补推")
return False, pod_node
# 小包节点在这个节点之前,则需要补推节点
current_state_id = package.state.id if package.state else None
pod_node_id = pod_node.id
_logger.info(f"小包 {package.id} 补推判断: 当前状态ID={current_state_id}, POD节点ID={pod_node_id}, 需要补推={current_state_id < pod_node_id if current_state_id else False}")
if current_state_id:
need_push = current_state_id < pod_node_id
return need_push, pod_node
else:
_logger.warning(f"小包 {package.id} 没有状态,不需要补推")
return False, pod_node
except Exception as e:
_logger.error(f"判断小包 {package.id} 是否需要补推失败: {str(e)}")
import traceback
_logger.error(traceback.format_exc())
return False, pod_node
def _get_write_node_previous_interval_time(self, package, match_node):
"""
获取写入节点前序间隔时间
:param package: 小包对象
:param match_node: 匹配的节点
:return: 前序间隔时间(timedelta对象)
"""
try:
# 获取前序间隔时间(分钟数)
before_minutes = package.state.calculate_total_interval(match_node)
if before_minutes and before_minutes > 0:
# 将分钟数转换为timedelta对象
from datetime import timedelta
return timedelta(minutes=before_minutes)
return None
except Exception as e:
_logger.error(f"获取小包 {package.id} 前序间隔时间失败: {str(e)}")
return None
def _convert_local_time_to_utc(self, local_time):
"""
将本地时间(用户时区)转换为UTC时间(0时区)
:param local_time: 本地时间的datetime对象(naive或aware)
:return: UTC时间的datetime对象(naive,用于数据库存储)
"""
try:
# 获取用户时区,如果没有则使用系统默认时区(通常为Asia/Shanghai或UTC)
user_tz_name = self.env.user.tz
if not user_tz_name:
# 尝试从系统配置获取默认时区,如果没有则使用UTC
user_tz_name = self.env['ir.config_parameter'].sudo().get_param('timezone') or 'UTC'
user_tz = pytz.timezone(user_tz_name)
# 如果local_time是naive datetime(没有时区信息),假设它是用户时区的时间
if local_time.tzinfo is None:
# 将naive datetime标记为用户时区
local_time_aware = user_tz.localize(local_time)
else:
# 如果已经是aware datetime,先转换到用户时区,再转换到UTC
local_time_aware = local_time.astimezone(user_tz)
# 转换为UTC时间
utc_time = local_time_aware.astimezone(pytz.UTC)
# 返回naive UTC datetime(Odoo数据库通常存储naive UTC datetime)
utc_time_naive = utc_time.replace(tzinfo=None)
_logger.info(f"时区转换: 本地时间 {local_time} (用户时区: {user_tz_name}) -> UTC时间 {utc_time_naive}")
return utc_time_naive
except Exception as e:
_logger.error(f"时区转换失败: {str(e)},使用原时间(假设已经是UTC)")
# 如果转换失败,返回原时间并移除时区信息(假设已经是UTC)
return local_time.replace(tzinfo=None) if hasattr(local_time, 'tzinfo') and local_time.tzinfo else local_time
...@@ -8,12 +8,12 @@ import redis ...@@ -8,12 +8,12 @@ import redis
import config import config
# 默认字符gbk # 默认字符gbk
# logging.basicConfig(filename='./push_data_logger.log', level=logging.INFO) logging.basicConfig(filename='./push_data_logger.log', level=logging.INFO)
# 设置文件字符为utf-8 # 设置文件字符为utf-8
logging.basicConfig(handlers=[logging.FileHandler('logs/mail_push.log', 'a', 'utf-8')], # logging.basicConfig(handlers=[logging.FileHandler('logs/mail_push.log', 'a', 'utf-8')],
format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO) # format='%(asctime)s %(levelname)s %(message)s', level=logging.INFO)
class Order_dispose(object): class Order_dispose(object):
...@@ -42,10 +42,12 @@ class Order_dispose(object): ...@@ -42,10 +42,12 @@ class Order_dispose(object):
bl_record = bl_obj.browse(bl_ids) bl_record = bl_obj.browse(bl_ids)
else: else:
bl_record = bl_obj.browse(data['id']) bl_record = bl_obj.browse(data['id'])
# utc_time = datetime.strptime(data['utc_time'], "%Y-%m-%d %H:%M:%S")
utc_time = data.get('utc_time') utc_time = data.get('utc_time')
user_login = data.get('user_login') user_login = data.get('user_login')
bl_record.mail_auto_push(utc_time, ship_packages, action_type, user_login, config.pda_db_user) logging.info('user_login: %s', user_login)
pod_node_id = data.get('pod_node_id')
bl_record.mail_auto_push(utc_time, ship_packages, action_type, user_login, user_login or config.pda_db_user,
pod_node_id=pod_node_id)
except Exception as ex: except Exception as ex:
logging.error('mail_auto_push error:%s' % str(ex)) logging.error('mail_auto_push error:%s' % str(ex))
return res_data return res_data
...@@ -54,7 +56,7 @@ class Order_dispose(object): ...@@ -54,7 +56,7 @@ class Order_dispose(object):
try: try:
pool = redis.ConnectionPool(**config.redis_options) pool = redis.ConnectionPool(**config.redis_options)
r = redis.Redis(connection_pool=pool) r = redis.Redis(connection_pool=pool)
logging.info(u'redis连接成功') logging.info(u'redis connection success')
Order_dispose = Order_dispose() Order_dispose = Order_dispose()
while 1: while 1:
try: try:
...@@ -65,4 +67,4 @@ try: ...@@ -65,4 +67,4 @@ try:
logging.error(e) logging.error(e)
continue continue
except Exception as e: except Exception as e:
logging.error("登录失败:%s" % e) logging.error("login failed:%s" % e)
...@@ -14,10 +14,10 @@ pytesseract ...@@ -14,10 +14,10 @@ pytesseract
# 2. 安装OpenCV: pip install opencv-python # 2. 安装OpenCV: pip install opencv-python
# Linux系统: # Linux系统:
# 1. 更新包列表: sudo apt update # 1. 更新包列表: apt update
# 2. 安装Tesseract OCR: sudo apt install tesseract-ocr # 2. 安装Tesseract OCR: sudo apt install tesseract-ocr
# 3. 安装英语语言包: sudo apt install tesseract-ocr-eng # 3. 安装英语语言包: apt install tesseract-ocr-eng
# 4. 安装OpenCV系统依赖: sudo apt install libopencv-dev python3-opencv # 4. 安装OpenCV系统依赖: apt install libopencv-dev python3-opencv
# 5. 安装OpenCV Python包: pip install opencv-python-headless # 5. 安装OpenCV Python包: pip install opencv-python-headless
# macOS系统: # macOS系统:
......
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论