Andrew Wallo 10 months ago
parent
commit
eef9e4b61a

+ 22
- 9
app/Filament/Company/Pages/Reports/AccountTransactions.php View File

@@ -17,7 +17,6 @@ use Filament\Forms\Components\Actions;
17 17
 use Filament\Forms\Components\Select;
18 18
 use Filament\Forms\Form;
19 19
 use Filament\Support\Enums\Alignment;
20
-use Filament\Support\Enums\MaxWidth;
21 20
 use Filament\Tables\Actions\Action;
22 21
 use Guava\FilamentClusters\Forms\Cluster;
23 22
 use Illuminate\Contracts\Support\Htmlable;
@@ -39,17 +38,16 @@ class AccountTransactions extends BaseReportPage
39 38
         $this->exportService = $exportService;
40 39
     }
41 40
 
42
-    public function getMaxContentWidth(): MaxWidth | string | null
43
-    {
44
-        return 'max-w-[90rem]';
45
-    }
46
-
47 41
     protected function initializeDefaultFilters(): void
48 42
     {
49 43
         if (empty($this->getFilterState('selectedAccount'))) {
50 44
             $this->setFilterState('selectedAccount', 'all');
51 45
         }
52 46
 
47
+        if (empty($this->getFilterState('basis'))) {
48
+            $this->setFilterState('basis', 'accrual');
49
+        }
50
+
53 51
         if (empty($this->getFilterState('selectedEntity'))) {
54 52
             $this->setFilterState('selectedEntity', 'all');
55 53
         }
@@ -83,7 +81,7 @@ class AccountTransactions extends BaseReportPage
83 81
     public function filtersForm(Form $form): Form
84 82
     {
85 83
         return $form
86
-            ->columns(4)
84
+            ->columns(3)
87 85
             ->schema([
88 86
                 Select::make('selectedAccount')
89 87
                     ->label('Account')
@@ -97,10 +95,18 @@ class AccountTransactions extends BaseReportPage
97 95
                 ])->extraFieldWrapperAttributes([
98 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 105
                 Select::make('selectedEntity')
101 106
                     ->label('Entity')
102 107
                     ->options($this->getEntityOptions())
103
-                    ->searchable(),
108
+                    ->searchable()
109
+                    ->selectablePlaceholder(false),
104 110
                 Actions::make([
105 111
                     Actions\Action::make('applyFilters')
106 112
                         ->label('Update Report')
@@ -151,7 +157,14 @@ class AccountTransactions extends BaseReportPage
151 157
 
152 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 170
     protected function getTransformer(ReportDTO $reportDTO): ExportableReport

+ 5
- 1
app/Filament/Company/Resources/Purchases/BillResource.php View File

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

+ 4
- 0
app/Filament/Company/Resources/Sales/InvoiceResource.php View File

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

+ 12
- 1
app/Models/Accounting/Bill.php View File

@@ -190,7 +190,7 @@ class Bill extends Model
190 190
 
191 191
     public function createInitialTransaction(?Carbon $postedAt = null): void
192 192
     {
193
-        $postedAt ??= now();
193
+        $postedAt ??= $this->date;
194 194
 
195 195
         $transaction = $this->transactions()->create([
196 196
             'company_id' => $this->company_id,
@@ -243,6 +243,17 @@ class Bill extends Model
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 257
     public static function getReplicateAction(string $action = ReplicateAction::class): MountableAction
247 258
     {
248 259
         return $action::make()

+ 30
- 10
app/Models/Accounting/Invoice.php View File

@@ -198,10 +198,10 @@ class Invoice extends Model
198 198
 
199 199
         if ($isRefund) {
200 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 202
         } else {
203 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 207
         // Create transaction
@@ -225,31 +225,45 @@ class Invoice extends Model
225 225
             throw new \RuntimeException('Invoice is not in draft status.');
226 226
         }
227 227
 
228
+        $this->createApprovalTransaction();
229
+
228 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 240
         $transaction = $this->transactions()->create([
231 241
             'company_id' => $this->company_id,
232 242
             'type' => TransactionType::Journal,
233
-            'posted_at' => $approvedAt,
243
+            'posted_at' => $this->date,
234 244
             'amount' => $this->total,
235 245
             'description' => 'Invoice Approval for Invoice #' . $this->invoice_number,
236 246
         ]);
237 247
 
248
+        $baseDescription = "{$this->client->name}: Invoice #{$this->invoice_number}";
249
+
238 250
         $transaction->journalEntries()->create([
239 251
             'company_id' => $this->company_id,
240 252
             'type' => JournalEntryType::Debit,
241 253
             'account_id' => Account::getAccountsReceivableAccount()->id,
242 254
             'amount' => $this->total,
243
-            'description' => $transaction->description,
255
+            'description' => $baseDescription,
244 256
         ]);
245 257
 
246 258
         foreach ($this->lineItems as $lineItem) {
259
+            $lineItemDescription = "{$baseDescription} › {$lineItem->offering->name}";
260
+
247 261
             $transaction->journalEntries()->create([
248 262
                 'company_id' => $this->company_id,
249 263
                 'type' => JournalEntryType::Credit,
250 264
                 'account_id' => $lineItem->offering->income_account_id,
251 265
                 'amount' => $lineItem->subtotal,
252
-                'description' => $transaction->description,
266
+                'description' => $lineItemDescription,
253 267
             ]);
254 268
 
255 269
             foreach ($lineItem->adjustments as $adjustment) {
@@ -258,15 +272,21 @@ class Invoice extends Model
258 272
                     'type' => $adjustment->category->isDiscount() ? JournalEntryType::Debit : JournalEntryType::Credit,
259 273
                     'account_id' => $adjustment->account_id,
260 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 292
     public static function getApproveDraftAction(string $action = Action::class): MountableAction

+ 9
- 2
app/Services/AccountService.php View File

@@ -113,9 +113,9 @@ class AccountService
113 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 119
             $query->select(
120 120
                 'journal_entries.id',
121 121
                 'journal_entries.account_id',
@@ -130,6 +130,13 @@ class AccountService
130 130
                 ->orderBy('transactions.posted_at')
131 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 140
             if ($entityId) {
134 141
                 $entityId = (int) $entityId;
135 142
                 if ($entityId < 0) {

+ 7
- 3
app/Services/ReportService.php View File

@@ -152,7 +152,7 @@ class ReportService
152 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 157
         $columns ??= [];
158 158
         $defaultCurrency = CurrencyAccessor::getDefaultCurrency();
@@ -164,11 +164,16 @@ class ReportService
164 164
         $query = $this->accountService->getAccountBalances($startDate, $endDate, $accountIds)
165 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 169
         $reportCategories = [];
170 170
 
171 171
         foreach ($accounts as $account) {
172
+            /** @var Account $account */
173
+            if ($account->journalEntries->isEmpty()) {
174
+                continue;
175
+            }
176
+
172 177
             $accountTransactions = [];
173 178
             $currentBalance = $account->starting_balance;
174 179
 
@@ -183,7 +188,6 @@ class ReportService
183 188
                 tableAction: null
184 189
             );
185 190
 
186
-            /** @var Account $account */
187 191
             foreach ($account->journalEntries as $journalEntry) {
188 192
                 $transaction = $journalEntry->transaction;
189 193
                 $signedAmount = $journalEntry->signed_amount;

+ 1
- 1
resources/views/components/company/tables/reports/account-transactions.blade.php View File

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

Loading…
Cancel
Save