Compare commits
6 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b919f3ae30 | |||
| 52ce51aac7 | |||
| da3ff199d8 | |||
| 9e6576a594 | |||
| 19a552452f | |||
| 995adb5cd8 |
+59
-10
@@ -594,6 +594,9 @@ def upload_file(
|
||||
order_date_dt = datetime.strptime(custom_date, "%Y-%m-%d")
|
||||
except (ValueError, TypeError):
|
||||
raise HTTPException(status_code=400, detail="주문 날짜 형식이 올바르지 않습니다 (YYYY-MM-DD).")
|
||||
# 기존 데이터/검색 쿼리가 "YYYY-MM-DD HH:MM:SS" 문자열 비교를 사용하므로
|
||||
# 새 업로드 데이터도 동일하게 시간 부분(00:00:00)을 붙여 저장한다.
|
||||
order_date_str = order_date_dt.strftime("%Y-%m-%d %H:%M:%S")
|
||||
|
||||
# 1. 파일 확장자 검증
|
||||
if not files:
|
||||
@@ -644,7 +647,7 @@ def upload_file(
|
||||
address = row_value(rlist, column_map, "address")
|
||||
|
||||
order = Order(
|
||||
order_date=custom_date, # 사용자가 지정한 주문 날짜 강제 입력
|
||||
order_date=order_date_str, # 달력에서 고른 날짜 + 00:00:00 (검색 호환)
|
||||
sequence_num=row_value(rlist, column_map, "sequence_num"),
|
||||
order_no=row_value(rlist, column_map, "order_no"),
|
||||
order_no_mall=row_value(rlist, column_map, "order_no_mall"),
|
||||
@@ -763,14 +766,16 @@ def upload_file(
|
||||
s_code = comp["single_code"]
|
||||
comp_qty = safe_int_quantity(comp["quantity"], default=1)
|
||||
single_qty_sums[s_code] = single_qty_sums.get(s_code, 0) + (qty * comp_qty)
|
||||
# 세트 구성 단품 정보가 single_items 에 없을 수도 있으므로 보강
|
||||
# 세트 구성 단품 정보가 single_items 에 없을 수도 있으므로 placeholder 만 보강.
|
||||
# 사방넷 코드는 비워서, 결과 엑셀 빌드 단계에서 제외 대상으로 인식되게 둔다.
|
||||
if s_code not in single_items:
|
||||
single_items[s_code] = {"sabangnet_code": s_code, "name": ""}
|
||||
single_items[s_code] = {"sabangnet_code": "", "name": ""}
|
||||
else:
|
||||
# 마스터 미등록 코드: 단품으로 간주, 이름은 주문서의 상품명으로 채움
|
||||
# 마스터 미등록 코드: 사방넷 코드를 알 수 없으므로 placeholder 로만 추적.
|
||||
# 결과 엑셀에서는 제외된다.
|
||||
unknown_codes.add(prod_code)
|
||||
single_items[prod_code] = {
|
||||
"sabangnet_code": prod_code,
|
||||
"sabangnet_code": "",
|
||||
"name": order.product_name or ""
|
||||
}
|
||||
single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty
|
||||
@@ -785,27 +790,69 @@ def upload_file(
|
||||
|
||||
# 5. 결과 엑셀 데이터 빌드
|
||||
# A열=사방넷코드, B열=합계수량, C열=이름. 헤더는 요청 사양(상품코드[필수]/가용수량/불용수량).
|
||||
# 사방넷 코드가 없는(마스터 미등록·구성품 미등록) 단품은 결과에서 제외한다.
|
||||
excel_rows = []
|
||||
skipped_no_sabang: list[str] = []
|
||||
for s_code, total_qty in single_qty_sums.items():
|
||||
s_info = single_items.get(s_code, {"sabangnet_code": s_code, "name": ""})
|
||||
s_info = single_items.get(s_code, {"sabangnet_code": "", "name": ""})
|
||||
sabang_code = (s_info.get("sabangnet_code") or "").strip()
|
||||
if not sabang_code:
|
||||
skipped_no_sabang.append(s_code)
|
||||
continue
|
||||
excel_rows.append({
|
||||
"상품코드[필수]": s_info["sabangnet_code"] or s_code,
|
||||
"상품코드[필수]": sabang_code,
|
||||
"가용수량": total_qty,
|
||||
"불용수량": s_info["name"] or ""
|
||||
"불용수량": s_info.get("name") or ""
|
||||
})
|
||||
|
||||
if skipped_no_sabang:
|
||||
log_event(
|
||||
current_user,
|
||||
"낱개 출고 분석 — 사방넷 코드 없는 항목 제외",
|
||||
"warning",
|
||||
{"skipped_count": len(skipped_no_sabang), "sample": skipped_no_sabang[:20]},
|
||||
)
|
||||
|
||||
df_out = pd.DataFrame(excel_rows, columns=["상품코드[필수]", "가용수량", "불용수량"])
|
||||
# A열(사방넷 코드) 기준 오름차순 정렬 — 헤더는 영향 받지 않음
|
||||
if not df_out.empty:
|
||||
df_out = df_out.sort_values(
|
||||
by="상품코드[필수]",
|
||||
key=lambda col: col.astype(str),
|
||||
kind="mergesort",
|
||||
).reset_index(drop=True)
|
||||
|
||||
# 엑셀 파일 쓰기 (1행 헤더, 2행부터 데이터 — pandas to_excel 기본 동작)
|
||||
# 작성 직후 A~C 열 너비를 콘텐츠 길이에 맞춰 자동 조절한다.
|
||||
# (한글은 ASCII보다 폭이 약 2배이므로 가중치 적용)
|
||||
def _excel_display_len(value) -> int:
|
||||
s = "" if value is None else str(value)
|
||||
width = 0
|
||||
for ch in s:
|
||||
width += 2 if ord(ch) > 127 else 1
|
||||
return width
|
||||
|
||||
excel_io = io.BytesIO()
|
||||
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
|
||||
df_out.to_excel(writer, index=False, sheet_name="발주")
|
||||
worksheet = writer.sheets["발주"]
|
||||
from openpyxl.utils import get_column_letter
|
||||
for col_idx, col_name in enumerate(df_out.columns, start=1):
|
||||
header_w = _excel_display_len(col_name)
|
||||
data_w = max(
|
||||
(_excel_display_len(v) for v in df_out[col_name].values),
|
||||
default=0,
|
||||
)
|
||||
# 최소 10, 최대 60 으로 클램프 + 여백 2
|
||||
worksheet.column_dimensions[get_column_letter(col_idx)].width = (
|
||||
min(60, max(10, max(header_w, data_w) + 2))
|
||||
)
|
||||
excel_io.seek(0)
|
||||
|
||||
# 6. 다운로드 파일명: "MM월 DD일 (요일)_발주_(낱개 상품 출고).xls"
|
||||
# 6. 다운로드 파일명: "MM월 DD일 (요일)_발주_(낱개 상품 출고).xlsx"
|
||||
weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
|
||||
weekday_str = weekdays[order_date_dt.weekday()]
|
||||
filename = f"{order_date_dt.strftime('%m')}월 {order_date_dt.strftime('%d')}일 ({weekday_str})_발주_(낱개 상품 출고).xls"
|
||||
filename = f"{order_date_dt.strftime('%m')}월 {order_date_dt.strftime('%d')}일 ({weekday_str})_발주_(낱개 상품 출고).xlsx"
|
||||
encoded_filename = quote(filename)
|
||||
|
||||
headers = {
|
||||
@@ -821,7 +868,9 @@ def upload_file(
|
||||
"filenames": filenames_str,
|
||||
"uploaded_count": len(db_records),
|
||||
"single_code_count": len(single_qty_sums),
|
||||
"excel_row_count": len(excel_rows),
|
||||
"unknown_code_count": len(unknown_codes),
|
||||
"skipped_no_sabang_count": len(skipped_no_sabang),
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@@ -1974,6 +1974,7 @@ input[type="checkbox"] {
|
||||
border: 1px dashed #cbd5e1;
|
||||
border-radius: 8px;
|
||||
padding: 0.75rem;
|
||||
margin-bottom: 1.5rem;
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
align-items: center;
|
||||
|
||||
+1
-1
@@ -411,7 +411,7 @@
|
||||
<div class="modal-content glass-panel upload-date-modal-content">
|
||||
<span class="close-btn" id="upload-date-close">×</span>
|
||||
<h2><i class="fa-regular fa-calendar-check"></i> 업로드 날짜 지정</h2>
|
||||
<p class="modal-instruction-text">발주 데이터의 주문 날짜로 입력될 날짜를 선택해주세요.</p>
|
||||
<p class="modal-instruction-text">발주 데이터의 주문 날짜로 날짜를 선택해주세요.</p>
|
||||
|
||||
<!-- Custom Calendar Picker Container -->
|
||||
<div class="custom-calendar-picker">
|
||||
|
||||
+13
-5
@@ -399,10 +399,18 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
if (!files || files.length === 0) return;
|
||||
|
||||
pendingFiles = files;
|
||||
calendarActiveDate = new Date();
|
||||
calendarSelectedDate = null;
|
||||
btnSubmitUploadDate.disabled = true;
|
||||
selectedDateText.textContent = '선택 안 됨';
|
||||
|
||||
// 기본 선택값: 오늘 날짜 (사용자가 명시적으로 다시 고를 수도 있음)
|
||||
const today = new Date();
|
||||
const yyyy = today.getFullYear();
|
||||
const mm = String(today.getMonth() + 1).padStart(2, '0');
|
||||
const dd = String(today.getDate()).padStart(2, '0');
|
||||
const weekdayNames = ["일요일", "월요일", "화요일", "수요일", "목요일", "금요일", "토요일"];
|
||||
|
||||
calendarActiveDate = new Date(yyyy, today.getMonth(), today.getDate());
|
||||
calendarSelectedDate = `${yyyy}-${mm}-${dd}`;
|
||||
selectedDateText.textContent = `${yyyy}년 ${today.getMonth() + 1}월 ${today.getDate()}일 (${weekdayNames[today.getDay()]})`;
|
||||
btnSubmitUploadDate.disabled = false;
|
||||
|
||||
renderCalendar();
|
||||
uploadDateModal.classList.add('show');
|
||||
@@ -430,7 +438,7 @@ document.addEventListener('DOMContentLoaded', () => {
|
||||
});
|
||||
|
||||
if (response.ok) {
|
||||
let downloadFilename = '낱개상품출고.xls';
|
||||
let downloadFilename = '낱개상품출고.xlsx';
|
||||
const disposition = response.headers.get('Content-Disposition');
|
||||
if (disposition && disposition.indexOf('filename*=') !== -1) {
|
||||
const matches = disposition.split("filename*=UTF-8''");
|
||||
|
||||
Reference in New Issue
Block a user