提交 8279ed7a authored 作者: 刘擎阳's avatar 刘擎阳

1.优化

上级 53af0a0d
...@@ -2710,15 +2710,20 @@ msgid "Package Goods" ...@@ -2710,15 +2710,20 @@ msgid "Package Goods"
msgstr "小包商品" msgstr "小包商品"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__goods_qty
#: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__ship_package_qty #: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__ship_package_qty
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_big_package__goods_qty #: model:ir.model.fields,field_description:ccs_base.field_cc_history_big_package__goods_qty
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_big_package__ship_package_qty #: model:ir.model.fields,field_description:ccs_base.field_cc_history_big_package__ship_package_qty
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_big_package_view
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_big_package_view #: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_history_big_package_view
msgid "Package Qty" msgid "Package Qty"
msgstr "小包数" msgstr "小包数"
#. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_big_package__goods_qty
#: model_terms:ir.ui.view,arch_db:ccs_base.form_cc_big_package_view
msgid "Goods Qty"
msgstr "商品数"
#. module: ccs_base #. module: ccs_base
#: model:ir.model.fields,field_description:ccs_base.field_cc_history_ship_package__buyer_region #: model:ir.model.fields,field_description:ccs_base.field_cc_history_ship_package__buyer_region
#: model:ir.model.fields,field_description:ccs_base.field_cc_ship_package__buyer_region #: model:ir.model.fields,field_description:ccs_base.field_cc_ship_package__buyer_region
...@@ -4463,3 +4468,13 @@ msgstr "" ...@@ -4463,3 +4468,13 @@ msgstr ""
#, python-format #, python-format
msgid "预览操作失败: %s" msgid "预览操作失败: %s"
msgstr "" msgstr ""
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_bl_view
msgid "Customs Clearance Status"
msgstr "关务提单状态"
#. module: ccs_base
#: model_terms:ir.ui.view,arch_db:ccs_base.search_cc_bl_view
msgid "Last Mile Provider"
msgstr "尾程服务商"
\ No newline at end of file
...@@ -40,7 +40,7 @@ class CcBigPackage(models.Model): ...@@ -40,7 +40,7 @@ class CcBigPackage(models.Model):
# 增加包裹数量字段, 用于显示包裹数量, 根据big_package_line_ids计算 # 增加包裹数量字段, 用于显示包裹数量, 根据big_package_line_ids计算
ship_package_qty = fields.Integer(string='Package Qty', compute='_compute_big_package_qty', store=True) ship_package_qty = fields.Integer(string='Package Qty', compute='_compute_big_package_qty', store=True)
# 商品纪录数 # 商品纪录数
goods_qty = fields.Integer(string='Package Qty', compute='_compute_big_package_qty', store=True) goods_qty = fields.Integer(string='Goods Qty', compute='_compute_big_package_qty', store=True)
# New fields: pallet_number (char type), pallet_usage_date # New fields: pallet_number (char type), pallet_usage_date
# 托盘号(char型),托盘使用日期 # 托盘号(char型),托盘使用日期
......
...@@ -20,6 +20,11 @@ ...@@ -20,6 +20,11 @@
<field name="implied_ids" eval="[(4, ref('group_clearance_of_customs_user'))]"/> <field name="implied_ids" eval="[(4, ref('group_clearance_of_customs_user'))]"/>
</record> </record>
<record id="group_import_package" model="res.groups">
<field name="name">包裹数据导入</field>
<field name="category_id" ref="ccs_base.module_category_clearance_of_customs"/>
<field name="implied_ids" eval="[(4, ref('base.group_user'))]"/>
</record>
</data> </data>
......
...@@ -313,6 +313,10 @@ ...@@ -313,6 +313,10 @@
context="{'group_by': 'cc_country_id'}"/> context="{'group_by': 'cc_country_id'}"/>
<filter domain="[]" name="groupby_state" string="Status" <filter domain="[]" name="groupby_state" string="Status"
context="{'group_by': 'state'}"/> context="{'group_by': 'state'}"/>
<filter domain="[]" name="groupby_customs_clearance_status" string="Customs Clearance Status"
context="{'group_by': 'customs_clearance_status'}"/>
<filter domain="[]" name="groupby_last_mile_provider_ids" string="Last Mile Provider"
context="{'group_by': 'last_mile_provider_ids'}"/>
</group> </group>
<searchpanel> <searchpanel>
<field icon="fa-users" select="multi" name="cc_company_id"/> <field icon="fa-users" select="multi" name="cc_company_id"/>
......
...@@ -992,3 +992,14 @@ msgstr "" ...@@ -992,3 +992,14 @@ msgstr ""
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_bl_patrol__email_sent #: model:ir.model.fields,field_description:ccs_connect_tiktok.field_bl_patrol__email_sent
msgid "邮件已发送" msgid "邮件已发送"
msgstr "" msgstr ""
#. module: ccs_connect_tiktok
#: model_terms:ir.ui.view,arch_db:ccs_connect_tiktok.search_cc_ship_package_view_inherit
msgid "Is Sync"
msgstr "是否同步"
#. module: ccs_connect_tiktok
#: model_terms:ir.ui.view,arch_db:ccs_connect_tiktok.search_cc_ship_package_view_inherit
msgid "Next Provider"
msgstr "下阶段服务商"
...@@ -34,6 +34,12 @@ ...@@ -34,6 +34,12 @@
<search position="inside"> <search position="inside">
<filter string="Not Sync" name="filter_is_sync" domain="[('is_sync','=',False)]"/> <filter string="Not Sync" name="filter_is_sync" domain="[('is_sync','=',False)]"/>
</search> </search>
<xpath expr="//search//group" position="inside">
<filter domain="[]" name="groupby_is_sync" string="Is Sync"
context="{'group_by': 'is_sync'}"/>
<filter domain="[]" name="groupby_next_provider_name" string="Next Provider"
context="{'group_by': 'next_provider_name'}"/>
</xpath>
</field> </field>
</record> </record>
......
# 文件:wizards/collection_receive_wizard.py
import xlrd
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date, datetime
import logging
import pytz
_logger = logging.getLogger(__name__)
class PackageDataWizard(models.TransientModel):
_name = 'package.data.wizard'
_description = '包裹数据处理'
attachment_ids = fields.Many2many('ir.attachment', 'ref_package_data_attachment', string='附件')
node_id = fields.Many2one('cc.node', string='变更状态')
# 新增时区字段
timezone = fields.Selection(
selection=lambda self: [(tz, tz) for tz in pytz.all_timezones],
string='时区',
default=lambda self: self.env.user.tz or self.env.context.get('tz') or 'UTC'
)
def get_file_path(self, report_file_ids):
"""
得到excel表格的内容
:return:
"""
report_path = report_file_ids._full_path(report_file_ids.store_fname)
if report_file_ids.name[-3:] == 'xls':
data = xlrd.open_workbook(report_path, formatting_info=True)
else:
data = xlrd.open_workbook(report_path)
return data
def submit(self):
for attachment_obj in self.attachment_ids:
data = self.get_file_path(attachment_obj)
first_table = data.sheets()[0]
try:
if self.node_id.name == '待尾程提货':
select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),
('name', '=', '已提货')],
limit=1)
else:
select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),
('name', '=', '待尾程提货')],
limit=1)
# 新增:定义一个累加器,记录整个 Excel 总共更新了多少个小包
total_updated_count = 0
for i in range(1, first_table.nrows):
line = first_table.row_values(i)
big_no = str(line[1]).strip()
if not big_no:
continue
lh_time = line[10]
wj_time = line[5] # 尾程交货时间
# 大包数据
wj_user_name = line[4] # 尾程交货人
lh_user_name = line[8] # 理货人
pallet_date = line[6] # 托盘日期
pallet_no = str(line[7]).replace('.0', '').replace(' ', '') # 托盘号
wj_user_obj = self.env['res.users'].sudo().search([('login', '=', wj_user_name)], limit=1)
lh_user_obj = self.env['res.users'].sudo().search([('login', '=', lh_user_name)], limit=1)
update_time = lh_time if self.node_id.name == '待尾程提货' else wj_time
ship_package_objs = self.env['cc.ship.package'].sudo().search([
('big_package_id.big_package_no', '=', big_no),
('state', '=', select_node_obj.id)
]) # 已提货
if ship_package_objs:
# 核心修改:计算当前大包查到的小包数量
current_count = len(ship_package_objs)
total_updated_count += current_count # 累加到总数
# 日志记录:每个大包具体查到了多少个小包
_logger.info(f"处理第 {i} 行: 大包号 [{big_no}] 查到 {current_count} 个小包准备更新。")
sql = """
UPDATE cc_ship_package
SET state = %s, process_time = %s , is_sync=False
WHERE id IN %s
"""
self.env.cr.execute(sql, (self.node_id.id, update_time, tuple(ship_package_objs.ids)))
# 更新大包
big_package_obj = self.env['cc.big.package'].sudo().search([('big_package_no', '=', big_no)], limit=1)
sql = """
UPDATE cc_big_package
SET pallet_number = %s, pallet_usage_date = %s , tally_user_id= %s,
tally_time = %s, delivery_user_id = %s , delivery_time= %s
WHERE id = %s
"""
self.env.cr.execute(sql, (pallet_no, pallet_date, lh_user_obj.id if lh_user_obj else None,
lh_time, wj_user_obj.id if wj_user_obj else None, wj_time, big_package_obj.id))
pallet_obj = self.env['cc.pallet'].sudo().search([('name', '=', pallet_no)], limit=1)
pallet_obj.usage_state = 'used'
else:
# 选做:把没查到数据的大包也用 warning 级别记录下来,方便后期排查漏刷的数据
_logger.warning(f"处理第 {i} 行: 大包号 [{big_no}] 未查到状态为'已提货'的小包,跳过更新。")
# 循环结束后,记录该文件的最终战果
_logger.info(f"==== 附件处理完毕 ==== 整个表格共查到并更新了 {total_updated_count} 个小包!")
except Exception as err:
_logger.error(f'package_data_xls error : {str(err)}')
# 文件:wizards/collection_receive_wizard.py import base64
import xlrd import io
from odoo import models, fields, api, _ import json
from odoo.exceptions import ValidationError
from datetime import date, datetime
import logging import logging
import pytz
from datetime import datetime, date
from odoo import models, fields, api, _
from odoo.exceptions import UserError
import openpyxl
_logger = logging.getLogger(__name__) _logger = logging.getLogger(__name__)
...@@ -13,84 +17,451 @@ class PackageDataWizard(models.TransientModel): ...@@ -13,84 +17,451 @@ class PackageDataWizard(models.TransientModel):
attachment_ids = fields.Many2many('ir.attachment', 'ref_package_data_attachment', string='附件') attachment_ids = fields.Many2many('ir.attachment', 'ref_package_data_attachment', string='附件')
node_id = fields.Many2one('cc.node', string='变更状态') node_id = fields.Many2one('cc.node', string='变更状态')
report_file = fields.Binary(string=u'异常数据', readonly=True)
file_name = fields.Char(u'名称')
def get_file_path(self, report_file_ids): timezone = fields.Selection(
selection=lambda self: [(tz, tz) for tz in pytz.all_timezones],
string='时区',
default=lambda self: self.env.user.tz or self.env.context.get('tz') or 'UTC',
required=True
)
def _parse_and_convert_tz(self, val):
""" """
得到excel表格的内容 将任意时间格式(字符串/日期/时间/Excel数字/时间戳)转为 0时区 (UTC) 时间
:return:
""" """
report_path = report_file_ids._full_path(report_file_ids.store_fname) if val is None or val == '':
if report_file_ids.name[-3:] == 'xls': return False
data = xlrd.open_workbook(report_path, formatting_info=True)
dt = False
if isinstance(val, (int, float)):
if val > 1000000:
if val > 100000000000:
val = val / 1000.0
try:
dt = datetime.fromtimestamp(val)
except Exception:
return False
else: else:
data = xlrd.open_workbook(report_path) try:
return data dt = datetime(1899, 12, 30) + fields.Datetime.context_timestamp(self,
datetime.now()).tzinfo.utcoffset(
None) * 0
from datetime import timedelta
dt = datetime(1899, 12, 30) + timedelta(days=val)
except Exception:
return False
elif isinstance(val, str):
try:
dt = fields.Datetime.from_string(val)
except ValueError:
return False
elif isinstance(val, datetime):
dt = val
elif isinstance(val, date):
dt = datetime.combine(val, datetime.min.time())
def submit(self): if not dt:
for attachment_obj in self.attachment_ids: return False
data = self.get_file_path(attachment_obj)
first_table = data.sheets()[0] local_tz = pytz.timezone(self.timezone)
if dt.tzinfo is None:
local_dt = local_tz.localize(dt)
else:
local_dt = dt.astimezone(local_tz)
utc_dt = local_dt.astimezone(pytz.UTC).replace(tzinfo=None)
return utc_dt
def action_import_data_with_validation(self):
"""
表格数据校验与导入核心逻辑:
1. 校验未来时
2. 根据当前 node_id 校验与系统存量数据的时序
3. 错误回写表格,保存到当前向导的 report_file 字段
4. 成功返回 JSON 格式数据
"""
if not self.attachment_ids:
raise UserError("请先上传 Excel 附件!")
attachment = self.attachment_ids[0]
file_data = base64.b64decode(attachment.datas)
try:
wb = openpyxl.load_workbook(io.BytesIO(file_data))
sheet = wb.active
except Exception as e:
raise UserError(f"读取 Excel 文件失败,请检查格式: {e}")
# 1. 动态映射表头
headers = [str(cell.value).strip() if cell.value else '' for cell in sheet[1]]
if '大包号' not in headers:
raise UserError("缺少必要的表头: 【大包号】,请检查模板!")
header_map = {col: idx for idx, col in enumerate(headers)}
# 预设错误回写列 (如果已经有错误原因列则覆盖,否则新增一列)
error_col_idx = header_map.get('错误原因', sheet.max_column) + 1
if '错误原因' not in headers:
sheet.cell(row=1, column=error_col_idx, value="错误原因")
now_utc = datetime.utcnow()
has_error = False
final_data_to_insert = []
# --- 2. 批量查库,优化性能 ---
all_pkg_nums = []
for row in sheet.iter_rows(min_row=2, values_only=True):
pkg_num = row[header_map['大包号']] if header_map.get('大包号') is not None else False
if pkg_num:
all_pkg_nums.append(str(pkg_num).strip())
system_packages = self.env['cc.big.package'].search([('big_package_no', 'in', all_pkg_nums)])
# 【优化点】:因为上面搜索的是 big_package_no,字典映射改用 big_package_no 更安全
sys_pkg_map = {pkg.big_package_no: pkg for pkg in system_packages if pkg.big_package_no}
current_node_name = self.node_id.name if self.node_id else ''
# --- 3. 逐行遍历校验 ---
for row_idx, row in enumerate(sheet.iter_rows(min_row=2, max_col=error_col_idx), start=2):
pkg_num_cell = row[header_map.get('大包号')] if header_map.get('大包号') is not None else None
pkg_num = str(pkg_num_cell.value).strip() if pkg_num_cell and pkg_num_cell.value else False
if not pkg_num:
continue
row_errors = []
row_data_dict = {}
# 读取所有列数据存入字典
for col_name, col_idx in header_map.items():
if col_name == '错误原因':
continue
row_data_dict[col_name] = row[col_idx].value
# 获取并转换需要校验的两个时间
raw_tally = row_data_dict.get('理货时间')
raw_delivery = row_data_dict.get('尾程交货时间')
tally_utc = self._parse_and_convert_tz(raw_tally)
delivery_utc = self._parse_and_convert_tz(raw_delivery)
if tally_utc:
row_data_dict['理货时间'] = tally_utc.strftime('%Y-%m-%d %H:%M:%S')
if delivery_utc:
row_data_dict['尾程交货时间'] = delivery_utc.strftime('%Y-%m-%d %H:%M:%S')
# -----------------------------------------------------
# 【需求 1】:理货时间、尾程交货时间不能是未来时间
# -----------------------------------------------------
if tally_utc and tally_utc > now_utc:
row_errors.append("理货时间不能是未来时间")
if delivery_utc and delivery_utc > now_utc:
row_errors.append("尾程交货时间不能是未来时间")
# -----------------------------------------------------
# 结合系统数据的状态时序校验 (需求 2, 3)
# -----------------------------------------------------
sys_pkg = sys_pkg_map.get(pkg_num)
# 【需求 2】:系统选择“待尾程提货”,理货时间不能小于系统的提货时间
if current_node_name == '待尾程提货' and tally_utc and sys_pkg and sys_pkg.pickup_time:
if tally_utc < sys_pkg.pickup_time:
row_errors.append("理货时间不能小于系统内该大包的提货时间")
# 【需求 3】:系统选择“尾程交接”,尾程交货时间不能小于系统的理货时间
if current_node_name == '尾程交接' and delivery_utc and sys_pkg and sys_pkg.tally_time:
if delivery_utc < sys_pkg.tally_time:
row_errors.append("尾程交货时间不能小于系统内该大包的理货时间")
# --- 4. 组装结果与回写 ---
if row_errors:
has_error = True
error_msg = " | ".join(row_errors)
# 将错误写在这一行的最后一列
row[error_col_idx - 1].value = error_msg
else:
final_data_to_insert.append(row_data_dict)
# --- 5. 【修改部分】:异常数据保存到模型字段,刷新页面 ---
if has_error:
out_stream = io.BytesIO()
wb.save(out_stream)
out_stream.seek(0)
# 生成文件名和 Base64 数据
error_file_name = f'异常数据.xlsx'
encoded_file = base64.b64encode(out_stream.read())
# 写入当前向导记录
self.write({
'report_file': encoded_file,
'file_name': error_file_name
})
# 返回前端动作:刷新当前向导页面 (用户将能看到并点击下载你的 report_file)
return {
'type': 'ir.actions.act_window',
'res_model': 'package.data.wizard',
'res_id': self.id,
'view_mode': 'form',
'target': 'new',
'name': '导入包裹数据'
}
# --- 6. 校验全部通过,返回 JSON 格式数据 ---
json_result = json.dumps(final_data_to_insert, ensure_ascii=False, indent=4)
return json_result
# _logger.info("\n" + "=" * 50)
# _logger.info(f"所有数据校验完美通过!共计 {len(final_data_to_insert)} 行。")
# _logger.info("返回的 JSON 数据如下:\n%s", json_result)
# _logger.info("=" * 50)
#
# # 如果需要彻底清空错误文件字段,避免下次正常上传时依然显示,可以在这里加上清理代码
# self.write({
# 'report_file': False,
# 'file_name': False
# })
#
# return {
# 'type': 'ir.actions.client',
# 'tag': 'display_notification',
# 'params': {
# 'title': '校验与转换成功',
# 'message': f'全部 {len(final_data_to_insert)} 条数据校验通过!已转换为 JSON 数据格式。',
# 'type': 'success',
# 'sticky': False,
# }
# }
# def submit(self):
# for attachment_obj in self.attachment_ids:
# data = self.get_file_path(attachment_obj)
# first_table = data.sheets()[0]
# try:
# if self.node_id.name == '待尾程提货':
# select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),
# ('name', '=', '已提货')],
# limit=1)
# else:
# select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'),
# ('name', '=', '待尾程提货')],
# limit=1)
# # 新增:定义一个累加器,记录整个 Excel 总共更新了多少个小包
# total_updated_count = 0
# for i in range(1, first_table.nrows):
# line = first_table.row_values(i)
# big_no = str(line[1]).strip()
# if not big_no:
# continue
# lh_time = line[10] # 理货时间
# wj_time = line[5] # 尾程交货时间
# # 大包数据
# wj_user_name = line[4] # 尾程交货人
# lh_user_name = line[8] # 理货人
# pallet_date = line[6] # 托盘日期
# pallet_no = str(line[7]).replace('.0', '').replace(' ', '') # 托盘号
# wj_user_obj = self.env['res.users'].sudo().search([('login', '=', wj_user_name)], limit=1)
# lh_user_obj = self.env['res.users'].sudo().search([('login', '=', lh_user_name)], limit=1)
#
# update_time = lh_time if self.node_id.name == '待尾程提货' else wj_time
# ship_package_objs = self.env['cc.ship.package'].sudo().search([
# ('big_package_id.big_package_no', '=', big_no),
# ('state', '=', select_node_obj.id)
# ]) # 已提货
# if ship_package_objs:
# # 核心修改:计算当前大包查到的小包数量
# current_count = len(ship_package_objs)
# total_updated_count += current_count # 累加到总数
# # 日志记录:每个大包具体查到了多少个小包
# _logger.info(f"处理第 {i} 行: 大包号 [{big_no}] 查到 {current_count} 个小包准备更新。")
# sql = """
# UPDATE cc_ship_package
# SET state = %s, process_time = %s , is_sync=False
# WHERE id IN %s
# """
# self.env.cr.execute(sql, (self.node_id.id, update_time, tuple(ship_package_objs.ids)))
# # 更新大包
# big_package_obj = self.env['cc.big.package'].sudo().search([('big_package_no', '=', big_no)], limit=1)
# sql = """
# UPDATE cc_big_package
# SET pallet_number = %s, pallet_usage_date = %s , tally_user_id= %s,
# tally_time = %s, delivery_user_id = %s , delivery_time= %s
# WHERE id = %s
# """
# self.env.cr.execute(sql, (pallet_no, pallet_date, lh_user_obj.id if lh_user_obj else None,
# lh_time, wj_user_obj.id if wj_user_obj else None, wj_time, big_package_obj.id))
# pallet_obj = self.env['cc.pallet'].sudo().search([('name', '=', pallet_no)], limit=1)
# pallet_obj.usage_state = 'used'
# else:
# # 选做:把没查到数据的大包也用 warning 级别记录下来,方便后期排查漏刷的数据
# _logger.warning(f"处理第 {i} 行: 大包号 [{big_no}] 未查到状态为'已提货'的小包,跳过更新。")
# # 循环结束后,记录该文件的最终战果
# _logger.info(f"==== 附件处理完毕 ==== 整个表格共查到并更新了 {total_updated_count} 个小包!")
# except Exception as err:
# _logger.error(f'package_data_xls error : {str(err)}')
def process_validated_json_data(self, json_data):
"""
接收 action_import_data_with_validation 返回的 JSON 数据并执行数据库更新
:param json_data: 可以是 JSON 字符串,也可以是解析后的 Python 字典列表 (List[Dict])
"""
try: try:
# 1. 确保数据是可迭代的列表对象
if isinstance(json_data, str):
data_list = json.loads(json_data)
else:
data_list = json_data
if not data_list:
_logger.warning("未接收到有效的 JSON 数据进行更新。")
return
# 2. 确定需要匹配的“上一节点”状态
if self.node_id.name == '待尾程提货': if self.node_id.name == '待尾程提货':
select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), select_node_obj = self.env['cc.node'].sudo().search([
('name', '=', '已提货')], ('node_type', '=', 'package'),
limit=1) ('name', '=', '已提货')
], limit=1)
else: else:
select_node_obj = self.env['cc.node'].sudo().search([('node_type', '=', 'package'), select_node_obj = self.env['cc.node'].sudo().search([
('name', '=', '待尾程提货')], ('node_type', '=', 'package'),
limit=1) ('name', '=', '待尾程提货')
# 新增:定义一个累加器,记录整个 Excel 总共更新了多少个小包 ], limit=1)
if not select_node_obj:
_logger.error("在系统中未找到匹配的 cc.node 状态字典数据!")
return
# 3. 初始化累加器与用户查询缓存(避免重复查库拖慢速度)
total_updated_count = 0 total_updated_count = 0
for i in range(1, first_table.nrows): user_cache = {}
line = first_table.row_values(i)
big_no = str(line[0]).strip() def get_user_id(login_name):
"""本地缓存获取用户 ID"""
if not login_name:
return None
if login_name not in user_cache:
user = self.env['res.users'].sudo().search([('login', '=', login_name)], limit=1)
user_cache[login_name] = user.id if user else None
return user_cache[login_name]
# 4. 遍历处理 JSON 数据
for index, row in enumerate(data_list, start=1):
big_no = str(row.get('大包号', '')).strip()
if not big_no: if not big_no:
continue continue
lh_time = line[9]
wj_time = line[4] # 尾程交货时间
# 大包数据
wj_user_name = line[3] # 尾程交货人
lh_user_name = line[8] # 理货人
pallet_date = line[5] # 托盘日期
pallet_no = str(line[6]).replace('.0', '').replace(' ', '') # 托盘号
wj_user_obj = self.env['res.users'].sudo().search([('login', '=', wj_user_name)], limit=1)
lh_user_obj = self.env['res.users'].sudo().search([('login', '=', lh_user_name)], limit=1)
# 提取数据(如果为空,转换为 None,防止 PostgreSQL 写入日期/时间字段时报错)
lh_time = row.get('理货时间') or None
wj_time = row.get('尾程交货时间') or None
wj_user_name = str(row.get('尾程交货人', '')).strip()
lh_user_name = str(row.get('理货人', '')).strip()
pallet_date = row.get('托盘使用日期') or None
# 处理托盘号(防呆:去除小数和空格)
raw_pallet_no = str(row.get('托盘号', ''))
pallet_no = raw_pallet_no.replace('.0', '').replace(' ', '') if raw_pallet_no else None
# 获取用户 ID
wj_user_id = get_user_id(wj_user_name)
lh_user_id = get_user_id(lh_user_name)
# 确定最终更新到小包上的时间
update_time = lh_time if self.node_id.name == '待尾程提货' else wj_time update_time = lh_time if self.node_id.name == '待尾程提货' else wj_time
# 5. 查找符合条件的小包并执行 SQL 更新
ship_package_objs = self.env['cc.ship.package'].sudo().search([ ship_package_objs = self.env['cc.ship.package'].sudo().search([
('big_package_id.big_package_no', '=', big_no), ('big_package_id.big_package_no', '=', big_no),
('state', '=', select_node_obj.id) ('state', '=', select_node_obj.id)
]) # 已提货 ])
if ship_package_objs: if ship_package_objs:
# 核心修改:计算当前大包查到的小包数量
current_count = len(ship_package_objs) current_count = len(ship_package_objs)
total_updated_count += current_count # 累加到总数 total_updated_count += current_count
# 日志记录:每个大包具体查到了多少个小包 _logger.info(f"处理第 {index} 行: 大包号 [{big_no}] 查到 {current_count} 个小包准备更新。")
_logger.info(f"处理第 {i} 行: 大包号 [{big_no}] 查到 {current_count} 个小包准备更新。")
sql = """ # 原生 SQL 批量更新小包
sql_small = """
UPDATE cc_ship_package UPDATE cc_ship_package
SET state = %s, process_time = %s , is_sync=False SET state = %s, process_time = %s, is_sync = False
WHERE id IN %s WHERE id IN %s
""" """
self.env.cr.execute(sql, (self.node_id.id, update_time, tuple(ship_package_objs.ids))) self.env.cr.execute(sql_small, (self.node_id.id, update_time, tuple(ship_package_objs.ids)))
# 更新大包
big_package_obj = self.env['cc.big.package'].sudo().search([('big_package_no', '=', big_no)], limit=1) # 6. 查找大包并更新
sql = """ big_package_obj = self.env['cc.big.package'].sudo().search([('big_package_no', '=', big_no)],
limit=1)
if big_package_obj:
sql_big = """
UPDATE cc_big_package UPDATE cc_big_package
SET pallet_number = %s, pallet_usage_date = %s , tally_user_id= %s, SET pallet_number = %s,
tally_time = %s, delivery_user_id = %s , delivery_time= %s pallet_usage_date = %s,
tally_user_id = %s,
tally_time = %s,
delivery_user_id = %s,
delivery_time = %s
WHERE id = %s WHERE id = %s
""" """
self.env.cr.execute(sql, (pallet_no, pallet_date, lh_user_obj.id if lh_user_obj else None, self.env.cr.execute(sql_big, (
lh_time, wj_user_obj.id if wj_user_obj else None, wj_time, big_package_obj.id)) pallet_no,
pallet_date,
lh_user_id,
lh_time,
wj_user_id,
wj_time,
big_package_obj.id
))
# 7. 更新托盘状态
if pallet_no:
pallet_obj = self.env['cc.pallet'].sudo().search([('name', '=', pallet_no)], limit=1) pallet_obj = self.env['cc.pallet'].sudo().search([('name', '=', pallet_no)], limit=1)
if pallet_obj:
pallet_obj.usage_state = 'used' pallet_obj.usage_state = 'used'
else: else:
# 选做:把没查到数据的大包也用 warning 级别记录下来,方便后期排查漏刷的数据 _logger.warning(f"处理第 {index} 行: 大包号 [{big_no}] 未查到状态为 '{select_node_obj.name}' 的小包,跳过更新。")
_logger.warning(f"处理第 {i} 行: 大包号 [{big_no}] 未查到状态为'已提货'的小包,跳过更新。")
# 循环结束后,记录该文件的最终战果 _logger.info(f"==== JSON 数据处理完毕 ==== 整个队列共查到并更新了 {total_updated_count} 个小包!")
_logger.info(f"==== 附件处理完毕 ==== 整个表格共查到并更新了 {total_updated_count} 个小包!")
return True
except Exception as err: except Exception as err:
_logger.error(f'package_data_xls error : {str(err)}') self.env.cr.rollback() # 发生异常时回滚数据库操作
_logger.error(f'process_validated_json_data error: {str(err)}')
raise
def submit(self):
# 1. 获取校验结果
# 可能的返回值有两种:
# - 校验失败:返回一个 Dict (ir.actions.act_window 动作)
# - 校验成功:返回一个 JSON 字符串
result = self.action_import_data_with_validation()
# 2. 拦截错误动作:如果返回的是字典且包含前端动作,说明校验报错了
if isinstance(result, dict) and result.get('type') == 'ir.actions.act_window':
# 直接将动作抛给前端,刷新弹窗显示异常文件,终止后续入库逻辑
return result
# 3. 校验完美通过,执行入库更新
self.process_validated_json_data(result)
# 4. 清理残留的错误文件(防呆设计)
self.write({
'report_file': False,
'file_name': False
})
# 5. 返回成功的前端提示,给用户明确的反馈
return {
'type': 'ir.actions.client',
'tag': 'display_notification',
'params': {
'title': '导入与更新成功',
'message': '包裹数据校验通过,并已成功写入系统!',
'type': 'success',
'sticky': False,
}
}
<?xml version="1.0" encoding="utf-8"?> <?xml version="1.0" encoding="utf-8"?>
<odoo> <odoo>
<data> <data>
<record id="view_package_data_wizard_form" model="ir.ui.view"> <record id="view_package_data_wizard_form" model="ir.ui.view">
<field name="name">package.data.wizard.form</field> <field name="name">package.data.wizard.form</field>
<field name="model">package.data.wizard</field> <field name="model">package.data.wizard</field>
<field name="arch" type="xml"> <field name="arch" type="xml">
<form string="包裹数据处理向导"> <form string="包裹数据处理向导">
<div class="alert alert-info" role="alert" style="margin-bottom: 16px;">
<strong><i class="fa fa-info-circle"/> 操作指引:</strong>
<ul class="mb-0 mt-1">
<li>请先 <a href="/ccs_connect_tiktok/static/xlsx/template.xlsx?v=20250715001" class="alert-link fw-bold" target="_blank"><i class="fa fa-download"/> 下载数据模板</a></li>
<li class="text-danger fw-bold">请务必严格按照模板填写信息,切勿修改表头,否则系统无法识别!</li>
</ul>
</div>
<group> <group>
<group> <group>
<field name="attachment_ids" widget="many2many_binary" string="上传Excel附件" required="1"/> <field name="attachment_ids" widget="many2many_binary" string="上传Excel附件" required="True"/>
<field name="node_id" required="1" domain="[('node_type', '=', 'package'), ('name', 'in', ['待尾程提货', '尾程交接'])]" options="{'no_edit': True, 'no_create': True}"/> <field name="node_id" required="True"
domain="[('node_type', '=', 'package'), ('name', 'in', ['待尾程提货', '尾程交接'])]"
options="{'no_edit': True, 'no_create': True}"/>
<field name="timezone"/>
</group>
<group>
<field name="file_name" invisible="1"/>
<field name="report_file" filename="file_name" widget="binary"
attrs="{'invisible': [('report_file', '=', False)]}"/>
</group> </group>
</group> </group>
<div attrs="{'invisible': [('report_file', '=', False)]}" class="alert alert-warning mt-2" role="alert">
<strong><i class="fa fa-exclamation-triangle"/> 检测到异常数据!</strong>
<br/>
请点击上方下载 <b>异常数据文件</b>,根据文件最后一列的错误提示修改原表,然后再重新上传导入。
</div>
<footer> <footer>
<button name="submit" string="确认处理" type="object" class="btn-primary"/> <button name="submit" string="确认处理" type="object" class="btn-primary" data-hotkey="q"/>
<button string="取消" class="btn-secondary" special="cancel"/> <button string="取消" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer> </footer>
</form> </form>
</field> </field>
</record> </record>
<record id="action_package_data_wizard" model="ir.actions.act_window"> <record id="action_package_data_wizard" model="ir.actions.act_window">
<field name="name">包裹数据导入处理</field> <field name="name">包裹数据导入</field>
<field name="type">ir.actions.act_window</field> <field name="type">ir.actions.act_window</field>
<field name="res_model">package.data.wizard</field> <field name="res_model">package.data.wizard</field>
<field name="view_mode">form</field> <field name="view_mode">form</field>
...@@ -32,8 +56,8 @@ ...@@ -32,8 +56,8 @@
<menuitem id="menu_package_data_wizard" <menuitem id="menu_package_data_wizard"
name="导入包裹数据" name="导入包裹数据"
action="action_package_data_wizard" groups="base.group_system" action="action_package_data_wizard"
groups="base.group_system"
sequence="50"/> sequence="50"/>
</data> </data>
</odoo> </odoo>
\ No newline at end of file
...@@ -157,3 +157,23 @@ class BaseModel(models.AbstractModel): ...@@ -157,3 +157,23 @@ class BaseModel(models.AbstractModel):
return super(BaseModel, self).name_search( return super(BaseModel, self).name_search(
name=name, args=args, operator=operator, limit=limit name=name, args=args, operator=operator, limit=limit
) )
@api.model
def read_group(self, domain, fields, groupby, offset=0, limit=None, orderby=False, lazy=True):
"""Override read_group for all models to support multi-search domains"""
try:
# 1. 拦截前端传来的 domain,使用你已经写好的转换器进行解析
new_domain = self._process_multi_search_args(domain)
# 2. 将解析后正确的 domain 传给原生的 read_group 去做聚合查询
return super(BaseModel, self).read_group(
new_domain, fields, groupby, offset=offset, limit=limit,
orderby=orderby, lazy=lazy
)
except Exception as e:
_logger.warning("Multi-search processing failed in read_group, falling back to normal: %s", str(e))
# 降级处理:如果转换出错,用原始 domain 继续执行,防止系统崩溃
return super(BaseModel, self).read_group(
domain, fields, groupby, offset=offset, limit=limit,
orderby=orderby, lazy=lazy
)
\ No newline at end of file
Markdown 格式
0%
您添加了 0 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论