From 6e32c685905f1dd59e84fbbc3265ed1558acbb4f Mon Sep 17 00:00:00 2001 From: king Date: Mon, 15 Jun 2026 17:29:54 +0900 Subject: [PATCH] =?UTF-8?q?feat(malaysia):=20=EC=9E=AC=EA=B3=A0=EC=A1=B0?= =?UTF-8?q?=EC=82=AC=EC=97=90=20'=EC=9D=B4=EC=A0=84=EA=B0=92=20=EB=B6=88?= =?UTF-8?q?=EB=9F=AC=EC=98=A4=EA=B8=B0'=20=EB=B2=84=ED=8A=BC=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 작성중 재고조사 상세에서 직전 조사의 랙 저장값을 한 번에 불러와 채운다. 사용자는 이어서 수정 후 저장/확정. 직전 조사에 랙 입력이 있을 때만 버튼 노출. - db.previous_rack_entries: 현재 제외·취소 제외, 랙 있는 가장 최근 조사의 랙 항목 반환(replace_rack 입력 형식과 호환) - POST /stocktakes/{id}/rack/load-previous: 이전 랙을 현재 조사로 replace_rack 적용 후 리다이렉트(화면 복원). 작성중만 허용, 이전값 없으면 400 - 상세 핸들러에 has_prev_rack 플래그, 템플릿에 formaction 버튼(덮어쓰기 confirm) - i18n '이전값 불러오기'→'Load previous', JS 캐시버스트 v20260615b Co-Authored-By: Claude Opus 4.8 --- app/modules/malaysia/db.py | 22 +++++++++++ app/modules/malaysia/router.py | 38 ++++++++++++++++++- .../malaysia/templates/malaysia/_i18n.html | 2 +- .../templates/malaysia/stocktake_detail.html | 6 +++ app/static/malaysia.js | 1 + 5 files changed, 67 insertions(+), 2 deletions(-) diff --git a/app/modules/malaysia/db.py b/app/modules/malaysia/db.py index ec4cf8c..05e734e 100644 --- a/app/modules/malaysia/db.py +++ b/app/modules/malaysia/db.py @@ -416,6 +416,28 @@ class MalaysiaStockStore: ).fetchone() return self._serialize(row) if row else None + def previous_rack_entries( + self, *, warehouse_code: str, exclude_stocktake_id: int + ) -> list[dict[str, Any]]: + """직전(현재 조사 제외, 취소 제외, 랙 입력 있는) 재고조사의 랙 항목. + + '이전값 불러오기' 버튼용. 조사 날짜 기준 가장 최근 다른 조사를 고른다. + replace_rack 의 entries 형식(cell_code/sku_code/units_per_box/box_count)과 + 호환되는 list_rack_entries 결과를 그대로 반환. 없으면 []. + """ + with self._pool.connection() as conn: + head = conn.execute( + "SELECT s.id FROM daily_stocktake s " + "JOIN daily_stocktake_rack r ON r.stocktake_id = s.id " + "WHERE s.warehouse_code = %s AND s.id <> %s AND s.status <> 'cancelled' " + "GROUP BY s.id " + "ORDER BY s.stocktake_date DESC, s.id DESC LIMIT 1", + (warehouse_code, exclude_stocktake_id), + ).fetchone() + if not head: + return [] + return self.list_rack_entries(stocktake_id=head["id"]) + def delete_stocktake(self, *, stocktake_id: int) -> None: """재고조사 완전 삭제(헤더+라인 CASCADE). 슈퍼관리자 전용 동작. diff --git a/app/modules/malaysia/router.py b/app/modules/malaysia/router.py index 97f6e76..1e69079 100644 --- a/app/modules/malaysia/router.py +++ b/app/modules/malaysia/router.py @@ -507,6 +507,12 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse: rack_by_cell: dict[str, list[dict[str, Any]]] = {} for e in st.list_rack_entries(stocktake_id=stocktake_id): rack_by_cell.setdefault(e["cell_code"], []).append(e) + is_draft = head["status"] == "draft" + # 작성중일 때만 '이전값 불러오기' 가능 여부 계산(직전 조사에 랙 입력이 있을 때) + has_prev_rack = bool( + is_draft + and st.previous_rack_entries(warehouse_code=wh, exclude_stocktake_id=stocktake_id) + ) ctx = _base_ctx(request, user, st, wh=wh) ctx.update( { @@ -519,7 +525,8 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse: "entered": entered, "result_rows": computed["rows"], "missing_bom": computed["missing_bom"], - "is_draft": head["status"] == "draft", + "is_draft": is_draft, + "has_prev_rack": has_prev_rack, } ) return render_template(request, "malaysia/stocktake_detail.html", ctx) @@ -606,6 +613,35 @@ async def stocktake_rack_bulk( return RedirectResponse(url=f"/malaysia/stocktakes/{stocktake_id}", status_code=303) +@router.post("/stocktakes/{stocktake_id:int}/rack/load-previous") +async def stocktake_rack_load_previous( + request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user) +) -> RedirectResponse: + """직전 재고조사의 랙 저장값을 현재(작성중) 조사로 불러와 채운다. + + 이전 랙 항목을 그대로 replace_rack 으로 적용 → 화면에 복원되어 표시된다. + 사용자는 이어서 수정 후 저장/확정한다. 현재 입력은 덮어쓰므로 화면에서 confirm. + """ + st = _store(request) + if st is None: + raise HTTPException(status_code=503, detail="malaysia_stock_db 미설정") + head = st.get_stocktake(stocktake_id=stocktake_id, with_lines=False) + if head is None: + raise HTTPException(status_code=404, detail="재고조사를 찾을 수 없습니다.") + if head["status"] != "draft": + raise HTTPException(status_code=400, detail="작성중인 조사만 불러올 수 있습니다.") + entries = st.previous_rack_entries( + warehouse_code=head["warehouse_code"], exclude_stocktake_id=stocktake_id + ) + if not entries: + raise HTTPException(status_code=400, detail="불러올 이전 재고조사 랙 데이터가 없습니다.") + try: + st.replace_rack(stocktake_id=stocktake_id, entries=entries) + except (KeyError, ValueError) as exc: + raise HTTPException(status_code=400, detail=str(exc)) + return RedirectResponse(url=f"/malaysia/stocktakes/{stocktake_id}", status_code=303) + + @router.post("/stocktakes/{stocktake_id:int}/rack/finalize") async def stocktake_rack_finalize( request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user) diff --git a/app/modules/malaysia/templates/malaysia/_i18n.html b/app/modules/malaysia/templates/malaysia/_i18n.html index 4c9c50a..d6ef7a5 100644 --- a/app/modules/malaysia/templates/malaysia/_i18n.html +++ b/app/modules/malaysia/templates/malaysia/_i18n.html @@ -32,4 +32,4 @@
- + diff --git a/app/modules/malaysia/templates/malaysia/stocktake_detail.html b/app/modules/malaysia/templates/malaysia/stocktake_detail.html index bffb6c8..167e11c 100644 --- a/app/modules/malaysia/templates/malaysia/stocktake_detail.html +++ b/app/modules/malaysia/templates/malaysia/stocktake_detail.html @@ -104,6 +104,12 @@ {% if stocktake.finalized_at %}
확정시각 {{ stocktake.finalized_at }}
{% endif %} {% if missing_bom %}⚠ BOM 누락: {{ missing_bom|join(', ') }}{% endif %} 랙 칸에 아이템·입수량(박스당 개수)·박스 수를 입력하면 낱개/콤보 수량이 자동 집계됩니다. 한 칸에 여러 아이템은 + 로 추가하세요. 저장 또는 확정 시 화면 입력이 함께 반영됩니다. + {% if is_draft and has_prev_rack %} + + {% endif %} {% if is_draft %}{% endif %} {% if is_draft and is_admin %}