Procházet zdrojové kódy

refactor bank account/account relationship

3.x
Andrew Wallo před 11 měsíci
rodič
revize
4cb82533f5

+ 9
- 11
app/Filament/Company/Pages/Accounting/Transactions.php Zobrazit soubor

770
 
770
 
771
     protected function getBankAccountOptions(?int $excludedAccountId = null, ?int $currentBankAccountId = null): array
771
     protected function getBankAccountOptions(?int $excludedAccountId = null, ?int $currentBankAccountId = null): array
772
     {
772
     {
773
-        return BankAccount::join('accounts', 'accounts.bank_account_id', '=', 'bank_accounts.id')
774
-            ->where('accounts.archived', false)
775
-            ->select(['bank_accounts.id', 'accounts.name', 'accounts.subtype_id'])
776
-            ->with(['account.subtype' => static function ($query) {
773
+        return BankAccount::query()
774
+            ->whereHas('account', function (Builder $query) {
775
+                $query->where('archived', false);
776
+            })
777
+            ->with(['account' => function ($query) {
778
+                $query->where('archived', false);
779
+            }, 'account.subtype' => function ($query) {
777
                 $query->select(['id', 'name']);
780
                 $query->select(['id', 'name']);
778
             }])
781
             }])
779
-            ->when($excludedAccountId, function (Builder $query) use ($excludedAccountId) {
780
-                $query->whereNot('accounts.id', $excludedAccountId);
781
-            })
782
-            ->when($currentBankAccountId, function (Builder $query) use ($currentBankAccountId) {
783
-                // Ensure the current bank account is included even if archived
784
-                $query->orWhere('bank_accounts.id', $currentBankAccountId);
785
-            })
782
+            ->when($excludedAccountId, fn (Builder $query) => $query->where('account_id', '!=', $excludedAccountId))
783
+            ->when($currentBankAccountId, fn (Builder $query) => $query->orWhere('id', $currentBankAccountId))
786
             ->get()
784
             ->get()
787
             ->groupBy('account.subtype.name')
785
             ->groupBy('account.subtype.name')
788
             ->map(fn (Collection $bankAccounts, string $subtype) => $bankAccounts->pluck('account.name', 'id'))
786
             ->map(fn (Collection $bankAccounts, string $subtype) => $bankAccounts->pluck('account.name', 'id'))

+ 3
- 3
app/Models/Accounting/Account.php Zobrazit soubor

18
 use Illuminate\Database\Eloquent\Model;
18
 use Illuminate\Database\Eloquent\Model;
19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
20
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
 use Illuminate\Database\Eloquent\Relations\HasMany;
21
+use Illuminate\Database\Eloquent\Relations\HasOne;
21
 use Illuminate\Support\Carbon;
22
 use Illuminate\Support\Carbon;
22
 
23
 
23
 #[ObservedBy(AccountObserver::class)]
24
 #[ObservedBy(AccountObserver::class)]
41
         'description',
42
         'description',
42
         'archived',
43
         'archived',
43
         'default',
44
         'default',
44
-        'bank_account_id',
45
         'created_by',
45
         'created_by',
46
         'updated_by',
46
         'updated_by',
47
     ];
47
     ];
74
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
74
         return $this->belongsTo(Currency::class, 'currency_code', 'code');
75
     }
75
     }
76
 
76
 
77
-    public function bankAccount(): BelongsTo
77
+    public function bankAccount(): HasOne
78
     {
78
     {
79
-        return $this->belongsTo(BankAccount::class, 'bank_account_id');
79
+        return $this->hasOne(BankAccount::class, 'account_id');
80
     }
80
     }
81
 
81
 
82
     public function getLastTransactionDate(): ?string
82
     public function getLastTransactionDate(): ?string

+ 3
- 2
app/Models/Banking/BankAccount.php Zobrazit soubor

33
 
33
 
34
     protected $fillable = [
34
     protected $fillable = [
35
         'company_id',
35
         'company_id',
36
+        'account_id',
36
         'institution_id',
37
         'institution_id',
37
         'type',
38
         'type',
38
         'number',
39
         'number',
55
         return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
56
         return $this->hasOne(ConnectedBankAccount::class, 'bank_account_id');
56
     }
57
     }
57
 
58
 
58
-    public function account(): HasOne
59
+    public function account(): BelongsTo
59
     {
60
     {
60
-        return $this->hasOne(Account::class, 'bank_account_id');
61
+        return $this->belongsTo(Account::class, 'account_id');
61
     }
62
     }
62
 
63
 
63
     public function institution(): BelongsTo
64
     public function institution(): BelongsTo

+ 2
- 2
app/Services/AccountService.php Zobrazit soubor

226
             ->whereExists(function (\Illuminate\Database\Query\Builder $subQuery) {
226
             ->whereExists(function (\Illuminate\Database\Query\Builder $subQuery) {
227
                 $subQuery->select(DB::raw(1))
227
                 $subQuery->select(DB::raw(1))
228
                     ->from('journal_entries as je')
228
                     ->from('journal_entries as je')
229
-                    ->join('accounts as bank_accounts', 'bank_accounts.id', '=', 'je.account_id')
230
-                    ->whereNotNull('bank_accounts.bank_account_id')
229
+                    ->join('bank_accounts', 'bank_accounts.account_id', '=', 'je.account_id') // Join bank_accounts on account_id
230
+                    ->whereNotNull('bank_accounts.id') // Ensure there is a linked BankAccount
231
                     ->whereColumn('je.transaction_id', 'journal_entries.transaction_id');
231
                     ->whereColumn('je.transaction_id', 'journal_entries.transaction_id');
232
             })
232
             })
233
             ->groupBy('accounts.id')
233
             ->groupBy('accounts.id')

+ 11
- 9
app/Services/ChartOfAccountsService.php Zobrazit soubor

48
     {
48
     {
49
         if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
49
         if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
50
             $baseCode = $subtypeConfig['base_code'];
50
             $baseCode = $subtypeConfig['base_code'];
51
-
52
             $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
51
             $defaultCurrencyCode = CurrencyAccessor::getDefaultCurrency();
53
 
52
 
54
             if (empty($defaultCurrencyCode)) {
53
             if (empty($defaultCurrencyCode)) {
56
             }
55
             }
57
 
56
 
58
             foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
57
             foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
59
-                $bankAccount = null;
60
-
61
-                if ($subtypeConfig['multi_currency'] && isset($subtypeConfig['bank_account_type'])) {
62
-                    $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
63
-                }
64
-
65
-                $company->accounts()->createQuietly([
66
-                    'bank_account_id' => $bankAccount?->id,
58
+                // Create the Account without directly setting bank_account_id
59
+                $account = $company->accounts()->createQuietly([
67
                     'subtype_id' => $subtype->id,
60
                     'subtype_id' => $subtype->id,
68
                     'category' => $subtype->type->getCategory()->value,
61
                     'category' => $subtype->type->getCategory()->value,
69
                     'type' => $subtype->type->value,
62
                     'type' => $subtype->type->value,
75
                     'created_by' => $company->owner->id,
68
                     'created_by' => $company->owner->id,
76
                     'updated_by' => $company->owner->id,
69
                     'updated_by' => $company->owner->id,
77
                 ]);
70
                 ]);
71
+
72
+                // Check if we need to create a BankAccount for this Account
73
+                if ($subtypeConfig['multi_currency'] && isset($subtypeConfig['bank_account_type'])) {
74
+                    $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
75
+
76
+                    // Associate the BankAccount with the Account
77
+                    $bankAccount->account()->associate($account);
78
+                    $bankAccount->saveQuietly();
79
+                }
78
             }
80
             }
79
         }
81
         }
80
     }
82
     }

+ 19
- 11
database/factories/Accounting/AccountFactory.php Zobrazit soubor

39
 
39
 
40
     public function withBankAccount(string $name): static
40
     public function withBankAccount(string $name): static
41
     {
41
     {
42
-        return $this->state(function (array $attributes) use ($name) {
43
-            $bankAccount = BankAccount::factory()->create();
42
+        return $this->afterCreating(function (Account $account) use ($name) {
44
             $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
43
             $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
45
 
44
 
46
-            return [
47
-                'bank_account_id' => $bankAccount->id,
45
+            // Create and associate a BankAccount with the Account
46
+            $bankAccount = BankAccount::factory()->create([
47
+                'account_id' => $account->id, // Associate with Account
48
+            ]);
49
+
50
+            // Update the Account with the subtype and name
51
+            $account->update([
48
                 'subtype_id' => $accountSubtype->id,
52
                 'subtype_id' => $accountSubtype->id,
49
                 'name' => $name,
53
                 'name' => $name,
50
-            ];
54
+            ]);
51
         });
55
         });
52
     }
56
     }
53
 
57
 
54
     public function withForeignBankAccount(string $name, string $currencyCode, float $rate): static
58
     public function withForeignBankAccount(string $name, string $currencyCode, float $rate): static
55
     {
59
     {
56
-        return $this->state(function (array $attributes) use ($currencyCode, $rate, $name) {
57
-            $currency = Currency::factory()->forCurrency($currencyCode, $rate)->create();
58
-            $bankAccount = BankAccount::factory()->create();
60
+        return $this->afterCreating(function (Account $account) use ($currencyCode, $rate, $name) {
59
             $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
61
             $accountSubtype = AccountSubtype::where('name', 'Cash and Cash Equivalents')->first();
60
 
62
 
61
-            return [
62
-                'bank_account_id' => $bankAccount->id,
63
+            // Create the Currency and BankAccount
64
+            $currency = Currency::factory()->forCurrency($currencyCode, $rate)->create();
65
+            $bankAccount = BankAccount::factory()->create([
66
+                'account_id' => $account->id, // Associate with Account
67
+            ]);
68
+
69
+            // Update the Account with the subtype, name, and currency code
70
+            $account->update([
63
                 'subtype_id' => $accountSubtype->id,
71
                 'subtype_id' => $accountSubtype->id,
64
                 'name' => $name,
72
                 'name' => $name,
65
                 'currency_code' => $currency->code,
73
                 'currency_code' => $currency->code,
66
-            ];
74
+            ]);
67
         });
75
         });
68
     }
76
     }
69
 }
77
 }

+ 13
- 13
database/migrations/2023_09_03_100000_create_accounting_tables.php Zobrazit soubor

36
             $table->timestamps();
36
             $table->timestamps();
37
         });
37
         });
38
 
38
 
39
-        Schema::create('bank_accounts', function (Blueprint $table) {
40
-            $table->id();
41
-            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
42
-            $table->foreignId('institution_id')->nullable()->constrained('institutions')->nullOnDelete();
43
-            $table->string('type')->default(BankAccountType::DEFAULT);
44
-            $table->string('number', 20)->nullable();
45
-            $table->boolean('enabled')->default(true);
46
-            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
47
-            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
48
-            $table->timestamps();
49
-        });
50
-
51
         Schema::create('accounts', function (Blueprint $table) {
39
         Schema::create('accounts', function (Blueprint $table) {
52
             $table->id();
40
             $table->id();
53
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
41
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
61
             $table->text('description')->nullable();
49
             $table->text('description')->nullable();
62
             $table->boolean('archived')->default(false);
50
             $table->boolean('archived')->default(false);
63
             $table->boolean('default')->default(false);
51
             $table->boolean('default')->default(false);
64
-            $table->foreignId('bank_account_id')->nullable()->constrained('bank_accounts')->cascadeOnDelete();
65
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
52
             $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
66
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
53
             $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
67
             $table->timestamps();
54
             $table->timestamps();
69
             $table->unique(['company_id', 'code']);
56
             $table->unique(['company_id', 'code']);
70
         });
57
         });
71
 
58
 
59
+        Schema::create('bank_accounts', function (Blueprint $table) {
60
+            $table->id();
61
+            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
62
+            $table->foreignId('account_id')->nullable()->constrained('accounts')->nullOnDelete();
63
+            $table->foreignId('institution_id')->nullable()->constrained('institutions')->nullOnDelete();
64
+            $table->string('type')->default(BankAccountType::DEFAULT);
65
+            $table->string('number', 20)->nullable();
66
+            $table->boolean('enabled')->default(true);
67
+            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
68
+            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
69
+            $table->timestamps();
70
+        });
71
+
72
         Schema::create('connected_bank_accounts', function (Blueprint $table) {
72
         Schema::create('connected_bank_accounts', function (Blueprint $table) {
73
             $table->id();
73
             $table->id();
74
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
74
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();

+ 7
- 3
database/migrations/2024_11_13_225714_create_adjustmentables_table.php Zobrazit soubor

13
     {
13
     {
14
         Schema::create('adjustmentables', function (Blueprint $table) {
14
         Schema::create('adjustmentables', function (Blueprint $table) {
15
             $table->id();
15
             $table->id();
16
-            $table->foreignId('adjustment_id')->constrained()->cascadeOnDelete();
17
-            $table->string('adjustment_type');
18
-            $table->morphs('adjustmentable');
16
+            $table->unsignedBigInteger('adjustment_id');   // No foreign key constraint due to polymorphism
17
+            $table->string('adjustment_type');             // Type of adjustment (e.g., "App\Models\Tax" or "App\Models\Discount")
18
+            $table->morphs('adjustmentable');              // Creates adjustmentable_id and adjustmentable_type
19
             $table->timestamps();
19
             $table->timestamps();
20
+
21
+            // Optional indexes for efficient querying
22
+            $table->index(['adjustment_id', 'adjustment_type']);
23
+            $table->index(['adjustmentable_id', 'adjustmentable_type']);
20
         });
24
         });
21
     }
25
     }
22
 
26
 

+ 6
- 0
tests/Feature/Accounting/TransactionTest.php Zobrazit soubor

132
         ->withForeignBankAccount('CAD Bank Account', 'CAD', 1.36)
132
         ->withForeignBankAccount('CAD Bank Account', 'CAD', 1.36)
133
         ->create();
133
         ->create();
134
 
134
 
135
+    $foreignBankAccount->refresh();
136
+
135
     ConfigureCurrencies::syncCurrencies();
137
     ConfigureCurrencies::syncCurrencies();
136
 
138
 
137
     // Create a transfer of 1500 CAD from the foreign bank account to USD bank account
139
     // Create a transfer of 1500 CAD from the foreign bank account to USD bank account
164
         ->withForeignBankAccount('BHD Bank Account', 'BHD', 0.38)
166
         ->withForeignBankAccount('BHD Bank Account', 'BHD', 0.38)
165
         ->create();
167
         ->create();
166
 
168
 
169
+    $foreignBankAccount->refresh();
170
+
167
     ConfigureCurrencies::syncCurrencies();
171
     ConfigureCurrencies::syncCurrencies();
168
 
172
 
169
     // Create a deposit of 1500 BHD to the foreign bank account
173
     // Create a deposit of 1500 BHD to the foreign bank account
195
         ->withForeignBankAccount('Foreign Bank Account', 'GBP', 0.76) // GBP account
199
         ->withForeignBankAccount('Foreign Bank Account', 'GBP', 0.76) // GBP account
196
         ->create();
200
         ->create();
197
 
201
 
202
+    $foreignBankAccount->refresh();
203
+
198
     ConfigureCurrencies::syncCurrencies();
204
     ConfigureCurrencies::syncCurrencies();
199
 
205
 
200
     /** @var Transaction $transaction */
206
     /** @var Transaction $transaction */

Načítá se…
Zrušit
Uložit