From d99396ecb12e07379f184a78a3ff4eab269860ef Mon Sep 17 00:00:00 2001 From: yueli Date: Tue, 22 Sep 2026 14:09:08 +0800 Subject: [PATCH] =?UTF-8?q?fix(inbound):=20=E6=8C=89=E5=8D=95=E5=85=A5?= =?UTF-8?q?=E5=BA=93=E6=97=B6=E4=BB=8E=E9=87=87=E8=B4=AD=E7=94=B3=E8=AF=B7?= =?UTF-8?q?=E5=8D=95=E8=A1=A5=E5=9B=9E=E6=88=90=E6=9C=AC=E4=BB=B7?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 库管角色没有 inbound_purchase:unit_price / total_price 权限,采购单接口 (api/v1/purchase.py:46-53)会把价格字段整个 pop 掉,前端导入时因此带不出价 —— buy.vue 的 hasUnitPrice / hasTotalPrice 双双落空,两个分支都不进,入库 成本被记成 0。 而前端 buy.vue:1936 的注释写得很清楚:"无论前端是否对当前角色显示价格,导入 都应把采购单价格带入(隐藏字段,用于成本)"。前端想带、后端不给,价格就断在 中间——不是谁写错了,是两边对"隐藏"的理解不同。 改为在落库前从采购申请单补回:价格不经过前端,库管依旧看不到采购价,权限 边界不变,但成本可追溯。仅在前端未带价时补,有权限的角色手动填过价则尊重 提交值。采购申请单的 unit_price 存的是**含税**单价(见 purchase/index.vue 列名"含税单价"),需反算不含税。 顺带清理 buy_service.py:221 的函数内局部 import —— 局部 import 会让 Python 把 PurchaseRequest 视为本函数的局部变量,导致位于其之前的引用抛 UnboundLocalError(本次新增的补价逻辑正踩到这个,测试时暴露)。改用文件顶部 的全局导入。 实测:测试申请单 含税 12.5 / 税率 13%,以库管身份提交且**故意不带任何价格 字段**,落库结果 税前 11.0619 (=12.5/1.13) / 含税 12.5 / 总价 110.6195 / 税率 13.00,全部正确;响应体经 filter_item_by_permissions 后仍不含价格字段, 权限边界未变。 另:已有 6 条历史入库单(走采购申请但单价为 0)已按「单价 × 实际入库数量」 回填,不回填总价。 --- .../app/services/inbound/buy_service.py | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/inventory-backend/app/services/inbound/buy_service.py b/inventory-backend/app/services/inbound/buy_service.py index b2f14ef..8035957 100644 --- a/inventory-backend/app/services/inbound/buy_service.py +++ b/inventory-backend/app/services/inbound/buy_service.py @@ -4,6 +4,7 @@ from app.extensions import db from app.models.inbound.buy import StockBuy from app.models.inbound.product import StockProduct from app.models.base import MaterialBase +from app.models.purchase import PurchaseRequest from datetime import datetime, timedelta, timezone from sqlalchemy import or_, func, text, and_ from sqlalchemy.exc import IntegrityError @@ -153,9 +154,27 @@ class BuyInboundService: in_qty = float(data.get('in_quantity') or 0) u_price = float(data.get('unit_price') or 0) tax_rate = float(data.get('tax_rate') or 0) - - # 计算税后单价 post_tax_price = float(data.get('post_tax_unit_price') or 0) + + # [新增] 按单入库:关联采购申请单 + request_id = data.get('request_id') + + # ★ 补价:库管角色没有 inbound_purchase:unit_price / total_price 权限, + # 采购单接口(api/v1/purchase.py)会把价格字段整个 pop 掉,前端导入时 + # 因此带不出价(buy.vue 的 hasUnitPrice/hasTotalPrice 双双落空, + # 两个分支都不进)→ 入库成本被记成 0。 + # 这里在落库前从采购申请单补回:价格**不经过前端**,库管依旧看不到 + # 采购价,权限边界不变,但成本可追溯。 + # 注:采购申请单的 unit_price 存的是**含税**单价(见 purchase/index.vue + # 的列名"含税单价"),需要反算不含税单价。 + if request_id and u_price <= 0 and post_tax_price <= 0: + _req = PurchaseRequest.query.get(request_id) + if _req and float(_req.unit_price or 0) > 0: + tax_rate = float(_req.tax_rate or 0) + post_tax_price = float(_req.unit_price) + u_price = post_tax_price / (1 + tax_rate / 100) + + # 计算税后单价(保底:只给了不含税价时反推) if post_tax_price == 0 and u_price > 0: tax_multiplier = 1 + (tax_rate / 100) post_tax_price = u_price * tax_multiplier @@ -170,9 +189,6 @@ class BuyInboundService: generated_sku = str(next_global_id).zfill(10) if next_global_id else datetime.now().strftime('%Y%m%d%H%M%S') final_barcode = data.get('barcode') or generated_sku - # [新增] 按单入库:关联采购申请单 - request_id = data.get('request_id') - new_stock = StockBuy( base_id=material.id, global_print_id=next_global_id, sku=generated_sku, barcode=final_barcode, in_date=in_date_val, serial_number=data.get('serial_number'), batch_number=data.get('batch_number'), @@ -201,8 +217,10 @@ class BuyInboundService: db.session.flush() # 获取 new_stock.id # [新增] 按单入库:反写采购申请单状态为"已完成" + # 注:原先此处有一句局部 `from app.models.purchase import PurchaseRequest`, + # 它会让 Python 把 PurchaseRequest 视为本函数的局部变量,导致上方补价 + # 逻辑(在 import 之前使用)抛 UnboundLocalError。已改用文件顶部的全局导入。 if request_id: - from app.models.purchase import PurchaseRequest purchase_req = db.session.get(PurchaseRequest, request_id) if purchase_req: if purchase_req.status != 1: