7 Commits

Author SHA1 Message Date
0b4a2b29d6 V3.91,9.22推送 2026-09-22 14:21:00 +08:00
3851ebd7bc fix(inbound): 导入采购单改为单价优先,修复多发/少发时单价被摊薄
原逻辑是总价优先(hasTotalPrice 分支在前):用「申请单总价 ÷ 本次入库数量」
反算单价。但申请单的 total_price 是「申请数量」的钱,实际入库数量未必相同
—— 厂家怕出问题多发(申请 100 个、实发 104 个)是常态,用总价除以实际数量
会把单价摊薄或抬高。

改为单价优先、总价仅作兜底,总价由「单价 × 实际入库数量」得出。与后端
buy_service 的补价口径一致。

顺带修一处隐患:导入新单前清掉 postTaxTotalManuallySet / postTaxTotalInput。
此前若上一单手工改过含税总价,这两个 ref 会残留,updatePrices 末尾会拿着
旧总价回头覆盖新导入的价格 —— 连续导入两张单时必现。

影响面:库管本就拿不到价格(接口剥离),此段对其一直是空转;受影响的是有
价格权限的角色在页面上导入采购单时的计算口径。
2026-09-22 14:09:09 +08:00
d99396ecb1 fix(inbound): 按单入库时从采购申请单补回成本价
库管角色没有 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)已按「单价 × 实际入库数量」
回填,不回填总价。
2026-09-22 14:09:08 +08:00
91db54982c feat(track): 入库/出库 webhook 按公司路由到对应实例
三个触发点此前都不带 company_name,无从路由:
  · product_service / semi_service:入库通知补 material.company_name
  · outbound_service:track_notifications 元组加 company(取自
    stock_record.base.company_name;维修单走 repair.base,两处
    relationship 均已定义),发送时带进 payload

出库路径同时**去掉**了显式传入的 url=TRACK_OUTBOUND_WEBHOOK_URL ——
url 参数优先级最高,保留它会让出库永远打到默认实例,company 路由形同
虚设。url 参数本身保留,需要强制覆盖时仍可用。

实测:
  LICA 入库:待仓库收货 → 已入库,IRIS 库该 SN 0 行
  LICA 出库:已入库 → 已出库,IRIS 库该 SN 0 行
  IRIS 回归:对已入库设备重发入库通知,状态幂等不变
  精确隔离:发一条 IRIS 通知,lica_backend 日志 +0 行、track_backend +19 行

注意:不做"两个实例都推一遍"——两个 Track 的 webhook key 是同一把、
两边都会鉴权通过,而 Track 按 serial_number/sku 匹配,相同型号设备会
同时命中两库,把 IRIS 的设备误标成已入库,且产生 2 倍审计与流转节点。
2026-09-22 13:13:54 +08:00
0f929468a8 fix(track): track-lookup 按公司路由,修复 LICA 扫码误报"物料不存在"
lookup_product() 此前写死读全局 TRACK_API_URL(指向 IRIS)。LICA 员工在
LICA 业务里扫码 → 请求打到 IRIS 库 → 查不到 → MOM 报"物料不存在"。

改用 get_current_company_filter() + resolve_track_route(…, 'api') 选实例,
复用既有工具而非另写一套。无请求/JWT 上下文时(脚本、后台任务)包一层
异常保护回落默认实例,不阻断查询。

实测(身份证 0000000000000001 只存在于 LICA 库):
  LICA 员工 → company_filter='LICA' → 命中 sn='0000000000000001'
  IRIS 员工 → company_filter='IRIS' → 未命中
改造前两者都会打到 IRIS,LICA 员工必然查不到。
2026-09-22 13:13:53 +08:00
b85be2dfdf feat(track): 新增 resolve_track_route,按公司解析 Track 实例地址
resolve_track_route(company_name, channel) 支持 api / inbound / outbound
三个通道。未命中一律回落到扁平变量,**绝不返回空**——漏配一个 key 就
静默断链,比配置写错更难排查。

非空但不在路由表内的值(含 get_current_company_filter() 的
'__NO_COMPANY__' 哨兵、拼写异常的公司名)会打 WARNING 暴露出来,但不
阻断业务——这类值属于主数据问题,交给业务确认,代码不猜。

notify_track() 相应扩展:新增 channel 参数(不传则按 payload['event']
推断 inbound/outbound),url 仍为最高优先级(显式指定时不做公司路由)。

实测各分支:IRIS/LICA 各通道命中正确;None/空串/未知公司/缺通道/空路由表
全部按预期回落。
2026-09-22 13:13:53 +08:00
de34a939d0 feat(track): 新增 TRACK_ROUTES 多实例路由表配置
Track 现在有两个独立实例(IRIS / LICA),MOM 此前所有 Track 配置都写死
指向 IRIS,LICA 的业务拿不到入库/出库联动,且 /track-lookup 对 LICA
员工是坏的。

新增 TRACK_ROUTES 环境变量(JSON),按 material_base.company_name 决定
业务落到哪个实例:
  {"IRIS": {"api":..., "inbound":..., "outbound":...}, "LICA": {...}}

原有三个扁平变量(TRACK_API_URL / TRACK_WEBHOOK_URL /
TRACK_OUTBOUND_WEBHOOK_URL)保留为「未命中路由表」时的回落默认值;
TRACK_ROUTES 缺省为空表时,行为与改造前逐字节一致(向后兼容)。

⚠️ JSON 解析失败直接抛异常让进程起不来(fail fast)——路由表写坏却静默
回落,会造成"某些公司悄悄断链",比启动报错难排查得多。

docker-compose.yml 用容器名互访(inventory_api / track_backend /
lica_backend 三个容器同属 projects_default 网络,不走宿主机端口)。

判定维度用 company_name 而非 category:实测 category 两个方向都会错
(10 条 LICA 物料挂在 IRIS/ 分类下;IRIS 分类里含 "IRIS/成品/LICA/..."
那是产品线名不是部门名,共 171 条)。
2026-09-22 13:13:52 +08:00
10 changed files with 156 additions and 27 deletions

View File

@ -41,6 +41,16 @@ services:
TRACK_API_URL: ${TRACK_API_URL:-} TRACK_API_URL: ${TRACK_API_URL:-}
# Track 出库 webhook(发货出库时通知 Track 标记"已出库") # Track 出库 webhook(发货出库时通知 Track 标记"已出库")
TRACK_OUTBOUND_WEBHOOK_URL: ${TRACK_OUTBOUND_WEBHOOK_URL:-} TRACK_OUTBOUND_WEBHOOK_URL: ${TRACK_OUTBOUND_WEBHOOK_URL:-}
# ★ 多 Track 实例路由表:按 material_base.company_name 决定走哪个实例。
# 上面三个扁平变量保留为「未命中路由表」时的回落默认值(向后兼容)。
# ★ 用容器名互访(同属 projects_default 网络),不走宿主机端口。
TRACK_ROUTES: >-
{"IRIS":{"api":"http://track_backend:8000",
"inbound":"http://track_backend:8000/api/v1/external/webhooks/mom-inbound",
"outbound":"http://track_backend:8000/api/v1/external/webhooks/mom-outbound"},
"LICA":{"api":"http://lica_backend:8000",
"inbound":"http://lica_backend:8000/api/v1/external/webhooks/mom-inbound",
"outbound":"http://lica_backend:8000/api/v1/external/webhooks/mom-outbound"}}
depends_on: depends_on:
- db - db

View File

@ -4,6 +4,7 @@ from app.extensions import db
from app.models.inbound.buy import StockBuy from app.models.inbound.buy import StockBuy
from app.models.inbound.product import StockProduct from app.models.inbound.product import StockProduct
from app.models.base import MaterialBase from app.models.base import MaterialBase
from app.models.purchase import PurchaseRequest
from datetime import datetime, timedelta, timezone from datetime import datetime, timedelta, timezone
from sqlalchemy import or_, func, text, and_ from sqlalchemy import or_, func, text, and_
from sqlalchemy.exc import IntegrityError from sqlalchemy.exc import IntegrityError
@ -153,9 +154,27 @@ class BuyInboundService:
in_qty = float(data.get('in_quantity') or 0) in_qty = float(data.get('in_quantity') or 0)
u_price = float(data.get('unit_price') or 0) u_price = float(data.get('unit_price') or 0)
tax_rate = float(data.get('tax_rate') or 0) tax_rate = float(data.get('tax_rate') or 0)
# 计算税后单价
post_tax_price = float(data.get('post_tax_unit_price') 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: if post_tax_price == 0 and u_price > 0:
tax_multiplier = 1 + (tax_rate / 100) tax_multiplier = 1 + (tax_rate / 100)
post_tax_price = u_price * tax_multiplier 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') 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 final_barcode = data.get('barcode') or generated_sku
# [新增] 按单入库:关联采购申请单
request_id = data.get('request_id')
new_stock = StockBuy( new_stock = StockBuy(
base_id=material.id, global_print_id=next_global_id, sku=generated_sku, barcode=final_barcode, 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'), 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 db.session.flush() # 获取 new_stock.id
# [新增] 按单入库:反写采购申请单状态为"已完成" # [新增] 按单入库:反写采购申请单状态为"已完成"
# 注:原先此处有一句局部 `from app.models.purchase import PurchaseRequest`,
# 它会让 Python 把 PurchaseRequest 视为本函数的局部变量,导致上方补价
# 逻辑(在 import 之前使用)抛 UnboundLocalError。已改用文件顶部的全局导入。
if request_id: if request_id:
from app.models.purchase import PurchaseRequest
purchase_req = db.session.get(PurchaseRequest, request_id) purchase_req = db.session.get(PurchaseRequest, request_id)
if purchase_req: if purchase_req:
if purchase_req.status != 1: if purchase_req.status != 1:

View File

@ -226,6 +226,8 @@ class ProductInboundService:
'sku': material.spec_model or material.name, 'sku': material.spec_model or material.name,
'serial_number': new_stock.serial_number, 'serial_number': new_stock.serial_number,
'quantity': float(new_stock.in_quantity or 0), 'quantity': float(new_stock.in_quantity or 0),
# ★ 公司名决定通知哪个 Track 实例(IRIS / LICA),缺失则回落默认
'company_name': getattr(material, 'company_name', None),
'operator': get_current_operator(), 'operator': get_current_operator(),
}) })
return new_stock return new_stock

View File

@ -265,6 +265,8 @@ class SemiInboundService:
'sku': material.spec_model or material.name, 'sku': material.spec_model or material.name,
'serial_number': webhook_sn, 'serial_number': webhook_sn,
'quantity': float(new_stock.in_quantity or 0), 'quantity': float(new_stock.in_quantity or 0),
# ★ 公司名决定通知哪个 Track 实例(IRIS / LICA),缺失则回落默认
'company_name': getattr(material, 'company_name', None),
'operator': get_current_operator(), 'operator': get_current_operator(),
}) })
return new_stock return new_stock

View File

@ -359,7 +359,11 @@ class OutboundService:
repair.shipping_date = current_time repair.shipping_date = current_time
# 收集 Track 联动信息(维修单带 serial_number) # 收集 Track 联动信息(维修单带 serial_number)
track_notifications.append((getattr(repair, 'serial_number', None), source_table, quantity)) # ★ 连同 company_name 一起攒起来,发送时按公司路由到对应 Track 实例
track_notifications.append((
getattr(repair, 'serial_number', None), source_table, quantity,
getattr(getattr(repair, 'base', None), 'company_name', None),
))
# 创建出库记录 # 创建出库记录
new_record = TransOutbound( new_record = TransOutbound(
@ -394,7 +398,11 @@ class OutboundService:
raise ValueError(f"库存记录不存在 (ID: {stock_id})") raise ValueError(f"库存记录不存在 (ID: {stock_id})")
# 收集 Track 联动信息(库存表 serial_number = Track 身份证) # 收集 Track 联动信息(库存表 serial_number = Track 身份证)
track_notifications.append((getattr(stock_record, 'serial_number', None), source_table, quantity)) # ★ 连同 company_name 一起攒起来,发送时按公司路由到对应 Track 实例
track_notifications.append((
getattr(stock_record, 'serial_number', None), source_table, quantity,
getattr(getattr(stock_record, 'base', None), 'company_name', None),
))
new_record = TransOutbound( new_record = TransOutbound(
sku=item.get('sku'), sku=item.get('sku'),
@ -419,11 +427,11 @@ class OutboundService:
db.session.commit() db.session.commit()
# ★ 出库后通知 Track(发货出库 → Track 标记"已出库") # ★ 出库后通知 Track(发货出库 → Track 标记"已出库")
# 仅在配置 TRACK_OUTBOUND_WEBHOOK_URL 时生效;notify_track 自身容错不阻断业务 # 按每条的 company_name 路由到对应实例(IRIS / LICA)。
# 这里刻意**不**显式传 url:显式 url 优先级最高,会一律打到默认实例,
# 导致 LICA 的出库永远收不到联动。notify_track 自身容错,不阻断业务。
try: try:
from flask import current_app for sn, src_tbl, qty, company in track_notifications:
outbound_url = current_app.config.get('TRACK_OUTBOUND_WEBHOOK_URL')
for sn, src_tbl, qty in track_notifications:
if not sn: if not sn:
continue continue
notify_track({ notify_track({
@ -432,8 +440,9 @@ class OutboundService:
'serial_number': str(sn), 'serial_number': str(sn),
'quantity': float(qty or 0), 'quantity': float(qty or 0),
'outbound_type': common_data['outbound_type'], 'outbound_type': common_data['outbound_type'],
'company_name': company,
'operator': get_current_operator(), 'operator': get_current_operator(),
}, url=outbound_url) })
except Exception as e: except Exception as e:
import logging import logging
logging.getLogger(__name__).warning(f"⚠️ Track 出库通知失败: {e}") logging.getLogger(__name__).warning(f"⚠️ Track 出库通知失败: {e}")

View File

@ -29,15 +29,28 @@ def lookup_product(code):
sku/material_name/spec_model/material_type/order_no),未命中或异常返回 None。 sku/material_name/spec_model/material_type/order_no),未命中或异常返回 None。
""" """
try: try:
base_url = (current_app.config.get('TRACK_API_URL') or '').strip()
api_key = (current_app.config.get('TRACK_WEBHOOK_KEY') or '').strip() api_key = (current_app.config.get('TRACK_WEBHOOK_KEY') or '').strip()
if not base_url:
logger.info("[TrackQuery] 未配置 TRACK_API_URL,跳过查询")
return None
if not api_key: if not api_key:
logger.info("[TrackQuery] 未配置 TRACK_WEBHOOK_KEY,跳过查询") logger.info("[TrackQuery] 未配置 TRACK_WEBHOOK_KEY,跳过查询")
return None return None
# ★ 按当前用户所属公司路由到对应的 Track 实例(IRIS / LICA)。
# 复用 get_current_company_filter():超管/跨域用户返回 None → 回落默认实例。
# 改造前这里写死读 TRACK_API_URL,LICA 员工在 LICA 业务里扫码会打到 IRIS 库,
# 查不到 → MOM 误报"物料不存在"。
from app.services.track_webhook_service import resolve_track_route
from app.utils.decorators import get_current_company_filter
try:
company = get_current_company_filter()
except Exception:
# 无请求/JWT 上下文(脚本、后台任务)时回落默认实例,不阻断查询
company = None
base_url = resolve_track_route(company, 'api')
if not base_url:
logger.info("[TrackQuery] 未解析到 Track API 地址,跳过查询")
return None
url = f'{base_url}/api/v1/external/products/lookup' url = f'{base_url}/api/v1/external/products/lookup'
headers = { headers = {
'Content-Type': 'application/json', 'Content-Type': 'application/json',

View File

@ -37,6 +37,54 @@ def _post_to_track(payload, url, api_key):
logger.error("[TrackWebhook] 发送失败 url=%s, err=%s", url, e) logger.error("[TrackWebhook] 发送失败 url=%s, err=%s", url, e)
# 各通道未命中路由表时回落到哪个扁平配置变量(保持改造前的单实例行为)
_TRACK_ROUTE_FALLBACK = {
'api': 'TRACK_API_URL',
'inbound': 'TRACK_WEBHOOK_URL',
'outbound': 'TRACK_OUTBOUND_WEBHOOK_URL',
}
def resolve_track_route(company_name, channel):
"""按公司名解析 Track 实例地址。
channel: 'api' | 'inbound' | 'outbound'
⚠️ 未命中必须**回落到扁平变量**,不能返回空 —— 漏配一个 key 就静默断链,
比配置写错更难排查。
"""
routes = current_app.config.get('TRACK_ROUTES') or {}
if company_name and company_name in routes:
url = ((routes.get(company_name) or {}).get(channel) or '').strip()
if url:
return url
# 公司在表里但该通道没配 —— 这是配置漏项,务必暴露出来
logger.warning(
"[TrackRoute] 公司 %s 的 %s 通道未配置,回落到全局默认地址",
company_name, channel,
)
elif company_name:
# 非空但不在路由表内:可能是拼写异常的公司名,也可能是
# get_current_company_filter() 的 '__NO_COMPANY__' 哨兵(用户 JWT 无公司)。
# 不猜、不阻断,只暴露给业务确认。
logger.warning(
"[TrackRoute] 公司 '%s' 不在 TRACK_ROUTES 中,回落到全局默认地址(channel=%s)。"
"若该值不是合法部门名,请业务确认主数据",
company_name, channel,
)
fallback_key = _TRACK_ROUTE_FALLBACK.get(channel)
if not fallback_key:
return ''
return (current_app.config.get(fallback_key) or '').strip()
def _channel_from_event(event):
"""从事件名推断路由通道;缺省按入库处理。"""
return 'outbound' if 'outbound' in (event or '').lower() else 'inbound'
def get_current_operator(): def get_current_operator():
"""从 JWT 中安全获取当前操作人姓名(失败返回空字符串,不抛异常)""" """从 JWT 中安全获取当前操作人姓名(失败返回空字符串,不抛异常)"""
try: try:
@ -50,7 +98,7 @@ def get_current_operator():
return '' return ''
def notify_track(payload, url=None): def notify_track(payload, url=None, channel=None):
""" """
异步通知 Track 系统业务事件(入库/出库等)。 异步通知 Track 系统业务事件(入库/出库等)。
@ -58,10 +106,15 @@ def notify_track(payload, url=None):
- 未配置 URL / Key 为空 -> 静默跳过 - 未配置 URL / Key 为空 -> 静默跳过
- 网络异常 / 超时 -> 仅记录日志 - 网络异常 / 超时 -> 仅记录日志
:param url: 指定 Track webhook 地址;不传则用默认 TRACK_WEBHOOK_URL(入库) :param url: 显式指定 Track webhook 地址,优先级最高(指定后不再做公司路由)
:param channel: 路由通道 'inbound' | 'outbound';不传则按 payload['event'] 推断。
仅在不传 url 时生效——实例由 payload['company_name'] 决定。
""" """
try: try:
url = (url or current_app.config.get('TRACK_WEBHOOK_URL') or '').strip() if not url:
channel = channel or _channel_from_event(payload.get('event'))
url = resolve_track_route(payload.get('company_name'), channel)
url = (url or '').strip()
api_key = (current_app.config.get('TRACK_WEBHOOK_KEY') or '').strip() api_key = (current_app.config.get('TRACK_WEBHOOK_KEY') or '').strip()
if not url: if not url:
logger.info("[TrackWebhook] 未配置 webhook URL,跳过通知") logger.info("[TrackWebhook] 未配置 webhook URL,跳过通知")

View File

@ -1,3 +1,4 @@
import json
import os import os
from datetime import timedelta from datetime import timedelta
@ -85,3 +86,17 @@ class Config:
TRACK_API_URL = os.getenv('TRACK_API_URL', 'http://track_backend:8000') TRACK_API_URL = os.getenv('TRACK_API_URL', 'http://track_backend:8000')
# Track 出库 webhook 接收地址(发货出库时通知 Track 标记"已出库") # Track 出库 webhook 接收地址(发货出库时通知 Track 标记"已出库")
TRACK_OUTBOUND_WEBHOOK_URL = os.getenv('TRACK_OUTBOUND_WEBHOOK_URL', '') TRACK_OUTBOUND_WEBHOOK_URL = os.getenv('TRACK_OUTBOUND_WEBHOOK_URL', '')
# =========================================================
# 9. 多 Track 实例路由表 (IRIS / LICA)
# =========================================================
# JSON 结构: {"IRIS": {"api": "...", "inbound": "...", "outbound": "..."}, "LICA": {...}}
# 由 material_base.company_name 决定业务落到哪个 Track 实例。
# 缺省为空字典 → 全部回落到上面第 7/8 节的扁平变量,行为与改造前一致。
#
# ⚠️ 解析失败直接让进程起不来(fail fast)——路由表写坏却静默回落,
# 会造成"某些公司悄悄断链",比启动报错难排查得多。
try:
TRACK_ROUTES = json.loads(os.getenv('TRACK_ROUTES', '{}') or '{}')
except ValueError as e:
raise RuntimeError(f'TRACK_ROUTES 不是合法 JSON,请检查环境变量: {e}')

View File

@ -239,7 +239,7 @@ const handleLogout = () => {
<footer v-if="!isLoginPage" class="app-footer"> <footer v-if="!isLoginPage" class="app-footer">
<span class="version-tag"> <span class="version-tag">
<el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon> <el-icon style="vertical-align: middle; margin-right: 4px"><InfoFilled /></el-icon>
当前版本:V3.90 当前版本:V3.91
</span> </span>
</footer> </footer>

View File

@ -1943,14 +1943,21 @@ const confirmPurchaseImport = () => {
form.tax_rate = Number(po.tax_rate) form.tax_rate = Number(po.tax_rate)
} }
if (hasTotalPrice) { // 导入新单前清掉上一单残留的"手工总价"标记,否则 updatePrices 末尾会用旧值回头覆盖
// 优先: 含税总价 → 调用 post_total 逻辑反算单价 postTaxTotalManuallySet.value = false
postTaxTotalInput.value = Number(po.total_price) postTaxTotalInput.value = undefined
onPostTaxTotalChange(postTaxTotalInput.value)
} else if (hasUnitPrice) { if (hasUnitPrice) {
// 备选: 含税单价 → 反算不含税 // ★ 单价优先。申请单的 total_price 是「申请数量」的钱,而实际入库数量未必相同
// —— 厂家怕出问题多发(申请 100 个、实发 104 个)是常态。用总价去除以实际
// 入库数量会把单价摊薄或抬高,必须一律以单价为准,总价由「单价 × 实际入库数量」得出。
// 含税单价 → 反算不含税
form.post_tax_unit_price = Number(po.unit_price) form.post_tax_unit_price = Number(po.unit_price)
updatePrices('post') updatePrices('post')
} else if (hasTotalPrice) {
// 兜底:申请单只有总价、没有单价时,才按总价反算
postTaxTotalInput.value = Number(po.total_price)
onPostTaxTotalChange(postTaxTotalInput.value)
} }
if (po.supplier_link) { if (po.supplier_link) {
form.detail_link = po.supplier_link form.detail_link = po.supplier_link