diff --git a/app/modules/malaysia/db.py b/app/modules/malaysia/db.py
index 981563f..c6dd9a6 100644
--- a/app/modules/malaysia/db.py
+++ b/app/modules/malaysia/db.py
@@ -269,6 +269,25 @@ class MalaysiaStockStore:
),
).fetchone()
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
def list_movements(
@@ -507,10 +526,15 @@ class MalaysiaStockStore:
raise KeyError(stocktake_id)
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:
head = conn.execute(
@@ -527,6 +551,19 @@ class MalaysiaStockStore:
"ORDER BY sku_code ASC",
(head["id"],),
).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()
d = head["stocktake_date"]
date_str = d.isoformat() if isinstance(d, date) else str(d)
@@ -534,7 +571,9 @@ class MalaysiaStockStore:
{
"set_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
]
@@ -883,6 +922,7 @@ class MalaysiaStockStore:
"""전체 창고의 재고 수량 데이터 초기화.
삭제 대상: stock_movement(입고/출고/조정/조사반영) +
+ set_movement(콤보 출고 원장) +
daily_stocktake/daily_stocktake_line(재고조사).
보존 대상: warehouses, malaysia_items(마스터).
한 트랜잭션으로 처리. 반환: 삭제 건수.
@@ -890,9 +930,10 @@ class MalaysiaStockStore:
with self._pool.connection() as conn:
with conn.transaction():
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
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}
# ════════════════════════════════════════════════════════════
# 직렬화
diff --git a/app/modules/malaysia/templates/malaysia/index.html b/app/modules/malaysia/templates/malaysia/index.html
index 0dcfc9c..1c7560e 100644
--- a/app/modules/malaysia/templates/malaysia/index.html
+++ b/app/modules/malaysia/templates/malaysia/index.html
@@ -59,21 +59,28 @@
콤보 재고 (세트 단위)
- {% if set_stocktake_date %}최근 재고조사 {{ set_stocktake_date }} 기준 입력 수량{% else %}재고조사 입력 없음{% endif %}
+ {% if set_stocktake_date %}콤보재고 = 재고조사({{ set_stocktake_date }}) − 이후 출고{% else %}재고조사 입력 없음{% endif %}
- | Set Code | Set Name | Qty |
+
+ | Set Code | Set Name |
+ 조사 |
+ 출고 |
+ 재고 |
+
{% for s in set_rows %}
| {{ s.set_code }} |
{{ s.set_name }} |
+ {{ "{:,}".format(s.stocktake_qty) }} |
+ {% if s.out_qty %}−{{ "{:,}".format(s.out_qty) }}{% else %}—{% endif %} |
{{ "{:,}".format(s.qty) }} |
{% endfor %}
{% if not set_rows %}
- | 콤보 재고 입력이 없습니다. |
+ | 콤보 재고 입력이 없습니다. |
{% endif %}
diff --git a/scripts/sql/malaysia_stock_db_init.sql b/scripts/sql/malaysia_stock_db_init.sql
index 0b6ad26..a5c58d8 100644
--- a/scripts/sql/malaysia_stock_db_init.sql
+++ b/scripts/sql/malaysia_stock_db_init.sql
@@ -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_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) 일일 재고조사 (헤더)
-- 같은 날짜 + 같은 창고에 finalized 는 1건만 허용(부분 유니크 인덱스).
@@ -279,7 +304,7 @@ ON CONFLICT (item_code) DO NOTHING;
-- ════════════════════════════════════════════════════════════
GRANT USAGE ON SCHEMA public TO malaysia_app;
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
TO malaysia_app;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO malaysia_app;
diff --git a/scripts/sql/malaysia_stock_db_set_movement.sql b/scripts/sql/malaysia_stock_db_set_movement.sql
new file mode 100644
index 0000000..4141681
--- /dev/null
+++ b/scripts/sql/malaysia_stock_db_set_movement.sql
@@ -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;