feat(cupang): 제품명 카탈로그 설정 + 달력 공휴일 색상 + 폼 UI 개선
- 설정 화면 추가: /cupang/products — itemcode_db 검색 등록 + 수동 등록. 제품명 드롭다운 소스. cupang_products 테이블 신규(init.sql 멱등 재실행 필요) - 폼 품목라인: 제품명 드롭다운 선택 → 제품코드 자동 입력 - 라인 컬럼 순서 제품명·제품코드·수량·입수량·박스계산·메모 - 제품코드 140px, 수량·입수량 60px 폭 고정 - 공통 헤더 입력란 겹침 수정(min-width:0, width:100%, box-sizing) - 달력: 일/공휴일 빨강, 토 파랑, 글자 크기 확대. 한국 공휴일 모듈(holidays.py) - document_no null → "None" 출력 버그 수정 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -201,6 +201,59 @@ class CupangDBStore:
|
||||
"""product_code → rule. 폼/계산에서 빠르게 조회하기 위한 dict."""
|
||||
return {r["product_code"]: r for r in self.list_box_rules()}
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 제품명 카탈로그 (cupang_products)
|
||||
# itemcode_db 에서 가져와 등록한 제품 목록. 폼의 제품명 드롭다운 소스.
|
||||
# 제품명 선택 → product_code 자동 채움.
|
||||
# ════════════════════════════════════════════════════════════
|
||||
def list_products(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 cupang_products {where} "
|
||||
"ORDER BY active DESC, sort_order ASC, product_name ASC"
|
||||
).fetchall()
|
||||
return [self._product_serialize(r) for r in rows]
|
||||
|
||||
def upsert_product(
|
||||
self, *, product_code: str, product_name: str, sort_order: int = 0
|
||||
) -> dict[str, Any]:
|
||||
code = (product_code or "").strip()
|
||||
name = (product_name or "").strip()
|
||||
if not code or not name:
|
||||
raise ValueError("제품코드와 제품명 모두 필요합니다.")
|
||||
with self._pool.connection() as conn:
|
||||
row = conn.execute(
|
||||
"""
|
||||
INSERT INTO cupang_products (product_code, product_name, sort_order)
|
||||
VALUES (%s, %s, %s)
|
||||
ON CONFLICT (product_code) DO UPDATE
|
||||
SET product_name = EXCLUDED.product_name,
|
||||
sort_order = EXCLUDED.sort_order,
|
||||
active = TRUE
|
||||
RETURNING *
|
||||
""",
|
||||
(code, name, sort_order),
|
||||
).fetchone()
|
||||
return self._product_serialize(row)
|
||||
|
||||
def deactivate_product(self, *, product_id: int) -> None:
|
||||
with self._pool.connection() as conn:
|
||||
cur = conn.execute(
|
||||
"UPDATE cupang_products SET active = FALSE WHERE id = %s",
|
||||
(product_id,),
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
raise KeyError(product_id)
|
||||
|
||||
def delete_product(self, *, product_id: int) -> None:
|
||||
with self._pool.connection() as conn:
|
||||
cur = conn.execute(
|
||||
"DELETE FROM cupang_products WHERE id = %s", (product_id,)
|
||||
)
|
||||
if cur.rowcount == 0:
|
||||
raise KeyError(product_id)
|
||||
|
||||
# ════════════════════════════════════════════════════════════
|
||||
# 출고 묶음 (cupang_shipments + cupang_shipment_lines)
|
||||
# ════════════════════════════════════════════════════════════
|
||||
@@ -549,6 +602,20 @@ class CupangDBStore:
|
||||
out[k] = v.astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _product_serialize(row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if row is None:
|
||||
return None
|
||||
out = dict(row)
|
||||
out["id"] = int(out["id"])
|
||||
out["active"] = bool(out.get("active", True))
|
||||
out["sort_order"] = int(out.get("sort_order", 0))
|
||||
for k in ("created_at", "updated_at"):
|
||||
v = out.get(k)
|
||||
if isinstance(v, datetime):
|
||||
out[k] = v.astimezone(timezone.utc).isoformat(timespec="seconds")
|
||||
return out
|
||||
|
||||
@staticmethod
|
||||
def _shipment_serialize(row: dict[str, Any] | None) -> dict[str, Any] | None:
|
||||
if row is None:
|
||||
|
||||
Reference in New Issue
Block a user