Andrew Wallo 1年前
父节点
当前提交
6a5805ada4
共有 2 个文件被更改,包括 218 次插入55 次删除
  1. 101
    0
      app/Services/AccountService.php
  2. 117
    55
      app/Services/ReportService.php

+ 101
- 0
app/Services/AccountService.php 查看文件

@@ -10,6 +10,8 @@ use App\Models\Accounting\Transaction;
10 10
 use App\Repositories\Accounting\JournalEntryRepository;
11 11
 use App\Utilities\Currency\CurrencyAccessor;
12 12
 use App\ValueObjects\Money;
13
+use Closure;
14
+use Illuminate\Database\Query\Builder;
13 15
 use Illuminate\Support\Facades\DB;
14 16
 
15 17
 class AccountService implements AccountHandler
@@ -50,6 +52,105 @@ class AccountService implements AccountHandler
50 52
         return new Money($balances['starting_balance'], $account->currency_code);
51 53
     }
52 54
 
55
+    public function getStartingBalanceSubquery($startDate, $accountIds = null): Builder
56
+    {
57
+        return DB::table('journal_entries')
58
+            ->select('journal_entries.account_id')
59
+            ->selectRaw('
60
+                SUM(
61
+                    CASE
62
+                        WHEN accounts.category IN ("asset", "expense") THEN
63
+                            CASE WHEN journal_entries.type = "debit" THEN journal_entries.amount ELSE -journal_entries.amount END
64
+                        ELSE
65
+                            CASE WHEN journal_entries.type = "credit" THEN journal_entries.amount ELSE -journal_entries.amount END
66
+                    END
67
+                ) AS starting_balance
68
+            ')
69
+            ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
70
+            ->join('accounts', 'accounts.id', '=', 'journal_entries.account_id')
71
+            ->where('transactions.posted_at', '<', $startDate)
72
+            ->groupBy('journal_entries.account_id');
73
+    }
74
+
75
+    public function getTotalDebitSubquery($startDate, $endDate, $accountIds = null): Builder
76
+    {
77
+        return DB::table('journal_entries')
78
+            ->select('journal_entries.account_id')
79
+            ->selectRaw('
80
+                SUM(CASE WHEN journal_entries.type = "debit" THEN journal_entries.amount ELSE 0 END) as total_debit
81
+            ')
82
+            ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
83
+            ->whereBetween('transactions.posted_at', [$startDate, $endDate])
84
+            ->groupBy('journal_entries.account_id');
85
+    }
86
+
87
+    public function getTotalCreditSubquery($startDate, $endDate, $accountIds = null): Builder
88
+    {
89
+        return DB::table('journal_entries')
90
+            ->select('journal_entries.account_id')
91
+            ->selectRaw('
92
+                SUM(CASE WHEN journal_entries.type = "credit" THEN journal_entries.amount ELSE 0 END) as total_credit
93
+            ')
94
+            ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
95
+            ->whereBetween('transactions.posted_at', [$startDate, $endDate])
96
+            ->groupBy('journal_entries.account_id');
97
+    }
98
+
99
+    public function getTransactionDetailsSubquery($startDate, $endDate): Closure
100
+    {
101
+        return function ($query) use ($startDate, $endDate) {
102
+            $query->select(
103
+                'journal_entries.id',
104
+                'journal_entries.account_id',
105
+                'journal_entries.transaction_id',
106
+                'journal_entries.type',
107
+                'journal_entries.amount',
108
+                DB::raw('journal_entries.amount * IF(journal_entries.type = "debit", 1, -1) AS signed_amount')
109
+            )
110
+                ->whereBetween('transactions.posted_at', [$startDate, $endDate])
111
+                ->join('transactions', 'transactions.id', '=', 'journal_entries.transaction_id')
112
+                ->orderBy('transactions.posted_at')
113
+                ->with('transaction:id,type,description,posted_at');
114
+        };
115
+    }
116
+
117
+    public function getAccountBalances($startDate, $endDate, $accountIds = null): \Illuminate\Database\Eloquent\Builder
118
+    {
119
+        $query = Account::query()
120
+            ->select([
121
+                'accounts.id',
122
+                'accounts.name',
123
+                'accounts.category',
124
+                'accounts.subtype_id',
125
+                'accounts.currency_code',
126
+                'accounts.code',
127
+            ])
128
+            ->leftJoinSub($this->getStartingBalanceSubquery($startDate), 'starting_balance', function ($join) {
129
+                $join->on('accounts.id', '=', 'starting_balance.account_id');
130
+            })
131
+            ->leftJoinSub($this->getTotalDebitSubquery($startDate, $endDate), 'total_debit', function ($join) {
132
+                $join->on('accounts.id', '=', 'total_debit.account_id');
133
+            })
134
+            ->leftJoinSub($this->getTotalCreditSubquery($startDate, $endDate), 'total_credit', function ($join) {
135
+                $join->on('accounts.id', '=', 'total_credit.account_id');
136
+            })
137
+            ->addSelect([
138
+                'starting_balance.starting_balance',
139
+                'total_debit.total_debit',
140
+                'total_credit.total_credit',
141
+            ])
142
+            ->with(['subtype:id,name'])
143
+            ->whereHas('journalEntries.transaction', function ($query) use ($startDate, $endDate) {
144
+                $query->whereBetween('posted_at', [$startDate, $endDate]);
145
+            });
146
+
147
+        if ($accountIds !== null) {
148
+            $query->whereIn('accounts.id', (array) $accountIds);
149
+        }
150
+
151
+        return $query;
152
+    }
153
+
53 154
     public function getEndingBalance(Account $account, string $startDate, string $endDate): ?Money
54 155
     {
55 156
         $calculatedBalances = $this->calculateBalances($account, $startDate, $endDate);

+ 117
- 55
app/Services/ReportService.php 查看文件

@@ -8,11 +8,10 @@ use App\DTO\AccountDTO;
8 8
 use App\DTO\AccountTransactionDTO;
9 9
 use App\DTO\ReportDTO;
10 10
 use App\Enums\Accounting\AccountCategory;
11
+use App\Enums\Accounting\JournalEntryType;
11 12
 use App\Models\Accounting\Account;
12 13
 use App\Support\Column;
13 14
 use App\Utilities\Currency\CurrencyAccessor;
14
-use Illuminate\Database\Eloquent\Builder;
15
-use Illuminate\Database\Eloquent\Relations\Relation;
16 15
 use Illuminate\Support\Collection;
17 16
 
18 17
 class ReportService
@@ -56,7 +55,9 @@ class ReportService
56 55
     {
57 56
         $allCategories = $this->accountService->getAccountCategoryOrder();
58 57
 
59
-        $categoryGroupedAccounts = $this->getCategoryGroupedAccounts($allCategories);
58
+        $accountIds = Account::whereHas('journalEntries')->pluck('id')->toArray();
59
+
60
+        $accounts = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds)->get();
60 61
 
61 62
         $balanceFields = ['starting_balance', 'debit_balance', 'credit_balance', 'net_movement', 'ending_balance'];
62 63
 
@@ -64,35 +65,105 @@ class ReportService
64 65
 
65 66
         $updatedBalanceFields = array_filter($balanceFields, fn (string $balanceField) => in_array($balanceField, $columnNameKeys, true));
66 67
 
67
-        return $this->buildReport(
68
-            $allCategories,
69
-            $categoryGroupedAccounts,
70
-            fn (Account $account) => $this->accountService->getBalances($account, $startDate, $endDate, $updatedBalanceFields),
71
-            $updatedBalanceFields,
72
-            $columns,
73
-            fn (string $categoryName, array &$categorySummaryBalances) => $this->adjustAccountBalanceCategoryFields($categoryName, $categorySummaryBalances),
74
-        );
68
+        $accountCategories = [];
69
+        $reportTotalBalances = array_fill_keys($updatedBalanceFields, 0);
70
+
71
+        foreach ($allCategories as $categoryPluralName) {
72
+            $categoryName = AccountCategory::fromPluralLabel($categoryPluralName);
73
+            $accountsInCategory = $accounts->where('category', $categoryName)->keyBy('id');
74
+            $categorySummaryBalances = array_fill_keys($updatedBalanceFields, 0);
75
+
76
+            $categoryAccounts = [];
77
+
78
+            foreach ($accountsInCategory as $account) {
79
+                $accountBalances = $this->calculateAccountBalances($account, $categoryName);
80
+
81
+                if ($this->isZeroBalance($accountBalances)) {
82
+                    continue;
83
+                }
84
+
85
+                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
86
+                    if (array_key_exists($accountBalanceType, $categorySummaryBalances)) {
87
+                        $categorySummaryBalances[$accountBalanceType] += $accountBalance;
88
+                    }
89
+                }
90
+
91
+                $filteredAccountBalances = $this->filterBalances($accountBalances, $updatedBalanceFields);
92
+                $formattedAccountBalances = $this->formatBalances($filteredAccountBalances);
93
+
94
+                $categoryAccounts[] = new AccountDTO(
95
+                    $account->name,
96
+                    $account->code,
97
+                    $account->id,
98
+                    $formattedAccountBalances,
99
+                );
100
+            }
101
+
102
+            $this->adjustAccountBalanceCategoryFields($categoryName, $categorySummaryBalances);
103
+
104
+            foreach ($updatedBalanceFields as $field) {
105
+                if (array_key_exists($field, $categorySummaryBalances)) {
106
+                    $reportTotalBalances[$field] += $categorySummaryBalances[$field];
107
+                }
108
+            }
109
+
110
+            $filteredCategorySummaryBalances = $this->filterBalances($categorySummaryBalances, $updatedBalanceFields);
111
+            $formattedCategorySummaryBalances = $this->formatBalances($filteredCategorySummaryBalances);
112
+
113
+            $accountCategories[$categoryPluralName] = new AccountCategoryDTO(
114
+                $categoryAccounts,
115
+                $formattedCategorySummaryBalances,
116
+            );
117
+        }
118
+
119
+        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances);
120
+
121
+        return new ReportDTO($accountCategories, $formattedReportTotalBalances, $columns);
122
+    }
123
+
124
+    private function calculateAccountBalances(Account $account, AccountCategory $category): array
125
+    {
126
+        $balances = [
127
+            'debit_balance' => $account->total_debit,
128
+            'credit_balance' => $account->total_credit,
129
+        ];
130
+
131
+        if (in_array($category, [AccountCategory::Liability, AccountCategory::Equity, AccountCategory::Revenue])) {
132
+            $balances['net_movement'] = $account->total_credit - $account->total_debit;
133
+        } else {
134
+            $balances['net_movement'] = $account->total_debit - $account->total_credit;
135
+        }
136
+
137
+        if (! in_array($category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
138
+            $balances['starting_balance'] = $account->starting_balance;
139
+            $balances['ending_balance'] = $account->starting_balance + $account->total_credit - $account->total_debit;
140
+        }
141
+
142
+        return $balances;
143
+    }
144
+
145
+    private function adjustAccountBalanceCategoryFields(AccountCategory $category, array &$categorySummaryBalances): void
146
+    {
147
+        if (in_array($category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
148
+            unset($categorySummaryBalances['starting_balance'], $categorySummaryBalances['ending_balance']);
149
+        }
150
+    }
151
+
152
+    private function isZeroBalance(array $balances): bool
153
+    {
154
+        return array_sum(array_map('abs', $balances)) === 0;
75 155
     }
76 156
 
77 157
     public function buildAccountTransactionsReport(string $startDate, string $endDate, ?array $columns = null, ?string $accountId = 'all'): ReportDTO
78 158
     {
79 159
         $columns ??= [];
80
-        $query = Account::whereHas('journalEntries.transaction', function (Builder $query) use ($startDate, $endDate) {
81
-            $query->whereBetween('posted_at', [$startDate, $endDate]);
82
-        })
83
-            ->with(['journalEntries' => function (Relation $query) use ($startDate, $endDate) {
84
-                $query->whereHas('transaction', function (Builder $query) use ($startDate, $endDate) {
85
-                    $query->whereBetween('posted_at', [$startDate, $endDate]);
86
-                })
87
-                    ->with('transaction:id,type,description,posted_at')
88
-                    ->select(['account_id', 'transaction_id'])
89
-                    ->selectRaw('SUM(CASE WHEN type = "debit" THEN amount ELSE 0 END) AS total_debit')
90
-                    ->selectRaw('SUM(CASE WHEN type = "credit" THEN amount ELSE 0 END) AS total_credit')
91
-                    ->selectRaw('(SELECT MIN(posted_at) FROM transactions WHERE transactions.id = journal_entries.transaction_id) AS earliest_posted_at')
92
-                    ->groupBy('account_id', 'transaction_id')
93
-                    ->orderBy('earliest_posted_at');
94
-            }])
95
-            ->select(['id', 'name', 'category', 'subtype_id', 'currency_code']);
160
+        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
161
+
162
+        $accountIds = $accountId !== 'all' ? [$accountId] : null;
163
+
164
+        $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds);
165
+
166
+        $query->with(['journalEntries' => $this->accountService->getTransactionDetailsSubquery($startDate, $endDate)]);
96 167
 
97 168
         if ($accountId !== 'all') {
98 169
             $query->where('id', $accountId);
@@ -102,15 +173,9 @@ class ReportService
102 173
 
103 174
         $reportCategories = [];
104 175
 
105
-        $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
106
-
107 176
         foreach ($accounts as $account) {
108 177
             $accountTransactions = [];
109
-            $startingBalance = $this->accountService->getStartingBalance($account, $startDate, true);
110
-
111
-            $currentBalance = $startingBalance?->getAmount() ?? 0;
112
-            $totalDebit = 0;
113
-            $totalCredit = 0;
178
+            $currentBalance = $account->starting_balance;
114 179
 
115 180
             $accountTransactions[] = new AccountTransactionDTO(
116 181
                 id: null,
@@ -118,42 +183,46 @@ class ReportService
118 183
                 description: '',
119 184
                 debit: '',
120 185
                 credit: '',
121
-                balance: $startingBalance?->formatInDefaultCurrency() ?? 0,
186
+                balance: money($currentBalance, $defaultCurrency)->format(),
122 187
                 type: null,
123
-                tableAction: null,
188
+                tableAction: null
124 189
             );
125 190
 
191
+            /** @var Account $account */
126 192
             foreach ($account->journalEntries as $journalEntry) {
127 193
                 $transaction = $journalEntry->transaction;
128
-                $totalDebit += $journalEntry->total_debit;
129
-                $totalCredit += $journalEntry->total_credit;
194
+                $signedAmount = $journalEntry->signed_amount;
130 195
 
131
-                $currentBalance += $journalEntry->total_debit;
132
-                $currentBalance -= $journalEntry->total_credit;
196
+                // Adjust balance based on account category
197
+                if (in_array($account->category, [AccountCategory::Asset, AccountCategory::Expense])) {
198
+                    $currentBalance += $signedAmount;
199
+                } else {
200
+                    $currentBalance -= $signedAmount;
201
+                }
133 202
 
134 203
                 $accountTransactions[] = new AccountTransactionDTO(
135 204
                     id: $transaction->id,
136 205
                     date: $transaction->posted_at->format('Y-m-d'),
137 206
                     description: $transaction->description ?? '',
138
-                    debit: $journalEntry->total_debit ? money($journalEntry->total_debit, $defaultCurrency)->format() : '',
139
-                    credit: $journalEntry->total_credit ? money($journalEntry->total_credit, $defaultCurrency)->format() : '',
207
+                    debit: $journalEntry->type === JournalEntryType::Debit ? money(abs($signedAmount), $defaultCurrency)->format() : '',
208
+                    credit: $journalEntry->type === JournalEntryType::Credit ? money(abs($signedAmount), $defaultCurrency)->format() : '',
140 209
                     balance: money($currentBalance, $defaultCurrency)->format(),
141 210
                     type: $transaction->type,
142
-                    tableAction: $transaction->type->isJournal() ? 'updateJournalTransaction' : 'updateTransaction',
211
+                    tableAction: $transaction->type->isJournal() ? 'updateJournalTransaction' : 'updateTransaction'
143 212
                 );
144 213
             }
145 214
 
146
-            $balanceChange = $currentBalance - ($startingBalance?->getAmount() ?? 0);
215
+            $balanceChange = $currentBalance - $account->starting_balance;
147 216
 
148 217
             $accountTransactions[] = new AccountTransactionDTO(
149 218
                 id: null,
150 219
                 date: 'Totals and Ending Balance',
151 220
                 description: '',
152
-                debit: money($totalDebit, $defaultCurrency)->format(),
153
-                credit: money($totalCredit, $defaultCurrency)->format(),
221
+                debit: money($account->total_debit, $defaultCurrency)->format(),
222
+                credit: money($account->total_credit, $defaultCurrency)->format(),
154 223
                 balance: money($currentBalance, $defaultCurrency)->format(),
155 224
                 type: null,
156
-                tableAction: null,
225
+                tableAction: null
157 226
             );
158 227
 
159 228
             $accountTransactions[] = new AccountTransactionDTO(
@@ -164,7 +233,7 @@ class ReportService
164 233
                 credit: '',
165 234
                 balance: money($balanceChange, $defaultCurrency)->format(),
166 235
                 type: null,
167
-                tableAction: null,
236
+                tableAction: null
168 237
             );
169 238
 
170 239
             $reportCategories[] = [
@@ -260,13 +329,6 @@ class ReportService
260 329
         return new ReportDTO($accountCategories, $formattedReportTotalBalances, $allFields);
261 330
     }
262 331
 
263
-    private function adjustAccountBalanceCategoryFields(string $categoryName, array &$categorySummaryBalances): void
264
-    {
265
-        if (in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
266
-            unset($categorySummaryBalances['starting_balance'], $categorySummaryBalances['ending_balance']);
267
-        }
268
-    }
269
-
270 332
     public function buildTrialBalanceReport(string $startDate, string $endDate, array $columns = []): ReportDTO
271 333
     {
272 334
         $allCategories = $this->accountService->getAccountCategoryOrder();

正在加载...
取消
保存