feat: 실사 재고 바코드 리스트 — 창고에 있어야 하는 번호 조회/엑셀/인쇄
문의 #34: 실사 시 현재 창고에 남아있어야 하는(재고 상태) 봉투 번호(바코드) 목록을 뽑아 실물과 대조할 수 있게 신규 화면 추가. - bag/inventory/stock-barcode-list 라우트 + Bag::stockBarcodeList() - bag_receiving_pack_code 중 brpc_state='in_stock' 기준(기존 실사 스냅샷과 동일) - 품목/번호(박스·팩·시트·LOT) 필터, 인쇄, 엑셀저장(전체) - 화면 표시는 상위 2,000건 상한 + 안내 배너(합계·엑셀은 전체 기준) - 재고 관리 메뉴에 전 지자체 등록 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -38,6 +38,7 @@ $routes->get('bag/issue', 'Bag::issueLegacy');
|
|||||||
$routes->get('bag/issue/cancel', 'Bag::issue');
|
$routes->get('bag/issue/cancel', 'Bag::issue');
|
||||||
$routes->get('bag/inventory', 'Bag::inventory');
|
$routes->get('bag/inventory', 'Bag::inventory');
|
||||||
$routes->get('bag/inventory/export', 'Bag::inventoryExport');
|
$routes->get('bag/inventory/export', 'Bag::inventoryExport');
|
||||||
|
$routes->get('bag/inventory/stock-barcode-list', 'Bag::stockBarcodeList'); // 실사 재고 바코드 리스트(창고에 있어야 하는 번호)
|
||||||
$routes->get('bag/inventory/inspection-select', 'Bag::inspectionSelect');
|
$routes->get('bag/inventory/inspection-select', 'Bag::inspectionSelect');
|
||||||
$routes->get('bag/inventory/inspection-work', 'Bag::inspectionWork');
|
$routes->get('bag/inventory/inspection-work', 'Bag::inspectionWork');
|
||||||
$routes->post('bag/inventory/inspection-run', 'Bag::inspectionRun');
|
$routes->post('bag/inventory/inspection-run', 'Bag::inspectionRun');
|
||||||
|
|||||||
@@ -1312,6 +1312,111 @@ SQL);
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 실사 재고 바코드 리스트 — 현재 창고에 있어야 하는(재고로 남아있는) 봉투 번호(바코드) 목록.
|
||||||
|
* bag_receiving_pack_code 중 brpc_state='in_stock' 인 팩/박스 코드를 품목·박스·팩·시트번호(시작~끝)·매수로 뽑아
|
||||||
|
* 실사 전 대조용으로 조회/엑셀/인쇄한다. (기존 실사 스냅샷과 동일한 in_stock 기준)
|
||||||
|
*/
|
||||||
|
public function stockBarcodeList(): string|ResponseInterface|RedirectResponse
|
||||||
|
{
|
||||||
|
$lgIdx = $this->lgIdx();
|
||||||
|
if (! $lgIdx) {
|
||||||
|
return redirect()->to(site_url('bag/inventory'))->with('error', '지자체를 선택해 주세요.');
|
||||||
|
}
|
||||||
|
|
||||||
|
$db = \Config\Database::connect();
|
||||||
|
$bagCode = trim((string) ($this->request->getGet('bag_code') ?? ''));
|
||||||
|
$keyword = trim((string) ($this->request->getGet('keyword') ?? ''));
|
||||||
|
|
||||||
|
// 품목 선택 옵션 — 현재 재고로 남아있는 품목만
|
||||||
|
$itemOptions = $db->table('bag_receiving_pack_code')
|
||||||
|
->select('brpc_bag_code, MAX(brpc_bag_name) AS bag_name, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_qty', false)
|
||||||
|
->where('brpc_lg_idx', $lgIdx)
|
||||||
|
->where('brpc_state', 'in_stock')
|
||||||
|
->where('brpc_bag_code !=', '')
|
||||||
|
->groupBy('brpc_bag_code')
|
||||||
|
->orderBy('brpc_bag_code', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
$builder = $db->table('bag_receiving_pack_code')
|
||||||
|
->select('brpc_bag_code, brpc_bag_name, brpc_lot_no, brpc_box_code, brpc_pack_code, brpc_sheet_start_code, brpc_sheet_end_code, brpc_sheet_qty')
|
||||||
|
->where('brpc_lg_idx', $lgIdx)
|
||||||
|
->where('brpc_state', 'in_stock');
|
||||||
|
if ($bagCode !== '') {
|
||||||
|
$builder->where('brpc_bag_code', $bagCode);
|
||||||
|
}
|
||||||
|
if ($keyword !== '') {
|
||||||
|
$builder->groupStart()
|
||||||
|
->like('brpc_box_code', $keyword)
|
||||||
|
->orLike('brpc_pack_code', $keyword)
|
||||||
|
->orLike('brpc_sheet_start_code', $keyword)
|
||||||
|
->orLike('brpc_sheet_end_code', $keyword)
|
||||||
|
->orLike('brpc_lot_no', $keyword)
|
||||||
|
->groupEnd();
|
||||||
|
}
|
||||||
|
$rows = $builder
|
||||||
|
->orderBy('brpc_bag_code', 'ASC')
|
||||||
|
->orderBy('brpc_box_code', 'ASC')
|
||||||
|
->orderBy('brpc_pack_code', 'ASC')
|
||||||
|
->get()
|
||||||
|
->getResultArray();
|
||||||
|
|
||||||
|
// 합계 — 박스 수(고유), 팩 수, 총 매수
|
||||||
|
$totalPacks = count($rows);
|
||||||
|
$totalSheets = 0;
|
||||||
|
$boxSet = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$totalSheets += (int) ($r['brpc_sheet_qty'] ?? 0);
|
||||||
|
$bx = trim((string) ($r['brpc_box_code'] ?? ''));
|
||||||
|
if ($bx !== '') {
|
||||||
|
$boxSet[$bx] = true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
$totalBoxes = count($boxSet);
|
||||||
|
|
||||||
|
// 엑셀 다운로드 — 현재 필터 그대로
|
||||||
|
if ((string) ($this->request->getGet('export') ?? '') === '1') {
|
||||||
|
helper('export');
|
||||||
|
$exportRows = [];
|
||||||
|
foreach ($rows as $r) {
|
||||||
|
$exportRows[] = [
|
||||||
|
(string) ($r['brpc_bag_name'] ?? '') !== '' ? (string) $r['brpc_bag_name'] : (string) ($r['brpc_bag_code'] ?? ''),
|
||||||
|
(string) ($r['brpc_lot_no'] ?? ''),
|
||||||
|
(string) ($r['brpc_box_code'] ?? ''),
|
||||||
|
(string) ($r['brpc_pack_code'] ?? ''),
|
||||||
|
(string) ($r['brpc_sheet_start_code'] ?? ''),
|
||||||
|
(string) ($r['brpc_sheet_end_code'] ?? ''),
|
||||||
|
number_format((int) ($r['brpc_sheet_qty'] ?? 0)),
|
||||||
|
];
|
||||||
|
}
|
||||||
|
$exportRows[] = ['합계', '', '박스 ' . number_format($totalBoxes), '팩 ' . number_format($totalPacks), '', '', number_format($totalSheets)];
|
||||||
|
export_xlsx(
|
||||||
|
'실사재고바코드리스트_' . date('Ymd') . '.xlsx',
|
||||||
|
'창고재고바코드',
|
||||||
|
['품목', 'LOT', '박스코드', '팩코드', '시작번호', '끝번호', '매수'],
|
||||||
|
$exportRows
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
// 화면 표시는 성능을 위해 상위 N건까지만(합계·엑셀 저장은 항상 전체 기준)
|
||||||
|
$displayLimit = 2000;
|
||||||
|
$displayTruncated = count($rows) > $displayLimit;
|
||||||
|
$displayRows = $displayTruncated ? array_slice($rows, 0, $displayLimit) : $rows;
|
||||||
|
|
||||||
|
return $this->render('실사 재고 바코드 리스트', 'bag/inventory_stock_barcode_list', [
|
||||||
|
'bagCode' => $bagCode,
|
||||||
|
'keyword' => $keyword,
|
||||||
|
'itemOptions' => $itemOptions,
|
||||||
|
'rows' => $displayRows,
|
||||||
|
'totalBoxes' => $totalBoxes,
|
||||||
|
'totalPacks' => $totalPacks,
|
||||||
|
'totalSheets' => $totalSheets,
|
||||||
|
'displayTruncated' => $displayTruncated,
|
||||||
|
'displayLimit' => $displayLimit,
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @return array{
|
* @return array{
|
||||||
* rows: list<array{group:string,name:string,total_qty:int,gugun_qty:int,agency_qty:int}>,
|
* rows: list<array{group:string,name:string,total_qty:int,gugun_qty:int,agency_qty:int}>,
|
||||||
|
|||||||
129
app/Views/bag/inventory_stock_barcode_list.php
Normal file
129
app/Views/bag/inventory_stock_barcode_list.php
Normal file
@@ -0,0 +1,129 @@
|
|||||||
|
<?php
|
||||||
|
/**
|
||||||
|
* 실사 재고 바코드 리스트 — 현재 창고에 있어야 하는 봉투 번호(바코드) 목록.
|
||||||
|
* @var string $bagCode
|
||||||
|
* @var string $keyword
|
||||||
|
* @var list<array<string,mixed>> $itemOptions
|
||||||
|
* @var list<array<string,mixed>> $rows
|
||||||
|
* @var int $totalBoxes
|
||||||
|
* @var int $totalPacks
|
||||||
|
* @var int $totalSheets
|
||||||
|
*/
|
||||||
|
$bagCode = (string) ($bagCode ?? '');
|
||||||
|
$keyword = (string) ($keyword ?? '');
|
||||||
|
$itemOptions = is_array($itemOptions ?? null) ? $itemOptions : [];
|
||||||
|
$rows = is_array($rows ?? null) ? $rows : [];
|
||||||
|
$totalBoxes = (int) ($totalBoxes ?? 0);
|
||||||
|
$totalPacks = (int) ($totalPacks ?? 0);
|
||||||
|
$totalSheets = (int) ($totalSheets ?? 0);
|
||||||
|
$displayTruncated = (bool) ($displayTruncated ?? false);
|
||||||
|
$displayLimit = (int) ($displayLimit ?? 2000);
|
||||||
|
$exportQs = http_build_query(['bag_code' => $bagCode, 'keyword' => $keyword, 'export' => 1]);
|
||||||
|
?>
|
||||||
|
<div class="space-y-2">
|
||||||
|
<section class="border border-gray-300 rounded-lg bg-white p-3 no-print">
|
||||||
|
<div class="flex items-start justify-between gap-2 mb-2">
|
||||||
|
<div>
|
||||||
|
<h2 class="text-base font-bold text-gray-800">실사 재고 바코드 리스트</h2>
|
||||||
|
<p class="text-xs text-gray-500 mt-0.5">현재 창고에 남아있어야 하는(재고 상태) 봉투 번호를 뽑아 실사 시 실물과 대조하세요.</p>
|
||||||
|
</div>
|
||||||
|
<div class="flex items-center gap-2 shrink-0">
|
||||||
|
<button type="button" onclick="window.print()" class="border border-gray-300 text-gray-600 px-3 py-1.5 rounded-sm text-sm hover:bg-gray-50 transition">인쇄</button>
|
||||||
|
<a href="<?= esc(site_url('bag/inventory/stock-barcode-list') . '?' . $exportQs) ?>" class="border border-green-500 text-green-700 px-3 py-1.5 rounded-sm text-sm hover:bg-green-50 transition">엑셀저장</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<form method="get" action="<?= esc(site_url('bag/inventory/stock-barcode-list')) ?>" class="flex flex-wrap items-end gap-2 text-sm">
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">품목</label>
|
||||||
|
<select name="bag_code" class="border border-gray-300 rounded px-2 py-1 w-56">
|
||||||
|
<option value="">— 전체 품목 —</option>
|
||||||
|
<?php foreach ($itemOptions as $opt): ?>
|
||||||
|
<?php $oc = (string) ($opt['brpc_bag_code'] ?? ''); ?>
|
||||||
|
<option value="<?= esc($oc, 'attr') ?>" <?= $oc === $bagCode ? 'selected' : '' ?>>
|
||||||
|
<?= esc(($opt['bag_name'] ?? '') !== '' ? $opt['bag_name'] : $oc) ?>
|
||||||
|
(팩 <?= number_format((int) ($opt['pack_cnt'] ?? 0)) ?> · <?= number_format((int) ($opt['sheet_qty'] ?? 0)) ?>매)
|
||||||
|
</option>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<label class="block text-xs font-bold text-gray-600 mb-1">번호 검색</label>
|
||||||
|
<input type="text" name="keyword" value="<?= esc($keyword, 'attr') ?>" placeholder="박스/팩/시트번호/LOT" class="border border-gray-300 rounded px-2 py-1 w-56"/>
|
||||||
|
</div>
|
||||||
|
<button type="submit" class="bg-navy-700 text-white px-4 py-1.5 rounded-sm text-sm hover:opacity-90 transition" style="background:#1f2b4d;">조회</button>
|
||||||
|
<?php if ($bagCode !== '' || $keyword !== ''): ?>
|
||||||
|
<a href="<?= esc(site_url('bag/inventory/stock-barcode-list')) ?>" class="text-gray-500 hover:underline text-sm pb-1">초기화</a>
|
||||||
|
<?php endif; ?>
|
||||||
|
</form>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="border border-gray-300 rounded-lg bg-white p-3">
|
||||||
|
<div class="flex flex-wrap items-center gap-x-4 gap-y-1 text-sm mb-2 print-title">
|
||||||
|
<span class="font-bold text-gray-800">창고 재고 바코드</span>
|
||||||
|
<span class="text-gray-600">박스 <b class="text-gray-900"><?= number_format($totalBoxes) ?></b></span>
|
||||||
|
<span class="text-gray-600">팩 <b class="text-gray-900"><?= number_format($totalPacks) ?></b></span>
|
||||||
|
<span class="text-gray-600">총 매수 <b class="text-gray-900"><?= number_format($totalSheets) ?></b></span>
|
||||||
|
<span class="text-gray-400 text-xs ml-auto no-print"><?= esc(date('Y-m-d H:i')) ?> 기준</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<?php if ($displayTruncated): ?>
|
||||||
|
<div class="mb-2 text-xs text-amber-700 bg-amber-50 border border-amber-200 rounded px-2 py-1 no-print">
|
||||||
|
전체 <?= number_format($totalPacks) ?>건 중 화면에는 상위 <?= number_format($displayLimit) ?>건만 표시됩니다. 전체는 <b>엑셀저장</b>으로 받거나 <b>품목</b>을 선택해 좁혀 보세요.
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
|
<div class="overflow-x-auto">
|
||||||
|
<table class="w-full text-sm border-collapse">
|
||||||
|
<thead>
|
||||||
|
<tr class="bg-gray-100 text-gray-700">
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-center w-12">No</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">품목</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left w-24">LOT</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">박스코드</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">팩코드</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">시작번호</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-left">끝번호</th>
|
||||||
|
<th class="border border-gray-300 px-2 py-1 text-right w-20">매수</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<?php if ($rows === []): ?>
|
||||||
|
<tr><td colspan="8" class="border border-gray-300 px-2 py-6 text-center text-gray-400">재고로 남아있는 바코드가 없습니다.</td></tr>
|
||||||
|
<?php else: ?>
|
||||||
|
<?php foreach ($rows as $i => $r): ?>
|
||||||
|
<tr class="hover:bg-blue-50/40">
|
||||||
|
<td class="border border-gray-300 px-2 py-1 text-center text-gray-500"><?= $i + 1 ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1"><?= esc(($r['brpc_bag_name'] ?? '') !== '' ? $r['brpc_bag_name'] : ($r['brpc_bag_code'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1"><?= esc((string) ($r['brpc_lot_no'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 font-mono text-xs"><?= esc((string) ($r['brpc_box_code'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 font-mono text-xs"><?= esc((string) ($r['brpc_pack_code'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 font-mono text-xs"><?= esc((string) ($r['brpc_sheet_start_code'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 font-mono text-xs"><?= esc((string) ($r['brpc_sheet_end_code'] ?? '')) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 text-right"><?= number_format((int) ($r['brpc_sheet_qty'] ?? 0)) ?></td>
|
||||||
|
</tr>
|
||||||
|
<?php endforeach; ?>
|
||||||
|
<?php endif; ?>
|
||||||
|
</tbody>
|
||||||
|
<?php if ($rows !== []): ?>
|
||||||
|
<tfoot>
|
||||||
|
<tr class="bg-gray-50 font-bold text-gray-800">
|
||||||
|
<td class="border border-gray-300 px-2 py-1 text-center" colspan="3">합계</td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1">박스 <?= number_format($totalBoxes) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1" colspan="3">팩 <?= number_format($totalPacks) ?></td>
|
||||||
|
<td class="border border-gray-300 px-2 py-1 text-right"><?= number_format($totalSheets) ?></td>
|
||||||
|
</tr>
|
||||||
|
</tfoot>
|
||||||
|
<?php endif; ?>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
@media print {
|
||||||
|
.no-print { display: none !important; }
|
||||||
|
.print-title { margin-bottom: 6px; }
|
||||||
|
table { font-size: 11px; }
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Reference in New Issue
Block a user