, defaultSenderIdx: int} */ private function receivingManagerPickers(int $lgIdx): array { $senders = model(ManagerModel::class, false) ->where('mg_lg_idx', $lgIdx) ->where('mg_state', 1) ->where('mg_dept_code', 'company') ->orderBy('mg_name', 'ASC') ->findAll(); $sessionName = trim((string) (session()->get('mb_name') ?? '')); $defaultSenderIdx = 0; foreach ($senders as $s) { if ((string) ($s->mg_name ?? '') === $sessionName) { $defaultSenderIdx = (int) ($s->mg_idx ?? 0); break; } } if ($defaultSenderIdx <= 0 && $senders !== []) { $defaultSenderIdx = (int) ($senders[0]->mg_idx ?? 0); } return [ 'senders' => $senders, 'defaultSenderIdx' => $defaultSenderIdx, ]; } /** * 인수자 드롭다운: 맨 위에 현재 로그인 회원, 이어서 대행소(agency) 담당자. * value 는 br_receiver_ref 로 전달: m_{mb_idx} | g_{mg_idx} * * @return array{receiverOptions: list, defaultReceiverRef: string} */ private function receivingReceiverSelect(int $lgIdx): array { $sessionMbIdx = (int) (session()->get('mb_idx') ?? 0); $sessionName = trim((string) (session()->get('mb_name') ?? '')); $normalizeName = static fn (string $name): string => preg_replace('/\s+/u', '', trim($name)) ?? ''; $normSession = $normalizeName($sessionName); $options = []; if ($sessionMbIdx > 0) { $label = $sessionName !== '' ? $sessionName : '로그인 사용자'; $options[] = ['ref' => 'm_' . $sessionMbIdx, 'label' => $label]; } $agencyManagers = model(ManagerModel::class, false) ->where('mg_lg_idx', $lgIdx) ->where('mg_state', 1) ->where('mg_dept_code', 'agency') ->orderBy('mg_name', 'ASC') ->findAll(); foreach ($agencyManagers as $rcv) { $mgIdx = (int) ($rcv->mg_idx ?? 0); $receiverName = trim((string) ($rcv->mg_name ?? '')); if ($mgIdx <= 0) { continue; } if ($normSession !== '' && $normalizeName($receiverName) === $normSession) { continue; } $options[] = ['ref' => 'g_' . $mgIdx, 'label' => $receiverName]; } $defaultRef = $options !== [] ? (string) ($options[0]['ref'] ?? '') : ''; return [ 'receiverOptions' => $options, 'defaultReceiverRef' => $defaultRef, ]; } /** * @param list $options */ private function sanitizeReceiverRef(array $options, string $ref): string { foreach ($options as $opt) { if (($opt['ref'] ?? '') === $ref) { return $ref; } } return ''; } private function parseReceiverRefToStoredIdx(int $lgIdx, string $ref): int { $ref = trim($ref); if (preg_match('/^m_(\d+)$/', $ref, $mm)) { $mbIdx = (int) $mm[1]; if ($mbIdx <= 0 || $mbIdx !== (int) (session()->get('mb_idx') ?? 0)) { return 0; } return $mbIdx; } if (preg_match('/^g_(\d+)$/', $ref, $mg)) { return $this->assertAgencyReceiverIdx($lgIdx, (int) $mg[1]); } return 0; } private function assertAgencyReceiverIdx(int $lgIdx, int $mgIdx): int { if ($mgIdx <= 0) { return 0; } $row = model(ManagerModel::class, false)->where([ 'mg_idx' => $mgIdx, 'mg_lg_idx' => $lgIdx, 'mg_state' => 1, 'mg_dept_code' => 'agency', ])->first(); return $row ? $mgIdx : 0; } private function resolveCompanySenderName(int $lgIdx, int $mgIdx): string { if ($mgIdx <= 0) { return ''; } $row = model(ManagerModel::class, false)->where([ 'mg_idx' => $mgIdx, 'mg_lg_idx' => $lgIdx, 'mg_state' => 1, 'mg_dept_code' => 'company', ])->first(); return $row ? trim((string) ($row->mg_name ?? '')) : ''; } private function render(string $title, string $viewFile, array $data = []): string { return view('bag/layout/main', [ 'title' => $title, 'content' => view($viewFile, $data), ]); } // ────────────────────────────────────────────── // 기본정보관리 (단가·포장 단위 진입 허브) // ────────────────────────────────────────────── public function basicInfo(): string { return $this->render('기본정보관리', 'bag/basic_info', []); } /** 봉투 단가 조회 (사이트) — 기간·봉투구분·봉투코드 필터, 적용기간 겹침, 페이징·인쇄 */ public function prices(): string|RedirectResponse { helper('admin'); if ($this->request->is('post')) { $post = $this->request->getPost(); $pick = static function (array $src, string $key): ?string { if (! array_key_exists($key, $src)) { return null; } $v = $src[$key]; if ($v === null || is_array($v)) { return null; } $s = trim((string) $v); return $s === '' ? null : $s; }; session()->setFlashdata('bag_prices_filter', [ 'start_y' => $pick($post, 'start_y'), 'start_m' => $pick($post, 'start_m'), 'start_d' => $pick($post, 'start_d'), 'end_y' => $pick($post, 'end_y'), 'end_m' => $pick($post, 'end_m'), 'end_d' => $pick($post, 'end_d'), 'bag_kind_e' => $pick($post, 'bag_kind_e'), 'bag_code' => $pick($post, 'bag_code'), ]); return redirect()->to(site_url('bag/prices')); } $lgIdx = $this->lgIdx(); $bagPrices = []; $get = $this->request->getGet(); $flash = session()->getFlashdata('bag_prices_filter'); $readSrc = static function (array $src, string $key): ?string { if (! array_key_exists($key, $src)) { return null; } $v = $src[$key]; if ($v === null || is_array($v)) { return null; } $s = trim((string) $v); return $s === '' ? null : $s; }; $filterKeys = [ 'start_y', 'start_m', 'start_d', 'end_y', 'end_m', 'end_d', 'bag_kind_e', 'bag_code', 'start_date', 'end_date', ]; $hasExplicitGetFilter = false; foreach ($filterKeys as $fk) { $v = $get[$fk] ?? null; if ($v !== null && ! is_array($v) && trim((string) $v) !== '') { $hasExplicitGetFilter = true; break; } } $src = []; if ($hasExplicitGetFilter) { $src = $get; } elseif (is_array($flash)) { $src = $flash; } $sy = $readSrc($src, 'start_y'); $sm = $readSrc($src, 'start_m'); $sd = $readSrc($src, 'start_d'); $ey = $readSrc($src, 'end_y'); $em = $readSrc($src, 'end_m'); $ed = $readSrc($src, 'end_d'); $startDate = null; if ($sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== '') { $startDate = parse_ymd_from_triple($sy, $sm, $sd); } if ($startDate === null) { $g = $readSrc($src, 'start_date'); $startDate = ($g !== null && $g !== '') ? $g : null; } $endDate = null; if ($ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== '') { $endDate = parse_ymd_from_triple($ey, $em, $ed); } if ($endDate === null) { $g = $readSrc($src, 'end_date'); $endDate = ($g !== null && $g !== '') ? $g : null; } $startParts = ['y' => '', 'm' => '', 'd' => '']; $endParts = ['y' => '', 'm' => '', 'd' => '']; if ($startDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $startDate, $m)) { $startParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]]; } elseif ($sy !== null && $sm !== null && $sd !== null && $sy !== '' && $sm !== '' && $sd !== '') { $iy = (int) $sy; $im = (int) $sm; $id = (int) $sd; if ($iy >= 1000 && $iy <= 9999 && $im >= 1 && $im <= 12 && $id >= 1 && $id <= 31) { $startParts = ['y' => (string) $iy, 'm' => $im, 'd' => $id]; } } if ($endDate !== null && preg_match('/^(\d{4})-(\d{2})-(\d{2})$/', $endDate, $m)) { $endParts = ['y' => $m[1], 'm' => (int) $m[2], 'd' => (int) $m[3]]; } elseif ($ey !== null && $em !== null && $ed !== null && $ey !== '' && $em !== '' && $ed !== '') { $iy = (int) $ey; $im = (int) $em; $id = (int) $ed; if ($iy >= 1000 && $iy <= 9999 && $im >= 1 && $im <= 12 && $id >= 1 && $id <= 31) { $endParts = ['y' => (string) $iy, 'm' => $im, 'd' => $id]; } } $dateYearMin = (int) date('Y') - 12; $dateYearMax = (int) date('Y') + 2; $bagKindE = $readSrc($src, 'bag_kind_e'); $bagCode = $readSrc($src, 'bag_code'); $pager = null; $bagCodes = []; $bagKindOpts = []; $printLines = []; $printLgName = ''; if ($lgIdx !== null) { try { $priceModel = model(BagPriceModel::class); $builder = $priceModel->where('bp_lg_idx', $lgIdx); if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) { $qStart = ($startDate !== null && $startDate !== '') ? $startDate : $endDate; $qEnd = ($endDate !== null && $endDate !== '') ? $endDate : $startDate; if (strcmp((string) $qStart, (string) $qEnd) > 0) { [$qStart, $qEnd] = [$qEnd, $qStart]; } $builder->where('bp_start_date <=', $qEnd); $builder->groupStart() ->where('bp_end_date IS NULL') ->orWhere('bp_end_date >=', $qStart) ->groupEnd(); } if ($bagKindE !== null && $bagKindE !== '') { $ek = model(CodeKindModel::class)->where('ck_code', 'E')->first(); if ($ek) { $eDetail = model(CodeDetailModel::class) ->where('cd_ck_idx', (int) $ek->ck_idx) ->where('cd_code', $bagKindE) ->where('cd_state', 1) ->first(); if ($eDetail !== null) { $builder->like('bp_bag_code', (string) $bagKindE, 'after'); } } } if ($bagCode !== null && $bagCode !== '') { $ok = model(CodeKindModel::class)->where('ck_code', 'O')->first(); if ($ok) { $oDetail = model(CodeDetailModel::class)->findResolvedByKindAndCode((int) $ok->ck_idx, (string) $bagCode, $lgIdx); if ($oDetail !== null) { $builder->where('bp_bag_code', $bagCode); } } } $bagPrices = $builder->orderBy('bp_bag_code', 'ASC')->orderBy('bp_start_date', 'DESC')->paginate(20); $queryForPager = []; $tripleS = $sy !== null && $sy !== '' && $sm !== null && $sm !== '' && $sd !== null && $sd !== ''; $tripleE = $ey !== null && $ey !== '' && $em !== null && $em !== '' && $ed !== null && $ed !== ''; if ($tripleS) { $queryForPager['start_y'] = $sy; $queryForPager['start_m'] = $sm; $queryForPager['start_d'] = $sd; } else { $legacyS = $readSrc($src, 'start_date'); if ($legacyS !== null) { $queryForPager['start_date'] = $legacyS; } } if ($tripleE) { $queryForPager['end_y'] = $ey; $queryForPager['end_m'] = $em; $queryForPager['end_d'] = $ed; } else { $legacyE = $readSrc($src, 'end_date'); if ($legacyE !== null) { $queryForPager['end_date'] = $legacyE; } } if ($bagKindE !== null && $bagKindE !== '') { $queryForPager['bag_kind_e'] = $bagKindE; } if ($bagCode !== null && $bagCode !== '') { $queryForPager['bag_code'] = $bagCode; } $queryForPager = array_filter( $queryForPager, static fn ($v) => $v !== null && $v !== '' ); $pagerPath = site_url('bag/prices'); if ($queryForPager !== []) { $pagerPath .= '?' . http_build_query($queryForPager); } $priceModel->pager->setPath($pagerPath); $pager = $priceModel->pager; $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kindO ? model(CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) : []; $kindE = model(CodeKindModel::class)->where('ck_code', 'E')->first(); $bagKindOpts = $kindE ? model(CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null) : []; $lgRow = model(LocalGovernmentModel::class)->find($lgIdx); $printLgName = $lgRow !== null ? $lgRow->lg_name : ''; } catch (DatabaseException $e) { log_message('error', '[prices] bag_price 조회 실패: ' . $e->getMessage()); } } if (($startDate !== null && $startDate !== '') || ($endDate !== null && $endDate !== '')) { $qs = ($startDate !== null && $startDate !== '') ? $startDate : $endDate; $qe = ($endDate !== null && $endDate !== '') ? $endDate : $startDate; if (strcmp((string) $qs, (string) $qe) > 0) { [$qs, $qe] = [$qe, $qs]; } $printLines[] = '조회기간(적용기간 겹침): ' . format_ymd_korean($qs) . ' ~ ' . format_ymd_korean($qe); } if ($bagKindE !== null && $bagKindE !== '') { foreach ($bagKindOpts as $cd) { if ((string) $cd->cd_code === (string) $bagKindE) { $printLines[] = '봉투구분: ' . $cd->cd_name . ' (' . $bagKindE . ')'; break; } } } if ($bagCode !== null && $bagCode !== '') { $printLines[] = '봉투코드: ' . $bagCode; } $viewData = [ 'lgIdx' => $lgIdx, 'bagPrices' => $bagPrices, 'pager' => $pager, 'startDate' => $startDate, 'endDate' => $endDate, 'startParts' => $startParts, 'endParts' => $endParts, 'dateYearMin' => $dateYearMin, 'dateYearMax' => $dateYearMax, 'bag_kind_e' => $bagKindE, 'bag_code' => $bagCode, 'bag_codes' => $bagCodes, 'bag_kind_options' => $bagKindOpts, 'printExtraLines' => $printLines, ]; if ($printLgName !== '') { $viewData['printLgName'] = $printLgName; } return $this->render('봉투 단가', 'bag/prices', $viewData); } /** 포장 단위 조회 (사이트) */ public function packagingUnits(): string { $lgIdx = $this->lgIdx(); $packagingUnits = []; if ($lgIdx) { try { $packagingUnits = model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->orderBy('pu_bag_code', 'ASC')->findAll(); } catch (DatabaseException $e) { log_message('error', '[packagingUnits] packaging_unit 조회 실패: ' . $e->getMessage()); } } return $this->render('포장 단위', 'bag/packaging_units', ['packagingUnits' => $packagingUnits]); } /** * 기본코드 종류·세부코드 조회 전용 (사이트 메뉴 기본코드관리) */ public function codeKinds(): string { $kindModel = model(CodeKindModel::class); $detailModel = model(CodeDetailModel::class); $kinds = []; $countMap = []; $selectedKind = null; $detailList = []; $rowCanEdit = []; $lgIdx = $this->lgIdx(); try { $kinds = $kindModel->orderBy('ck_code', 'ASC')->findAll(); foreach ($kinds as $row) { $countMap[$row->ck_idx] = (int) $detailModel->where('cd_ck_idx', $row->ck_idx) ->filterByTenantScope($lgIdx) ->countAllResults(); } } catch (\Throwable $e) { log_message('error', '[codeKinds] 실패: {type} {message} @ {file}:{line} / lg={lg}, user={user}, level={level}', [ 'type' => $e::class, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'lg' => $lgIdx !== null ? (string) $lgIdx : 'null', 'user' => (string) (session()->get('mb_id') ?? ''), 'level' => (string) (session()->get('mb_level') ?? ''), ]); session()->setFlashdata('error', '기본코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.'); } $level = (int) session()->get('mb_level'); $canManageDetails = Roles::canManageCodeMaster($level); if ($kinds !== []) { $selectedCkIdx = (int) ($this->request->getGet('ck_idx') ?? 0); foreach ($kinds as $row) { if ((int) $row->ck_idx === $selectedCkIdx) { $selectedKind = $row; break; } } if ($selectedKind === null) { $selectedKind = $kinds[0]; } } if ($selectedKind !== null) { $detailList = $detailModel->where('cd_ck_idx', (int) $selectedKind->ck_idx) ->filterByTenantScope($lgIdx) ->orderBy('cd_sort', 'ASC') ->orderBy('cd_code', 'ASC') ->orderBy('cd_idx', 'ASC') ->findAll(); helper('admin'); $adminLg = admin_effective_lg_idx(); foreach ($detailList as $row) { $rowCanEdit[$row->cd_idx] = Roles::canEditCodeDetailRow($level, $row, $adminLg); } } return $this->render('기본코드관리', 'bag/code_kinds', [ 'codeKinds' => $kinds, 'countMap' => $countMap, 'canManageKinds' => Roles::canManageCodeKindMaster($level), 'canManageDetails' => $canManageDetails, 'selectedKind' => $selectedKind, 'detailList' => $detailList, 'rowCanEdit' => $rowCanEdit, ]); } /** * 기본코드 세부 목록 (사이트 레이아웃). 등록·수정·삭제 폼은 /admin/code-details/* 유지. */ public function codeDetails(int $ckIdx) { $kindModel = model(CodeKindModel::class); $detailModel = model(CodeDetailModel::class); $kind = null; try { $kind = $kindModel->find($ckIdx); } catch (\Throwable $e) { log_message('error', '[codeDetails] kind 조회 실패: {type} {message} @ {file}:{line} / ck={ck}, user={user}, level={level}', [ 'type' => $e::class, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'ck' => (string) $ckIdx, 'user' => (string) (session()->get('mb_id') ?? ''), 'level' => (string) (session()->get('mb_level') ?? ''), ]); return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.'); } if ($kind === null) { return redirect()->to(site_url('bag/code-kinds'))->with('error', '코드 종류를 찾을 수 없습니다.'); } $lgIdx = $this->lgIdx(); try { $list = $detailModel->where('cd_ck_idx', $ckIdx) ->filterByTenantScope($lgIdx) ->orderBy('cd_sort', 'ASC') ->orderBy('cd_code', 'ASC') ->orderBy('cd_idx', 'ASC') ->paginate(20); $pager = $detailModel->pager; } catch (\Throwable $e) { log_message('error', '[codeDetails] list 조회 실패: {type} {message} @ {file}:{line} / ck={ck}, lg={lg}, user={user}, level={level}', [ 'type' => $e::class, 'message' => $e->getMessage(), 'file' => $e->getFile(), 'line' => $e->getLine(), 'ck' => (string) $ckIdx, 'lg' => $lgIdx !== null ? (string) $lgIdx : 'null', 'user' => (string) (session()->get('mb_id') ?? ''), 'level' => (string) (session()->get('mb_level') ?? ''), ]); return redirect()->to(site_url('bag/code-kinds'))->with('error', '세부코드 조회 중 오류가 발생했습니다. 관리자에게 로그 확인을 요청해 주세요.'); } helper('admin'); $level = (int) session()->get('mb_level'); $adminLg = admin_effective_lg_idx(); $canManage = Roles::canManageCodeMaster($level); $rowCanEdit = []; foreach ($list as $row) { $rowCanEdit[$row->cd_idx] = Roles::canEditCodeDetailRow($level, $row, $adminLg); } $title = ($canManage ? '세부코드 관리' : '세부코드 조회') . ' — ' . $kind->ck_name . ' (' . $kind->ck_code . ')'; return $this->render($title, 'bag/code_details', [ 'kind' => $kind, 'list' => $list, 'pager' => $pager, 'canManage' => $canManage, 'rowCanEdit' => $rowCanEdit, ]); } // ────────────────────────────────────────────── // 발주 입고 관리 // ────────────────────────────────────────────── public function purchaseInbound(): string { $lgIdx = $this->lgIdx(); $data = ['orders' => [], 'receivings' => [], 'startDate' => null, 'endDate' => null]; if ($lgIdx) { $startDate = $this->request->getGet('start_date'); $endDate = $this->request->getGet('end_date'); $data['startDate'] = $startDate; $data['endDate'] = $endDate; // 발주 목록 $orderBuilder = model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->whereLatestHead($lgIdx); if ($startDate) $orderBuilder->where('bo_order_date >=', $startDate); if ($endDate) $orderBuilder->where('bo_order_date <=', $endDate); $data['orders'] = $orderBuilder->orderBy('bo_order_date', 'DESC')->paginate(20, 'orders'); $data['orderPager'] = model(BagOrderModel::class)->pager; // 발주별 품목 합계 $itemSummary = []; foreach ($data['orders'] as $order) { $items = model(BagOrderItemModel::class)->where('boi_bo_idx', $order->bo_idx)->findAll(); $totalQty = 0; $totalAmt = 0; foreach ($items as $it) { $totalQty += (int) $it->boi_qty_sheet; $totalAmt += (float) $it->boi_amount; } $itemSummary[$order->bo_idx] = ['qty' => $totalQty, 'amount' => $totalAmt, 'count' => count($items)]; } $data['itemSummary'] = $itemSummary; // 입고 목록 $recvBuilder = model(BagReceivingModel::class)->where('br_lg_idx', $lgIdx); if ($startDate) $recvBuilder->where('br_receive_date >=', $startDate); if ($endDate) $recvBuilder->where('br_receive_date <=', $endDate); $data['receivings'] = $recvBuilder->orderBy('br_receive_date', 'DESC')->paginate(20, 'receivings'); $data['recvPager'] = model(BagReceivingModel::class)->pager; } return $this->render('발주 입고 관리', 'bag/purchase_inbound', $data); } // ────────────────────────────────────────────── // 불출 관리 // ────────────────────────────────────────────── public function issueLegacy(): RedirectResponse { return redirect()->to(site_url('bag/issue/cancel')); } public function issue(): string { $lgIdx = $this->lgIdx(); $data = [ 'filters' => [ 'issue_month' => (string) ($this->request->getGet('issue_month') ?? ''), 'dest_name' => (string) ($this->request->getGet('dest_name') ?? ''), 'issue_type' => (string) ($this->request->getGet('issue_type') ?? ''), 'bag_code' => (string) ($this->request->getGet('bag_code') ?? ''), ], 'monthOptions' => [], 'destOptions' => [], 'typeOptions' => ['무료용', '공공용'], 'bagOptions' => [], 'issueGroups' => [], 'detailRows' => [], 'detailSourceRows' => [], 'codeRows' => [], 'selectedGroupDate' => '', 'selectedGroupDest' => '', 'selectedIssueId' => 0, 'selectedBagCode' => '', ]; if (! $lgIdx) { return $this->render('불출 관리', 'bag/issue', $data); } $db = \Config\Database::connect(); $issueTable = $db->table('bag_issue'); $hasItemCodeTable = $db->tableExists('bag_issue_item_code'); $filterMonth = trim((string) $data['filters']['issue_month']); $filterDest = trim((string) $data['filters']['dest_name']); $filterType = trim((string) $data['filters']['issue_type']); $filterBag = trim((string) $data['filters']['bag_code']); $applyCommonFilters = static function ($builder) use ($lgIdx, $filterMonth, $filterDest, $filterType, $filterBag): void { $builder->where('bi2_lg_idx', $lgIdx); if (preg_match('/^\d{4}-\d{2}$/', $filterMonth) === 1) { $start = $filterMonth . '-01'; $end = date('Y-m-t', strtotime($start)); $builder->where('bi2_issue_date >=', $start); $builder->where('bi2_issue_date <=', $end); } if ($filterDest !== '') { $builder->where('bi2_dest_name', $filterDest); } if ($filterType !== '') { $builder->where('bi2_issue_type', $filterType); } if ($filterBag !== '') { $builder->where('bi2_bag_code', $filterBag); } }; $monthRows = $db->table('bag_issue') ->select("DATE_FORMAT(bi2_issue_date, '%Y-%m') AS issue_month", false) ->where('bi2_lg_idx', $lgIdx) ->groupBy("DATE_FORMAT(bi2_issue_date, '%Y-%m')", false) ->orderBy('issue_month', 'DESC') ->get() ->getResultArray(); foreach ($monthRows as $row) { $month = (string) ($row['issue_month'] ?? ''); if ($month !== '') { $data['monthOptions'][] = $month; } } $destRows = $db->table('bag_issue') ->select('bi2_dest_name') ->where('bi2_lg_idx', $lgIdx) ->groupBy('bi2_dest_name') ->orderBy('bi2_dest_name', 'ASC') ->get() ->getResultArray(); $data['destOptions'] = array_values(array_filter(array_map(static fn ($r): string => (string) ($r['bi2_dest_name'] ?? ''), $destRows))); $bagRows = $db->table('bag_issue') ->select('bi2_bag_code, MAX(bi2_bag_name) AS bi2_bag_name', false) ->where('bi2_lg_idx', $lgIdx) ->groupBy('bi2_bag_code') ->orderBy('bi2_bag_code', 'ASC') ->get() ->getResultArray(); foreach ($bagRows as $row) { $code = (string) ($row['bi2_bag_code'] ?? ''); if ($code === '') { continue; } $data['bagOptions'][] = [ 'code' => $code, 'name' => (string) ($row['bi2_bag_name'] ?? $code), ]; } $groupBuilder = $db->table('bag_issue') ->select('bi2_issue_date, bi2_dest_name, COUNT(*) AS row_count, SUM(bi2_qty) AS total_qty', false) ->groupBy('bi2_issue_date, bi2_dest_name'); $applyCommonFilters($groupBuilder); $data['issueGroups'] = $groupBuilder ->orderBy('bi2_issue_date', 'DESC') ->orderBy('bi2_dest_name', 'ASC') ->get() ->getResultArray(); $selectedDate = (string) ($this->request->getGet('sel_date') ?? ''); $selectedDest = (string) ($this->request->getGet('sel_dest') ?? ''); if (($selectedDate === '' || $selectedDest === '') && $data['issueGroups'] !== []) { $selectedDate = (string) ($data['issueGroups'][0]['bi2_issue_date'] ?? ''); $selectedDest = (string) ($data['issueGroups'][0]['bi2_dest_name'] ?? ''); } $data['selectedGroupDate'] = $selectedDate; $data['selectedGroupDest'] = $selectedDest; if ($selectedDate !== '' && $selectedDest !== '') { $detailBuilder = $db->table('bag_issue') ->select('bi2_idx, bi2_issue_date, bi2_issue_type, bi2_bag_code, bi2_bag_name, bi2_qty, bi2_status') ->where('bi2_lg_idx', $lgIdx) ->where('bi2_issue_date', $selectedDate) ->where('bi2_dest_name', $selectedDest); if ($filterType !== '') { $detailBuilder->where('bi2_issue_type', $filterType); } if ($filterBag !== '') { $detailBuilder->where('bi2_bag_code', $filterBag); } $data['detailRows'] = $detailBuilder ->orderBy('bi2_idx', 'ASC') ->get() ->getResultArray(); $data['detailSourceRows'] = $data['detailRows']; } $detailIssueIds = array_map(static fn ($row): int => (int) ($row['bi2_idx'] ?? 0), $data['detailRows']); $cancelMap = []; $codeQtyMap = []; if ($hasItemCodeTable && $detailIssueIds !== []) { $aggRows = $db->table('bag_issue_item_code') ->select('bic_bi2_idx, SUM(bic_qty) AS sum_qty, SUM(bic_cancel_qty) AS sum_cancel', false) ->whereIn('bic_bi2_idx', $detailIssueIds) ->groupBy('bic_bi2_idx') ->get() ->getResultArray(); foreach ($aggRows as $agg) { $idx = (int) ($agg['bic_bi2_idx'] ?? 0); if ($idx <= 0) { continue; } $cancelMap[$idx] = (int) ($agg['sum_cancel'] ?? 0); $codeQtyMap[$idx] = (int) ($agg['sum_qty'] ?? 0); } } foreach ($data['detailRows'] as &$row) { $idx = (int) ($row['bi2_idx'] ?? 0); $cancelQty = (int) ($cancelMap[$idx] ?? 0); $baseQty = isset($codeQtyMap[$idx]) ? (int) $codeQtyMap[$idx] : ((int) ($row['bi2_qty'] ?? 0) + $cancelQty); $row['base_qty'] = max(0, $baseQty); $row['cancel_qty'] = max(0, min($row['base_qty'], $cancelQty)); $row['remain_qty'] = max(0, $row['base_qty'] - $row['cancel_qty']); } unset($row); $data['detailSourceRows'] = $data['detailRows']; $aggByBag = []; foreach ($data['detailRows'] as $row) { $bagCodeKey = (string) ($row['bi2_bag_code'] ?? ''); if ($bagCodeKey === '') { continue; } if (! isset($aggByBag[$bagCodeKey])) { $aggByBag[$bagCodeKey] = [ 'bi2_issue_date' => (string) ($row['bi2_issue_date'] ?? ''), 'bi2_issue_type' => (string) ($row['bi2_issue_type'] ?? ''), 'bi2_bag_code' => $bagCodeKey, 'bi2_bag_name' => (string) ($row['bi2_bag_name'] ?? $bagCodeKey), 'base_qty' => 0, 'cancel_qty' => 0, 'issue_ids' => [], ]; } $aggByBag[$bagCodeKey]['base_qty'] += (int) ($row['base_qty'] ?? 0); $aggByBag[$bagCodeKey]['cancel_qty'] += (int) ($row['cancel_qty'] ?? 0); $aggByBag[$bagCodeKey]['issue_ids'][] = (int) ($row['bi2_idx'] ?? 0); } $data['detailRows'] = array_values($aggByBag); $selectedIssueId = (int) ($this->request->getGet('sel_issue_id') ?? 0); if ($selectedIssueId <= 0 && $data['detailRows'] !== []) { $selectedIssueId = (int) (($data['detailRows'][0]['issue_ids'][0] ?? 0)); } $data['selectedIssueId'] = $selectedIssueId; $selectedBagCode = trim((string) ($this->request->getGet('sel_bag_code') ?? '')); if ($selectedBagCode === '' && $data['detailRows'] !== []) { $selectedBagCode = (string) ($data['detailRows'][0]['bi2_bag_code'] ?? ''); } $data['selectedBagCode'] = $selectedBagCode; if ($selectedBagCode !== '') { $selectedIssueIds = []; foreach (($data['detailRows'] ?? []) as $detailRow) { if ((string) ($detailRow['bi2_bag_code'] ?? '') !== $selectedBagCode) { continue; } $selectedIssueIds = array_values(array_filter(array_map('intval', (array) ($detailRow['issue_ids'] ?? [])))); break; } $sourceByIssue = []; foreach (($data['detailSourceRows'] ?? []) as $sourceRow) { if ((string) ($sourceRow['bi2_bag_code'] ?? '') !== $selectedBagCode) { continue; } $sourceIssueId = (int) ($sourceRow['bi2_idx'] ?? 0); if ($sourceIssueId <= 0) { continue; } $sourceByIssue[$sourceIssueId] = $sourceRow; } if ($hasItemCodeTable) { if ($selectedIssueIds !== []) { $existingRows = $db->table('bag_issue_item_code') ->select('bic_bi2_idx') ->where('bic_lg_idx', $lgIdx) ->where('bic_bag_code', $selectedBagCode) ->whereIn('bic_bi2_idx', $selectedIssueIds) ->groupBy('bic_bi2_idx') ->get() ->getResultArray(); $existingIssueSet = []; foreach ($existingRows as $existingRow) { $issueId = (int) ($existingRow['bic_bi2_idx'] ?? 0); if ($issueId > 0) { $existingIssueSet[$issueId] = true; } } foreach ($selectedIssueIds as $issueId) { $issueId = (int) $issueId; if ($issueId <= 0 || isset($existingIssueSet[$issueId])) { continue; } $source = $sourceByIssue[$issueId] ?? null; if (! is_array($source)) { continue; } $sourceQty = max(0, (int) ($source['base_qty'] ?? 0)); if ($sourceQty <= 0) { continue; } $sourceCancel = max(0, min($sourceQty, (int) ($source['cancel_qty'] ?? 0))); $db->table('bag_issue_item_code')->insert([ 'bic_lg_idx' => $lgIdx, 'bic_bi2_idx' => $issueId, 'bic_bag_code' => $selectedBagCode, 'bic_issue_code' => sprintf('%02d-%06d-001', (int) date('y'), $issueId), 'bic_qty' => $sourceQty, 'bic_cancel_qty' => $sourceCancel, 'bic_state' => ($sourceCancel >= $sourceQty) ? 'cancelled' : 'normal', 'bic_regdate' => date('Y-m-d H:i:s'), ]); } $data['codeRows'] = $db->table('bag_issue_item_code') ->select('bic_idx, bic_bi2_idx, bic_issue_code, bic_qty, bic_cancel_qty') ->where('bic_lg_idx', $lgIdx) ->where('bic_bag_code', $selectedBagCode) ->whereIn('bic_bi2_idx', $selectedIssueIds) ->orderBy('bic_bi2_idx', 'ASC') ->orderBy('bic_idx', 'ASC') ->get() ->getResultArray(); } } $existingIssueIds = []; foreach (($data['codeRows'] ?? []) as $codeRow) { $existingIssueId = (int) ($codeRow['bic_bi2_idx'] ?? 0); if ($existingIssueId > 0) { $existingIssueIds[$existingIssueId] = true; } } foreach ($sourceByIssue as $sourceIssueId => $sourceRow) { if (isset($existingIssueIds[$sourceIssueId])) { continue; } $data['codeRows'][] = [ 'bic_idx' => 0, 'bic_bi2_idx' => $sourceIssueId, 'bic_issue_code' => sprintf('%02d-%06d-001', (int) date('y'), $sourceIssueId), 'bic_qty' => (int) ($sourceRow['base_qty'] ?? 0), 'bic_cancel_qty' => (int) ($sourceRow['cancel_qty'] ?? 0), ]; } if (($data['codeRows'] ?? []) !== []) { usort($data['codeRows'], static function (array $a, array $b): int { $issueCmp = ((int) ($a['bic_bi2_idx'] ?? 0)) <=> ((int) ($b['bic_bi2_idx'] ?? 0)); if ($issueCmp !== 0) { return $issueCmp; } return ((int) ($a['bic_idx'] ?? 0)) <=> ((int) ($b['bic_idx'] ?? 0)); }); } } return $this->render('불출 관리', 'bag/issue', $data); } public function issueCancelSave(): RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/issue/cancel'))->with('error', '지자체를 선택해 주세요.'); } $db = \Config\Database::connect(); $issueModel = model(BagIssueModel::class); $inventoryModel = model(BagInventoryModel::class); $hasItemCodeTable = $db->tableExists('bag_issue_item_code'); $codeCancelQtyInput = $this->request->getPost('code_cancel_qty'); $codeCancelQtyInput = is_array($codeCancelQtyInput) ? $codeCancelQtyInput : []; $codeCheckedInput = $this->request->getPost('code_cancel_check'); $codeCheckedInput = is_array($codeCheckedInput) ? $codeCheckedInput : []; $issueCancelQtyInput = $this->request->getPost('issue_cancel_qty'); $issueCancelQtyInput = is_array($issueCancelQtyInput) ? $issueCancelQtyInput : []; $issueCheckedInput = $this->request->getPost('issue_cancel_check'); $issueCheckedInput = is_array($issueCheckedInput) ? $issueCheckedInput : []; $issueDeltaMap = []; $touchedIssueIds = []; $db->transStart(); if ($hasItemCodeTable && $codeCancelQtyInput !== []) { $codeIds = array_values(array_unique(array_map('intval', array_keys($codeCancelQtyInput)))); $codeIds = array_values(array_filter($codeIds, static fn ($v): bool => $v > 0)); if ($codeIds !== []) { $rows = $db->table('bag_issue_item_code') ->select('bic_idx, bic_bi2_idx, bic_qty, bic_cancel_qty') ->where('bic_lg_idx', $lgIdx) ->whereIn('bic_idx', $codeIds) ->get() ->getResultArray(); foreach ($rows as $row) { $bicIdx = (int) ($row['bic_idx'] ?? 0); $bi2Idx = (int) ($row['bic_bi2_idx'] ?? 0); $qty = (int) ($row['bic_qty'] ?? 0); $oldCancel = (int) ($row['bic_cancel_qty'] ?? 0); $isChecked = isset($codeCheckedInput[(string) $bicIdx]); $inputCancel = (int) ($codeCancelQtyInput[(string) $bicIdx] ?? 0); $newCancel = $isChecked ? $qty : max(0, min($qty, $inputCancel)); if ($newCancel === $oldCancel) { continue; } $db->table('bag_issue_item_code') ->where('bic_idx', $bicIdx) ->update([ 'bic_cancel_qty' => $newCancel, 'bic_state' => ($newCancel >= $qty) ? 'cancelled' : 'normal', ]); if (! isset($issueDeltaMap[$bi2Idx])) { $issueDeltaMap[$bi2Idx] = 0; } $issueDeltaMap[$bi2Idx] += ($newCancel - $oldCancel); $touchedIssueIds[$bi2Idx] = true; } } } $fallbackIssueIds = array_values(array_unique(array_map('intval', array_keys($issueCancelQtyInput)))); $fallbackIssueIds = array_values(array_filter($fallbackIssueIds, static fn ($v): bool => $v > 0)); if ($fallbackIssueIds !== []) { $issueRows = $issueModel ->where('bi2_lg_idx', $lgIdx) ->whereIn('bi2_idx', $fallbackIssueIds) ->findAll(); foreach ($issueRows as $issueRow) { $bi2Idx = (int) ($issueRow->bi2_idx ?? 0); if ($bi2Idx <= 0 || isset($touchedIssueIds[$bi2Idx])) { continue; } $baseQty = (int) ($issueRow->bi2_qty ?? 0); $isChecked = isset($issueCheckedInput[(string) $bi2Idx]); $inputCancel = (int) ($issueCancelQtyInput[(string) $bi2Idx] ?? 0); $newCancel = $isChecked ? $baseQty : max(0, min($baseQty, $inputCancel)); if ($newCancel <= 0) { continue; } $issueModel->update($bi2Idx, [ 'bi2_qty' => $baseQty - $newCancel, 'bi2_status' => ($newCancel >= $baseQty) ? 'cancelled' : 'normal', ]); $inventoryModel->adjustQty( $lgIdx, (string) ($issueRow->bi2_bag_code ?? ''), (string) ($issueRow->bi2_bag_name ?? ''), $newCancel ); } } if ($touchedIssueIds !== []) { $issueIds = array_keys($touchedIssueIds); $aggRows = $db->table('bag_issue_item_code') ->select('bic_bi2_idx, SUM(bic_qty) AS sum_qty, SUM(bic_cancel_qty) AS sum_cancel', false) ->whereIn('bic_bi2_idx', $issueIds) ->groupBy('bic_bi2_idx') ->get() ->getResultArray(); $aggMap = []; foreach ($aggRows as $row) { $idx = (int) ($row['bic_bi2_idx'] ?? 0); if ($idx <= 0) { continue; } $aggMap[$idx] = [ 'sum_qty' => (int) ($row['sum_qty'] ?? 0), 'sum_cancel' => (int) ($row['sum_cancel'] ?? 0), ]; } $issues = $issueModel->where('bi2_lg_idx', $lgIdx)->whereIn('bi2_idx', $issueIds)->findAll(); foreach ($issues as $issue) { $bi2Idx = (int) ($issue->bi2_idx ?? 0); $sumQty = (int) ($aggMap[$bi2Idx]['sum_qty'] ?? (int) ($issue->bi2_qty ?? 0)); $sumCancel = (int) ($aggMap[$bi2Idx]['sum_cancel'] ?? 0); $remain = max(0, $sumQty - $sumCancel); $issueModel->update($bi2Idx, [ 'bi2_qty' => $remain, 'bi2_status' => ($remain <= 0 ? 'cancelled' : 'normal'), ]); $delta = (int) ($issueDeltaMap[$bi2Idx] ?? 0); if ($delta !== 0) { $inventoryModel->adjustQty( $lgIdx, (string) ($issue->bi2_bag_code ?? ''), (string) ($issue->bi2_bag_name ?? ''), $delta ); } } } $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->withInput()->with('error', '불출 취소 저장 중 오류가 발생했습니다.'); } return redirect()->back()->with('success', '불출 취소 수량이 저장되었습니다.'); } // ────────────────────────────────────────────── // 재고 관리 // ────────────────────────────────────────────── public function inventory(): string { $lgIdx = $this->lgIdx(); $baseDate = trim((string) ($this->request->getGet('base_date') ?? date('Y-m-d'))); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $baseDate)) { $baseDate = date('Y-m-d'); } $agencyIdx = (int) ($this->request->getGet('agency_idx') ?? 0); $data = [ 'baseDate' => $baseDate, 'agencyIdx' => $agencyIdx, 'agencyOptions' => [], 'rows' => [], 'subtotals' => [], 'grandTotals' => ['total' => 0, 'gugun' => 0, 'agency' => 0], ]; if ($lgIdx) { $agencyModel = model(SalesAgencyModel::class); $data['agencyOptions'] = $agencyModel ->where('sa_lg_idx', $lgIdx) ->orderForDisplay() ->findAll(); $report = $this->buildInventoryStatusData($lgIdx, $baseDate, $agencyIdx); $data = array_merge($data, $report); } return $this->render('재고 현황', 'bag/inventory', $data); } public function inventoryExport(): ResponseInterface|RedirectResponse { helper(['admin', 'export']); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $baseDate = trim((string) ($this->request->getGet('base_date') ?? date('Y-m-d'))); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $baseDate)) { $baseDate = date('Y-m-d'); } $agencyIdx = (int) ($this->request->getGet('agency_idx') ?? 0); $report = $this->buildInventoryStatusData($lgIdx, $baseDate, $agencyIdx); $rows = []; foreach (($report['rows'] ?? []) as $row) { $rows[] = [ (string) ($row['group'] ?? ''), (string) ($row['name'] ?? ''), (int) ($row['total_qty'] ?? 0), (int) ($row['gugun_qty'] ?? 0), (int) ($row['agency_qty'] ?? 0), ]; } foreach (($report['subtotals'] ?? []) as $subtotal) { $rows[] = [ (string) ($subtotal['group'] ?? ''), '소계', (int) ($subtotal['total_qty'] ?? 0), (int) ($subtotal['gugun_qty'] ?? 0), (int) ($subtotal['agency_qty'] ?? 0), ]; } $rows[] = [ '', '합계', (int) ($report['grandTotals']['total'] ?? 0), (int) ($report['grandTotals']['gugun'] ?? 0), (int) ($report['grandTotals']['agency'] ?? 0), ]; export_xlsx( '재고현황_' . str_replace('-', '', $baseDate) . '.xlsx', '재고현황', ['품목구분', '봉투/스티커 종류', '계', '시군구 재고', '대행소 재고'], $rows ); } /** * @return array{ * rows: list, * subtotals: list, * grandTotals: array{total:int,gugun:int,agency:int} * } */ private function buildInventoryStatusData(int $lgIdx, string $baseDate, int $agencyIdx): array { $builder = model(BagInventoryModel::class) ->where('bi_lg_idx', $lgIdx) ->where('bi_updated_at <=', $baseDate . ' 23:59:59') ->orderBy('bi_bag_code', 'ASC'); // 대행소 재고 연계 테이블이 아직 없어 agency 필터는 조회조건 표시용으로만 유지한다. if ($agencyIdx > 0) { // no-op } $list = $builder->findAll(); $rows = []; $subtotalMap = []; $groupOrder = []; $grand = ['total' => 0, 'gugun' => 0, 'agency' => 0]; foreach ($list as $row) { $bagName = trim((string) ($row->bi_bag_name ?? '')); $bagCode = trim((string) ($row->bi_bag_code ?? '')); $group = $this->inventoryGroupLabel($bagName, $bagCode); if (! isset($groupOrder[$group])) { $groupOrder[$group] = count($groupOrder); } $gugunQty = max(0, (int) ($row->bi_qty ?? 0)); $agencyQty = 0; $totalQty = $gugunQty + $agencyQty; $rows[] = [ 'group' => $group, 'name' => $bagName !== '' ? $bagName : $bagCode, 'total_qty' => $totalQty, 'gugun_qty' => $gugunQty, 'agency_qty' => $agencyQty, '_sort' => $groupOrder[$group], ]; if (! isset($subtotalMap[$group])) { $subtotalMap[$group] = ['group' => $group, 'total_qty' => 0, 'gugun_qty' => 0, 'agency_qty' => 0]; } $subtotalMap[$group]['total_qty'] += $totalQty; $subtotalMap[$group]['gugun_qty'] += $gugunQty; $subtotalMap[$group]['agency_qty'] += $agencyQty; $grand['total'] += $totalQty; $grand['gugun'] += $gugunQty; $grand['agency'] += $agencyQty; } usort($rows, static function (array $a, array $b): int { $g = ((int) ($a['_sort'] ?? 0)) <=> ((int) ($b['_sort'] ?? 0)); if ($g !== 0) { return $g; } return strnatcmp((string) ($a['name'] ?? ''), (string) ($b['name'] ?? '')); }); foreach ($rows as &$row) { unset($row['_sort']); } unset($row); $subtotals = array_values($subtotalMap); usort($subtotals, static function (array $a, array $b) use ($groupOrder): int { return ((int) ($groupOrder[$a['group']] ?? 0)) <=> ((int) ($groupOrder[$b['group']] ?? 0)); }); return [ 'rows' => $rows, 'subtotals' => $subtotals, 'grandTotals' => $grand, ]; } private function inventoryGroupLabel(string $bagName, string $bagCode): string { $name = trim($bagName); $code = trim($bagCode); $source = $name !== '' ? $name : $code; if (mb_strpos($source, '스티커') !== false) { if (mb_strpos($source, '음식물') !== false) { return '음식물 스티커'; } if (mb_strpos($source, '폐기물') !== false) { return '대형폐기물 스티커'; } return '기타 스티커'; } if (mb_strpos($source, '재사용') !== false) { return '재사용'; } if (mb_strpos($source, '공공') !== false || mb_strpos($source, '공동주택') !== false) { return '공공용'; } if (mb_strpos($source, '음식물') !== false) { return '음식물 봉투'; } return '일반용'; } public function inspectionSelect(): string|RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $this->ensureInspectionPackSnapshotTable(); $today = date('Y-m-d'); $startDate = trim((string) ($this->request->getGet('start_date') ?? date('Y-m-01'))); $endDate = trim((string) ($this->request->getGet('end_date') ?? $today)); $workDate = trim((string) ($this->request->getGet('work_date') ?? $today)); $itemCode = trim((string) ($this->request->getGet('item_code') ?? '')); $selectedInspectionId = (int) ($this->request->getGet('bis_id') ?? 0); $viewType = trim((string) ($this->request->getGet('view_type') ?? 'box')); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { $startDate = date('Y-m-01'); } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { $endDate = $today; } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) { $workDate = $today; } if (! in_array($viewType, ['box', 'pack'], true)) { $viewType = 'box'; } $inventoryRows = model(BagInventoryModel::class) ->where('bi_lg_idx', $lgIdx) ->orderBy('bi_bag_code', 'ASC') ->findAll(); $db = \Config\Database::connect(); $barcodeRows = $db->table('bag_receiving_pack_code') ->select('brpc_bag_code') ->distinct() ->where('brpc_lg_idx', $lgIdx) ->where('brpc_bag_code !=', '') ->get() ->getResultArray(); $barcodeSet = []; foreach ($barcodeRows as $row) { $code = trim((string) ($row['brpc_bag_code'] ?? '')); if ($code !== '') { $barcodeSet[$code] = true; } } $popupItems = []; foreach ($inventoryRows as $inv) { $code = trim((string) ($inv->bi_bag_code ?? '')); if ($code === '') { continue; } $name = trim((string) ($inv->bi_bag_name ?? $code)); $qty = (int) ($inv->bi_qty ?? 0); $hasBarcode = isset($barcodeSet[$code]); $popupItems[] = [ 'bag_code' => $code, 'bag_name' => $name, 'qty' => $qty, 'has_barcode' => $hasBarcode, ]; } if ($selectedInspectionId <= 0) { $latestInspection = $db->table('bag_inventory_inspection') ->select("bis_idx, (CASE bis_status WHEN 'confirmed' THEN 3 WHEN 'counting' THEN 2 WHEN 'selected' THEN 1 ELSE 0 END) AS status_rank", false) ->where('bis_lg_idx', $lgIdx) ->where('bis_work_date >=', $startDate) ->where('bis_work_date <=', $endDate) ->orderBy('status_rank', 'DESC') ->orderBy('bis_work_date', 'DESC') ->orderBy('bis_idx', 'DESC') ->get() ->getRowArray(); $selectedInspectionId = (int) ($latestInspection['bis_idx'] ?? 0); } $inspectionRuns = $db->table('bag_inventory_inspection') ->select('bis_idx, bis_work_date, bis_status') ->where('bis_lg_idx', $lgIdx) ->where('bis_work_date >=', $startDate) ->where('bis_work_date <=', $endDate) ->orderBy('bis_work_date', 'DESC') ->orderBy('bis_idx', 'DESC') ->get() ->getResultArray(); $overviewBuilder = $db->table('bag_inventory_inspection_item i') ->select('i.bisi_idx, i.bisi_bis_idx, i.bisi_bag_code, i.bisi_bag_name, i.bisi_system_qty, h.bis_work_date, h.bis_status') ->join('bag_inventory_inspection h', 'h.bis_idx = i.bisi_bis_idx', 'inner') ->where('h.bis_lg_idx', $lgIdx) ->where('h.bis_work_date >=', $startDate) ->where('h.bis_work_date <=', $endDate); if ($selectedInspectionId > 0) { $overviewBuilder->where('i.bisi_bis_idx', $selectedInspectionId); } if ($itemCode !== '') { $overviewBuilder->where('i.bisi_bag_code', $itemCode); } $overviewRows = $overviewBuilder ->orderBy('h.bis_work_date', 'ASC') ->orderBy('i.bisi_bag_code', 'ASC') ->orderBy('i.bisi_idx', 'ASC') ->get() ->getResultArray(); $overviewRows = $this->expandInspectionRowsByBox($db, $lgIdx, $overviewRows, true); $selectedInspectionItemId = (int) ($this->request->getGet('sel_item_id') ?? 0); if ($selectedInspectionItemId <= 0 && $overviewRows !== []) { $selectedInspectionItemId = (int) ($overviewRows[0]['bisi_idx'] ?? 0); } $selectedInspectionItem = null; foreach ($overviewRows as $row) { if ((int) ($row['bisi_idx'] ?? 0) === $selectedInspectionItemId) { $selectedInspectionItem = $row; $selectedInspectionId = (int) ($row['bisi_bis_idx'] ?? 0); break; } } $items = []; foreach ($overviewRows as $row) { $code = trim((string) ($row['bisi_bag_code'] ?? '')); if ($code === '' || isset($items[$code])) { continue; } $items[$code] = [ 'bag_code' => $code, 'bag_name' => trim((string) ($row['bisi_bag_name'] ?? $code)), ]; } $items = array_values($items); $boxRows = []; $sheetRows = []; $selectedBoxCode = trim((string) ($this->request->getGet('sel_box_code') ?? '')); $selectedPackCode = trim((string) ($this->request->getGet('sel_pack_code') ?? '')); if (is_array($selectedInspectionItem)) { $this->ensureInspectionPackSnapshotForItem($lgIdx, $selectedInspectionItemId); $bagCode = trim((string) ($selectedInspectionItem['bisi_bag_code'] ?? '')); if ($bagCode !== '') { $boxRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_idx, bisp_box_code, bisp_pack_code, bisp_sheet_start_code, bisp_sheet_end_code, bisp_sheet_qty') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bisi_idx', $selectedInspectionItemId) ->where('bisp_bag_code', $bagCode) ->orderBy('bisp_sheet_qty', 'DESC') ->orderBy('bisp_idx', 'ASC') ->get() ->getResultArray(); } if ($selectedBoxCode === '' && $boxRows !== []) { $selectedBoxCode = (string) ($boxRows[0]['bisp_box_code'] ?? ''); } if ($selectedPackCode === '' && $boxRows !== []) { $selectedPackCode = (string) ($boxRows[0]['bisp_pack_code'] ?? ''); } foreach ($boxRows as $boxRow) { $boxCode = (string) ($boxRow['bisp_box_code'] ?? ''); $packCode = (string) ($boxRow['bisp_pack_code'] ?? ''); if ($boxCode === '' || $packCode === '') { continue; } if ($boxCode !== $selectedBoxCode || $packCode !== $selectedPackCode) { continue; } $startCode = (string) ($boxRow['bisp_sheet_start_code'] ?? ''); $endCode = (string) ($boxRow['bisp_sheet_end_code'] ?? ''); $sheetRows = [[ 'no' => 1, 'biss_sheet_code' => $startCode . ' ~ ' . $endCode, 'biss_system_qty' => max(0, (int) ($boxRow['bisp_sheet_qty'] ?? 0)), ]]; break; } } return $this->render('실사 선별 조회', 'bag/inventory_inspection_select_overview', [ 'startDate' => $startDate, 'endDate' => $endDate, 'workDate' => $workDate, 'itemCode' => $itemCode, 'viewType' => $viewType, 'inspectionRuns' => $inspectionRuns, 'items' => $items, 'selectedInspectionId' => $selectedInspectionId, 'selectedInspectionItemId' => $selectedInspectionItemId, 'overviewRows' => $overviewRows, 'boxRows' => $boxRows, 'sheetRows' => $sheetRows, 'selectedBoxCode' => $selectedBoxCode, 'selectedPackCode' => $selectedPackCode, 'popupItems' => $popupItems, ]); } public function inspectionWork(): string|RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $this->ensureInspectionPackSnapshotTable(); $this->ensureInspectionSheetSnapshotTable(); $today = date('Y-m-d'); $startDate = trim((string) ($this->request->getGet('start_date') ?? date('Y-m-01'))); $endDate = trim((string) ($this->request->getGet('end_date') ?? $today)); $workDate = trim((string) ($this->request->getGet('work_date') ?? $today)); $itemCode = trim((string) ($this->request->getGet('item_code') ?? '')); $selectedInspectionId = (int) ($this->request->getGet('bis_id') ?? 0); $viewType = trim((string) ($this->request->getGet('view_type') ?? 'box')); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) { $startDate = date('Y-m-01'); } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) { $endDate = $today; } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) { $workDate = $today; } if (! in_array($viewType, ['box', 'pack'], true)) { $viewType = 'box'; } $inventoryRows = model(BagInventoryModel::class) ->where('bi_lg_idx', $lgIdx) ->orderBy('bi_bag_code', 'ASC') ->findAll(); $db = \Config\Database::connect(); $barcodeRows = $db->table('bag_receiving_pack_code') ->select('brpc_bag_code') ->distinct() ->where('brpc_lg_idx', $lgIdx) ->where('brpc_bag_code !=', '') ->get() ->getResultArray(); $barcodeSet = []; foreach ($barcodeRows as $row) { $code = trim((string) ($row['brpc_bag_code'] ?? '')); if ($code !== '') { $barcodeSet[$code] = true; } } $popupItems = []; foreach ($inventoryRows as $inv) { $code = trim((string) ($inv->bi_bag_code ?? '')); if ($code === '') { continue; } $popupItems[] = [ 'bag_code' => $code, 'bag_name' => trim((string) ($inv->bi_bag_name ?? $code)), 'qty' => (int) ($inv->bi_qty ?? 0), 'has_barcode' => isset($barcodeSet[$code]), ]; } if ($selectedInspectionId <= 0) { $latestInspection = $db->table('bag_inventory_inspection') ->select("bis_idx, (CASE bis_status WHEN 'confirmed' THEN 3 WHEN 'counting' THEN 2 WHEN 'selected' THEN 1 ELSE 0 END) AS status_rank", false) ->where('bis_lg_idx', $lgIdx) ->where('bis_work_date >=', $startDate) ->where('bis_work_date <=', $endDate) ->orderBy('status_rank', 'DESC') ->orderBy('bis_work_date', 'DESC') ->orderBy('bis_idx', 'DESC') ->get() ->getRowArray(); $selectedInspectionId = (int) ($latestInspection['bis_idx'] ?? 0); } $inspectionRuns = $db->table('bag_inventory_inspection') ->select('bis_idx, bis_work_date, bis_status') ->where('bis_lg_idx', $lgIdx) ->where('bis_work_date >=', $startDate) ->where('bis_work_date <=', $endDate) ->orderBy('bis_work_date', 'DESC') ->orderBy('bis_idx', 'DESC') ->get() ->getResultArray(); $requestedInspectionItemId = (int) ($this->request->getGet('sel_item_id') ?? 0); if ($requestedInspectionItemId > 0) { $this->ensureInspectionPackSnapshotForItem($lgIdx, $requestedInspectionItemId); } $overviewBuilder = $db->table('bag_inventory_inspection_item i') ->select('i.bisi_idx, i.bisi_bis_idx, i.bisi_bag_code, i.bisi_bag_name, i.bisi_system_qty, i.bisi_actual_qty, i.bisi_diff_qty, i.bisi_apply_yn, h.bis_work_date, h.bis_status') ->join('bag_inventory_inspection h', 'h.bis_idx = i.bisi_bis_idx', 'inner') ->where('h.bis_lg_idx', $lgIdx) ->where('h.bis_work_date >=', $startDate) ->where('h.bis_work_date <=', $endDate); if ($selectedInspectionId > 0) { $overviewBuilder->where('i.bisi_bis_idx', $selectedInspectionId); } if ($itemCode !== '') { $overviewBuilder->where('i.bisi_bag_code', $itemCode); } $overviewRows = $overviewBuilder ->orderBy('h.bis_work_date', 'ASC') ->orderBy('i.bisi_bag_code', 'ASC') ->orderBy('i.bisi_idx', 'ASC') ->get() ->getResultArray(); $overviewRows = $this->expandInspectionRowsByBox($db, $lgIdx, $overviewRows, true); $selectedInspectionItemId = (int) ($this->request->getGet('sel_item_id') ?? 0); if ($selectedInspectionItemId <= 0 && $overviewRows !== []) { $selectedInspectionItemId = (int) ($overviewRows[0]['bisi_idx'] ?? 0); } $selectedInspectionItem = null; foreach ($overviewRows as $row) { if ((int) ($row['bisi_idx'] ?? 0) === $selectedInspectionItemId) { $selectedInspectionItem = $row; $selectedInspectionId = (int) ($row['bisi_bis_idx'] ?? 0); break; } } $items = []; foreach ($overviewRows as $row) { $code = trim((string) ($row['bisi_bag_code'] ?? '')); if ($code === '' || isset($items[$code])) { continue; } $items[$code] = [ 'bag_code' => $code, 'bag_name' => trim((string) ($row['bisi_bag_name'] ?? $code)), 'qty' => (int) ($row['bisi_system_qty'] ?? 0), 'has_barcode' => true, ]; } $items = array_values($items); $boxRows = []; $sheetRows = []; $selectedBoxCode = trim((string) ($this->request->getGet('sel_box_code') ?? '')); $selectedPackCode = trim((string) ($this->request->getGet('sel_pack_code') ?? '')); if (is_array($selectedInspectionItem)) { $this->ensureInspectionPackSnapshotForItem($lgIdx, $selectedInspectionItemId); $bagCode = trim((string) ($selectedInspectionItem['bisi_bag_code'] ?? '')); if ($bagCode !== '') { $boxRowsAll = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_idx, bisp_box_code, bisp_pack_code, bisp_sheet_start_code, bisp_sheet_end_code, bisp_sheet_qty, bisp_actual_qty, bisp_diff_qty') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bisi_idx', $selectedInspectionItemId) ->where('bisp_bag_code', $bagCode) ->orderBy('bisp_sheet_qty', 'DESC') ->orderBy('bisp_idx', 'ASC') ->get() ->getResultArray(); foreach ($boxRowsAll as &$boxRow) { $systemQty = max(0, (int) ($boxRow['bisp_sheet_qty'] ?? 0)); $actualRaw = $boxRow['bisp_actual_qty'] ?? null; $actualQty = $actualRaw === null ? $systemQty : max(0, (int) $actualRaw); // 화면 초기 표시값은 포장량/재고/실사재고를 동일하게 맞춘다. $displayQty = $actualQty; $boxRow['bisp_sheet_qty'] = $displayQty; $boxRow['bisp_actual_qty'] = $displayQty; $boxRow['bisp_diff_qty'] = 0; } unset($boxRow); if ($selectedBoxCode !== '') { $boxRows = array_values(array_filter( $boxRowsAll, static fn (array $row): bool => trim((string) ($row['bisp_box_code'] ?? '')) === $selectedBoxCode )); } else { $boxRows = $boxRowsAll; } } if ($selectedBoxCode === '' && $boxRows !== []) { $selectedBoxCode = (string) ($boxRows[0]['bisp_box_code'] ?? ''); $boxRows = array_values(array_filter( $boxRows, static fn (array $row): bool => trim((string) ($row['bisp_box_code'] ?? '')) === $selectedBoxCode )); } if ($selectedPackCode === '' && $boxRows !== []) { $selectedPackCode = (string) ($boxRows[0]['bisp_pack_code'] ?? ''); } foreach ($boxRows as $boxRow) { $boxCode = (string) ($boxRow['bisp_box_code'] ?? ''); $packCode = (string) ($boxRow['bisp_pack_code'] ?? ''); if ($boxCode === '' || $packCode === '') { continue; } if ($boxCode !== $selectedBoxCode || $packCode !== $selectedPackCode) { continue; } $this->ensureInspectionSheetSnapshotForPack( $lgIdx, $selectedInspectionItemId, $packCode, (string) ($boxRow['bisp_sheet_start_code'] ?? ''), (string) ($boxRow['bisp_sheet_end_code'] ?? '') ); $sheetRows = $db->table('bag_inventory_inspection_sheet_snapshot') ->select('biss_idx, biss_sheet_code, biss_system_qty, biss_actual_qty, biss_diff_qty, biss_checked_yn') ->where('biss_lg_idx', $lgIdx) ->where('biss_bisi_idx', $selectedInspectionItemId) ->where('biss_pack_code', $packCode) ->orderBy('biss_sheet_code', 'ASC') ->get() ->getResultArray(); $n = 1; foreach ($sheetRows as &$sr) { $sr['no'] = $n++; } unset($sr); break; } } return $this->render('실사 선별 관리', 'bag/inventory_inspection_select', [ 'startDate' => $startDate, 'endDate' => $endDate, 'workDate' => $workDate, 'itemCode' => $itemCode, 'viewType' => $viewType, 'inspectionRuns' => $inspectionRuns, 'items' => $items, 'popupItems' => $popupItems, 'overviewRows' => $overviewRows, 'selectedInspectionItemId' => $selectedInspectionItemId, 'selectedInspectionId' => $selectedInspectionId, 'boxRows' => $boxRows, 'selectedBoxCode' => $selectedBoxCode, 'selectedPackCode' => $selectedPackCode, 'sheetRows' => $sheetRows, ]); } /** * @return list */ private function expandInspectionRowsByBox(\CodeIgniter\Database\BaseConnection $db, int $lgIdx, array $overviewRows, bool $includeActual): array { if ($overviewRows === []) { return []; } $itemIds = array_values(array_filter(array_map( static fn (array $r): int => (int) ($r['bisi_idx'] ?? 0), $overviewRows ))); if ($itemIds === []) { return $overviewRows; } $boxAggRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_bisi_idx, bisp_box_code, SUM(bisp_sheet_qty) AS sum_system, SUM(COALESCE(bisp_actual_qty,0)) AS sum_actual, SUM(COALESCE(bisp_diff_qty,0)) AS sum_diff', false) ->where('bisp_lg_idx', $lgIdx) ->whereIn('bisp_bisi_idx', $itemIds) ->groupBy('bisp_bisi_idx, bisp_box_code') ->orderBy('bisp_bisi_idx', 'ASC') ->orderBy('bisp_box_code', 'ASC') ->get() ->getResultArray(); $boxAggMap = []; foreach ($boxAggRows as $bRow) { $id = (int) ($bRow['bisp_bisi_idx'] ?? 0); if ($id <= 0) { continue; } $boxAggMap[$id][] = $bRow; } $expandedRows = []; foreach ($overviewRows as $row) { $itemId = (int) ($row['bisi_idx'] ?? 0); $group = $boxAggMap[$itemId] ?? []; if ($group === []) { $row['box_code'] = ''; $expandedRows[] = $row; continue; } foreach ($group as $g) { $expanded = $row; $expanded['box_code'] = trim((string) ($g['bisp_box_code'] ?? '')); $expanded['bisi_total_system_qty'] = (int) ($row['bisi_system_qty'] ?? 0); $expanded['bisi_system_qty'] = (int) ($g['sum_system'] ?? 0); if ($includeActual) { $expanded['bisi_actual_qty'] = (int) ($g['sum_actual'] ?? 0); $expanded['bisi_diff_qty'] = (int) ($g['sum_diff'] ?? 0); } $expandedRows[] = $expanded; } } return $expandedRows; } private function expandInspectionRowsByPack(\CodeIgniter\Database\BaseConnection $db, int $lgIdx, array $overviewRows, bool $includeActual): array { if ($overviewRows === []) { return []; } $itemIds = array_values(array_filter(array_map( static fn (array $r): int => (int) ($r['bisi_idx'] ?? 0), $overviewRows ))); if ($itemIds === []) { return $overviewRows; } $packRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_bisi_idx, bisp_idx, bisp_box_code, bisp_sheet_qty, COALESCE(bisp_actual_qty,0) AS bisp_actual_qty, COALESCE(bisp_diff_qty,0) AS bisp_diff_qty', false) ->where('bisp_lg_idx', $lgIdx) ->whereIn('bisp_bisi_idx', $itemIds) ->orderBy('bisp_bisi_idx', 'ASC') ->orderBy('bisp_box_code', 'ASC') ->orderBy('bisp_idx', 'ASC') ->get() ->getResultArray(); $packMap = []; foreach ($packRows as $pRow) { $id = (int) ($pRow['bisp_bisi_idx'] ?? 0); if ($id <= 0) { continue; } $packMap[$id][] = $pRow; } $expandedRows = []; foreach ($overviewRows as $row) { $itemId = (int) ($row['bisi_idx'] ?? 0); $group = $packMap[$itemId] ?? []; if ($group === []) { $row['box_code'] = ''; $expandedRows[] = $row; continue; } foreach ($group as $g) { $expanded = $row; $expanded['box_code'] = trim((string) ($g['bisp_box_code'] ?? '')); $expanded['bisi_total_system_qty'] = (int) ($row['bisi_system_qty'] ?? 0); $expanded['bisi_system_qty'] = (int) ($g['bisp_sheet_qty'] ?? 0); if ($includeActual) { $expanded['bisi_actual_qty'] = (int) ($g['bisp_actual_qty'] ?? 0); $expanded['bisi_diff_qty'] = (int) ($g['bisp_diff_qty'] ?? 0); } $expandedRows[] = $expanded; } } return $expandedRows; } /** * @return list */ private function expandSheetCodes(string $startCode, string $endCode): array { $startCode = trim($startCode); $endCode = trim($endCode); if ($startCode === '' || $endCode === '') { return []; } if (preg_match('/^(.*?)(\d+)$/', $startCode, $sm) !== 1 || preg_match('/^(.*?)(\d+)$/', $endCode, $em) !== 1) { return [['no' => 1, 'sheet_code' => $startCode, 'qty' => 1]]; } $startPrefix = (string) ($sm[1] ?? ''); $endPrefix = (string) ($em[1] ?? ''); $startNumRaw = (string) ($sm[2] ?? ''); $endNumRaw = (string) ($em[2] ?? ''); if ($startPrefix !== $endPrefix) { return [['no' => 1, 'sheet_code' => $startCode, 'qty' => 1]]; } $startNum = (int) $startNumRaw; $endNum = (int) $endNumRaw; if ($startNum <= 0 || $endNum < $startNum) { return [['no' => 1, 'sheet_code' => $startCode, 'qty' => 1]]; } $width = max(strlen($startNumRaw), strlen($endNumRaw)); $rows = []; $no = 1; for ($n = $startNum; $n <= $endNum; $n++) { $rows[] = [ 'no' => $no++, 'sheet_code' => $startPrefix . str_pad((string) $n, $width, '0', STR_PAD_LEFT), 'qty' => 1, ]; if ($no > 10000) { break; } } return $rows; } public function inspectionRun(): RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $this->ensureInspectionPackSnapshotTable(); $this->ensureInspectionSheetSnapshotTable(); $workDate = trim((string) ($this->request->getPost('work_date') ?? '')); if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $workDate)) { return redirect()->back()->withInput()->with('error', '작업일자를 확인해 주세요.'); } $selectedCodes = $this->request->getPost('bag_codes'); $selectedCodes = is_array($selectedCodes) ? array_values(array_unique(array_map(static fn ($v): string => trim((string) $v), $selectedCodes))) : []; $selectedCodes = array_values(array_filter($selectedCodes, static fn ($v): bool => $v !== '')); if ($selectedCodes === []) { return redirect()->back()->withInput()->with('error', '실사 대상 품목을 선택해 주세요.'); } $db = \Config\Database::connect(); $barcodeRows = $db->table('bag_receiving_pack_code') ->select('brpc_bag_code') ->distinct() ->where('brpc_lg_idx', $lgIdx) ->whereIn('brpc_bag_code', $selectedCodes) ->get() ->getResultArray(); $barcodeSet = []; foreach ($barcodeRows as $row) { $code = trim((string) ($row['brpc_bag_code'] ?? '')); if ($code !== '') { $barcodeSet[$code] = true; } } $effectiveCodes = array_values(array_filter($selectedCodes, static fn ($code): bool => isset($barcodeSet[$code]))); if ($effectiveCodes === []) { return redirect()->back()->withInput()->with('error', '바코드가 있는 품목만 실사 대상으로 선택할 수 있습니다.'); } foreach ($effectiveCodes as $code) { $this->ensureReceivingPackCodesForBag($lgIdx, $code); } $inventoryRows = $db->table('bag_inventory') ->select('bi_bag_code, bi_bag_name, bi_qty') ->where('bi_lg_idx', $lgIdx) ->whereIn('bi_bag_code', $effectiveCodes) ->orderBy('bi_bag_code', 'ASC') ->get() ->getResultArray(); if ($inventoryRows === []) { return redirect()->back()->withInput()->with('error', '선택한 품목의 재고 데이터가 없습니다.'); } $db->transStart(); $firstInspectionItemId = 0; $db->table('bag_inventory_inspection')->insert([ 'bis_lg_idx' => $lgIdx, 'bis_work_date' => $workDate, 'bis_status' => 'selected', 'bis_reg_mb_idx' => (int) (session()->get('mb_idx') ?? 0), 'bis_regdate' => date('Y-m-d H:i:s'), 'bis_moddate' => null, ]); $inspectionId = (int) $db->insertID(); foreach ($inventoryRows as $row) { $code = trim((string) ($row['bi_bag_code'] ?? '')); if ($code === '' || ! isset($barcodeSet[$code])) { continue; } $systemQty = (int) ($row['bi_qty'] ?? 0); $db->table('bag_inventory_inspection_item')->insert([ 'bisi_bis_idx' => $inspectionId, 'bisi_bag_code' => $code, 'bisi_bag_name' => trim((string) ($row['bi_bag_name'] ?? $code)), 'bisi_system_qty' => $systemQty, 'bisi_actual_qty' => null, 'bisi_diff_qty' => 0, 'bisi_has_barcode' => 'Y', 'bisi_apply_yn' => 'N', ]); $inspectionItemId = (int) $db->insertID(); if ($firstInspectionItemId <= 0 && $inspectionItemId > 0) { $firstInspectionItemId = $inspectionItemId; } } $db->transComplete(); if (! $db->transStatus() || $inspectionId <= 0) { return redirect()->back()->withInput()->with('error', '전산 선별 처리 중 오류가 발생했습니다.'); } $query = http_build_query([ 'start_date' => $workDate, 'end_date' => $workDate, 'bis_id' => $inspectionId, 'sel_item_id' => $firstInspectionItemId, ]); return redirect()->to(site_url('bag/inventory/inspection-work?' . $query)) ->with('success', '전산 선별 처리가 완료되었습니다.'); } public function inspectionSelectSave(): RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $this->ensureInspectionPackSnapshotTable(); $this->ensureInspectionSheetSnapshotTable(); $inspectionItemId = (int) ($this->request->getPost('bisi_idx') ?? 0); if ($inspectionItemId <= 0) { return redirect()->back()->with('error', '실사 대상 품목이 올바르지 않습니다.'); } $returnQuery = $this->inspectionReturnQueryFromPost($inspectionItemId); $db = \Config\Database::connect(); $item = $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $inspectionItemId) ->get() ->getRowArray(); if (! is_array($item) || (int) ($item['bisi_idx'] ?? 0) <= 0) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 대상 품목을 찾을 수 없습니다.'); } $requestInspectionId = (int) ($this->request->getPost('bis_id') ?? 0); $itemInspectionId = (int) ($item['bisi_bis_idx'] ?? 0); if ($requestInspectionId > 0 && $requestInspectionId !== $itemInspectionId) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '선택한 실사 작업과 품목 정보가 일치하지 않습니다.'); } $header = $db->table('bag_inventory_inspection') ->select('bis_idx') ->where('bis_idx', $itemInspectionId) ->where('bis_lg_idx', $lgIdx) ->get() ->getRowArray(); if (! is_array($header) || (int) ($header['bis_idx'] ?? 0) <= 0) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 작업 정보가 올바르지 않습니다.'); } $actualInput = $this->request->getPost('pack_actual_qty'); $actualInput = is_array($actualInput) ? $actualInput : []; $actualJson = trim((string) ($this->request->getPost('pack_actual_json') ?? '')); $actualFromJson = false; if ($actualJson !== '') { $decoded = json_decode($actualJson, true); if (is_array($decoded)) { $actualInput = []; foreach ($decoded as $k => $v) { $key = trim((string) $k); if ($key === '' || ! ctype_digit($key)) { continue; } $actualInput[$key] = max(0, (int) $v); } $actualFromJson = true; } } if ($actualFromJson && $actualInput === []) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '저장할 실사 수량(JSON)이 비어 있습니다. 다시 시도해 주세요.'); } if (! $actualFromJson && $actualInput === []) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '저장할 실사 수량이 없습니다. 수량을 변경한 뒤 다시 저장해 주세요.'); } $snapshotRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_idx, bisp_bag_code, bisp_pack_code, bisp_sheet_start_code, bisp_sheet_end_code, bisp_sheet_qty, bisp_actual_qty') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bisi_idx', $inspectionItemId) ->orderBy('bisp_idx', 'ASC') ->get() ->getResultArray(); if ($snapshotRows === []) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 팩 스냅샷이 없습니다.'); } $db->transStart(); $sumActual = 0; $packUpdates = []; $changedPackQtyMap = []; $bagCodeForSync = trim((string) ($item['bisi_bag_code'] ?? '')); $capacityMap = []; if ($bagCodeForSync !== '') { $capacityRows = $db->table('bag_receiving_pack_code') ->select('brpc_pack_code, brpc_sheet_qty') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_bag_code', $bagCodeForSync) ->where('brpc_pack_code !=', '') ->get() ->getResultArray(); foreach ($capacityRows as $cRow) { $packCode = trim((string) ($cRow['brpc_pack_code'] ?? '')); if ($packCode === '') { continue; } $capacityMap[$packCode] = max(0, (int) ($cRow['brpc_sheet_qty'] ?? 0)); } } foreach ($snapshotRows as $row) { $idx = (int) ($row['bisp_idx'] ?? 0); if ($idx <= 0) { continue; } $systemQty = max(0, (int) ($row['bisp_sheet_qty'] ?? 0)); $existingActualRaw = $row['bisp_actual_qty'] ?? null; $existingActual = $existingActualRaw === null ? $systemQty : max(0, (int) $existingActualRaw); $key = (string) $idx; $actualQty = array_key_exists($key, $actualInput) ? max(0, (int) $actualInput[$key]) : $existingActual; $packCode = trim((string) ($row['bisp_pack_code'] ?? '')); $startCode = trim((string) ($row['bisp_sheet_start_code'] ?? '')); $currentEndCode = trim((string) ($row['bisp_sheet_end_code'] ?? '')); if ($packCode !== '' && isset($capacityMap[$packCode])) { $maxQty = (int) ($capacityMap[$packCode] ?? 0); if ($maxQty > 0 && $actualQty > $maxQty) { $db->transRollback(); return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '팩 ' . $packCode . '의 허용 수량(' . number_format($maxQty) . '장)을 초과했습니다.'); } } $nextEndCode = $this->resolveSheetEndCodeByQty($startCode, $currentEndCode, $actualQty); $sumActual += $actualQty; if (! array_key_exists($key, $actualInput)) { continue; } if ($packCode !== '' && $actualQty !== $existingActual) { $changedPackQtyMap[$packCode] = [ 'qty' => $actualQty, 'end_code' => $nextEndCode, ]; } $packUpdates[] = [ 'bisp_idx' => $idx, 'bisp_sheet_qty' => $actualQty, 'bisp_actual_qty' => $actualQty, 'bisp_diff_qty' => 0, 'bisp_sheet_end_code' => $nextEndCode, 'bisp_checked_yn' => 'Y', ]; } if ($packUpdates !== []) { $chunk = 500; $count = count($packUpdates); for ($i = 0; $i < $count; $i += $chunk) { $slice = array_slice($packUpdates, $i, $chunk); $db->table('bag_inventory_inspection_pack_snapshot')->updateBatch($slice, 'bisp_idx'); } } // 같은 봉투코드/팩코드는 다른 실사작업에서도 동일 실사값으로 보이도록 동기화 // (요구사항: 48에서 12로 저장하면 47에서도 12로 조회) if ($bagCodeForSync !== '' && $changedPackQtyMap !== []) { foreach ($changedPackQtyMap as $packCode => $meta) { $qty = max(0, (int) ($meta['qty'] ?? 0)); $endCode = (string) ($meta['end_code'] ?? ''); $db->table('bag_inventory_inspection_pack_snapshot') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bag_code', $bagCodeForSync) ->where('bisp_pack_code', $packCode) ->update([ 'bisp_sheet_qty' => $qty, 'bisp_actual_qty' => $qty, 'bisp_diff_qty' => 0, 'bisp_sheet_end_code' => $endCode, 'bisp_checked_yn' => 'Y', ]); } } $systemQty = max(0, (int) ($item['bisi_system_qty'] ?? 0)); $newDiff = $sumActual - $systemQty; $prevDiff = (int) ($item['bisi_diff_qty'] ?? 0); $alreadyApplied = (string) ($item['bisi_apply_yn'] ?? 'N') === 'Y'; $applyDelta = $alreadyApplied ? ($newDiff - $prevDiff) : $newDiff; $invModel = model(BagInventoryModel::class); if ($applyDelta !== 0) { $invModel->adjustQty( $lgIdx, (string) ($item['bisi_bag_code'] ?? ''), (string) ($item['bisi_bag_name'] ?? ''), $applyDelta ); } $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $inspectionItemId) ->update([ 'bisi_system_qty' => $sumActual, 'bisi_actual_qty' => $sumActual, 'bisi_diff_qty' => 0, 'bisi_apply_yn' => 'Y', ]); $inspectionId = (int) ($item['bisi_bis_idx'] ?? 0); $remain = $db->table('bag_inventory_inspection_item') ->where('bisi_bis_idx', $inspectionId) ->where('bisi_apply_yn', 'N') ->countAllResults(); $db->table('bag_inventory_inspection') ->where('bis_idx', $inspectionId) ->update([ 'bis_status' => ($remain === 0) ? 'confirmed' : 'counting', 'bis_moddate' => date('Y-m-d H:i:s'), ]); $db->transComplete(); if (! $db->transStatus()) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 저장 중 오류가 발생했습니다.'); } $savedItem = $db->table('bag_inventory_inspection_item') ->select('bisi_system_qty, bisi_actual_qty, bisi_apply_yn') ->where('bisi_idx', $inspectionItemId) ->get() ->getRowArray(); if ( ! is_array($savedItem) || (string) ($savedItem['bisi_apply_yn'] ?? 'N') !== 'Y' || (int) ($savedItem['bisi_actual_qty'] ?? -1) !== $sumActual ) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 저장 검증에 실패했습니다. 다시 저장해 주세요.'); } return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('success', '실사 저장 완료 (합계: ' . number_format($sumActual) . '장)'); } public function inspectionSelectConfirm(): RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $inspectionItemId = (int) ($this->request->getPost('bisi_idx') ?? 0); if ($inspectionItemId <= 0) { return redirect()->back()->with('error', '실사 대상 품목이 올바르지 않습니다.'); } $returnQuery = $this->inspectionReturnQueryFromPost($inspectionItemId); $db = \Config\Database::connect(); $item = $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $inspectionItemId) ->get() ->getRowArray(); if (! is_array($item)) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 대상 품목을 찾을 수 없습니다.'); } $requestInspectionId = (int) ($this->request->getPost('bis_id') ?? 0); $itemInspectionId = (int) ($item['bisi_bis_idx'] ?? 0); if ($requestInspectionId > 0 && $requestInspectionId !== $itemInspectionId) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '선택한 실사 작업과 품목 정보가 일치하지 않습니다.'); } $header = $db->table('bag_inventory_inspection') ->select('bis_idx') ->where('bis_idx', $itemInspectionId) ->where('bis_lg_idx', $lgIdx) ->get() ->getRowArray(); if (! is_array($header) || (int) ($header['bis_idx'] ?? 0) <= 0) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 작업 정보가 올바르지 않습니다.'); } if ((string) ($item['bisi_apply_yn'] ?? 'N') === 'Y') { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '이미 확정된 실사 품목입니다.'); } $actualQty = $item['bisi_actual_qty']; if ($actualQty === null) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '먼저 실사 수량을 저장해 주세요.'); } $diff = (int) ($item['bisi_diff_qty'] ?? 0); $invModel = model(BagInventoryModel::class); $db->transStart(); if ($diff !== 0) { $invModel->adjustQty( $lgIdx, (string) ($item['bisi_bag_code'] ?? ''), (string) ($item['bisi_bag_name'] ?? ''), $diff ); } $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $inspectionItemId) ->update(['bisi_apply_yn' => 'Y']); $inspectionId = (int) ($item['bisi_bis_idx'] ?? 0); $remain = $db->table('bag_inventory_inspection_item') ->where('bisi_bis_idx', $inspectionId) ->where('bisi_apply_yn', 'N') ->countAllResults(); $db->table('bag_inventory_inspection') ->where('bis_idx', $inspectionId) ->update([ 'bis_status' => ($remain === 0) ? 'confirmed' : 'counting', 'bis_moddate' => date('Y-m-d H:i:s'), ]); $db->transComplete(); if (! $db->transStatus()) { return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('error', '실사 확정 중 오류가 발생했습니다.'); } return redirect()->to(site_url('bag/inventory/inspection-work?' . $returnQuery)) ->with('success', '실사 결과가 재고에 반영되었습니다.'); } private function inspectionReturnQueryFromPost(int $fallbackItemId): string { $startDate = trim((string) ($this->request->getPost('start_date') ?? '')); $endDate = trim((string) ($this->request->getPost('end_date') ?? '')); $bisId = (int) ($this->request->getPost('bis_id') ?? 0); $itemCode = trim((string) ($this->request->getPost('item_code') ?? '')); $viewType = trim((string) ($this->request->getPost('view_type') ?? 'box')); $selItemId = (int) ($this->request->getPost('sel_item_id') ?? $fallbackItemId); $selBoxCode = trim((string) ($this->request->getPost('sel_box_code') ?? '')); $selPackCode = trim((string) ($this->request->getPost('sel_pack_code') ?? '')); return http_build_query([ 'start_date' => $startDate, 'end_date' => $endDate, 'bis_id' => $bisId, 'item_code' => $itemCode, 'view_type' => $viewType, 'sel_item_id' => $selItemId > 0 ? $selItemId : $fallbackItemId, 'sel_box_code' => $selBoxCode, 'sel_pack_code' => $selPackCode, ]); } public function inspectionDetail(int $id): string|RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $db = \Config\Database::connect(); $inspection = $db->table('bag_inventory_inspection') ->where('bis_idx', $id) ->where('bis_lg_idx', $lgIdx) ->get() ->getRowArray(); if (! is_array($inspection)) { return redirect()->to(site_url('bag/inventory/inspection-select'))->with('error', '실사 작업을 찾을 수 없습니다.'); } $items = $db->table('bag_inventory_inspection_item') ->where('bisi_bis_idx', $id) ->orderBy('bisi_bag_code', 'ASC') ->get() ->getResultArray(); return $this->render('실사 조회', 'bag/inventory_inspection_detail', [ 'inspection' => $inspection, 'items' => $items, ]); } public function inspectionSave(int $id): RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $db = \Config\Database::connect(); $inspection = $db->table('bag_inventory_inspection') ->where('bis_idx', $id) ->where('bis_lg_idx', $lgIdx) ->get() ->getRowArray(); if (! is_array($inspection)) { return redirect()->to(site_url('bag/inventory/inspection-select'))->with('error', '실사 작업을 찾을 수 없습니다.'); } $actualQtyInput = $this->request->getPost('actual_qty'); $actualQtyInput = is_array($actualQtyInput) ? $actualQtyInput : []; if ($actualQtyInput === []) { return redirect()->back()->with('error', '실사 수량을 입력해 주세요.'); } $itemIds = array_values(array_unique(array_map('intval', array_keys($actualQtyInput)))); $itemIds = array_values(array_filter($itemIds, static fn ($v): bool => $v > 0)); if ($itemIds === []) { return redirect()->back()->with('error', '실사 수량 입력 대상이 없습니다.'); } $rows = $db->table('bag_inventory_inspection_item') ->where('bisi_bis_idx', $id) ->whereIn('bisi_idx', $itemIds) ->get() ->getResultArray(); $rowMap = []; foreach ($rows as $r) { $rowMap[(int) ($r['bisi_idx'] ?? 0)] = $r; } $db->transStart(); foreach ($itemIds as $itemId) { if (! isset($rowMap[$itemId])) { continue; } $systemQty = (int) ($rowMap[$itemId]['bisi_system_qty'] ?? 0); $actualQty = max(0, (int) ($actualQtyInput[(string) $itemId] ?? 0)); $diffQty = $actualQty - $systemQty; $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $itemId) ->update([ 'bisi_actual_qty' => $actualQty, 'bisi_diff_qty' => $diffQty, ]); } $db->table('bag_inventory_inspection') ->where('bis_idx', $id) ->update([ 'bis_status' => 'counted', 'bis_moddate' => date('Y-m-d H:i:s'), ]); $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->with('error', '실사 저장 중 오류가 발생했습니다.'); } return redirect()->back()->with('success', '실사 수량이 저장되었습니다.'); } public function inspectionApply(int $id): RedirectResponse { $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $this->ensureInventoryInspectionTables(); $db = \Config\Database::connect(); $inspection = $db->table('bag_inventory_inspection') ->where('bis_idx', $id) ->where('bis_lg_idx', $lgIdx) ->get() ->getRowArray(); if (! is_array($inspection)) { return redirect()->to(site_url('bag/inventory/inspection-select'))->with('error', '실사 작업을 찾을 수 없습니다.'); } $items = $db->table('bag_inventory_inspection_item') ->where('bisi_bis_idx', $id) ->where('bisi_actual_qty IS NOT NULL', null, false) ->where('bisi_apply_yn', 'N') ->get() ->getResultArray(); if ($items === []) { return redirect()->back()->with('error', '재고 반영할 실사 데이터가 없습니다.'); } $invModel = model(BagInventoryModel::class); $db->transStart(); foreach ($items as $item) { $diff = (int) ($item['bisi_diff_qty'] ?? 0); if ($diff !== 0) { $invModel->adjustQty( $lgIdx, (string) ($item['bisi_bag_code'] ?? ''), (string) ($item['bisi_bag_name'] ?? ''), $diff ); } $db->table('bag_inventory_inspection_item') ->where('bisi_idx', (int) ($item['bisi_idx'] ?? 0)) ->update(['bisi_apply_yn' => 'Y']); } $db->table('bag_inventory_inspection') ->where('bis_idx', $id) ->update([ 'bis_status' => 'applied', 'bis_moddate' => date('Y-m-d H:i:s'), ]); $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->with('error', '재고 반영 중 오류가 발생했습니다.'); } return redirect()->back()->with('success', '실사 결과가 재고에 반영되었습니다.'); } private function ensureReceivingPackCodeTableAndBackfill(int $lgIdx): void { $db = \Config\Database::connect(); if (! $db->tableExists('bag_receiving_pack_code')) { $db->query(<<<'SQL' CREATE TABLE IF NOT EXISTS `bag_receiving_pack_code` ( `brpc_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, `brpc_br_idx` INT UNSIGNED NOT NULL, `brpc_lg_idx` INT UNSIGNED NOT NULL, `brpc_bag_code` VARCHAR(50) NOT NULL, `brpc_bag_name` VARCHAR(100) NOT NULL DEFAULT '', `brpc_lot_no` VARCHAR(50) NOT NULL DEFAULT '', `brpc_box_code` VARCHAR(80) NOT NULL DEFAULT '', `brpc_pack_code` VARCHAR(80) NOT NULL, `brpc_sheet_start_code` VARCHAR(120) NOT NULL, `brpc_sheet_end_code` VARCHAR(120) NOT NULL, `brpc_sheet_qty` INT UNSIGNED NOT NULL DEFAULT 0, `brpc_state` VARCHAR(20) NOT NULL DEFAULT 'in_stock', `brpc_regdate` DATETIME NOT NULL, PRIMARY KEY (`brpc_idx`), UNIQUE KEY `uk_brpc_pack_code` (`brpc_pack_code`), KEY `idx_brpc_br_idx` (`brpc_br_idx`), KEY `idx_brpc_lg_bag` (`brpc_lg_idx`,`brpc_bag_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); } $unitRows = model(PackagingUnitModel::class) ->where('pu_lg_idx', $lgIdx) ->where('pu_state', 1) ->findAll(); $unitMap = []; foreach ($unitRows as $unit) { $unitMap[(string) ($unit->pu_bag_code ?? '')] = [ 'pack_per_sheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)), 'total_per_box' => max(1, (int) ($unit->pu_total_per_box ?? 1)), ]; } while (true) { $missingRows = $db->table('bag_receiving r') ->select('r.br_idx, r.br_bo_idx, r.br_bag_code, r.br_bag_name, r.br_qty_sheet, o.bo_lot_no') ->join('bag_order o', 'o.bo_idx = r.br_bo_idx', 'left') ->join('bag_receiving_pack_code c', 'c.brpc_br_idx = r.br_idx', 'left') ->where('r.br_lg_idx', $lgIdx) ->where('c.brpc_idx IS NULL', null, false) ->orderBy('r.br_idx', 'ASC') ->limit(500) ->get() ->getResultArray(); if ($missingRows === []) { break; } foreach ($missingRows as $row) { $bagCode = (string) ($row['br_bag_code'] ?? ''); $unit = $unitMap[$bagCode] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1]; $this->createReceivingPackCodes( $lgIdx, (int) ($row['br_idx'] ?? 0), (int) ($row['br_bo_idx'] ?? 0), $bagCode, (string) ($row['br_bag_name'] ?? ''), (int) ($row['br_qty_sheet'] ?? 0), (int) ($unit['pack_per_sheet'] ?? 1), (int) ($unit['total_per_box'] ?? 1), (string) ($row['bo_lot_no'] ?? '') ); } } } private function ensureReceivingPackCodesForBag(int $lgIdx, string $bagCode): void { $bagCode = trim($bagCode); if ($lgIdx <= 0 || $bagCode === '') { return; } $db = \Config\Database::connect(); if (! $db->tableExists('bag_receiving_pack_code')) { return; } $unit = model(PackagingUnitModel::class) ->where('pu_lg_idx', $lgIdx) ->where('pu_state', 1) ->where('pu_bag_code', $bagCode) ->first(); $packPerSheet = max(1, (int) ($unit->pu_pack_per_sheet ?? 1)); $totalPerBox = max(1, (int) ($unit->pu_total_per_box ?? 1)); while (true) { $missingRows = $db->table('bag_receiving r') ->select('r.br_idx, r.br_bo_idx, r.br_bag_code, r.br_bag_name, r.br_qty_sheet, o.bo_lot_no') ->join('bag_order o', 'o.bo_idx = r.br_bo_idx', 'left') ->join('bag_receiving_pack_code c', 'c.brpc_br_idx = r.br_idx', 'left') ->where('r.br_lg_idx', $lgIdx) ->where('r.br_bag_code', $bagCode) ->where('c.brpc_idx IS NULL', null, false) ->orderBy('r.br_idx', 'ASC') ->limit(200) ->get() ->getResultArray(); if ($missingRows === []) { break; } foreach ($missingRows as $row) { $this->createReceivingPackCodes( $lgIdx, (int) ($row['br_idx'] ?? 0), (int) ($row['br_bo_idx'] ?? 0), (string) ($row['br_bag_code'] ?? ''), (string) ($row['br_bag_name'] ?? ''), (int) ($row['br_qty_sheet'] ?? 0), $packPerSheet, $totalPerBox, (string) ($row['bo_lot_no'] ?? '') ); } } } private function createReceivingPackCodes( int $lgIdx, int $brIdx, int $boIdx, string $bagCode, string $bagName, int $qtySheet, int $packPerSheet, int $totalPerBox, string $lotNo = '' ): void { if ($brIdx <= 0 || $qtySheet <= 0 || $bagCode === '') { return; } $db = \Config\Database::connect(); if (! $db->tableExists('bag_receiving_pack_code')) { return; } $exists = $db->table('bag_receiving_pack_code') ->where('brpc_br_idx', $brIdx) ->countAllResults(); if ($exists > 0) { return; } $lotNo = trim($lotNo); if ($lotNo === '' && $boIdx > 0) { $order = model(BagOrderModel::class)->find($boIdx); $lotNo = trim((string) ($order->bo_lot_no ?? '')); } if ($lotNo === '') { $lotNo = $bagCode; } $packPerSheet = max(1, $packPerSheet); $totalPerBox = max(1, $totalPerBox); $packsPerBox = max(1, intdiv($totalPerBox, $packPerSheet)); $packCount = (int) ceil($qtySheet / $packPerSheet); $sheetCursor = 1; $regdate = date('Y-m-d H:i:s'); $rows = []; for ($packSeq = 1; $packSeq <= $packCount; $packSeq++) { $boxSeq = (int) ceil($packSeq / $packsPerBox); $sheetQty = min($packPerSheet, max(0, $qtySheet - (($packSeq - 1) * $packPerSheet))); if ($sheetQty <= 0) { break; } $sheetStartNo = $sheetCursor; $sheetEndNo = $sheetCursor + $sheetQty - 1; $sheetCursor = $sheetEndNo + 1; $boxCode = sprintf('%s-%06d-B%03d', $lotNo, $brIdx, $boxSeq); $packCode = sprintf('%s-%06d-P%03d', $lotNo, $brIdx, $packSeq); $startCode = sprintf('%s-S%05d', $packCode, $sheetStartNo); $endCode = sprintf('%s-S%05d', $packCode, $sheetEndNo); $rows[] = [ 'brpc_br_idx' => $brIdx, 'brpc_lg_idx' => $lgIdx, 'brpc_bag_code' => $bagCode, 'brpc_bag_name' => $bagName !== '' ? $bagName : $bagCode, 'brpc_lot_no' => $lotNo, 'brpc_box_code' => $boxCode, 'brpc_pack_code' => $packCode, 'brpc_sheet_start_code' => $startCode, 'brpc_sheet_end_code' => $endCode, 'brpc_sheet_qty' => $sheetQty, 'brpc_state' => 'in_stock', 'brpc_regdate' => $regdate, ]; } if ($rows !== []) { $db->table('bag_receiving_pack_code')->insertBatch($rows); } } private function ensureInspectionPackSnapshotTable(): void { $db = \Config\Database::connect(); if ($db->tableExists('bag_inventory_inspection_pack_snapshot')) { return; } $db->query(<<<'SQL' CREATE TABLE IF NOT EXISTS `bag_inventory_inspection_pack_snapshot` ( `bisp_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, `bisp_bisi_idx` INT UNSIGNED NOT NULL, `bisp_lg_idx` INT UNSIGNED NOT NULL, `bisp_bag_code` VARCHAR(50) NOT NULL, `bisp_box_code` VARCHAR(80) NOT NULL DEFAULT '', `bisp_pack_code` VARCHAR(80) NOT NULL, `bisp_sheet_start_code` VARCHAR(120) NOT NULL, `bisp_sheet_end_code` VARCHAR(120) NOT NULL, `bisp_sheet_qty` INT UNSIGNED NOT NULL DEFAULT 0, `bisp_actual_qty` INT UNSIGNED NULL DEFAULT NULL, `bisp_diff_qty` INT NOT NULL DEFAULT 0, `bisp_checked_yn` CHAR(1) NOT NULL DEFAULT 'N', `bisp_regdate` DATETIME NOT NULL, PRIMARY KEY (`bisp_idx`), UNIQUE KEY `uk_bisp_item_pack` (`bisp_bisi_idx`,`bisp_pack_code`), KEY `idx_bisp_item` (`bisp_bisi_idx`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); } private function ensureInspectionSheetSnapshotTable(): void { $db = \Config\Database::connect(); if ($db->tableExists('bag_inventory_inspection_sheet_snapshot')) { return; } $db->query(<<<'SQL' CREATE TABLE IF NOT EXISTS `bag_inventory_inspection_sheet_snapshot` ( `biss_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, `biss_bisi_idx` INT UNSIGNED NOT NULL, `biss_lg_idx` INT UNSIGNED NOT NULL, `biss_pack_code` VARCHAR(80) NOT NULL, `biss_sheet_code` VARCHAR(120) NOT NULL, `biss_system_qty` INT UNSIGNED NOT NULL DEFAULT 1, `biss_actual_qty` INT UNSIGNED NULL DEFAULT NULL, `biss_diff_qty` INT NOT NULL DEFAULT 0, `biss_checked_yn` CHAR(1) NOT NULL DEFAULT 'N', `biss_regdate` DATETIME NOT NULL, PRIMARY KEY (`biss_idx`), UNIQUE KEY `uk_biss_item_sheet` (`biss_bisi_idx`,`biss_sheet_code`), KEY `idx_biss_item_pack` (`biss_bisi_idx`,`biss_pack_code`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); } private function ensureInspectionPackSnapshotForItem(int $lgIdx, int $inspectionItemId, bool $forceRebuild = false): void { if ($inspectionItemId <= 0) { return; } $db = \Config\Database::connect(); if (! $db->tableExists('bag_inventory_inspection_pack_snapshot')) { return; } $item = $db->table('bag_inventory_inspection_item') ->where('bisi_idx', $inspectionItemId) ->where('bisi_has_barcode', 'Y') ->get() ->getRowArray(); if (! is_array($item)) { return; } $bagCode = trim((string) ($item['bisi_bag_code'] ?? '')); if ($bagCode === '') { return; } $this->ensureReceivingPackCodesForBag($lgIdx, $bagCode); $sourceRows = $db->table('bag_receiving_pack_code') ->select('brpc_box_code, brpc_pack_code, brpc_sheet_start_code, brpc_sheet_end_code, brpc_sheet_qty') ->where('brpc_lg_idx', $lgIdx) ->where('brpc_bag_code', $bagCode) ->where('brpc_state', 'in_stock') ->orderBy('brpc_box_code', 'ASC') ->orderBy('brpc_pack_code', 'ASC') ->get() ->getResultArray(); if ($sourceRows === []) { return; } $existingSnapshot = $db->table('bag_inventory_inspection_pack_snapshot') ->select('COUNT(*) AS row_cnt', false) ->where('bisp_bisi_idx', $inspectionItemId) ->get() ->getRowArray(); $existingCount = (int) ($existingSnapshot['row_cnt'] ?? 0); // 실사 저장 이후에는 사용자가 수정한 수량을 유지해야 하므로 // 강제 재생성이 아니면 기존 스냅샷이 존재할 때 재생성하지 않는다. if (! $forceRebuild && $existingCount > 0) { $this->applyLatestPackAdjustmentsToSnapshot($db, $lgIdx, $inspectionItemId, $bagCode); return; } // 스냅샷은 선택 품목의 현재 in_stock 전체 팩/박스를 기준으로 재생성한다. $db->table('bag_inventory_inspection_pack_snapshot') ->where('bisp_bisi_idx', $inspectionItemId) ->delete(); $insertRows = []; $now = date('Y-m-d H:i:s'); foreach ($sourceRows as $src) { $rowQty = max(0, (int) ($src['brpc_sheet_qty'] ?? 0)); if ($rowQty <= 0) { continue; } $startCode = (string) ($src['brpc_sheet_start_code'] ?? ''); $endCode = (string) ($src['brpc_sheet_end_code'] ?? ''); $insertRows[] = [ 'bisp_bisi_idx' => $inspectionItemId, 'bisp_lg_idx' => $lgIdx, 'bisp_bag_code' => $bagCode, 'bisp_box_code' => (string) ($src['brpc_box_code'] ?? ''), 'bisp_pack_code' => (string) ($src['brpc_pack_code'] ?? ''), 'bisp_sheet_start_code' => $startCode, 'bisp_sheet_end_code' => $endCode, 'bisp_sheet_qty' => $rowQty, 'bisp_actual_qty' => $rowQty, 'bisp_diff_qty' => 0, 'bisp_checked_yn' => 'N', 'bisp_regdate' => $now, ]; } if ($insertRows !== []) { $db->table('bag_inventory_inspection_pack_snapshot')->insertBatch($insertRows); } $this->applyLatestPackAdjustmentsToSnapshot($db, $lgIdx, $inspectionItemId, $bagCode); } private function applyLatestPackAdjustmentsToSnapshot( \CodeIgniter\Database\BaseConnection $db, int $lgIdx, int $inspectionItemId, string $bagCode ): void { $bagCode = trim($bagCode); if ($inspectionItemId <= 0 || $bagCode === '') { return; } $latestRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_pack_code, MAX(bisp_idx) AS latest_idx', false) ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bag_code', $bagCode) ->where('bisp_checked_yn', 'Y') ->where('bisp_pack_code !=', '') ->groupBy('bisp_pack_code') ->get() ->getResultArray(); if ($latestRows === []) { return; } $latestIdxList = []; foreach ($latestRows as $row) { $latestIdx = (int) ($row['latest_idx'] ?? 0); if ($latestIdx > 0) { $latestIdxList[] = $latestIdx; } } if ($latestIdxList === []) { return; } $latestValues = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_pack_code, bisp_sheet_qty') ->whereIn('bisp_idx', $latestIdxList) ->get() ->getResultArray(); if ($latestValues === []) { return; } $latestMap = []; foreach ($latestValues as $row) { $packCode = trim((string) ($row['bisp_pack_code'] ?? '')); if ($packCode === '') { continue; } $latestMap[$packCode] = [ 'qty' => max(0, (int) ($row['bisp_sheet_qty'] ?? 0)), ]; } if ($latestMap === []) { return; } $currentRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_idx, bisp_pack_code, bisp_sheet_start_code, bisp_sheet_end_code, bisp_sheet_qty') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bisi_idx', $inspectionItemId) ->where('bisp_bag_code', $bagCode) ->get() ->getResultArray(); if ($currentRows === []) { return; } $updates = []; foreach ($currentRows as $row) { $packCode = trim((string) ($row['bisp_pack_code'] ?? '')); if ($packCode === '' || ! isset($latestMap[$packCode])) { continue; } $targetQty = (int) ($latestMap[$packCode]['qty'] ?? 0); $currentQty = max(0, (int) ($row['bisp_sheet_qty'] ?? 0)); $startCode = trim((string) ($row['bisp_sheet_start_code'] ?? '')); $currentEndCode = trim((string) ($row['bisp_sheet_end_code'] ?? '')); $targetEndCode = $this->resolveSheetEndCodeByQty($startCode, $currentEndCode, $targetQty); if ($targetQty === $currentQty && $targetEndCode === $currentEndCode) { continue; } $updates[] = [ 'bisp_idx' => (int) ($row['bisp_idx'] ?? 0), 'bisp_sheet_qty' => $targetQty, 'bisp_actual_qty' => $targetQty, 'bisp_diff_qty' => 0, 'bisp_sheet_end_code' => $targetEndCode, 'bisp_checked_yn' => 'Y', ]; } if ($updates !== []) { $db->table('bag_inventory_inspection_pack_snapshot')->updateBatch($updates, 'bisp_idx'); } } private function ensureInspectionSheetSnapshotForItem(int $lgIdx, int $inspectionItemId): void { if ($inspectionItemId <= 0) { return; } $db = \Config\Database::connect(); if (! $db->tableExists('bag_inventory_inspection_sheet_snapshot')) { return; } $exists = $db->table('bag_inventory_inspection_sheet_snapshot') ->where('biss_bisi_idx', $inspectionItemId) ->countAllResults(); if ($exists > 0) { return; } $packRows = $db->table('bag_inventory_inspection_pack_snapshot') ->select('bisp_pack_code, bisp_sheet_start_code, bisp_sheet_end_code') ->where('bisp_lg_idx', $lgIdx) ->where('bisp_bisi_idx', $inspectionItemId) ->orderBy('bisp_idx', 'ASC') ->get() ->getResultArray(); if ($packRows === []) { return; } $insertRows = []; $now = date('Y-m-d H:i:s'); foreach ($packRows as $packRow) { $packCode = (string) ($packRow['bisp_pack_code'] ?? ''); if ($packCode === '') { continue; } $codes = $this->expandSheetCodes( (string) ($packRow['bisp_sheet_start_code'] ?? ''), (string) ($packRow['bisp_sheet_end_code'] ?? '') ); foreach ($codes as $codeRow) { $sheetCode = (string) ($codeRow['sheet_code'] ?? ''); if ($sheetCode === '') { continue; } $insertRows[] = [ 'biss_bisi_idx' => $inspectionItemId, 'biss_lg_idx' => $lgIdx, 'biss_pack_code' => $packCode, 'biss_sheet_code' => $sheetCode, 'biss_system_qty' => 1, 'biss_actual_qty' => null, 'biss_diff_qty' => 0, 'biss_checked_yn' => 'N', 'biss_regdate' => $now, ]; if (count($insertRows) >= 1000) { $db->table('bag_inventory_inspection_sheet_snapshot')->insertBatch($insertRows); $insertRows = []; } } } if ($insertRows !== []) { $db->table('bag_inventory_inspection_sheet_snapshot')->insertBatch($insertRows); } } private function ensureInspectionSheetSnapshotForPack( int $lgIdx, int $inspectionItemId, string $packCode, string $startCode, string $endCode ): void { if ($inspectionItemId <= 0 || trim($packCode) === '') { return; } $db = \Config\Database::connect(); if (! $db->tableExists('bag_inventory_inspection_sheet_snapshot')) { return; } $exists = $db->table('bag_inventory_inspection_sheet_snapshot') ->where('biss_lg_idx', $lgIdx) ->where('biss_bisi_idx', $inspectionItemId) ->where('biss_pack_code', $packCode) ->countAllResults(); if ($exists > 0) { return; } $codes = $this->expandSheetCodes($startCode, $endCode); if ($codes === []) { return; } $now = date('Y-m-d H:i:s'); $rows = []; foreach ($codes as $codeRow) { $sheetCode = trim((string) ($codeRow['sheet_code'] ?? '')); if ($sheetCode === '') { continue; } $rows[] = [ 'biss_bisi_idx' => $inspectionItemId, 'biss_lg_idx' => $lgIdx, 'biss_pack_code' => $packCode, 'biss_sheet_code' => $sheetCode, 'biss_system_qty' => 1, 'biss_actual_qty' => null, 'biss_diff_qty' => 0, 'biss_checked_yn' => 'N', 'biss_regdate' => $now, ]; } if ($rows !== []) { $db->table('bag_inventory_inspection_sheet_snapshot')->insertBatch($rows); } } private function resolveSheetEndCodeByQty(string $startCode, string $fallbackEndCode, int $qty): string { if ($qty <= 0) { return $fallbackEndCode; } if (preg_match('/^(.*?)(\d+)$/', $startCode, $m) !== 1) { return $fallbackEndCode; } $prefix = (string) ($m[1] ?? ''); $startNumRaw = (string) ($m[2] ?? ''); $startNum = (int) $startNumRaw; if ($startNum <= 0) { return $fallbackEndCode; } $endNum = $startNum + $qty - 1; $width = strlen($startNumRaw); return $prefix . str_pad((string) $endNum, $width, '0', STR_PAD_LEFT); } /** * @param list> $rows * @return list> */ private function trimPackRowsToTargetQty(array $rows, int $targetQty): array { $targetQty = max(0, $targetQty); if ($targetQty <= 0 || $rows === []) { return []; } $trimmed = []; $remain = $targetQty; foreach ($rows as $row) { if ($remain <= 0) { break; } $rowQty = max(0, (int) ($row['bisp_sheet_qty'] ?? 0)); if ($rowQty <= 0) { continue; } if ($rowQty <= $remain) { $trimmed[] = $row; $remain -= $rowQty; continue; } $startCode = (string) ($row['bisp_sheet_start_code'] ?? ''); $endCode = (string) ($row['bisp_sheet_end_code'] ?? ''); $row['bisp_sheet_qty'] = $remain; $row['bisp_sheet_end_code'] = $this->resolveSheetEndCodeByQty($startCode, $endCode, $remain); $trimmed[] = $row; $remain = 0; } return $trimmed; } private function ensureInventoryInspectionTables(): void { $db = \Config\Database::connect(); if (! $db->tableExists('bag_inventory_inspection')) { $db->query(<<<'SQL' CREATE TABLE IF NOT EXISTS `bag_inventory_inspection` ( `bis_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, `bis_lg_idx` INT UNSIGNED NOT NULL, `bis_work_date` DATE NOT NULL, `bis_status` VARCHAR(20) NOT NULL DEFAULT 'selected', `bis_reg_mb_idx` INT UNSIGNED NOT NULL DEFAULT 0, `bis_regdate` DATETIME NOT NULL, `bis_moddate` DATETIME NULL DEFAULT NULL, PRIMARY KEY (`bis_idx`), KEY `idx_bis_lg_work` (`bis_lg_idx`, `bis_work_date`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); } if (! $db->tableExists('bag_inventory_inspection_item')) { $db->query(<<<'SQL' CREATE TABLE IF NOT EXISTS `bag_inventory_inspection_item` ( `bisi_idx` INT UNSIGNED NOT NULL AUTO_INCREMENT, `bisi_bis_idx` INT UNSIGNED NOT NULL, `bisi_bag_code` VARCHAR(50) NOT NULL, `bisi_bag_name` VARCHAR(100) NOT NULL DEFAULT '', `bisi_system_qty` INT NOT NULL DEFAULT 0, `bisi_actual_qty` INT NULL DEFAULT NULL, `bisi_diff_qty` INT NOT NULL DEFAULT 0, `bisi_has_barcode` CHAR(1) NOT NULL DEFAULT 'Y', `bisi_apply_yn` CHAR(1) NOT NULL DEFAULT 'N', PRIMARY KEY (`bisi_idx`), KEY `idx_bisi_bis` (`bisi_bis_idx`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci SQL); } else { $fields = $db->getFieldNames('bag_inventory_inspection_pack_snapshot'); if (! in_array('bisp_actual_qty', $fields, true)) { $db->query("ALTER TABLE `bag_inventory_inspection_pack_snapshot` ADD COLUMN `bisp_actual_qty` INT UNSIGNED NULL DEFAULT NULL AFTER `bisp_sheet_qty`"); } if (! in_array('bisp_diff_qty', $fields, true)) { $db->query("ALTER TABLE `bag_inventory_inspection_pack_snapshot` ADD COLUMN `bisp_diff_qty` INT NOT NULL DEFAULT 0 AFTER `bisp_actual_qty`"); } if (! in_array('bisp_checked_yn', $fields, true)) { $db->query("ALTER TABLE `bag_inventory_inspection_pack_snapshot` ADD COLUMN `bisp_checked_yn` CHAR(1) NOT NULL DEFAULT 'N' AFTER `bisp_diff_qty`"); } } } // ────────────────────────────────────────────── // 판매 관리 // ────────────────────────────────────────────── public function sales(): string { $lgIdx = $this->lgIdx(); $data = ['salesList' => [], 'orderList' => [], 'startDate' => null, 'endDate' => null]; if ($lgIdx) { $startDate = $this->request->getGet('start_date'); $endDate = $this->request->getGet('end_date'); $data['startDate'] = $startDate; $data['endDate'] = $endDate; // 판매/반품 $saleBuilder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx); if ($startDate) $saleBuilder->where('bs_sale_date >=', $startDate); if ($endDate) $saleBuilder->where('bs_sale_date <=', $endDate); $data['salesList'] = $saleBuilder->orderBy('bs_sale_date', 'DESC')->paginate(20, 'sales'); $data['salesPager'] = model(BagSaleModel::class)->pager; // 주문 접수 $orderBuilder = model(ShopOrderModel::class)->where('so_lg_idx', $lgIdx); if ($startDate) $orderBuilder->where('so_delivery_date >=', $startDate); if ($endDate) $orderBuilder->where('so_delivery_date <=', $endDate); $data['orderList'] = $orderBuilder->orderBy('so_idx', 'DESC')->paginate(20, 'shoporders'); $data['orderPager'] = model(ShopOrderModel::class)->pager; } return $this->render('판매 관리', 'bag/sales', $data); } // ────────────────────────────────────────────── // 판매 현황 // ────────────────────────────────────────────── public function salesStats(): string { $lgIdx = $this->lgIdx(); $data = ['result' => [], 'startDate' => null, 'endDate' => null]; if ($lgIdx) { $startDate = $this->request->getGet('start_date'); $endDate = $this->request->getGet('end_date'); $data['startDate'] = $startDate; $data['endDate'] = $endDate; $builder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx)->where('bs_type', 'sale'); if ($startDate) $builder->where('bs_sale_date >=', $startDate); if ($endDate) $builder->where('bs_sale_date <=', $endDate); $data['result'] = $builder->orderBy('bs_sale_date', 'DESC')->paginate(20); $data['pager'] = model(BagSaleModel::class)->pager; } return $this->render('판매 현황', 'bag/sales_stats', $data); } // ────────────────────────────────────────────── // 봉투 수불 관리 // ────────────────────────────────────────────── public function flow(): string { $lgIdx = $this->lgIdx(); $data = ['receiving' => [], 'sales' => [], 'issues' => [], 'inventory' => [], 'startDate' => null, 'endDate' => null]; if ($lgIdx) { $startDate = $this->request->getGet('start_date'); $endDate = $this->request->getGet('end_date'); $data['startDate'] = $startDate; $data['endDate'] = $endDate; $data['inventory'] = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll(); $recvBuilder = model(BagReceivingModel::class)->where('br_lg_idx', $lgIdx); if ($startDate) $recvBuilder->where('br_receive_date >=', $startDate); if ($endDate) $recvBuilder->where('br_receive_date <=', $endDate); $data['receiving'] = $recvBuilder->findAll(); $saleBuilder = model(BagSaleModel::class)->where('bs_lg_idx', $lgIdx); if ($startDate) $saleBuilder->where('bs_sale_date >=', $startDate); if ($endDate) $saleBuilder->where('bs_sale_date <=', $endDate); $data['sales'] = $saleBuilder->findAll(); $issueBuilder = model(BagIssueModel::class)->where('bi2_lg_idx', $lgIdx); if ($startDate) $issueBuilder->where('bi2_issue_date >=', $startDate); if ($endDate) $issueBuilder->where('bi2_issue_date <=', $endDate); $data['issues'] = $issueBuilder->findAll(); } return $this->render('봉투 수불 관리', 'bag/flow', $data); } // ────────────────────────────────────────────── // 통계 분석 관리 // ────────────────────────────────────────────── public function analytics(): string { return $this->render('통계 분석 관리', 'bag/analytics', []); } // ────────────────────────────────────────────── // 창 (프로그램 창 관리 - 추후) // ────────────────────────────────────────────── public function window(): string { return $this->render('창', 'bag/window', []); } // ────────────────────────────────────────────── // 도움말 // ────────────────────────────────────────────── public function help(): string { return $this->render('도움말', 'bag/help', []); } // ────────────────────────────────────────────── // 재고 조정 (실사) // ────────────────────────────────────────────── public function inventoryAdjust(): string { $lgIdx = $this->lgIdx(); $inventory = $lgIdx ? model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->orderBy('bi_bag_code')->findAll() : []; return $this->render('재고 조정', 'bag/inventory_adjust', compact('inventory')); } public function inventoryAdjustStore() { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.'); } $rules = [ 'bag_code' => 'required|max_length[50]', 'adjust_type' => 'required|in_list[set,add,sub]', 'qty' => 'required|is_natural', ]; if (! $this->validate($rules)) { return redirect()->back()->withInput()->with('errors', $this->validator->getErrors()); } $bagCode = $this->request->getPost('bag_code'); $type = $this->request->getPost('adjust_type'); $qty = (int) $this->request->getPost('qty'); $invModel = model(BagInventoryModel::class); $existing = $invModel->where('bi_lg_idx', $lgIdx)->where('bi_bag_code', $bagCode)->first(); if ($type === 'set') { if ($existing) { $invModel->update($existing->bi_idx, ['bi_qty' => $qty, 'bi_updated_at' => date('Y-m-d H:i:s')]); } } elseif ($type === 'add') { $bagName = $existing ? $existing->bi_bag_name : ''; $invModel->adjustQty($lgIdx, $bagCode, $bagName, $qty); } elseif ($type === 'sub') { $bagName = $existing ? $existing->bi_bag_name : ''; $invModel->adjustQty($lgIdx, $bagCode, $bagName, -$qty); } return redirect()->to(site_url('bag/inventory'))->with('success', '재고가 조정되었습니다.'); } // ══════════════════════════════════════════════ // CRUD — 사이트 레이아웃으로 등록/처리 폼 제공 // ══════════════════════════════════════════════ // --- 불출 등록 --- public function issueCreate(): string { helper('admin'); $lgIdx = $this->lgIdx(); $kindO = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kindO ? model(CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx) : []; $bagNameMap = []; foreach ($bagCodes as $cd) { $bagNameMap[(string) ($cd->cd_code ?? '')] = (string) ($cd->cd_name ?? ''); } $inventoryRows = $lgIdx ? model(BagInventoryModel::class) ->where('bi_lg_idx', $lgIdx) ->where('bi_qty >', 0) ->orderBy('bi_bag_code', 'ASC') ->findAll() : []; $inventoryMap = []; foreach ($inventoryRows as $inv) { $code = (string) ($inv->bi_bag_code ?? ''); if ($code === '') { continue; } $inventoryMap[$code] = (int) ($inv->bi_qty ?? 0); } $unitRows = $lgIdx ? model(PackagingUnitModel::class) ->where('pu_lg_idx', $lgIdx) ->where('pu_state', 1) ->findAll() : []; $packagingMap = []; foreach ($unitRows as $unit) { $code = (string) ($unit->pu_bag_code ?? ''); if ($code === '') { continue; } $packagingMap[$code] = [ 'packPerSheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)), 'totalPerBox' => max(1, (int) ($unit->pu_total_per_box ?? 1)), ]; } $bagMeta = []; foreach ($inventoryMap as $code => $qty) { $bagMeta[$code] = [ 'name' => (string) ($bagNameMap[$code] ?? ''), 'inventoryQty' => (int) $qty, 'packPerSheet' => (int) (($packagingMap[$code]['packPerSheet'] ?? 1)), 'totalPerBox' => (int) (($packagingMap[$code]['totalPerBox'] ?? 1)), ]; } $availableBagRows = []; foreach ($inventoryMap as $code => $qty) { $availableBagRows[] = [ 'bag_code' => (string) $code, 'bag_name' => (string) ($bagNameMap[$code] ?? $code), 'inventory_qty' => (int) $qty, 'pack_per_sheet' => (int) (($packagingMap[$code]['packPerSheet'] ?? 1)), 'total_per_box' => (int) (($packagingMap[$code]['totalPerBox'] ?? 1)), ]; } $recentIssueRows = $lgIdx ? model(BagIssueModel::class) ->where('bi2_lg_idx', $lgIdx) ->orderBy('bi2_issue_date', 'DESC') ->orderBy('bi2_idx', 'DESC') ->findAll(20) : []; $kindD = model(CodeKindModel::class)->where('ck_code', 'D')->first(); $dongCodes = $kindD ? model(CodeDetailModel::class)->getByKind((int) $kindD->ck_idx, true, $lgIdx) : []; $today = date('Y-m-d'); $freeDongRows = []; if ($lgIdx) { $freeDongRows = model(\App\Models\FreeRecipientModel::class) ->builder() ->select('fr_dong_code') ->distinct() ->where('fr_lg_idx', $lgIdx) ->where('fr_state', 1) ->groupStart() ->where('fr_end_date IS NULL') ->orWhere('fr_end_date >=', $today) ->groupEnd() ->where('fr_dong_code !=', '') ->get() ->getResult(); } $freeDongSet = []; foreach ($freeDongRows as $row) { $code = trim((string) ($row->fr_dong_code ?? '')); if ($code !== '') { $freeDongSet[$code] = true; } } $destTypeOptions = ['구청', '기타']; if ($lgIdx) { $typeRows = model(\App\Models\FreeRecipientModel::class) ->builder() ->select('fr_type_code, fr_name') ->distinct() ->where('fr_lg_idx', $lgIdx) ->where('fr_state', 1) ->groupStart() ->where('fr_end_date IS NULL') ->orWhere('fr_end_date >=', $today) ->groupEnd() ->whereIn('fr_type_code', ['office', 'target']) ->orderBy('fr_name', 'ASC') ->get() ->getResult(); foreach ($typeRows as $row) { $typeCode = trim((string) ($row->fr_type_code ?? '')); $name = trim((string) ($row->fr_name ?? '')); if ($typeCode === 'office') { $destTypeOptions[] = '동사무소'; continue; } if ($typeCode === 'target' && $name !== '') { $destTypeOptions[] = $name; } } $destTypeOptions = array_values(array_unique($destTypeOptions)); } return $this->render('불출 처리', 'bag/create_bag_issue', compact( 'bagCodes', 'bagMeta', 'inventoryMap', 'packagingMap', 'availableBagRows', 'recentIssueRows', 'dongCodes', 'freeDongSet', 'destTypeOptions' )); } public function issueStore() { $admin = new \App\Controllers\Admin\BagIssue(); $admin->initController($this->request, $this->response, service('logger')); $result = $admin->store(); if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) { $to = (string) $result->getHeaderLine('Location'); $to = str_replace('/admin/bag-issues', '/bag/issue', $to); return redirect()->to($to)->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors')); } return redirect()->to(site_url('bag/issue/cancel'))->with('success', '불출 처리되었습니다.'); } public function issueCancel(int $id) { $admin = new \App\Controllers\Admin\BagIssue(); $admin->initController($this->request, $this->response, service('logger')); $admin->cancel($id); return redirect()->to(site_url('bag/issue/cancel'))->with('success', session()->getFlashdata('success') ?? '취소되었습니다.'); } // --- 발주 등록 --- public function orderCreate(): string { helper('admin'); $lgIdx = $this->lgIdx(); $companies = $lgIdx ? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll() : []; $associations = $lgIdx ? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '협회')->where('cp_state', 1)->findAll() : []; $agencies = $lgIdx ? model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; $priceMapRows = $lgIdx ? model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx) : []; $units = $lgIdx ? model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll() : []; $recentOrders = $lgIdx ? model(BagOrderModel::class)->where('bo_lg_idx', $lgIdx)->whereLatestHead($lgIdx)->orderBy('bo_order_date', 'DESC')->orderBy('bo_idx', 'DESC')->findAll(12) : []; $companyMap = []; foreach ($companies as $company) { $companyMap[(int) $company->cp_idx] = (string) $company->cp_name; } $agencyMap = []; foreach ($agencies as $agency) { $agencyMap[(int) $agency->sa_idx] = '[' . ($agency->sa_kind ?? '') . '] ' . ($agency->sa_code ?? '') . ' — ' . ($agency->sa_name ?? ''); } $bagNameMap = []; foreach ($bagCodes as $codeDetail) { $bagNameMap[(string) $codeDetail->cd_code] = (string) $codeDetail->cd_name; } $priceMap = []; foreach ($priceMapRows as $bagCode => $price) { $priceMap[(string) $bagCode] = (float) ($price->bp_order_price ?? 0); } $unitMap = []; foreach ($units as $unit) { $unitMap[(string) $unit->pu_bag_code] = [ 'boxPerPack' => (int) $unit->pu_box_per_pack, 'packPerSheet' => (int) $unit->pu_pack_per_sheet, 'totalPerBox' => (int) $unit->pu_total_per_box, ]; } $bagReferenceRows = []; foreach ($bagCodes as $codeDetail) { $bagCode = (string) $codeDetail->cd_code; $unit = $unitMap[$bagCode] ?? ['boxPerPack' => 0, 'packPerSheet' => 0, 'totalPerBox' => 0]; $bagReferenceRows[] = [ 'code' => $bagCode, 'name' => (string) ($bagNameMap[$bagCode] ?? ''), 'orderPrice' => (float) ($priceMap[$bagCode] ?? 0), 'boxPerPack' => (int) $unit['boxPerPack'], 'packPerSheet' => (int) $unit['packPerSheet'], 'totalPerBox' => (int) $unit['totalPerBox'], ]; } return $this->render( '발주 등록', 'bag/create_bag_order', array_merge( compact( 'companies', 'associations', 'agencies', 'bagCodes', 'recentOrders', 'companyMap', 'agencyMap', 'bagReferenceRows' ), ['editMode' => false, 'editDefaults' => null] ) ); } /** * LOT-No 디스켓 불출: 발주 건을 선택해 암호화 seed 파일 생성/다운로드. */ public function orderLotSeed(): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $startMonth = (string) ($this->request->getGet('start_month') ?? date('Y-m')); $endMonth = (string) ($this->request->getGet('end_month') ?? date('Y-m')); if (! preg_match('/^\d{4}-\d{2}$/', $startMonth)) { $startMonth = date('Y-m'); } if (! preg_match('/^\d{4}-\d{2}$/', $endMonth)) { $endMonth = $startMonth; } if (strtotime($startMonth . '-01') > strtotime($endMonth . '-01')) { [$startMonth, $endMonth] = [$endMonth, $startMonth]; } $lotNo = trim((string) ($this->request->getGet('lot_no') ?? '')); $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0); $startDate = $startMonth . '-01'; $endDate = date('Y-m-t', strtotime($endMonth . '-01 00:00:00')); $orderModel = model(BagOrderModel::class); $builder = $orderModel ->where('bo_lg_idx', $lgIdx) ->whereLatestHead($lgIdx) ->where('bo_order_date >=', $startDate) ->where('bo_order_date <=', $endDate) ->whereIn('bo_status', ['normal', 'cancelled']) ->orderBy('bo_order_date', 'DESC') ->orderBy('bo_idx', 'DESC'); if ($lotNo !== '') { $builder->where('bo_lot_no', $lotNo); } if ($companyIdx > 0) { $builder->where('bo_company_idx', $companyIdx); } $orders = $builder->paginate(20); $pager = $orderModel->pager; $companies = model(CompanyModel::class) ->where('cp_lg_idx', $lgIdx) ->where('cp_type', '제작업체') ->where('cp_state', 1) ->orderBy('cp_name', 'ASC') ->findAll(); $companyMap = []; foreach ($companies as $company) { $companyMap[(int) ($company->cp_idx ?? 0)] = (string) ($company->cp_name ?? ''); } $orderIds = array_values(array_map(static fn ($o): int => (int) ($o->bo_idx ?? 0), $orders)); $itemSummary = []; if ($orderIds !== []) { $items = model(BagOrderItemModel::class) ->whereIn('boi_bo_idx', $orderIds) ->orderBy('boi_bo_idx', 'ASC') ->findAll(); foreach ($items as $item) { $boIdx = (int) ($item->boi_bo_idx ?? 0); if (! isset($itemSummary[$boIdx])) { $itemSummary[$boIdx] = [ 'line_count' => 0, 'qty_box' => 0, 'qty_sheet' => 0, ]; } $itemSummary[$boIdx]['line_count']++; $itemSummary[$boIdx]['qty_box'] += (int) ($item->boi_qty_box ?? 0); $itemSummary[$boIdx]['qty_sheet'] += (int) ($item->boi_qty_sheet ?? 0); } } return $this->render('LOT-No 디스켓 불출', 'bag/order_lot_seed', [ 'orders' => $orders, 'pager' => $pager, 'startMonth' => $startMonth, 'endMonth' => $endMonth, 'lotNo' => $lotNo, 'companyIdx' => $companyIdx, 'companies' => $companies, 'companyMap' => $companyMap, 'itemSummary' => $itemSummary, ]); } public function orderLotSeedGenerate(): RedirectResponse|ResponseInterface { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $boIdx = (int) ($this->request->getPost('bo_idx') ?? 0); if ($boIdx <= 0) { return redirect()->back()->with('error', '발주 건을 선택해 주세요.'); } $order = model(BagOrderModel::class) ->where('bo_lg_idx', $lgIdx) ->where('bo_idx', $boIdx) ->first(); if (! $order) { return redirect()->back()->with('error', '발주 정보를 찾을 수 없습니다.'); } $lotNo = trim((string) ($order->bo_lot_no ?? '')); $uuid = trim((string) ($order->bo_uuid ?? '')); $version = max(1, (int) ($order->bo_version ?? 1)); if ($lotNo === '' || $uuid === '') { return redirect()->back()->with('error', '발주의 LOT/UUID 정보가 없어 seed 파일을 생성할 수 없습니다.'); } $items = model(BagOrderItemModel::class) ->where('boi_bo_idx', $boIdx) ->orderBy('boi_idx', 'ASC') ->findAll(); if ($items === []) { return redirect()->back()->with('error', '발주 품목이 없어 seed 파일을 생성할 수 없습니다.'); } $unitRows = model(PackagingUnitModel::class) ->where('pu_lg_idx', $lgIdx) ->where('pu_state', 1) ->findAll(); $packMap = []; foreach ($unitRows as $unit) { $code = trim((string) ($unit->pu_bag_code ?? '')); if ($code === '') { continue; } $packMap[$code] = [ 'pack_per_sheet' => max(1, (int) ($unit->pu_pack_per_sheet ?? 1)), 'total_per_box' => max(1, (int) ($unit->pu_total_per_box ?? 1)), ]; } $orderData = [ 'bo_idx' => $boIdx, 'bo_uuid' => (string) $uuid, 'bo_version' => $version, 'bo_lg_idx' => (int) ($order->bo_lg_idx ?? 0), 'bo_gugun_code' => (string) ($order->bo_gugun_code ?? ''), 'bo_dong_code' => (string) ($order->bo_dong_code ?? ''), 'bo_company_idx' => (int) ($order->bo_company_idx ?? 0), 'bo_agency_idx' => (int) ($order->bo_agency_idx ?? 0), 'bo_fee_rate' => (float) ($order->bo_fee_rate ?? 0), 'bo_order_date' => (string) ($order->bo_order_date ?? ''), 'bo_lot_no' => (string) $lotNo, 'bo_status' => (string) ($order->bo_status ?? 'normal'), ]; $hashItems = []; foreach ($items as $item) { $code = (string) ($item->boi_bag_code ?? ''); $pack = $packMap[$code] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1]; $qtySheet = max(0, (int) ($item->boi_qty_sheet ?? 0)); $qtyPack = intdiv($qtySheet, max(1, (int) $pack['pack_per_sheet'])); $hashItems[] = [ 'boi_idx' => (int) ($item->boi_idx ?? 0), 'boi_bag_code' => $code, 'boi_bag_name' => (string) ($item->boi_bag_name ?? ''), 'boi_unit_price' => (float) ($item->boi_unit_price ?? 0), 'boi_qty_box' => (int) ($item->boi_qty_box ?? 0), 'boi_qty_pack' => $qtyPack, 'boi_qty_sheet' => $qtySheet, 'pack_per_sheet' => (int) $pack['pack_per_sheet'], 'total_per_box' => (int) $pack['total_per_box'], 'boi_amount' => (float) ($item->boi_amount ?? 0), ]; } $orderHash = trim((string) ($order->bo_hash ?? '')); if ($orderHash === '') { $payload = [ 'bo_idx' => $boIdx, 'order' => $orderData, 'items' => $hashItems, ]; $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: (string) $boIdx; $orderHash = hash('sha256', $payloadJson); } $seedPath = $this->generateLotSeedFile($uuid, $version, $lotNo, $orderData, $hashItems, $orderHash); $seedBinary = @file_get_contents($seedPath); if (! is_string($seedBinary) || $seedBinary === '') { return redirect()->back()->with('error', 'seed 파일 생성에는 성공했으나 파일을 읽을 수 없습니다.'); } return $this->response ->download($seedPath, $seedBinary) ->setFileName(basename($seedPath)); } /** * @param array $orderData * @param array> $items */ private function generateLotSeedFile(string $uuid, int $version, string $lotNo, array $orderData, array $items, string $orderHash): string { $baseDir = WRITEPATH . 'barcode-seeds'; if (! is_dir($baseDir)) { mkdir($baseDir, 0775, true); } $keyDir = WRITEPATH . 'keys'; if (! is_dir($keyDir)) { mkdir($keyDir, 0775, true); } $privateKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_private.pem'; $publicKeyPath = $keyDir . DIRECTORY_SEPARATOR . 'barcode_seed_public.pem'; if (! is_file($privateKeyPath) || ! is_file($publicKeyPath)) { $config = [ 'private_key_bits' => 2048, 'private_key_type' => OPENSSL_KEYTYPE_RSA, ]; $resource = openssl_pkey_new($config); if ($resource !== false) { $privatePem = ''; openssl_pkey_export($resource, $privatePem); $details = openssl_pkey_get_details($resource); $publicPem = $details['key'] ?? ''; if ($privatePem !== '' && $publicPem !== '') { file_put_contents($privateKeyPath, $privatePem); file_put_contents($publicKeyPath, $publicPem); } } } $payload = [ 'uuid' => $uuid, 'version' => $version, 'lot_no' => $lotNo, 'order_hash' => $orderHash, 'order' => $orderData, 'items' => $items, ]; $payloadJson = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES) ?: '{}'; $aesKey = random_bytes(32); $iv = random_bytes(16); $cipherRaw = openssl_encrypt($payloadJson, 'AES-256-CBC', $aesKey, OPENSSL_RAW_DATA, $iv); if ($cipherRaw === false) { $cipherRaw = $payloadJson; } $encryptedKey = ''; $publicPem = is_file($publicKeyPath) ? file_get_contents($publicKeyPath) : ''; if (is_string($publicPem) && $publicPem !== '') { openssl_public_encrypt($aesKey, $encryptedKey, $publicPem, OPENSSL_PKCS1_OAEP_PADDING); } $seed = [ 'algorithm' => ['symmetric' => 'AES-256-CBC', 'asymmetric' => 'RSA-2048'], 'lot_no' => $lotNo, 'uuid' => $uuid, 'version' => $version, 'iv_b64' => base64_encode($iv), 'key_b64' => $encryptedKey !== '' ? base64_encode($encryptedKey) : '', 'cipher_b64' => base64_encode((string) $cipherRaw), 'payload_hash' => hash('sha256', $payloadJson), 'created_at' => date('c'), ]; $fileName = sprintf('%s_v%d_diskette_%s.seed.json', $lotNo, $version, date('Ymd_His')); $fullPath = $baseDir . DIRECTORY_SEPARATOR . $fileName; file_put_contents($fullPath, json_encode($seed, JSON_PRETTY_PRINT | JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES)); return $fullPath; } /** * 발주 변경 허브: 발주월·변경 구분 선택 후 목록에서 발주를 선택 (GBMS 발주 변경 화면 흐름). */ public function orderChange(): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); $month = $this->request->getGet('month'); if ($month === null || $month === '' || ! is_string($month) || ! preg_match('/^\d{4}-\d{2}$/', $month)) { $month = date('Y-m'); } $hubMode = $this->request->getGet('hub_mode'); $hubMode = in_array($hubMode, ['price', 'meta', 'delete'], true) ? $hubMode : 'meta'; $companyMap = []; if ($lgIdx) { $companies = model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll(); foreach ($companies as $company) { $companyMap[(int) $company->cp_idx] = (string) $company->cp_name; } } $monthOrders = []; if ($lgIdx) { $start = $month . '-01'; $end = date('Y-m-t', strtotime($start . ' 00:00:00')); $monthOrders = model(BagOrderModel::class) ->where('bo_lg_idx', $lgIdx) ->whereLatestHead($lgIdx) ->where('bo_order_date >=', $start) ->where('bo_order_date <=', $end) ->whereIn('bo_status', ['normal', 'cancelled']) ->orderBy('bo_order_date', 'DESC') ->orderBy('bo_idx', 'DESC') ->findAll(); } if ($hubMode === 'delete') { foreach ($monthOrders as $row) { if ((string) ($row->bo_status ?? '') === 'normal') { return redirect()->to( site_url('bag/order/revise/' . (int) $row->bo_idx . '?change_mode=delete') ); } } if ($lgIdx) { session()->setFlashdata('error', '해당 월에 삭제할 수 있는 발주(정상)가 없습니다.'); } } return $this->render( '발주 변경', 'bag/order_change', compact('month', 'hubMode', 'monthOrders', 'companyMap') ); } public function orderRevise(int $id): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); $orderModel = model(BagOrderModel::class); $itemModel = model(\App\Models\BagOrderItemModel::class); $target = $orderModel->find($id); if (! $target || (int) $target->bo_lg_idx !== $lgIdx) { return redirect()->to(site_url('bag/order/change'))->with('error', '수정할 발주를 찾을 수 없습니다.'); } if ((string) ($target->bo_status ?? '') !== 'normal') { return redirect()->to(site_url('bag/order/change'))->with('error', '변경할 수 없는 발주입니다.'); } $changeMode = $this->request->getGet('change_mode'); $changeMode = in_array($changeMode, ['price', 'meta', 'delete'], true) ? $changeMode : 'meta'; $companies = $lgIdx ? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)->findAll() : []; $associations = $lgIdx ? model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '협회')->where('cp_state', 1)->findAll() : []; $agencies = $lgIdx ? model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; $priceMapRows = $lgIdx ? model(BagPriceModel::class)->latestActiveMapByBagCode($lgIdx) : []; $units = $lgIdx ? model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll() : []; $companyMap = []; foreach ($companies as $company) { $companyMap[(int) $company->cp_idx] = (string) $company->cp_name; } $agencyMap = []; foreach ($agencies as $agency) { $agencyMap[(int) $agency->sa_idx] = '[' . ($agency->sa_kind ?? '') . '] ' . ($agency->sa_code ?? '') . ' — ' . ($agency->sa_name ?? ''); } $bagNameMap = []; foreach ($bagCodes as $codeDetail) { $bagNameMap[(string) $codeDetail->cd_code] = (string) $codeDetail->cd_name; } $priceMap = []; foreach ($priceMapRows as $bagCode => $price) { $priceMap[(string) $bagCode] = (float) ($price->bp_order_price ?? 0); } $unitMap = []; foreach ($units as $unit) { $unitMap[(string) $unit->pu_bag_code] = [ 'boxPerPack' => (int) $unit->pu_box_per_pack, 'packPerSheet' => (int) $unit->pu_pack_per_sheet, 'totalPerBox' => (int) $unit->pu_total_per_box, ]; } $bagReferenceRows = []; foreach ($bagCodes as $codeDetail) { $bagCode = (string) $codeDetail->cd_code; $unit = $unitMap[$bagCode] ?? ['boxPerPack' => 0, 'packPerSheet' => 0, 'totalPerBox' => 0]; $bagReferenceRows[] = [ 'code' => $bagCode, 'name' => (string) ($bagNameMap[$bagCode] ?? ''), 'orderPrice' => (float) ($priceMap[$bagCode] ?? 0), 'boxPerPack' => (int) $unit['boxPerPack'], 'packPerSheet' => (int) $unit['packPerSheet'], 'totalPerBox' => (int) $unit['totalPerBox'], ]; } $items = $itemModel->where('boi_bo_idx', (int) $target->bo_idx)->orderBy('boi_idx', 'ASC')->findAll(); $orderReturnMonth = substr((string) ($target->bo_order_date ?? date('Y-m-d')), 0, 7); $monthStart = $orderReturnMonth . '-01'; $monthEnd = date('Y-m-t', strtotime($monthStart . ' 00:00:00')); $recentOrders = $lgIdx ? $orderModel->where('bo_lg_idx', $lgIdx) ->whereLatestHead($lgIdx) ->where('bo_order_date >=', $monthStart) ->where('bo_order_date <=', $monthEnd) ->whereIn('bo_status', ['normal', 'cancelled']) ->orderBy('bo_order_date', 'DESC') ->orderBy('bo_idx', 'DESC') ->findAll() : []; $itemCodes = []; $itemQtyBoxes = []; $itemQtySheets = []; foreach ($items as $item) { $itemCodes[] = (string) ($item->boi_bag_code ?? ''); $itemQtyBoxes[] = (int) ($item->boi_qty_box ?? 0); $itemQtySheets[] = (int) ($item->boi_qty_sheet ?? 0); } $savedLinePrices = []; foreach ($items as $item) { $savedLinePrices[(string) ($item->boi_bag_code ?? '')] = (float) ($item->boi_unit_price ?? 0); } foreach ($bagReferenceRows as &$brow) { $c = (string) ($brow['code'] ?? ''); if ($c !== '' && isset($savedLinePrices[$c])) { $brow['orderPrice'] = $savedLinePrices[$c]; } } unset($brow); $orderLotNo = (string) ($target->bo_lot_no ?? ''); $editDefaults = [ 'bo_source_idx' => (int) $target->bo_idx, 'bo_order_date' => (string) ($target->bo_order_date ?? date('Y-m-d')), 'bo_order_month_ui' => substr((string) ($target->bo_order_date ?? date('Y-m-d')), 0, 7), 'bo_fee_rate' => (string) ($target->bo_fee_rate ?? '0'), 'bo_association_idx' => (string) ($target->bo_association_idx ?? ''), 'bo_company_idx' => (string) ($target->bo_company_idx ?? ''), 'bo_agency_idx' => (string) ($target->bo_agency_idx ?? ''), 'item_bag_code' => $itemCodes, 'item_qty_box' => $itemQtyBoxes, 'item_qty_sheet' => $itemQtySheets, ]; return $this->render( '발주 변경', 'bag/create_bag_order', compact( 'companies', 'associations', 'agencies', 'bagCodes', 'recentOrders', 'companyMap', 'agencyMap', 'bagReferenceRows', 'editDefaults', 'changeMode', 'orderReturnMonth', 'orderLotNo' ) + ['editMode' => true, 'hubReturn' => true] ); } public function orderStore() { $admin = new \App\Controllers\Admin\BagOrder(); $admin->initController($this->request, $this->response, service('logger')); $result = $admin->store(); if ($result instanceof RedirectResponse) { $success = session()->getFlashdata('success'); $error = session()->getFlashdata('error'); $errors = session()->getFlashdata('errors'); if (! empty($error) || ! empty($errors)) { $sourceIdx = (int) ($this->request->getPost('bo_source_idx') ?? 0); $reviseMode = (string) ($this->request->getPost('bo_change_mode') ?? 'meta'); $redirectUrl = $sourceIdx > 0 ? site_url('bag/order/revise/' . $sourceIdx . '?change_mode=' . rawurlencode($reviseMode)) : site_url('bag/order/create'); return redirect()->to($redirectUrl) ->withInput() ->with('error', $error) ->with('errors', $errors); } $returnHub = (int) ($this->request->getPost('order_return_hub') ?? 0) === 1; $returnMonth = (string) ($this->request->getPost('order_return_month') ?? ''); $sourceIdxPost = (int) ($this->request->getPost('bo_source_idx') ?? 0); if ($returnHub && $sourceIdxPost > 0 && preg_match('/^\d{4}-\d{2}$/', $returnMonth)) { return redirect()->to(site_url('bag/order/change?month=' . $returnMonth)) ->with('success', $success ?? '발주가 저장되었습니다.'); } return redirect()->to(site_url('bag/order/create')) ->with('success', $success); } $returnHub = (int) ($this->request->getPost('order_return_hub') ?? 0) === 1; $returnMonth = (string) ($this->request->getPost('order_return_month') ?? ''); if ($returnHub && (int) ($this->request->getPost('bo_source_idx') ?? 0) > 0 && preg_match('/^\d{4}-\d{2}$/', $returnMonth)) { return redirect()->to(site_url('bag/order/change?month=' . $returnMonth))->with('success', '발주가 저장되었습니다.'); } return redirect()->to(site_url('bag/order/create'))->with('success', '발주 등록되었습니다.'); } public function orderDeletePost() { $id = (int) ($this->request->getPost('bo_idx') ?? 0); if ($id <= 0) { return redirect()->to(site_url('bag/order/change'))->with('error', '삭제할 발주를 선택해 주세요.'); } return $this->orderDelete($id); } public function orderDelete(int $id) { helper('admin'); $lgIdx = $this->lgIdx(); if ($lgIdx === null || $lgIdx <= 0) { return redirect()->to(site_url('bag/order/change'))->with('error', '지자체를 선택해 주세요.'); } $orderModel = model(BagOrderModel::class); $order = $orderModel->find($id); if (! $order || (int) $order->bo_lg_idx !== $lgIdx) { return redirect()->to(site_url('bag/order/change'))->with('error', '발주를 찾을 수 없습니다.'); } if ((string) ($order->bo_status ?? '') !== 'normal') { return redirect()->to(site_url('bag/order/change'))->with('error', '삭제할 수 없는 발주입니다.'); } $month = substr((string) ($order->bo_order_date ?? date('Y-m-d')), 0, 7); $admin = new \App\Controllers\Admin\BagOrder(); $admin->initController($this->request, $this->response, service('logger')); $response = $admin->delete($id); if ($response instanceof RedirectResponse) { $msg = session()->getFlashdata('success') ?? '발주가 삭제 처리되었습니다.'; return redirect()->to(site_url('bag/order/change?month=' . $month))->with('success', $msg); } return redirect()->to(site_url('bag/order/change?month=' . $month))->with('success', '처리되었습니다.'); } public function orderCancel(int $id) { helper('admin'); $lgIdx = $this->lgIdx(); if (!$lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 확인할 수 없습니다.'); } $orderModel = model(BagOrderModel::class); $order = $orderModel->find($id); if (!$order || (int) $order->bo_lg_idx !== $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '발주를 찾을 수 없습니다.'); } $before = (array) $order; $orderModel->update($id, ['bo_status' => 'cancelled', 'bo_moddate' => date('Y-m-d H:i:s')]); helper('audit'); audit_log('update', 'bag_order', $id, $before, ['bo_status' => 'cancelled']); return redirect()->to(site_url('bag/purchase-inbound'))->with('success', '발주가 취소되었습니다.'); } // --- 입고 처리 --- public function receivingCreate(): string { return $this->receivingScanner(); } public function receivingStore() { return $this->receivingScannerStore(); } /** * 발주 입고(스캐너 대체 수동입력) * - 미입고가 남은 발주의 LOT·봉투(이름)로 조회 범위를 좁힌 뒤 입고 처리 * - 인수자: 대행소(agency) 담당자, 기본값 동명이면 로그인 사용자명과 일치하는 담당자 * - 인계자: 제작업체(company) 담당자 */ public function receivingScanner(): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $companyIdx = (int) old('company_idx', (int) ($this->request->getGet('company_idx') ?? 0)); $lotNo = ''; $bagCode = ''; $companies = model(CompanyModel::class) ->where('cp_lg_idx', $lgIdx) ->where('cp_type', '제작업체') ->where('cp_state', 1) ->orderBy('cp_name', 'ASC') ->findAll(); $defaultCompanyIdx = ! empty($companies) ? (int) ($companies[0]->cp_idx ?? 0) : 0; if ($companyIdx > 0) { $validCompany = false; foreach ($companies as $company) { if ((int) ($company->cp_idx ?? 0) === $companyIdx) { $validCompany = true; break; } } if (! $validCompany) { $companyIdx = $defaultCompanyIdx; } } elseif ($defaultCompanyIdx > 0) { // 초기 진입 시 드롭다운 최상단 제작업체를 기본 선택한다. $companyIdx = $defaultCompanyIdx; } $lotChoices = []; $bagFilterOptions = $this->receivingBagFilterOptions($lgIdx, $companyIdx, ''); $pick = $this->receivingManagerPickers($lgIdx); $recvSel = $this->receivingReceiverSelect($lgIdx); $receiverRef = (string) old('br_receiver_ref', $recvSel['defaultReceiverRef']); $receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef); if ($receiverRef === '') { $receiverRef = $recvSel['defaultReceiverRef']; } $senderIdx = (int) old('br_sender_idx', $pick['defaultSenderIdx']); $rows = $companyIdx > 0 ? $this->buildReceivingCandidateRows($lgIdx, $companyIdx, '', true, '') : []; $rowsByKey = []; foreach ($rows as $row) { $rowsByKey[(string) $row['row_key']] = $row; } return $this->render( '발주 입고(스캐너)', 'bag/receiving_scanner', [ 'companyIdx' => $companyIdx, 'companies' => $companies, 'lotNo' => '', 'bagCode' => '', 'bagFilterOptions' => $bagFilterOptions, 'lotChoices' => $lotChoices, 'receiverOptions' => $recvSel['receiverOptions'], 'receiverRef' => $receiverRef, 'senders' => $pick['senders'], 'senderIdx' => $senderIdx, 'rows' => $rows, 'rowsByKey' => $rowsByKey, ] ); } public function receivingScannerStore(): RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $receiveDate = (string) ($this->request->getPost('br_receive_date') ?? date('Y-m-d')); $companyIdx = (int) ($this->request->getPost('company_idx') ?? 0); $lotNo = ''; $filterBagCode = ''; $recvSel = $this->receivingReceiverSelect($lgIdx); $receiverRef = (string) ($this->request->getPost('br_receiver_ref') ?? ''); $receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef); if ($receiverRef === '') { $receiverRef = $recvSel['defaultReceiverRef']; } $receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef); $senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0); $inputQty = $this->request->getPost('receive_qty_sheet'); $inputQty = is_array($inputQty) ? $inputQty : []; if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) { return redirect()->back()->withInput()->with('error', '입고일 형식을 확인해 주세요.'); } if ($companyIdx <= 0) { return redirect()->back()->withInput()->with('error', '제작업체를 선택해 주세요.'); } if ($receiverIdx <= 0) { return redirect()->back()->withInput()->with('error', '인수자를 선택해 주세요.'); } $senderResolved = $this->resolveCompanySenderName($lgIdx, $senderIdx); $rows = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, '', true, ''); $rowMap = []; foreach ($rows as $row) { $rowMap[(string) $row['row_key']] = $row; } $insertRows = []; foreach ($inputQty as $rowKey => $qtyRaw) { $rowKey = (string) $rowKey; $qty = (int) $qtyRaw; if ($qty <= 0 || ! isset($rowMap[$rowKey])) { continue; } $base = $rowMap[$rowKey]; $pending = (int) ($base['pending_qty_sheet'] ?? 0); if ($pending <= 0) { continue; } if ($qty > $pending) { $qty = $pending; } $totalPerBox = max(1, (int) ($base['total_per_box'] ?? 1)); $qtyBox = intdiv($qty, $totalPerBox); $sender = $senderResolved !== '' ? $senderResolved : (string) ($base['company_rep_name'] ?? ''); $insertRows[] = [ 'br_bo_idx' => (int) $base['bo_idx'], 'br_lg_idx' => $lgIdx, 'br_bag_code' => (string) $base['bag_code'], 'br_bag_name' => (string) $base['bag_name'], 'br_qty_box' => $qtyBox, 'br_qty_sheet' => $qty, 'br_receive_date' => $receiveDate, 'br_receiver_idx' => $receiverIdx, 'br_sender_name' => $sender, 'br_type' => 'scanner', 'br_regdate' => date('Y-m-d H:i:s'), ]; } if (empty($insertRows)) { return redirect()->back()->withInput()->with('error', '입고 처리할 수량을 입력해 주세요.'); } $recvModel = model(BagReceivingModel::class); $invModel = model(BagInventoryModel::class); $db = \Config\Database::connect(); $db->transStart(); foreach ($insertRows as $row) { $recvModel->insert($row); $brIdx = (int) $recvModel->getInsertID(); $invModel->adjustQty( $lgIdx, (string) $row['br_bag_code'], (string) $row['br_bag_name'], (int) $row['br_qty_sheet'] ); $this->createReceivingPackCodes( $lgIdx, $brIdx, (int) $row['br_bo_idx'], (string) $row['br_bag_code'], (string) $row['br_bag_name'], (int) $row['br_qty_sheet'], max(1, (int) ($rowMap[(string) ((int) $row['br_bo_idx'] . '|' . (string) $row['br_bag_code'])]['pack_per_sheet'] ?? 1)), max(1, (int) ($rowMap[(string) ((int) $row['br_bo_idx'] . '|' . (string) $row['br_bag_code'])]['total_per_box'] ?? 1)) ); } $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->withInput()->with('error', '입고 처리 중 오류가 발생했습니다.'); } $query = ['company_idx' => $companyIdx]; return redirect()->to(site_url('bag/receiving/scanner') . '?' . http_build_query($query)) ->with('success', count($insertRows) . '건 입고 처리되었습니다.'); } /** * 일괄 입고: LOT-봉투 행 기준 미입고량 전체 입고. */ public function receivingBatch(): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0); $bagCode = trim((string) ($this->request->getGet('bag_code') ?? '')); $companies = model(CompanyModel::class) ->where('cp_lg_idx', $lgIdx) ->where('cp_type', '제작업체') ->where('cp_state', 1) ->orderBy('cp_name', 'ASC') ->findAll(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodeOptions = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; $pick = $this->receivingManagerPickers($lgIdx); $recvSel = $this->receivingReceiverSelect($lgIdx); $receiverRef = (string) old('br_receiver_ref', $recvSel['defaultReceiverRef']); $receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef); if ($receiverRef === '') { $receiverRef = $recvSel['defaultReceiverRef']; } // 조회 화면에서는 입고완료 행도 함께 보여 미입고량 0을 확인할 수 있게 한다. $rows = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, $bagCode, false, ''); return $this->render( '일괄 입고', 'bag/receiving_batch', [ 'companyIdx' => $companyIdx, 'bagCode' => $bagCode, 'companies' => $companies, 'bagCodeOptions' => $bagCodeOptions, 'receiverOptions' => $recvSel['receiverOptions'], 'receiverRef' => $receiverRef, 'senders' => $pick['senders'], 'senderIdx' => (int) old('br_sender_idx', $pick['defaultSenderIdx']), 'rows' => $rows, ] ); } public function receivingBatchStore(): RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $companyIdx = (int) ($this->request->getPost('company_idx') ?? 0); $bagCode = trim((string) ($this->request->getPost('bag_code') ?? '')); $selected = $this->request->getPost('selected_rows'); $selected = is_array($selected) ? array_map('strval', $selected) : []; $receiveDate = (string) ($this->request->getPost('br_receive_date') ?? date('Y-m-d')); $recvSel = $this->receivingReceiverSelect($lgIdx); $receiverRef = (string) ($this->request->getPost('br_receiver_ref') ?? ''); $receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef); if ($receiverRef === '') { $receiverRef = $recvSel['defaultReceiverRef']; } $receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef); $senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0); if (empty($selected)) { return redirect()->back()->withInput()->with('error', '일괄 입고할 행을 선택해 주세요.'); } if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) { return redirect()->back()->withInput()->with('error', '입고일 형식을 확인해 주세요.'); } if ($receiverIdx <= 0) { return redirect()->back()->withInput()->with('error', '인수자를 선택해 주세요.'); } $senderResolved = $this->resolveCompanySenderName($lgIdx, $senderIdx); $rows = $this->buildReceivingCandidateRows($lgIdx, 0, '', true, ''); $rowMap = []; foreach ($rows as $row) { $rowMap[(string) $row['row_key']] = $row; } $insertRows = []; foreach ($selected as $rowKey) { if (! isset($rowMap[$rowKey])) { continue; } $base = $rowMap[$rowKey]; $qty = (int) ($base['pending_qty_sheet'] ?? 0); if ($qty <= 0) { continue; } $totalPerBox = max(1, (int) ($base['total_per_box'] ?? 1)); $qtyBox = intdiv($qty, $totalPerBox); $sender = $senderResolved !== '' ? $senderResolved : (string) ($base['company_rep_name'] ?? ''); $insertRows[] = [ 'br_bo_idx' => (int) $base['bo_idx'], 'br_lg_idx' => $lgIdx, 'br_bag_code' => (string) $base['bag_code'], 'br_bag_name' => (string) $base['bag_name'], 'br_qty_box' => $qtyBox, 'br_qty_sheet' => $qty, 'br_receive_date' => $receiveDate, 'br_receiver_idx' => $receiverIdx, 'br_sender_name' => $sender, 'br_type' => 'batch', 'br_regdate' => date('Y-m-d H:i:s'), ]; } if (empty($insertRows)) { return redirect()->back()->withInput()->with('error', '선택한 행에 입고할 수량이 없습니다.'); } $recvModel = model(BagReceivingModel::class); $invModel = model(BagInventoryModel::class); $db = \Config\Database::connect(); $db->transStart(); foreach ($insertRows as $row) { $recvModel->insert($row); $brIdx = (int) $recvModel->getInsertID(); $invModel->adjustQty( $lgIdx, (string) $row['br_bag_code'], (string) $row['br_bag_name'], (int) $row['br_qty_sheet'] ); $this->createReceivingPackCodes( $lgIdx, $brIdx, (int) $row['br_bo_idx'], (string) $row['br_bag_code'], (string) $row['br_bag_name'], (int) $row['br_qty_sheet'], max(1, (int) ($rowMap[(string) ((int) $row['br_bo_idx'] . '|' . (string) $row['br_bag_code'])]['pack_per_sheet'] ?? 1)), max(1, (int) ($rowMap[(string) ((int) $row['br_bo_idx'] . '|' . (string) $row['br_bag_code'])]['total_per_box'] ?? 1)) ); } $db->transComplete(); if (! $db->transStatus()) { return redirect()->back()->withInput()->with('error', '일괄 입고 처리 중 오류가 발생했습니다.'); } return redirect()->to(site_url('bag/receiving/batch?company_idx=' . $companyIdx . '&bag_code=' . rawurlencode($bagCode))) ->with('success', count($insertRows) . '건 일괄 입고 처리되었습니다.'); } public function receivingStatus(): string|RedirectResponse { helper('admin'); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $startDate = (string) ($this->request->getGet('start_date') ?? date('Y-m-01')); $endDate = (string) ($this->request->getGet('end_date') ?? date('Y-m-d')); $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0); $bagCode = trim((string) ($this->request->getGet('bag_code') ?? '')); $receiveType = (string) ($this->request->getGet('receive_type') ?? 'all'); if (! in_array($receiveType, ['all', 'completed', 'pending'], true)) { $receiveType = 'all'; } $companies = model(CompanyModel::class) ->where('cp_lg_idx', $lgIdx) ->where('cp_type', '제작업체') ->where('cp_state', 1) ->orderBy('cp_name', 'ASC') ->findAll(); $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodeOptions = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; $rows = $this->buildReceivingStatusRows($lgIdx, $startDate, $endDate, $companyIdx, $bagCode, $receiveType); $groupTotals = []; $grandTotalReceive = 0; foreach ($rows as $row) { $key = (string) ($row['display_date'] ?? ''); if (! isset($groupTotals[$key])) { $groupTotals[$key] = 0; } $groupTotals[$key] += (int) ($row['received_qty_sheet'] ?? 0); $grandTotalReceive += (int) ($row['received_qty_sheet'] ?? 0); } return $this->render( '입고 현황', 'bag/receiving_status', compact( 'startDate', 'endDate', 'companyIdx', 'bagCode', 'receiveType', 'companies', 'bagCodeOptions', 'rows', 'groupTotals', 'grandTotalReceive' ) ); } public function receivingStatusExport(): RedirectResponse { helper(['admin', 'export']); $lgIdx = $this->lgIdx(); if (! $lgIdx) { return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.'); } $startDate = (string) ($this->request->getGet('start_date') ?? date('Y-m-01')); $endDate = (string) ($this->request->getGet('end_date') ?? date('Y-m-d')); $companyIdx = (int) ($this->request->getGet('company_idx') ?? 0); $bagCode = trim((string) ($this->request->getGet('bag_code') ?? '')); $receiveType = (string) ($this->request->getGet('receive_type') ?? 'all'); if (! in_array($receiveType, ['all', 'completed', 'pending'], true)) { $receiveType = 'all'; } $rows = $this->buildReceivingStatusRows($lgIdx, $startDate, $endDate, $companyIdx, $bagCode, $receiveType); $exportRows = []; foreach ($rows as $row) { $exportRows[] = [ (string) ($row['display_date'] ?? ''), (string) ($row['bag_name'] ?? ''), (int) ($row['received_qty_sheet'] ?? 0), (string) ($row['order_date'] ?? ''), (int) ($row['order_qty_sheet'] ?? 0), (string) ($row['order_no'] ?? ''), (string) ($row['company_name'] ?? ''), (string) ($row['receive_status_label'] ?? ''), (string) ($row['agency_name'] ?? ''), '', ]; } export_xlsx( '입고현황_' . date('Ymd'), '입고현황', ['입고일자', '품명', '입고수량', '발주일자', '발주수량', '발주번호', '제작업체', '입고여부', '입고처', '비고'], $exportRows ); } /** * 미입고 잔량이 있는 발주 LOT 목록(스캐너 입고용 드롭다운). * * @return list */ private function buildReceivingPendingLotChoices(int $lgIdx, int $companyIdx = 0): array { $rows = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, '', true, ''); $byLot = []; foreach ($rows as $r) { $lot = (string) ($r['lot_no'] ?? ''); if ($lot === '') { continue; } if (! isset($byLot[$lot])) { $byLot[$lot] = [ 'lot_no' => $lot, 'bo_idx' => (int) ($r['bo_idx'] ?? 0), 'order_date' => (string) ($r['order_date'] ?? ''), 'company_name' => (string) ($r['company_name'] ?? ''), 'pending_lines' => 0, ]; } $byLot[$lot]['pending_lines']++; } $list = array_values($byLot); usort($list, static function (array $a, array $b): int { $da = (string) ($a['order_date'] ?? ''); $db = (string) ($b['order_date'] ?? ''); if ($da === $db) { return strcmp((string) ($b['lot_no'] ?? ''), (string) ($a['lot_no'] ?? '')); } return strcmp($db, $da); }); return $list; } private function sanitizeLotNoForReceiving(int $lgIdx, int $companyIdx, string $lotNo): string { $lotNo = trim($lotNo); if ($lotNo === '') { return ''; } foreach ($this->buildReceivingPendingLotChoices($lgIdx, $companyIdx) as $choice) { if ((string) ($choice['lot_no'] ?? '') === $lotNo) { return $lotNo; } } return ''; } /** * 선택 조건(제작업체 + LOT)에 해당하는 미입고 품목(봉투) 목록 — 조회 조건 드롭다운용. * * @return list */ private function receivingBagFilterOptions(int $lgIdx, int $companyIdx, string $lotNo = ''): array { if ($companyIdx <= 0) { return []; } $allForFilter = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, '', true, $lotNo); $byCode = []; foreach ($allForFilter as $r) { $c = (string) ($r['bag_code'] ?? ''); if ($c === '') { continue; } if (! isset($byCode[$c])) { $byCode[$c] = (string) ($r['bag_name'] ?? ''); } } $list = []; foreach ($byCode as $code => $name) { $list[] = ['bag_code' => $code, 'bag_name' => $name]; } usort($list, static fn (array $a, array $b): int => strcmp($a['bag_name'], $b['bag_name'])); return $list; } private function sanitizeBagCodeForReceiving(int $lgIdx, int $companyIdx, string $lotNo, string $bagCode): string { $bagCode = trim($bagCode); if ($bagCode === '' || $companyIdx <= 0) { return ''; } foreach ($this->receivingBagFilterOptions($lgIdx, $companyIdx, $lotNo) as $opt) { if ($opt['bag_code'] === $bagCode) { return $bagCode; } } return ''; } /** * 입고 대상 후보(LOT-봉투행) 생성. * * @param string $lotNo 빈 문자열이면 LOT 제한 없음. 지정 시 해당 LOT(최신 헤드) 발주만. */ private function buildReceivingCandidateRows(int $lgIdx, int $companyIdx = 0, string $bagCode = '', bool $onlyPending = true, string $lotNo = ''): array { $orderBuilder = model(BagOrderModel::class) ->where('bo_lg_idx', $lgIdx) ->whereLatestHead($lgIdx) ->where('bo_status', 'normal') ->orderBy('bo_order_date', 'DESC') ->orderBy('bo_idx', 'DESC'); if ($lotNo !== '') { $orderBuilder->where('bo_lot_no', $lotNo); } if ($companyIdx > 0) { $orderBuilder->where('bo_company_idx', $companyIdx); } $orders = $orderBuilder->findAll(); if (empty($orders)) { return []; } $orderIds = array_map(static fn($o) => (int) ($o->bo_idx ?? 0), $orders); $companyMap = []; foreach (model(CompanyModel::class)->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->findAll() as $company) { $companyMap[(int) ($company->cp_idx ?? 0)] = [ 'name' => (string) ($company->cp_name ?? ''), 'rep' => (string) ($company->cp_rep_name ?? ''), ]; } $agencyMap = []; foreach (model(SalesAgencyModel::class)->where('sa_lg_idx', $lgIdx)->orderForDisplay()->findAll() as $agency) { $agencyMap[(int) ($agency->sa_idx ?? 0)] = (string) ($agency->sa_name ?? ''); } $unitMap = []; foreach (model(PackagingUnitModel::class)->where('pu_lg_idx', $lgIdx)->where('pu_state', 1)->findAll() as $unit) { $unitMap[(string) ($unit->pu_bag_code ?? '')] = [ 'pack_per_sheet' => (int) ($unit->pu_pack_per_sheet ?? 1), 'total_per_box' => (int) ($unit->pu_total_per_box ?? 1), ]; } $itemBuilder = model(BagOrderItemModel::class)->whereIn('boi_bo_idx', $orderIds); if ($bagCode !== '') { $itemBuilder->where('boi_bag_code', $bagCode); } $items = $itemBuilder->orderBy('boi_bo_idx', 'DESC')->orderBy('boi_idx', 'ASC')->findAll(); $receivedRows = model(BagReceivingModel::class) ->select('br_bo_idx, br_bag_code, SUM(br_qty_sheet) as recv_qty_sheet, MAX(br_receive_date) as last_receive_date') ->where('br_lg_idx', $lgIdx) ->whereIn('br_bo_idx', $orderIds) ->groupBy('br_bo_idx, br_bag_code') ->findAll(); $receivedMap = []; foreach ($receivedRows as $recv) { $receivedMap[(int) ($recv->br_bo_idx ?? 0) . '|' . (string) ($recv->br_bag_code ?? '')] = [ 'recv_qty_sheet' => (int) ($recv->recv_qty_sheet ?? 0), 'last_receive_date' => (string) ($recv->last_receive_date ?? ''), ]; } $orderMap = []; foreach ($orders as $order) { $orderMap[(int) ($order->bo_idx ?? 0)] = $order; } $rows = []; foreach ($items as $item) { $boIdx = (int) ($item->boi_bo_idx ?? 0); if (! isset($orderMap[$boIdx])) { continue; } $order = $orderMap[$boIdx]; $itemBagCode = (string) ($item->boi_bag_code ?? ''); $recv = $receivedMap[$boIdx . '|' . $itemBagCode] ?? ['recv_qty_sheet' => 0, 'last_receive_date' => '']; $orderQtySheet = (int) ($item->boi_qty_sheet ?? 0); $receivedQtySheet = min($orderQtySheet, (int) ($recv['recv_qty_sheet'] ?? 0)); $pendingQtySheet = max(0, $orderQtySheet - $receivedQtySheet); if ($onlyPending && $pendingQtySheet <= 0) { continue; } $unit = $unitMap[$itemBagCode] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1]; $companyInfo = $companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ['name' => '', 'rep' => '']; $rows[] = [ 'row_key' => $boIdx . '|' . $itemBagCode, 'bo_idx' => $boIdx, 'order_no' => sprintf('%06d', $boIdx), 'lot_no' => (string) ($order->bo_lot_no ?? ''), 'order_date' => (string) ($order->bo_order_date ?? ''), 'company_name' => (string) ($companyInfo['name'] ?? ''), 'company_rep_name' => (string) ($companyInfo['rep'] ?? ''), 'agency_name' => (string) ($agencyMap[(int) ($order->bo_agency_idx ?? 0)] ?? ''), 'bag_code' => $itemBagCode, 'bag_name' => (string) ($item->boi_bag_name ?? ''), 'order_qty_sheet' => $orderQtySheet, 'received_qty_sheet' => $receivedQtySheet, 'pending_qty_sheet' => $pendingQtySheet, 'pack_per_sheet' => max(1, (int) ($unit['pack_per_sheet'] ?? 1)), 'total_per_box' => max(1, (int) ($unit['total_per_box'] ?? 1)), 'last_receive_date' => (string) ($recv['last_receive_date'] ?? ''), ]; } return $rows; } private function buildReceivingStatusRows( int $lgIdx, string $startDate, string $endDate, int $companyIdx, string $bagCode, string $receiveType ): array { $rows = $this->buildReceivingCandidateRows($lgIdx, $companyIdx, $bagCode, false, ''); $filtered = []; foreach ($rows as $row) { $pendingQty = (int) ($row['pending_qty_sheet'] ?? 0); $isCompleted = $pendingQty <= 0; if ($receiveType === 'completed' && ! $isCompleted) { continue; } if ($receiveType === 'pending' && $isCompleted) { continue; } $displayDate = (string) ($row['last_receive_date'] ?? ''); if ($displayDate === '') { $displayDate = (string) ($row['order_date'] ?? ''); } if ($startDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate) && $displayDate < $startDate) { continue; } if ($endDate !== '' && preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate) && $displayDate > $endDate) { continue; } $row['display_date'] = $displayDate; $row['receive_status_label'] = $isCompleted ? '완료' : '미완료'; $filtered[] = $row; } usort($filtered, static function (array $a, array $b): int { $da = (string) ($a['display_date'] ?? ''); $db = (string) ($b['display_date'] ?? ''); if ($da === $db) { return strcmp((string) ($a['bag_name'] ?? ''), (string) ($b['bag_name'] ?? '')); } return strcmp($da, $db); }); return $filtered; } // --- 판매 등록 --- public function saleCreate(): string { helper('admin'); $lgIdx = $this->lgIdx(); $shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; return $this->render('판매 등록', 'bag/create_bag_sale', compact('shops', 'bagCodes')); } public function saleStore() { $admin = new \App\Controllers\Admin\BagSale(); $admin->initController($this->request, $this->response, service('logger')); $result = $admin->store(); if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) { return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors')); } return redirect()->to(site_url('bag/sales'))->with('success', '판매 등록되었습니다.'); } // --- 주문 접수 --- public function shopOrderCreate(): string { helper('admin'); $lgIdx = $this->lgIdx(); $shops = $lgIdx ? model(DesignatedShopModel::class)->where('ds_lg_idx', $lgIdx)->where('ds_state', 1)->findAll() : []; $kind = model(CodeKindModel::class)->where('ck_code', 'O')->first(); $bagCodes = $kind ? model(CodeDetailModel::class)->getByKind((int) $kind->ck_idx, true, $lgIdx) : []; return $this->render('주문 접수', 'bag/create_shop_order', compact('shops', 'bagCodes')); } public function shopOrderStore() { $admin = new \App\Controllers\Admin\ShopOrder(); $admin->initController($this->request, $this->response, service('logger')); $result = $admin->store(); if ($result instanceof \CodeIgniter\HTTP\RedirectResponse) { return redirect()->to(site_url('bag/sales'))->with('success', session()->getFlashdata('success'))->with('errors', session()->getFlashdata('errors')); } return redirect()->to(site_url('bag/sales'))->with('success', '주문 접수되었습니다.'); } }