feat(dispatch): 말레이시아 TikTok 출고관리 모듈 추가

03_TikTok_Order_Export.xlsx 업로드 → 1박스=1카드 출고 작업 리스트,
SKU 피킹 요약, Kagayaku 전달표(A4 인쇄)를 자동 생성.

- 1박스 묶음 기준: Package ID > Tracking ID > Order ID, 같은 박스 같은 SKU 합산
- 작업 상태 5단계 토글(AJAX 즉시 저장) + dispatch_logs 기록
- 고객 이름/주소/전화 미저장(파싱 시 폐기, 스키마에 컬럼 없음)
- 엑셀은 openpyxl 파싱(pandas 미사용), DB는 dispatch_db 전용
- 인증은 기존 Google OAuth + 신규 권한키 dispatch 재사용
- scripts/sql/dispatch_db_init.sql, docs/DISPATCH_MODULE.md 동봉

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-19 15:11:43 +09:00
parent 8453c425f8
commit 99eea01fc1
19 changed files with 1868 additions and 0 deletions
@@ -0,0 +1,77 @@
{% extends "erp_base.html" %}
{% block head_extra %}
<style>
.dsp-ho-grid { display:grid;grid-template-columns:repeat(auto-fit,minmax(140px,1fr));gap:12px;margin:12px 0; }
.dsp-ho-stat { border:1px solid #e2e8f0;border-radius:10px;padding:12px;text-align:center;background:#fff; }
.dsp-ho-stat .n { font-size:26px;font-weight:700;color:#0f172a; }
.dsp-ho-stat .l { font-size:13px;color:#64748b; }
.dsp-track td { font-family:ui-monospace,SFMono-Regular,Menlo,monospace;white-space:nowrap; }
@media print {
.erp-sidebar, .erp-topbar, .erp-page-actions, .dsp-noprint { display:none !important; }
.erp-content, .erp-page, .erp-app { margin:0 !important;padding:0 !important;display:block !important; }
.erp-card { border:none !important;box-shadow:none !important;padding:0 !important; }
body { background:#fff !important; }
.dsp-print-head { display:block !important; }
@page { size:A4;margin:14mm; }
}
.dsp-print-head { display:none; }
</style>
{% endblock %}
{% block content %}
<section class="dsp">
{% include "dispatch/_nav.html" %}
<div class="dsp-noprint" style="margin:8px 0;">
<button class="erp-btn erp-btn-primary" type="button" onclick="window.print()">🖨 인쇄하기</button>
</div>
<div class="erp-card">
<div class="dsp-print-head" style="margin-bottom:10px;">
<h2 style="margin:0;">Kagayaku 전달 리스트</h2>
</div>
<div class="cpg-card-head">
<h2>{{ batch.batch_name or 'TikTok 출고' }}</h2>
</div>
<p class="erp-muted" style="margin:4px 0 4px;">
날짜 <b>{{ batch.dispatch_date }}</b> · 플랫폼 <b>{{ batch.platform }}</b>
</p>
<div class="dsp-ho-grid">
<div class="dsp-ho-stat">
<div class="n">{{ handover.total_boxes }}</div>
<div class="l">총 박스 수</div>
</div>
{% for pv in handover.by_provider %}
<div class="dsp-ho-stat">
<div class="n">{{ pv.box_count }}</div>
<div class="l">{{ pv.provider }}</div>
</div>
{% endfor %}
</div>
<h3 style="margin:16px 0 8px;">Tracking ID 목록 ({{ handover.tracking_list|length }})</h3>
<div class="erp-table-wrap">
<table class="erp-table dsp-track">
<thead>
<tr><th style="text-align:right">No</th><th>Tracking ID</th><th>배송사</th><th>Order ID</th></tr>
</thead>
<tbody>
{% for t in handover.tracking_list %}
<tr>
<td style="text-align:right">{{ t.seq }}</td>
<td>{{ t.tracking_id or '—' }}</td>
<td>{{ t.shipping_provider or '—' }}</td>
<td>{{ t.order_id or '—' }}</td>
</tr>
{% endfor %}
{% if not handover.tracking_list %}
<tr><td colspan="4" class="erp-muted">전달할 박스가 없습니다.</td></tr>
{% endif %}
</tbody>
</table>
</div>
</div>
</section>
{% endblock %}