Files
track/frontend/src/services/productApi.ts
duxingchen 5d5aea1015 refactor(outbound): 出库单据与领用物料合并成一张表
这两者本来就是同一件事(这台设备对应 MOM 的哪些出库单、领了哪些料),
却因为粒度不同被拆成两张表、界面上两张卡:用户要面对两个入口两个删除按钮,
还会问「我在那边挂的怎么这边看不见」。更糟的是**单据级那张没有 mom_line_id,
挂上去的料根本报不了废**。

- 新建 product_outbound_materials,统一到**明细级**(只有它带 mom_line_id,
  而报废要用它定位)。单据级信息(申请单号/备注/撤回)作为冗余列落在每条明细上。
  task_id 改为可空 —— 任务只是溯源信息,不再是组织维度,展示/报废/删除按设备走。
- 接口从 7 个收敛成 3 个(GET/POST/DELETE /products/{id}/outbound-materials,
  外加整单删 by-order)。任务级那套连同 TaskResponse.outbound_materials 一起删掉:
  保留第二个入口只会让「同一个东西两个地方」重新长出来。
- MOM 回调存档改为按 outbound_no 去 MOM **现查明细**逐行落 —— 不查的话
  这台设备「领了什么料」永远是空的,也就报不了废。查不到时退化成单据级存档,
  宁可显示「有这张单但看不到明细」,也不要静默丢掉这张单。
- 扫码响应补 outbound_materials(附「谁挂上去的」中文名,服务端解析)。
  ⚠️ 依赖 task_tree_loader 的 selectinload —— 异步 session 下懒加载会
  MissingGreenlet。
- 前端两张卡合并成一张:按出库单号分组、点开看明细,明细行才有报废/删除。
2026-09-23 15:18:06 +08:00

126 lines
4.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import api from "./api";
import type {
ProductOutboundMaterial,
ProductScanResponse,
ProductScrap,
} from "../types/api";
/** 扫码查询 — 根据 16 位产品身份证查产品 + 顶层任务 */
export async function scanProduct(serialNumber: string): Promise<ProductScanResponse> {
const { data } = await api.get<ProductScanResponse>(
`/products/scan/${encodeURIComponent(serialNumber)}`
);
return data;
}
// ============================================================
// 设备的 MOM 出库明细(统一后只有这一组)
//
// 原先这里是两组接口,对应两张表:
// · outbound-orders —— 产品 ↔ 出库**单**(单据级 product_outbounds
// · materials —— 任务 ↔ 出库**明细**(明细级 task_outbound_materials
// 两者是同一个概念,却因为粒度不同被拆开:用户要面对两个入口两张卡,
// 而且走单据级挂的料**没有明细行 id报不了废**。
// 现已合并成一张表、一组接口、界面上只有一张卡。
// ============================================================
/**
* 列出该设备挂载的全部 MOM 出库明细(按出库时间倒序)。
* 对应后端 GET /api/v1/products/{productId}/outbound-materials
*
* 一行 = 一条出库明细。`mom_line_id` 为空表示这条是**单据级存档**
* MOM 回调时查不到明细),能看、能标撤回,但**不能报废**。
*/
export async function getProductOutboundMaterials(
productId: string,
): Promise<ProductOutboundMaterial[]> {
const { data } = await api.get<ProductOutboundMaterial[]>(
`/products/${productId}/outbound-materials`,
);
return data;
}
/**
* 给设备挂载 MOM 出库明细(网页端/移动端的「+ 领料」都走这里)。
* 对应后端 POST /api/v1/products/{productId}/outbound-materials
*
* ⚠️ 只传 `mom_line_ids`,物料快照由后端现查 MOM —— 前端不传快照。
* ⚠️ `taskId` 可空,仅作溯源(这条料挂在哪条任务上),不参与展示/报废/删除。
* 幂等:已挂过的明细会被后端跳过。返回该设备当前**全部**出库明细。
*/
export async function mountProductOutboundMaterials(
productId: string,
momLineIds: number[],
taskId?: string | null,
): Promise<ProductOutboundMaterial[]> {
const { data } = await api.post<ProductOutboundMaterial[]>(
`/products/${productId}/outbound-materials`,
{ mom_line_ids: momLineIds, task_id: taskId || null },
);
return data;
}
/**
* 摘掉一条**人工挂载**的出库明细(挂错了要能撤)。
* 对应后端 DELETE /api/v1/products/{productId}/outbound-materials/{materialId}
*
* ⚠️ 只能删 `source='manual'` 的。MOM 回调自动存档的行后端返回 409 ——
* 那是系统事实,要撤得去 MOM 撤回。返回该设备**剩余**的全部出库明细。
*/
export async function removeProductOutboundMaterial(
productId: string,
materialId: number,
): Promise<ProductOutboundMaterial[]> {
const { data } = await api.delete<ProductOutboundMaterial[]>(
`/products/${productId}/outbound-materials/${materialId}`,
);
return data;
}
/**
* 整张出库单一起摘掉(挂错了要能撤)。
* 对应后端 DELETE /api/v1/products/{productId}/outbound-materials/by-order/{outboundNo}
*
* 界面上更容易碰到的是「这一单整个挂错了」,逐条删要删好几下。
* 规则与逐条删一致:含 webhook 存档记录的单整单删不掉(后端 409
*/
export async function removeProductOutboundOrder(
productId: string,
outboundNo: string,
): Promise<ProductOutboundMaterial[]> {
const { data } = await api.delete<ProductOutboundMaterial[]>(
`/products/${productId}/outbound-materials/by-order/${encodeURIComponent(outboundNo)}`,
);
return data;
}
// ============================================================
// 生产报废
// ============================================================
/**
* 列出该设备的报废记录(含实时回查 MOM 的状态与金额)。
* 对应后端 GET /api/v1/products/{productId}/scraps
*/
export async function listProductScraps(productId: string): Promise<ProductScrap[]> {
const { data } = await api.get<ProductScrap[]>(`/products/${productId}/scraps`);
return data;
}
/**
* 提交一条生产报废(领用的料在生产中损坏)。
* 对应后端 POST /api/v1/products/{productId}/scraps
*
* ⚠️ `track_ref` 必须在**打开弹窗时生成一次**并在重试时复用 ——
* 每次提交都换新的话,用户重试会在 MOM 里多报一张报废单。
*/
export async function submitProductScrap(
productId: string,
payload: { mom_line_id: number; quantity: number; track_ref: string; reason?: string | null },
): Promise<ProductScrap> {
const { data } = await api.post<ProductScrap>(
`/products/${productId}/scraps`, payload,
);
return data;
}