feat(malaysia): 랙 보기 드래그 재배치·저장·인쇄 미리보기
랙 보기에서 칸을 드래그해 다른 칸으로 구성을 옮긴다(채워진 칸끼리 맞교환, Side A↔B 가능). 칸 번호는 유지되고 수량 합계도 불변(위치만). 저장하면 그 배치로 기록되며, 재고조사 '이전값 불러오기'가 이 최종 위치를 불러온다. - db.reposition_rack: 칸(cell_code) 배치만 교체, daily_stocktake_line 보존, 확정본도 허용(취소만 불가). 위치 이동은 전산재고에 영향 없음 - db.previous_rack_entries: 정렬을 랙 저장시각(MAX created_at) 기준으로 통일 → '이전값 불러오기'가 랙 보기 최종 위치와 일치 - POST /malaysia/rack/save: latest_rack_stocktake 대상 reposition - rack_view.html + malaysia_rack_view.js: HTML5 드래그 이동/swap, 칸 체크 선택, 저장(병렬배열 직렬화), 인쇄 미리보기 모달 - 인쇄: 선택한 칸을 칸마다 A4 가로 1장, 굵은 대형 글씨(@media print), 미리보기 후 인쇄 - i18n 항목 추가, malaysia.js 캐시버스트 v20260615c Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -431,13 +431,67 @@ class MalaysiaStockStore:
|
||||
"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",
|
||||
"ORDER BY MAX(r.created_at) 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 reposition_rack(
|
||||
self, *, stocktake_id: int, entries: list[dict[str, Any]]
|
||||
) -> dict[str, Any]:
|
||||
"""랙 위치 재배치 전용(랙 보기 드래그 저장).
|
||||
|
||||
daily_stocktake_rack 의 칸(cell_code) 배치만 교체한다. 위치 이동은
|
||||
SKU 총수량을 바꾸지 않으므로 daily_stocktake_line 은 건드리지 않는다
|
||||
(확정본의 집계/전산재고 보존). 확정(finalized) 조사도 허용 — 위치만
|
||||
변경. 취소된 조사는 불가. entries 형식은 replace_rack 과 동일.
|
||||
"""
|
||||
valid_cells = set(store.rack_cell_codes())
|
||||
clean: list[tuple[str, str, int, int]] = []
|
||||
for e in entries:
|
||||
cell = (e.get("cell_code") or "").strip().upper()
|
||||
if cell not in valid_cells:
|
||||
raise ValueError(f"알 수 없는 랙 위치: {cell or '(빈값)'}")
|
||||
code = store.validate_stocktake_code(str(e.get("sku_code") or ""))
|
||||
upb = store.validate_qty(
|
||||
e.get("units_per_box"), allow_zero=False, allow_negative=False
|
||||
)
|
||||
box = store.validate_qty(
|
||||
e.get("box_count"), allow_zero=False, allow_negative=False
|
||||
)
|
||||
clean.append((cell, code, upb, box))
|
||||
|
||||
cell_seq: dict[str, int] = {}
|
||||
with self._pool.connection() as conn:
|
||||
with conn.transaction():
|
||||
head = conn.execute(
|
||||
"SELECT status FROM daily_stocktake WHERE id = %s FOR UPDATE",
|
||||
(stocktake_id,),
|
||||
).fetchone()
|
||||
if not head:
|
||||
raise KeyError(stocktake_id)
|
||||
if head["status"] == "cancelled":
|
||||
raise ValueError("취소된 조사는 수정할 수 없습니다.")
|
||||
conn.execute(
|
||||
"DELETE FROM daily_stocktake_rack WHERE stocktake_id = %s",
|
||||
(stocktake_id,),
|
||||
)
|
||||
for cell, code, upb, box in clean:
|
||||
ln = cell_seq.get(cell, 0)
|
||||
cell_seq[cell] = ln + 1
|
||||
conn.execute(
|
||||
"""
|
||||
INSERT INTO daily_stocktake_rack
|
||||
(stocktake_id, cell_code, line_no, sku_code,
|
||||
units_per_box, box_count)
|
||||
VALUES (%s,%s,%s,%s,%s,%s)
|
||||
""",
|
||||
(stocktake_id, cell, ln, code, upb, box),
|
||||
)
|
||||
return {"entries": len(clean)}
|
||||
|
||||
def delete_stocktake(self, *, stocktake_id: int) -> None:
|
||||
"""재고조사 완전 삭제(헤더+라인 CASCADE). 슈퍼관리자 전용 동작.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user