diff --git a/inventory-backend/app/api/v1/scrap.py b/inventory-backend/app/api/v1/scrap.py index 4032317..955a9a8 100644 --- a/inventory-backend/app/api/v1/scrap.py +++ b/inventory-backend/app/api/v1/scrap.py @@ -1,5 +1,5 @@ # inventory-backend/app/api/v1/scrap.py -from flask import Blueprint, request, jsonify +from flask import Blueprint, request, jsonify, current_app from flask_jwt_extended import jwt_required, get_jwt_identity, get_jwt from app.utils.decorators import permission_required, get_current_company_filter from app.services.auth_service import AuthService @@ -71,8 +71,34 @@ def scan_barcode(): if not barcode: return jsonify({'code': 400, 'msg': '请提供条码'}), 400 + # ★ 可选:当前正在执行的报废申请单 id。 + # 传入后,扫码命中多个来源时优先返回该单批准明细里指定的那一条。 + # 必要性:在管不良品的 SKU 复制自原库存行,同一码可能同时命中 + # stock_buy#M 与 trans_defective_goods#N,这是两个不同实物, + # 条码本身无法区分,只有单据上下文能决定该扫到哪个。 + request_id = request.args.get('request_id', type=int) + prefer_pairs = None + if request_id: + try: + from app.models.scrap_approval import ScrapApproval + req = db.session.get(ScrapApproval, request_id) + if req: + prefer_pairs = set() + for it in (req.get_items() or []): + try: + prefer_pairs.add(( + str(it.get('source_table') or '').strip(), + int(it.get('stock_id')), + )) + except (TypeError, ValueError): + continue + except Exception as e: + # 优先集构造失败不应阻断扫码 —— 退回固定顺序选路,行为与改造前一致 + current_app.logger.warning(f"[scrap/scan] 构造优先命中集失败: {e}") + prefer_pairs = None + try: - result = ScrapService.get_stock_by_barcode(barcode) + result = ScrapService.get_stock_by_barcode(barcode, prefer_pairs=prefer_pairs) if result: # ★ Fail-Closed: 扫码响应剥离价格字段 result.pop('price', None) @@ -174,8 +200,23 @@ def get_scrap_records(): class ScrapService: @staticmethod - def get_stock_by_barcode(barcode): - """根据条码查找库存""" + def get_stock_by_barcode(barcode, prefer_pairs=None): + """ + 根据条码查找库存实物。 + + prefer_pairs: {(source_table, row_id), ...} 可选 —— 「优先命中集」, + 由调用方从**当前报废申请单的批准明细**构造。 + + ★ 为什么需要它(这是必须的,不是优化): + 在管不良品的 SKU 是从原库存行**复制**的(退回时 + sku=getattr(stock_row,'sku','')),因此同一个条码可能同时命中 + stock_buy#M 与 trans_defective_goods#N —— 这是**两个不同的实物**。 + 仅凭条码无法区分,原实现「按固定顺序返回首个命中」会让库存行 + 永久遮蔽不良品,导致执行时报「不在批准明细中」。 + 只有单据上下文能决定该扫到哪一个,故由调用方传入优先集。 + + 返回值形状与改造前完全一致,前端无需按来源分支处理。 + """ if not barcode: return None clean_code = barcode.strip() @@ -187,32 +228,32 @@ class ScrapService: return float(item.pre_tax_unit_price) if item.pre_tax_unit_price else 0 return 0 - # 1. 查询成品 + # ---- 收集全部候选(不再首个即返回)---- + candidates = [] + prod = StockProduct.query.filter( db.or_(StockProduct.barcode == clean_code, StockProduct.sku == clean_code) ).first() if prod: res = ScrapService._format_stock(prod, 'stock_product') res['price'] = get_price(prod, 'stock_product') - return res + candidates.append(res) - # 2. 查询半成品 semi = StockSemi.query.filter( db.or_(StockSemi.barcode == clean_code, StockSemi.sku == clean_code) ).first() if semi: res = ScrapService._format_stock(semi, 'stock_semi') res['price'] = 0 - return res + candidates.append(res) - # 3. 查询原材料 buy = StockBuy.query.filter( db.or_(StockBuy.barcode == clean_code, StockBuy.sku == clean_code) ).first() if buy: res = ScrapService._format_stock(buy, 'stock_buy') res['price'] = get_price(buy, 'stock_buy') - return res + candidates.append(res) # 4. 查询在管不良品台账(逆向物流:坏件不入库存表,由独立台账承载) # @@ -231,7 +272,7 @@ class ScrapService: if defective: # 在管量即「可报废量」,回填到既有字段形状,前端无需按来源分支处理 remain = float(defective.remaining_qty or 0) - return { + candidates.append({ 'id': defective.id, 'sku': defective.sku, 'barcode': defective.sku, @@ -243,9 +284,24 @@ class ScrapService: 'stock_quantity': remain, 'available_quantity': remain, 'source_table': 'trans_defective_goods', - } + }) - return None + if not candidates: + return None + + # ---- 选路:单据上下文优先,否则维持改造前的固定顺序(首个命中)---- + # ★ 同一 SKU 同时存在于库存表与在管台账时,条码无法区分二者; + # 批准明细说该报废哪一个,就返回哪一个。 + if prefer_pairs: + for res in candidates: + try: + pair = (str(res.get('source_table') or '').strip(), int(res.get('id'))) + except (TypeError, ValueError): + continue + if pair in prefer_pairs: + return res + + return candidates[0] @staticmethod def _format_stock(item, table_type): diff --git a/inventory-web/src/api/scrap.ts b/inventory-web/src/api/scrap.ts index 468f1a1..a38ba98 100644 --- a/inventory-web/src/api/scrap.ts +++ b/inventory-web/src/api/scrap.ts @@ -1,11 +1,16 @@ import request from '@/utils/request' // 1. 扫码查询库存 -export function scanBarcode(barcode: string) { +// +// ★ requestId 必须传:在管不良品的 SKU 复制自原库存行,同一码可能同时 +// 命中 stock_buy#M 与 trans_defective_goods#N —— 这是两个不同实物, +// 条码本身无法区分。带上当前申请单 id 后,后端会优先返回该单批准明细 +// 指定的那一条,避免「清单里明明有、扫码却说不在批准明细中」。 +export function scanBarcode(barcode: string, requestId?: number) { return request({ url: '/v1/scrap/scan', method: 'get', - params: { barcode } + params: { barcode, request_id: requestId } }) } diff --git a/inventory-web/src/views/operation/scrap/create.vue b/inventory-web/src/views/operation/scrap/create.vue index a2918db..d820ad2 100644 --- a/inventory-web/src/views/operation/scrap/create.vue +++ b/inventory-web/src/views/operation/scrap/create.vue @@ -576,8 +576,9 @@ const handleManualInput = async () => { return } - // 2. 查库 - const res: any = await scanBarcode(code) + // 2. 查库(★ 必须带当前申请单 id:同一 SKU 可能同时存在于库存表与 + // 在管不良品台账,两者是不同实物,只有单据上下文能决定该扫到哪个) + const res: any = await scanBarcode(code, selectedRequest.value?.id) if (res.code === 200 && res.data) { const item = res.data