From fc953576628b4d46bc0b11cf4f53d22f92de66fc Mon Sep 17 00:00:00 2001 From: yueli Date: Fri, 11 Sep 2026 14:05:59 +0800 Subject: [PATCH] =?UTF-8?q?feat(stocktake):=20=E8=B7=A8=E5=85=AC=E5=8F=B8?= =?UTF-8?q?=E6=89=AB=E7=A0=81=E7=BB=99=E7=B2=BE=E5=87=86=E6=8A=A5=E9=94=99?= =?UTF-8?q?=EF=BC=8C0=20=E5=BA=93=E5=AD=98=E7=9B=98=E7=9B=88=E5=9C=A8?= =?UTF-8?q?=E6=98=8E=E7=BB=86=E4=B8=AD=E5=8F=AF=E8=A7=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 【跨公司拦截】 get_stock_info 带 company_name 过滤后,扫到别家公司的货会直接查不到, 返回 404「未找到物料」—— 与「条码根本不存在」完全无法区分,现场人员 既不知道是扫错了还是扫了别人的货。 新增 find_stock_owner_company():不施加公司过滤地全局查条码归属; get_stock_info 未命中时由 _classify_missing_stock() 复核,区分两种情形: · 条码存在但属于别家公司 → 403「条码 [X] 属于【LICA】,请勿跨公司盘点」 · 条码不存在 → 404「未找到该物料库存: X」 该方案的前提「SKU/条码全系统唯一」已核验成立:三张库存表的表内重复、 跨公司重复、跨表重复六项检查均为 0,故归属至多命中一条,无歧义。 /scan 与 /draft/add 两个入口均已接入。 【0 库存盘盈可见性】 merged-list 的 union_sql 原本严格要求 stock_quantity > 0,导致账面为 0、 但已被扫入的盘盈物料不出现在明细抽屉里,而 total_scanned 却把它计入 「已盘」—— 工人看到已盘 +1 却在明细里找不到,以为系统丢了数据。 现改为 stock_quantity > 0 OR id IN (本会话该表的草稿 stock_id)。 条件严格限定在**本会话**的草稿,不会把全库 0 库存物料都放出来。 实测: IRIS 扫 LICA 条码 → 403「条码 [0000002097] 属于【LICA】,请勿跨公司盘点」 扫不存在条码 → 404「未找到该物料库存: NOPE-99999」 已扫的 0 库存物料 → 明细可见(账面 0 / 实盘 5 / 差异 +5 / 库位 Y3/3/3/4) 未扫的 0 库存物料 → 明细 0 行(范围正确) --- inventory-backend/app/api/v1/inbound/stock.py | 74 +++++++++++++++++-- 1 file changed, 68 insertions(+), 6 deletions(-) diff --git a/inventory-backend/app/api/v1/inbound/stock.py b/inventory-backend/app/api/v1/inbound/stock.py index a1b0a48..67699ad 100644 --- a/inventory-backend/app/api/v1/inbound/stock.py +++ b/inventory-backend/app/api/v1/inbound/stock.py @@ -261,6 +261,50 @@ def _get_session_scope(session_id): return STOCKTAKE_SCOPE_ACTIVE, list(locs) +def find_stock_owner_company(uuid_or_barcode): + """全局查找该条码/SKU 属于哪个公司 —— **不施加任何公司过滤**。 + + 只在 get_stock_info 未命中时调用,用来区分两种「查不到」: + · 条码根本不存在 → 让工人核对是否扫错 / 敲错 + · 条码存在但属于别家公司 → 明确告知跨公司,而不是笼统的「未找到物料」 + + 前提:SKU / 条码在全系统全局唯一。已核验三张库存表「表内重复、跨公司重复、 + 跨表重复」六项检查均为 0,因此这里至多命中一条,不存在归属歧义。 + + :return: 公司名;查不到则 None + """ + code = str(uuid_or_barcode).strip() + if not code: + return None + + checks = [ + (StockProduct, lambda m, c: db.or_(m.barcode == c, m.sku == c, m.serial_number == c)), + (StockSemi, lambda m, c: db.or_(m.barcode == c, m.sku == c, m.serial_number == c)), + (StockBuy, lambda m, c: db.or_(m.barcode == c, m.sku == c)), + ] + for model, cond_fn in checks: + if not model: + continue + row = db.session.query(MaterialBase.company_name).join( + model, model.base_id == MaterialBase.id + ).filter(cond_fn(model, code)).first() + if row and row[0]: + return row[0] + return None + + +def _classify_missing_stock(code, company_name): + """把「查不到库存」细分为「不存在」与「跨公司」。 + + :return: (msg, http_status) —— 调用方按自己的响应结构包装 + """ + if company_name and company_name != '__NO_COMPANY__': + owner = find_stock_owner_company(code) + if owner and owner != company_name: + return f'条码 [{code}] 属于【{owner}】,请勿跨公司盘点', 403 + return f'未找到该物料库存: {code}', 404 + + def get_stock_info(uuid_or_barcode, company_name=None): """ 根据 uuid 或 barcode 查询库存信息(★ 精确匹配优先,性能与准确性兼顾) @@ -631,9 +675,12 @@ def scan_stock_by_barcode(): if not barcode: return jsonify({'code': 400, 'msg': 'barcode 不能为空'}), 400 - item, source_table, stock_id = get_stock_info(barcode, get_current_company_filter()) + company_limit = get_current_company_filter() + item, source_table, stock_id = get_stock_info(barcode, company_limit) if not item: - return jsonify({'code': 404, 'msg': f'未找到该物料库存: {barcode}'}), 404 + # 区分「条码不存在」与「属于别家公司」,给现场人员可执行的提示 + msg, status = _classify_missing_stock(barcode, company_limit) + return jsonify({'code': status, 'msg': msg}), status d = item.to_dict() d['stock_type'] = source_table.replace('stock_', '') @@ -796,7 +843,9 @@ def add_draft(): # 获取库存信息 item, source_table, stock_id = get_stock_info(uuid, company_name) if not item: - return jsonify({"message": "未找到对应的库存记录"}), 404 + # 区分「条码不存在」与「属于别家公司」 + msg, status = _classify_missing_stock(uuid, company_name) + return jsonify({"message": msg}), status # 账面总库存 stock_qty = float(item.stock_quantity) if item.stock_quantity else 0 @@ -1974,18 +2023,31 @@ def get_draft_merged_list(): company_cond_bare = ' AND company_name = :company' if company_name is not None else '' # ── 公共 CTE / 子查询片段 ── + # ★ 0 库存盘盈可见性:账面为 0、但本会话已被扫入的物料也要进明细列表。 + # 否则工人扫到货架上账面为 0 的实物(盘盈)时,「已盘」计数会 +1, + # 明细里却找不到这一行,看起来像系统丢了数据。 + # 注意条件必须限定在**本会话**的草稿,不能是库容里存在过草稿就放行。 union_sql = """ SELECT id, 'stock_buy' AS source_table, sku, stock_quantity AS stock_qty, warehouse_location, base_id - FROM stock_buy WHERE stock_quantity > 0 + FROM stock_buy + WHERE stock_quantity > 0 + OR id IN (SELECT sd.stock_id FROM stocktake_draft sd + WHERE sd.source_table = 'stock_buy' AND sd.session_id = :sid) UNION ALL SELECT id, 'stock_semi', sku, stock_quantity, warehouse_location, base_id - FROM stock_semi WHERE stock_quantity > 0 + FROM stock_semi + WHERE stock_quantity > 0 + OR id IN (SELECT sd.stock_id FROM stocktake_draft sd + WHERE sd.source_table = 'stock_semi' AND sd.session_id = :sid) UNION ALL SELECT id, 'stock_product', sku, stock_quantity, warehouse_location, base_id - FROM stock_product WHERE stock_quantity > 0 + FROM stock_product + WHERE stock_quantity > 0 + OR id IN (SELECT sd.stock_id FROM stocktake_draft sd + WHERE sd.source_table = 'stock_product' AND sd.session_id = :sid) """ # ── 构建 WHERE 条件 ──