change(cupang): 화면 문구 "박스" → "상자" 통일
템플릿·라우터 메시지·페이지 제목과 seed/DDL 기본값(쿠팡박스→쿠팡상자)까지 표기를 맞췄다. URL(/box-calc)과 코드 식별자(box_name 등)는 그대로. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
@@ -31,10 +31,10 @@ Claude Code는 이 저장소에서 작업을 시작하기 전에 **반드시 아
|
|||||||
- 반품관리
|
- 반품관리
|
||||||
- 외부 쇼핑몰 API 연동 (카페24, 네이버 스마트스토어, 사방넷 등)
|
- 외부 쇼핑몰 API 연동 (카페24, 네이버 스마트스토어, 사방넷 등)
|
||||||
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
- 개인경비 (`app/modules/expense/`, `expense_db`)
|
||||||
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/박스 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용. 출고 묶음은 **박스 계산 화면의 [분배 확정] 으로만** 만든다(신규 등록 폼 없음, `/cupang/new` 는 박스 계산으로 리다이렉트). 확정 조건: 미배분 박스 0 + 담긴 센터마다 출고방식(택배/파렛트) 선택. 확정 시 센터마다 출고 묶음 1건(`status=출고준비`, 센터입고일=출고일+1일, 박스 구성은 `cupang_shipments.box_plan` JSONB 에 스냅샷 — 혼합 상자 내용물·상자 종류 표시용) + 출고리스트 엑셀 자동 다운로드(`GET /cupang/export.xlsx?date=`, 시트명 YYYYMMDD). 양식 생성은 `app/modules/cupang/export.py` 한 곳에서 만들어 xlsx·구글시트가 공유. `CUPANG_SHEET_ID` + 인증(사용자 OAuth `GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 또는 서비스 계정 `GOOGLE_SHEETS_CREDENTIALS[_JSON]`) 설정 시 확정과 동시에 Google 스프레드시트에 출고일 시트를 생성/덮어쓰기(`app/integrations/google_sheets.py`, 미설정이면 조용히 skip). 작업 중 상태는 `cupang_box_calc_drafts` 에 이름 붙여 임시 저장/불러오기. 쿠팡 발주 엑셀(xlsx) 다중 업로드 지원(`POST /cupang/api/box-calc/upload`, openpyxl) — F13 입고예정일의 하루 전 = 출고일, 22행부터 B=쿠팡상품코드·F=센터명·G=수량을 읽어 `cupang_products.coupang_item_code` 로 제품 매칭 후 센터별 합산 → 센터 단위로 박스 계산·자동 배분
|
- 쿠팡 밀크런 (`app/modules/cupang/`, `cupang_db`) — 출고 달력/상자 계산/입고센터 관리, 상품은 `itemcode_db` 읽기 전용. 출고 묶음은 **상자 계산 화면의 [분배 확정] 으로만** 만든다(신규 등록 폼 없음, `/cupang/new` 는 상자 계산으로 리다이렉트). 확정 조건: 미배분 상자 0 + 담긴 센터마다 출고방식(택배/파렛트) 선택. 확정 시 센터마다 출고 묶음 1건(`status=출고준비`, 센터입고일=출고일+1일, 상자 구성은 `cupang_shipments.box_plan` JSONB 에 스냅샷 — 혼합 상자 내용물·상자 종류 표시용) + 출고리스트 엑셀 자동 다운로드(`GET /cupang/export.xlsx?date=`, 시트명 YYYYMMDD). 양식 생성은 `app/modules/cupang/export.py` 한 곳에서 만들어 xlsx·구글시트가 공유. `CUPANG_SHEET_ID` + 인증(사용자 OAuth `GOOGLE_SHEETS_OAUTH_REFRESH_TOKEN` 또는 서비스 계정 `GOOGLE_SHEETS_CREDENTIALS[_JSON]`) 설정 시 확정과 동시에 Google 스프레드시트에 출고일 시트를 생성/덮어쓰기(`app/integrations/google_sheets.py`, 미설정이면 조용히 skip). 작업 중 상태는 `cupang_box_calc_drafts` 에 이름 붙여 임시 저장/불러오기. 쿠팡 발주 엑셀(xlsx) 다중 업로드 지원(`POST /cupang/api/box-calc/upload`, openpyxl) — F13 입고예정일의 하루 전 = 출고일, 22행부터 B=쿠팡상품코드·F=센터명·G=수량을 읽어 `cupang_products.coupang_item_code` 로 제품 매칭 후 센터별 합산 → 센터 단위로 상자 계산·자동 배분
|
||||||
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
- 휴가 관리 (`app/modules/vacation/`, `vacation_db`) — 월간 달력(구글식 bar)/연차·반차 신청/승인 워크플로/공휴일·연차 설정. 권한키 `vacation`·`vacation_approver`
|
||||||
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
- 말레이시아 창고 재고관리 (`app/modules/malaysia/`, `malaysia_stock_db`) — 낱개(MT/MX/MZ) 입출고·조정, 세트(MY) BOM, 일일 재고조사(세트→낱개 자동 분해), 현재고 현황. 뚜껑(MD-)은 재고 집계 제외 — 단, 창고 랙에는 위치 확인용으로 배치 가능(`store.LID_ITEMS`). 상품은 `itemcode_db` 읽기 전용. 권한키 `malaysia`
|
||||||
- 말레이시아 배송 (`app/modules/dispatch/`, `dispatch_db`) — TikTok·Shopee 출고관리. 플랫폼별 데이터 엑셀 업로드(TikTok=03_TikTok_Order_Export.xlsx, Shopee=Packing List.Doorstep Delivery.xlsx) → 1박스=1카드 출고 작업 리스트·SKU 피킹 요약·Kagayaku 전달표 자동 생성. 1박스 묶음 기준 Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산. 작업 상태 토글(`dispatch_logs` 기록). 받는 사람 이름/전화/주소는 박스 단위로 저장(작업 카드 표시 + 출고 엑셀 생성용 — 개인정보). 배치 다운로드 zip 에 업로드 원본 + 취합 출고 엑셀(`YYYY.MM.DD(Ddd)_tictoc|shopee.xlsx`) 포함. 엑셀은 openpyxl 파싱/생성. 권한키 `dispatch`. 상세는 `docs/DISPATCH_MODULE.md`
|
- 말레이시아 배송 (`app/modules/dispatch/`, `dispatch_db`) — TikTok·Shopee 출고관리. 플랫폼별 데이터 엑셀 업로드(TikTok=03_TikTok_Order_Export.xlsx, Shopee=Packing List.Doorstep Delivery.xlsx) → 1상자=1카드 출고 작업 리스트·SKU 피킹 요약·Kagayaku 전달표 자동 생성. 1상자 묶음 기준 Package ID > Tracking ID > Order ID, 같은 상자 같은 SKU 합산. 작업 상태 토글(`dispatch_logs` 기록). 받는 사람 이름/전화/주소는 상자 단위로 저장(작업 카드 표시 + 출고 엑셀 생성용 — 개인정보). 배치 다운로드 zip 에 업로드 원본 + 취합 출고 엑셀(`YYYY.MM.DD(Ddd)_tictoc|shopee.xlsx`) 포함. 엑셀은 openpyxl 파싱/생성. 권한키 `dispatch`. 상세는 `docs/DISPATCH_MODULE.md`
|
||||||
- 카페24 상품관리 (`app/modules/cafe24/`, `cafe24_db`) — 카페24 관리자에 들어가지 않고 상품 상세페이지(description HTML) 조회·편집·즉시적용·예약적용·자동복원·버전 롤백·일괄수정. 카페24 OAuth/API 클라이언트는 향후 주문관리와 공유하기 위해 **공통 계층 `app/integrations/cafe24/`** 에 둔다 — 라우터에서 `httpx`/`requests` 직접 호출 금지. 토큰은 Fernet 암호화 저장(`CAFE24_TOKEN_SECRET`), 로그/화면에 토큰·시크릿 절대 미출력. 쓰기 직전 항상 카페24 현재 HTML 을 다시 읽어 `BACKUP` revision 생성(로컬 값을 현재값으로 가정 금지). 예약은 DB 저장 + 별도 worker(`app/modules/cafe24/worker.py`, compose 서비스 `dbx-cafe24-worker`)가 처리 — 웹 프로세스에서 대기하지 않는다. 권한키 `cafe24`(연결/해제는 admin 전용). 상세는 `docs/CAFE24_MODULE.md`
|
- 카페24 상품관리 (`app/modules/cafe24/`, `cafe24_db`) — 카페24 관리자에 들어가지 않고 상품 상세페이지(description HTML) 조회·편집·즉시적용·예약적용·자동복원·버전 롤백·일괄수정. 카페24 OAuth/API 클라이언트는 향후 주문관리와 공유하기 위해 **공통 계층 `app/integrations/cafe24/`** 에 둔다 — 라우터에서 `httpx`/`requests` 직접 호출 금지. 토큰은 Fernet 암호화 저장(`CAFE24_TOKEN_SECRET`), 로그/화면에 토큰·시크릿 절대 미출력. 쓰기 직전 항상 카페24 현재 HTML 을 다시 읽어 `BACKUP` revision 생성(로컬 값을 현재값으로 가정 금지). 예약은 DB 저장 + 별도 worker(`app/modules/cafe24/worker.py`, compose 서비스 `dbx-cafe24-worker`)가 처리 — 웹 프로세스에서 대기하지 않는다. 권한키 `cafe24`(연결/해제는 admin 전용). 상세는 `docs/CAFE24_MODULE.md`
|
||||||
- 프로젝트 관리 (`app/modules/project/`, `project_db`) — 아사나식. 프로젝트/서브프로젝트(self-FK `parent_id`, CASCADE)·업무(`tasks`: 담당자·우선순위·시작/마감)·진행단계(`project_stages` 칸반, 생성시 기본 4단계 seed)·멤버 배정(`project_members`)·활동이력(`project_activity`). 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 버튼 토글 + 보드(드래그로 단계 이동)/리스트. 진입 권한키 `project`(관리자 페이지 토글로 직원별 부여, admin 자동). 프로젝트 생성/삭제·사용자 배정은 `is_admin` 만, 배정 멤버(또는 owner)는 서브프로젝트/업무/단계 CRUD. 멤버 배정 후보는 `project` 권한 보유 등록 사용자에서 자동 목록(`GET /project/api/assignable-users`). 업무 배정·완료 시 관리자에게 메일(`app/mail.py` stdlib smtplib, `SMTP_*`+`PROJECT_NOTIFY_EMAIL` env, 미설정 시 조용히 skip, `BackgroundTasks` 비동기). 상세는 `docs/PROJECT_MODULE.md`
|
- 프로젝트 관리 (`app/modules/project/`, `project_db`) — 아사나식. 프로젝트/서브프로젝트(self-FK `parent_id`, CASCADE)·업무(`tasks`: 담당자·우선순위·시작/마감)·진행단계(`project_stages` 칸반, 생성시 기본 4단계 seed)·멤버 배정(`project_members`)·활동이력(`project_activity`). 메인 뷰 달력(FullCalendar)/타임라인(vis-timeline) 버튼 토글 + 보드(드래그로 단계 이동)/리스트. 진입 권한키 `project`(관리자 페이지 토글로 직원별 부여, admin 자동). 프로젝트 생성/삭제·사용자 배정은 `is_admin` 만, 배정 멤버(또는 owner)는 서브프로젝트/업무/단계 CRUD. 멤버 배정 후보는 `project` 권한 보유 등록 사용자에서 자동 목록(`GET /project/api/assignable-users`). 업무 배정·완료 시 관리자에게 메일(`app/mail.py` stdlib smtplib, `SMTP_*`+`PROJECT_NOTIFY_EMAIL` env, 미설정 시 조용히 skip, `BackgroundTasks` 비동기). 상세는 `docs/PROJECT_MODULE.md`
|
||||||
|
|
||||||
|
|||||||
+10
-10
@@ -8,8 +8,8 @@
|
|||||||
앱 계정(cupang_app)은 SELECT/INSERT/UPDATE/DELETE 권한만 받는다.
|
앱 계정(cupang_app)은 SELECT/INSERT/UPDATE/DELETE 권한만 받는다.
|
||||||
- 연결 풀은 lazy open — 부팅 시 DB 가 잠시 끊겨도 컨테이너가 죽지 않게.
|
- 연결 풀은 lazy open — 부팅 시 DB 가 잠시 끊겨도 컨테이너가 죽지 않게.
|
||||||
|
|
||||||
박스 수 계산은 서버에서 `store.compute_boxes` 로 재계산하여 저장한다.
|
상자 수 계산은 서버에서 `store.compute_boxes` 로 재계산하여 저장한다.
|
||||||
클라이언트가 보낸 박스 수는 신뢰하지 않는다.
|
클라이언트가 보낸 상자 수는 신뢰하지 않는다.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
@@ -136,7 +136,7 @@ class CupangDBStore:
|
|||||||
return {"deleted": True, "deactivated": False}
|
return {"deleted": True, "deactivated": False}
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 박스 입수량 규칙 (cupang_box_rules)
|
# 상자 입수량 규칙 (cupang_box_rules)
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
def list_box_rules(self, *, include_inactive: bool = False) -> list[dict[str, Any]]:
|
def list_box_rules(self, *, include_inactive: bool = False) -> list[dict[str, Any]]:
|
||||||
where = "" if include_inactive else "WHERE active = TRUE"
|
where = "" if include_inactive else "WHERE active = TRUE"
|
||||||
@@ -153,7 +153,7 @@ class CupangDBStore:
|
|||||||
product_code: str,
|
product_code: str,
|
||||||
units_per_box: int,
|
units_per_box: int,
|
||||||
product_name_snapshot: str = "",
|
product_name_snapshot: str = "",
|
||||||
box_name: str = "쿠팡박스",
|
box_name: str = "쿠팡상자",
|
||||||
memo: str = "",
|
memo: str = "",
|
||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
code = (product_code or "").strip()
|
code = (product_code or "").strip()
|
||||||
@@ -161,7 +161,7 @@ class CupangDBStore:
|
|||||||
raise ValueError("제품코드 필수")
|
raise ValueError("제품코드 필수")
|
||||||
upb = int(units_per_box)
|
upb = int(units_per_box)
|
||||||
if upb <= 0:
|
if upb <= 0:
|
||||||
raise ValueError("박스당 입수량은 1 이상이어야 합니다.")
|
raise ValueError("상자당 입수량은 1 이상이어야 합니다.")
|
||||||
with self._pool.connection() as conn:
|
with self._pool.connection() as conn:
|
||||||
row = conn.execute(
|
row = conn.execute(
|
||||||
"""
|
"""
|
||||||
@@ -176,7 +176,7 @@ class CupangDBStore:
|
|||||||
active = TRUE
|
active = TRUE
|
||||||
RETURNING *
|
RETURNING *
|
||||||
""",
|
""",
|
||||||
(code, product_name_snapshot.strip(), (box_name or "쿠팡박스").strip(), upb, memo.strip()),
|
(code, product_name_snapshot.strip(), (box_name or "쿠팡상자").strip(), upb, memo.strip()),
|
||||||
).fetchone()
|
).fetchone()
|
||||||
return self._rule_serialize(row)
|
return self._rule_serialize(row)
|
||||||
|
|
||||||
@@ -196,8 +196,8 @@ class CupangDBStore:
|
|||||||
raise KeyError(rule_id)
|
raise KeyError(rule_id)
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 박스 계산 임시 저장 (cupang_box_calc_drafts)
|
# 상자 계산 임시 저장 (cupang_box_calc_drafts)
|
||||||
# payload 는 화면 상태 스냅샷(JSONB). 박스 수 계산은 불러온 뒤
|
# payload 는 화면 상태 스냅샷(JSONB). 상자 수 계산은 불러온 뒤
|
||||||
# 서버에서 다시 하므로 여기 값은 신뢰 대상이 아니다.
|
# 서버에서 다시 하므로 여기 값은 신뢰 대상이 아니다.
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
def list_box_calc_drafts(self, *, limit: int = 50) -> list[dict[str, Any]]:
|
def list_box_calc_drafts(self, *, limit: int = 50) -> list[dict[str, Any]]:
|
||||||
@@ -348,7 +348,7 @@ class CupangDBStore:
|
|||||||
) -> dict[str, Any]:
|
) -> dict[str, Any]:
|
||||||
"""등록된 제품 수정(제품명/제품코드/쿠팡상품코드).
|
"""등록된 제품 수정(제품명/제품코드/쿠팡상품코드).
|
||||||
|
|
||||||
제품코드가 바뀌면 같은 코드로 연결돼 있던 박스 규칙(cupang_box_rules)의
|
제품코드가 바뀌면 같은 코드로 연결돼 있던 상자 규칙(cupang_box_rules)의
|
||||||
product_code 도 함께 옮긴다(출고 라인은 과거 기록이라 스냅샷 유지).
|
product_code 도 함께 옮긴다(출고 라인은 과거 기록이라 스냅샷 유지).
|
||||||
"""
|
"""
|
||||||
name = (product_name or "").strip()
|
name = (product_name or "").strip()
|
||||||
@@ -694,7 +694,7 @@ class CupangDBStore:
|
|||||||
"worker": str(header.get("worker") or "").strip(),
|
"worker": str(header.get("worker") or "").strip(),
|
||||||
"status": status,
|
"status": status,
|
||||||
"memo": str(header.get("memo") or "").strip(),
|
"memo": str(header.get("memo") or "").strip(),
|
||||||
# 확정 당시 박스 구성(제품별/혼합). 화면 표시 전용이라 형태만 검사한다.
|
# 확정 당시 상자 구성(제품별/혼합). 화면 표시 전용이라 형태만 검사한다.
|
||||||
"box_plan": header.get("box_plan") if isinstance(header.get("box_plan"), list) else [],
|
"box_plan": header.get("box_plan") if isinstance(header.get("box_plan"), list) else [],
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -154,7 +154,7 @@ async def index(request: Request) -> HTMLResponse:
|
|||||||
# 선택일의 묶음 — 달력과 같은 기준(출고일)
|
# 선택일의 묶음 — 달력과 같은 기준(출고일)
|
||||||
sel_shipments = [s for s in shipments if s.get("ship_date") == sel]
|
sel_shipments = [s for s in shipments if s.get("ship_date") == sel]
|
||||||
|
|
||||||
# 오른쪽 상세: 센터별 상품 목록·수량·박스 수·출고방식
|
# 오른쪽 상세: 센터별 상품 목록·수량·상자 수·출고방식
|
||||||
for s in sel_shipments:
|
for s in sel_shipments:
|
||||||
full = store.get_shipment(shipment_id=s["id"])
|
full = store.get_shipment(shipment_id=s["id"])
|
||||||
lines = (full.get("lines") if full else []) or []
|
lines = (full.get("lines") if full else []) or []
|
||||||
@@ -334,16 +334,16 @@ async def export_shipments_xlsx(request: Request, date: str = "") -> Any:
|
|||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@router.get("/new")
|
@router.get("/new")
|
||||||
async def new_form(request: Request) -> RedirectResponse:
|
async def new_form(request: Request) -> RedirectResponse:
|
||||||
"""신규 등록 폼은 없앴다. 출고 묶음은 박스 계산의 [분배 확정] 으로만 만든다.
|
"""신규 등록 폼은 없앴다. 출고 묶음은 상자 계산의 [분배 확정] 으로만 만든다.
|
||||||
|
|
||||||
예전 링크/북마크가 404 나지 않도록 박스 계산으로 보낸다.
|
예전 링크/북마크가 404 나지 않도록 상자 계산으로 보낸다.
|
||||||
"""
|
"""
|
||||||
return RedirectResponse(url="/cupang/box-calc", status_code=303)
|
return RedirectResponse(url="/cupang/box-calc", status_code=303)
|
||||||
|
|
||||||
|
|
||||||
@router.get("/{shipment_id:int}", response_class=HTMLResponse)
|
@router.get("/{shipment_id:int}", response_class=HTMLResponse)
|
||||||
async def detail(request: Request, shipment_id: int) -> HTMLResponse:
|
async def detail(request: Request, shipment_id: int) -> HTMLResponse:
|
||||||
"""출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성(읽기 전용).
|
"""출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성(읽기 전용).
|
||||||
|
|
||||||
수정 기능은 없앴다. 잘못 만들었으면 달력에서 삭제하고 다시 확정한다.
|
수정 기능은 없앴다. 잘못 만들었으면 달력에서 삭제하고 다시 확정한다.
|
||||||
"""
|
"""
|
||||||
@@ -611,7 +611,7 @@ async def center_delete(
|
|||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 박스 입수량 관리
|
# 상자 입수량 관리
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@router.get("/box-rules", response_class=HTMLResponse)
|
@router.get("/box-rules", response_class=HTMLResponse)
|
||||||
async def box_rules_page(request: Request) -> HTMLResponse:
|
async def box_rules_page(request: Request) -> HTMLResponse:
|
||||||
@@ -629,8 +629,8 @@ async def box_rules_page(request: Request) -> HTMLResponse:
|
|||||||
"user": user,
|
"user": user,
|
||||||
"is_admin": is_admin(user),
|
"is_admin": is_admin(user),
|
||||||
"nav_items": build_erp_nav(user, active="cupang"),
|
"nav_items": build_erp_nav(user, active="cupang"),
|
||||||
"page_title": "쿠팡 밀크런 — 박스 입수량",
|
"page_title": "쿠팡 밀크런 — 상자 입수량",
|
||||||
"page_subtitle": "제품코드별 쿠팡박스 1박스당 입수량 설정",
|
"page_subtitle": "제품코드별 쿠팡상자 1상자당 입수량 설정",
|
||||||
"box_rules": store.list_box_rules(include_inactive=True),
|
"box_rules": store.list_box_rules(include_inactive=True),
|
||||||
"products": store.list_products(),
|
"products": store.list_products(),
|
||||||
"search_enabled": bool((_itemcode(request)) and _itemcode(request).enabled),
|
"search_enabled": bool((_itemcode(request)) and _itemcode(request).enabled),
|
||||||
@@ -644,7 +644,7 @@ async def box_rule_upsert(
|
|||||||
product_code: str = Form(...),
|
product_code: str = Form(...),
|
||||||
units_per_box: int = Form(...),
|
units_per_box: int = Form(...),
|
||||||
product_name_snapshot: str = Form(""),
|
product_name_snapshot: str = Form(""),
|
||||||
box_name: str = Form("쿠팡박스"),
|
box_name: str = Form("쿠팡상자"),
|
||||||
memo: str = Form(""),
|
memo: str = Form(""),
|
||||||
user: dict[str, Any] = Depends(_require_user),
|
user: dict[str, Any] = Depends(_require_user),
|
||||||
) -> RedirectResponse:
|
) -> RedirectResponse:
|
||||||
@@ -681,7 +681,7 @@ async def box_rule_delete(
|
|||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 박스 계산기 — 제품명 + 수량 → 박스 수 / 남은 낱개
|
# 상자 계산기 — 제품명 + 수량 → 상자 수 / 남은 낱개
|
||||||
# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다.
|
# 저장하지 않는 계산 전용 화면. 규칙은 cupang_box_rules 를 그대로 사용한다.
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@router.get("/box-calc", response_class=HTMLResponse)
|
@router.get("/box-calc", response_class=HTMLResponse)
|
||||||
@@ -701,7 +701,7 @@ async def box_calc_page(request: Request) -> HTMLResponse:
|
|||||||
"is_admin": is_admin(user),
|
"is_admin": is_admin(user),
|
||||||
"nav_items": build_erp_nav(user, active="cupang"),
|
"nav_items": build_erp_nav(user, active="cupang"),
|
||||||
"page_title": "쿠팡 밀크런 — 쿠팡 발주 업로드",
|
"page_title": "쿠팡 밀크런 — 쿠팡 발주 업로드",
|
||||||
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 박스를 계산합니다.",
|
"page_subtitle": "쿠팡 발주 엑셀을 올리면 센터별 수량을 합산해 상자를 계산합니다.",
|
||||||
"box_rules": store.list_box_rules(),
|
"box_rules": store.list_box_rules(),
|
||||||
# 센터 선택 드롭다운은 가나다순 (한글 음절은 코드포인트 순 = 가나다순)
|
# 센터 선택 드롭다운은 가나다순 (한글 음절은 코드포인트 순 = 가나다순)
|
||||||
"centers": sorted(store.list_centers(), key=lambda c: (c.get("name") or "")),
|
"centers": sorted(store.list_centers(), key=lambda c: (c.get("name") or "")),
|
||||||
@@ -866,7 +866,7 @@ async def box_calc_upload(
|
|||||||
if unknown_centers:
|
if unknown_centers:
|
||||||
warnings.append("등록되지 않은 센터: " + ", ".join(sorted(unknown_centers)))
|
warnings.append("등록되지 않은 센터: " + ", ".join(sorted(unknown_centers)))
|
||||||
if no_rule:
|
if no_rule:
|
||||||
warnings.append("박스 입수량 미설정: " + ", ".join(sorted(no_rule)))
|
warnings.append("상자 입수량 미설정: " + ", ".join(sorted(no_rule)))
|
||||||
|
|
||||||
uniq_dates = sorted(set(ship_dates))
|
uniq_dates = sorted(set(ship_dates))
|
||||||
|
|
||||||
@@ -927,7 +927,7 @@ async def box_calc_api(
|
|||||||
payload: dict[str, Any] = Body(...),
|
payload: dict[str, Any] = Body(...),
|
||||||
user: dict[str, Any] = Depends(_require_user),
|
user: dict[str, Any] = Depends(_require_user),
|
||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""[{product_code, quantity}] → 제품별 박스 계산 + 박스명별 합계.
|
"""[{product_code, quantity}] → 제품별 상자 계산 + 상자명별 합계.
|
||||||
|
|
||||||
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
|
클라이언트 계산을 신뢰하지 않고 store.compute_boxes 로 서버에서 계산한다.
|
||||||
"""
|
"""
|
||||||
@@ -943,9 +943,9 @@ async def box_calc_api(
|
|||||||
|
|
||||||
|
|
||||||
def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
||||||
"""[{product_code, quantity}] → 제품별 박스 + 자투리 혼합 박스 + 합계.
|
"""[{product_code, quantity}] → 제품별 상자 + 자투리 혼합 상자 + 합계.
|
||||||
|
|
||||||
박스 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
|
상자 계산 화면(API)과 출고 상세 보기가 같은 결과를 쓰도록 한 곳에 둔다.
|
||||||
"""
|
"""
|
||||||
from .store import compute_boxes # noqa: WPS433
|
from .store import compute_boxes # noqa: WPS433
|
||||||
|
|
||||||
@@ -1003,12 +1003,12 @@ def _compute_boxes(store: Any, raw_items: list[Any]) -> dict[str, Any]:
|
|||||||
|
|
||||||
|
|
||||||
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
||||||
"""제품별 자투리(remainder_units)를 같은 박스명끼리 모아 혼합 박스에 담는다.
|
"""제품별 자투리(remainder_units)를 같은 상자명끼리 모아 혼합 상자에 담는다.
|
||||||
|
|
||||||
- 한 박스의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
|
- 한 상자의 용량을 1 로 두고, 제품 1개가 차지하는 부피를 1/units_per_box 로 본다.
|
||||||
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 박스)
|
(예: 쿠팡 2호 = 8개들이 → 1개 = 1/8 상자)
|
||||||
- 같은 박스명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
|
- 같은 상자명끼리만 섞는다. 서로 다른 입수량이 섞여도 부피 합으로 정확히 계산된다.
|
||||||
- 한 제품의 자투리가 두 박스에 나뉘어 담기는 것은 허용(그래야 박스 수가 최소).
|
- 한 제품의 자투리가 두 상자에 나뉘어 담기는 것은 허용(그래야 상자 수가 최소).
|
||||||
- 오차 없이 계산하려고 float 대신 Fraction 을 쓴다.
|
- 오차 없이 계산하려고 float 대신 Fraction 을 쓴다.
|
||||||
"""
|
"""
|
||||||
groups: dict[str, list[dict[str, Any]]] = {}
|
groups: dict[str, list[dict[str, Any]]] = {}
|
||||||
@@ -1019,7 +1019,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
|
|
||||||
out: list[dict[str, Any]] = []
|
out: list[dict[str, Any]] = []
|
||||||
for box_name in sorted(groups):
|
for box_name in sorted(groups):
|
||||||
# 자투리가 많은 제품부터 담아 박스 안 품목 수를 줄인다.
|
# 자투리가 많은 제품부터 담아 상자 안 품목 수를 줄인다.
|
||||||
items = sorted(groups[box_name], key=lambda r: -r["remainder_units"])
|
items = sorted(groups[box_name], key=lambda r: -r["remainder_units"])
|
||||||
boxes: list[dict[str, Any]] = []
|
boxes: list[dict[str, Any]] = []
|
||||||
cur: list[dict[str, Any]] = []
|
cur: list[dict[str, Any]] = []
|
||||||
@@ -1030,7 +1030,7 @@ def _pack_leftovers(results: list[dict[str, Any]]) -> list[dict[str, Any]]:
|
|||||||
left = int(r["remainder_units"])
|
left = int(r["remainder_units"])
|
||||||
while left > 0:
|
while left > 0:
|
||||||
take = min(left, int(free / unit))
|
take = min(left, int(free / unit))
|
||||||
if take == 0: # 남은 자리 없음 → 새 박스
|
if take == 0: # 남은 자리 없음 → 새 상자
|
||||||
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
|
boxes.append({"items": cur, "fill_percent": float(round((1 - free) * 100, 1))})
|
||||||
cur, free = [], Fraction(1)
|
cur, free = [], Fraction(1)
|
||||||
continue
|
continue
|
||||||
@@ -1245,9 +1245,9 @@ async def product_delete(
|
|||||||
# 분배 확정 — 센터별 출고 묶음 생성
|
# 분배 확정 — 센터별 출고 묶음 생성
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
def _clean_box_plan(raw: Any) -> list[dict[str, Any]]:
|
def _clean_box_plan(raw: Any) -> list[dict[str, Any]]:
|
||||||
"""화면이 보낸 박스 구성을 표시용으로만 정리해서 저장한다.
|
"""화면이 보낸 상자 구성을 표시용으로만 정리해서 저장한다.
|
||||||
|
|
||||||
수량·박스 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
|
수량·상자 수는 라인(제품 합계)이 정답이고, 이 값은 "어느 상자에 무엇이
|
||||||
담겼는지"를 보여주기 위한 스냅샷이다.
|
담겼는지"를 보여주기 위한 스냅샷이다.
|
||||||
"""
|
"""
|
||||||
if not isinstance(raw, list):
|
if not isinstance(raw, list):
|
||||||
@@ -1305,7 +1305,7 @@ async def box_calc_confirm(
|
|||||||
) -> JSONResponse:
|
) -> JSONResponse:
|
||||||
"""{ship_date, centers:[{center_id, ship_method, boxes, items:[...]}]} → 센터마다 출고 묶음 1건 생성.
|
"""{ship_date, centers:[{center_id, ship_method, boxes, items:[...]}]} → 센터마다 출고 묶음 1건 생성.
|
||||||
|
|
||||||
화면에서 보낸 박스 수는 요약 표시용이고, 라인의 박스 수는 저장 시
|
화면에서 보낸 상자 수는 요약 표시용이고, 라인의 상자 수는 저장 시
|
||||||
서버(store.compute_boxes)가 수량과 입수량으로 다시 계산한다.
|
서버(store.compute_boxes)가 수량과 입수량으로 다시 계산한다.
|
||||||
"""
|
"""
|
||||||
from .store import SHIP_METHODS as _METHODS # noqa: WPS433
|
from .store import SHIP_METHODS as _METHODS # noqa: WPS433
|
||||||
@@ -1345,7 +1345,7 @@ async def box_calc_confirm(
|
|||||||
status_code=400, detail=f"{center['name']} 의 출고방식을 선택하세요."
|
status_code=400, detail=f"{center['name']} 의 출고방식을 선택하세요."
|
||||||
)
|
)
|
||||||
|
|
||||||
# 같은 제품이 여러 박스로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
|
# 같은 제품이 여러 상자로 나뉘어 담겼을 수 있으므로 제품코드로 합친다.
|
||||||
merged: dict[str, int] = {}
|
merged: dict[str, int] = {}
|
||||||
for it in raw.get("items") or []:
|
for it in raw.get("items") or []:
|
||||||
if not isinstance(it, dict):
|
if not isinstance(it, dict):
|
||||||
@@ -1382,10 +1382,10 @@ async def box_calc_confirm(
|
|||||||
except (TypeError, ValueError):
|
except (TypeError, ValueError):
|
||||||
boxes = 0
|
boxes = 0
|
||||||
pieces = sum(merged.values())
|
pieces = sum(merged.values())
|
||||||
# 화면에서 고른 상자 종류별 박스 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
|
# 화면에서 고른 상자 종류별 상자 수(예: "쿠팡상자41"). 출고리스트 엑셀의 "출고" 칸.
|
||||||
summary = str(raw.get("box_summary") or "").strip()
|
summary = str(raw.get("box_summary") or "").strip()
|
||||||
if not summary:
|
if not summary:
|
||||||
summary = f"{boxes}박스 · {pieces}개" if boxes else f"{pieces}개"
|
summary = f"{boxes}상자 · {pieces}개" if boxes else f"{pieces}개"
|
||||||
|
|
||||||
plans.append(
|
plans.append(
|
||||||
{
|
{
|
||||||
@@ -1417,7 +1417,7 @@ async def box_calc_confirm(
|
|||||||
"outbound_summary": plan["summary"],
|
"outbound_summary": plan["summary"],
|
||||||
"worker": worker,
|
"worker": worker,
|
||||||
"status": "출고준비",
|
"status": "출고준비",
|
||||||
"memo": "박스 계산에서 분배 확정",
|
"memo": "상자 계산에서 분배 확정",
|
||||||
"box_plan": plan["box_plan"],
|
"box_plan": plan["box_plan"],
|
||||||
},
|
},
|
||||||
lines=plan["lines"],
|
lines=plan["lines"],
|
||||||
@@ -1438,7 +1438,7 @@ async def box_calc_confirm(
|
|||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 박스 계산 임시 저장 (화면 상태 스냅샷)
|
# 상자 계산 임시 저장 (화면 상태 스냅샷)
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
@router.get("/api/box-calc/drafts")
|
@router.get("/api/box-calc/drafts")
|
||||||
async def box_calc_drafts_list(
|
async def box_calc_drafts_list(
|
||||||
|
|||||||
@@ -58,7 +58,7 @@ DEFAULT_CENTERS: tuple[str, ...] = (
|
|||||||
|
|
||||||
|
|
||||||
def compute_boxes(quantity: int, units_per_box: int | None) -> dict[str, Any]:
|
def compute_boxes(quantity: int, units_per_box: int | None) -> dict[str, Any]:
|
||||||
"""수량 + 박스당 입수량으로 필요한 박스 수를 계산한다.
|
"""수량 + 상자당 입수량으로 필요한 상자 수를 계산한다.
|
||||||
|
|
||||||
클라이언트 계산을 신뢰하지 않고 서버에서 이 함수로 재계산한다.
|
클라이언트 계산을 신뢰하지 않고 서버에서 이 함수로 재계산한다.
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -16,8 +16,8 @@
|
|||||||
{% if not box_rules %}
|
{% if not box_rules %}
|
||||||
<div class="erp-card cpg-form-card">
|
<div class="erp-card cpg-form-card">
|
||||||
<p class="erp-muted">
|
<p class="erp-muted">
|
||||||
등록된 박스 입수량 규칙이 없습니다.
|
등록된 상자 입수량 규칙이 없습니다.
|
||||||
<a href="/cupang/box-rules">박스 입수량 설정</a>에서 먼저 제품별 입수량을 등록하세요.
|
<a href="/cupang/box-rules">상자 입수량 설정</a>에서 먼저 제품별 입수량을 등록하세요.
|
||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
{% else %}
|
{% else %}
|
||||||
@@ -30,7 +30,7 @@
|
|||||||
|
|
||||||
<div class="cpg-calc3">
|
<div class="cpg-calc3">
|
||||||
|
|
||||||
<!-- ① 박스 계산 — 제품명 + 수량만 입력 -->
|
<!-- ① 상자 계산 — 제품명 + 수량만 입력 -->
|
||||||
<div class="erp-card cpg-form-card cpg-calc-card">
|
<div class="erp-card cpg-form-card cpg-calc-card">
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>① 쿠팡 발주 업로드</h2>
|
<h2>① 쿠팡 발주 업로드</h2>
|
||||||
@@ -75,10 +75,10 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ② 박스 요약 — 센터에 배분하고 남은 수량이 실시간 반영 -->
|
<!-- ② 상자 요약 — 센터에 배분하고 남은 수량이 실시간 반영 -->
|
||||||
<div class="erp-card cpg-form-card cpg-calc-sum">
|
<div class="erp-card cpg-form-card cpg-calc-sum">
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>② 박스 요약</h2>
|
<h2>② 상자 요약</h2>
|
||||||
<span class="erp-muted">카드를 ③ 센터로 끌어다 놓거나, 카드를 클릭해 담습니다.</span>
|
<span class="erp-muted">카드를 ③ 센터로 끌어다 놓거나, 카드를 클릭해 담습니다.</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -88,10 +88,10 @@
|
|||||||
<div class="cpg-sum-kpis" id="cpg-sum-kpis"></div>
|
<div class="cpg-sum-kpis" id="cpg-sum-kpis"></div>
|
||||||
|
|
||||||
<div class="cpg-sum-scroll">
|
<div class="cpg-sum-scroll">
|
||||||
<h3 class="cpg-sum-h3">제품별 박스 <span class="erp-muted">잔여 기준</span></h3>
|
<h3 class="cpg-sum-h3">제품별 상자 <span class="erp-muted">잔여 기준</span></h3>
|
||||||
<div class="cpg-sum-prods" id="cpg-sum-prods"></div>
|
<div class="cpg-sum-prods" id="cpg-sum-prods"></div>
|
||||||
|
|
||||||
<h3 class="cpg-sum-h3">자투리 혼합 박스 <span class="erp-muted" id="cpg-sum-mix-note"></span></h3>
|
<h3 class="cpg-sum-h3">자투리 혼합 상자 <span class="erp-muted" id="cpg-sum-mix-note"></span></h3>
|
||||||
<div class="cpg-sum-boxes" id="cpg-sum-boxes"></div>
|
<div class="cpg-sum-boxes" id="cpg-sum-boxes"></div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -103,7 +103,7 @@
|
|||||||
<h2>③ 센터 분배</h2>
|
<h2>③ 센터 분배</h2>
|
||||||
<span class="erp-muted">담을 센터를 골라 추가하세요.</span>
|
<span class="erp-muted">담을 센터를 골라 추가하세요.</span>
|
||||||
<button type="button" class="erp-btn erp-btn-primary cpg-confirm-btn" id="cpg-confirm" disabled
|
<button type="button" class="erp-btn erp-btn-primary cpg-confirm-btn" id="cpg-confirm" disabled
|
||||||
title="모든 박스를 배분하고 센터마다 출고방식을 고르면 활성화됩니다.">분배 확정</button>
|
title="모든 상자를 배분하고 센터마다 출고방식을 고르면 활성화됩니다.">분배 확정</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{% if not centers %}
|
{% if not centers %}
|
||||||
@@ -118,7 +118,7 @@
|
|||||||
|
|
||||||
</div><!-- /cpg-calc3 -->
|
</div><!-- /cpg-calc3 -->
|
||||||
|
|
||||||
<!-- 담을 박스 수량 입력 대화상자 -->
|
<!-- 담을 상자 수량 입력 대화상자 -->
|
||||||
<div class="cpg-modal" id="cpg-dlg" hidden>
|
<div class="cpg-modal" id="cpg-dlg" hidden>
|
||||||
<div class="cpg-modal-back" data-dlg-close></div>
|
<div class="cpg-modal-back" data-dlg-close></div>
|
||||||
<div class="cpg-modal-box" role="dialog" aria-modal="true" aria-labelledby="cpg-dlg-title">
|
<div class="cpg-modal-box" role="dialog" aria-modal="true" aria-labelledby="cpg-dlg-title">
|
||||||
@@ -126,7 +126,7 @@
|
|||||||
<p class="cpg-dlg-item" id="cpg-dlg-item"></p>
|
<p class="cpg-dlg-item" id="cpg-dlg-item"></p>
|
||||||
<div class="erp-field"><span>센터</span>
|
<div class="erp-field"><span>센터</span>
|
||||||
<div class="cpg-dlg-centers" id="cpg-dlg-centers" role="group" aria-label="담을 센터"></div></div>
|
<div class="cpg-dlg-centers" id="cpg-dlg-centers" role="group" aria-label="담을 센터"></div></div>
|
||||||
<label class="erp-field"><span>박스 수량</span>
|
<label class="erp-field"><span>상자 수량</span>
|
||||||
<input class="erp-input" type="number" id="cpg-dlg-qty" min="1" step="1" /></label>
|
<input class="erp-input" type="number" id="cpg-dlg-qty" min="1" step="1" /></label>
|
||||||
<p class="erp-muted" id="cpg-dlg-hint"></p>
|
<p class="erp-muted" id="cpg-dlg-hint"></p>
|
||||||
<div class="erp-page-actions cpg-dlg-actions">
|
<div class="erp-page-actions cpg-dlg-actions">
|
||||||
@@ -195,7 +195,7 @@
|
|||||||
<script type="application/json" id="cpg-calc-rules">{{ box_rules | tojson }}</script>
|
<script type="application/json" id="cpg-calc-rules">{{ box_rules | tojson }}</script>
|
||||||
<script type="application/json" id="cpg-centers">{{ centers | tojson }}</script>
|
<script type="application/json" id="cpg-centers">{{ centers | tojson }}</script>
|
||||||
<script>
|
<script>
|
||||||
// 박스 계산 3열 화면.
|
// 상자 계산 3열 화면.
|
||||||
// ① 제품명·수량 입력 → 서버(POST /cupang/api/box-calc)에서 계산
|
// ① 제품명·수량 입력 → 서버(POST /cupang/api/box-calc)에서 계산
|
||||||
// ② 계산 결과 요약 — 드래그(또는 클릭) 소스
|
// ② 계산 결과 요약 — 드래그(또는 클릭) 소스
|
||||||
// ③ 선택한 센터에만 분배. 담은 수량만큼 ② 의 잔여가 즉시 줄어든다.
|
// ③ 선택한 센터에만 분배. 담은 수량만큼 ② 의 잔여가 즉시 줄어든다.
|
||||||
@@ -243,8 +243,8 @@
|
|||||||
|
|
||||||
// ── 상태 ─────────────────────────────────────────
|
// ── 상태 ─────────────────────────────────────────
|
||||||
var calc = { results: [], mixes: [] }; // 서버 계산 결과
|
var calc = { results: [], mixes: [] }; // 서버 계산 결과
|
||||||
var units = {}; // key -> 박스 1개당 상품 수
|
var units = {}; // key -> 상자 1개당 상품 수
|
||||||
var contents = {}; // key -> 혼합 박스 내용물 [{product_name, quantity}]
|
var contents = {}; // key -> 혼합 상자 내용물 [{product_name, quantity}]
|
||||||
var labels = {}; // key -> 표시 이름
|
var labels = {}; // key -> 표시 이름
|
||||||
var openCenters = []; // ③ 에 추가한 센터 id (문자열)
|
var openCenters = []; // ③ 에 추가한 센터 id (문자열)
|
||||||
var alloc = {}; // centerId -> [{id, key, count}]
|
var alloc = {}; // centerId -> [{id, key, count}]
|
||||||
@@ -255,7 +255,7 @@
|
|||||||
function prodKey(code) { return "p:" + code; }
|
function prodKey(code) { return "p:" + code; }
|
||||||
function mixKey(boxName, idx) { return "m:" + boxName + "#" + idx; }
|
function mixKey(boxName, idx) { return "m:" + boxName + "#" + idx; }
|
||||||
|
|
||||||
// 자투리 혼합 박스에 쓸 상자 종류 — 요약 카드에서 고른다(표시용).
|
// 자투리 혼합 상자에 쓸 상자 종류 — 요약 카드에서 고른다(표시용).
|
||||||
var MIX_TYPES = ["쿠팡상자", "3호상자", "6호상자"];
|
var MIX_TYPES = ["쿠팡상자", "3호상자", "6호상자"];
|
||||||
var mixType = {}; // mixKey -> 고른 상자 종류
|
var mixType = {}; // mixKey -> 고른 상자 종류
|
||||||
var mixOwner = {}; // mixKey -> 센터명(업로드로 만들어진 경우)
|
var mixOwner = {}; // mixKey -> 센터명(업로드로 만들어진 경우)
|
||||||
@@ -360,8 +360,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
// ── 쿠팡 발주 엑셀 업로드 ─────────────────────────
|
// ── 쿠팡 발주 엑셀 업로드 ─────────────────────────
|
||||||
// 업로드 → 센터별 수량 합산(서버) → 센터마다 박스 계산 → ②③ 자동 채움.
|
// 업로드 → 센터별 수량 합산(서버) → 센터마다 상자 계산 → ②③ 자동 채움.
|
||||||
// 박스는 센터별로 포장하므로 계산도 센터 단위로 한다(자투리 혼합 박스 포함).
|
// 상자는 센터별로 포장하므로 계산도 센터 단위로 한다(자투리 혼합 상자 포함).
|
||||||
var poFile = document.getElementById("cpg-po-file");
|
var poFile = document.getElementById("cpg-po-file");
|
||||||
var poMsg = document.getElementById("cpg-po-msg");
|
var poMsg = document.getElementById("cpg-po-msg");
|
||||||
var poResult = document.getElementById("cpg-po-result");
|
var poResult = document.getElementById("cpg-po-result");
|
||||||
@@ -412,7 +412,7 @@
|
|||||||
return '<button type="button" class="cpg-date-tab' + (d === activeDate ? " is-active" : "") + '"' +
|
return '<button type="button" class="cpg-date-tab' + (d === activeDate ? " is-active" : "") + '"' +
|
||||||
' data-date="' + esc(d) + '">' +
|
' data-date="' + esc(d) + '">' +
|
||||||
'<strong>' + esc(d || "날짜 미확인") + "</strong>" +
|
'<strong>' + esc(d || "날짜 미확인") + "</strong>" +
|
||||||
'<span>센터 ' + cn + "곳 · " + bx + "박스</span>" +
|
'<span>센터 ' + cn + "곳 · " + bx + "상자</span>" +
|
||||||
"</button>";
|
"</button>";
|
||||||
}).join("");
|
}).join("");
|
||||||
}
|
}
|
||||||
@@ -464,8 +464,8 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// 센터별 계산 결과를 하나의 ② 요약으로 합치고, 각 박스를 그 센터에 자동 배분한다.
|
// 센터별 계산 결과를 하나의 ② 요약으로 합치고, 각 상자를 그 센터에 자동 배분한다.
|
||||||
// (한 출고일 안에서만 합친다 — 박스는 센터 단위로 포장)
|
// (한 출고일 안에서만 합친다 — 상자는 센터 단위로 포장)
|
||||||
function buildDataset(groups, perCenter) {
|
function buildDataset(groups, perCenter) {
|
||||||
var mergedProds = {}; // code -> 합산 결과
|
var mergedProds = {}; // code -> 합산 결과
|
||||||
var mergedMixes = {}; // box_name -> {box_name, boxes:[]}
|
var mergedMixes = {}; // box_name -> {box_name, boxes:[]}
|
||||||
@@ -603,7 +603,7 @@
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// 날짜 × 센터 단위로 박스 계산 (박스는 센터별로 포장되므로)
|
// 날짜 × 센터 단위로 상자 계산 (상자는 센터별로 포장되므로)
|
||||||
var jobs = [];
|
var jobs = [];
|
||||||
dateGroups.forEach(function (g) {
|
dateGroups.forEach(function (g) {
|
||||||
g.centers.forEach(function (c) {
|
g.centers.forEach(function (c) {
|
||||||
@@ -663,7 +663,7 @@
|
|||||||
var hit = calc.results.filter(function (r) { return r.product_code === code; })[0];
|
var hit = calc.results.filter(function (r) { return r.product_code === code; })[0];
|
||||||
return hit ? hit.full_boxes : 0;
|
return hit ? hit.full_boxes : 0;
|
||||||
}
|
}
|
||||||
return 1; // 혼합 박스는 1장 = 1박스
|
return 1; // 혼합 상자는 1장 = 1상자
|
||||||
}
|
}
|
||||||
|
|
||||||
function remainFor(key) { return Math.max(0, totalFor(key) - allocatedFor(key)); }
|
function remainFor(key) { return Math.max(0, totalFor(key) - allocatedFor(key)); }
|
||||||
@@ -736,7 +736,7 @@
|
|||||||
});
|
});
|
||||||
var assigned = (totalProd + totalMix) - (remainProd + remainMix);
|
var assigned = (totalProd + totalMix) - (remainProd + remainMix);
|
||||||
|
|
||||||
// 모든 박스를 센터에 배분하면 "센터 배분" 칸에 전체 완료 도장을 찍는다.
|
// 모든 상자를 센터에 배분하면 "센터 배분" 칸에 전체 완료 도장을 찍는다.
|
||||||
var totalBoxes = totalProd + totalMix;
|
var totalBoxes = totalProd + totalMix;
|
||||||
var allDone = totalBoxes > 0 && (remainProd + remainMix) === 0;
|
var allDone = totalBoxes > 0 && (remainProd + remainMix) === 0;
|
||||||
var allStamp = "";
|
var allStamp = "";
|
||||||
@@ -747,9 +747,9 @@
|
|||||||
allStamped = allDone;
|
allStamped = allDone;
|
||||||
|
|
||||||
sumKpis.innerHTML =
|
sumKpis.innerHTML =
|
||||||
kpi("총 박스", totalBoxes + "박스", "제품별 " + totalProd + " + 혼합 " + totalMix) +
|
kpi("총 상자", totalBoxes + "상자", "제품별 " + totalProd + " + 혼합 " + totalMix) +
|
||||||
kpi("센터 배분", assigned + "박스", "③ 에 담긴 수량", allStamp) +
|
kpi("센터 배분", assigned + "상자", "③ 에 담긴 수량", allStamp) +
|
||||||
kpi("미배분 잔여", (remainProd + remainMix) + "박스", "자투리 " + leftoverUnits + "개 포함");
|
kpi("미배분 잔여", (remainProd + remainMix) + "상자", "자투리 " + leftoverUnits + "개 포함");
|
||||||
|
|
||||||
var ph = "";
|
var ph = "";
|
||||||
calc.results.forEach(function (r) {
|
calc.results.forEach(function (r) {
|
||||||
@@ -764,8 +764,8 @@
|
|||||||
(remain ? "" : stampHtml(key)) +
|
(remain ? "" : stampHtml(key)) +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
'<div class="cpg-sum-prod-nums">' +
|
'<div class="cpg-sum-prod-nums">' +
|
||||||
"<strong>" + remain + "박스</strong>" +
|
"<strong>" + remain + "상자</strong>" +
|
||||||
'<span class="erp-muted">' + (remain * r.units_per_box) + "개 · 총 " + r.full_boxes + "박스" +
|
'<span class="erp-muted">' + (remain * r.units_per_box) + "개 · 총 " + r.full_boxes + "상자" +
|
||||||
(used ? " · 배분 " + used : "") + "</span>" +
|
(used ? " · 배분 " + used : "") + "</span>" +
|
||||||
(r.remainder_units
|
(r.remainder_units
|
||||||
? '<span class="cpg-sum-left">자투리 ' + r.remainder_units + "개</span>"
|
? '<span class="cpg-sum-left">자투리 ' + r.remainder_units + "개</span>"
|
||||||
@@ -779,7 +779,7 @@
|
|||||||
sumMixNote.textContent = "— 자투리 없음";
|
sumMixNote.textContent = "— 자투리 없음";
|
||||||
sumBoxes.innerHTML = '<p class="erp-muted">남는 낱개가 없습니다.</p>';
|
sumBoxes.innerHTML = '<p class="erp-muted">남는 낱개가 없습니다.</p>';
|
||||||
} else {
|
} else {
|
||||||
sumMixNote.textContent = "— 1장 = 1박스";
|
sumMixNote.textContent = "— 1장 = 1상자";
|
||||||
var bh = "", n = 0;
|
var bh = "", n = 0;
|
||||||
calc.mixes.forEach(function (m) {
|
calc.mixes.forEach(function (m) {
|
||||||
m.boxes.forEach(function (b, i) {
|
m.boxes.forEach(function (b, i) {
|
||||||
@@ -809,14 +809,14 @@
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 혼합 박스는 안에 무엇이 몇 개 들어가는지 트리로 펼쳐 보여준다.
|
// 혼합 상자는 안에 무엇이 몇 개 들어가는지 트리로 펼쳐 보여준다.
|
||||||
function treeHtml(entry) {
|
function treeHtml(entry) {
|
||||||
var list = contents[entry.key];
|
var list = contents[entry.key];
|
||||||
if (!list || !list.length) return "";
|
if (!list || !list.length) return "";
|
||||||
return '<ul class="cpg-dist-tree">' + list.map(function (it) {
|
return '<ul class="cpg-dist-tree">' + list.map(function (it) {
|
||||||
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
return '<li><span class="cpg-tree-name">' + esc(it.product_name) + "</span>" +
|
||||||
'<span class="cpg-tree-qty">' + (it.quantity * entry.count) + "개</span>" +
|
'<span class="cpg-tree-qty">' + (it.quantity * entry.count) + "개</span>" +
|
||||||
(entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "박스)</span>" : "") +
|
(entry.count > 1 ? '<span class="cpg-tree-per">(' + it.quantity + "개 × " + entry.count + "상자)</span>" : "") +
|
||||||
"</li>";
|
"</li>";
|
||||||
}).join("") + "</ul>";
|
}).join("") + "</ul>";
|
||||||
}
|
}
|
||||||
@@ -838,7 +838,7 @@
|
|||||||
'<div class="cpg-dist-line">' +
|
'<div class="cpg-dist-line">' +
|
||||||
'<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) + "</span>" +
|
'<span class="cpg-dist-name">' + esc(labels[a.key] || a.key) + "</span>" +
|
||||||
'<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' +
|
'<input class="erp-input cpg-dist-qty" type="number" min="1" max="999" step="1" inputmode="numeric" value="' + a.count + '" />' +
|
||||||
'<span class="cpg-dist-unit">박스 · ' + (a.count * per) + "개</span>" +
|
'<span class="cpg-dist-unit">상자 · ' + (a.count * per) + "개</span>" +
|
||||||
'<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' +
|
'<button type="button" class="erp-btn erp-btn-danger cpg-dist-del" title="빼기" aria-label="빼기">✕</button>' +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
treeHtml(a) +
|
treeHtml(a) +
|
||||||
@@ -855,14 +855,14 @@
|
|||||||
'<option value="파렛트"' + (method === "파렛트" ? " selected" : "") + '>파렛트</option>' +
|
'<option value="파렛트"' + (method === "파렛트" ? " selected" : "") + '>파렛트</option>' +
|
||||||
"</select>" +
|
"</select>" +
|
||||||
'<span class="cpg-dist-sum">' +
|
'<span class="cpg-dist-sum">' +
|
||||||
'<span class="erp-badge erp-badge-neutral">' + boxes + "박스</span>" +
|
'<span class="erp-badge erp-badge-neutral">' + boxes + "상자</span>" +
|
||||||
'<span class="erp-badge erp-badge-neutral">' + pieces + "개</span>" +
|
'<span class="erp-badge erp-badge-neutral">' + pieces + "개</span>" +
|
||||||
'<button type="button" class="erp-btn erp-btn-outline cpg-dist-close" title="센터 빼기" aria-label="센터 빼기">✕</button>' +
|
'<button type="button" class="erp-btn erp-btn-outline cpg-dist-close" title="센터 빼기" aria-label="센터 빼기">✕</button>' +
|
||||||
"</span>" +
|
"</span>" +
|
||||||
"</div>" +
|
"</div>" +
|
||||||
(rows.length
|
(rows.length
|
||||||
? '<ul class="cpg-dist-items">' + items + "</ul>"
|
? '<ul class="cpg-dist-items">' + items + "</ul>"
|
||||||
: '<p class="cpg-dist-hint">여기로 박스를 끌어다 놓기</p>') +
|
: '<p class="cpg-dist-hint">여기로 상자를 끌어다 놓기</p>') +
|
||||||
"</div>";
|
"</div>";
|
||||||
});
|
});
|
||||||
distList.innerHTML = html;
|
distList.innerHTML = html;
|
||||||
@@ -875,15 +875,15 @@
|
|||||||
function openDialog(key, centerId) {
|
function openDialog(key, centerId) {
|
||||||
if (!openCenters.length) { msg.textContent = "먼저 ③ 에서 센터를 추가하세요."; return; }
|
if (!openCenters.length) { msg.textContent = "먼저 ③ 에서 센터를 추가하세요."; return; }
|
||||||
var remain = remainFor(key);
|
var remain = remainFor(key);
|
||||||
if (remain <= 0) { msg.textContent = "남은 박스가 없습니다."; return; }
|
if (remain <= 0) { msg.textContent = "남은 상자가 없습니다."; return; }
|
||||||
|
|
||||||
pending = { key: key };
|
pending = { key: key };
|
||||||
dlgItem.textContent = (labels[key] || key) + " — 잔여 " + remain + "박스 (" + (remain * (units[key] || 0)) + "개)";
|
dlgItem.textContent = (labels[key] || key) + " — 잔여 " + remain + "상자 (" + (remain * (units[key] || 0)) + "개)";
|
||||||
dlgCenterId = String(centerId != null ? centerId : openCenters[0]);
|
dlgCenterId = String(centerId != null ? centerId : openCenters[0]);
|
||||||
renderDialogCenters();
|
renderDialogCenters();
|
||||||
dlgQty.max = remain;
|
dlgQty.max = remain;
|
||||||
dlgQty.value = key.indexOf("m:") === 0 ? 1 : remain;
|
dlgQty.value = key.indexOf("m:") === 0 ? 1 : remain;
|
||||||
dlgHint.textContent = "1박스 = " + (units[key] || 0) + "개 · 최대 " + remain + "박스";
|
dlgHint.textContent = "1상자 = " + (units[key] || 0) + "개 · 최대 " + remain + "상자";
|
||||||
dlg.hidden = false;
|
dlg.hidden = false;
|
||||||
dlgQty.focus();
|
dlgQty.focus();
|
||||||
dlgQty.select();
|
dlgQty.select();
|
||||||
@@ -916,7 +916,7 @@
|
|||||||
closeDialog();
|
closeDialog();
|
||||||
render();
|
render();
|
||||||
msg.textContent = got
|
msg.textContent = got
|
||||||
? centerName(cid) + " ← " + (labels[key] || key) + " " + got + "박스"
|
? centerName(cid) + " ← " + (labels[key] || key) + " " + got + "상자"
|
||||||
: "담을 수량이 없습니다.";
|
: "담을 수량이 없습니다.";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1069,7 +1069,7 @@
|
|||||||
var max = entry.count + remainFor(entry.key);
|
var max = entry.count + remainFor(entry.key);
|
||||||
if (want > max) { want = max; e.target.value = max; }
|
if (want > max) { want = max; e.target.value = max; }
|
||||||
entry.count = want;
|
entry.count = want;
|
||||||
li.querySelector(".cpg-dist-unit").textContent = "박스 · " + (want * (units[entry.key] || 0)) + "개";
|
li.querySelector(".cpg-dist-unit").textContent = "상자 · " + (want * (units[entry.key] || 0)) + "개";
|
||||||
var tree = li.querySelector(".cpg-dist-tree");
|
var tree = li.querySelector(".cpg-dist-tree");
|
||||||
if (tree) tree.outerHTML = treeHtml(entry);
|
if (tree) tree.outerHTML = treeHtml(entry);
|
||||||
renderSummary();
|
renderSummary();
|
||||||
@@ -1105,7 +1105,7 @@
|
|||||||
pieces += a.count * (units[a.key] || 0);
|
pieces += a.count * (units[a.key] || 0);
|
||||||
});
|
});
|
||||||
var badges = zone.querySelectorAll(".cpg-dist-sum .erp-badge");
|
var badges = zone.querySelectorAll(".cpg-dist-sum .erp-badge");
|
||||||
if (badges[0]) badges[0].textContent = boxes + "박스";
|
if (badges[0]) badges[0].textContent = boxes + "상자";
|
||||||
if (badges[1]) badges[1].textContent = pieces + "개";
|
if (badges[1]) badges[1].textContent = pieces + "개";
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1203,9 +1203,9 @@
|
|||||||
function confirmBlockReason() {
|
function confirmBlockReason() {
|
||||||
var t = boxTotals();
|
var t = boxTotals();
|
||||||
if (!t.total) return "먼저 ① 에서 계산하세요.";
|
if (!t.total) return "먼저 ① 에서 계산하세요.";
|
||||||
if (t.remain) return "아직 " + t.remain + "박스가 남았습니다.";
|
if (t.remain) return "아직 " + t.remain + "상자가 남았습니다.";
|
||||||
var cids = filledCenters();
|
var cids = filledCenters();
|
||||||
if (!cids.length) return "센터에 담긴 박스가 없습니다.";
|
if (!cids.length) return "센터에 담긴 상자가 없습니다.";
|
||||||
var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
|
var missing = cids.filter(function (cid) { return !methods[String(cid)]; });
|
||||||
if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
|
if (missing.length) return "출고방식 미선택: " + missing.map(centerName).join(", ");
|
||||||
return "";
|
return "";
|
||||||
@@ -1218,7 +1218,7 @@
|
|||||||
confirmBtn.title = why || "출고일자를 골라 출고 묶음을 만듭니다.";
|
confirmBtn.title = why || "출고일자를 골라 출고 묶음을 만듭니다.";
|
||||||
}
|
}
|
||||||
|
|
||||||
// 센터별 전송 payload — 혼합 박스는 내용물 제품으로 풀어서 담는다.
|
// 센터별 전송 payload — 혼합 상자는 내용물 제품으로 풀어서 담는다.
|
||||||
function buildPlans() {
|
function buildPlans() {
|
||||||
return filledCenters().map(function (cid) {
|
return filledCenters().map(function (cid) {
|
||||||
var rows = alloc[cid] || [];
|
var rows = alloc[cid] || [];
|
||||||
@@ -1234,7 +1234,7 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
// 출고 엑셀 "출고" 칸 — 상자 종류별 박스 수 (예: "쿠팡상자, 41개")
|
// 출고 엑셀 "출고" 칸 — 상자 종류별 상자 수 (예: "쿠팡상자, 41개")
|
||||||
// 종류가 여럿이면 " / " 로 잇는다 (예: "쿠팡상자, 41개 / 3호상자, 2개")
|
// 종류가 여럿이면 " / " 로 잇는다 (예: "쿠팡상자, 41개 / 3호상자, 2개")
|
||||||
var byType = {};
|
var byType = {};
|
||||||
var order = [];
|
var order = [];
|
||||||
@@ -1329,7 +1329,7 @@
|
|||||||
var pieces = pl.items.reduce(function (n, it) { return n + it.quantity; }, 0);
|
var pieces = pl.items.reduce(function (n, it) { return n + it.quantity; }, 0);
|
||||||
return '<div class="cpg-cfm-row">' +
|
return '<div class="cpg-cfm-row">' +
|
||||||
'<span class="cpg-cfm-cname">' + esc(centerName(pl.center_id)) + "</span>" +
|
'<span class="cpg-cfm-cname">' + esc(centerName(pl.center_id)) + "</span>" +
|
||||||
'<span class="cpg-cfm-cmeta">' + esc(pl.ship_method) + " · " + pl.boxes + "박스 · " + pieces + "개</span>" +
|
'<span class="cpg-cfm-cmeta">' + esc(pl.ship_method) + " · " + pl.boxes + "상자 · " + pieces + "개</span>" +
|
||||||
"</div>";
|
"</div>";
|
||||||
}).join("");
|
}).join("");
|
||||||
renderCal();
|
renderCal();
|
||||||
@@ -1565,7 +1565,7 @@
|
|||||||
if (!items.length) addRow("", 0);
|
if (!items.length) addRow("", 0);
|
||||||
draftName.value = d.title || "";
|
draftName.value = d.title || "";
|
||||||
closeDraft();
|
closeDraft();
|
||||||
// 박스 수는 서버에서 다시 계산한 뒤 분배 내역을 얹는다.
|
// 상자 수는 서버에서 다시 계산한 뒤 분배 내역을 얹는다.
|
||||||
run(snap);
|
run(snap);
|
||||||
})
|
})
|
||||||
.catch(function () { draftMsg.textContent = "불러오기 실패 — 다시 시도하세요."; });
|
.catch(function () { draftMsg.textContent = "불러오기 실패 — 다시 시도하세요."; });
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -14,7 +14,7 @@
|
|||||||
<!-- 왼쪽: 추가/수정 (product_code UNIQUE → upsert) -->
|
<!-- 왼쪽: 추가/수정 (product_code UNIQUE → upsert) -->
|
||||||
<div class="erp-card cpg-form-card cpg-brule-add">
|
<div class="erp-card cpg-form-card cpg-brule-add">
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>박스 입수량 추가 / 수정</h2>
|
<h2>상자 입수량 추가 / 수정</h2>
|
||||||
<span class="erp-muted">같은 제품코드는 덮어씁니다.</span>
|
<span class="erp-muted">같은 제품코드는 덮어씁니다.</span>
|
||||||
</div>
|
</div>
|
||||||
<form method="post" action="/cupang/box-rules">
|
<form method="post" action="/cupang/box-rules">
|
||||||
@@ -32,9 +32,9 @@
|
|||||||
<input class="erp-input cpg-brule-code" type="text" name="product_code" id="brule-code" required placeholder="자동" /></label>
|
<input class="erp-input cpg-brule-code" type="text" name="product_code" id="brule-code" required placeholder="자동" /></label>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-brule-row">
|
<div class="cpg-brule-row">
|
||||||
<label class="erp-field"><span>박스이름</span>
|
<label class="erp-field"><span>상자이름</span>
|
||||||
<input class="erp-input cpg-brule-box" type="text" name="box_name" value="쿠팡박스" /></label>
|
<input class="erp-input cpg-brule-box" type="text" name="box_name" value="쿠팡상자" /></label>
|
||||||
<label class="erp-field"><span>박스당 입수량 *</span>
|
<label class="erp-field"><span>상자당 입수량 *</span>
|
||||||
<span class="cpg-upb-wrap">
|
<span class="cpg-upb-wrap">
|
||||||
<input class="erp-input cpg-brule-upb" type="number" name="units_per_box" min="1" required />
|
<input class="erp-input cpg-brule-upb" type="number" name="units_per_box" min="1" required />
|
||||||
<span class="cpg-upb-unit">개</span>
|
<span class="cpg-upb-unit">개</span>
|
||||||
@@ -74,7 +74,7 @@
|
|||||||
<div class="erp-table-wrap">
|
<div class="erp-table-wrap">
|
||||||
<table class="erp-table">
|
<table class="erp-table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr><th>제품명</th><th>제품코드</th><th>박스명</th><th>입수량</th><th>메모</th><th>동작</th></tr>
|
<tr><th>제품명</th><th>제품코드</th><th>상자명</th><th>입수량</th><th>메모</th><th>동작</th></tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
{% for r in box_rules %}
|
{% for r in box_rules %}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
<span class="cpg-settings-btns">
|
<span class="cpg-settings-btns">
|
||||||
<a class="erp-btn erp-btn-outline" href="/cupang/products">제품명 설정</a>
|
<a class="erp-btn erp-btn-outline" href="/cupang/products">제품명 설정</a>
|
||||||
<a class="erp-btn erp-btn-outline" href="/cupang/centers">입고센터 관리</a>
|
<a class="erp-btn erp-btn-outline" href="/cupang/centers">입고센터 관리</a>
|
||||||
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">박스 입수량 설정</a>
|
<a class="erp-btn erp-btn-outline" href="/cupang/box-rules">상자 입수량 설정</a>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-actions-spacer"></div>
|
<div class="cpg-actions-spacer"></div>
|
||||||
@@ -97,7 +97,7 @@
|
|||||||
<a class="cpg-ship-name" href="/cupang/{{ s.id }}">{{ s.center_name_snapshot or '센터 미지정' }}</a>
|
<a class="cpg-ship-name" href="/cupang/{{ s.id }}">{{ s.center_name_snapshot or '센터 미지정' }}</a>
|
||||||
<span class="erp-badge erp-badge-neutral">{{ s.ship_method }}</span>
|
<span class="erp-badge erp-badge-neutral">{{ s.ship_method }}</span>
|
||||||
<span class="cpg-dist-sum">
|
<span class="cpg-dist-sum">
|
||||||
<span class="erp-badge erp-badge-neutral">{{ s.total_boxes }}박스</span>
|
<span class="erp-badge erp-badge-neutral">{{ s.total_boxes }}상자</span>
|
||||||
<span class="erp-badge erp-badge-neutral">{{ s.total_qty }}개</span>
|
<span class="erp-badge erp-badge-neutral">{{ s.total_qty }}개</span>
|
||||||
<button type="button" class="cpg-icon-btn is-danger" title="이 출고 삭제"
|
<button type="button" class="cpg-icon-btn is-danger" title="이 출고 삭제"
|
||||||
data-del="{{ s.id }}" data-name="{{ s.center_name_snapshot }}">삭제</button>
|
data-del="{{ s.id }}" data-name="{{ s.center_name_snapshot }}">삭제</button>
|
||||||
@@ -110,7 +110,7 @@
|
|||||||
<li class="cpg-dist-item{% if e.kind == 'mix' %} is-mix{% endif %}">
|
<li class="cpg-dist-item{% if e.kind == 'mix' %} is-mix{% endif %}">
|
||||||
<div class="cpg-dist-line">
|
<div class="cpg-dist-line">
|
||||||
<span class="cpg-dist-name">{{ e.name }}</span>
|
<span class="cpg-dist-name">{{ e.name }}</span>
|
||||||
<span class="cpg-dist-unit">{{ e.count }}박스 · {{ e.quantity }}개</span>
|
<span class="cpg-dist-unit">{{ e.count }}상자 · {{ e.quantity }}개</span>
|
||||||
</div>
|
</div>
|
||||||
{% if e.kind == 'mix' and e['items'] %}
|
{% if e.kind == 'mix' and e['items'] %}
|
||||||
<ul class="cpg-dist-tree">
|
<ul class="cpg-dist-tree">
|
||||||
@@ -123,12 +123,12 @@
|
|||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% else %}
|
{% else %}
|
||||||
{# 예전에 만든 출고(박스 구성 미저장) — 제품 합계만 표시 #}
|
{# 예전에 만든 출고(상자 구성 미저장) — 제품 합계만 표시 #}
|
||||||
{% for it in s['items'] %}
|
{% for it in s['items'] %}
|
||||||
<li class="cpg-dist-item">
|
<li class="cpg-dist-item">
|
||||||
<div class="cpg-dist-line">
|
<div class="cpg-dist-line">
|
||||||
<span class="cpg-dist-name">{{ it.name }}</span>
|
<span class="cpg-dist-name">{{ it.name }}</span>
|
||||||
<span class="cpg-dist-unit">{{ it.boxes }}박스 · {{ it.qty }}개</span>
|
<span class="cpg-dist-unit">{{ it.boxes }}상자 · {{ it.qty }}개</span>
|
||||||
</div>
|
</div>
|
||||||
</li>
|
</li>
|
||||||
{% endfor %}
|
{% endfor %}
|
||||||
@@ -141,7 +141,7 @@
|
|||||||
</ul>
|
</ul>
|
||||||
{% else %}
|
{% else %}
|
||||||
<p class="erp-muted cpg-empty">선택한 날짜의 출고 묶음이 없습니다.
|
<p class="erp-muted cpg-empty">선택한 날짜의 출고 묶음이 없습니다.
|
||||||
<a href="/cupang/box-calc">박스 계산에서 등록</a></p>
|
<a href="/cupang/box-calc">상자 계산에서 등록</a></p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
{% extends "erp_base.html" %}
|
{% extends "erp_base.html" %}
|
||||||
|
|
||||||
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901n" />{% endblock %}
|
{% block head_extra %}<link rel="stylesheet" href="/static/cupang.css?v=20260901o" />{% endblock %}
|
||||||
|
|
||||||
{% block content %}
|
{% block content %}
|
||||||
{# 출고 묶음 보기 — 박스 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
{# 출고 묶음 보기 — 상자 계산 화면과 같은 3열 구성. 읽기 전용(수정 없음). #}
|
||||||
<section class="cpg">
|
<section class="cpg">
|
||||||
|
|
||||||
<div class="erp-page-actions">
|
<div class="erp-page-actions">
|
||||||
@@ -44,18 +44,18 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- ② 박스 요약 -->
|
<!-- ② 상자 요약 -->
|
||||||
<div class="erp-card cpg-form-card cpg-calc-sum">
|
<div class="erp-card cpg-form-card cpg-calc-sum">
|
||||||
<div class="cpg-card-head">
|
<div class="cpg-card-head">
|
||||||
<h2>② 박스 요약</h2>
|
<h2>② 상자 요약</h2>
|
||||||
<span class="erp-muted">확정 당시 수량 기준으로 다시 계산한 값</span>
|
<span class="erp-muted">확정 당시 수량 기준으로 다시 계산한 값</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div id="cpg-sum-body">
|
<div id="cpg-sum-body">
|
||||||
<div class="cpg-sum-kpis">
|
<div class="cpg-sum-kpis">
|
||||||
<div class="cpg-kpi">
|
<div class="cpg-kpi">
|
||||||
<span class="cpg-kpi-label">총 박스</span>
|
<span class="cpg-kpi-label">총 상자</span>
|
||||||
<strong class="cpg-kpi-value">{{ calc.grand_total_boxes }}박스</strong>
|
<strong class="cpg-kpi-value">{{ calc.grand_total_boxes }}상자</strong>
|
||||||
<span class="cpg-kpi-hint">제품별 + 혼합</span>
|
<span class="cpg-kpi-hint">제품별 + 혼합</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-kpi">
|
<div class="cpg-kpi">
|
||||||
@@ -71,7 +71,7 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="cpg-sum-scroll">
|
<div class="cpg-sum-scroll">
|
||||||
<h3 class="cpg-sum-h3">제품별 박스</h3>
|
<h3 class="cpg-sum-h3">제품별 상자</h3>
|
||||||
<div class="cpg-sum-prods">
|
<div class="cpg-sum-prods">
|
||||||
{% for r in calc.results if r.configured %}
|
{% for r in calc.results if r.configured %}
|
||||||
<div class="cpg-sum-prod is-done">
|
<div class="cpg-sum-prod is-done">
|
||||||
@@ -79,7 +79,7 @@
|
|||||||
<span class="cpg-sum-prod-name">{{ r.product_name }}</span>
|
<span class="cpg-sum-prod-name">{{ r.product_name }}</span>
|
||||||
</div>
|
</div>
|
||||||
<div class="cpg-sum-prod-nums">
|
<div class="cpg-sum-prod-nums">
|
||||||
<strong>{{ r.full_boxes }}박스</strong>
|
<strong>{{ r.full_boxes }}상자</strong>
|
||||||
<span class="erp-muted">{{ r.quantity }}개 · 입수량 {{ r.units_per_box }}</span>
|
<span class="erp-muted">{{ r.quantity }}개 · 입수량 {{ r.units_per_box }}</span>
|
||||||
{% if r.remainder_units %}
|
{% if r.remainder_units %}
|
||||||
<span class="cpg-sum-left">자투리 {{ r.remainder_units }}개</span>
|
<span class="cpg-sum-left">자투리 {{ r.remainder_units }}개</span>
|
||||||
@@ -91,14 +91,14 @@
|
|||||||
{% endfor %}
|
{% endfor %}
|
||||||
{% set unconfigured = calc.results | rejectattr('configured') | list %}
|
{% set unconfigured = calc.results | rejectattr('configured') | list %}
|
||||||
{% if unconfigured %}
|
{% if unconfigured %}
|
||||||
<p class="erp-muted">박스 입수량 미설정:
|
<p class="erp-muted">상자 입수량 미설정:
|
||||||
{% for r in unconfigured %}{{ r.product_name }}{% if not loop.last %}, {% endif %}{% endfor %}
|
{% for r in unconfigured %}{{ r.product_name }}{% if not loop.last %}, {% endif %}{% endfor %}
|
||||||
</p>
|
</p>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<h3 class="cpg-sum-h3">자투리 혼합 박스
|
<h3 class="cpg-sum-h3">자투리 혼합 상자
|
||||||
<span class="erp-muted">{% if not calc.mixes %}— 자투리 없음{% else %}— 1장 = 1박스{% endif %}</span>
|
<span class="erp-muted">{% if not calc.mixes %}— 자투리 없음{% else %}— 1장 = 1상자{% endif %}</span>
|
||||||
</h3>
|
</h3>
|
||||||
<div class="cpg-sum-boxes">
|
<div class="cpg-sum-boxes">
|
||||||
{% for m in calc.mixes %}
|
{% for m in calc.mixes %}
|
||||||
@@ -135,7 +135,7 @@
|
|||||||
<strong>{{ shipment.center_name_snapshot or '센터 미지정' }}</strong>
|
<strong>{{ shipment.center_name_snapshot or '센터 미지정' }}</strong>
|
||||||
<span class="erp-badge erp-badge-neutral">{{ shipment.ship_method }}</span>
|
<span class="erp-badge erp-badge-neutral">{{ shipment.ship_method }}</span>
|
||||||
<span class="cpg-dist-sum">
|
<span class="cpg-dist-sum">
|
||||||
<span class="erp-badge erp-badge-neutral">{{ calc.grand_total_boxes }}박스</span>
|
<span class="erp-badge erp-badge-neutral">{{ calc.grand_total_boxes }}상자</span>
|
||||||
<span class="erp-badge erp-badge-neutral">{{ total_qty }}개</span>
|
<span class="erp-badge erp-badge-neutral">{{ total_qty }}개</span>
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -145,7 +145,7 @@
|
|||||||
<li class="cpg-dist-item{% if e.kind == 'mix' %} is-mix{% endif %}">
|
<li class="cpg-dist-item{% if e.kind == 'mix' %} is-mix{% endif %}">
|
||||||
<div class="cpg-dist-line">
|
<div class="cpg-dist-line">
|
||||||
<span class="cpg-dist-name">{{ e.name }}</span>
|
<span class="cpg-dist-name">{{ e.name }}</span>
|
||||||
<span class="cpg-dist-unit">{{ e.count }}박스 · {{ e.quantity }}개</span>
|
<span class="cpg-dist-unit">{{ e.count }}상자 · {{ e.quantity }}개</span>
|
||||||
</div>
|
</div>
|
||||||
{% if e.kind == 'mix' and e['items'] %}
|
{% if e.kind == 'mix' and e['items'] %}
|
||||||
<ul class="cpg-dist-tree">
|
<ul class="cpg-dist-tree">
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
-- 쿠팡 밀크런 — 박스 입수량 규칙 초기 입력 (cupang_db)
|
-- 쿠팡 밀크런 — 상자 입수량 규칙 초기 입력 (cupang_db)
|
||||||
--
|
--
|
||||||
-- 실행:
|
-- 실행:
|
||||||
-- docker exec -i postgres-db psql -U postgres -d cupang_db \
|
-- docker exec -i postgres-db psql -U postgres -d cupang_db \
|
||||||
@@ -14,20 +14,20 @@ BEGIN;
|
|||||||
INSERT INTO cupang_box_rules
|
INSERT INTO cupang_box_rules
|
||||||
(product_code, product_name_snapshot, box_name, units_per_box, memo, active)
|
(product_code, product_name_snapshot, box_name, units_per_box, memo, active)
|
||||||
VALUES
|
VALUES
|
||||||
('MS-1001', '미라네 1호 세트', '쿠팡박스', 8, '피킹비 750원', TRUE),
|
('MS-1001', '미라네 1호 세트', '쿠팡상자', 8, '피킹비 750원', TRUE),
|
||||||
('MS-1002', '미라네 2호 세트', '쿠팡박스', 8, '피킹비 1100원', TRUE),
|
('MS-1002', '미라네 2호 세트', '쿠팡상자', 8, '피킹비 1100원', TRUE),
|
||||||
('MS-1003', '미라네 3호 세트', '쿠팡박스', 8, '피킹비 1200원', TRUE),
|
('MS-1003', '미라네 3호 세트', '쿠팡상자', 8, '피킹비 1200원', TRUE),
|
||||||
('MS-1004', '미라네 4호 세트', '쿠팡박스', 8, '피킹비 1000원', TRUE),
|
('MS-1004', '미라네 4호 세트', '쿠팡상자', 8, '피킹비 1000원', TRUE),
|
||||||
('MS-1005', '미라네 5호 세트', '쿠팡박스', 8, '피킹비 650원', TRUE),
|
('MS-1005', '미라네 5호 세트', '쿠팡상자', 8, '피킹비 650원', TRUE),
|
||||||
('MS-1006', '미라네 6호 세트', '쿠팡박스', 8, '피킹비 750원', TRUE),
|
('MS-1006', '미라네 6호 세트', '쿠팡상자', 8, '피킹비 750원', TRUE),
|
||||||
('MS-1007', '미라네 7호 세트', '쿠팡박스', 8, '피킹비 850원', TRUE),
|
('MS-1007', '미라네 7호 세트', '쿠팡상자', 8, '피킹비 850원', TRUE),
|
||||||
('MS-9812', '황토 김치통 1호세트', '쿠팡박스', 8, '피킹비 750원', TRUE),
|
('MS-9812', '황토 김치통 1호세트', '쿠팡상자', 8, '피킹비 750원', TRUE),
|
||||||
('MS-9813', '황토 김치통 2호세트', '쿠팡박스', 8, '피킹비 650원', TRUE),
|
('MS-9813', '황토 김치통 2호세트', '쿠팡상자', 8, '피킹비 650원', TRUE),
|
||||||
('MS-9814', '황토 김치통 3호세트', '쿠팡박스', 8, '피킹비 750원', TRUE),
|
('MS-9814', '황토 김치통 3호세트', '쿠팡상자', 8, '피킹비 750원', TRUE),
|
||||||
('MS-9815', '황토 김치통 4호세트', '쿠팡박스', 2, '피킹비 850원', TRUE),
|
('MS-9815', '황토 김치통 4호세트', '쿠팡상자', 2, '피킹비 850원', TRUE),
|
||||||
('MS-9816', '황토 김치통 5호세트', '쿠팡박스', 2, '피킹비 900원', TRUE),
|
('MS-9816', '황토 김치통 5호세트', '쿠팡상자', 2, '피킹비 900원', TRUE),
|
||||||
('MS-9817', '황토 김치통 6호세트', '쿠팡박스', 1, '피킹비 1000원', TRUE),
|
('MS-9817', '황토 김치통 6호세트', '쿠팡상자', 1, '피킹비 1000원', TRUE),
|
||||||
('MS-9818', '황토 김치통 7호세트', '쿠팡박스', 1, '피킹비 1100원', TRUE)
|
('MS-9818', '황토 김치통 7호세트', '쿠팡상자', 1, '피킹비 1100원', TRUE)
|
||||||
ON CONFLICT (product_code) DO UPDATE
|
ON CONFLICT (product_code) DO UPDATE
|
||||||
SET product_name_snapshot = EXCLUDED.product_name_snapshot,
|
SET product_name_snapshot = EXCLUDED.product_name_snapshot,
|
||||||
box_name = EXCLUDED.box_name,
|
box_name = EXCLUDED.box_name,
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
-- 쿠팡 밀크런 — 박스 입수량 규칙 2차 입력 (cupang_db)
|
-- 쿠팡 밀크런 — 상자 입수량 규칙 2차 입력 (cupang_db)
|
||||||
-- 미라클통/미라클백/빠져락/멀티/점보 19건
|
-- 미라클통/미라클백/빠져락/멀티/점보 19건
|
||||||
--
|
--
|
||||||
-- 실행:
|
-- 실행:
|
||||||
@@ -15,7 +15,7 @@
|
|||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
BEGIN;
|
BEGIN;
|
||||||
|
|
||||||
-- box_no: 원자료의 박스 호수. 참고용 컬럼이며 memo 에는 넣지 않는다(피킹비만 기록).
|
-- box_no: 원자료의 상자 호수. 참고용 컬럼이며 memo 에는 넣지 않는다(피킹비만 기록).
|
||||||
CREATE TEMP TABLE _seed2 (
|
CREATE TEMP TABLE _seed2 (
|
||||||
product_name TEXT,
|
product_name TEXT,
|
||||||
picking_fee INTEGER,
|
picking_fee INTEGER,
|
||||||
@@ -48,7 +48,7 @@ INSERT INTO cupang_box_rules
|
|||||||
(product_code, product_name_snapshot, box_name, units_per_box, memo, active)
|
(product_code, product_name_snapshot, box_name, units_per_box, memo, active)
|
||||||
SELECT p.product_code,
|
SELECT p.product_code,
|
||||||
p.product_name,
|
p.product_name,
|
||||||
'쿠팡박스',
|
'쿠팡상자',
|
||||||
s.units_per_box,
|
s.units_per_box,
|
||||||
'피킹비 ' || s.picking_fee || '원',
|
'피킹비 ' || s.picking_fee || '원',
|
||||||
TRUE
|
TRUE
|
||||||
@@ -66,7 +66,7 @@ SET product_name_snapshot = EXCLUDED.product_name_snapshot,
|
|||||||
-- 카탈로그에 없어 입력되지 않은 제품 (수기 추가 후 재실행 대상)
|
-- 카탈로그에 없어 입력되지 않은 제품 (수기 추가 후 재실행 대상)
|
||||||
SELECT s.product_name AS "카탈로그_미등록_제품명",
|
SELECT s.product_name AS "카탈로그_미등록_제품명",
|
||||||
s.units_per_box AS "입수량",
|
s.units_per_box AS "입수량",
|
||||||
s.box_no AS "박스호수"
|
s.box_no AS "상자호수"
|
||||||
FROM _seed2 s
|
FROM _seed2 s
|
||||||
WHERE NOT EXISTS (
|
WHERE NOT EXISTS (
|
||||||
SELECT 1 FROM cupang_products p
|
SELECT 1 FROM cupang_products p
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- cupang_db 마이그레이션 005 — 출고 묶음에 박스 구성(box_plan) 보관
|
-- cupang_db 마이그레이션 005 — 출고 묶음에 상자 구성(box_plan) 보관
|
||||||
-- =====================================================================
|
-- =====================================================================
|
||||||
-- 사유: 확정 시점의 박스 구성(제품별 박스 / 자투리 혼합 박스의 내용물과
|
-- 사유: 확정 시점의 상자 구성(제품별 상자 / 자투리 혼합 상자의 내용물과
|
||||||
-- 고른 상자 종류)을 그대로 보여주기 위함. 라인(제품·수량)만으로는
|
-- 고른 상자 종류)을 그대로 보여주기 위함. 라인(제품·수량)만으로는
|
||||||
-- "어느 혼합 상자에 무엇이 몇 개" 들어갔는지 복원할 수 없다.
|
-- "어느 혼합 상자에 무엇이 몇 개" 들어갔는지 복원할 수 없다.
|
||||||
--
|
--
|
||||||
|
|||||||
@@ -72,13 +72,13 @@ CREATE TRIGGER trg_cupang_centers_updated
|
|||||||
FOR EACH ROW EXECUTE FUNCTION cupang_set_updated_at();
|
FOR EACH ROW EXECUTE FUNCTION cupang_set_updated_at();
|
||||||
|
|
||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
-- 2) 박스 입수량 규칙
|
-- 2) 상자 입수량 규칙
|
||||||
-- ════════════════════════════════════════════════════════════
|
-- ════════════════════════════════════════════════════════════
|
||||||
CREATE TABLE IF NOT EXISTS cupang_box_rules (
|
CREATE TABLE IF NOT EXISTS cupang_box_rules (
|
||||||
id BIGSERIAL PRIMARY KEY,
|
id BIGSERIAL PRIMARY KEY,
|
||||||
product_code TEXT NOT NULL UNIQUE,
|
product_code TEXT NOT NULL UNIQUE,
|
||||||
product_name_snapshot TEXT,
|
product_name_snapshot TEXT,
|
||||||
box_name TEXT NOT NULL DEFAULT '쿠팡박스',
|
box_name TEXT NOT NULL DEFAULT '쿠팡상자',
|
||||||
units_per_box INTEGER NOT NULL CHECK (units_per_box > 0),
|
units_per_box INTEGER NOT NULL CHECK (units_per_box > 0),
|
||||||
active BOOLEAN NOT NULL DEFAULT TRUE,
|
active BOOLEAN NOT NULL DEFAULT TRUE,
|
||||||
memo TEXT NOT NULL DEFAULT '',
|
memo TEXT NOT NULL DEFAULT '',
|
||||||
@@ -129,7 +129,7 @@ CREATE TABLE IF NOT EXISTS cupang_shipments (
|
|||||||
worker TEXT NOT NULL DEFAULT '',
|
worker TEXT NOT NULL DEFAULT '',
|
||||||
status TEXT NOT NULL DEFAULT '작성중',
|
status TEXT NOT NULL DEFAULT '작성중',
|
||||||
memo TEXT NOT NULL DEFAULT '',
|
memo TEXT NOT NULL DEFAULT '',
|
||||||
-- 확정 당시 박스 구성(제품별/혼합 상자와 내용물). 상세 화면 표시용.
|
-- 확정 당시 상자 구성(제품별/혼합 상자와 내용물). 상세 화면 표시용.
|
||||||
box_plan JSONB NOT NULL DEFAULT '[]'::jsonb,
|
box_plan JSONB NOT NULL DEFAULT '[]'::jsonb,
|
||||||
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
created_at TIMESTAMPTZ NOT NULL DEFAULT now(),
|
||||||
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
updated_at TIMESTAMPTZ NOT NULL DEFAULT now()
|
||||||
|
|||||||
Reference in New Issue
Block a user