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