fix: sync actual order quantity to order backup

This commit is contained in:
Codex 2026-10-07 15:17:15 +08:00
parent b46717065d
commit 18b3fd7ff7

View File

@ -349,6 +349,25 @@ def _select_confirm_bom(request_bom, saved_bom, base_bom):
return base_bom return base_bom
def _extract_product_actual_qty(items, product_code):
"""从 BOM 明细中读取明确填写的成品实际下单数量。
返回 None 表示请求没有携带成品行;返回 0 也表示用户明确填写了 0,
不能再回退到旧的正式下单数量。"""
target_code = f'成品_{product_code}'
for item in items or []:
if not isinstance(item, dict) or item.get('material_code') != target_code:
continue
value = item.get('actual_quantity')
if value in (None, ''):
return None
try:
return max(0, int(float(value)))
except (TypeError, ValueError):
return 0
return None
def _normalize_pending_bom_items(items): def _normalize_pending_bom_items(items):
"""规范化待下单 BOM,同时保留下单前/后包材数据。""" """规范化待下单 BOM,同时保留下单前/后包材数据。"""
normalized = [] normalized = []
@ -1076,7 +1095,9 @@ def purchase_update_pending():
bom_items = data.get('bom_items') bom_items = data.get('bom_items')
if bom_items is not None: if bom_items is not None:
import json import json
bom_str = json.dumps(bom_items, ensure_ascii=False) normalized_bom_items = _normalize_pending_bom_items(bom_items)
product_actual_qty = _extract_product_actual_qty(normalized_bom_items, product_code)
bom_str = json.dumps(normalized_bom_items, ensure_ascii=False)
try: try:
old_bom_items = json.loads(current.get('bom_items') or '[]') old_bom_items = json.loads(current.get('bom_items') or '[]')
except (TypeError, ValueError): except (TypeError, ValueError):
@ -1141,9 +1162,15 @@ def purchase_update_pending():
changes.append( changes.append(
f'{display_code} {label}:{old_display} → {new_display}' f'{display_code} {label}:{old_display} → {new_display}'
) )
if product_actual_qty is None:
query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s AND status='pending'", (bom_str, product_code)) query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s AND status='pending'", (bom_str, product_code))
else:
old_formal_qty = int((current.get('formal_order_qty') or 0))
if old_formal_qty != product_actual_qty:
changes.append(f'成品实际下单数量:{old_formal_qty} → {product_actual_qty}')
query("UPDATE pending_orders SET formal_order_qty=%s, bom_items=%s WHERE product_code=%s AND status='pending'", (product_actual_qty, bom_str, product_code))
# 待下单详情中的“下单前包材”与计划预测共用 bom_packaging 真源,保存时同步更新。 # 待下单详情中的“下单前包材”与计划预测共用 bom_packaging 真源,保存时同步更新。
_sync_pending_pre_packaging(product_code, _normalize_pending_bom_items(bom_items)) _sync_pending_pre_packaging(product_code, normalized_bom_items)
detail = f'更新 {product_code}' detail = f'更新 {product_code}'
if changes: if changes:
@ -1227,6 +1254,13 @@ def purchase_confirm_order():
if not rows: if not rows:
continue continue
row = rows[0] row = rows[0]
request_bom = bom_data_map.get(pc)
request_product_actual_qty = _extract_product_actual_qty(request_bom, pc)
if request_product_actual_qty is not None:
# 用户在下单信息弹窗中明确填写的成品实际下单数量优先于旧快照。
# 这一步必须发生在生成国内/海外订单之前,否则订单和下单备份会沿用旧数量。
row['formal_order_qty'] = request_product_actual_qty
query("UPDATE pending_orders SET formal_order_qty=%s WHERE id=%s AND status='pending'", (request_product_actual_qty, row['id']))
# 1.5 原子抢占:先标记为已下单,防止重复点击/并发请求重复生成订单。 # 1.5 原子抢占:先标记为已下单,防止重复点击/并发请求重复生成订单。
# 只有抢占成功(status 从 pending 变为 ordered)的请求才会继续插入订单, # 只有抢占成功(status 从 pending 变为 ordered)的请求才会继续插入订单,
@ -1344,7 +1378,6 @@ def purchase_confirm_order():
# 当前确认下单请求携带的 bom_data_map 是本次弹窗的最新数据源; # 当前确认下单请求携带的 bom_data_map 是本次弹窗的最新数据源;
# 未携带时使用待下单记录中已保存的 BOM,确保刷新页面后确认下单仍能带上包材数量和备注。 # 未携带时使用待下单记录中已保存的 BOM,确保刷新页面后确认下单仍能带上包材数量和备注。
request_bom = bom_data_map.get(pc)
bom_list = _select_confirm_bom(request_bom, db_bom_list, []) bom_list = _select_confirm_bom(request_bom, db_bom_list, [])
if not bom_list: if not bom_list:
# 待下单记录尚未保存 BOM 时,才回退到当前产品 BOM 基础清单。 # 待下单记录尚未保存 BOM 时,才回退到当前产品 BOM 基础清单。
@ -1357,11 +1390,9 @@ def purchase_confirm_order():
bom_list = _normalize_backup_bom(bom_list) bom_list = _normalize_backup_bom(bom_list)
# 成品实际下单数量优先取下单信息弹窗中的“成品实际下单数量”, # 成品实际下单数量优先取下单信息弹窗中的“成品实际下单数量”,
# 未填写时才回退到待下单记录的正式下单数量。 # 未填写时才回退到待下单记录的正式下单数量。
product_actual_qty = _extract_product_actual_qty(bom_list, pc)
if product_actual_qty is None:
product_actual_qty = int(qty or 0) product_actual_qty = int(qty or 0)
for bm in bom_list:
if bm.get('material_code') == f'成品_{pc}' and bm.get('actual_quantity') is not None:
product_actual_qty = int(bm.get('actual_quantity') or 0)
break
logging.warning(f'[SYNC_PACKAGING] pc={pc}, formal_qty={qty}, product_actual_qty={product_actual_qty}, bom_list_len={len(bom_list)}') logging.warning(f'[SYNC_PACKAGING] pc={pc}, formal_qty={qty}, product_actual_qty={product_actual_qty}, bom_list_len={len(bom_list)}')
# BOM用量以 bom 表为权威来源:前端 bom_items 里的 quantity 可能被「实际下单数量」污染, # BOM用量以 bom 表为权威来源:前端 bom_items 里的 quantity 可能被「实际下单数量」污染,