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

Merge branch 'release/3.14.0'

......@@ -2710,15 +2710,20 @@ msgid "Package Goods"
msgstr "小包商品"
#. 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_history_big_package__goods_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
msgid "Package Qty"
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
#: 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
......@@ -4463,3 +4468,13 @@ msgstr ""
#, python-format
msgid "预览操作失败: %s"
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):
# 增加包裹数量字段, 用于显示包裹数量, 根据big_package_line_ids计算
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
# 托盘号(char型),托盘使用日期
......
......@@ -20,6 +20,11 @@
<field name="implied_ids" eval="[(4, ref('group_clearance_of_customs_user'))]"/>
</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>
......
......@@ -313,6 +313,10 @@
context="{'group_by': 'cc_country_id'}"/>
<filter domain="[]" name="groupby_state" string="Status"
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>
<searchpanel>
<field icon="fa-users" select="multi" name="cc_company_id"/>
......
......@@ -992,3 +992,14 @@ msgstr ""
#: model:ir.model.fields,field_description:ccs_connect_tiktok.field_bl_patrol__email_sent
msgid "邮件已发送"
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 @@
<search position="inside">
<filter string="Not Sync" name="filter_is_sync" domain="[('is_sync','=',False)]"/>
</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>
</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 xlrd
from odoo import models, fields, api, _
from odoo.exceptions import ValidationError
from datetime import date, datetime
import base64
import io
import json
import logging
import pytz
from datetime import datetime, date
from odoo import models, fields, api, _
from odoo.exceptions import UserError
from dateutil import parser
import openpyxl
_logger = logging.getLogger(__name__)
......@@ -13,84 +18,528 @@ class PackageDataWizard(models.TransientModel):
attachment_ids = fields.Many2many('ir.attachment', 'ref_package_data_attachment', 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表格的内容
:return:
将任意时间格式(字符串/日期/时间/Excel数字/时间戳)转为 0时区 (UTC) 时间
"""
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
if val is None or val == '':
return False
def submit(self):
for attachment_obj in self.attachment_ids:
data = self.get_file_path(attachment_obj)
first_table = data.sheets()[0]
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:
try:
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:
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[0]).strip()
if not big_no:
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)
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'
# 优先尝试框架原生的标准解析,效率高
dt = fields.Datetime.from_string(val)
except (ValueError, AttributeError):
try:
# 原生解析失败时,使用 dateutil 兜底解析 "2026/6/25" 等非标格式
dt = parser.parse(val)
except Exception:
return False
elif isinstance(val, datetime):
dt = val
elif isinstance(val, date):
dt = datetime.combine(val, datetime.min.time())
if not dt:
return False
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)])
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 ''
# 【新增优化 4】:预先批量查询“已提货”节点ID及所有相关小包数据
picked_up_node_id = False
sp_by_bp_map = {}
if current_node_name == '待尾程提货':
picked_up_node = self.env['cc.node'].search([('node_type', '=', 'package'), ('name', '=', '已提货')], limit=1)
if picked_up_node:
picked_up_node_id = picked_up_node.id
# 一次性查出表格里所有大包关联的小包
small_packages = self.env['cc.ship.package'].search(
[('big_package_id.big_package_no', 'in', all_pkg_nums)])
# 按大包号对小包进行分组字典映射
for sp in small_packages:
bp_no = sp.big_package_id.big_package_no
if bp_no:
if bp_no not in sp_by_bp_map:
sp_by_bp_map[bp_no] = []
sp_by_bp_map[bp_no].append(sp)
# --- 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('尾程交货时间')
raw_pallet_date = row_data_dict.get('托盘使用日期')
tally_utc = self._parse_and_convert_tz(raw_tally)
delivery_utc = self._parse_and_convert_tz(raw_delivery)
pallet_date_utc = self._parse_and_convert_tz(raw_pallet_date)
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')
if pallet_date_utc:
row_data_dict['托盘使用日期'] = pallet_date_utc.strftime('%Y-%m-%d %H:%M:%S')
# 提取人员与托盘号文本(防呆:去空格判断)
tally_person = row_data_dict.get('理货人')
delivery_person = row_data_dict.get('尾程交货人')
pallet_no = row_data_dict.get('托盘号')
tally_person_str = str(tally_person).strip() if tally_person else ''
delivery_person_str = str(delivery_person).strip() if delivery_person else ''
pallet_no_str = str(pallet_no).strip() if pallet_no else ''
# -----------------------------------------------------
# 【优化 1, 2, 4】:根据变更状态校验时序、必填项及小包状态
# -----------------------------------------------------
if current_node_name == '待尾程提货':
if not raw_tally:
row_errors.append("变更状态为待尾程提货时,理货时间不能为空")
if not tally_person_str:
row_errors.append("变更状态为待尾程提货时,理货人不能为空")
if tally_person_str:
user = self.env['res.users'].sudo().search([('login', '=', tally_person_str)], limit=1)
if not user:
row_errors.append("系统不存在该理货人")
# 【新增优化 4】:小包“已提货”状态交叉校验
if picked_up_node_id:
sps = sp_by_bp_map.get(pkg_num, [])
if not sps:
row_errors.append("该大包下未找到任何系统小包记录")
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)}')
# 找出所有状态不是“已提货”的小包 (注: 这里假定小包模型代表单号的字段为 name)
not_ready_sps = [sp.logistic_order_no for sp in sps if sp.state.id != picked_up_node_id]
if len(not_ready_sps) == len(sps):
row_errors.append("该大包下的小包全部不是已提货状态")
elif len(not_ready_sps) > 0:
# 部分未提货,截取前10个单号展示
show_sps = not_ready_sps[:10]
row_errors.append(f"部分小包不是已提货状态: {', '.join(show_sps)}等")
elif current_node_name == '尾程交接':
if not raw_delivery:
row_errors.append("变更状态为尾程交接时,尾程交货时间不能为空")
if not delivery_person_str:
row_errors.append("变更状态为尾程交接时,尾程交货人不能为空")
if delivery_person_str:
user = self.env['res.users'].sudo().search([('login', '=', delivery_person_str)], limit=1)
if not user:
row_errors.append("系统不存在该尾程交货人")
# -----------------------------------------------------
# 【优化 3】:托盘号、托盘使用日期必填及未来日期校验
# -----------------------------------------------------
if not pallet_no_str:
row_errors.append("托盘号不能为空")
# print(raw_pallet_date, pallet_date_utc, now_utc)
if not raw_pallet_date:
row_errors.append("托盘使用日期不能为空")
elif pallet_date_utc and pallet_date_utc > now_utc:
row_errors.append("托盘使用日期不能是未来日期")
# -----------------------------------------------------
# 原逻辑:理货时间、尾程交货时间不能是未来时间
# -----------------------------------------------------
if tally_utc and tally_utc > now_utc:
row_errors.append("理货时间不能是未来时间")
if delivery_utc and delivery_utc > now_utc:
row_errors.append("尾程交货时间不能是未来时间")
# -----------------------------------------------------
# 结合系统数据的状态时序校验
# -----------------------------------------------------
sys_pkg = sys_pkg_map.get(pkg_num)
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("理货时间必须大于系统内该大包的提货时间")
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)
error_file_name = f'异常数据.xlsx'
encoded_file = base64.b64encode(out_stream.read())
self.write({
'report_file': encoded_file,
'file_name': error_file_name
})
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)
# print(json_result)
return json_result
# 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:
# 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 == '待尾程提货':
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)
if not select_node_obj:
_logger.error("在系统中未找到匹配的 cc.node 状态字典数据!")
return
# 3. 初始化累加器与用户查询缓存(避免重复查库拖慢速度)
total_updated_count = 0
user_cache = {}
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:
continue
# 提取数据(如果为空,转换为 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
# 5. 查找符合条件的小包并执行 SQL 更新
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"处理第 {index} 行: 大包号 [{big_no}] 查到 {current_count} 个小包准备更新。")
# 原生 SQL 批量更新小包
sql_small = """
UPDATE cc_ship_package
SET state = %s, process_time = %s, is_sync = False
WHERE id IN %s
"""
self.env.cr.execute(sql_small, (self.node_id.id, update_time, tuple(ship_package_objs.ids)))
# 6. 查找大包并更新
big_package_obj = self.env['cc.big.package'].sudo().search([('big_package_no', '=', big_no)],
limit=1)
if big_package_obj:
if self.node_id.name == '待尾程提货':
sql_big = """
UPDATE cc_big_package
SET pallet_number = %s,
pallet_usage_date = %s,
tally_user_id = %s,
tally_time = %s
WHERE id = %s
"""
self.env.cr.execute(sql_big, (
pallet_no,
pallet_date,
lh_user_id,
lh_time,
big_package_obj.id
))
big_package_obj.tally_state = 'checked_goods'
else:
sql_big = """
UPDATE cc_big_package
SET pallet_number = %s,
pallet_usage_date = %s,
delivery_user_id = %s,
delivery_time = %s
WHERE id = %s
"""
self.env.cr.execute(sql_big, (
pallet_no,
pallet_date,
wj_user_id,
wj_time,
big_package_obj.id
))
big_package_obj.tally_state = 'handover_completed'
# 7. 更新托盘状态
if pallet_no:
pallet_obj = self.env['cc.pallet'].sudo().search([('name', '=', pallet_no)], limit=1)
if pallet_obj:
pallet_obj.usage_state = 'used'
else:
_logger.warning(f"处理第 {index} 行: 大包号 [{big_no}] 未查到状态为 '{select_node_obj.name}' 的小包,跳过更新。")
_logger.info(f"==== JSON 数据处理完毕 ==== 整个队列共查到并更新了 {total_updated_count} 个小包!")
return True
except Exception as 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,
'next': {
'type': 'ir.actions.client',
'tag': 'reload',
}
}
}
<?xml version="1.0" encoding="utf-8"?>
<odoo>
<data>
<record id="view_package_data_wizard_form" model="ir.ui.view">
<field name="name">package.data.wizard.form</field>
<field name="model">package.data.wizard</field>
<field name="arch" type="xml">
<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>
<field name="attachment_ids" widget="many2many_binary" string="上传Excel附件" required="1"/>
<field name="node_id" required="1" domain="[('node_type', '=', 'package'), ('name', 'in', ['待尾程提货', '尾程交接'])]" options="{'no_edit': True, 'no_create': True}"/>
<field name="attachment_ids" widget="many2many_binary" string="上传Excel附件" required="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>
<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>
<button name="submit" string="确认处理" type="object" class="btn-primary"/>
<button string="取消" class="btn-secondary" special="cancel"/>
<button name="submit" string="确认处理" type="object" class="btn-primary" data-hotkey="q"/>
<button string="取消" class="btn-secondary" special="cancel" data-hotkey="z"/>
</footer>
</form>
</field>
</record>
<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="res_model">package.data.wizard</field>
<field name="view_mode">form</field>
<field name="target">new</field>
</record>
<menuitem id="menu_package_data_wizard"
<menuitem id="menu_import_package_data_wizard"
name="导入包裹数据"
action="action_package_data_wizard" groups="base.group_system"
action="action_package_data_wizard"
groups="ccs_base.group_import_package"
sequence="50"/>
</data>
</odoo>
\ No newline at end of file
......@@ -157,3 +157,23 @@ class BaseModel(models.AbstractModel):
return super(BaseModel, self).name_search(
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 到此讨论。请谨慎行事。
请先完成此评论的编辑!
注册 或者 后发表评论