feat(malaysia): 콤보 출고를 콤보 재고현황에 반영
- set_movement(콤보 단위 출고 원장) 테이블 신설 + 마이그레이션 SQL - create_set_out: 낱개 OUT 과 함께 콤보 OUT 1행 기록(같은 트랜잭션) - 콤보 재고 = 최근 재고조사 콤보수량 − 재고조사일 이후 콤보 출고 - 재고현황 콤보 패널: 조사/출고/재고 3열 표시 - reset_all_data 에 set_movement 삭제 포함 배포: malaysia_stock_db_set_movement.sql 적용(테이블+GRANT) 후 코드 배포. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -269,6 +269,25 @@ class MalaysiaStockStore:
|
|||||||
),
|
),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
results.append(self._serialize(row))
|
results.append(self._serialize(row))
|
||||||
|
# 콤보(세트) 단위 출고 원장 — 콤보 재고현황 차감용.
|
||||||
|
conn.execute(
|
||||||
|
"""
|
||||||
|
INSERT INTO set_movement
|
||||||
|
(movement_date, warehouse_code, set_code, movement_type,
|
||||||
|
qty, ref_type, ref_no, memo, created_by)
|
||||||
|
VALUES (%s,%s,%s,'OUT',%s,%s,%s,%s,%s)
|
||||||
|
""",
|
||||||
|
(
|
||||||
|
movement_date.strip(),
|
||||||
|
warehouse_code.strip(),
|
||||||
|
sc,
|
||||||
|
q,
|
||||||
|
ref_t,
|
||||||
|
(ref_no or "").strip(),
|
||||||
|
(memo or f"{sc} {q}세트 출고").strip(),
|
||||||
|
(created_by or "").lower().strip(),
|
||||||
|
),
|
||||||
|
)
|
||||||
return results
|
return results
|
||||||
|
|
||||||
def list_movements(
|
def list_movements(
|
||||||
@@ -507,10 +526,15 @@ class MalaysiaStockStore:
|
|||||||
raise KeyError(stocktake_id)
|
raise KeyError(stocktake_id)
|
||||||
|
|
||||||
def latest_set_quantities(self, *, warehouse_code: str) -> dict[str, Any]:
|
def latest_set_quantities(self, *, warehouse_code: str) -> dict[str, Any]:
|
||||||
"""가장 최근(취소 제외) 재고조사에 입력된 콤보(MY-) 수량.
|
"""콤보(MY-) 단위 재고현황 — 우측 패널용.
|
||||||
|
|
||||||
반환: {"stocktake_date": str|None, "rows": [{set_code,set_name,qty}]}
|
콤보는 movement 테이블(낱개 전용)에 직접 안 들어가므로, 재고조사 입력을
|
||||||
재고현황 우측 패널용 — 세트 단위로 입력된 콤보 재고를 그대로 보여줌.
|
기준점으로 set_movement(콤보 출고 원장)를 차감해 현재 콤보 재고를 낸다.
|
||||||
|
|
||||||
|
콤보재고 = 최근(취소 제외) 재고조사 콤보수량 − 재고조사일 이후 콤보 OUT 합
|
||||||
|
|
||||||
|
반환: {"stocktake_date": str|None,
|
||||||
|
"rows": [{set_code, set_name, stocktake_qty, out_qty, qty}]}
|
||||||
"""
|
"""
|
||||||
with self._pool.connection() as conn:
|
with self._pool.connection() as conn:
|
||||||
head = conn.execute(
|
head = conn.execute(
|
||||||
@@ -527,6 +551,19 @@ class MalaysiaStockStore:
|
|||||||
"ORDER BY sku_code ASC",
|
"ORDER BY sku_code ASC",
|
||||||
(head["id"],),
|
(head["id"],),
|
||||||
).fetchall()
|
).fetchall()
|
||||||
|
# 재고조사일 이후의 콤보 출고(OUT)/조정(ADJUST) 합. 재고조사 시점
|
||||||
|
# 물리 카운트가 기준이므로, 그 이후 출고만 차감한다.
|
||||||
|
out_rows = conn.execute(
|
||||||
|
"""
|
||||||
|
SELECT set_code,
|
||||||
|
SUM(CASE WHEN movement_type = 'OUT' THEN qty ELSE -qty END) AS out_qty
|
||||||
|
FROM set_movement
|
||||||
|
WHERE warehouse_code = %s AND movement_date > %s
|
||||||
|
GROUP BY set_code
|
||||||
|
""",
|
||||||
|
(warehouse_code, head["stocktake_date"]),
|
||||||
|
).fetchall()
|
||||||
|
out_map = {r["set_code"]: int(r["out_qty"] or 0) for r in out_rows}
|
||||||
names = self.item_name_map()
|
names = self.item_name_map()
|
||||||
d = head["stocktake_date"]
|
d = head["stocktake_date"]
|
||||||
date_str = d.isoformat() if isinstance(d, date) else str(d)
|
date_str = d.isoformat() if isinstance(d, date) else str(d)
|
||||||
@@ -534,7 +571,9 @@ class MalaysiaStockStore:
|
|||||||
{
|
{
|
||||||
"set_code": r["sku_code"],
|
"set_code": r["sku_code"],
|
||||||
"set_name": names.get(r["sku_code"], r["sku_code"]),
|
"set_name": names.get(r["sku_code"], r["sku_code"]),
|
||||||
"qty": int(r["qty"]),
|
"stocktake_qty": int(r["qty"]),
|
||||||
|
"out_qty": out_map.get(r["sku_code"], 0),
|
||||||
|
"qty": int(r["qty"]) - out_map.get(r["sku_code"], 0),
|
||||||
}
|
}
|
||||||
for r in lines
|
for r in lines
|
||||||
]
|
]
|
||||||
@@ -883,6 +922,7 @@ class MalaysiaStockStore:
|
|||||||
"""전체 창고의 재고 수량 데이터 초기화.
|
"""전체 창고의 재고 수량 데이터 초기화.
|
||||||
|
|
||||||
삭제 대상: stock_movement(입고/출고/조정/조사반영) +
|
삭제 대상: stock_movement(입고/출고/조정/조사반영) +
|
||||||
|
set_movement(콤보 출고 원장) +
|
||||||
daily_stocktake/daily_stocktake_line(재고조사).
|
daily_stocktake/daily_stocktake_line(재고조사).
|
||||||
보존 대상: warehouses, malaysia_items(마스터).
|
보존 대상: warehouses, malaysia_items(마스터).
|
||||||
한 트랜잭션으로 처리. 반환: 삭제 건수.
|
한 트랜잭션으로 처리. 반환: 삭제 건수.
|
||||||
@@ -890,9 +930,10 @@ class MalaysiaStockStore:
|
|||||||
with self._pool.connection() as conn:
|
with self._pool.connection() as conn:
|
||||||
with conn.transaction():
|
with conn.transaction():
|
||||||
mv = conn.execute("DELETE FROM stock_movement").rowcount or 0
|
mv = conn.execute("DELETE FROM stock_movement").rowcount or 0
|
||||||
|
sm = conn.execute("DELETE FROM set_movement").rowcount or 0
|
||||||
ln = conn.execute("DELETE FROM daily_stocktake_line").rowcount or 0
|
ln = conn.execute("DELETE FROM daily_stocktake_line").rowcount or 0
|
||||||
hd = conn.execute("DELETE FROM daily_stocktake").rowcount or 0
|
hd = conn.execute("DELETE FROM daily_stocktake").rowcount or 0
|
||||||
return {"movements": mv, "stocktake_lines": ln, "stocktakes": hd}
|
return {"movements": mv, "set_movements": sm, "stocktake_lines": ln, "stocktakes": hd}
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 직렬화
|
# 직렬화
|
||||||
|
|||||||
@@ -59,21 +59,28 @@
|
|||||||
<div class="erp-card">
|
<div class="erp-card">
|
||||||
<div class="cpg-card-head"><h2>콤보 재고 (세트 단위)</h2></div>
|
<div class="cpg-card-head"><h2>콤보 재고 (세트 단위)</h2></div>
|
||||||
<span class="erp-muted">
|
<span class="erp-muted">
|
||||||
{% if set_stocktake_date %}최근 재고조사 {{ set_stocktake_date }} 기준 입력 수량{% else %}재고조사 입력 없음{% endif %}
|
{% if set_stocktake_date %}콤보재고 = 재고조사({{ set_stocktake_date }}) − 이후 출고{% else %}재고조사 입력 없음{% endif %}
|
||||||
</span>
|
</span>
|
||||||
<div class="erp-table-wrap" style="margin-top:8px;">
|
<div class="erp-table-wrap" style="margin-top:8px;">
|
||||||
<table class="erp-table">
|
<table class="erp-table">
|
||||||
<thead><tr><th class="ccol">Set Code</th><th>Set Name</th><th style="text-align:right">Qty</th></tr></thead>
|
<thead><tr>
|
||||||
|
<th class="ccol">Set Code</th><th>Set Name</th>
|
||||||
|
<th style="text-align:right">조사</th>
|
||||||
|
<th style="text-align:right">출고</th>
|
||||||
|
<th style="text-align:right">재고</th>
|
||||||
|
</tr></thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for s in set_rows %}
|
{% for s in set_rows %}
|
||||||
<tr>
|
<tr>
|
||||||
<td class="ccol">{{ s.set_code }}</td>
|
<td class="ccol">{{ s.set_code }}</td>
|
||||||
<td class="nm" data-en="{{ s.set_name }}" data-ko="{{ ko_names.get(s.set_code, s.set_name) }}">{{ s.set_name }}</td>
|
<td class="nm" data-en="{{ s.set_name }}" data-ko="{{ ko_names.get(s.set_code, s.set_name) }}">{{ s.set_name }}</td>
|
||||||
|
<td style="text-align:right;color:#64748b;">{{ "{:,}".format(s.stocktake_qty) }}</td>
|
||||||
|
<td style="text-align:right;">{% if s.out_qty %}<span style="color:#b91c1c;">−{{ "{:,}".format(s.out_qty) }}</span>{% else %}<span style="color:#94a3b8;">—</span>{% endif %}</td>
|
||||||
<td style="text-align:right;font-weight:600;">{{ "{:,}".format(s.qty) }}</td>
|
<td style="text-align:right;font-weight:600;">{{ "{:,}".format(s.qty) }}</td>
|
||||||
</tr>
|
</tr>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% if not set_rows %}
|
{% if not set_rows %}
|
||||||
<tr><td colspan="3" class="erp-muted">콤보 재고 입력이 없습니다.</td></tr>
|
<tr><td colspan="5" class="erp-muted">콤보 재고 입력이 없습니다.</td></tr>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
|||||||
@@ -165,6 +165,31 @@ CREATE INDEX IF NOT EXISTS idx_stock_mv_date ON stock_movement (movement_date
|
|||||||
CREATE INDEX IF NOT EXISTS idx_stock_mv_type ON stock_movement (movement_type);
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_type ON stock_movement (movement_type);
|
||||||
CREATE INDEX IF NOT EXISTS idx_stock_mv_ref ON stock_movement (ref_type, ref_no);
|
CREATE INDEX IF NOT EXISTS idx_stock_mv_ref ON stock_movement (ref_type, ref_no);
|
||||||
|
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
-- 4-b) 콤보(세트) 출고 원장 (세트 단위)
|
||||||
|
-- set_code 는 MY- 만. 세트 출고 시 stock_movement(낱개 분해)와 함께
|
||||||
|
-- 여기에 콤보 단위 OUT 을 1행 기록한다(같은 트랜잭션).
|
||||||
|
-- 콤보 재고현황 = 최근 재고조사 콤보수량 − 재고조사일 이후 OUT 합.
|
||||||
|
-- 콤보 입고는 업무상 불가(낱개로만 입고) → OUT/ADJUST 만 허용.
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
CREATE TABLE IF NOT EXISTS set_movement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
movement_date DATE NOT NULL,
|
||||||
|
warehouse_code TEXT NOT NULL REFERENCES warehouses(warehouse_code),
|
||||||
|
set_code TEXT NOT NULL CHECK (set_code LIKE 'MY-%'),
|
||||||
|
movement_type TEXT NOT NULL DEFAULT 'OUT' CHECK (movement_type IN ('OUT', 'ADJUST')),
|
||||||
|
qty INTEGER NOT NULL,
|
||||||
|
ref_type TEXT NOT NULL DEFAULT '',
|
||||||
|
ref_no TEXT NOT NULL DEFAULT '',
|
||||||
|
memo TEXT NOT NULL DEFAULT '',
|
||||||
|
created_by TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
-- OUT 은 양수만. ADJUST 만 음수 허용.
|
||||||
|
CHECK ((movement_type = 'OUT' AND qty > 0) OR movement_type = 'ADJUST')
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_set_mv_wh_set ON set_movement (warehouse_code, set_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_set_mv_date ON set_movement (movement_date);
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
-- 5) 일일 재고조사 (헤더)
|
-- 5) 일일 재고조사 (헤더)
|
||||||
-- 같은 날짜 + 같은 창고에 finalized 는 1건만 허용(부분 유니크 인덱스).
|
-- 같은 날짜 + 같은 창고에 finalized 는 1건만 허용(부분 유니크 인덱스).
|
||||||
@@ -279,7 +304,7 @@ ON CONFLICT (item_code) DO NOTHING;
|
|||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
GRANT USAGE ON SCHEMA public TO malaysia_app;
|
GRANT USAGE ON SCHEMA public TO malaysia_app;
|
||||||
GRANT SELECT, INSERT, UPDATE, DELETE ON
|
GRANT SELECT, INSERT, UPDATE, DELETE ON
|
||||||
warehouses, malaysia_items, set_bom, stock_movement,
|
warehouses, malaysia_items, set_bom, stock_movement, set_movement,
|
||||||
daily_stocktake, daily_stocktake_line, daily_stocktake_rack
|
daily_stocktake, daily_stocktake_line, daily_stocktake_rack
|
||||||
TO malaysia_app;
|
TO malaysia_app;
|
||||||
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO malaysia_app;
|
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO malaysia_app;
|
||||||
|
|||||||
@@ -0,0 +1,26 @@
|
|||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
-- 마이그레이션: 콤보(세트) 출고 원장 set_movement 추가
|
||||||
|
-- 기존 운영 malaysia_stock_db 에 적용(신규 설치는 init 에 이미 포함).
|
||||||
|
-- superuser/owner 로 실행. 멱등(IF NOT EXISTS).
|
||||||
|
-- ════════════════════════════════════════════════════════════
|
||||||
|
CREATE TABLE IF NOT EXISTS set_movement (
|
||||||
|
id BIGSERIAL PRIMARY KEY,
|
||||||
|
movement_date DATE NOT NULL,
|
||||||
|
warehouse_code TEXT NOT NULL REFERENCES warehouses(warehouse_code),
|
||||||
|
set_code TEXT NOT NULL CHECK (set_code LIKE 'MY-%'),
|
||||||
|
movement_type TEXT NOT NULL DEFAULT 'OUT' CHECK (movement_type IN ('OUT', 'ADJUST')),
|
||||||
|
qty INTEGER NOT NULL,
|
||||||
|
ref_type TEXT NOT NULL DEFAULT '',
|
||||||
|
ref_no TEXT NOT NULL DEFAULT '',
|
||||||
|
memo TEXT NOT NULL DEFAULT '',
|
||||||
|
created_by TEXT NOT NULL DEFAULT '',
|
||||||
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
|
CHECK ((movement_type = 'OUT' AND qty > 0) OR movement_type = 'ADJUST')
|
||||||
|
);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_set_mv_wh_set ON set_movement (warehouse_code, set_code);
|
||||||
|
CREATE INDEX IF NOT EXISTS idx_set_mv_date ON set_movement (movement_date);
|
||||||
|
|
||||||
|
GRANT SELECT, INSERT, UPDATE, DELETE ON set_movement TO malaysia_app;
|
||||||
|
GRANT USAGE, SELECT ON SEQUENCE set_movement_id_seq TO malaysia_app;
|
||||||
|
|
||||||
|
SELECT 'set_movement ready' AS status;
|
||||||
Reference in New Issue
Block a user