사이트·관리자 봉투 물류 기능(수불·통계·레포트·재고·발주)과 DB·메뉴·E2E를 운영 반영한다.
통계 분석(전년대비·월별·계절별), 수급계획·LOT 수불, 지정판매소·실사·메뉴 링크 등을 포함한다. Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
463
app/Libraries/BagFlowReportBuilder.php
Normal file
463
app/Libraries/BagFlowReportBuilder.php
Normal file
@@ -0,0 +1,463 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
namespace App\Libraries;
|
||||
|
||||
/**
|
||||
* 기간별 봉투 수불 현황 집계 (bag/flow)
|
||||
*/
|
||||
class BagFlowReportBuilder
|
||||
{
|
||||
private \CodeIgniter\Database\BaseConnection $db;
|
||||
|
||||
public function __construct(?\CodeIgniter\Database\BaseConnection $db = null)
|
||||
{
|
||||
$this->db = $db ?? \Config\Database::connect();
|
||||
}
|
||||
|
||||
private static function bagCodeKey(mixed $code): string
|
||||
{
|
||||
return (string) $code;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array{
|
||||
* rows: list<array<string, mixed>>,
|
||||
* bagKindLabels: array<string, string>,
|
||||
* queried: bool
|
||||
* }
|
||||
*/
|
||||
public function build(
|
||||
int $lgIdx,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
string $aggMode,
|
||||
string $bagCodeFilter,
|
||||
string $bagKindFilter,
|
||||
int $saIdx,
|
||||
bool $queried
|
||||
): array {
|
||||
$bagKindLabels = $this->loadBagKindLabels();
|
||||
$products = $this->loadProducts($lgIdx, $bagCodeFilter, $bagKindFilter);
|
||||
if ($products === [] || ! $queried) {
|
||||
return ['rows' => [], 'bagKindLabels' => $bagKindLabels, 'queried' => $queried];
|
||||
}
|
||||
|
||||
$codes = array_keys($products);
|
||||
$dayBefore = date('Y-m-d', strtotime($startDate . ' -1 day'));
|
||||
$openingRaw = $this->aggregateMovements($lgIdx, $codes, $saIdx, null, $dayBefore);
|
||||
$opening = $this->collapseOpeningBalances($openingRaw);
|
||||
$periodMoves = $this->aggregateMovements($lgIdx, $codes, $saIdx, $startDate, $endDate);
|
||||
|
||||
if ($aggMode === 'daily') {
|
||||
$rows = $this->buildDailyRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
|
||||
} else {
|
||||
$rows = $this->buildPeriodRows($products, $opening, $periodMoves, $startDate, $endDate, $bagKindLabels);
|
||||
}
|
||||
|
||||
return ['rows' => $rows, 'bagKindLabels' => $bagKindLabels, 'queried' => true];
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string> code => name
|
||||
*/
|
||||
private function loadProducts(int $lgIdx, string $bagCodeFilter, string $bagKindFilter): array
|
||||
{
|
||||
$kindO = model(\App\Models\CodeKindModel::class)->where('ck_code', 'O')->first();
|
||||
if (! $kindO) {
|
||||
return [];
|
||||
}
|
||||
$details = model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindO->ck_idx, true, $lgIdx);
|
||||
$products = [];
|
||||
foreach ($details as $d) {
|
||||
$code = (string) ($d->cd_code ?? '');
|
||||
if ($code === '') {
|
||||
continue;
|
||||
}
|
||||
if ($bagCodeFilter !== '' && $code !== $bagCodeFilter) {
|
||||
continue;
|
||||
}
|
||||
if ($bagKindFilter !== '' && ! str_starts_with($code, $bagKindFilter)) {
|
||||
continue;
|
||||
}
|
||||
$products[self::bagCodeKey($code)] = (string) ($d->cd_name ?? $code);
|
||||
}
|
||||
|
||||
return $products;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, string>
|
||||
*/
|
||||
private function loadBagKindLabels(): array
|
||||
{
|
||||
$kindE = model(\App\Models\CodeKindModel::class)->where('ck_code', 'E')->first();
|
||||
if (! $kindE) {
|
||||
return [];
|
||||
}
|
||||
$labels = [];
|
||||
foreach (model(\App\Models\CodeDetailModel::class)->getByKind((int) $kindE->ck_idx, true, null) as $d) {
|
||||
$labels[(string) $d->cd_code] = (string) $d->cd_name;
|
||||
}
|
||||
|
||||
return $labels;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $codes
|
||||
* @return array<string, array<string, array<string, int>>> bag_code => date => metrics
|
||||
*/
|
||||
private function aggregateMovements(
|
||||
int $lgIdx,
|
||||
array $codes,
|
||||
int $saIdx,
|
||||
?string $fromDate,
|
||||
?string $toDate
|
||||
): array {
|
||||
if ($codes === []) {
|
||||
return [];
|
||||
}
|
||||
|
||||
$buckets = [];
|
||||
$ensure = static function (string $code, string $date) use (&$buckets): array {
|
||||
if (! isset($buckets[$code][$date])) {
|
||||
$buckets[$code][$date] = self::emptyMetrics();
|
||||
}
|
||||
|
||||
return $buckets[$code][$date];
|
||||
};
|
||||
|
||||
$hasMisc = $this->db->query("SHOW TABLES LIKE 'bag_misc_flow'")->getNumRows() > 0;
|
||||
$hasDsSa = $this->db->fieldExists('ds_sa_idx', 'designated_shop');
|
||||
$codePlaceholders = implode(',', array_fill(0, count($codes), '?'));
|
||||
|
||||
// 입고(발주 입고)
|
||||
$sql = "
|
||||
SELECT br_bag_code AS bag_code, br_receive_date AS mv_date, SUM(br_qty_sheet) AS qty
|
||||
FROM bag_receiving
|
||||
WHERE br_lg_idx = ? AND br_bag_code IN ({$codePlaceholders})
|
||||
";
|
||||
$params = array_merge([$lgIdx], $codes);
|
||||
if ($fromDate !== null) {
|
||||
$sql .= ' AND br_receive_date >= ?';
|
||||
$params[] = $fromDate;
|
||||
}
|
||||
if ($toDate !== null) {
|
||||
$sql .= ' AND br_receive_date <= ?';
|
||||
$params[] = $toDate;
|
||||
}
|
||||
$sql .= ' GROUP BY br_bag_code, br_receive_date';
|
||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
||||
$code = (string) $row->bag_code;
|
||||
$date = (string) $row->mv_date;
|
||||
$m = $ensure($code, $date);
|
||||
$m['recv_in'] += (int) $row->qty;
|
||||
$buckets[$code][$date] = $m;
|
||||
}
|
||||
|
||||
// 판매·반품(반품=입고)
|
||||
$sql = "
|
||||
SELECT bs.bs_bag_code AS bag_code, bs.bs_sale_date AS mv_date, bs.bs_type AS mv_type,
|
||||
SUM(ABS(bs.bs_qty)) AS qty
|
||||
FROM bag_sale bs
|
||||
";
|
||||
if ($saIdx > 0 && $hasDsSa) {
|
||||
$sql .= ' INNER JOIN designated_shop ds ON ds.ds_idx = bs.bs_ds_idx AND ds.ds_sa_idx = ?';
|
||||
}
|
||||
$sql .= " WHERE bs.bs_lg_idx = ? AND bs.bs_bag_code IN ({$codePlaceholders})";
|
||||
$params = $saIdx > 0 && $hasDsSa ? [$saIdx, $lgIdx] : [$lgIdx];
|
||||
$params = array_merge($params, $codes);
|
||||
if ($fromDate !== null) {
|
||||
$sql .= ' AND bs.bs_sale_date >= ?';
|
||||
$params[] = $fromDate;
|
||||
}
|
||||
if ($toDate !== null) {
|
||||
$sql .= ' AND bs.bs_sale_date <= ?';
|
||||
$params[] = $toDate;
|
||||
}
|
||||
$sql .= ' GROUP BY bs.bs_bag_code, bs.bs_sale_date, bs.bs_type';
|
||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
||||
$code = (string) $row->bag_code;
|
||||
$date = (string) $row->mv_date;
|
||||
$qty = (int) $row->qty;
|
||||
$m = $ensure($code, $date);
|
||||
$type = (string) $row->mv_type;
|
||||
if ($type === 'return') {
|
||||
$m['recv_return'] += $qty;
|
||||
} else {
|
||||
$m['out_sale'] += $qty;
|
||||
}
|
||||
$buckets[$code][$date] = $m;
|
||||
}
|
||||
|
||||
// 불출
|
||||
$sql = "
|
||||
SELECT bi2_bag_code AS bag_code, bi2_issue_date AS mv_date, bi2_issue_type AS issue_type,
|
||||
SUM(bi2_qty) AS qty
|
||||
FROM bag_issue
|
||||
WHERE bi2_lg_idx = ? AND bi2_status = 'normal' AND bi2_bag_code IN ({$codePlaceholders})
|
||||
";
|
||||
$params = array_merge([$lgIdx], $codes);
|
||||
if ($fromDate !== null) {
|
||||
$sql .= ' AND bi2_issue_date >= ?';
|
||||
$params[] = $fromDate;
|
||||
}
|
||||
if ($toDate !== null) {
|
||||
$sql .= ' AND bi2_issue_date <= ?';
|
||||
$params[] = $toDate;
|
||||
}
|
||||
$sql .= ' GROUP BY bi2_bag_code, bi2_issue_date, bi2_issue_type';
|
||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
||||
$code = (string) $row->bag_code;
|
||||
$date = (string) $row->mv_date;
|
||||
$qty = (int) $row->qty;
|
||||
$m = $ensure($code, $date);
|
||||
$issueType = (string) $row->issue_type;
|
||||
if (str_contains($issueType, '무료')) {
|
||||
$m['out_issue_free'] += $qty;
|
||||
} else {
|
||||
$m['out_issue_gen'] += $qty;
|
||||
}
|
||||
$buckets[$code][$date] = $m;
|
||||
}
|
||||
|
||||
if ($hasMisc) {
|
||||
$sql = "
|
||||
SELECT bmf_bag_code AS bag_code, bmf_date AS mv_date, bmf_type AS mv_type,
|
||||
SUM(bmf_qty) AS qty
|
||||
FROM bag_misc_flow
|
||||
WHERE bmf_lg_idx = ? AND bmf_bag_code IN ({$codePlaceholders})
|
||||
";
|
||||
$params = array_merge([$lgIdx], $codes);
|
||||
if ($fromDate !== null) {
|
||||
$sql .= ' AND bmf_date >= ?';
|
||||
$params[] = $fromDate;
|
||||
}
|
||||
if ($toDate !== null) {
|
||||
$sql .= ' AND bmf_date <= ?';
|
||||
$params[] = $toDate;
|
||||
}
|
||||
$sql .= ' GROUP BY bmf_bag_code, bmf_date, bmf_type';
|
||||
foreach ($this->db->query($sql, $params)->getResult() as $row) {
|
||||
$code = (string) $row->bag_code;
|
||||
$date = (string) $row->mv_date;
|
||||
$qty = (int) $row->qty;
|
||||
$m = $ensure($code, $date);
|
||||
if ((string) $row->mv_type === 'in') {
|
||||
$m['recv_misc'] += $qty;
|
||||
} else {
|
||||
$m['out_misc'] += $qty;
|
||||
}
|
||||
$buckets[$code][$date] = $m;
|
||||
}
|
||||
}
|
||||
|
||||
$normalized = [];
|
||||
foreach ($buckets as $code => $byDate) {
|
||||
$key = self::bagCodeKey($code);
|
||||
foreach ($byDate as $date => $m) {
|
||||
$normalized[$key][$date] = self::finalizeMetrics($m);
|
||||
}
|
||||
}
|
||||
|
||||
return $normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $products
|
||||
* @param array<string, array<string, int>> $opening date key '_open'
|
||||
* @param array<string, array<string, array<string, int>>> $periodMoves
|
||||
* @param array<string, string> $bagKindLabels
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function buildPeriodRows(
|
||||
array $products,
|
||||
array $opening,
|
||||
array $periodMoves,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
array $bagKindLabels
|
||||
): array {
|
||||
$periodKey = $startDate . '~' . $endDate;
|
||||
$grouped = [];
|
||||
foreach ($products as $codeKey => $name) {
|
||||
$code = self::bagCodeKey($codeKey);
|
||||
$kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
|
||||
$grouped[$kind][] = ['code' => $code, 'name' => $name];
|
||||
}
|
||||
ksort($grouped);
|
||||
|
||||
$rows = [];
|
||||
$grand = self::emptyMetrics();
|
||||
$grand['row_type'] = 'grand';
|
||||
$grand['date'] = '';
|
||||
$grand['item_name'] = '총계';
|
||||
|
||||
foreach ($grouped as $kind => $items) {
|
||||
$sub = self::emptyMetrics();
|
||||
$sub['row_type'] = 'subtotal';
|
||||
$sub['date'] = '';
|
||||
$sub['item_name'] = ($bagKindLabels[$kind] ?? '기타') . ' 소계';
|
||||
|
||||
foreach ($items as $item) {
|
||||
$code = self::bagCodeKey($item['code']);
|
||||
$m = self::emptyMetrics();
|
||||
foreach ($periodMoves[$code] ?? [] as $dayMetrics) {
|
||||
$m = self::mergeMetrics($m, $dayMetrics);
|
||||
}
|
||||
$m = self::finalizeMetrics($m);
|
||||
$m['prev_stock'] = (int) ($opening[$code] ?? 0);
|
||||
$m['balance'] = $m['prev_stock'] + $m['recv_total'] - $m['out_total'];
|
||||
$m['row_type'] = 'data';
|
||||
$m['date'] = $periodKey;
|
||||
$m['item_name'] = $item['name'];
|
||||
$m['bag_code'] = $code;
|
||||
$m['bag_kind'] = $kind;
|
||||
$rows[] = $m;
|
||||
$sub = self::mergeMetrics($sub, $m);
|
||||
}
|
||||
|
||||
$sub = self::finalizeMetrics($sub);
|
||||
$sub['balance'] = $sub['prev_stock'] + $sub['recv_total'] - $sub['out_total'];
|
||||
$rows[] = $sub;
|
||||
$grand = self::mergeMetrics($grand, $sub);
|
||||
}
|
||||
|
||||
$grand = self::finalizeMetrics($grand);
|
||||
$grand['balance'] = $grand['prev_stock'] + $grand['recv_total'] - $grand['out_total'];
|
||||
$rows[] = $grand;
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, array<string, array<string, int>>> $openingRaw
|
||||
* @return array<string, int> bag_code => 전일(기간 전) 재고
|
||||
*/
|
||||
private function collapseOpeningBalances(array $openingRaw): array
|
||||
{
|
||||
$out = [];
|
||||
foreach ($openingRaw as $code => $byDate) {
|
||||
$net = self::emptyMetrics();
|
||||
foreach ($byDate as $m) {
|
||||
$net = self::mergeMetrics($net, $m);
|
||||
}
|
||||
$net = self::finalizeMetrics($net);
|
||||
$out[self::bagCodeKey($code)] = $net['recv_total'] - $net['out_total'];
|
||||
}
|
||||
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, string> $products
|
||||
* @param array<string, array<string, array<string, int>>> $opening
|
||||
* @param array<string, array<string, array<string, int>>> $periodMoves
|
||||
* @param array<string, string> $bagKindLabels
|
||||
* @return list<array<string, mixed>>
|
||||
*/
|
||||
private function buildDailyRows(
|
||||
array $products,
|
||||
array $opening,
|
||||
array $periodMoves,
|
||||
string $startDate,
|
||||
string $endDate,
|
||||
array $bagKindLabels
|
||||
): array {
|
||||
$dates = [];
|
||||
$cursor = strtotime($startDate);
|
||||
$endTs = strtotime($endDate);
|
||||
while ($cursor <= $endTs) {
|
||||
$dates[] = date('Y-m-d', $cursor);
|
||||
$cursor = strtotime('+1 day', $cursor);
|
||||
}
|
||||
|
||||
$rows = [];
|
||||
foreach ($products as $codeKey => $name) {
|
||||
$code = self::bagCodeKey($codeKey);
|
||||
$kind = strlen($code) >= 2 ? substr($code, 0, 2) : '';
|
||||
$running = (int) ($opening[$code] ?? 0);
|
||||
foreach ($dates as $date) {
|
||||
$dayM = $periodMoves[$code][$date] ?? self::emptyMetrics();
|
||||
$dayM = self::finalizeMetrics($dayM);
|
||||
$prev = $running;
|
||||
$running = $prev + $dayM['recv_total'] - $dayM['out_total'];
|
||||
$dayM['prev_stock'] = $prev;
|
||||
$dayM['balance'] = $running;
|
||||
$dayM['row_type'] = 'data';
|
||||
$dayM['date'] = $date;
|
||||
$dayM['item_name'] = $name;
|
||||
$dayM['bag_code'] = $code;
|
||||
$dayM['bag_kind'] = $kind;
|
||||
if ($this->rowHasActivity($dayM)) {
|
||||
$rows[] = $dayM;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $rows;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int|float> $m
|
||||
*/
|
||||
private function rowHasActivity(array $m): bool
|
||||
{
|
||||
foreach (['recv_in', 'recv_return', 'recv_misc', 'out_sale', 'out_issue_gen', 'out_issue_free', 'out_return', 'out_misc'] as $k) {
|
||||
if ((int) ($m[$k] ?? 0) !== 0) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return (int) ($m['prev_stock'] ?? 0) !== 0;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private static function emptyMetrics(): array
|
||||
{
|
||||
return [
|
||||
'prev_stock' => 0,
|
||||
'recv_in' => 0,
|
||||
'recv_return' => 0,
|
||||
'recv_misc' => 0,
|
||||
'recv_total' => 0,
|
||||
'out_sale' => 0,
|
||||
'out_issue_gen' => 0,
|
||||
'out_issue_free' => 0,
|
||||
'out_return' => 0,
|
||||
'out_misc' => 0,
|
||||
'out_total' => 0,
|
||||
'balance' => 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $m
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private static function finalizeMetrics(array $m): array
|
||||
{
|
||||
$m['recv_total'] = (int) $m['recv_in'] + (int) $m['recv_return'] + (int) $m['recv_misc'];
|
||||
$m['out_total'] = (int) $m['out_sale'] + (int) $m['out_issue_gen'] + (int) $m['out_issue_free']
|
||||
+ (int) $m['out_return'] + (int) $m['out_misc'];
|
||||
|
||||
return $m;
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string, int> $a
|
||||
* @param array<string, int> $b
|
||||
* @return array<string, int>
|
||||
*/
|
||||
private static function mergeMetrics(array $a, array $b): array
|
||||
{
|
||||
foreach (self::emptyMetrics() as $k => $_) {
|
||||
$a[$k] = (int) ($a[$k] ?? 0) + (int) ($b[$k] ?? 0);
|
||||
}
|
||||
|
||||
return $a;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user