feat: 발주입고(스캐너)·일괄입고 좌우 워크스페이스 재설계 + 실시간 스캔 입고
- 발주입고/일괄입고를 공용 워크스페이스로 통합: 좌측 발주현황(탭: 제작업체·발주기간 / 인수자·인계자, 정렬) · 우측 선택발주 상세 + 스캔/수량 입고 · 하단 입고 세부내역(박스코드) - 실시간 스캔 입고 엔드포인트 receivingScanBox 추가(스캔 1회=1박스, 수량 직접입력 병행) - buildReceivingCandidateRows에 카테고리(봉투/음식물·폐기물)·발주기간·대행소 필터 추가 - 일괄입고를 '일괄입고(음식물,폐기물)'로 개편, 음식물·폐기물만 표시(메뉴명 변경 SQL 포함) Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4704,12 +4704,20 @@ SQL);
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주 입고(스캐너 대체 수동입력)
|
||||
* - 미입고가 남은 발주의 LOT·봉투(이름)로 조회 범위를 좁힌 뒤 입고 처리
|
||||
* - 인수자: 대행소(agency) 담당자, 기본값 동명이면 로그인 사용자명과 일치하는 담당자
|
||||
* - 인계자: 제작업체(company) 담당자
|
||||
* 발주 입고(스캐너) — 좌우 분할 워크스페이스로 렌더.
|
||||
*/
|
||||
public function receivingScanner(): string|RedirectResponse
|
||||
{
|
||||
return $this->renderReceivingWorkspace('scanner');
|
||||
}
|
||||
|
||||
/**
|
||||
* 발주입고(스캐너) / 일괄입고 공용 워크스페이스.
|
||||
* 좌측: 발주 현황(탭: 제작업체·발주기간 / 인수자·인계자, 정렬) · 우측: 선택 발주 상세 + 스캔/수량 입고 + 입고 세부내역.
|
||||
*
|
||||
* @param string $mode 'scanner'(종량제 봉투) | 'batch'(음식물·폐기물)
|
||||
*/
|
||||
private function renderReceivingWorkspace(string $mode): string|RedirectResponse
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
@@ -4717,75 +4725,206 @@ SQL);
|
||||
return redirect()->to(site_url('bag/purchase-inbound'))->with('error', '지자체를 선택해 주세요.');
|
||||
}
|
||||
|
||||
$companyIdx = (int) old('company_idx', (int) ($this->request->getGet('company_idx') ?? 0));
|
||||
$lotNo = '';
|
||||
$bagCode = '';
|
||||
$isBatch = ($mode === 'batch');
|
||||
$category = $isBatch ? 'bulk' : 'bag';
|
||||
$title = $isBatch ? '일괄입고(음식물,폐기물)' : '발주 입고(스캐너)';
|
||||
|
||||
$tab = (string) ($this->request->getGet('tab') ?? 'company');
|
||||
if (! in_array($tab, ['company', 'agency'], true)) {
|
||||
$tab = 'company';
|
||||
}
|
||||
|
||||
$companyIdx = (int) ($this->request->getGet('company_idx') ?? 0);
|
||||
$agencyIdx = (int) ($this->request->getGet('agency_idx') ?? 0);
|
||||
$startMonth = (string) ($this->request->getGet('start_month') ?? '');
|
||||
$endMonth = (string) ($this->request->getGet('end_month') ?? '');
|
||||
$startDate = preg_match('/^\d{4}-\d{2}$/', $startMonth) ? $startMonth . '-01' : '';
|
||||
$endDate = preg_match('/^\d{4}-\d{2}$/', $endMonth) ? date('Y-m-t', strtotime($endMonth . '-01 00:00:00')) : '';
|
||||
|
||||
$companies = model(CompanyModel::class)
|
||||
->where('cp_lg_idx', $lgIdx)
|
||||
->where('cp_type', '제작업체')
|
||||
->where('cp_state', 1)
|
||||
->orderBy('cp_name', 'ASC')
|
||||
->findAll();
|
||||
->where('cp_lg_idx', $lgIdx)->where('cp_type', '제작업체')->where('cp_state', 1)
|
||||
->orderBy('cp_name', 'ASC')->findAll();
|
||||
$agencies = model(SalesAgencyModel::class)
|
||||
->where('sa_lg_idx', $lgIdx)->orderForDisplay()->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, '')
|
||||
: [];
|
||||
// 조회 기준: company 탭 = 제작업체+발주기간 / agency 탭 = 인수자(대행소)+인계자(제작업체)
|
||||
$rows = $this->buildReceivingCandidateRows(
|
||||
$lgIdx,
|
||||
$companyIdx,
|
||||
'',
|
||||
true,
|
||||
'',
|
||||
$tab === 'agency' ? $agencyIdx : 0,
|
||||
$category,
|
||||
$tab === 'company' ? $startDate : '',
|
||||
$tab === 'company' ? $endDate : ''
|
||||
);
|
||||
$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,
|
||||
]
|
||||
$pick = $this->receivingManagerPickers($lgIdx);
|
||||
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
||||
|
||||
// 발주기간 드롭다운 옵션(최근 5년치 월)
|
||||
$monthOptions = [];
|
||||
$baseYear = (int) date('Y');
|
||||
for ($y = $baseYear; $y >= $baseYear - 4; $y--) {
|
||||
for ($m = 12; $m >= 1; $m--) {
|
||||
$v = sprintf('%04d-%02d', $y, $m);
|
||||
$monthOptions[] = ['value' => $v, 'label' => $y . '년 ' . $m . '월'];
|
||||
}
|
||||
}
|
||||
|
||||
return $this->render($title, 'bag/receiving_scanner', [
|
||||
'mode' => $mode,
|
||||
'isBatch' => $isBatch,
|
||||
'workspaceTitle' => $title,
|
||||
'tab' => $tab,
|
||||
'companies' => $companies,
|
||||
'agencies' => $agencies,
|
||||
'companyIdx' => $companyIdx,
|
||||
'agencyIdx' => $agencyIdx,
|
||||
'startMonth' => $startMonth,
|
||||
'endMonth' => $endMonth,
|
||||
'monthOptions' => $monthOptions,
|
||||
'receiverOptions' => $recvSel['receiverOptions'],
|
||||
'receiverRef' => $recvSel['defaultReceiverRef'],
|
||||
'senders' => $pick['senders'],
|
||||
'senderIdx' => $pick['defaultSenderIdx'],
|
||||
'rows' => $rows,
|
||||
'rowsByKey' => $rowsByKey,
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 실시간 입고 처리(스캔 1회 = 1박스, 또는 수량 직접입력). AJAX 전용.
|
||||
* 선택 발주행에 대해 입고 레코드 생성 → 재고 반영 → 박스코드(brpc) 생성 후 반환.
|
||||
*/
|
||||
public function receivingScanBox(): ResponseInterface
|
||||
{
|
||||
helper('admin');
|
||||
$lgIdx = $this->lgIdx();
|
||||
if (! $lgIdx) {
|
||||
return $this->response->setJSON(['ok' => false, 'message' => '지자체를 선택해 주세요.']);
|
||||
}
|
||||
|
||||
$rowKey = (string) ($this->request->getPost('row_key') ?? '');
|
||||
$receiverRef = (string) ($this->request->getPost('br_receiver_ref') ?? '');
|
||||
$senderIdx = (int) ($this->request->getPost('br_sender_idx') ?? 0);
|
||||
$receiveDate = (string) ($this->request->getPost('br_receive_date') ?? date('Y-m-d'));
|
||||
$barcode = trim((string) ($this->request->getPost('barcode') ?? ''));
|
||||
$mode = (string) ($this->request->getPost('mode') ?? 'scanner');
|
||||
$qtySheetIn = (int) ($this->request->getPost('qty_sheet') ?? 0);
|
||||
$category = ($mode === 'batch') ? 'bulk' : 'bag';
|
||||
|
||||
$refresh = ['csrf' => csrf_hash()];
|
||||
|
||||
if (! preg_match('/^\d{4}-\d{2}-\d{2}$/', $receiveDate)) {
|
||||
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '입고일 형식을 확인해 주세요.']));
|
||||
}
|
||||
$recvSel = $this->receivingReceiverSelect($lgIdx);
|
||||
$receiverRef = $this->sanitizeReceiverRef($recvSel['receiverOptions'], $receiverRef);
|
||||
if ($receiverRef === '') {
|
||||
$receiverRef = $recvSel['defaultReceiverRef'];
|
||||
}
|
||||
$receiverIdx = $this->parseReceiverRefToStoredIdx($lgIdx, $receiverRef);
|
||||
if ($receiverIdx <= 0) {
|
||||
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '인수자를 선택해 주세요.']));
|
||||
}
|
||||
|
||||
$rows = $this->buildReceivingCandidateRows($lgIdx, 0, '', true, '', 0, $category, '', '');
|
||||
$map = [];
|
||||
foreach ($rows as $r) {
|
||||
$map[(string) $r['row_key']] = $r;
|
||||
}
|
||||
if (! isset($map[$rowKey])) {
|
||||
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '선택한 발주를 찾을 수 없거나 이미 전량 입고되었습니다.']));
|
||||
}
|
||||
$base = $map[$rowKey];
|
||||
$pending = (int) $base['pending_qty_sheet'];
|
||||
if ($pending <= 0) {
|
||||
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '미입고 잔량이 없습니다.']));
|
||||
}
|
||||
|
||||
$totalPerBox = max(1, (int) $base['total_per_box']);
|
||||
$qty = $qtySheetIn > 0 ? $qtySheetIn : $totalPerBox; // 스캔 1회 = 1박스
|
||||
if ($qty > $pending) {
|
||||
$qty = $pending;
|
||||
}
|
||||
$qtyBox = intdiv($qty, $totalPerBox);
|
||||
$senderName = $this->resolveCompanySenderName($lgIdx, $senderIdx);
|
||||
$sender = $senderName !== '' ? $senderName : (string) ($base['company_rep_name'] ?? '');
|
||||
|
||||
$recvModel = model(BagReceivingModel::class);
|
||||
$invModel = model(BagInventoryModel::class);
|
||||
$db = \Config\Database::connect();
|
||||
$db->transStart();
|
||||
$recvModel->insert([
|
||||
'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'),
|
||||
]);
|
||||
$brIdx = (int) $recvModel->getInsertID();
|
||||
$invModel->adjustQty($lgIdx, (string) $base['bag_code'], (string) $base['bag_name'], $qty);
|
||||
$this->createReceivingPackCodes(
|
||||
$lgIdx,
|
||||
$brIdx,
|
||||
(int) $base['bo_idx'],
|
||||
(string) $base['bag_code'],
|
||||
(string) $base['bag_name'],
|
||||
$qty,
|
||||
max(1, (int) $base['pack_per_sheet']),
|
||||
$totalPerBox,
|
||||
(string) ($base['lot_no'] ?? '')
|
||||
);
|
||||
$db->transComplete();
|
||||
if (! $db->transStatus()) {
|
||||
return $this->response->setJSON(array_merge($refresh, ['ok' => false, 'message' => '입고 처리 중 오류가 발생했습니다.']));
|
||||
}
|
||||
|
||||
$boxRows = $db->table('bag_receiving_pack_code')
|
||||
->select('brpc_box_code, COUNT(*) AS pack_cnt, SUM(brpc_sheet_qty) AS sheet_sum')
|
||||
->where('brpc_br_idx', $brIdx)
|
||||
->groupBy('brpc_box_code')
|
||||
->orderBy('brpc_box_code', 'ASC')
|
||||
->get()->getResultArray();
|
||||
$boxes = array_map(static fn ($b): array => [
|
||||
'box_code' => (string) ($b['brpc_box_code'] ?? ''),
|
||||
'packs' => (int) ($b['pack_cnt'] ?? 0),
|
||||
'sheet' => (int) ($b['sheet_sum'] ?? 0),
|
||||
], $boxRows);
|
||||
|
||||
// 갱신된 미입고/입고량 재계산
|
||||
$after = $this->buildReceivingCandidateRows($lgIdx, 0, '', false, '', 0, $category, '', '');
|
||||
$updated = null;
|
||||
foreach ($after as $r) {
|
||||
if ((string) $r['row_key'] === $rowKey) {
|
||||
$updated = $r;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
return $this->response->setJSON(array_merge($refresh, [
|
||||
'ok' => true,
|
||||
'row_key' => $rowKey,
|
||||
'bag_name' => (string) $base['bag_name'],
|
||||
'qty_sheet' => $qty,
|
||||
'qty_box' => $qtyBox,
|
||||
'received_qty_sheet' => (int) ($updated['received_qty_sheet'] ?? ((int) $base['received_qty_sheet'] + $qty)),
|
||||
'pending_qty_sheet' => (int) ($updated['pending_qty_sheet'] ?? max(0, $pending - $qty)),
|
||||
'boxes' => $boxes,
|
||||
'scanned' => $barcode,
|
||||
]));
|
||||
}
|
||||
|
||||
public function receivingScannerStore(): RedirectResponse
|
||||
@@ -4906,51 +5045,12 @@ SQL);
|
||||
/**
|
||||
* 일괄 입고: 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,
|
||||
]
|
||||
);
|
||||
return $this->renderReceivingWorkspace('batch');
|
||||
}
|
||||
|
||||
public function receivingBatchStore(): RedirectResponse
|
||||
@@ -5266,7 +5366,28 @@ SQL);
|
||||
*
|
||||
* @param string $lotNo 빈 문자열이면 LOT 제한 없음. 지정 시 해당 LOT(최신 헤드) 발주만.
|
||||
*/
|
||||
private function buildReceivingCandidateRows(int $lgIdx, int $companyIdx = 0, string $bagCode = '', bool $onlyPending = true, string $lotNo = ''): array
|
||||
/**
|
||||
* 봉투 이름 기준 품목 분류 (판매대장 통계와 동일 규칙).
|
||||
* 반환: '음식물' | '폐기물' | '봉투'
|
||||
*/
|
||||
private function receivingBagCategory(string $bagName): string
|
||||
{
|
||||
if (mb_strpos($bagName, '음식물') !== false) {
|
||||
return '음식물';
|
||||
}
|
||||
if (mb_strpos($bagName, '폐기물') !== false || mb_strpos($bagName, '대형') !== false) {
|
||||
return '폐기물';
|
||||
}
|
||||
|
||||
return '봉투';
|
||||
}
|
||||
|
||||
/**
|
||||
* @param string $category '' = 전체, 'bulk' = 음식물+폐기물(일괄입고용), 'bag' = 봉투(종량제)만
|
||||
* @param string $startDate 'YYYY-MM-DD' (발주일 하한, '' = 무시)
|
||||
* @param string $endDate 'YYYY-MM-DD' (발주일 상한, '' = 무시)
|
||||
*/
|
||||
private function buildReceivingCandidateRows(int $lgIdx, int $companyIdx = 0, string $bagCode = '', bool $onlyPending = true, string $lotNo = '', int $agencyIdx = 0, string $category = '', string $startDate = '', string $endDate = ''): array
|
||||
{
|
||||
$orderBuilder = model(BagOrderModel::class)
|
||||
->where('bo_lg_idx', $lgIdx)
|
||||
@@ -5280,6 +5401,15 @@ SQL);
|
||||
if ($companyIdx > 0) {
|
||||
$orderBuilder->where('bo_company_idx', $companyIdx);
|
||||
}
|
||||
if ($agencyIdx > 0) {
|
||||
$orderBuilder->where('bo_agency_idx', $agencyIdx);
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $startDate)) {
|
||||
$orderBuilder->where('bo_order_date >=', $startDate);
|
||||
}
|
||||
if (preg_match('/^\d{4}-\d{2}-\d{2}$/', $endDate)) {
|
||||
$orderBuilder->where('bo_order_date <=', $endDate);
|
||||
}
|
||||
$orders = $orderBuilder->findAll();
|
||||
if (empty($orders)) {
|
||||
return [];
|
||||
@@ -5348,6 +5478,16 @@ SQL);
|
||||
|
||||
$unit = $unitMap[$itemBagCode] ?? ['pack_per_sheet' => 1, 'total_per_box' => 1];
|
||||
$companyInfo = $companyMap[(int) ($order->bo_company_idx ?? 0)] ?? ['name' => '', 'rep' => ''];
|
||||
$bagName = (string) ($item->boi_bag_name ?? '');
|
||||
$bagCategory = $this->receivingBagCategory($bagName);
|
||||
|
||||
// 카테고리 필터 (일괄입고: 음식물+폐기물만 / bag: 봉투만)
|
||||
if ($category === 'bulk' && $bagCategory === '봉투') {
|
||||
continue;
|
||||
}
|
||||
if ($category === 'bag' && $bagCategory !== '봉투') {
|
||||
continue;
|
||||
}
|
||||
|
||||
$rows[] = [
|
||||
'row_key' => $boIdx . '|' . $itemBagCode,
|
||||
@@ -5359,7 +5499,8 @@ SQL);
|
||||
'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 ?? ''),
|
||||
'bag_name' => $bagName,
|
||||
'bag_category' => $bagCategory,
|
||||
'order_qty_sheet' => $orderQtySheet,
|
||||
'received_qty_sheet' => $receivedQtySheet,
|
||||
'pending_qty_sheet' => $pendingQtySheet,
|
||||
|
||||
Reference in New Issue
Block a user