diff --git a/routes/purchase.py b/routes/purchase.py index edb2c7b..259b029 100644 --- a/routes/purchase.py +++ b/routes/purchase.py @@ -349,6 +349,25 @@ def _select_confirm_bom(request_bom, saved_bom, 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): """规范化待下单 BOM,同时保留下单前/后包材数据。""" normalized = [] @@ -1076,7 +1095,9 @@ def purchase_update_pending(): bom_items = data.get('bom_items') if bom_items is not None: 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: old_bom_items = json.loads(current.get('bom_items') or '[]') except (TypeError, ValueError): @@ -1141,9 +1162,15 @@ def purchase_update_pending(): changes.append( f'{display_code} {label}:{old_display} → {new_display}' ) - query("UPDATE pending_orders SET bom_items=%s WHERE product_code=%s AND status='pending'", (bom_str, product_code)) + 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)) + 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 真源,保存时同步更新。 - _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}' if changes: @@ -1227,6 +1254,13 @@ def purchase_confirm_order(): if not rows: continue 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 原子抢占:先标记为已下单,防止重复点击/并发请求重复生成订单。 # 只有抢占成功(status 从 pending 变为 ordered)的请求才会继续插入订单, @@ -1344,7 +1378,6 @@ def purchase_confirm_order(): # 当前确认下单请求携带的 bom_data_map 是本次弹窗的最新数据源; # 未携带时使用待下单记录中已保存的 BOM,确保刷新页面后确认下单仍能带上包材数量和备注。 - request_bom = bom_data_map.get(pc) bom_list = _select_confirm_bom(request_bom, db_bom_list, []) if not bom_list: # 待下单记录尚未保存 BOM 时,才回退到当前产品 BOM 基础清单。 @@ -1357,11 +1390,9 @@ def purchase_confirm_order(): bom_list = _normalize_backup_bom(bom_list) # 成品实际下单数量优先取下单信息弹窗中的“成品实际下单数量”, # 未填写时才回退到待下单记录的正式下单数量。 - 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 + product_actual_qty = _extract_product_actual_qty(bom_list, pc) + if product_actual_qty is None: + product_actual_qty = int(qty or 0) 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 可能被「实际下单数量」污染,