Andrew Wallo 10 月之前
父節點
當前提交
eef9e4b61a

+ 22
- 9
app/Filament/Company/Pages/Reports/AccountTransactions.php 查看文件

17
 use Filament\Forms\Components\Select;
17
 use Filament\Forms\Components\Select;
18
 use Filament\Forms\Form;
18
 use Filament\Forms\Form;
19
 use Filament\Support\Enums\Alignment;
19
 use Filament\Support\Enums\Alignment;
20
-use Filament\Support\Enums\MaxWidth;
21
 use Filament\Tables\Actions\Action;
20
 use Filament\Tables\Actions\Action;
22
 use Guava\FilamentClusters\Forms\Cluster;
21
 use Guava\FilamentClusters\Forms\Cluster;
23
 use Illuminate\Contracts\Support\Htmlable;
22
 use Illuminate\Contracts\Support\Htmlable;
39
         $this->exportService = $exportService;
38
         $this->exportService = $exportService;
40
     }
39
     }
41
 
40
 
42
-    public function getMaxContentWidth(): MaxWidth | string | null
43
-    {
44
-        return 'max-w-[90rem]';
45
-    }
46
-
47
     protected function initializeDefaultFilters(): void
41
     protected function initializeDefaultFilters(): void
48
     {
42
     {
49
         if (empty($this->getFilterState('selectedAccount'))) {
43
         if (empty($this->getFilterState('selectedAccount'))) {
50
             $this->setFilterState('selectedAccount', 'all');
44
             $this->setFilterState('selectedAccount', 'all');
51
         }
45
         }
52
 
46
 
47
+        if (empty($this->getFilterState('basis'))) {
48
+            $this->setFilterState('basis', 'accrual');
49
+        }
50
+
53
         if (empty($this->getFilterState('selectedEntity'))) {
51
         if (empty($this->getFilterState('selectedEntity'))) {
54
             $this->setFilterState('selectedEntity', 'all');
52
             $this->setFilterState('selectedEntity', 'all');
55
         }
53
         }
83
     public function filtersForm(Form $form): Form
81
     public function filtersForm(Form $form): Form
84
     {
82
     {
85
         return $form
83
         return $form
86
-            ->columns(4)
84
+            ->columns(3)
87
             ->schema([
85
             ->schema([
88
                 Select::make('selectedAccount')
86
                 Select::make('selectedAccount')
89
                     ->label('Account')
87
                     ->label('Account')
97
                 ])->extraFieldWrapperAttributes([
95
                 ])->extraFieldWrapperAttributes([
98
                     'class' => 'report-hidden-label',
96
                     'class' => 'report-hidden-label',
99
                 ]),
97
                 ]),
98
+                Select::make('basis')
99
+                    ->label('Accounting Basis')
100
+                    ->options([
101
+                        'accrual' => 'Accrual (Paid & Unpaid)',
102
+                        'cash' => 'Cash Basis (Paid)',
103
+                    ])
104
+                    ->selectablePlaceholder(false),
100
                 Select::make('selectedEntity')
105
                 Select::make('selectedEntity')
101
                     ->label('Entity')
106
                     ->label('Entity')
102
                     ->options($this->getEntityOptions())
107
                     ->options($this->getEntityOptions())
103
-                    ->searchable(),
108
+                    ->searchable()
109
+                    ->selectablePlaceholder(false),
104
                 Actions::make([
110
                 Actions::make([
105
                     Actions\Action::make('applyFilters')
111
                     Actions\Action::make('applyFilters')
106
                         ->label('Update Report')
112
                         ->label('Update Report')
151
 
157
 
152
     protected function buildReport(array $columns): ReportDTO
158
     protected function buildReport(array $columns): ReportDTO
153
     {
159
     {
154
-        return $this->reportService->buildAccountTransactionsReport($this->getFormattedStartDate(), $this->getFormattedEndDate(), $columns, $this->getFilterState('selectedAccount'), $this->getFilterState('selectedEntity'));
160
+        return $this->reportService->buildAccountTransactionsReport(
161
+            startDate: $this->getFormattedStartDate(),
162
+            endDate: $this->getFormattedEndDate(),
163
+            columns: $columns,
164
+            accountId: $this->getFilterState('selectedAccount'),
165
+            basis: $this->getFilterState('basis'),
166
+            entityId: $this->getFilterState('selectedEntity'),
167
+        );
155
     }
168
     }
156
 
169
 
157
     protected function getTransformer(ReportDTO $reportDTO): ExportableReport
170
     protected function getTransformer(ReportDTO $reportDTO): ExportableReport

+ 5
- 1
app/Filament/Company/Resources/Purchases/BillResource.php 查看文件

175
 
175
 
176
                                 $bill->refresh();
176
                                 $bill->refresh();
177
 
177
 
178
-                                $bill->createInitialTransaction();
178
+                                if (! $bill->initialTransaction) {
179
+                                    $bill->createInitialTransaction();
180
+                                } else {
181
+                                    $bill->updateInitialTransaction();
182
+                                }
179
                             })
183
                             })
180
                             ->headers([
184
                             ->headers([
181
                                 Header::make('Items')->width('15%'),
185
                                 Header::make('Items')->width('15%'),

+ 4
- 0
app/Filament/Company/Resources/Sales/InvoiceResource.php 查看文件

232
                                     'discount_total' => $discountTotal,
232
                                     'discount_total' => $discountTotal,
233
                                     'total' => $grandTotal,
233
                                     'total' => $grandTotal,
234
                                 ]);
234
                                 ]);
235
+
236
+                                if ($invoice->approved_at && $invoice->approvalTransaction) {
237
+                                    $invoice->updateApprovalTransaction();
238
+                                }
235
                             })
239
                             })
236
                             ->headers([
240
                             ->headers([
237
                                 Header::make('Items')->width('15%'),
241
                                 Header::make('Items')->width('15%'),

+ 12
- 1
app/Models/Accounting/Bill.php 查看文件

190
 
190
 
191
     public function createInitialTransaction(?Carbon $postedAt = null): void
191
     public function createInitialTransaction(?Carbon $postedAt = null): void
192
     {
192
     {
193
-        $postedAt ??= now();
193
+        $postedAt ??= $this->date;
194
 
194
 
195
         $transaction = $this->transactions()->create([
195
         $transaction = $this->transactions()->create([
196
             'company_id' => $this->company_id,
196
             'company_id' => $this->company_id,
243
         }
243
         }
244
     }
244
     }
245
 
245
 
246
+    public function updateInitialTransaction(): void
247
+    {
248
+        $transaction = $this->initialTransaction;
249
+
250
+        if ($transaction) {
251
+            $transaction->delete();
252
+        }
253
+
254
+        $this->createInitialTransaction();
255
+    }
256
+
246
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
257
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
247
     {
258
     {
248
         return $action::make()
259
         return $action::make()

+ 30
- 10
app/Models/Accounting/Invoice.php 查看文件

198
 
198
 
199
         if ($isRefund) {
199
         if ($isRefund) {
200
             $transactionType = TransactionType::Withdrawal;
200
             $transactionType = TransactionType::Withdrawal;
201
-            $transactionDescription = 'Refund for Overpayment on Invoice #' . $this->invoice_number;
201
+            $transactionDescription = "Invoice #{$this->invoice_number}: Refund to {$this->client->name}";
202
         } else {
202
         } else {
203
             $transactionType = TransactionType::Deposit;
203
             $transactionType = TransactionType::Deposit;
204
-            $transactionDescription = 'Payment for Invoice #' . $this->invoice_number;
204
+            $transactionDescription = "Invoice #{$this->invoice_number}: Payment from {$this->client->name}";
205
         }
205
         }
206
 
206
 
207
         // Create transaction
207
         // Create transaction
225
             throw new \RuntimeException('Invoice is not in draft status.');
225
             throw new \RuntimeException('Invoice is not in draft status.');
226
         }
226
         }
227
 
227
 
228
+        $this->createApprovalTransaction();
229
+
228
         $approvedAt ??= now();
230
         $approvedAt ??= now();
229
 
231
 
232
+        $this->update([
233
+            'approved_at' => $approvedAt,
234
+            'status' => InvoiceStatus::Unsent,
235
+        ]);
236
+    }
237
+
238
+    public function createApprovalTransaction(): void
239
+    {
230
         $transaction = $this->transactions()->create([
240
         $transaction = $this->transactions()->create([
231
             'company_id' => $this->company_id,
241
             'company_id' => $this->company_id,
232
             'type' => TransactionType::Journal,
242
             'type' => TransactionType::Journal,
233
-            'posted_at' => $approvedAt,
243
+            'posted_at' => $this->date,
234
             'amount' => $this->total,
244
             'amount' => $this->total,
235
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
245
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
236
         ]);
246
         ]);
237
 
247
 
248
+        $baseDescription = "{$this->client->name}: Invoice #{$this->invoice_number}";
249
+
238
         $transaction->journalEntries()->create([
250
         $transaction->journalEntries()->create([
239
             'company_id' => $this->company_id,
251
             'company_id' => $this->company_id,
240
             'type' => JournalEntryType::Debit,
252
             'type' => JournalEntryType::Debit,
241
             'account_id' => Account::getAccountsReceivableAccount()->id,
253
             'account_id' => Account::getAccountsReceivableAccount()->id,
242
             'amount' => $this->total,
254
             'amount' => $this->total,
243
-            'description' => $transaction->description,
255
+            'description' => $baseDescription,
244
         ]);
256
         ]);
245
 
257
 
246
         foreach ($this->lineItems as $lineItem) {
258
         foreach ($this->lineItems as $lineItem) {
259
+            $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
260
+
247
             $transaction->journalEntries()->create([
261
             $transaction->journalEntries()->create([
248
                 'company_id' => $this->company_id,
262
                 'company_id' => $this->company_id,
249
                 'type' => JournalEntryType::Credit,
263
                 'type' => JournalEntryType::Credit,
250
                 'account_id' => $lineItem->offering->income_account_id,
264
                 'account_id' => $lineItem->offering->income_account_id,
251
                 'amount' => $lineItem->subtotal,
265
                 'amount' => $lineItem->subtotal,
252
-                'description' => $transaction->description,
266
+                'description' => $lineItemDescription,
253
             ]);
267
             ]);
254
 
268
 
255
             foreach ($lineItem->adjustments as $adjustment) {
269
             foreach ($lineItem->adjustments as $adjustment) {
258
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
272
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
259
                     'account_id' => $adjustment->account_id,
273
                     'account_id' => $adjustment->account_id,
260
                     'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
274
                     'amount' => $lineItem->calculateAdjustmentTotal($adjustment)->getAmount(),
261
-                    'description' => $transaction->description,
275
+                    'description' => $lineItemDescription,
262
                 ]);
276
                 ]);
263
             }
277
             }
264
         }
278
         }
279
+    }
265
 
280
 
266
-        $this->update([
267
-            'approved_at' => $approvedAt,
268
-            'status' => InvoiceStatus::Unsent,
269
-        ]);
281
+    public function updateApprovalTransaction(): void
282
+    {
283
+        $transaction = $this->approvalTransaction;
284
+
285
+        if ($transaction) {
286
+            $transaction->delete();
287
+        }
288
+
289
+        $this->createApprovalTransaction();
270
     }
290
     }
271
 
291
 
272
     public static function getApproveDraftAction(string $action = Action::class): MountableAction
292
     public static function getApproveDraftAction(string $action = Action::class): MountableAction

+ 9
- 2
app/Services/AccountService.php 查看文件

113
         return array_filter($balances, static fn ($value) => $value !== null);
113
         return array_filter($balances, static fn ($value) => $value !== null);
114
     }
114
     }
115
 
115
 
116
-    public function getTransactionDetailsSubquery(string $startDate, string $endDate, ?string $entityId = null): Closure
116
+    public function getTransactionDetailsSubquery(string $startDate, string $endDate, string $basis = 'accrual', ?string $entityId = null): Closure
117
     {
117
     {
118
-        return static function ($query) use ($startDate, $endDate, $entityId) {
118
+        return static function ($query) use ($startDate, $endDate, $basis, $entityId) {
119
             $query->select(
119
             $query->select(
120
                 'journal_entries.id',
120
                 'journal_entries.id',
121
                 'journal_entries.account_id',
121
                 'journal_entries.account_id',
130
                 ->orderBy('transactions.posted_at')
130
                 ->orderBy('transactions.posted_at')
131
                 ->with('transaction:id,type,description,posted_at');
131
                 ->with('transaction:id,type,description,posted_at');
132
 
132
 
133
+            if ($basis === 'cash') {
134
+                $query->where(function ($query) {
135
+                    $query->whereNull('transactions.transactionable_id')
136
+                        ->orWhere('transactions.is_payment', true);
137
+                });
138
+            }
139
+
133
             if ($entityId) {
140
             if ($entityId) {
134
                 $entityId = (int) $entityId;
141
                 $entityId = (int) $entityId;
135
                 if ($entityId < 0) {
142
                 if ($entityId < 0) {

+ 7
- 3
app/Services/ReportService.php 查看文件

152
         return new Money($retainedEarnings, CurrencyAccessor::getDefaultCurrency());
152
         return new Money($retainedEarnings, CurrencyAccessor::getDefaultCurrency());
153
     }
153
     }
154
 
154
 
155
-    public function buildAccountTransactionsReport(string $startDate, string $endDate, ?array $columns = null, ?string $accountId = 'all', ?string $entityId = 'all'): ReportDTO
155
+    public function buildAccountTransactionsReport(string $startDate, string $endDate, ?array $columns = null, ?string $accountId = 'all', ?string $basis = 'accrual', ?string $entityId = 'all'): ReportDTO
156
     {
156
     {
157
         $columns ??= [];
157
         $columns ??= [];
158
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
158
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
164
         $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds)
164
         $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds)
165
             ->orderByRaw('LENGTH(code), code');
165
             ->orderByRaw('LENGTH(code), code');
166
 
166
 
167
-        $accounts = $query->with(['journalEntries' => $this->accountService->getTransactionDetailsSubquery($startDate, $endDate, $entityId)])->get();
167
+        $accounts = $query->with(['journalEntries' => $this->accountService->getTransactionDetailsSubquery($startDate, $endDate, $basis, $entityId)])->get();
168
 
168
 
169
         $reportCategories = [];
169
         $reportCategories = [];
170
 
170
 
171
         foreach ($accounts as $account) {
171
         foreach ($accounts as $account) {
172
+            /** @var Account $account */
173
+            if ($account->journalEntries->isEmpty()) {
174
+                continue;
175
+            }
176
+
172
             $accountTransactions = [];
177
             $accountTransactions = [];
173
             $currentBalance = $account->starting_balance;
178
             $currentBalance = $account->starting_balance;
174
 
179
 
183
                 tableAction: null
188
                 tableAction: null
184
             );
189
             );
185
 
190
 
186
-            /** @var Account $account */
187
             foreach ($account->journalEntries as $journalEntry) {
191
             foreach ($account->journalEntries as $journalEntry) {
188
                 $transaction = $journalEntry->transaction;
192
                 $transaction = $journalEntry->transaction;
189
                 $signedAmount = $journalEntry->signed_amount;
193
                 $signedAmount = $journalEntry->signed_amount;

+ 1
- 1
resources/views/components/company/tables/reports/account-transactions.blade.php 查看文件

1
 <table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
1
 <table class="w-full table-auto divide-y divide-gray-200 dark:divide-white/5">
2
     <x-company.tables.header :headers="$report->getHeaders()" :alignmentClass="[$report, 'getAlignmentClass']"/>
2
     <x-company.tables.header :headers="$report->getHeaders()" :alignmentClass="[$report, 'getAlignmentClass']"/>
3
     @foreach($report->getCategories() as $categoryIndex => $category)
3
     @foreach($report->getCategories() as $categoryIndex => $category)
4
-        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
4
+        <tbody class="divide-y divide-gray-200 dark:divide-white/5">
5
         <!-- Category Header -->
5
         <!-- Category Header -->
6
         <tr class="bg-gray-50 dark:bg-white/5">
6
         <tr class="bg-gray-50 dark:bg-white/5">
7
             <x-filament-tables::cell tag="th" colspan="{{ count($report->getHeaders()) }}" class="text-left">
7
             <x-filament-tables::cell tag="th" colspan="{{ count($report->getHeaders()) }}" class="text-left">

Loading…
取消
儲存