|
|
|
@@ -27,7 +27,6 @@ import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs"; |
|
|
|
import dayjs, { type Dayjs } from "dayjs"; |
|
|
|
import "dayjs/locale/zh-hk"; |
|
|
|
import { |
|
|
|
fetchStockLedgerFixAdjPreview, |
|
|
|
runStockLedgerFixAdj, |
|
|
|
fetchStockLedgerFixDay, |
|
|
|
fetchStockLedgerFixInventory, |
|
|
|
@@ -41,7 +40,6 @@ import { |
|
|
|
searchStockLedgerFixInventory, |
|
|
|
searchStockLedgerFixLot, |
|
|
|
downloadStockLedgerFixSql, |
|
|
|
type StockLedgerFixAdjPreview, |
|
|
|
type StockLedgerFixCheckPart, |
|
|
|
type StockLedgerFixDayDetail, |
|
|
|
type StockLedgerFixInventoryPreview, |
|
|
|
@@ -291,10 +289,7 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
const [inventoryRunning, setInventoryRunning] = useState(false); |
|
|
|
const [inventoryError, setInventoryError] = useState<string | null>(null); |
|
|
|
const [inventoryMessage, setInventoryMessage] = useState<string | null>(null); |
|
|
|
const adjLoadInFlight = useRef(false); |
|
|
|
const adjRunInFlight = useRef(false); |
|
|
|
const [adjPreview, setAdjPreview] = useState<StockLedgerFixAdjPreview | null>(null); |
|
|
|
const [adjLoading, setAdjLoading] = useState(false); |
|
|
|
const [adjRunning, setAdjRunning] = useState(false); |
|
|
|
const [adjError, setAdjError] = useState<string | null>(null); |
|
|
|
const [adjMessage, setAdjMessage] = useState<string | null>(null); |
|
|
|
@@ -348,32 +343,6 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
} |
|
|
|
}, []); |
|
|
|
|
|
|
|
const loadAdjPreview = useCallback(async () => { |
|
|
|
if (adjLoadInFlight.current) return; |
|
|
|
const d = adjDate.trim(); |
|
|
|
if (!d) { |
|
|
|
setAdjError("請填 ADJ 日期"); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (d > dayjs().format("YYYY-MM-DD")) { |
|
|
|
setAdjError("ADJ 日期不能是未來"); |
|
|
|
return; |
|
|
|
} |
|
|
|
adjLoadInFlight.current = true; |
|
|
|
setAdjLoading(true); |
|
|
|
setAdjError(null); |
|
|
|
try { |
|
|
|
setAdjPreview(await fetchStockLedgerFixAdjPreview(d)); |
|
|
|
} catch (e) { |
|
|
|
console.error(e); |
|
|
|
setAdjError(apiErrorMessage(e, "無法載入 ADJ 預覽(需要 ADMIN / TESTING)")); |
|
|
|
setAdjPreview(null); |
|
|
|
} finally { |
|
|
|
setAdjLoading(false); |
|
|
|
adjLoadInFlight.current = false; |
|
|
|
} |
|
|
|
}, [adjDate]); |
|
|
|
|
|
|
|
useEffect(() => { |
|
|
|
void loadInventory(); |
|
|
|
}, [loadInventory]); |
|
|
|
@@ -450,13 +419,17 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
|
|
|
|
const onAdjApply = async () => { |
|
|
|
if (adjRunInFlight.current) return; |
|
|
|
const d = adjDate.trim() || adjPreview?.adjDate; |
|
|
|
const d = adjDate.trim(); |
|
|
|
if (!d) { |
|
|
|
setAdjError("請填 ADJ 日期並先預覽"); |
|
|
|
setAdjError("請填 ADJ 日期"); |
|
|
|
return; |
|
|
|
} |
|
|
|
if (d > dayjs().format("YYYY-MM-DD")) { |
|
|
|
setAdjError("ADJ 日期不能是未來"); |
|
|
|
return; |
|
|
|
} |
|
|
|
const ok = window.confirm( |
|
|
|
`會在 ${d} 先修超發 remain→0(${adjPreview?.overIssueCount ?? 0} 條 lot,合計 ${adjPreview?.sumOverIssue ?? "?"},會改 lot inQty)。再補 miss ADJ:入向 ${adjPreview?.adjInCount ?? 0} 筆、出向 ${adjPreview?.adjOutCount ?? 0} 筆。入向全部掛同一張入倉單、出向全部掛同一張出倉單(miss 不改 lot 數量)。缺入合計 ${adjPreview?.sumMissIn ?? "?"}、缺出合計 ${adjPreview?.sumMissOut ?? "?"}。確定?`, |
|
|
|
`會在 ${d} 對最後餘額不等於 onHandQty 的庫存桶各寫一筆 ADJ(入或出),掛在既有 lot 上。不改 lot line、不改 inventory。已對齊的桶不會再寫。確定?`, |
|
|
|
); |
|
|
|
if (!ok) return; |
|
|
|
adjRunInFlight.current = true; |
|
|
|
@@ -466,9 +439,8 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
try { |
|
|
|
const res = await runStockLedgerFixAdj(d); |
|
|
|
setAdjMessage( |
|
|
|
`ADJ ${res.adjDate}:超發 remain→0 ${res.overIssuePatched}、入 ${res.insertedIn}、出 ${res.insertedOut}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}`, |
|
|
|
`ADJ ${res.adjDate}:入 ${res.insertedIn}、出 ${res.insertedOut}、lotQty ${res.filledLotQty}、balance ${res.filledBalance}、日結 ${res.dayRowsWritten}`, |
|
|
|
); |
|
|
|
await loadAdjPreview(); |
|
|
|
} catch (e) { |
|
|
|
console.error(e); |
|
|
|
setAdjError(apiErrorMessage(e, "ADJ 對齊失敗")); |
|
|
|
@@ -744,13 +716,11 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
|
|
|
|
<Paper sx={{ p: 2 }}> |
|
|
|
<Stack spacing={1.5}> |
|
|
|
<Typography variant="h6">2.7 ADJ:超發 remain→0,再對齊 ledger</Typography> |
|
|
|
<Typography variant="h6">2.7 ADJ:對齊最後餘額與 onHandQty</Typography> |
|
|
|
<Typography variant="body2" color="text.secondary"> |
|
|
|
請先選 ADJ 日期再預覽。預設昨天;freeze dump 夜可選今天(例如 31/8 23:17 寫 ADJ 到 |
|
|
|
31/8)。先處理超發(line 入少於出):把該 lot 的 inQty 補到等於 outQty(remain=0,trigger |
|
|
|
重算 inventory)。再處理 miss:line 與 ledger 有差就 ADJ,掛既有 lot 的入/出倉行(不改 lot |
|
|
|
數量)。一次 Apply 共用一張入倉單、一張出倉單,下面多行;同一 ADJ 日期再跑會沿用這兩張單。 |
|
|
|
明細最多 20 列。 |
|
|
|
選 ADJ 日期後直接執行。預設昨天;freeze dump 夜可選今天。最後餘額低於 onHandQty |
|
|
|
的桶補一筆入倉 ADJ,高於 onHandQty 的桶補一筆出倉 ADJ,掛在既有 lot 上。不改 lot |
|
|
|
line、不改 inventory。一次執行共用一張入倉單、一張出倉單;已對齊的桶再跑不會再寫。 |
|
|
|
</Typography> |
|
|
|
<Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap> |
|
|
|
<TextField |
|
|
|
@@ -760,29 +730,28 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
value={adjDate} |
|
|
|
onChange={(e) => { |
|
|
|
setAdjDate(e.target.value); |
|
|
|
setAdjPreview(null); |
|
|
|
setAdjMessage(null); |
|
|
|
}} |
|
|
|
disabled={adjLoading || adjRunning} |
|
|
|
disabled={adjRunning} |
|
|
|
InputLabelProps={{ shrink: true }} |
|
|
|
inputProps={{ max: dayjs().format("YYYY-MM-DD") }} |
|
|
|
/> |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
disabled={adjLoading || adjRunning} |
|
|
|
disabled={adjRunning} |
|
|
|
onClick={() => { |
|
|
|
setAdjDate(dayjs().format("YYYY-MM-DD")); |
|
|
|
setAdjPreview(null); |
|
|
|
setAdjMessage(null); |
|
|
|
}} |
|
|
|
> |
|
|
|
今天(freeze 夜) |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
size="small" |
|
|
|
disabled={adjLoading || adjRunning} |
|
|
|
disabled={adjRunning} |
|
|
|
onClick={() => { |
|
|
|
setAdjDate(dayjs().subtract(1, "day").format("YYYY-MM-DD")); |
|
|
|
setAdjPreview(null); |
|
|
|
setAdjMessage(null); |
|
|
|
}} |
|
|
|
> |
|
|
|
昨天 |
|
|
|
@@ -790,65 +759,12 @@ const StockLedgerFixPageClient: React.FC = () => { |
|
|
|
</Stack> |
|
|
|
{adjError && <Alert severity="error">{adjError}</Alert>} |
|
|
|
{adjMessage && <Alert severity="success">{adjMessage}</Alert>} |
|
|
|
{adjLoading && !adjPreview && <CircularProgress size={24} />} |
|
|
|
{adjPreview && ( |
|
|
|
<Typography variant="body2"> |
|
|
|
ADJ 日期 {adjPreview.adjDate};可處理 {adjPreview.lotCount} 條 |
|
|
|
lot(超發 remain→0 {adjPreview.overIssueCount}、合計 {adjPreview.sumOverIssue};miss 入{" "} |
|
|
|
{adjPreview.adjInCount}、出 {adjPreview.adjOutCount});缺入合計{" "} |
|
|
|
{adjPreview.sumMissIn}、缺出合計 {adjPreview.sumMissOut};SKU 淨額{" "} |
|
|
|
{adjPreview.skuNet} |
|
|
|
{adjPreview.skuNet !== "0" ? "(非 0 表示有缺入沒有對應缺出)" : ""} |
|
|
|
{adjPreview.skippedNegCount > 0 |
|
|
|
? `;含反向 ADJ ${adjPreview.skippedNegCount} 條(ledger 多記)` |
|
|
|
: ""} |
|
|
|
。下列最多 20 列(超發優先)。 |
|
|
|
</Typography> |
|
|
|
)} |
|
|
|
{adjPreview && adjPreview.rows.length > 0 && ( |
|
|
|
<Table size="small"> |
|
|
|
<TableHead> |
|
|
|
<TableRow> |
|
|
|
<TableCell>lotLineId</TableCell> |
|
|
|
<TableCell>itemCode</TableCell> |
|
|
|
<TableCell align="right">line in/out</TableCell> |
|
|
|
<TableCell align="right">ledger in/out</TableCell> |
|
|
|
<TableCell align="right">缺入</TableCell> |
|
|
|
<TableCell align="right">缺出</TableCell> |
|
|
|
<TableCell align="right">超發</TableCell> |
|
|
|
</TableRow> |
|
|
|
</TableHead> |
|
|
|
<TableBody> |
|
|
|
{adjPreview.rows.map((r) => ( |
|
|
|
<TableRow key={r.lotLineId}> |
|
|
|
<TableCell>{r.lotLineId}</TableCell> |
|
|
|
<TableCell>{r.itemCode ?? "—"}</TableCell> |
|
|
|
<TableCell align="right"> |
|
|
|
{r.lineIn} / {r.lineOut} |
|
|
|
</TableCell> |
|
|
|
<TableCell align="right"> |
|
|
|
{r.ledgerIn} / {r.ledgerOut} |
|
|
|
</TableCell> |
|
|
|
<TableCell align="right">{r.missIn}</TableCell> |
|
|
|
<TableCell align="right">{r.missOut}</TableCell> |
|
|
|
<TableCell align="right">{r.overIssue}</TableCell> |
|
|
|
</TableRow> |
|
|
|
))} |
|
|
|
</TableBody> |
|
|
|
</Table> |
|
|
|
)} |
|
|
|
{adjRunning && <CircularProgress size={24} />} |
|
|
|
<Stack direction="row" spacing={1}> |
|
|
|
<Button |
|
|
|
variant="outlined" |
|
|
|
disabled={adjLoading || adjRunning} |
|
|
|
onClick={() => void loadAdjPreview()} |
|
|
|
> |
|
|
|
{adjLoading ? "預覽中…" : adjPreview ? "重新預覽" : "預覽"} |
|
|
|
</Button> |
|
|
|
<Button |
|
|
|
variant="contained" |
|
|
|
color="warning" |
|
|
|
disabled={adjRunning || adjLoading || !adjPreview} |
|
|
|
disabled={adjRunning} |
|
|
|
onClick={() => void onAdjApply()} |
|
|
|
> |
|
|
|
{adjRunning ? "寫入 ADJ 中…" : "Apply ADJ"} |
|
|
|
|