2026-03-19 11:08:53 +09:00
|
|
|
<?php
|
|
|
|
|
|
|
|
|
|
namespace App\Http\Controllers\Finance;
|
|
|
|
|
|
|
|
|
|
use App\Http\Controllers\Controller;
|
|
|
|
|
use App\Models\Barobill\AccountCode;
|
|
|
|
|
use Illuminate\Http\JsonResponse;
|
|
|
|
|
use Illuminate\Http\Request;
|
|
|
|
|
use Illuminate\Support\Facades\DB;
|
|
|
|
|
|
|
|
|
|
class IncomeStatementController extends Controller
|
|
|
|
|
{
|
|
|
|
|
// 손익계산서 항목 구조 (한국 일반기업회계기준)
|
2026-03-19 11:38:00 +09:00
|
|
|
// sub_categories: DB account_codes.sub_category 값 매핑 (배열 = 복수 sub_category 합산)
|
|
|
|
|
// tax_codes: 특정 계정코드를 별도 항목으로 분리 (법인세/소득세)
|
2026-03-19 11:08:53 +09:00
|
|
|
private const PL_STRUCTURE = [
|
2026-03-19 11:38:00 +09:00
|
|
|
['code' => 'I', 'name' => '매출액', 'type' => 'sum', 'category' => 'revenue', 'sub_categories' => ['sales_revenue']],
|
|
|
|
|
['code' => 'II', 'name' => '매출원가', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['cogs', 'construction_cost']],
|
2026-03-19 11:08:53 +09:00
|
|
|
['code' => 'III', 'name' => '매출총이익', 'type' => 'calc', 'formula' => 'I - II'],
|
2026-03-19 11:38:00 +09:00
|
|
|
['code' => 'IV', 'name' => '판매비와관리비', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['selling_admin']],
|
2026-03-19 11:08:53 +09:00
|
|
|
['code' => 'V', 'name' => '영업이익', 'type' => 'calc', 'formula' => 'III - IV'],
|
2026-03-19 11:38:00 +09:00
|
|
|
['code' => 'VI', 'name' => '영업외수익', 'type' => 'sum', 'category' => 'revenue', 'sub_categories' => ['other_revenue']],
|
|
|
|
|
['code' => 'VII', 'name' => '영업외비용', 'type' => 'sum', 'category' => 'expense', 'sub_categories' => ['other_expense'], 'exclude_codes' => ['99800', '99900']],
|
2026-03-19 11:08:53 +09:00
|
|
|
['code' => 'VIII', 'name' => '법인세비용차감전순이익', 'type' => 'calc', 'formula' => 'V + VI - VII'],
|
2026-03-19 11:38:00 +09:00
|
|
|
['code' => 'IX', 'name' => '법인세비용', 'type' => 'sum', 'category' => 'expense', 'tax_codes' => ['99800', '99900']],
|
2026-03-19 11:08:53 +09:00
|
|
|
['code' => 'X', 'name' => '당기순이익', 'type' => 'calc', 'formula' => 'VIII - IX'],
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 손익계산서 페이지
|
|
|
|
|
*/
|
|
|
|
|
public function index(Request $request)
|
|
|
|
|
{
|
|
|
|
|
if ($request->header('HX-Request')) {
|
|
|
|
|
return response('', 200)->header('HX-Redirect', route('finance.income-statement'));
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return view('finance.income-statement');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 손익계산서 데이터 조회
|
|
|
|
|
*/
|
|
|
|
|
public function data(Request $request): JsonResponse
|
|
|
|
|
{
|
|
|
|
|
$tenantId = session('selected_tenant_id', 1);
|
|
|
|
|
$startDate = $request->input('start_date', now()->startOfYear()->format('Y-m-d'));
|
|
|
|
|
$endDate = $request->input('end_date', now()->endOfYear()->format('Y-m-d'));
|
|
|
|
|
$unit = $request->input('unit', 'won');
|
|
|
|
|
|
|
|
|
|
// 전기 기간 계산 (전년 동기)
|
|
|
|
|
$prevStartDate = date('Y-m-d', strtotime($startDate.' -1 year'));
|
|
|
|
|
$prevEndDate = date('Y-m-d', strtotime($endDate.' -1 year'));
|
|
|
|
|
|
|
|
|
|
// 당기/전기 계정별 합계
|
|
|
|
|
$currentSums = $this->getAccountSums($tenantId, $startDate, $endDate);
|
|
|
|
|
$previousSums = $this->getAccountSums($tenantId, $prevStartDate, $prevEndDate);
|
|
|
|
|
|
|
|
|
|
// 계정과목 목록 (sub_category별 세부 항목 표시용)
|
|
|
|
|
$accountCodes = AccountCode::where('tenant_id', $tenantId)
|
|
|
|
|
->where('is_active', true)
|
|
|
|
|
->whereIn('category', ['revenue', 'expense'])
|
|
|
|
|
->orderBy('sort_order')
|
|
|
|
|
->orderBy('code')
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
// 손익계산서 구조 조립
|
|
|
|
|
$sections = [];
|
|
|
|
|
$calcValues = [];
|
|
|
|
|
|
|
|
|
|
foreach (self::PL_STRUCTURE as $item) {
|
|
|
|
|
$section = [
|
|
|
|
|
'code' => $item['code'],
|
|
|
|
|
'name' => $item['name'],
|
|
|
|
|
'current_amount' => 0,
|
|
|
|
|
'previous_amount' => 0,
|
|
|
|
|
'items' => [],
|
|
|
|
|
'is_calculated' => $item['type'] === 'calc',
|
|
|
|
|
];
|
|
|
|
|
|
|
|
|
|
if ($item['type'] === 'sum') {
|
|
|
|
|
$relatedAccounts = $accountCodes->filter(function ($ac) use ($item) {
|
2026-03-19 11:38:00 +09:00
|
|
|
// tax_codes: 특정 계정코드만 포함 (법인세/소득세)
|
|
|
|
|
if (! empty($item['tax_codes'])) {
|
|
|
|
|
return in_array($ac->code, $item['tax_codes']);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sub_categories 배열로 매칭
|
|
|
|
|
$subCategories = $item['sub_categories'] ?? [];
|
|
|
|
|
if ($ac->category !== $item['category'] || ! in_array($ac->sub_category, $subCategories)) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// exclude_codes: 특정 계정코드 제외 (영업외비용에서 법인세 제외)
|
|
|
|
|
if (! empty($item['exclude_codes']) && in_array($ac->code, $item['exclude_codes'])) {
|
|
|
|
|
return false;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return true;
|
2026-03-19 11:08:53 +09:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
$currentTotal = 0;
|
|
|
|
|
$previousTotal = 0;
|
|
|
|
|
|
|
|
|
|
foreach ($relatedAccounts as $ac) {
|
|
|
|
|
$curDebit = $currentSums[$ac->code]['debit'] ?? 0;
|
|
|
|
|
$curCredit = $currentSums[$ac->code]['credit'] ?? 0;
|
|
|
|
|
$prevDebit = $previousSums[$ac->code]['debit'] ?? 0;
|
|
|
|
|
$prevCredit = $previousSums[$ac->code]['credit'] ?? 0;
|
|
|
|
|
|
|
|
|
|
// 수익: 대변 - 차변, 비용: 차변 - 대변
|
|
|
|
|
if ($ac->category === 'revenue') {
|
|
|
|
|
$curAmount = $curCredit - $curDebit;
|
|
|
|
|
$prevAmount = $prevCredit - $prevDebit;
|
|
|
|
|
} else {
|
|
|
|
|
$curAmount = $curDebit - $curCredit;
|
|
|
|
|
$prevAmount = $prevDebit - $prevCredit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if ($curAmount != 0 || $prevAmount != 0) {
|
|
|
|
|
$section['items'][] = [
|
|
|
|
|
'code' => $ac->code,
|
|
|
|
|
'name' => $ac->name,
|
|
|
|
|
'current' => $curAmount,
|
|
|
|
|
'previous' => $prevAmount,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$currentTotal += $curAmount;
|
|
|
|
|
$previousTotal += $prevAmount;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$section['current_amount'] = $currentTotal;
|
|
|
|
|
$section['previous_amount'] = $previousTotal;
|
|
|
|
|
$calcValues[$item['code']] = ['current' => $currentTotal, 'previous' => $previousTotal];
|
|
|
|
|
} elseif ($item['type'] === 'calc') {
|
|
|
|
|
$amounts = $this->evaluateFormula($item['formula'], $calcValues);
|
|
|
|
|
$section['current_amount'] = $amounts['current'];
|
|
|
|
|
$section['previous_amount'] = $amounts['previous'];
|
|
|
|
|
$calcValues[$item['code']] = $amounts;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
$sections[] = $section;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 단위 변환
|
|
|
|
|
$divisor = match ($unit) {
|
|
|
|
|
'thousand' => 1000,
|
|
|
|
|
'million' => 1000000,
|
|
|
|
|
default => 1,
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
if ($divisor > 1) {
|
|
|
|
|
$sections = $this->applyUnitDivisor($sections, $divisor);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 기수 계산
|
|
|
|
|
$currentYear = (int) date('Y', strtotime($endDate));
|
|
|
|
|
$fiscalYear = $this->getFiscalYear($tenantId, $currentYear);
|
|
|
|
|
|
|
|
|
|
return response()->json([
|
|
|
|
|
'period' => [
|
|
|
|
|
'current' => [
|
|
|
|
|
'start' => $startDate,
|
|
|
|
|
'end' => $endDate,
|
|
|
|
|
'label' => "제 {$fiscalYear} (당)기",
|
|
|
|
|
],
|
|
|
|
|
'previous' => [
|
|
|
|
|
'start' => $prevStartDate,
|
|
|
|
|
'end' => $prevEndDate,
|
|
|
|
|
'label' => '제 '.($fiscalYear - 1).' (전)기',
|
|
|
|
|
],
|
|
|
|
|
],
|
|
|
|
|
'unit' => $unit,
|
|
|
|
|
'sections' => $sections,
|
|
|
|
|
]);
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 계정코드별 차변/대변 합계 조회
|
|
|
|
|
*/
|
|
|
|
|
private function getAccountSums(int $tenantId, string $startDate, string $endDate): array
|
|
|
|
|
{
|
|
|
|
|
// 일반전표
|
|
|
|
|
$journalSums = DB::table('journal_entry_lines as jel')
|
|
|
|
|
->join('journal_entries as je', 'je.id', '=', 'jel.journal_entry_id')
|
|
|
|
|
->where('jel.tenant_id', $tenantId)
|
|
|
|
|
->whereBetween('je.entry_date', [$startDate, $endDate])
|
|
|
|
|
->whereNull('je.deleted_at')
|
|
|
|
|
->groupBy('jel.account_code')
|
|
|
|
|
->select([
|
|
|
|
|
'jel.account_code',
|
|
|
|
|
DB::raw('SUM(jel.debit_amount) as debit'),
|
|
|
|
|
DB::raw('SUM(jel.credit_amount) as credit'),
|
|
|
|
|
])
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
// 홈택스
|
|
|
|
|
$hometaxSums = DB::table('hometax_invoice_journals as hij')
|
|
|
|
|
->join('hometax_invoices as hi', 'hi.id', '=', 'hij.hometax_invoice_id')
|
|
|
|
|
->where('hij.tenant_id', $tenantId)
|
|
|
|
|
->whereBetween('hij.write_date', [$startDate, $endDate])
|
|
|
|
|
->whereNull('hi.deleted_at')
|
|
|
|
|
->groupBy('hij.account_code')
|
|
|
|
|
->select([
|
|
|
|
|
'hij.account_code',
|
|
|
|
|
DB::raw('SUM(hij.debit_amount) as debit'),
|
|
|
|
|
DB::raw('SUM(hij.credit_amount) as credit'),
|
|
|
|
|
])
|
|
|
|
|
->get();
|
|
|
|
|
|
|
|
|
|
$result = [];
|
|
|
|
|
foreach ($journalSums as $row) {
|
|
|
|
|
$result[$row->account_code] = [
|
|
|
|
|
'debit' => (int) $row->debit,
|
|
|
|
|
'credit' => (int) $row->credit,
|
|
|
|
|
];
|
|
|
|
|
}
|
|
|
|
|
foreach ($hometaxSums as $row) {
|
|
|
|
|
if (! isset($result[$row->account_code])) {
|
|
|
|
|
$result[$row->account_code] = ['debit' => 0, 'credit' => 0];
|
|
|
|
|
}
|
|
|
|
|
$result[$row->account_code]['debit'] += (int) $row->debit;
|
|
|
|
|
$result[$row->account_code]['credit'] += (int) $row->credit;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return $result;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 계산 공식 평가 (I - II, III - IV 등)
|
|
|
|
|
*/
|
|
|
|
|
private function evaluateFormula(string $formula, array $values): array
|
|
|
|
|
{
|
|
|
|
|
$current = 0;
|
|
|
|
|
$previous = 0;
|
|
|
|
|
|
|
|
|
|
$parts = preg_split('/\s*([+-])\s*/', $formula, -1, PREG_SPLIT_DELIM_CAPTURE);
|
|
|
|
|
$sign = 1;
|
|
|
|
|
|
|
|
|
|
foreach ($parts as $part) {
|
|
|
|
|
$part = trim($part);
|
|
|
|
|
if ($part === '+') {
|
|
|
|
|
$sign = 1;
|
|
|
|
|
} elseif ($part === '-') {
|
|
|
|
|
$sign = -1;
|
|
|
|
|
} elseif (isset($values[$part])) {
|
|
|
|
|
$current += $sign * $values[$part]['current'];
|
|
|
|
|
$previous += $sign * $values[$part]['previous'];
|
|
|
|
|
$sign = 1;
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return compact('current', 'previous');
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 금액 단위 변환
|
|
|
|
|
*/
|
|
|
|
|
private function applyUnitDivisor(array $sections, int $divisor): array
|
|
|
|
|
{
|
|
|
|
|
foreach ($sections as &$section) {
|
|
|
|
|
$section['current_amount'] = (int) round($section['current_amount'] / $divisor);
|
|
|
|
|
$section['previous_amount'] = (int) round($section['previous_amount'] / $divisor);
|
|
|
|
|
|
|
|
|
|
if (! empty($section['items'])) {
|
|
|
|
|
foreach ($section['items'] as &$item) {
|
|
|
|
|
$item['current'] = (int) round($item['current'] / $divisor);
|
|
|
|
|
$item['previous'] = (int) round($item['previous'] / $divisor);
|
|
|
|
|
}
|
|
|
|
|
unset($item);
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
unset($section);
|
|
|
|
|
|
|
|
|
|
return $sections;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 기수 계산 (테넌트별 설립연도 기반, 기본 1기 = 2005년)
|
|
|
|
|
*/
|
|
|
|
|
private function getFiscalYear(int $tenantId, int $currentYear): int
|
|
|
|
|
{
|
|
|
|
|
// 기본 1기 기준년도 (향후 테넌트 설정에서 가져올 수 있음)
|
|
|
|
|
$baseYear = 2005;
|
|
|
|
|
|
|
|
|
|
return $currentYear - $baseYear + 1;
|
|
|
|
|
}
|
|
|
|
|
}
|