feat(malaysia): 창고 랙 보기 엑셀 다운로드 추가
랙 보기 기준(latest_rack_stocktake)의 모든 칸 항목을 xlsx 로 내보내는 /rack/export 엔드포인트와 다운로드 버튼 추가. 컬럼: 아이템 코드/이름/ 수량/랙번호. 표 전체 자동필터, 각 열 내용 길이에 맞춰 너비 자동(한글 폭 보정). 뚜껑(MD-) 이름도 병합해 표시. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -464,6 +464,80 @@ async def movements_export(request: Request) -> StreamingResponse:
|
|||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/rack/export")
|
||||||
|
async def rack_export(request: Request) -> StreamingResponse:
|
||||||
|
"""창고 랙 보기 데이터를 엑셀로 내보낸다.
|
||||||
|
|
||||||
|
랙 보기와 동일 기준(latest_rack_stocktake)의 모든 칸 항목을 한 행씩.
|
||||||
|
컬럼: 아이템 코드 / 이름 / 수량 / 랙번호. 표 전체에 자동필터,
|
||||||
|
각 열은 내용 길이에 맞춰 너비 자동.
|
||||||
|
"""
|
||||||
|
import io # noqa: WPS433
|
||||||
|
|
||||||
|
guard = _guard(request)
|
||||||
|
if not isinstance(guard, tuple):
|
||||||
|
return guard # type: ignore[return-value]
|
||||||
|
st, _user = guard
|
||||||
|
wh = _selected_wh(request, st)
|
||||||
|
latest = st.latest_rack_stocktake(warehouse_code=wh)
|
||||||
|
names = {**st.item_name_map(), **store.lid_name_map()}
|
||||||
|
|
||||||
|
rows: list[tuple[str, str, int, str]] = []
|
||||||
|
if latest:
|
||||||
|
for e in st.list_rack_entries(stocktake_id=latest["id"]):
|
||||||
|
code = e["sku_code"]
|
||||||
|
qty = int(e["units_per_box"]) * int(e["box_count"])
|
||||||
|
rows.append((code, names.get(code, code), qty, e["cell_code"]))
|
||||||
|
# 랙번호 → 아이템코드 순 정렬(보기 흐름과 동일하게 칸 기준).
|
||||||
|
rows.sort(key=lambda r: (r[3], r[0]))
|
||||||
|
|
||||||
|
from openpyxl import Workbook # noqa: WPS433
|
||||||
|
from openpyxl.styles import Alignment, Font, PatternFill # noqa: WPS433
|
||||||
|
from openpyxl.utils import get_column_letter # noqa: WPS433
|
||||||
|
|
||||||
|
wb = Workbook()
|
||||||
|
ws = wb.active
|
||||||
|
ws.title = "rack"
|
||||||
|
header = ["아이템 코드", "이름", "수량", "랙번호"]
|
||||||
|
ws.append(header)
|
||||||
|
for code, name, qty, cell in rows:
|
||||||
|
ws.append([code, name, qty, cell])
|
||||||
|
|
||||||
|
# 제목행: 파랑 배경 + 흰색 굵은글씨 + 가운데 정렬
|
||||||
|
fill = PatternFill(start_color="FF4472C4", end_color="FF4472C4", fill_type="solid")
|
||||||
|
font = Font(color="FFFFFFFF", bold=True)
|
||||||
|
center = Alignment(horizontal="center", vertical="center")
|
||||||
|
for col in range(1, len(header) + 1):
|
||||||
|
cell = ws.cell(row=1, column=col)
|
||||||
|
cell.fill = fill
|
||||||
|
cell.font = font
|
||||||
|
cell.alignment = center
|
||||||
|
|
||||||
|
# 표 전체 자동필터
|
||||||
|
ws.auto_filter.ref = ws.dimensions
|
||||||
|
|
||||||
|
# 열너비 자동(내용 길이 기준, 한글 폭 보정). 최소 8 ~ 최대 50.
|
||||||
|
for col in range(1, len(header) + 1):
|
||||||
|
letter = get_column_letter(col)
|
||||||
|
longest = 0
|
||||||
|
for cell in ws[letter]:
|
||||||
|
v = "" if cell.value is None else str(cell.value)
|
||||||
|
width = sum(2 if ord(ch) > 0x1100 else 1 for ch in v)
|
||||||
|
longest = max(longest, width)
|
||||||
|
ws.column_dimensions[letter].width = max(8, min(50, longest + 2))
|
||||||
|
|
||||||
|
buf = io.BytesIO()
|
||||||
|
wb.save(buf)
|
||||||
|
buf.seek(0)
|
||||||
|
sdate = (latest or {}).get("stocktake_date", today_kst().isoformat())
|
||||||
|
fname = f"malaysia_rack_{wh}_{sdate}.xlsx"
|
||||||
|
return StreamingResponse(
|
||||||
|
buf,
|
||||||
|
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||||
|
headers={"Content-Disposition": f'attachment; filename="{fname}"'},
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
# 일일 재고조사
|
# 일일 재고조사
|
||||||
# ════════════════════════════════════════════════════════════
|
# ════════════════════════════════════════════════════════════
|
||||||
|
|||||||
@@ -140,6 +140,7 @@
|
|||||||
{% if stocktake %}
|
{% if stocktake %}
|
||||||
<div class="erp-page-actions" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
<div class="erp-page-actions" style="margin-top:8px;display:flex;gap:8px;flex-wrap:wrap;align-items:center;">
|
||||||
<span class="erp-muted i18n" style="flex:1 1 auto;min-width:0;">칸을 드래그해 다른 칸으로 옮기면 구성이 이동(채워진 칸끼리는 맞교환)됩니다. 칸 번호는 그대로입니다. 인쇄할 칸은 체크 후 인쇄하세요.</span>
|
<span class="erp-muted i18n" style="flex:1 1 auto;min-width:0;">칸을 드래그해 다른 칸으로 옮기면 구성이 이동(채워진 칸끼리는 맞교환)됩니다. 칸 번호는 그대로입니다. 인쇄할 칸은 체크 후 인쇄하세요.</span>
|
||||||
|
<a href="/malaysia/rack/export?wh={{ selected_wh }}" class="erp-btn erp-btn-outline">다운로드</a>
|
||||||
<button type="button" id="rv-print-btn" class="erp-btn erp-btn-outline">인쇄 미리보기</button>
|
<button type="button" id="rv-print-btn" class="erp-btn erp-btn-outline">인쇄 미리보기</button>
|
||||||
<button type="button" id="rv-save-btn" class="erp-btn erp-btn-primary">저장</button>
|
<button type="button" id="rv-save-btn" class="erp-btn erp-btn-primary">저장</button>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in New Issue
Block a user