feat(malaysia): 말레이시아 창고 재고관리 모듈 추가

- 신규 모듈 app/modules/malaysia (prefix /malaysia, 권한키 malaysia)
- malaysia_stock_db: warehouses/malaysia_items/set_bom/stock_movement/
  daily_stocktake/daily_stocktake_line (멱등 init SQL)
- 낱개(MT/MX/MZ) 입출고·조정 movement, 세트(MY) BOM 관리, 세트 출고 BOM 분해
- 일일 재고조사: 세트→낱개 자동 분해(direct+from_set=total), 확정 시 차이만 STOCKTAKE
- prefix 검증 이중화(DB CHECK + store.py 순수함수), MD- 전면 제외
- 상품명은 itemcode_db 읽기 전용 재사용(중복 마스터 없음)
- 화면 4종 + JSON API, 순수로직 테스트 14건, README/docs 갱신

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-11 13:34:31 +09:00
parent 13ac11cc0a
commit f137f2a6a6
19 changed files with 2583 additions and 1 deletions
+19
View File
@@ -17,6 +17,8 @@ from .modules.cupang import build_cupang_store, build_itemcode_reader
from .modules.cupang import router as cupang_router
from .modules.expense import CategoryStore, build_expense_store
from .modules.expense import router as expense_router
from .modules.malaysia import build_malaysia_store
from .modules.malaysia import router as malaysia_router
from .modules.vacation import build_vacation_store
from .modules.vacation import router as vacation_router
from .store import (
@@ -36,6 +38,7 @@ MODULE_LABELS: dict[str, str] = {
"expense": "개인경비",
"vacation": "휴가",
"cupang": "쿠팡 밀크런",
"malaysia": "말레이시아 재고",
"expense_approver": "개인경비",
"vacation_approver": "휴가",
}
@@ -108,6 +111,7 @@ _MODULE_TEMPLATE_DIRS = [
BASE_DIR / "modules" / "expense" / "templates",
BASE_DIR / "modules" / "cupang" / "templates",
BASE_DIR / "modules" / "vacation" / "templates",
BASE_DIR / "modules" / "malaysia" / "templates",
]
templates = Jinja2Templates(directory=str(BASE_DIR / "templates"))
templates.env.loader = ChoiceLoader(
@@ -135,11 +139,15 @@ app.state.cupang_store = build_cupang_store(dsn=env("CUPANG_DB_URL") or None)
app.state.itemcode_reader = build_itemcode_reader()
# 휴가 관리: VACATION_DB_URL 없으면 store=None(라우터가 "설정 필요" 안내).
app.state.vacation_store = build_vacation_store(dsn=env("VACATION_DB_URL") or None)
# 말레이시아 재고관리: MALAYSIA_STOCK_DB_URL 없으면 store=None(라우터가 "설정 필요" 안내).
# 상품명은 위 itemcode_reader(읽기 전용) 재사용.
app.state.malaysia_store = build_malaysia_store(dsn=env("MALAYSIA_STOCK_DB_URL") or None)
# 모듈 라우터 등록 — 신규 모듈 추가 시 여기 한 줄.
app.include_router(expense_router)
app.include_router(cupang_router)
app.include_router(vacation_router)
app.include_router(malaysia_router)
def public_url_for(request: Request, route_name: str) -> str:
@@ -282,6 +290,16 @@ def _menu_items_for(user_rec: dict[str, Any]) -> list[dict[str, Any]]:
"status": "ready",
"category": "관리",
},
{
"key": "malaysia",
"title": "말레이시아 재고",
"subtitle": "Malaysia Stock",
"description": "말레이시아 창고 입출고·세트 BOM·일일 재고조사를 관리합니다.",
"url": "/malaysia/",
"health_url": "/malaysia/health",
"status": "ready",
"category": "운영",
},
]
allowed = allowed_modules(user_rec)
for item in items:
@@ -298,6 +316,7 @@ def _icon_svg(name: str) -> str:
"corm": '<path d="M21 11.5a8.4 8.4 0 0 1-.9 3.8 8.5 8.5 0 0 1-7.6 4.7 8.4 8.4 0 0 1-3.8-.9L3 21l1.9-5.7a8.4 8.4 0 0 1-.9-3.8 8.5 8.5 0 0 1 4.7-7.6 8.4 8.4 0 0 1 3.8-.9h.5a8.5 8.5 0 0 1 8 8v.5z"/>',
"order": '<rect x="3" y="3" width="18" height="18" rx="2"/><line x1="3" y1="9" x2="21" y2="9"/><line x1="9" y1="21" x2="9" y2="9"/>',
"cupang": '<rect x="3" y="4" width="18" height="18" rx="2"/><line x1="3" y1="10" x2="21" y2="10"/><line x1="8" y1="2" x2="8" y2="6"/><line x1="16" y1="2" x2="16" y2="6"/>',
"malaysia": '<path d="M21 16V8a2 2 0 0 0-1-1.73l-7-4a2 2 0 0 0-2 0l-7 4A2 2 0 0 0 3 8v8a2 2 0 0 0 1 1.73l7 4a2 2 0 0 0 2 0l7-4A2 2 0 0 0 21 16z"/><polyline points="3.27 6.96 12 12.01 20.73 6.96"/><line x1="12" y1="22.08" x2="12" y2="12"/>',
"modules": '<rect x="3" y="3" width="7" height="7"/><rect x="14" y="3" width="7" height="7"/><rect x="3" y="14" width="7" height="7"/><rect x="14" y="14" width="7" height="7"/>',
}
body = paths.get(name, paths["modules"])