issueModel = model(BagIssueModel::class); $this->issueItemCodeModel = model(BagIssueItemCodeModel::class); } /** * 낱장 수량을 품목코드 단위로 분해한다. * * @return array */ private function buildIssueCodeRows(int $bi2Idx, int $sheetQty, array $packUnit): array { $sheetQty = max(0, $sheetQty); if ($sheetQty <= 0) { return []; } $chunkSize = max( 1, (int) ($packUnit['totalPerBox'] ?? 0), (int) ($packUnit['packPerSheet'] ?? 0) ); $rows = []; $remaining = $sheetQty; $seq = 1; while ($remaining > 0) { $qty = min($chunkSize, $remaining); $rows[] = [ 'issueCode' => sprintf('%d-%06d-%03d', (int) date('y'), $bi2Idx, $seq), 'qty' => $qty, ]; $remaining -= $qty; $seq++; } return $rows; } public function index() { helper('admin'); $lgIdx = admin_effective_lg_idx(); if (! $lgIdx) { return redirect()->to(work_area_home_url())->with('error', '지자체를 선택해 주세요.'); } $builder = $this->issueModel->where('bi2_lg_idx', $lgIdx); $startDate = $this->request->getGet('start_date'); $endDate = $this->request->getGet('end_date'); if ($startDate) { $builder->where('bi2_issue_date >=', $startDate); } if ($endDate) { $builder->where('bi2_issue_date <=', $endDate); } $list = $builder->orderBy('bi2_issue_date', 'DESC')->orderBy('bi2_idx', 'DESC')->paginate(20); $pager = $this->issueModel->pager; return $this->renderWorkPage('무료용 불출 관리', 'admin/bag_issue/index', compact('list', 'startDate', 'endDate', 'pager')); } public function create() { helper('admin'); $lgIdx = admin_effective_lg_idx(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; return $this->renderWorkPage('무료용 불출 처리', 'admin/bag_issue/create', compact('bagCodes')); } public function store() { helper('admin'); $lgIdx = admin_effective_lg_idx(); $rules = [ 'bi2_year' => 'required|is_natural_no_zero', 'bi2_quarter' => 'required|in_list[1,2,3,4]', 'bi2_issue_type' => 'required|max_length[20]', 'bi2_issue_date' => 'required|valid_date[Y-m-d]', 'bi2_dest_name' => 'required|max_length[100]', // 사이트 다건 입력(item_bag_code/item_qty)과 기존 관리자 단건 입력을 함께 허용 'bi2_bag_code' => 'permit_empty|max_length[50]', 'bi2_qty' => 'permit_empty|is_natural_no_zero', ]; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $issueType = trim((string) $this->request->getPost('bi2_issue_type')); $destType = trim((string) ($this->request->getPost('bi2_dest_type') ?? '')); $destName = trim((string) ($this->request->getPost('bi2_dest_name') ?? '')); $destDongCode = trim((string) ($this->request->getPost('bi2_dest_dong_code') ?? '')); if ($destType === '') { $destType = '동사무소'; } if ($issueType === '공공용' && $destType === '동사무소') { $destType = '구청'; } if ($issueType === '무료용' && $destDongCode !== '') { $existsFreeDong = model(FreeRecipientModel::class) ->where('fr_lg_idx', $lgIdx) ->where('fr_state', 1) ->where('fr_dong_code', $destDongCode) ->first(); if (! $existsFreeDong) { return redirect()->back()->withInput()->with('error', '선택한 불출처는 무료용 대상이 아닙니다.'); } } $invRows = model(BagInventoryModel::class) ->where('bi_lg_idx', $lgIdx) ->where('bi_qty >', 0) ->findAll(); $inventoryMap = []; foreach ($invRows as $inv) { $code = (string) ($inv->bi_bag_code ?? ''); if ($code === '') { continue; } $inventoryMap[$code] = [ 'qty' => (int) ($inv->bi_qty ?? 0), 'name' => (string) ($inv->bi_bag_name ?? ''), ]; } $unitRows = model(PackagingUnitModel::class) ->where('pu_lg_idx', $lgIdx) ->where('pu_state', 1) ->findAll(); $packMap = []; foreach ($unitRows as $unit) { $code = (string) ($unit->pu_bag_code ?? ''); if ($code === '') { continue; } $packMap[$code] = [ 'packPerSheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)), 'totalPerBox' => max(1, (int) ($unit->pu_total_per_box ?? 1)), ]; } $items = []; $itemCodes = $this->request->getPost('item_bag_code'); $itemQtys = $this->request->getPost('item_qty'); $itemPacks = $this->request->getPost('item_pack'); $itemCodes = is_array($itemCodes) ? $itemCodes : []; $itemQtys = is_array($itemQtys) ? $itemQtys : []; $itemPacks = is_array($itemPacks) ? $itemPacks : []; $count = max(count($itemCodes), count($itemQtys), count($itemPacks)); for ($i = 0; $i < $count; $i++) { $bagCode = trim((string) ($itemCodes[$i] ?? '')); $qtyRaw = (int) ($itemQtys[$i] ?? 0); $pack = trim((string) ($itemPacks[$i] ?? 'sheet')); if ($bagCode === '' || $qtyRaw <= 0) { continue; } if (! in_array($pack, ['box', 'pack', 'sheet'], true)) { $pack = 'sheet'; } $packUnit = $packMap[$bagCode] ?? ['packPerSheet' => 1, 'totalPerBox' => 1]; $sheetQty = $qtyRaw; if ($pack === 'box') { $sheetQty = $qtyRaw * (int) $packUnit['totalPerBox']; } elseif ($pack === 'pack') { $sheetQty = $qtyRaw * (int) $packUnit['packPerSheet']; } $sheetQty = max(1, (int) $sheetQty); $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $bagCode, $lgIdx) : null; $bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$bagCode]['name'] ?? ''); if ($bagName === '') { $bagName = (string) $bagCode; } $items[] = [ 'bagCode' => $bagCode, 'bagName' => $bagName, 'pack' => $pack, 'rawQty' => $qtyRaw, 'sheetQty' => $sheetQty, ]; } // 기존 관리자 단건 폼과의 호환 if ($items === []) { $singleBagCode = trim((string) $this->request->getPost('bi2_bag_code')); $singleQty = (int) $this->request->getPost('bi2_qty'); if ($singleBagCode !== '' && $singleQty > 0) { $detail = $kindO ? model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $kindO->ck_idx, $singleBagCode, $lgIdx) : null; $bagName = $detail ? (string) ($detail->cd_name ?? '') : (string) ($inventoryMap[$singleBagCode]['name'] ?? ''); if ($bagName === '') { $bagName = (string) $singleBagCode; } $items[] = [ 'bagCode' => $singleBagCode, 'bagName' => $bagName, 'pack' => 'sheet', 'rawQty' => $singleQty, 'sheetQty' => $singleQty, ]; } } if ($items === []) { return redirect()->back()->withInput()->with('error', '불출 품목을 1건 이상 입력해 주세요.'); } $requiredByBag = []; foreach ($items as $item) { $code = (string) $item['bagCode']; if (! isset($requiredByBag[$code])) { $requiredByBag[$code] = 0; } $requiredByBag[$code] += (int) $item['sheetQty']; } foreach ($requiredByBag as $code => $requiredQty) { $available = (int) ($inventoryMap[$code]['qty'] ?? 0); if ($available <= 0) { return redirect()->back()->withInput()->with('error', '입고 재고가 없는 봉투코드는 불출할 수 없습니다: ' . $code); } if ($available < $requiredQty) { return redirect()->back()->withInput()->with('error', '재고가 부족합니다: ' . $code . ' (재고 ' . number_format($available) . ', 요청 ' . number_format($requiredQty) . ')'); } } $db = \Config\Database::connect(); $db->transStart(); $hasIssueCodeTable = $db->tableExists('bag_issue_item_code'); $issueYear = (int) $this->request->getPost('bi2_year'); $issueQuarter = (int) $this->request->getPost('bi2_quarter'); $issueDate = (string) $this->request->getPost('bi2_issue_date'); $createdCount = 0; helper('audit'); foreach ($items as $item) { $issueData = [ 'bi2_lg_idx' => $lgIdx, 'bi2_year' => $issueYear, 'bi2_quarter' => $issueQuarter, 'bi2_issue_type' => $issueType, 'bi2_issue_date' => $issueDate, 'bi2_dest_type' => $destType, 'bi2_dest_name' => $destName, 'bi2_bag_code' => (string) $item['bagCode'], 'bi2_bag_name' => (string) $item['bagName'], 'bi2_qty' => (int) $item['sheetQty'], 'bi2_status' => 'normal', 'bi2_regdate' => date('Y-m-d H:i:s'), ]; $this->issueModel->insert($issueData); $bi2Idx = (int) $this->issueModel->getInsertID(); audit_log('create', 'bag_issue', $bi2Idx, null, array_merge($issueData, ['bi2_idx' => $bi2Idx])); if ($hasIssueCodeTable) { $codeRows = $this->buildIssueCodeRows($bi2Idx, (int) $item['sheetQty'], $packMap[(string) $item['bagCode']] ?? []); foreach ($codeRows as $codeRow) { $this->issueItemCodeModel->insert([ 'bic_lg_idx' => $lgIdx, 'bic_bi2_idx' => $bi2Idx, 'bic_bag_code' => (string) $item['bagCode'], 'bic_issue_code' => (string) $codeRow['issueCode'], 'bic_qty' => (int) $codeRow['qty'], 'bic_cancel_qty' => 0, 'bic_state' => 'normal', 'bic_regdate' => date('Y-m-d H:i:s'), ]); } } model(BagInventoryModel::class)->adjustQty( $lgIdx, (string) $item['bagCode'], (string) $item['bagName'], -((int) $item['sheetQty']) ); $createdCount++; } $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->withInput()->with('error', '불출 처리 중 오류가 발생했습니다.'); } return redirect()->to(mgmt_url('bag-issues'))->with('success', $createdCount . '건 불출 처리되었습니다.'); } public function cancel(int $id) { helper('admin'); $item = $this->issueModel->find($id); if (! $item || (int) $item->bi2_lg_idx !== admin_effective_lg_idx()) { return redirect()->to(mgmt_url('bag-issues'))->with('error', '불출 내역을 찾을 수 없습니다.'); } $db = \Config\Database::connect(); $db->transStart(); $hasIssueCodeTable = $db->tableExists('bag_issue_item_code'); $before = (array) $item; $this->issueModel->update($id, ['bi2_status' => 'cancelled']); helper('audit'); audit_log('update', 'bag_issue', $id, $before, ['bi2_status' => 'cancelled']); $restoreQty = (int) $item->bi2_qty; if ($hasIssueCodeTable) { $codeRows = $db->table('bag_issue_item_code') ->select('bic_idx, bic_qty, bic_cancel_qty') ->where('bic_lg_idx', (int) $item->bi2_lg_idx) ->where('bic_bi2_idx', $id) ->get() ->getResultArray(); $restoreQty = 0; foreach ($codeRows as $codeRow) { $bicIdx = (int) ($codeRow['bic_idx'] ?? 0); $qty = (int) ($codeRow['bic_qty'] ?? 0); $oldCancel = (int) ($codeRow['bic_cancel_qty'] ?? 0); $restoreQty += max(0, $qty - $oldCancel); $db->table('bag_issue_item_code') ->where('bic_idx', $bicIdx) ->update([ 'bic_cancel_qty' => $qty, 'bic_state' => 'cancelled', ]); } } model(BagInventoryModel::class)->adjustQty((int) $item->bi2_lg_idx, $item->bi2_bag_code, $item->bi2_bag_name, $restoreQty); $this->issueModel->update($id, ['bi2_qty' => 0, 'bi2_status' => 'cancelled']); $db->transComplete(); return redirect()->to(mgmt_url('bag-issues'))->with('success', '불출이 취소되었습니다.'); } }