feat(inventory): 매입 입고에서 음수 수량 허용 — 재고 차감 가능
- 입력 input의 min={1} 제거, 0만 막고 음수는 허용
- 입고 라인 표시: 양수 +N(에메랄드) / 음수 -N(로즈) 색상 구분
- API: ln.qty<=0 → ln.qty===0 만 skip. move_type 을 qty 부호로 IN/OUT 분기
(qty 컬럼은 부호 그대로 — 기존 stock_moves 컨벤션 일치)
- 음수 라인은 cost_price 미갱신
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -78,6 +78,10 @@ export default function InventoryPage() {
|
||||
|
||||
const addLine = () => {
|
||||
if (!pickItem) return;
|
||||
if (!pickQty || pickQty === 0) {
|
||||
Swal.fire({ icon: "warning", title: "수량을 입력하세요.", text: "양수=입고, 음수=차감" });
|
||||
return;
|
||||
}
|
||||
const it = items.find((x) => x.OBJID === pickItem);
|
||||
if (!it) return;
|
||||
setLines([...lines, { itemObjid: it.OBJID, itemName: it.ITEM_NAME, qty: pickQty }]);
|
||||
@@ -374,7 +378,14 @@ export default function InventoryPage() {
|
||||
placeholder="품목 선택"
|
||||
/>
|
||||
</div>
|
||||
<input type="number" min={1} value={pickQty} onChange={(e) => setPickQty(Number(e.target.value))} className="h-10 px-3 rounded-lg border border-slate-200" />
|
||||
<input
|
||||
type="number"
|
||||
step={1}
|
||||
value={pickQty}
|
||||
onChange={(e) => setPickQty(Number(e.target.value))}
|
||||
className="h-10 px-3 rounded-lg border border-slate-200"
|
||||
title="양수=입고, 음수=차감"
|
||||
/>
|
||||
<button onClick={addLine} className="h-10 px-4 rounded-lg bg-slate-800 text-white text-sm font-semibold">추가</button>
|
||||
</div>
|
||||
<div className="border border-slate-200 rounded-lg max-h-60 overflow-y-auto">
|
||||
@@ -389,7 +400,9 @@ export default function InventoryPage() {
|
||||
{lines.map((ln, i) => (
|
||||
<tr key={i} className="border-t border-slate-100">
|
||||
<td className="px-3 py-2">{ln.itemName}</td>
|
||||
<td className="px-3 py-2 text-right">{ln.qty}</td>
|
||||
<td className={`px-3 py-2 text-right font-semibold tabular-nums ${ln.qty < 0 ? "text-rose-600" : "text-emerald-700"}`}>
|
||||
{ln.qty > 0 ? `+${ln.qty}` : ln.qty}
|
||||
</td>
|
||||
<td className="px-3 py-2 text-right">
|
||||
<button onClick={() => setLines(lines.filter((_, j) => j !== i))} className="text-slate-400 hover:text-rose-500"><Trash2 size={12} /></button>
|
||||
</td>
|
||||
|
||||
@@ -24,9 +24,10 @@ export async function POST(req: NextRequest) {
|
||||
try {
|
||||
await client.query("BEGIN");
|
||||
for (const ln of lines) {
|
||||
if (!ln.itemObjid || !ln.qty || ln.qty <= 0) continue;
|
||||
// 0 만 skip — 음수는 차감(재고 조정 출고)로 허용
|
||||
if (!ln.itemObjid || !ln.qty || ln.qty === 0) continue;
|
||||
|
||||
// upsert stock
|
||||
// upsert stock — 음수면 자연스럽게 차감 (qty + EXCLUDED.qty)
|
||||
await client.query(
|
||||
`INSERT INTO momo_stocks (objid, wh_objid, item_objid, qty, update_date)
|
||||
VALUES ($1, $2, $3, $4, NOW())
|
||||
@@ -35,15 +36,16 @@ export async function POST(req: NextRequest) {
|
||||
[createObjectId(), whObjid, ln.itemObjid, ln.qty]
|
||||
);
|
||||
|
||||
// log
|
||||
// log — 부호로 IN/OUT 분기. qty 컬럼은 부호 그대로(OUT 은 음수, IN 은 양수)
|
||||
const moveType = ln.qty > 0 ? "IN" : "OUT";
|
||||
await client.query(
|
||||
`INSERT INTO momo_stock_moves (objid, wh_objid, item_objid, move_type, qty, ref_type, memo, regdate, regid)
|
||||
VALUES ($1, $2, $3, 'IN', $4, 'PROCUREMENT', $5, NOW(), $6)`,
|
||||
[createObjectId(), whObjid, ln.itemObjid, ln.qty, memo ?? null, userId]
|
||||
VALUES ($1, $2, $3, $4, $5, 'PROCUREMENT', $6, NOW(), $7)`,
|
||||
[createObjectId(), whObjid, ln.itemObjid, moveType, ln.qty, memo ?? null, userId]
|
||||
);
|
||||
|
||||
// 매입가가 들어오면 items.cost_price 도 갱신
|
||||
if (ln.costPrice && ln.costPrice > 0) {
|
||||
// 매입가가 들어오면 items.cost_price 도 갱신 (입고 시에만)
|
||||
if (ln.qty > 0 && ln.costPrice && ln.costPrice > 0) {
|
||||
await client.query(`UPDATE momo_items SET cost_price = $2 WHERE objid = $1`, [ln.itemObjid, ln.costPrice]);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user