여러 엑셀 파일 업로드 시 업로드 날짜 지정 기능 및 낱개 출고 자동분석 다운로드 기능 구현
This commit is contained in:
+175
-33
@@ -16,7 +16,7 @@ from sqlalchemy.orm import Session
|
||||
from sqlalchemy import String, and_, cast, func, or_, text
|
||||
from urllib.parse import quote
|
||||
|
||||
from app.database import SessionLocal, get_db
|
||||
from app.database import SessionLocal, get_db, get_secondary_engine
|
||||
from app.models import Order
|
||||
from app.auth import get_current_user
|
||||
from app.event_logger import log_event
|
||||
@@ -562,46 +562,188 @@ def run_upload_job(files_data: list[dict], user_id: str, user_data: dict):
|
||||
def upload_file(
|
||||
files: list[UploadFile] = File(...),
|
||||
upload_token: str = Form(...),
|
||||
current_user = Depends(get_current_user)
|
||||
custom_date: str = Form(...),
|
||||
current_user = Depends(get_current_user),
|
||||
db: Session = Depends(get_db)
|
||||
):
|
||||
# 1. 파일 확장자 검증
|
||||
for file in files:
|
||||
filename = file.filename or ""
|
||||
if not filename.endswith(".xls") and not filename.endswith(".xlsx"):
|
||||
raise HTTPException(status_code=400, detail="XLS 또는 XLSX 파일만 업로드할 수 있습니다.")
|
||||
|
||||
user_id = f"{current_user.id}:{upload_token}"
|
||||
files_data = []
|
||||
for file in files:
|
||||
contents = file.file.read()
|
||||
files_data.append({
|
||||
"contents": contents,
|
||||
"filename": file.filename or ""
|
||||
})
|
||||
|
||||
user_data = {
|
||||
"id": getattr(current_user, "id", ""),
|
||||
"email": getattr(current_user, "email", None),
|
||||
"name": getattr(current_user, "name", None),
|
||||
}
|
||||
|
||||
filenames_str = ", ".join([f["filename"] for f in files_data])
|
||||
log_event(current_user, "파일 업로드 시작", "started", {"filenames": filenames_str})
|
||||
set_upload_state(
|
||||
user_id,
|
||||
status="queued",
|
||||
message="업로드 작업이 등록되었습니다.",
|
||||
filenames_str = ", ".join([f.filename for f in files])
|
||||
log_event(
|
||||
current_user,
|
||||
"파일 업로드 시작 (낱개 출고 자동분석)",
|
||||
"started",
|
||||
{"filenames": filenames_str, "custom_date": custom_date}
|
||||
)
|
||||
threading.Thread(
|
||||
target=run_upload_job,
|
||||
args=(files_data, user_id, user_data),
|
||||
daemon=True,
|
||||
).start()
|
||||
|
||||
return {
|
||||
"status": "queued",
|
||||
"upload_token": upload_token,
|
||||
"message": "업로드 작업이 시작되었습니다. 진행률 창에서 완료 상태를 확인해주세요.",
|
||||
}
|
||||
db_records = []
|
||||
try:
|
||||
# 2. 파일 파싱 및 데이터베이스 레코드 임시 적재
|
||||
for file in files:
|
||||
contents = file.file.read()
|
||||
df = pd.read_excel(io.BytesIO(contents))
|
||||
df = df.where(pd.notnull(df), None)
|
||||
column_map = find_template_columns(list(df.columns))
|
||||
|
||||
for index, row in df.iterrows():
|
||||
rlist = list(row)
|
||||
recipient_phone = row_value(rlist, column_map, "recipient_phone")
|
||||
recipient_mobile = row_value(rlist, column_map, "recipient_mobile")
|
||||
tracking_number = row_value(rlist, column_map, "tracking_number")
|
||||
address = row_value(rlist, column_map, "address")
|
||||
|
||||
order = Order(
|
||||
order_date=custom_date, # 사용자가 지정한 주문 날짜 강제 입력
|
||||
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"),
|
||||
recipient_name=row_value(rlist, column_map, "recipient_name"),
|
||||
product_code=row_value(rlist, column_map, "product_code"),
|
||||
product_name=row_value(rlist, column_map, "product_name"),
|
||||
order_quantity=row_value(rlist, column_map, "order_quantity"),
|
||||
address=address,
|
||||
address_normalized=normalize_address_key(address),
|
||||
postal_code=row_value(rlist, column_map, "postal_code"),
|
||||
recipient_phone=recipient_phone,
|
||||
recipient_mobile=recipient_mobile,
|
||||
recipient_phone_normalized=normalize_phone(recipient_phone),
|
||||
recipient_mobile_normalized=normalize_phone(recipient_mobile),
|
||||
delivery_memo=row_value(rlist, column_map, "delivery_memo"),
|
||||
tracking_number=tracking_number,
|
||||
tracking_number_normalized=normalize_tracking(tracking_number),
|
||||
vendor=row_value(rlist, column_map, "vendor"),
|
||||
order_list_1=row_value(rlist, column_map, "order_list_1"),
|
||||
order_note=row_value(rlist, column_map, "order_note"),
|
||||
upload_date=datetime.now().strftime("%Y-%m-%d %H:%M:%S")
|
||||
)
|
||||
db_records.append(order)
|
||||
|
||||
# 3. 데이터베이스 벌크 저장 및 커밋
|
||||
batch_size = 1000
|
||||
for i in range(0, len(db_records), batch_size):
|
||||
chunk = db_records[i:i + batch_size]
|
||||
db.bulk_save_objects(chunk)
|
||||
db.commit()
|
||||
log_event(
|
||||
current_user,
|
||||
"파일 업로드 DB 저장 완료",
|
||||
"success",
|
||||
{"filenames": filenames_str, "uploaded_count": len(db_records)}
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
db.rollback()
|
||||
log_event(current_user, "파일 업로드 실패", "error", {"filenames": filenames_str, "error": str(e)})
|
||||
raise HTTPException(status_code=500, detail=f"데이터베이스 저장 실패: {str(e)}")
|
||||
|
||||
# 4. 품목 마스터(itemcode_db) 연동 및 낱개 수량 환산 합계 계산
|
||||
try:
|
||||
item_engine = get_secondary_engine("ITEM_DB_NAME")
|
||||
|
||||
single_items = {}
|
||||
set_components = {}
|
||||
|
||||
if item_engine:
|
||||
with item_engine.connect() as conn:
|
||||
# 1) 단품 마스터 맵 구성
|
||||
rs_single = conn.execute(text("SELECT item_code, sabangnet_code, name FROM single_items"))
|
||||
for row in rs_single:
|
||||
single_items[row[0]] = {
|
||||
"sabangnet_code": row[1] or "",
|
||||
"name": row[2] or ""
|
||||
}
|
||||
# 2) 세트 구성품 맵 구성
|
||||
rs_comp = conn.execute(text("SELECT set_code, single_code, quantity FROM set_components"))
|
||||
for row in rs_comp:
|
||||
s_code = row[0]
|
||||
if s_code not in set_components:
|
||||
set_components[s_code] = []
|
||||
set_components[s_code].append({
|
||||
"single_code": row[1],
|
||||
"quantity": row[2]
|
||||
})
|
||||
|
||||
# 3) 낱개 환산 처리
|
||||
single_qty_sums = {}
|
||||
for order in db_records:
|
||||
prod_code = (order.product_code or "").strip()
|
||||
if not prod_code:
|
||||
continue
|
||||
|
||||
qty = 1
|
||||
try:
|
||||
qty = int(order.order_quantity or 1)
|
||||
except:
|
||||
pass
|
||||
|
||||
if prod_code in single_items:
|
||||
single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty
|
||||
elif prod_code in set_components:
|
||||
for comp in set_components[prod_code]:
|
||||
s_code = comp["single_code"]
|
||||
comp_qty = comp["quantity"]
|
||||
single_qty_sums[s_code] = single_qty_sums.get(s_code, 0) + (qty * comp_qty)
|
||||
else:
|
||||
# DB에 등록되어 있지 않은 코드는 단품으로 간주하고, 사방넷코드와 이름을 보존
|
||||
single_items[prod_code] = {
|
||||
"sabangnet_code": prod_code,
|
||||
"name": order.product_name or ""
|
||||
}
|
||||
single_qty_sums[prod_code] = single_qty_sums.get(prod_code, 0) + qty
|
||||
|
||||
# 5. 결과 엑셀 데이터 빌드
|
||||
excel_rows = []
|
||||
for s_code, total_qty in single_qty_sums.items():
|
||||
s_info = single_items.get(s_code, {"sabangnet_code": s_code, "name": ""})
|
||||
excel_rows.append({
|
||||
"상품코드[필수]": s_info["sabangnet_code"] or s_code,
|
||||
"가용수량": total_qty,
|
||||
"불용수량": s_info["name"] or "" # C열 헤더는 "불용수량", 실데이터는 이름!
|
||||
})
|
||||
|
||||
df_out = pd.DataFrame(excel_rows)
|
||||
if df_out.empty:
|
||||
df_out = pd.DataFrame(columns=["상품코드[필수]", "가용수량", "불용수량"])
|
||||
else:
|
||||
df_out = df_out[["상품코드[필수]", "가용수량", "불용수량"]]
|
||||
|
||||
# 엑셀 파일 쓰기
|
||||
excel_io = io.BytesIO()
|
||||
with pd.ExcelWriter(excel_io, engine='openpyxl') as writer:
|
||||
df_out.to_excel(writer, index=False)
|
||||
excel_io.seek(0)
|
||||
|
||||
# 6. 다운로드 파일명 날짜 포맷 조합
|
||||
try:
|
||||
dt = datetime.strptime(custom_date, "%Y-%m-%d")
|
||||
except:
|
||||
dt = datetime.now()
|
||||
|
||||
weekdays = ["월요일", "화요일", "수요일", "목요일", "금요일", "토요일", "일요일"]
|
||||
weekday_str = weekdays[dt.weekday()]
|
||||
|
||||
# 형식 예: 05월 10일 (일요일)_발주_(낱개 상품 출고).xls
|
||||
filename = f"{dt.strftime('%m')}월 {dt.strftime('%d')}일 ({weekday_str})_발주_(낱개 상품 출고).xls"
|
||||
encoded_filename = quote(filename)
|
||||
|
||||
headers = {
|
||||
"Content-Disposition": f"attachment; filename*=UTF-8''{encoded_filename}",
|
||||
"Access-Control-Expose-Headers": "Content-Disposition"
|
||||
}
|
||||
|
||||
return StreamingResponse(
|
||||
excel_io,
|
||||
media_type="application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
|
||||
headers=headers
|
||||
)
|
||||
|
||||
except Exception as e:
|
||||
log_event(current_user, "낱개 출고 분석 오류", "error", {"error": str(e)})
|
||||
raise HTTPException(status_code=500, detail=f"낱개 출고 분석 및 엑셀 생성 실패: {str(e)}")
|
||||
|
||||
@router.get("/progress")
|
||||
async def upload_progress(
|
||||
|
||||
Reference in New Issue
Block a user