P2-15: 지정판매소 다조건 조회 (이름/구군/상태 필터) P2-17: 지정판매소 지도 표시 (Kakao Maps) P2-18: 지정판매소 현황 (연도별 신규/취소 통계) P5-04: 년 판매 현황 (월별 피벗 테이블) P5-05: 지정판매소별 판매현황 (판매소별 수량/금액) P5-06: 홈택스 세금계산서 엑셀 내보내기 P5-08: 반품/파기 현황 (기간별 조회) P5-10: LOT 수불 조회 (LOT 번호 검색) P5-11: 기타 입출고 (등록 + 재고 연동) CT-05: CRUD 로깅 (activity_log 테이블 + audit_helper) CT-06: 대시보드 실 데이터 (발주/판매/재고/불출 통계) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
439 lines
19 KiB
PHP
439 lines
19 KiB
PHP
<?php
|
|
|
|
namespace App\Controllers\Admin;
|
|
|
|
use App\Controllers\BaseController;
|
|
use App\Models\BagSaleModel;
|
|
use App\Models\BagIssueModel;
|
|
use App\Models\BagReceivingModel;
|
|
use App\Models\BagInventoryModel;
|
|
|
|
class SalesReport extends BaseController
|
|
{
|
|
/**
|
|
* P5-01: 판매 대장 (일자별/기간별)
|
|
*/
|
|
public function salesLedger()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$mode = $this->request->getGet('mode') ?? 'daily'; // daily or period
|
|
|
|
$saleModel = model(BagSaleModel::class);
|
|
$db = \Config\Database::connect();
|
|
|
|
if ($mode === 'daily') {
|
|
$result = $db->query("
|
|
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
|
SUM(ABS(bs_qty)) as total_qty,
|
|
SUM(bs_amount) as total_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('sale','return')
|
|
GROUP BY bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type
|
|
ORDER BY bs_sale_date DESC, bs_ds_name, bs_bag_code
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
} else {
|
|
$result = $db->query("
|
|
SELECT bs_ds_name, bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
|
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
|
GROUP BY bs_ds_name, bs_bag_code, bs_bag_name
|
|
ORDER BY bs_ds_name, bs_bag_code
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
}
|
|
|
|
return view('admin/layout', [
|
|
'title' => '판매 대장',
|
|
'content' => view('admin/sales_report/sales_ledger', compact('result', 'startDate', 'endDate', 'mode')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-02: 일계표
|
|
*/
|
|
public function dailySummary()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$date = $this->request->getGet('date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
// 당일 판매
|
|
$daily = $db->query("
|
|
SELECT bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date = ?
|
|
GROUP BY bs_bag_code, bs_bag_name
|
|
ORDER BY bs_bag_code
|
|
", [$lgIdx, $date])->getResult();
|
|
|
|
// 당월 누계
|
|
$monthStart = date('Y-m-01', strtotime($date));
|
|
$monthly = $db->query("
|
|
SELECT bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
|
GROUP BY bs_bag_code, bs_bag_name
|
|
ORDER BY bs_bag_code
|
|
", [$lgIdx, $monthStart, $date])->getResult();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '일계표',
|
|
'content' => view('admin/sales_report/daily_summary', compact('daily', 'monthly', 'date')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-03: 기간별 판매현황
|
|
*/
|
|
public function periodSales()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
$result = $db->query("
|
|
SELECT bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
|
SUM(CASE WHEN bs_type='return' THEN bs_amount ELSE 0 END) as return_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
|
GROUP BY bs_bag_code, bs_bag_name
|
|
ORDER BY bs_bag_code
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '기간별 판매현황',
|
|
'content' => view('admin/sales_report/period_sales', compact('result', 'startDate', 'endDate')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-04: 년 판매 현황 (월별)
|
|
*/
|
|
public function yearlySales()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$year = $this->request->getGet('year') ?? date('Y');
|
|
$db = \Config\Database::connect();
|
|
|
|
$result = $db->query("
|
|
SELECT bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=1 THEN ABS(bs_qty) ELSE 0 END) as m01,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=2 THEN ABS(bs_qty) ELSE 0 END) as m02,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=3 THEN ABS(bs_qty) ELSE 0 END) as m03,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=4 THEN ABS(bs_qty) ELSE 0 END) as m04,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=5 THEN ABS(bs_qty) ELSE 0 END) as m05,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=6 THEN ABS(bs_qty) ELSE 0 END) as m06,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=7 THEN ABS(bs_qty) ELSE 0 END) as m07,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=8 THEN ABS(bs_qty) ELSE 0 END) as m08,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=9 THEN ABS(bs_qty) ELSE 0 END) as m09,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=10 THEN ABS(bs_qty) ELSE 0 END) as m10,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=11 THEN ABS(bs_qty) ELSE 0 END) as m11,
|
|
SUM(CASE WHEN MONTH(bs_sale_date)=12 THEN ABS(bs_qty) ELSE 0 END) as m12,
|
|
SUM(ABS(bs_qty)) as total
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND YEAR(bs_sale_date) = ? AND bs_type = 'sale'
|
|
GROUP BY bs_bag_code, bs_bag_name
|
|
ORDER BY bs_bag_code
|
|
", [$lgIdx, $year])->getResult();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '년 판매 현황',
|
|
'content' => view('admin/sales_report/yearly_sales', compact('result', 'year')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-05: 지정판매소별 판매현황
|
|
*/
|
|
public function shopSales()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
$result = $db->query("
|
|
SELECT bs_ds_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='sale' THEN bs_amount ELSE 0 END) as sale_amount,
|
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty,
|
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_amount) ELSE 0 END) as return_amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
|
GROUP BY bs_ds_name
|
|
ORDER BY bs_ds_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '지정판매소별 판매현황',
|
|
'content' => view('admin/sales_report/shop_sales', compact('result', 'startDate', 'endDate')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-06: 홈택스 세금계산서 엑셀 내보내기
|
|
*/
|
|
public function hometaxExport()
|
|
{
|
|
helper(['admin', 'export']);
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
|
|
$db = \Config\Database::connect();
|
|
$rows = $db->query("
|
|
SELECT bs.bs_sale_date, ds.ds_biz_no as buyer_biz_no, ds.ds_name as buyer_name,
|
|
bs.bs_bag_name, ABS(bs.bs_qty) as qty, bs.bs_unit_price, bs.bs_amount
|
|
FROM bag_sale bs
|
|
LEFT JOIN designated_shop ds ON bs.bs_ds_idx = ds.ds_idx
|
|
WHERE bs.bs_lg_idx = ? AND bs.bs_sale_date BETWEEN ? AND ? AND bs.bs_type = 'sale'
|
|
ORDER BY bs.bs_sale_date, ds.ds_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
// 지자체 정보 (공급자)
|
|
$lg = model(\App\Models\LocalGovernmentModel::class)->find($lgIdx);
|
|
$supplierBizNo = $lg->lg_biz_no ?? '';
|
|
$supplierName = $lg->lg_name ?? '';
|
|
|
|
$csvRows = [];
|
|
foreach ($rows as $row) {
|
|
$amount = (int) $row->bs_amount;
|
|
$tax = (int) round($amount * 0.1);
|
|
$csvRows[] = [
|
|
str_replace('-', '', $row->bs_sale_date), // 작성일자 (YYYYMMDD)
|
|
$supplierBizNo, // 공급자사업자번호
|
|
$supplierName, // 공급자상호
|
|
$row->buyer_biz_no ?? '', // 공급받는자사업자번호
|
|
$row->buyer_name ?? '', // 공급받는자상호
|
|
$row->bs_bag_name, // 품목
|
|
(int) $row->qty, // 수량
|
|
(int) $row->bs_unit_price, // 단가
|
|
$amount, // 공급가액
|
|
$tax, // 세액
|
|
];
|
|
}
|
|
|
|
export_csv(
|
|
'홈택스_세금계산서_' . date('Ymd') . '.csv',
|
|
['작성일자', '공급자사업자번호', '공급자상호', '공급받는자사업자번호', '공급받는자상호', '품목', '수량', '단가', '공급가액', '세액'],
|
|
$csvRows
|
|
);
|
|
}
|
|
|
|
/**
|
|
* P5-08: 반품/파기 현황
|
|
*/
|
|
public function returns()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
$result = $db->query("
|
|
SELECT bs_sale_date, bs_ds_name, bs_bag_code, bs_bag_name, bs_type,
|
|
ABS(bs_qty) as qty, ABS(bs_amount) as amount
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ? AND bs_type IN('return','cancel')
|
|
ORDER BY bs_sale_date DESC, bs_ds_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '반품/파기 현황',
|
|
'content' => view('admin/sales_report/returns', compact('result', 'startDate', 'endDate')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-10: LOT 수불 조회
|
|
*/
|
|
public function lotFlow()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$lotNo = $this->request->getGet('lot_no') ?? '';
|
|
$order = null;
|
|
$items = [];
|
|
$receivings = [];
|
|
|
|
if ($lotNo !== '') {
|
|
$db = \Config\Database::connect();
|
|
$order = $db->query("SELECT * FROM bag_order WHERE bo_lg_idx = ? AND bo_lot_no = ?", [$lgIdx, $lotNo])->getRow();
|
|
if ($order) {
|
|
$items = $db->query("SELECT * FROM bag_order_item WHERE boi_bo_idx = ? ORDER BY boi_bag_code", [(int) $order->bo_idx])->getResult();
|
|
$receivings = $db->query("SELECT * FROM bag_receiving WHERE br_bo_idx = ? ORDER BY br_receive_date", [(int) $order->bo_idx])->getResult();
|
|
}
|
|
}
|
|
|
|
return view('admin/layout', [
|
|
'title' => 'LOT 수불 조회',
|
|
'content' => view('admin/sales_report/lot_flow', compact('lotNo', 'order', 'items', 'receivings')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-11: 기타 입출고 목록
|
|
*/
|
|
public function miscFlow()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
// bag_misc_flow 테이블이 존재하는지 확인
|
|
$tableExists = $db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
|
|
$result = [];
|
|
if ($tableExists) {
|
|
$result = $db->query("
|
|
SELECT * FROM bag_misc_flow
|
|
WHERE bmf_lg_idx = ? AND bmf_date BETWEEN ? AND ?
|
|
ORDER BY bmf_date DESC, bmf_idx DESC
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
}
|
|
|
|
// 봉투 코드 목록
|
|
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
$bagCodes = $kindO ? model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true) : [];
|
|
|
|
return view('admin/layout', [
|
|
'title' => '기타 입출고',
|
|
'content' => view('admin/sales_report/misc_flow', compact('result', 'startDate', 'endDate', 'bagCodes', 'tableExists')),
|
|
]);
|
|
}
|
|
|
|
/**
|
|
* P5-11: 기타 입출고 등록 처리
|
|
*/
|
|
public function miscFlowStore()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin/reports/misc-flow'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$rules = [
|
|
'bmf_type' => 'required|in_list[in,out]',
|
|
'bmf_bag_code' => 'required|max_length[50]',
|
|
'bmf_qty' => 'required|is_natural_no_zero',
|
|
'bmf_date' => 'required|valid_date[Y-m-d]',
|
|
'bmf_reason' => 'required|max_length[200]',
|
|
];
|
|
if (! $this->validate($rules)) {
|
|
return redirect()->back()->withInput()->with('errors', $this->validator->getErrors());
|
|
}
|
|
|
|
$bagCode = $this->request->getPost('bmf_bag_code');
|
|
$qty = (int) $this->request->getPost('bmf_qty');
|
|
$type = $this->request->getPost('bmf_type');
|
|
|
|
// 봉투명 조회
|
|
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
|
$detail = $kindO ? model(\App\Models\CodeDetailModel::class)->where('cd_ck_idx', $kindO->ck_idx)->where('cd_code', $bagCode)->first() : null;
|
|
$bagName = $detail ? $detail->cd_name : '';
|
|
|
|
$db = \Config\Database::connect();
|
|
$db->transStart();
|
|
|
|
$db->query("
|
|
INSERT INTO bag_misc_flow (bmf_lg_idx, bmf_type, bmf_bag_code, bmf_bag_name, bmf_qty, bmf_date, bmf_reason, bmf_regdate)
|
|
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
|
|
", [$lgIdx, $type, $bagCode, $bagName, $qty, $this->request->getPost('bmf_date'), $this->request->getPost('bmf_reason'), date('Y-m-d H:i:s')]);
|
|
|
|
// 재고 조정
|
|
$delta = ($type === 'in') ? $qty : -$qty;
|
|
model(BagInventoryModel::class)->adjustQty($lgIdx, $bagCode, $bagName, $delta);
|
|
|
|
$db->transComplete();
|
|
|
|
return redirect()->to(site_url('admin/reports/misc-flow'))->with('success', '기타 입출고가 등록되었습니다.');
|
|
}
|
|
|
|
/**
|
|
* P5-07: 봉투 수불 현황
|
|
*/
|
|
public function supplyDemand()
|
|
{
|
|
helper('admin');
|
|
$lgIdx = admin_effective_lg_idx();
|
|
if (!$lgIdx) return redirect()->to(site_url('admin'))->with('error', '지자체를 선택해 주세요.');
|
|
|
|
$startDate = $this->request->getGet('start_date') ?? date('Y-m-01');
|
|
$endDate = $this->request->getGet('end_date') ?? date('Y-m-d');
|
|
$db = \Config\Database::connect();
|
|
|
|
// 입고 합계
|
|
$receiving = $db->query("
|
|
SELECT br_bag_code, br_bag_name,
|
|
SUM(br_qty_sheet) as recv_qty
|
|
FROM bag_receiving
|
|
WHERE br_lg_idx = ? AND br_receive_date BETWEEN ? AND ?
|
|
GROUP BY br_bag_code, br_bag_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
// 판매 합계
|
|
$sales = $db->query("
|
|
SELECT bs_bag_code, bs_bag_name,
|
|
SUM(CASE WHEN bs_type='sale' THEN ABS(bs_qty) ELSE 0 END) as sale_qty,
|
|
SUM(CASE WHEN bs_type='return' THEN ABS(bs_qty) ELSE 0 END) as return_qty
|
|
FROM bag_sale
|
|
WHERE bs_lg_idx = ? AND bs_sale_date BETWEEN ? AND ?
|
|
GROUP BY bs_bag_code, bs_bag_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
// 불출 합계
|
|
$issues = $db->query("
|
|
SELECT bi2_bag_code, bi2_bag_name,
|
|
SUM(bi2_qty) as issue_qty
|
|
FROM bag_issue
|
|
WHERE bi2_lg_idx = ? AND bi2_issue_date BETWEEN ? AND ? AND bi2_status = 'normal'
|
|
GROUP BY bi2_bag_code, bi2_bag_name
|
|
", [$lgIdx, $startDate, $endDate])->getResult();
|
|
|
|
// 현재 재고
|
|
$inventory = model(BagInventoryModel::class)->where('bi_lg_idx', $lgIdx)->findAll();
|
|
|
|
return view('admin/layout', [
|
|
'title' => '봉투 수불 현황',
|
|
'content' => view('admin/sales_report/supply_demand', compact('receiving', 'sales', 'issues', 'inventory', 'startDate', 'endDate')),
|
|
]);
|
|
}
|
|
}
|