| @@ -161,30 +161,22 @@ async function fetchInventoriesImpl(data: SearchInventory) { | |||
| ); | |||
| } | |||
| async function fetchInventoriesLatestImpl(data: SearchInventory) { | |||
| const queryStr = convertObjToURLSearchParams(data); | |||
| return serverFetchJson<InventoryResultByPage>( | |||
| `${BASE_API_URL}/inventory/searchLatest/getRecordByPage?${queryStr}`, | |||
| { next: { tags: ["inventories"] } }, | |||
| ); | |||
| } | |||
| export const fetchInventories = cache(fetchInventoriesImpl); | |||
| /** | |||
| * FP-MTMS Version Checklist | Functions Ref. No. 18 | v1.0.3 | 2026-09-07 | |||
| * Inventory search page: latest inventory row per item (no baseUnit/uomId filter). | |||
| * ChangeList #24 — same as fetchInventories (all stock-UOM buckets). | |||
| * Backend `/inventory/searchLatest/getRecordByPage` is deprecated and aliases getRecordByPage. | |||
| */ | |||
| export const fetchInventoriesLatest = cache(fetchInventoriesLatestImpl); | |||
| export const fetchInventoriesLatest = cache(fetchInventoriesImpl); | |||
| /** Bypass React cache() after mutations so lists show fresh qty. */ | |||
| export async function fetchInventoriesFresh(data: SearchInventory) { | |||
| return fetchInventoriesImpl(data); | |||
| } | |||
| /** Bypass React cache() for inventory search page latest-inventory search. */ | |||
| /** @deprecated Use fetchInventoriesFresh — same full-page search. */ | |||
| export async function fetchInventoriesLatestFresh(data: SearchInventory) { | |||
| return fetchInventoriesLatestImpl(data); | |||
| return fetchInventoriesImpl(data); | |||
| } | |||
| async function fetchInventoryLotLinesImpl(data: SearchInventoryLotLine) { | |||
| @@ -14,6 +14,7 @@ export interface InventoryResult { | |||
| onHoldQty: number; | |||
| unavailableQty: number; | |||
| availableQty: number; | |||
| stockUomId?: number | null; | |||
| uomCode: string; | |||
| uomUdfudesc: string; | |||
| uomShortDesc: string; | |||
| @@ -0,0 +1,82 @@ | |||
| /** Keys used to pick one inventory row when an item has multiple stock-UOM buckets. */ | |||
| export type InventoryBucketPick = { | |||
| itemId?: number | null; | |||
| itemCode?: string | null; | |||
| itemName?: string | null; | |||
| uomId?: number | null; | |||
| stockUomId?: number | null; | |||
| uom?: string | null; | |||
| shortUom?: string | null; | |||
| stockUom?: string | null; | |||
| }; | |||
| export type InventoryBucketRow = { | |||
| itemId?: number | null; | |||
| itemCode?: string | null; | |||
| itemName?: string | null; | |||
| stockUomId?: number | null; | |||
| uomCode?: string | null; | |||
| uomUdfudesc?: string | null; | |||
| uomShortDesc?: string | null; | |||
| availableQty?: number | null; | |||
| onHandQty?: number | null; | |||
| unavailableQty?: number | null; | |||
| }; | |||
| const norm = (s?: string | null) => (s ?? "").trim().toLowerCase(); | |||
| /** Available = onHand − unavailable. Do not treat 0 as missing (`||` is wrong). */ | |||
| export function inventoryAvailableQty(inv: InventoryBucketRow): number { | |||
| if (inv.availableQty != null && !Number.isNaN(Number(inv.availableQty))) { | |||
| return Number(inv.availableQty); | |||
| } | |||
| return Number(inv.onHandQty ?? 0) - Number(inv.unavailableQty ?? 0); | |||
| } | |||
| function itemMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean { | |||
| if (pick.itemId != null && inv.itemId != null) { | |||
| return Number(inv.itemId) === Number(pick.itemId); | |||
| } | |||
| if (pick.itemCode && inv.itemCode) { | |||
| return inv.itemCode === pick.itemCode; | |||
| } | |||
| if (pick.itemName && inv.itemName) { | |||
| return inv.itemName === pick.itemName; | |||
| } | |||
| return false; | |||
| } | |||
| function uomMatches(inv: InventoryBucketRow, pick: InventoryBucketPick): boolean { | |||
| const pickUomId = pick.stockUomId ?? pick.uomId; | |||
| if (pickUomId != null && inv.stockUomId != null) { | |||
| return Number(inv.stockUomId) === Number(pickUomId); | |||
| } | |||
| const labels = [pick.uom, pick.shortUom, pick.stockUom].map(norm).filter(Boolean); | |||
| if (labels.length === 0) return true; | |||
| const invLabels = [inv.uomUdfudesc, inv.uomShortDesc, inv.uomCode].map(norm); | |||
| return labels.some((l) => invLabels.includes(l)); | |||
| } | |||
| export function matchInventoryBucket( | |||
| inventories: InventoryBucketRow[], | |||
| pick: InventoryBucketPick, | |||
| ): InventoryBucketRow | undefined { | |||
| const itemHits = inventories.filter((inv) => itemMatches(inv, pick)); | |||
| if (itemHits.length === 0) return undefined; | |||
| const hasUomPick = | |||
| pick.stockUomId != null || | |||
| pick.uomId != null || | |||
| [pick.uom, pick.shortUom, pick.stockUom].some((s) => Boolean(norm(s))); | |||
| if (hasUomPick) { | |||
| return itemHits.find((inv) => uomMatches(inv, pick)); | |||
| } | |||
| return itemHits[0]; | |||
| } | |||
| export function getStockAvailableFromInventories( | |||
| inventories: InventoryBucketRow[], | |||
| pick: InventoryBucketPick, | |||
| ): number { | |||
| const inv = matchInventoryBucket(inventories, pick); | |||
| return inv ? inventoryAvailableQty(inv) : 0; | |||
| } | |||
| @@ -533,6 +533,7 @@ export interface JobOrderLineInfo { | |||
| stockUom: string, | |||
| stockBaseUom: string, | |||
| stockUomId?: number | null, | |||
| availableStatus: string, | |||
| bomProcessId: number, | |||
| @@ -67,6 +67,8 @@ export interface JoDetail { | |||
| export interface JoDetailPickLine { | |||
| id: number; | |||
| itemId?: number; | |||
| uomId?: number; | |||
| code: string; | |||
| name: string; | |||
| type: string; | |||
| @@ -12,7 +12,7 @@ import { | |||
| analyzeQrCode, | |||
| SearchInventory, | |||
| SearchInventoryLotLine, | |||
| fetchInventoriesLatest, | |||
| fetchInventories, | |||
| fetchInventoryLotLines, | |||
| } from '@/app/api/inventory/actions'; | |||
| import { PrinterCombo } from '@/app/api/settings/printer'; | |||
| @@ -85,6 +85,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| uomCode: item.uom || uom, | |||
| uomUdfudesc: uom, | |||
| uomShortDesc: item.uom || uom, | |||
| stockUomId: undefined, | |||
| qtyPerSmallestUnit: 1, | |||
| baseUom: uom, | |||
| price: 0, | |||
| @@ -208,7 +209,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| pageSize: pagingController.pageSize, | |||
| }; | |||
| const response = await fetchInventoriesLatest(params); | |||
| const response = await fetchInventories(params); | |||
| if (response) { | |||
| setInventoriesTotalCount(response.total); | |||
| @@ -220,7 +221,7 @@ const InventorySearch: React.FC<Props> = ({ inventories, printerCombo }) => { | |||
| break; | |||
| case 'paging': | |||
| setFilteredInventories((fi) => | |||
| uniqBy([...fi, ...response.records], 'itemId'), | |||
| uniqBy([...fi, ...response.records], 'id'), | |||
| ); | |||
| } | |||
| } | |||
| @@ -3,6 +3,7 @@ import { useTranslation } from "react-i18next"; | |||
| import { JoDetailPickLine } from "@/app/api/jo"; | |||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||
| import { useEffect, useState, useMemo } from "react"; | |||
| import { useFormContext } from "react-hook-form"; | |||
| import { JoDetail } from "@/app/api/jo"; | |||
| @@ -50,15 +51,14 @@ const JoRelease: React.FC<Props> = ({ | |||
| }, [pickLines]); | |||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||
| const inventory = inventoryData.find(inventory => | |||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return 0; | |||
| return getStockAvailableFromInventories(inventoryData, { | |||
| itemId: pickLine.itemId, | |||
| itemCode: pickLine.code, | |||
| itemName: pickLine.name, | |||
| uomId: pickLine.uomId, | |||
| uom: pickLine.uom, | |||
| shortUom: pickLine.shortUom, | |||
| }); | |||
| }; | |||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||
| @@ -12,6 +12,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli | |||
| import HelpOutlineOutlinedIcon from '@mui/icons-material/HelpOutlineOutlined'; | |||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||
| import { useEffect, useState } from "react"; | |||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | |||
| @@ -54,15 +55,14 @@ const PickTable: React.FC<Props> = ({ | |||
| }, [pickLines]); | |||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||
| const inventory = inventoryData.find(inventory => | |||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return 0; | |||
| return getStockAvailableFromInventories(inventoryData, { | |||
| itemId: pickLine.itemId, | |||
| itemCode: pickLine.code, | |||
| itemName: pickLine.name, | |||
| uomId: pickLine.uomId, | |||
| uom: pickLine.uom, | |||
| shortUom: pickLine.shortUom, | |||
| }); | |||
| }; | |||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||
| @@ -25,6 +25,7 @@ import { msg } from "../Swal/CustomAlerts"; | |||
| import dayjs from "dayjs"; | |||
| //import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| @@ -132,15 +133,14 @@ const JoSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCombo, jobT | |||
| */ | |||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||
| const inventory = inventoryData.find(inventory => | |||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return 0; | |||
| return getStockAvailableFromInventories(inventoryData, { | |||
| itemId: pickLine.itemId, | |||
| itemCode: pickLine.code, | |||
| itemName: pickLine.name, | |||
| uomId: pickLine.uomId, | |||
| uom: pickLine.uom, | |||
| shortUom: pickLine.shortUom, | |||
| }); | |||
| }; | |||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||
| @@ -26,6 +26,7 @@ import { msg } from "../Swal/CustomAlerts"; | |||
| import dayjs from "dayjs"; | |||
| //import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { getStockAvailableFromInventories } from "@/app/api/inventory/inventoryBucket"; | |||
| import { PrinterCombo } from "@/app/api/settings/printer"; | |||
| import { JobTypeResponse } from "@/app/api/jo/actions"; | |||
| import { DatePicker, LocalizationProvider } from "@mui/x-date-pickers"; | |||
| @@ -133,15 +134,14 @@ const JoWorkbenchSearch: React.FC<Props> = ({ defaultInputs, bomCombo, printerCo | |||
| */ | |||
| const getStockAvailable = (pickLine: JoDetailPickLine) => { | |||
| const inventory = inventoryData.find(inventory => | |||
| inventory.itemCode === pickLine.code || inventory.itemName === pickLine.name | |||
| ); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| } | |||
| return 0; | |||
| return getStockAvailableFromInventories(inventoryData, { | |||
| itemId: pickLine.itemId, | |||
| itemCode: pickLine.code, | |||
| itemName: pickLine.name, | |||
| uomId: pickLine.uomId, | |||
| uom: pickLine.uom, | |||
| shortUom: pickLine.shortUom, | |||
| }); | |||
| }; | |||
| const isStockSufficient = (pickLine: JoDetailPickLine) => { | |||
| @@ -36,6 +36,7 @@ import CheckCircleOutlineOutlinedIcon from '@mui/icons-material/CheckCircleOutli | |||
| import DoDisturbAltRoundedIcon from '@mui/icons-material/DoDisturbAltRounded'; | |||
| import { fetchInventories } from "@/app/api/inventory/actions"; | |||
| import { InventoryResult } from "@/app/api/inventory"; | |||
| import { matchInventoryBucket, inventoryAvailableQty } from "@/app/api/inventory/inventoryBucket"; | |||
| import { releaseJoForWorkbench } from "@/app/api/jo/workbenchActions"; | |||
| import JobPickExecutionsecondscan from "../Jodetail/JobPickExecutionsecondscan"; | |||
| import ProcessSummaryHeader from "./ProcessSummaryHeader"; | |||
| @@ -187,13 +188,17 @@ const getStockAvailable = (line: JobOrderLineInfo) => { | |||
| if (line.type?.toLowerCase() === "consumables" || line.type?.toLowerCase() === "nm") { | |||
| return line.stockQty || 0; | |||
| } | |||
| const inventory = inventoryData.find(inv => | |||
| inv.itemCode === line.itemCode || inv.itemName === line.itemName | |||
| ); | |||
| const inventory = matchInventoryBucket(inventoryData, { | |||
| itemId: line.itemId, | |||
| itemCode: line.itemCode, | |||
| itemName: line.itemName, | |||
| stockUomId: line.stockUomId, | |||
| stockUom: line.stockUom, | |||
| }); | |||
| if (inventory) { | |||
| return inventory.availableQty || (inventory.onHandQty - inventory.onHoldQty - inventory.unavailableQty); | |||
| return inventoryAvailableQty(inventory); | |||
| } | |||
| return line.stockQty || 0; | |||
| return line.stockQty ?? 0; | |||
| }; | |||
| const handleOpenPlanStartDialog = useCallback(() => { | |||
| // 将 processData.date 转换为 dayjs 对象 | |||