db = $db ?? \Config\Database::connect(); } private static function bagCodeKey(mixed $code): string { return (string) $code; } /** * @return array{ * rows: list>, * bagKindLabels: array, * 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 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 */ 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 $codes * @return array>> 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 $products * @param array> $opening date key '_open' * @param array>> $periodMoves * @param array $bagKindLabels * @return list> */ 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>> $openingRaw * @return array 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 $products * @param array>> $opening * @param array>> $periodMoves * @param array $bagKindLabels * @return list> */ 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 $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 */ 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 $m * @return array */ 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 $a * @param array $b * @return array */ 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; } }