这两者本来就是同一件事(这台设备对应 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。
- 前端两张卡合并成一张:按出库单号分组、点开看明细,明细行才有报废/删除。
126 lines
4.8 KiB
TypeScript
126 lines
4.8 KiB
TypeScript
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;
|
||
}
|