feat(malaysia): 세트 BOM을 itemcode_db에서 읽고 입력을 그리드 일괄로 전환
- 세트구성(set_bom) 수동 관리 메뉴/라우트/API/템플릿 제거 - 세트 BOM은 itemcode_db set_components(set_code/single_code/quantity)에서 읽음 (MalaysiaItemcodeReader, ITEMCODE_DB_URL 재사용) - db.py: create_set_out/compute_result/finalize_stocktake 에 bom_map 주입식 - 입출고: 낱개 전체 그리드 일괄 등록 + 세트 출고 그리드(BOM 분해) - 일일 재고조사: 낱개+세트 전체 그리드에 수량만 키인 → 일괄 저장 - malaysia_stock_db.set_bom 테이블은 레거시(미사용) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
+18
-73
@@ -161,71 +161,11 @@ class MalaysiaStockStore:
|
||||
return updated
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 세트 구성표 (set_bom)
|
||||
# 세트 구성표 (BOM)
|
||||
# 별도 테이블/메뉴를 두지 않고 itemcode_db(set_components)에서 읽는다.
|
||||
# 라우터가 MalaysiaItemcodeReader.set_bom_map() 결과를 아래 메서드들에
|
||||
# bom_map 인자로 주입한다(cross-DB 조인 회피, store 는 DB I/O 만 담당).
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def list_bom(self, *, include_inactive: bool = False) -> list[dict[str, Any]]:
|
||||
where = "" if include_inactive else "WHERE active = TRUE"
|
||||
with self._pool.connection() as conn:
|
||||
rows = conn.execute(
|
||||
f"SELECT * FROM set_bom {where} "
|
||||
"ORDER BY set_code ASC, component_code ASC"
|
||||
).fetchall()
|
||||
return [self._serialize(r) for r in rows]
|
||||
|
||||
def list_bom_for_set(self, *, set_code: str) -> list[dict[str, Any]]:
|
||||
with self._pool.connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT * FROM set_bom WHERE set_code = %s AND active = TRUE "
|
||||
"ORDER BY component_code ASC",
|
||||
(store._norm(set_code),),
|
||||
).fetchall()
|
||||
return [self._serialize(r) for r in rows]
|
||||
|
||||
def bom_map(self) -> dict[str, list[dict[str, Any]]]:
|
||||
"""{ set_code: [{component_code, component_qty}, ...] } — active 만.
|
||||
|
||||
store.explode_stocktake / explode_set 에 그대로 전달.
|
||||
"""
|
||||
out: dict[str, list[dict[str, Any]]] = {}
|
||||
with self._pool.connection() as conn:
|
||||
rows = conn.execute(
|
||||
"SELECT set_code, component_code, component_qty "
|
||||
"FROM set_bom WHERE active = TRUE"
|
||||
).fetchall()
|
||||
for r in rows:
|
||||
out.setdefault(r["set_code"], []).append(
|
||||
{
|
||||
"component_code": r["component_code"],
|
||||
"component_qty": int(r["component_qty"]),
|
||||
}
|
||||
)
|
||||
return out
|
||||
|
||||
def upsert_bom_line(
|
||||
self, *, set_code: str, component_code: str, component_qty: int, active: bool = True
|
||||
) -> dict[str, Any]:
|
||||
sc = store.validate_bom_set_code(set_code)
|
||||
cc = store.validate_bom_component_code(component_code)
|
||||
qty = store.validate_qty(component_qty, allow_zero=False)
|
||||
with self._pool.connection() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
INSERT INTO set_bom (set_code, component_code, component_qty, active)
|
||||
VALUES (%s, %s, %s, %s)
|
||||
ON CONFLICT (set_code, component_code) DO UPDATE
|
||||
SET component_qty = EXCLUDED.component_qty,
|
||||
active = EXCLUDED.active
|
||||
RETURNING *
|
||||
""",
|
||||
(sc, cc, qty, bool(active)),
|
||||
).fetchone()
|
||||
return self._serialize(row)
|
||||
|
||||
def delete_bom_line(self, *, bom_id: int) -> None:
|
||||
with self._pool.connection() as conn:
|
||||
cur = conn.execute("DELETE FROM set_bom WHERE id = %s", (bom_id,))
|
||||
if cur.rowcount == 0:
|
||||
raise KeyError(bom_id)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 재고 이동 (stock_movement)
|
||||
@@ -284,19 +224,20 @@ class MalaysiaStockStore:
|
||||
warehouse_code: str,
|
||||
set_code: str,
|
||||
set_qty: int,
|
||||
bom_map: dict[str, list[dict[str, Any]]],
|
||||
ref_type: str = "",
|
||||
ref_no: str = "",
|
||||
memo: str = "",
|
||||
created_by: str = "",
|
||||
) -> list[dict[str, Any]]:
|
||||
"""세트 주문 출고 → set_bom 으로 분해해 구성품별 OUT movement 생성.
|
||||
"""세트 주문 출고 → BOM(itemcode_db)으로 분해해 구성품별 OUT movement 생성.
|
||||
|
||||
예: MY-0002 1세트 출고 → MT-0320, MT-0550 ... 각각 OUT.
|
||||
BOM 이 없으면 ValueError(요구사항 8).
|
||||
bom_map 은 라우터가 itemcode 리더에서 받아 주입. 구성 없으면 ValueError.
|
||||
"""
|
||||
sc = store.validate_bom_set_code(set_code)
|
||||
q = store.validate_qty(set_qty, allow_zero=False)
|
||||
exploded = store.explode_set(sc, q, self.bom_map())
|
||||
exploded = store.explode_set(sc, q, bom_map)
|
||||
if not exploded:
|
||||
raise ValueError(f"세트 {sc} 의 BOM 구성이 없습니다. 세트구성을 먼저 등록하세요.")
|
||||
ref_t = (ref_type or "set").strip() or "set"
|
||||
@@ -567,9 +508,12 @@ class MalaysiaStockStore:
|
||||
raise ValueError("확정된 조사는 취소할 수 없거나, 대상이 없습니다.")
|
||||
return self._serialize(row)
|
||||
|
||||
def compute_result(self, *, stocktake_id: int) -> dict[str, Any]:
|
||||
def compute_result(
|
||||
self, *, stocktake_id: int, bom_map: dict[str, list[dict[str, Any]]]
|
||||
) -> dict[str, Any]:
|
||||
"""재고조사 최종 계산 결과(낱개 아이템 기준).
|
||||
|
||||
bom_map 은 라우터가 itemcode 리더에서 받아 주입.
|
||||
반환:
|
||||
{
|
||||
"stocktake": {...헤더...},
|
||||
@@ -581,9 +525,8 @@ class MalaysiaStockStore:
|
||||
if not head:
|
||||
raise KeyError(stocktake_id)
|
||||
lines = head.get("lines", [])
|
||||
bmap = self.bom_map()
|
||||
exploded = store.explode_stocktake(lines, bmap)
|
||||
missing = store.missing_bom_sets(lines, bmap)
|
||||
exploded = store.explode_stocktake(lines, bom_map)
|
||||
missing = store.missing_bom_sets(lines, bom_map)
|
||||
names = self.item_name_map()
|
||||
rows = []
|
||||
for code in sorted(exploded.keys()):
|
||||
@@ -602,7 +545,9 @@ class MalaysiaStockStore:
|
||||
)
|
||||
return {"stocktake": head, "rows": rows, "missing_bom": missing}
|
||||
|
||||
def finalize_stocktake(self, *, stocktake_id: int) -> dict[str, Any]:
|
||||
def finalize_stocktake(
|
||||
self, *, stocktake_id: int, bom_map: dict[str, list[dict[str, Any]]]
|
||||
) -> dict[str, Any]:
|
||||
"""재고조사 확정 — 최종 낱개 total_qty 와 현재 시스템 재고의 '차이'만
|
||||
STOCKTAKE movement 로 생성한다(요구사항 7).
|
||||
|
||||
@@ -611,7 +556,7 @@ class MalaysiaStockStore:
|
||||
- BOM 없는 MY- 세트가 있으면 확정 거부(요구사항 8).
|
||||
반환: {"stocktake": {...}, "adjustments": [{item_code, before, after, diff}], "rows": [...]}
|
||||
"""
|
||||
computed = self.compute_result(stocktake_id=stocktake_id)
|
||||
computed = self.compute_result(stocktake_id=stocktake_id, bom_map=bom_map)
|
||||
head = computed["stocktake"]
|
||||
if head["status"] != "draft":
|
||||
raise ValueError("이미 확정/취소된 재고조사입니다.")
|
||||
|
||||
Reference in New Issue
Block a user