feat(malaysia): 재고조사에 '이전값 불러오기' 버튼 추가
작성중 재고조사 상세에서 직전 조사의 랙 저장값을 한 번에 불러와 채운다.
사용자는 이어서 수정 후 저장/확정. 직전 조사에 랙 입력이 있을 때만 버튼 노출.
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -416,6 +416,28 @@ class MalaysiaStockStore:
|
|||||||
).fetchone()
|
).fetchone()
|
||||||
return self._serialize(row) if row else None
|
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:
|
def delete_stocktake(self, *, stocktake_id: int) -> None:
|
||||||
"""재고조사 완전 삭제(헤더+라인 CASCADE). 슈퍼관리자 전용 동작.
|
"""재고조사 완전 삭제(헤더+라인 CASCADE). 슈퍼관리자 전용 동작.
|
||||||
|
|
||||||
|
|||||||
@@ -507,6 +507,12 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
|||||||
rack_by_cell: dict[str, list[dict[str, Any]]] = {}
|
rack_by_cell: dict[str, list[dict[str, Any]]] = {}
|
||||||
for e in st.list_rack_entries(stocktake_id=stocktake_id):
|
for e in st.list_rack_entries(stocktake_id=stocktake_id):
|
||||||
rack_by_cell.setdefault(e["cell_code"], []).append(e)
|
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 = _base_ctx(request, user, st, wh=wh)
|
||||||
ctx.update(
|
ctx.update(
|
||||||
{
|
{
|
||||||
@@ -519,7 +525,8 @@ async def stocktake_detail(request: Request, stocktake_id: int) -> HTMLResponse:
|
|||||||
"entered": entered,
|
"entered": entered,
|
||||||
"result_rows": computed["rows"],
|
"result_rows": computed["rows"],
|
||||||
"missing_bom": computed["missing_bom"],
|
"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)
|
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)
|
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")
|
@router.post("/stocktakes/{stocktake_id:int}/rack/finalize")
|
||||||
async def stocktake_rack_finalize(
|
async def stocktake_rack_finalize(
|
||||||
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
|
request: Request, stocktake_id: int, user: dict[str, Any] = Depends(_require_user)
|
||||||
|
|||||||
@@ -32,4 +32,4 @@
|
|||||||
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
|
<div style="display:flex;justify-content:flex-end;margin-bottom:8px;">
|
||||||
<button type="button" id="mys-lang-toggle" class="erp-btn erp-btn-outline">ENG</button>
|
<button type="button" id="mys-lang-toggle" class="erp-btn erp-btn-outline">ENG</button>
|
||||||
</div>
|
</div>
|
||||||
<script src="/static/malaysia.js?v=20260615a"></script>
|
<script src="/static/malaysia.js?v=20260615b"></script>
|
||||||
|
|||||||
@@ -104,6 +104,12 @@
|
|||||||
{% if stocktake.finalized_at %}<div><span class="erp-muted">확정시각</span> {{ stocktake.finalized_at }}</div>{% endif %}
|
{% if stocktake.finalized_at %}<div><span class="erp-muted">확정시각</span> {{ stocktake.finalized_at }}</div>{% endif %}
|
||||||
{% if missing_bom %}<span style="color:#b91c1c;font-weight:600;white-space:nowrap;">⚠ BOM 누락: {{ missing_bom|join(', ') }}</span>{% endif %}
|
{% if missing_bom %}<span style="color:#b91c1c;font-weight:600;white-space:nowrap;">⚠ BOM 누락: {{ missing_bom|join(', ') }}</span>{% endif %}
|
||||||
<span class="erp-muted i18n mys-hint">랙 칸에 아이템·입수량(박스당 개수)·박스 수를 입력하면 낱개/콤보 수량이 자동 집계됩니다. 한 칸에 여러 아이템은 + 로 추가하세요. 저장 또는 확정 시 화면 입력이 함께 반영됩니다.</span>
|
<span class="erp-muted i18n mys-hint">랙 칸에 아이템·입수량(박스당 개수)·박스 수를 입력하면 낱개/콤보 수량이 자동 집계됩니다. 한 칸에 여러 아이템은 + 로 추가하세요. 저장 또는 확정 시 화면 입력이 함께 반영됩니다.</span>
|
||||||
|
{% if is_draft and has_prev_rack %}
|
||||||
|
<button type="submit" class="erp-btn erp-btn-outline"
|
||||||
|
formaction="/malaysia/stocktakes/{{ stocktake.id }}/rack/load-previous"
|
||||||
|
formnovalidate
|
||||||
|
onclick="return confirm('이전 재고조사의 저장값을 불러옵니다. 현재 화면에 입력한 내용은 사라지고 이전값으로 대체됩니다. 계속?');">이전값 불러오기</button>
|
||||||
|
{% endif %}
|
||||||
{% if is_draft %}<button type="submit" class="erp-btn erp-btn-outline">입력 저장</button>{% endif %}
|
{% if is_draft %}<button type="submit" class="erp-btn erp-btn-outline">입력 저장</button>{% endif %}
|
||||||
{% if is_draft and is_admin %}
|
{% if is_draft and is_admin %}
|
||||||
<button type="submit" class="erp-btn erp-btn-primary"
|
<button type="submit" class="erp-btn erp-btn-primary"
|
||||||
|
|||||||
@@ -74,6 +74,7 @@
|
|||||||
"재고조사": "Stocktake",
|
"재고조사": "Stocktake",
|
||||||
"◀◀ 조사 목록": "◀◀ List",
|
"◀◀ 조사 목록": "◀◀ List",
|
||||||
"확정시각": "Finalized at",
|
"확정시각": "Finalized at",
|
||||||
|
"이전값 불러오기": "Load previous",
|
||||||
"입력 저장": "Save",
|
"입력 저장": "Save",
|
||||||
"확정 (전산 반영)": "Finalize (apply)",
|
"확정 (전산 반영)": "Finalize (apply)",
|
||||||
"삭제 (슈퍼관리자)": "Delete (super)",
|
"삭제 (슈퍼관리자)": "Delete (super)",
|
||||||
|
|||||||
Reference in New Issue
Block a user