feat: 전화접수 관리에 봉투코드 스캔 판매·포장 처리 기능 추가

지정판매소 판매의 스캔 로직(resolveDesignatedSaleBarcode 등)을 재사용해
전화접수 관리 화면에서 바로 봉투코드를 스캔하면:
- 접수 리스트에서 선택된 주문의 판매소로 즉시 판매처리(bag_sale 기록,
  재고 차감, 팩/박스코드 sold 전환, 전량 판매 시 so_received=1)
- 해당 품목의 포장량(soi_packed_qty)에 스캔 수량 누적 반영(수정 불가 표시)
- 포장 명세를 실제 스캔한 봉투코드 내역(봉투코드·수량·팩/박스/낱장)으로 표시

- 라우트: POST bag/order/phone/manage/scan → Bag::phoneOrderManageScan (JSON, 즉시 처리)
- 잔량 한도는 접수량(soi_qty), 중복/미등록/재고없음 바코드 거부
- CSRF는 쿠키+regenerate 환경이라 응답에 새 토큰을 실어 다음 스캔에 갱신
- phoneOrderManage 페이로드에 주문별 스캔 내역(scans)·포장량 집계 포함
This commit is contained in:
taekyoungc
2026-07-02 19:18:44 +09:00
parent faaadf8543
commit 130fb5ffdb
3 changed files with 291 additions and 34 deletions

View File

@@ -7144,18 +7144,46 @@ SQL;
}
}
// 스캔(=포장·판매)된 봉투코드 내역 — 포장 명세 표시 + 품목별 포장량(soi_packed_qty) 집계에 사용
$scansByOrder = [];
$packedByOrderBag = [];
if ($orderIds !== [] && $db->tableExists('bag_sale_scan_code')) {
$scanRows = $db->table('bag_sale_scan_code')
->select('bssc_so_idx, bssc_bag_code, bssc_bag_name, bssc_code, bssc_unit, bssc_qty')
->where('bssc_lg_idx', $lgIdx)
->whereIn('bssc_so_idx', $orderIds)
->where('bssc_state', 'sold')
->orderBy('bssc_idx', 'ASC')
->get()->getResultArray();
foreach ($scanRows as $s) {
$oid = (int) ($s['bssc_so_idx'] ?? 0);
$code = (string) ($s['bssc_bag_code'] ?? '');
$qty = (int) ($s['bssc_qty'] ?? 0);
$scansByOrder[$oid][] = [
'bag_code' => $code,
'bag_name' => (string) ($s['bssc_bag_name'] ?? ''),
'code' => (string) ($s['bssc_code'] ?? ''),
'unit' => (string) ($s['bssc_unit'] ?? ''),
'qty' => $qty,
];
$packedByOrderBag[$oid][$code] = ($packedByOrderBag[$oid][$code] ?? 0) + $qty;
}
}
$itemsByOrder = [];
foreach ($itemRows as $item) {
$orderId = (int) ($item->soi_so_idx ?? 0);
$code = (string) ($item->soi_bag_code ?? '');
$pack = $unitMap[$code] ?? ['boxSheets' => 0, 'packSheets' => 0];
// 포장량 = 스캔 집계 우선(포장 명세와 일치), 없으면 DB 저장값
$packedQty = $packedByOrderBag[$orderId][$code] ?? (int) ($item->soi_packed_qty ?? 0);
$itemsByOrder[$orderId][] = [
'soi_idx' => (int) ($item->soi_idx ?? 0),
'soi_bag_code' => $code,
'soi_bag_name' => (string) ($item->soi_bag_name ?? ''),
'soi_unit_price' => (int) ($item->soi_unit_price ?? 0),
'soi_qty' => (int) ($item->soi_qty ?? 0),
'soi_packed_qty' => (int) ($item->soi_packed_qty ?? 0),
'soi_packed_qty' => (int) $packedQty,
'soi_amount' => (int) ($item->soi_amount ?? 0),
'soi_box_count' => (int) ($item->soi_box_count ?? 0),
'soi_pack_count' => (int) ($item->soi_pack_count ?? 0),
@@ -7188,6 +7216,7 @@ SQL;
'so_total_amount' => (int) ($order->so_total_amount ?? 0),
'so_status' => (string) ($order->so_status ?? 'normal'),
'items' => $itemsByOrder[$id] ?? [],
'scans' => $scansByOrder[$id] ?? [],
];
}
@@ -7371,6 +7400,154 @@ SQL;
return redirect()->to(site_url('bag/order/phone/manage'))->with('success', '주문 수정 저장이 완료되었습니다.');
}
/**
* 전화접수 관리 — 봉투코드 스캔으로 즉시 판매·포장 처리(JSON 응답).
* 선택된 주문의 판매소로 판매 기록(bag_sale) + 재고 차감 + 팩코드 sold 전환 +
* 해당 품목의 포장량(soi_packed_qty) 반영. 잔량 한도는 접수량(soi_qty).
*/
public function phoneOrderManageScan()
{
helper('admin');
$lgIdx = $this->lgIdx();
if (! $lgIdx) {
return $this->response->setJSON(['ok' => false, 'message' => '지자체를 선택해 주세요.']);
}
$soIdx = (int) ($this->request->getPost('so_idx') ?? 0);
$barcode = trim((string) ($this->request->getPost('barcode') ?? ''));
if ($soIdx <= 0 || $barcode === '') {
return $this->response->setJSON(['ok' => false, 'message' => '주문 또는 바코드 값이 올바르지 않습니다.', 'csrf' => csrf_hash()]);
}
$orderModel = model(ShopOrderModel::class);
$order = $orderModel->where('so_idx', $soIdx)->where('so_lg_idx', $lgIdx)->first();
if (! $order || (string) ($order->so_status ?? '') !== 'normal') {
return $this->response->setJSON(['ok' => false, 'message' => '선택한 주문을 사용할 수 없습니다.', 'csrf' => csrf_hash()]);
}
$scan = $this->resolveDesignatedSaleBarcode($lgIdx, $barcode);
if (! ($scan['ok'] ?? false)) {
return $this->response->setJSON(array_merge($scan, ['csrf' => csrf_hash()]));
}
$bagCode = (string) ($scan['bag_code'] ?? '');
$orderItem = model(ShopOrderItemModel::class)
->where('soi_so_idx', $soIdx)
->where('soi_bag_code', $bagCode)
->first();
if (! $orderItem) {
return $this->response->setJSON(['ok' => false, 'message' => '선택 주문에 없는 봉투 종류입니다.', 'csrf' => csrf_hash()]);
}
// 이미 판매(=포장)된 순수량
$soldRow = model(BagSaleModel::class)
->select('COALESCE(SUM(bs_qty),0) AS sold_qty')
->where('bs_lg_idx', $lgIdx)
->where('bs_so_idx', $soIdx)
->where('bs_bag_code', $bagCode)
->whereIn('bs_type', ['sale', 'cancel', 'return', 'return_cancel'])
->first();
$soldQty = (int) ($soldRow->sold_qty ?? 0);
$receiptQty = (int) ($orderItem->soi_qty ?? 0);
$scanQty = (int) ($scan['qty'] ?? 0);
$remain = max(0, $receiptQty - $soldQty);
if ($scanQty <= 0) {
return $this->response->setJSON(['ok' => false, 'message' => '수량 계산에 실패했습니다.', 'csrf' => csrf_hash()]);
}
if ($scanQty > $remain) {
return $this->response->setJSON(['ok' => false, 'message' => '접수 잔량을 초과합니다. (잔량: ' . number_format($remain) . ')', 'csrf' => csrf_hash()]);
}
$dsIdx = (int) ($order->so_ds_idx ?? 0);
$bagName = (string) (($scan['bag_name'] ?? '') !== '' ? $scan['bag_name'] : ($orderItem->soi_bag_name ?? ''));
$unit = (string) ($scan['unit'] ?? '');
$packIds = is_array($scan['pack_ids'] ?? null) ? $scan['pack_ids'] : [];
$now = date('Y-m-d H:i:s');
$this->ensureDesignatedSaleScanCodeTable($lgIdx);
$db = \Config\Database::connect();
$db->transStart();
// 스캔 코드 기록(sold)
$scanCodeRow = $db->table('bag_sale_scan_code')
->where('bssc_lg_idx', $lgIdx)->where('bssc_code', $barcode)->get()->getRowArray();
if (is_array($scanCodeRow)) {
$db->table('bag_sale_scan_code')->where('bssc_idx', (int) ($scanCodeRow['bssc_idx'] ?? 0))->update([
'bssc_so_idx' => $soIdx, 'bssc_ds_idx' => $dsIdx, 'bssc_bag_code' => $bagCode,
'bssc_bag_name' => $bagName, 'bssc_unit' => $unit, 'bssc_qty' => $scanQty,
'bssc_state' => 'sold', 'bssc_regdate' => $now,
]);
} else {
$db->table('bag_sale_scan_code')->insert([
'bssc_lg_idx' => $lgIdx, 'bssc_so_idx' => $soIdx, 'bssc_ds_idx' => $dsIdx,
'bssc_bag_code' => $bagCode, 'bssc_bag_name' => $bagName, 'bssc_code' => $barcode,
'bssc_unit' => $unit, 'bssc_qty' => $scanQty, 'bssc_state' => 'sold', 'bssc_regdate' => $now,
]);
}
// 팩/박스는 수신 팩 상태를 sold로 전환(낱장은 유지)
if ($packIds !== [] && $unit !== '낱장') {
$db->table('bag_receiving_pack_code')
->where('brpc_lg_idx', $lgIdx)
->whereIn('brpc_idx', array_map(static fn ($v): int => (int) $v, $packIds))
->set(['brpc_state' => 'sold'])->update();
}
// 판매 기록 + 재고 차감
$unitPrice = (float) ($orderItem->soi_unit_price ?? 0);
model(BagSaleModel::class)->insert([
'bs_lg_idx' => $lgIdx, 'bs_so_idx' => $soIdx, 'bs_ds_idx' => $dsIdx,
'bs_ds_name' => (string) ($order->so_ds_name ?? ''), 'bs_sale_date' => date('Y-m-d'),
'bs_bag_code' => $bagCode, 'bs_bag_name' => $bagName, 'bs_qty' => $scanQty,
'bs_unit_price' => $unitPrice, 'bs_amount' => $unitPrice * $scanQty,
'bs_type' => 'sale', 'bs_regdate' => $now,
]);
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, -$scanQty);
// 포장량(soi_packed_qty) 반영
$newPacked = $soldQty + $scanQty;
model(ShopOrderItemModel::class)->update((int) ($orderItem->soi_idx ?? 0), ['soi_packed_qty' => $newPacked]);
// 전량 판매 시 so_received=1
$soldByBag = [];
$sumRows = model(BagSaleModel::class)
->select('bs_bag_code, COALESCE(SUM(bs_qty),0) AS sold_qty')
->where('bs_lg_idx', $lgIdx)->where('bs_so_idx', $soIdx)->where('bs_type', 'sale')
->groupBy('bs_bag_code')->findAll();
foreach ($sumRows as $r) {
$soldByBag[(string) ($r->bs_bag_code ?? '')] = (int) ($r->sold_qty ?? 0);
}
$allItems = model(ShopOrderItemModel::class)->where('soi_so_idx', $soIdx)->findAll();
$allSold = true;
foreach ($allItems as $it) {
if ((int) ($soldByBag[(string) ($it->soi_bag_code ?? '')] ?? 0) < (int) ($it->soi_qty ?? 0)) {
$allSold = false;
break;
}
}
$orderModel->update($soIdx, ['so_received' => $allSold ? 1 : 0]);
$db->transComplete();
if ($db->transStatus() === false) {
return $this->response->setJSON(['ok' => false, 'message' => '판매 처리 중 오류가 발생했습니다.', 'csrf' => csrf_hash()]);
}
return $this->response->setJSON([
'ok' => true,
'csrf' => csrf_hash(),
'so_idx' => $soIdx,
'soi_idx' => (int) ($orderItem->soi_idx ?? 0),
'bag_code' => $bagCode,
'bag_name' => $bagName,
'code' => $barcode,
'unit' => $unit,
'qty' => $scanQty,
'packed_qty' => $newPacked,
'so_received' => $allSold ? 1 : 0,
'remain' => max(0, $receiptQty - $newPacked),
]);
}
/**
* 전화 주문 취소(삭제가 아닌 상태값 변경).
*/