Parcourir la source

refactor: Service logic

3.x
wallo il y a 1 an
Parent
révision
dd1a095108
73 fichiers modifiés avec 2506 ajouts et 1927 suppressions
  1. 39
    0
      app/DTO/AccountBalanceDTO.php
  2. 33
    0
      app/DTO/AccountBalanceReportDTO.php
  3. 33
    0
      app/DTO/AccountCategoryDTO.php
  4. 33
    0
      app/DTO/AccountDTO.php
  5. 17
    27
      app/Enums/Accounting/AccountCategory.php
  6. 0
    166
      app/Enums/BankAccountSubtype.php
  7. 0
    87
      app/Enums/BankAccountType.php
  8. 0
    18
      app/Enums/CategoryType.php
  9. 18
    0
      app/Filament/Company/Pages/Accounting/AccountChart.php
  10. 52
    0
      app/Filament/Company/Pages/Reports.php
  11. 263
    0
      app/Filament/Company/Pages/Reports/AccountBalances.php
  12. 0
    22
      app/Filament/Company/Pages/Setting/CompanyDefault.php
  13. 31
    23
      app/Filament/Company/Pages/Setting/Localization.php
  14. 82
    24
      app/Filament/Company/Resources/Accounting/TransactionResource.php
  15. 72
    0
      app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ManageTransaction.php
  16. 80
    80
      app/Filament/Company/Resources/Banking/AccountResource.php
  17. 0
    188
      app/Filament/Company/Resources/Setting/CategoryResource.php
  18. 0
    52
      app/Filament/Company/Resources/Setting/CategoryResource/Pages/ManageCategory.php
  19. 0
    4
      app/Filament/Company/Resources/Setting/DiscountResource.php
  20. 19
    0
      app/Infolists/Components/ReportEntry.php
  21. 0
    16
      app/Listeners/ConfigureChartOfAccounts.php
  22. 0
    2
      app/Listeners/ConfigureCompanyNavigation.php
  23. 13
    15
      app/Listeners/HandleTransactionImport.php
  24. 0
    2
      app/Listeners/SyncAssociatedModels.php
  25. 0
    14
      app/Listeners/SyncWithCompanyDefaults.php
  26. 15
    1
      app/Livewire/Company/Service/ConnectedAccount/ListInstitutions.php
  27. 2
    27
      app/Models/Accounting/Account.php
  28. 9
    10
      app/Models/Accounting/Transaction.php
  29. 14
    0
      app/Models/Banking/ConnectedBankAccount.php
  30. 5
    6
      app/Models/Company.php
  31. 0
    79
      app/Models/Setting/Category.php
  32. 0
    15
      app/Models/Setting/CompanyDefault.php
  33. 21
    4
      app/Models/Setting/Localization.php
  34. 3
    57
      app/Observers/JournalEntryObserver.php
  35. 57
    2
      app/Observers/TransactionObserver.php
  36. 0
    1
      app/Providers/AuthServiceProvider.php
  37. 1
    2
      app/Providers/Filament/AdminPanelProvider.php
  38. 35
    0
      app/Repositories/Accounting/AccountSubtypeRepository.php
  39. 35
    0
      app/Repositories/Accounting/JournalEntryRepository.php
  40. 35
    0
      app/Repositories/Banking/ConnectedBankAccountRepository.php
  41. 42
    0
      app/Repositories/Setting/CurrencyRepository.php
  42. 76
    0
      app/Services/AccountBalancesExportService.php
  43. 179
    58
      app/Services/AccountService.php
  44. 0
    30
      app/Services/BankAccountService.php
  45. 0
    45
      app/Services/CompanyDefaultService.php
  46. 50
    0
      app/Services/ConnectedBankAccountService.php
  47. 15
    3
      app/Services/CurrencyService.php
  48. 0
    58
      app/Services/PlaidService.php
  49. 41
    107
      app/Services/TransactionService.php
  50. 0
    123
      app/Utilities/Plaid/AccountTypeMapper.php
  51. 25
    0
      app/ValueObjects/BalanceValue.php
  52. 2
    0
      composer.json
  53. 715
    333
      composer.lock
  54. 0
    12
      config/chart-of-accounts.php
  55. 2
    1
      database/factories/Setting/AppearanceFactory.php
  56. 0
    59
      database/factories/Setting/CategoryFactory.php
  57. 0
    52
      database/factories/Setting/CompanyDefaultFactory.php
  58. 2
    2
      database/factories/Setting/LocalizationFactory.php
  59. 0
    5
      database/migrations/2023_09_03_100000_create_accounting_tables.php
  60. 0
    38
      database/migrations/2023_09_07_193412_create_categories_table.php
  61. 0
    2
      database/migrations/2023_09_08_040159_create_company_defaults_table.php
  62. 2
    2
      database/migrations/2023_10_11_210415_create_localizations_table.php
  63. 1
    1
      database/migrations/2024_01_01_234943_create_transactions_table.php
  64. 31
    28
      package-lock.json
  65. 16
    0
      resources/css/filament/company/theme.css
  66. 2
    3
      resources/data/lang/en.json
  67. 136
    0
      resources/views/components/company/reports/account-balances.blade.php
  68. 38
    0
      resources/views/components/report-entry.blade.php
  69. 26
    19
      resources/views/filament/company/pages/accounting/chart.blade.php
  70. 3
    0
      resources/views/filament/company/pages/reports.blade.php
  71. 75
    0
      resources/views/filament/company/pages/reports/account-balances.blade.php
  72. 8
    0
      resources/views/infolists/components/report-entry.blade.php
  73. 2
    2
      resources/views/livewire/company/service/connected-account/list-institutions.blade.php

+ 39
- 0
app/DTO/AccountBalanceDTO.php Voir le fichier

@@ -0,0 +1,39 @@
1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use Livewire\Wireable;
6
+
7
+class AccountBalanceDTO implements Wireable
8
+{
9
+    public function __construct(
10
+        public ?string $startingBalance,
11
+        public string $debitBalance,
12
+        public string $creditBalance,
13
+        public ?string $netMovement,
14
+        public ?string $endingBalance,
15
+    ) {
16
+    }
17
+
18
+    public function toLivewire(): array
19
+    {
20
+        return [
21
+            'startingBalance' => $this->startingBalance,
22
+            'debitBalance' => $this->debitBalance,
23
+            'creditBalance' => $this->creditBalance,
24
+            'netMovement' => $this->netMovement,
25
+            'endingBalance' => $this->endingBalance,
26
+        ];
27
+    }
28
+
29
+    public static function fromLivewire($value): static
30
+    {
31
+        return new static(
32
+            $value['startingBalance'],
33
+            $value['debitBalance'],
34
+            $value['creditBalance'],
35
+            $value['netMovement'],
36
+            $value['endingBalance'],
37
+        );
38
+    }
39
+}

+ 33
- 0
app/DTO/AccountBalanceReportDTO.php Voir le fichier

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use Livewire\Wireable;
6
+
7
+class AccountBalanceReportDTO implements Wireable
8
+{
9
+    public function __construct(
10
+        /**
11
+         * @var AccountCategoryDTO[]
12
+         */
13
+        public array $categories,
14
+        public AccountBalanceDTO $overallTotal,
15
+    ) {
16
+    }
17
+
18
+    public function toLivewire(): array
19
+    {
20
+        return [
21
+            'categories' => $this->categories,
22
+            'overallTotal' => $this->overallTotal->toLivewire(),
23
+        ];
24
+    }
25
+
26
+    public static function fromLivewire($value): static
27
+    {
28
+        return new static(
29
+            $value['categories'],
30
+            AccountBalanceDTO::fromLivewire($value['overallTotal']),
31
+        );
32
+    }
33
+}

+ 33
- 0
app/DTO/AccountCategoryDTO.php Voir le fichier

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use Livewire\Wireable;
6
+
7
+class AccountCategoryDTO implements Wireable
8
+{
9
+    /**
10
+     * @param  AccountDTO[]  $accounts
11
+     */
12
+    public function __construct(
13
+        public array $accounts,
14
+        public AccountBalanceDTO $summary,
15
+    ) {
16
+    }
17
+
18
+    public function toLivewire(): array
19
+    {
20
+        return [
21
+            'accounts' => $this->accounts,
22
+            'summary' => $this->summary->toLivewire(),
23
+        ];
24
+    }
25
+
26
+    public static function fromLivewire($value): static
27
+    {
28
+        return new static(
29
+            $value['accounts'],
30
+            AccountBalanceDTO::fromLivewire($value['summary']),
31
+        );
32
+    }
33
+}

+ 33
- 0
app/DTO/AccountDTO.php Voir le fichier

@@ -0,0 +1,33 @@
1
+<?php
2
+
3
+namespace App\DTO;
4
+
5
+use Livewire\Wireable;
6
+
7
+class AccountDTO implements Wireable
8
+{
9
+    public function __construct(
10
+        public string $accountName,
11
+        public string $accountCode,
12
+        public AccountBalanceDTO $balance,
13
+    ) {
14
+    }
15
+
16
+    public function toLivewire(): array
17
+    {
18
+        return [
19
+            'accountName' => $this->accountName,
20
+            'accountCode' => $this->accountCode,
21
+            'balance' => $this->balance->toLivewire(),
22
+        ];
23
+    }
24
+
25
+    public static function fromLivewire($value): static
26
+    {
27
+        return new static(
28
+            $value['accountName'],
29
+            $value['accountCode'],
30
+            AccountBalanceDTO::fromLivewire($value['balance']),
31
+        );
32
+    }
33
+}

+ 17
- 27
app/Enums/Accounting/AccountCategory.php Voir le fichier

@@ -17,35 +17,25 @@ enum AccountCategory: string implements HasLabel
17 17
         return $this->name;
18 18
     }
19 19
 
20
-    public function getTypes(): array
20
+    public function getPluralLabel(): ?string
21 21
     {
22 22
         return match ($this) {
23
-            self::Asset => [
24
-                AccountType::CurrentAsset,
25
-                AccountType::NonCurrentAsset,
26
-                AccountType::ContraAsset,
27
-            ],
28
-            self::Liability => [
29
-                AccountType::CurrentLiability,
30
-                AccountType::NonCurrentLiability,
31
-                AccountType::ContraLiability,
32
-            ],
33
-            self::Equity => [
34
-                AccountType::Equity,
35
-                AccountType::ContraEquity,
36
-            ],
37
-            self::Revenue => [
38
-                AccountType::OperatingRevenue,
39
-                AccountType::NonOperatingRevenue,
40
-                AccountType::ContraRevenue,
41
-                AccountType::UncategorizedRevenue,
42
-            ],
43
-            self::Expense => [
44
-                AccountType::OperatingExpense,
45
-                AccountType::NonOperatingExpense,
46
-                AccountType::ContraExpense,
47
-                AccountType::UncategorizedExpense,
48
-            ],
23
+            self::Asset => 'Assets',
24
+            self::Liability => 'Liabilities',
25
+            self::Equity => 'Equity',
26
+            self::Revenue => 'Revenue',
27
+            self::Expense => 'Expenses',
49 28
         };
50 29
     }
30
+
31
+    public static function fromPluralLabel(string $label): ?self
32
+    {
33
+        foreach (self::cases() as $case) {
34
+            if ($case->getPluralLabel() === $label) {
35
+                return $case;
36
+            }
37
+        }
38
+
39
+        return null;
40
+    }
51 41
 }

+ 0
- 166
app/Enums/BankAccountSubtype.php Voir le fichier

@@ -1,166 +0,0 @@
1
-<?php
2
-
3
-namespace App\Enums;
4
-
5
-use Filament\Support\Contracts\HasLabel;
6
-
7
-enum BankAccountSubtype: string implements HasLabel
8
-{
9
-    // Depository Types
10
-
11
-    case Checking = 'checking';
12
-    case Savings = 'savings';
13
-    case HealthSavingsAccountCash = 'cash_hsa';
14
-    case CertificateOfDeposit = 'cd';
15
-    case MoneyMarket = 'money_market';
16
-    case Paypal = 'paypal';
17
-    case Prepaid = 'prepaid';
18
-    case CashManagement = 'cash_management';
19
-    case ElectronicBenefitsTransfer = 'ebt';
20
-
21
-    // Credit Types
22
-    case CreditCard = 'credit_card';
23
-    case PaypalCredit = 'paypal_credit';
24
-
25
-    // Loan Types
26
-    case Auto = 'auto';
27
-    case Business = 'business';
28
-    case Commercial = 'commercial';
29
-    case Construction = 'construction';
30
-    case Consumer = 'consumer';
31
-    case HomeEquity = 'home_equity';
32
-    case Loan = 'loan'; // Generic loan
33
-    case Mortgage = 'mortgage';
34
-    case Overdraft = 'overdraft';
35
-    case LineOfCredit = 'line_of_credit'; // Pre-approved line of credit
36
-    case Student = 'student';
37
-    case Other = 'other';
38
-
39
-    // Investment Types
40
-    case CollegeSavings529 = '529';
41
-    case Retirement401a = '401a';
42
-    case Retirement401K = '401k';
43
-    case Retirement403b = '403b';
44
-    case DeferredCompensation457b = '457b';
45
-    case Brokerage = 'brokerage';
46
-    case CashIndividualSavingsAccount = 'cash_isa';
47
-    case CryptoCurrencyExchange = 'crypto_exchange';
48
-    case EducationSavingsAccount = 'esa';
49
-    case FixedAnnuity = 'fixed_annuity';
50
-    case GuaranteedInvestmentCertificate = 'gic';
51
-    case HealthReimbursementArrangement = 'hra';
52
-    case HealthSavingsAccountNonCash = 'non_cash_hsa';
53
-    case IndividualRetirementAccount = 'ira';
54
-    case IndividualSavingsAccount = 'isa';
55
-    case KeoghPlan = 'keogh';
56
-    case LifeIncomeFund = 'lif';
57
-    case LifeInsuranceAccount = 'life_insurance';
58
-    case LockedInRetirementAccount = 'lira'; // Instead of LIRA
59
-    case LockedInRetirementIncomeFund = 'lrif'; // Instead of LRIF
60
-    case LockedInRetirementSavingsPlan = 'lrsp'; // Instead of LRSP
61
-    case MutualFundAccount = 'mutual_fund'; // Instead of MutualFund
62
-    case CryptoCurrencyWallet = 'non_custodial_wallet'; // Instead of NonCustodialWallet
63
-    case NonTaxableBrokerageAccount = 'non_taxable_brokerage_account'; // Instead of NonTaxableBrokerageAccount
64
-    case AnnuityAccountOther = 'other_annuity'; // Instead of OtherAnnuity
65
-    case InsuranceAccountOther = 'other_insurance'; // Instead of OtherInsurance
66
-    case PensionAccount = 'pension'; // Instead of Pension
67
-    case PrescribedRetirementIncomeFund = 'prif'; // Instead of PRIF
68
-    case ProfitSharingPlanAccount = 'profit_sharing_plan'; // Instead of ProfitSharingPlan
69
-    case QualifyingShareAccount = 'qshr'; // Instead of QSHR
70
-    case RegisteredDisabilitySavingsPlan = 'rdsp'; // Instead of RDSP
71
-    case RegisteredEducationSavingsPlan = 'resp'; // Instead of RESP
72
-    case RetirementAccountOther = 'retirement'; // Instead of Retirement
73
-    case RestrictedLifeIncomeFund = 'rlif'; // Instead of RLIF
74
-    case RothIRA = 'roth'; // Instead of Roth
75
-    case Roth401k = 'roth_401k'; // Instead of RothFourOhOneK
76
-    case RegisteredRetirementIncomeFund = 'rrif'; // Instead of RRIF
77
-    case RegisteredRetirementSavingsPlan = 'rrsp'; // Instead of RRSP
78
-    case SalaryReductionSEPPlan = 'sarsep'; // Instead of SARSEP
79
-    case SimplifiedEmployeePensionIRA = 'sep_ira'; // Instead of SEPIRA
80
-    case SavingsIncentiveMatchPlanForEmployeesIRA = 'simple_ira'; // Instead of SIMPLEIRA
81
-    case SelfInvestedPersonalPension = 'sipp'; // Instead of SIPP
82
-    case StockPlanAccount = 'stock_plan'; // Instead of StockPlan
83
-    case TaxFreeSavingsAccount = 'tfsa'; // Instead of TFSA
84
-    case TrustAccount = 'trust'; // Instead of Trust
85
-    case UniformGiftToMinorsAct = 'ugma'; // Instead of UGMA
86
-    case UniformTransfersToMinorsAct = 'utma'; // Instead of UTMA
87
-    case VariableAnnuityAccount = 'variable_annuity'; // Instead of VariableAnnuity
88
-
89
-    public function getLabel(): ?string
90
-    {
91
-        $label = match ($this) {
92
-            self::Checking => 'Checking',
93
-            self::Savings => 'Savings',
94
-            self::HealthSavingsAccountCash => 'Health Savings Account (Cash)',
95
-            self::CertificateOfDeposit => 'Certificate of Deposit',
96
-            self::MoneyMarket => 'Money Market',
97
-            self::Paypal => 'PayPal',
98
-            self::Prepaid => 'Prepaid',
99
-            self::CashManagement => 'Cash Management',
100
-            self::ElectronicBenefitsTransfer => 'Electronic Benefits Transfer (EBT)',
101
-            self::CreditCard => 'Credit Card',
102
-            self::PaypalCredit => 'PayPal Credit',
103
-            self::Auto => 'Auto',
104
-            self::Business => 'Business',
105
-            self::Commercial => 'Commercial',
106
-            self::Construction => 'Construction',
107
-            self::Consumer => 'Consumer',
108
-            self::HomeEquity => 'Home Equity',
109
-            self::Loan => 'Loan',
110
-            self::Mortgage => 'Mortgage',
111
-            self::Overdraft => 'Overdraft',
112
-            self::LineOfCredit => 'Line of Credit',
113
-            self::Student => 'Student',
114
-            self::Other => 'Other',
115
-            self::CollegeSavings529 => '529 College Savings Plan',
116
-            self::Retirement401a => '401(a)',
117
-            self::Retirement401K => '401(k)',
118
-            self::Retirement403b => '403(b)',
119
-            self::DeferredCompensation457b => '457(b)',
120
-            self::Brokerage => 'Brokerage',
121
-            self::CashIndividualSavingsAccount => 'Cash Individual Savings Account (ISA)',
122
-            self::CryptoCurrencyExchange => 'Crypto Currency Exchange',
123
-            self::EducationSavingsAccount => 'Education Savings Account (ESA)',
124
-            self::FixedAnnuity => 'Fixed Annuity',
125
-            self::GuaranteedInvestmentCertificate => 'Guaranteed Investment Certificate (GIC)',
126
-            self::HealthSavingsAccountNonCash => 'Health Savings Account (Non-Cash)',
127
-            self::IndividualRetirementAccount => 'Individual Retirement Account (IRA)',
128
-            self::IndividualSavingsAccount => 'Individual Savings Account (ISA)',
129
-            self::KeoghPlan => 'Keogh Plan',
130
-            self::LifeIncomeFund => 'Life Income Fund (LIF)',
131
-            self::LifeInsuranceAccount => 'Life Insurance Account',
132
-            self::LockedInRetirementAccount => 'Locked-In Retirement Account (LIRA)',
133
-            self::LockedInRetirementIncomeFund => 'Locked-In Retirement Income Fund (LRIF)',
134
-            self::LockedInRetirementSavingsPlan => 'Locked-In Retirement Savings Plan (LRSP)',
135
-            self::MutualFundAccount => 'Mutual Fund Account',
136
-            self::CryptoCurrencyWallet => 'Non-Custodial Wallet',
137
-            self::NonTaxableBrokerageAccount => 'Non-Taxable Brokerage Account',
138
-            self::AnnuityAccountOther => 'Other Annuity',
139
-            self::InsuranceAccountOther => 'Other Insurance',
140
-            self::PensionAccount => 'Pension',
141
-            self::PrescribedRetirementIncomeFund => 'Prescribed Retirement Income Fund (PRIF)',
142
-            self::ProfitSharingPlanAccount => 'Profit Sharing Plan',
143
-            self::QualifyingShareAccount => 'Qualifying Share Account (QSHR)',
144
-            self::RegisteredDisabilitySavingsPlan => 'Registered Disability Savings Plan (RDSP)',
145
-            self::RegisteredEducationSavingsPlan => 'Registered Education Savings Plan (RESP)',
146
-            self::RetirementAccountOther => 'Retirement',
147
-            self::RestrictedLifeIncomeFund => 'Restricted Life Income Fund (RLIF)',
148
-            self::RothIRA => 'Roth IRA',
149
-            self::Roth401k => 'Roth 401(k)',
150
-            self::RegisteredRetirementIncomeFund => 'Registered Retirement Income Fund (RRIF)',
151
-            self::RegisteredRetirementSavingsPlan => 'Registered Retirement Savings Plan (RRSP)',
152
-            self::SalaryReductionSEPPlan => 'Salary Reduction SEP Plan (SARSEP)',
153
-            self::SimplifiedEmployeePensionIRA => 'Simplified Employee Pension IRA (SEP IRA)',
154
-            self::SavingsIncentiveMatchPlanForEmployeesIRA => 'Savings Incentive Match Plan for Employees IRA (SIMPLE IRA)',
155
-            self::SelfInvestedPersonalPension => 'Self-Invested Personal Pension (SIPP)',
156
-            self::StockPlanAccount => 'Stock Plan',
157
-            self::TaxFreeSavingsAccount => 'Tax-Free Savings Account (TFSA)',
158
-            self::TrustAccount => 'Trust',
159
-            self::UniformGiftToMinorsAct => 'Uniform Gift to Minors Act (UGMA)',
160
-            self::UniformTransfersToMinorsAct => 'Uniform Transfers to Minors Act (UTMA)',
161
-            self::VariableAnnuityAccount => 'Variable Annuity',
162
-        };
163
-
164
-        return $label;
165
-    }
166
-}

+ 0
- 87
app/Enums/BankAccountType.php Voir le fichier

@@ -18,91 +18,4 @@ enum BankAccountType: string implements HasLabel
18 18
     {
19 19
         return translate($this->name);
20 20
     }
21
-
22
-    public function getSubtypes(): array
23
-    {
24
-        return match ($this) {
25
-            self::Depository => [
26
-                BankAccountSubtype::Checking,
27
-                BankAccountSubtype::Savings,
28
-                BankAccountSubtype::HealthSavingsAccountCash,
29
-                BankAccountSubtype::CertificateOfDeposit,
30
-                BankAccountSubtype::MoneyMarket,
31
-                BankAccountSubtype::Paypal,
32
-                BankAccountSubtype::Prepaid,
33
-                BankAccountSubtype::CashManagement,
34
-                BankAccountSubtype::ElectronicBenefitsTransfer,
35
-            ],
36
-            self::Credit => [
37
-                BankAccountSubtype::CreditCard,
38
-                BankAccountSubtype::PaypalCredit,
39
-            ],
40
-            self::Loan => [
41
-                BankAccountSubtype::Auto,
42
-                BankAccountSubtype::Business,
43
-                BankAccountSubtype::Commercial,
44
-                BankAccountSubtype::Construction,
45
-                BankAccountSubtype::Consumer,
46
-                BankAccountSubtype::HomeEquity,
47
-                BankAccountSubtype::Loan,
48
-                BankAccountSubtype::Mortgage,
49
-                BankAccountSubtype::Overdraft,
50
-                BankAccountSubtype::LineOfCredit,
51
-                BankAccountSubtype::Student,
52
-                BankAccountSubtype::Other,
53
-            ],
54
-            self::Investment => [
55
-                BankAccountSubtype::CollegeSavings529,
56
-                BankAccountSubtype::Retirement401a,
57
-                BankAccountSubtype::Retirement401K,
58
-                BankAccountSubtype::Retirement403b,
59
-                BankAccountSubtype::DeferredCompensation457b,
60
-                BankAccountSubtype::Brokerage,
61
-                BankAccountSubtype::CashIndividualSavingsAccount,
62
-                BankAccountSubtype::CryptoCurrencyExchange,
63
-                BankAccountSubtype::EducationSavingsAccount,
64
-                BankAccountSubtype::FixedAnnuity,
65
-                BankAccountSubtype::GuaranteedInvestmentCertificate,
66
-                BankAccountSubtype::HealthSavingsAccountNonCash,
67
-                BankAccountSubtype::IndividualRetirementAccount,
68
-                BankAccountSubtype::IndividualSavingsAccount,
69
-                BankAccountSubtype::KeoghPlan,
70
-                BankAccountSubtype::LifeIncomeFund,
71
-                BankAccountSubtype::LifeInsuranceAccount,
72
-                BankAccountSubtype::LockedInRetirementAccount,
73
-                BankAccountSubtype::LockedInRetirementIncomeFund,
74
-                BankAccountSubtype::LockedInRetirementSavingsPlan,
75
-                BankAccountSubtype::MutualFundAccount,
76
-                BankAccountSubtype::CryptoCurrencyWallet,
77
-                BankAccountSubtype::NonTaxableBrokerageAccount,
78
-                BankAccountSubtype::AnnuityAccountOther,
79
-                BankAccountSubtype::InsuranceAccountOther,
80
-                BankAccountSubtype::PensionAccount,
81
-                BankAccountSubtype::PrescribedRetirementIncomeFund,
82
-                BankAccountSubtype::ProfitSharingPlanAccount,
83
-                BankAccountSubtype::QualifyingShareAccount,
84
-                BankAccountSubtype::RegisteredDisabilitySavingsPlan,
85
-                BankAccountSubtype::RegisteredEducationSavingsPlan,
86
-                BankAccountSubtype::RetirementAccountOther,
87
-                BankAccountSubtype::RestrictedLifeIncomeFund,
88
-                BankAccountSubtype::RothIRA,
89
-                BankAccountSubtype::Roth401k,
90
-                BankAccountSubtype::RegisteredRetirementIncomeFund,
91
-                BankAccountSubtype::RegisteredRetirementSavingsPlan,
92
-                BankAccountSubtype::SalaryReductionSEPPlan,
93
-                BankAccountSubtype::SimplifiedEmployeePensionIRA,
94
-                BankAccountSubtype::SavingsIncentiveMatchPlanForEmployeesIRA,
95
-                BankAccountSubtype::SelfInvestedPersonalPension,
96
-                BankAccountSubtype::StockPlanAccount,
97
-                BankAccountSubtype::TaxFreeSavingsAccount,
98
-                BankAccountSubtype::TrustAccount,
99
-                BankAccountSubtype::UniformGiftToMinorsAct,
100
-                BankAccountSubtype::UniformTransfersToMinorsAct,
101
-                BankAccountSubtype::VariableAnnuityAccount,
102
-            ],
103
-            self::Other => [
104
-                BankAccountSubtype::Other,
105
-            ],
106
-        };
107
-    }
108 21
 }

+ 0
- 18
app/Enums/CategoryType.php Voir le fichier

@@ -1,18 +0,0 @@
1
-<?php
2
-
3
-namespace App\Enums;
4
-
5
-use Filament\Support\Contracts\HasLabel;
6
-
7
-enum CategoryType: string implements HasLabel
8
-{
9
-    case Expense = 'expense';
10
-    case Income = 'income';
11
-    case Item = 'item';
12
-    case Other = 'other';
13
-
14
-    public function getLabel(): ?string
15
-    {
16
-        return translate($this->name);
17
-    }
18
-}

+ 18
- 0
app/Filament/Company/Pages/Accounting/AccountChart.php Voir le fichier

@@ -3,8 +3,10 @@
3 3
 namespace App\Filament\Company\Pages\Accounting;
4 4
 
5 5
 use App\Enums\Accounting\AccountCategory;
6
+use App\Models\Accounting\Account;
6 7
 use App\Models\Accounting\Account as ChartModel;
7 8
 use App\Models\Accounting\AccountSubtype;
9
+use App\Services\AccountService;
8 10
 use App\Utilities\Accounting\AccountCode;
9 11
 use App\Utilities\Currency\CurrencyAccessor;
10 12
 use Filament\Actions\Action;
@@ -39,11 +41,27 @@ class AccountChart extends Page
39 41
     #[Url]
40 42
     public ?string $activeTab = null;
41 43
 
44
+    protected AccountService $accountService;
45
+
46
+    public function boot(AccountService $accountService): void
47
+    {
48
+        $this->accountService = $accountService;
49
+    }
50
+
42 51
     public function mount(): void
43 52
     {
44 53
         $this->activeTab = $this->activeTab ?? AccountCategory::Asset->value;
45 54
     }
46 55
 
56
+    public function getAccountBalance(Account $account): ?string
57
+    {
58
+        $company = $account->company;
59
+        $startDate = $company->locale->fiscalYearStartDate();
60
+        $endDate = $company->locale->fiscalYearEndDate();
61
+
62
+        return $this->accountService->getEndingBalance($account, $startDate, $endDate)?->formatted();
63
+    }
64
+
47 65
     protected function configureAction(Action $action): void
48 66
     {
49 67
         $action

+ 52
- 0
app/Filament/Company/Pages/Reports.php Voir le fichier

@@ -0,0 +1,52 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages;
4
+
5
+use App\Filament\Company\Pages\Reports\AccountBalances;
6
+use App\Infolists\Components\ReportEntry;
7
+use Filament\Infolists\Components\Section;
8
+use Filament\Infolists\Infolist;
9
+use Filament\Pages\Page;
10
+use Filament\Support\Colors\Color;
11
+
12
+class Reports extends Page
13
+{
14
+    protected static ?string $navigationIcon = 'heroicon-o-document-text';
15
+
16
+    protected static string $view = 'filament.company.pages.reports';
17
+
18
+    public function reportsInfolist(Infolist $infolist): Infolist
19
+    {
20
+        return $infolist
21
+            ->state([])
22
+            ->schema([
23
+                Section::make('Detailed Reports')
24
+                    ->aside()
25
+                    ->description('Dig into the details of your business’s transactions, balances, and accounts.')
26
+                    ->extraAttributes(['class' => 'es-report-card'])
27
+                    ->schema([
28
+                        ReportEntry::make('account_balances')
29
+                            ->hiddenLabel()
30
+                            ->heading('Account Balances')
31
+                            ->description('Summary view of balances and activity for all accounts.')
32
+                            ->icon('heroicon-o-currency-dollar')
33
+                            ->iconColor(Color::Teal)
34
+                            ->url(AccountBalances::getUrl()),
35
+                        ReportEntry::make('trial_balance')
36
+                            ->hiddenLabel()
37
+                            ->heading('Trial Balance')
38
+                            ->description('The sum of all debit and credit balances for all accounts on a single day. This helps to ensure that the books are in balance.')
39
+                            ->icon('heroicon-o-scale')
40
+                            ->iconColor(Color::Sky)
41
+                            ->url('#'),
42
+                        ReportEntry::make('account_transactions')
43
+                            ->hiddenLabel()
44
+                            ->heading('Account Transactions')
45
+                            ->description('A record of all transactions for a company. The general ledger is the core of a company\'s financial records.')
46
+                            ->icon('heroicon-o-adjustments-horizontal')
47
+                            ->iconColor(Color::Amber)
48
+                            ->url('#'),
49
+                    ]),
50
+            ]);
51
+    }
52
+}

+ 263
- 0
app/Filament/Company/Pages/Reports/AccountBalances.php Voir le fichier

@@ -0,0 +1,263 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Pages\Reports;
4
+
5
+use App\DTO\AccountBalanceReportDTO;
6
+use App\Models\Company;
7
+use App\Services\AccountBalancesExportService;
8
+use App\Services\AccountService;
9
+use Barryvdh\DomPDF\Facade\Pdf;
10
+use Carbon\CarbonPeriod;
11
+use Filament\Actions\Action;
12
+use Filament\Actions\ActionGroup;
13
+use Filament\Forms\Components\DatePicker;
14
+use Filament\Forms\Components\Select;
15
+use Filament\Forms\Components\Split;
16
+use Filament\Forms\Form;
17
+use Filament\Forms\Set;
18
+use Filament\Pages\Page;
19
+use Filament\Support\Enums\IconPosition;
20
+use Filament\Support\Enums\IconSize;
21
+use Illuminate\Support\Carbon;
22
+use Symfony\Component\HttpFoundation\StreamedResponse;
23
+
24
+class AccountBalances extends Page
25
+{
26
+    protected static string $view = 'filament.company.pages.reports.account-balances';
27
+
28
+    protected static ?string $slug = 'reports/account-balances';
29
+
30
+    public string $startDate = '';
31
+
32
+    public string $endDate = '';
33
+
34
+    public string $dateRange = '';
35
+
36
+    public string $fiscalYearStartDate = '';
37
+
38
+    public string $fiscalYearEndDate = '';
39
+
40
+    public Company $company;
41
+
42
+    public AccountBalanceReportDTO $accountBalanceReport;
43
+
44
+    protected AccountService $accountService;
45
+
46
+    protected AccountBalancesExportService $accountBalancesExportService;
47
+
48
+    public function boot(AccountService $accountService, AccountBalancesExportService $accountBalancesExportService): void
49
+    {
50
+        $this->accountService = $accountService;
51
+        $this->accountBalancesExportService = $accountBalancesExportService;
52
+    }
53
+
54
+    public function mount(): void
55
+    {
56
+        $this->company = auth()->user()->currentCompany;
57
+        $this->fiscalYearStartDate = $this->company->locale->fiscalYearStartDate();
58
+        $this->fiscalYearEndDate = $this->company->locale->fiscalYearEndDate();
59
+        $this->dateRange = $this->getDefaultDateRange();
60
+        $this->updateDateRange($this->dateRange);
61
+
62
+        $this->loadAccountBalances();
63
+    }
64
+
65
+    public function getDefaultDateRange(): string
66
+    {
67
+        return 'FY-' . now()->year;
68
+    }
69
+
70
+    public function loadAccountBalances(): void
71
+    {
72
+        $startTime = microtime(true);
73
+        $this->accountBalanceReport = $this->accountService->buildAccountBalanceReport($this->startDate, $this->endDate);
74
+        $endTime = microtime(true);
75
+        $executionTime = ($endTime - $startTime);
76
+        info('Account balance report loaded in ' . $executionTime . ' seconds');
77
+    }
78
+
79
+    protected function getHeaderActions(): array
80
+    {
81
+        return [
82
+            ActionGroup::make([
83
+                Action::make('exportCSV')
84
+                    ->label('CSV')
85
+                    ->action(fn () => $this->exportCSV()),
86
+                Action::make('exportPDF')
87
+                    ->label('PDF')
88
+                    ->action(fn () => $this->exportPDF()),
89
+            ])
90
+                ->label('Export')
91
+                ->button()
92
+                ->outlined()
93
+                ->dropdownWidth('max-w-[7rem]')
94
+                ->dropdownPlacement('bottom-end')
95
+                ->icon('heroicon-c-chevron-down')
96
+                ->iconSize(IconSize::Small)
97
+                ->iconPosition(IconPosition::After),
98
+        ];
99
+    }
100
+
101
+    public function exportCSV(): StreamedResponse
102
+    {
103
+        return $this->accountBalancesExportService->exportToCsv($this->company, $this->accountBalanceReport, $this->startDate, $this->endDate);
104
+    }
105
+
106
+    public function exportPDF(): StreamedResponse
107
+    {
108
+        $pdf = Pdf::loadView('components.company.reports.account-balances', [
109
+            'accountBalanceReport' => $this->accountBalanceReport,
110
+            'startDate' => Carbon::parse($this->startDate)->format('M d, Y'),
111
+            'endDate' => Carbon::parse($this->endDate)->format('M d, Y'),
112
+        ])->setPaper('a4')->setOption(['defaultFont' => 'sans-serif', 'isPhpEnabled' => true]);
113
+
114
+        return response()->streamDownload(function () use ($pdf) {
115
+            echo $pdf->stream();
116
+        }, 'account-balances.pdf');
117
+    }
118
+
119
+    public function form(Form $form): Form
120
+    {
121
+        return $form
122
+            ->schema([
123
+                Split::make([
124
+                    Select::make('dateRange')
125
+                        ->label('Date Range')
126
+                        ->options($this->getDateRangeOptions())
127
+                        ->selectablePlaceholder(false)
128
+                        ->afterStateUpdated(function ($state) {
129
+                            $this->updateDateRange($state);
130
+                        }),
131
+                    DatePicker::make('startDate')
132
+                        ->label('Start Date')
133
+                        ->displayFormat('Y-m-d')
134
+                        ->afterStateUpdated(static function (Set $set) {
135
+                            $set('dateRange', 'Custom');
136
+                        }),
137
+                    DatePicker::make('endDate')
138
+                        ->label('End Date')
139
+                        ->displayFormat('Y-m-d')
140
+                        ->afterStateUpdated(static function (Set $set) {
141
+                            $set('dateRange', 'Custom');
142
+                        }),
143
+                ])->live(),
144
+            ]);
145
+    }
146
+
147
+    public function getDateRangeOptions(): array
148
+    {
149
+        $earliestDate = Carbon::parse($this->accountService->getEarliestTransactionDate());
150
+        $currentDate = now();
151
+        $fiscalYearStartCurrent = Carbon::parse($this->fiscalYearStartDate);
152
+
153
+        $options = [
154
+            'Fiscal Year' => [],
155
+            'Fiscal Quarter' => [],
156
+            'Calendar Year' => [],
157
+            'Calendar Quarter' => [],
158
+            'Month' => [],
159
+            'Custom' => [],
160
+        ];
161
+
162
+        $period = CarbonPeriod::create($earliestDate, '1 month', $currentDate);
163
+
164
+        foreach ($period as $date) {
165
+            $options['Fiscal Year']['FY-' . $date->year] = $date->year;
166
+
167
+            $fiscalYearStart = $fiscalYearStartCurrent->copy()->subYears($currentDate->year - $date->year);
168
+
169
+            for ($i = 0; $i < 4; $i++) {
170
+                $quarterNumber = $i + 1;
171
+                $quarterStart = $fiscalYearStart->copy()->addMonths(($quarterNumber - 1) * 3);
172
+                $quarterEnd = $quarterStart->copy()->addMonths(3)->subDay();
173
+
174
+                if ($quarterStart->lessThanOrEqualTo($currentDate) && $quarterEnd->greaterThanOrEqualTo($earliestDate)) {
175
+                    $options['Fiscal Quarter']['FQ-' . $quarterNumber . '-' . $date->year] = 'Q' . $quarterNumber . ' ' . $date->year;
176
+                }
177
+            }
178
+
179
+            $options['Calendar Year']['Y-' . $date->year] = $date->year;
180
+            $quarterKey = 'Q-' . $date->quarter . '-' . $date->year;
181
+            $options['Calendar Quarter'][$quarterKey] = 'Q' . $date->quarter . ' ' . $date->year;
182
+            $options['Month']['M-' . $date->format('Y-m')] = $date->format('F Y');
183
+            $options['Custom']['Custom'] = 'Custom';
184
+        }
185
+
186
+        $options['Fiscal Year'] = array_reverse($options['Fiscal Year'], true);
187
+        $options['Fiscal Quarter'] = array_reverse($options['Fiscal Quarter'], true);
188
+        $options['Calendar Year'] = array_reverse($options['Calendar Year'], true);
189
+        $options['Calendar Quarter'] = array_reverse($options['Calendar Quarter'], true);
190
+        $options['Month'] = array_reverse($options['Month'], true);
191
+
192
+        return $options;
193
+    }
194
+
195
+    public function updateDateRange($state): void
196
+    {
197
+        [$type, $param1, $param2] = explode('-', $state) + [null, null, null];
198
+        $this->processDateRange($type, $param1, $param2);
199
+    }
200
+
201
+    public function processDateRange($type, $param1, $param2): void
202
+    {
203
+        match ($type) {
204
+            'FY' => $this->processFiscalYear($param1),
205
+            'FQ' => $this->processFiscalQuarter($param1, $param2),
206
+            'Y' => $this->processCalendarYear($param1),
207
+            'Q' => $this->processCalendarQuarter($param1, $param2),
208
+            'M' => $this->processMonth("{$param1}-{$param2}"),
209
+        };
210
+    }
211
+
212
+    public function processFiscalYear($year): void
213
+    {
214
+        $currentYear = now()->year;
215
+        $diff = $currentYear - $year;
216
+        $fiscalYearStart = Carbon::parse($this->fiscalYearStartDate)->subYears($diff);
217
+        $fiscalYearEnd = Carbon::parse($this->fiscalYearEndDate)->subYears($diff);
218
+        $this->setDateRange($fiscalYearStart, $fiscalYearEnd);
219
+    }
220
+
221
+    public function processFiscalQuarter($quarter, $year): void
222
+    {
223
+        $currentYear = now()->year;
224
+        $diff = $currentYear - $year;
225
+        $fiscalYearStart = Carbon::parse($this->company->locale->fiscal_year_start_date)->subYears($diff);
226
+        $quarterStart = $fiscalYearStart->copy()->addMonths(($quarter - 1) * 3);
227
+        $quarterEnd = $quarterStart->copy()->addMonths(3)->subDay();
228
+        $this->setDateRange($quarterStart, $quarterEnd);
229
+    }
230
+
231
+    public function processCalendarYear($year): void
232
+    {
233
+        $start = Carbon::createFromDate($year)->startOfYear();
234
+        $end = Carbon::createFromDate($year)->endOfYear();
235
+        $this->setDateRange($start, $end);
236
+    }
237
+
238
+    public function processCalendarQuarter($quarter, $year): void
239
+    {
240
+        $month = ($quarter - 1) * 3 + 1;
241
+        $start = Carbon::createFromDate($year, $month, 1);
242
+        $end = Carbon::createFromDate($year, $month, 1)->endOfQuarter();
243
+        $this->setDateRange($start, $end);
244
+    }
245
+
246
+    public function processMonth($yearMonth): void
247
+    {
248
+        $start = Carbon::parse($yearMonth)->startOfMonth();
249
+        $end = Carbon::parse($yearMonth)->endOfMonth();
250
+        $this->setDateRange($start, $end);
251
+    }
252
+
253
+    public function setDateRange(Carbon $start, Carbon $end): void
254
+    {
255
+        $this->startDate = $start->format('Y-m-d');
256
+        $this->endDate = $end->isFuture() ? now()->format('Y-m-d') : $end->format('Y-m-d');
257
+    }
258
+
259
+    public static function shouldRegisterNavigation(): bool
260
+    {
261
+        return false;
262
+    }
263
+}

+ 0
- 22
app/Filament/Company/Pages/Setting/CompanyDefault.php Voir le fichier

@@ -116,7 +116,6 @@ class CompanyDefault extends Page
116 116
             ->schema([
117 117
                 $this->getGeneralSection(),
118 118
                 $this->getModifiersSection(),
119
-                $this->getCategoriesSection(),
120 119
             ])
121 120
             ->model($this->record)
122 121
             ->statePath('data')
@@ -222,27 +221,6 @@ class CompanyDefault extends Page
222 221
             ])->columns();
223 222
     }
224 223
 
225
-    protected function getCategoriesSection(): Component
226
-    {
227
-        return Section::make('Categories')
228
-            ->schema([
229
-                Select::make('income_category_id')
230
-                    ->softRequired()
231
-                    ->localizeLabel()
232
-                    ->relationship('incomeCategory', 'name')
233
-                    ->saveRelationshipsUsing(null)
234
-                    ->required()
235
-                    ->preload(),
236
-                Select::make('expense_category_id')
237
-                    ->softRequired()
238
-                    ->localizeLabel()
239
-                    ->relationship('expenseCategory', 'name')
240
-                    ->saveRelationshipsUsing(null)
241
-                    ->searchable()
242
-                    ->preload(),
243
-            ])->columns();
244
-    }
245
-
246 224
     public function renderBadgeOptionLabel(string $label, string $color = 'primary', string $size = 'sm'): string
247 225
     {
248 226
         return Blade::render('<x-filament::badge color="' . $color . '" size="' . $size . '">' . e($label) . '</x-filament::badge>');

+ 31
- 23
app/Filament/Company/Pages/Setting/Localization.php Voir le fichier

@@ -13,19 +13,20 @@ use Filament\Actions\Action;
13 13
 use Filament\Actions\ActionGroup;
14 14
 use Filament\Facades\Filament;
15 15
 use Filament\Forms\Components\Component;
16
-use Filament\Forms\Components\DatePicker;
16
+use Filament\Forms\Components\Group;
17 17
 use Filament\Forms\Components\Section;
18 18
 use Filament\Forms\Components\Select;
19 19
 use Filament\Forms\Form;
20 20
 use Filament\Forms\Get;
21
+use Filament\Forms\Set;
21 22
 use Filament\Notifications\Notification;
22 23
 use Filament\Pages\Concerns\InteractsWithFormActions;
23 24
 use Filament\Pages\Page;
24 25
 use Filament\Support\Exceptions\Halt;
26
+use Guava\FilamentClusters\Forms\Cluster;
25 27
 use Illuminate\Auth\Access\AuthorizationException;
26 28
 use Illuminate\Contracts\Support\Htmlable;
27 29
 use Illuminate\Database\Eloquent\Model;
28
-use Illuminate\Support\Str;
29 30
 use Livewire\Attributes\Locked;
30 31
 
31 32
 use function Filament\authorize;
@@ -168,27 +169,6 @@ class Localization extends Page
168 169
 
169 170
         return Section::make('Financial & Fiscal')
170 171
             ->schema([
171
-                DatePicker::make('fiscal_year_start')
172
-                    ->localizeLabel()
173
-                    ->live()
174
-                    ->extraAttributes(['wire:key' => Str::random()]) // Required to reinitialize the datepicker when the date_format state changes
175
-                    ->maxDate(static fn (Get $get) => $get('fiscal_year_end'))
176
-                    ->displayFormat(static function (LocalizationModel $record, Get $get) {
177
-                        return $get('date_format') ?? DateFormat::DEFAULT;
178
-                    })
179
-                    ->seconds(false)
180
-                    ->softRequired(),
181
-                DatePicker::make('fiscal_year_end')
182
-                    ->softRequired()
183
-                    ->localizeLabel()
184
-                    ->live()
185
-                    ->extraAttributes(['wire:key' => Str::random()]) // Required to reinitialize the datepicker when the date_format state changes
186
-                    ->minDate(static fn (Get $get) => $get('fiscal_year_start'))
187
-                    ->disabled(static fn (Get $get): bool => ! filled($get('fiscal_year_start')))
188
-                    ->displayFormat(static function (LocalizationModel $record, Get $get) {
189
-                        return $get('date_format') ?? DateFormat::DEFAULT;
190
-                    })
191
-                    ->seconds(false),
192 172
                 Select::make('number_format')
193 173
                     ->softRequired()
194 174
                     ->localizeLabel()
@@ -197,6 +177,34 @@ class Localization extends Page
197 177
                     ->softRequired()
198 178
                     ->localizeLabel('Percent Position')
199 179
                     ->boolean($beforeNumber, $afterNumber, $selectPosition),
180
+                Group::make()
181
+                    ->schema([
182
+                        Cluster::make([
183
+                            Select::make('fiscal_year_end_month')
184
+                                ->softRequired()
185
+                                ->options(array_combine(range(1, 12), array_map(static fn ($month) => now()->month($month)->monthName, range(1, 12))))
186
+                                ->afterStateUpdated(static fn (Set $set) => $set('fiscal_year_end_day', null))
187
+                                ->columnSpan(2)
188
+                                ->live(),
189
+                            Select::make('fiscal_year_end_day')
190
+                                ->placeholder('Day')
191
+                                ->softRequired()
192
+                                ->columnSpan(1)
193
+                                ->options(function (Get $get) {
194
+                                    $month = $get('fiscal_year_end_month');
195
+
196
+                                    $daysInMonth = now()->month($month)->daysInMonth;
197
+
198
+                                    return array_combine(range(1, $daysInMonth), range(1, $daysInMonth));
199
+                                })
200
+                                ->live(),
201
+                        ])
202
+                            ->columns(3)
203
+                            ->columnSpan(2)
204
+                            ->required()
205
+                            ->markAsRequired(false)
206
+                            ->label('Fiscal Year End'),
207
+                    ])->columns(3),
200 208
             ])->columns();
201 209
     }
202 210
 

+ 82
- 24
app/Filament/Company/Resources/Accounting/TransactionResource.php Voir le fichier

@@ -2,8 +2,10 @@
2 2
 
3 3
 namespace App\Filament\Company\Resources\Accounting;
4 4
 
5
+use App\Enums\Accounting\AccountCategory;
5 6
 use App\Enums\DateFormat;
6 7
 use App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
8
+use App\Models\Accounting\Account;
7 9
 use App\Models\Accounting\Transaction;
8 10
 use App\Models\Banking\BankAccount;
9 11
 use App\Models\Setting\Localization;
@@ -11,9 +13,11 @@ use Filament\Forms;
11 13
 use Filament\Forms\Form;
12 14
 use Filament\Resources\Resource;
13 15
 use Filament\Support\Enums\FontWeight;
16
+use Filament\Support\Enums\MaxWidth;
14 17
 use Filament\Tables;
15 18
 use Filament\Tables\Table;
16 19
 use Illuminate\Support\Carbon;
20
+use Illuminate\Support\Collection;
17 21
 
18 22
 class TransactionResource extends Resource
19 23
 {
@@ -26,25 +30,44 @@ class TransactionResource extends Resource
26 30
         return $form
27 31
             ->schema([
28 32
                 Forms\Components\DatePicker::make('posted_at')
29
-                    ->label('Posted At')
33
+                    ->label('Date')
30 34
                     ->required()
31
-                    ->default(now()),
35
+                    ->displayFormat('Y-m-d')
36
+                    ->default(now()->format('Y-m-d')),
32 37
                 Forms\Components\TextInput::make('description')
33 38
                     ->label('Description'),
34
-                Forms\Components\Select::make('type')
35
-                    ->label('Type')
36
-                    ->options([
37
-                        'expense' => 'Expense',
38
-                        'income' => 'Income',
39
-                        'transfer' => 'Transfer',
40
-                    ])
39
+                Forms\Components\Select::make('bank_account_id')
40
+                    ->label('Account')
41
+                    ->options(fn () => static::getBankAccountOptions())
42
+                    ->live()
43
+                    ->searchable()
44
+                    ->preload()
41 45
                     ->required(),
42 46
                 Forms\Components\Select::make('method')
43
-                    ->label('Method')
47
+                    ->label('Type')
48
+                    ->live()
44 49
                     ->options([
45 50
                         'deposit' => 'Deposit',
46 51
                         'withdrawal' => 'Withdrawal',
47 52
                     ])
53
+                    ->default('deposit')
54
+                    ->afterStateUpdated(static function (Forms\Set $set, $state) {
55
+                        if ($state === 'deposit') {
56
+                            $account = Account::where('category', AccountCategory::Revenue)
57
+                                ->where('name', 'Uncategorized Income')->first();
58
+
59
+                            if ($account->exists()) {
60
+                                $set('account_id', $account->id);
61
+                            }
62
+                        } else {
63
+                            $account = Account::where('category', AccountCategory::Expense)
64
+                                ->where('name', 'Uncategorized Expense')->first();
65
+
66
+                            if ($account->exists()) {
67
+                                $set('account_id', $account->id);
68
+                            }
69
+                        }
70
+                    })
48 71
                     ->required(),
49 72
                 Forms\Components\TextInput::make('amount')
50 73
                     ->label('Amount')
@@ -60,12 +83,17 @@ class TransactionResource extends Resource
60 83
                         return 'USD';
61 84
                     })
62 85
                     ->required(),
63
-                Forms\Components\Select::make('category_id')
86
+                Forms\Components\Select::make('account_id')
64 87
                     ->label('Category')
65
-                    ->relationship('category', 'name')
88
+                    ->options(static fn (Forms\Get $get) => static::getAccountOptions($get('method')))
66 89
                     ->searchable()
67 90
                     ->preload()
68 91
                     ->required(),
92
+                Forms\Components\Textarea::make('notes')
93
+                    ->label('Notes')
94
+                    ->autosize()
95
+                    ->rows(10)
96
+                    ->columnSpanFull(),
69 97
             ]);
70 98
     }
71 99
 
@@ -75,6 +103,7 @@ class TransactionResource extends Resource
75 103
             ->columns([
76 104
                 Tables\Columns\TextColumn::make('posted_at')
77 105
                     ->label('Date')
106
+                    ->sortable()
78 107
                     ->formatStateUsing(static function ($state) {
79 108
                         $dateFormat = Localization::firstOrFail()->date_format->value ?? DateFormat::DEFAULT;
80 109
 
@@ -86,14 +115,8 @@ class TransactionResource extends Resource
86 115
                 Tables\Columns\TextColumn::make('description')
87 116
                     ->limit(50)
88 117
                     ->label('Description'),
89
-                Tables\Columns\TextColumn::make('category.name')
90
-                    ->label('Category')
91
-                    ->html()
92
-                    ->formatStateUsing(static function ($state, Transaction $record) {
93
-                        $color = $record->category->color ?? '#000000';
94
-
95
-                        return "<span style='display: inline-block; width: 8px; height: 8px; background-color: {$color}; border-radius: 50%; margin-right: 3px;'></span> {$state}";
96
-                    }),
118
+                Tables\Columns\TextColumn::make('account.name')
119
+                    ->label('Category'),
97 120
                 Tables\Columns\TextColumn::make('amount')
98 121
                     ->label('Amount')
99 122
                     ->sortable()
@@ -106,7 +129,21 @@ class TransactionResource extends Resource
106 129
                 //
107 130
             ])
108 131
             ->actions([
109
-                Tables\Actions\EditAction::make(),
132
+                Tables\Actions\EditAction::make()
133
+                    ->modalWidth(MaxWidth::ThreeExtraLarge)
134
+                    ->stickyModalHeader()
135
+                    ->stickyModalFooter()
136
+                    ->mutateFormDataUsing(static function (array $data): array {
137
+                        $method = $data['method'];
138
+
139
+                        if ($method === 'deposit') {
140
+                            $data['type'] = 'income';
141
+                        } else {
142
+                            $data['type'] = 'expense';
143
+                        }
144
+
145
+                        return $data;
146
+                    }),
110 147
             ])
111 148
             ->bulkActions([
112 149
                 Tables\Actions\BulkActionGroup::make([
@@ -125,9 +162,30 @@ class TransactionResource extends Resource
125 162
     public static function getPages(): array
126 163
     {
127 164
         return [
128
-            'index' => Pages\ListTransactions::route('/'),
129
-            'create' => Pages\CreateTransaction::route('/create'),
130
-            'edit' => Pages\EditTransaction::route('/{record}/edit'),
165
+            'index' => Pages\ManageTransaction::route('/'),
131 166
         ];
132 167
     }
168
+
169
+    public static function getBankAccountOptions(): array
170
+    {
171
+        $bankAccounts = BankAccount::with('account.subtype')->get();
172
+
173
+        return $bankAccounts->groupBy('account.subtype.name')
174
+            ->map(fn (Collection $bankAccounts) => $bankAccounts->pluck('account.name', 'id'))
175
+            ->toArray();
176
+    }
177
+
178
+    public static function getAccountOptions(mixed $method)
179
+    {
180
+        $excludedCategory = match ($method) {
181
+            'deposit' => AccountCategory::Expense,
182
+            'withdrawal' => AccountCategory::Revenue,
183
+        };
184
+
185
+        $accounts = Account::whereNot('category', $excludedCategory)->get();
186
+
187
+        return $accounts->groupBy(fn (Account $account) => $account->category->getLabel())
188
+            ->map(fn (Collection $accounts, string $category) => $accounts->mapWithKeys(fn (Account $account) => [$account->id => $account->name]))
189
+            ->toArray();
190
+    }
133 191
 }

+ 72
- 0
app/Filament/Company/Resources/Accounting/TransactionResource/Pages/ManageTransaction.php Voir le fichier

@@ -0,0 +1,72 @@
1
+<?php
2
+
3
+namespace App\Filament\Company\Resources\Accounting\TransactionResource\Pages;
4
+
5
+use App\Enums\Accounting\AccountCategory;
6
+use App\Filament\Company\Resources\Accounting\TransactionResource;
7
+use App\Models\Accounting\Account;
8
+use App\Models\Banking\BankAccount;
9
+use Filament\Actions;
10
+use Filament\Resources\Pages\ManageRecords;
11
+use Filament\Support\Enums\MaxWidth;
12
+
13
+class ManageTransaction extends ManageRecords
14
+{
15
+    public static string $resource = TransactionResource::class;
16
+
17
+    protected function getHeaderActions(): array
18
+    {
19
+        return [
20
+            Actions\CreateAction::make('addIncome')
21
+                ->label('Add Income')
22
+                ->modalWidth(MaxWidth::ThreeExtraLarge)
23
+                ->stickyModalHeader()
24
+                ->stickyModalFooter()
25
+                ->button()
26
+                ->outlined()
27
+                ->fillForm(static fn (): array => [
28
+                    'method' => 'deposit',
29
+                    'posted_at' => now()->format('Y-m-d'),
30
+                    'bank_account_id' => BankAccount::first()->isEnabled() ? BankAccount::first()->id : null,
31
+                    'amount' => '0.00',
32
+                    'account_id' => Account::where('category', AccountCategory::Revenue)->where('name', 'Uncategorized Income')->first()->id,
33
+                ])
34
+                ->mutateFormDataUsing(function (array $data): array {
35
+                    $method = $data['method'];
36
+
37
+                    if ($method === 'deposit') {
38
+                        $data['type'] = 'income';
39
+                    } else {
40
+                        $data['type'] = 'expense';
41
+                    }
42
+
43
+                    return $data;
44
+                }),
45
+            Actions\CreateAction::make('addExpense')
46
+                ->label('Add Expense')
47
+                ->modalWidth(MaxWidth::ThreeExtraLarge)
48
+                ->stickyModalHeader()
49
+                ->stickyModalFooter()
50
+                ->button()
51
+                ->outlined()
52
+                ->fillForm(static fn (): array => [
53
+                    'method' => 'withdrawal',
54
+                    'posted_at' => now()->format('Y-m-d'),
55
+                    'bank_account_id' => BankAccount::first()->isEnabled() ? BankAccount::first()->id : null,
56
+                    'amount' => '0.00',
57
+                    'account_id' => Account::where('category', AccountCategory::Expense)->where('name', 'Uncategorized Expense')->first()->id,
58
+                ])
59
+                ->mutateFormDataUsing(function (array $data): array {
60
+                    $method = $data['method'];
61
+
62
+                    if ($method === 'deposit') {
63
+                        $data['type'] = 'income';
64
+                    } else {
65
+                        $data['type'] = 'expense';
66
+                    }
67
+
68
+                    return $data;
69
+                }),
70
+        ];
71
+    }
72
+}

+ 80
- 80
app/Filament/Company/Resources/Banking/AccountResource.php Voir le fichier

@@ -9,6 +9,7 @@ use App\Facades\Forex;
9 9
 use App\Filament\Company\Resources\Banking\AccountResource\Pages;
10 10
 use App\Models\Accounting\AccountSubtype;
11 11
 use App\Models\Banking\BankAccount;
12
+use App\Services\AccountService;
12 13
 use App\Utilities\Currency\CurrencyAccessor;
13 14
 use App\Utilities\Currency\CurrencyConverter;
14 15
 use BackedEnum;
@@ -62,6 +63,7 @@ class AccountResource extends Resource
62 63
                             ->options(BankAccountType::class)
63 64
                             ->localizeLabel()
64 65
                             ->searchable()
66
+                            ->columnSpan(1)
65 67
                             ->default(BankAccountType::DEFAULT)
66 68
                             ->live()
67 69
                             ->afterStateUpdated(static function (Forms\Set $set, $state, ?BankAccount $record, string $operation) {
@@ -77,6 +79,7 @@ class AccountResource extends Resource
77 79
                             })
78 80
                             ->required(),
79 81
                         Forms\Components\Group::make()
82
+                            ->columnStart(2)
80 83
                             ->relationship('account')
81 84
                             ->schema([
82 85
                                 Forms\Components\Select::make('subtype_id')
@@ -93,94 +96,85 @@ class AccountResource extends Resource
93 96
                             ]),
94 97
                         Forms\Components\Group::make()
95 98
                             ->relationship('account')
99
+                            ->columns(2)
100
+                            ->columnSpanFull()
96 101
                             ->schema([
97 102
                                 Forms\Components\TextInput::make('name')
98 103
                                     ->maxLength(100)
99 104
                                     ->localizeLabel()
100 105
                                     ->required(),
101
-                            ]),
102
-                        Forms\Components\TextInput::make('number')
103
-                            ->localizeLabel('Account Number')
104
-                            ->unique(ignoreRecord: true, modifyRuleUsing: static function (Unique $rule, $state) {
105
-                                $companyId = Auth::user()->currentCompany->id;
106
-
107
-                                return $rule->where('company_id', $companyId)->where('number', $state);
108
-                            })
109
-                            ->maxLength(20)
110
-                            ->validationAttribute('account number')
111
-                            ->required(),
112
-                        ToggleButton::make('enabled')
113
-                            ->localizeLabel('Default'),
114
-                    ])->columns(),
115
-                Forms\Components\Section::make('Financial Details')
116
-                    ->relationship('account')
117
-                    ->schema([
118
-                        Forms\Components\Select::make('currency_code')
119
-                            ->localizeLabel('Currency')
120
-                            ->relationship('currency', 'name')
121
-                            ->default(CurrencyAccessor::getDefaultCurrency())
122
-                            ->preload()
123
-                            ->searchable()
124
-                            ->live()
125
-                            ->afterStateUpdated(static function (Forms\Set $set, $state, $old, Forms\Get $get) {
126
-                                $starting_balance = CurrencyConverter::convertAndSet($state, $old, $get('starting_balance'));
127
-
128
-                                if ($starting_balance !== null) {
129
-                                    $set('starting_balance', $starting_balance);
130
-                                }
131
-                            })
132
-                            ->required()
133
-                            ->createOptionForm([
134
-                                Forms\Components\Select::make('code')
135
-                                    ->localizeLabel()
106
+                                Forms\Components\Select::make('currency_code')
107
+                                    ->localizeLabel('Currency')
108
+                                    ->relationship('currency', 'name')
109
+                                    ->default(CurrencyAccessor::getDefaultCurrency())
110
+                                    ->preload()
136 111
                                     ->searchable()
137
-                                    ->options(CurrencyAccessor::getAvailableCurrencies())
138 112
                                     ->live()
139
-                                    ->afterStateUpdated(static function (callable $set, $state) {
140
-                                        if ($state === null) {
141
-                                            return;
142
-                                        }
143
-
144
-                                        $currency_code = currency($state);
145
-                                        $defaultCurrencyCode = currency()->getCurrency();
146
-                                        $forexEnabled = Forex::isEnabled();
147
-                                        $exchangeRate = $forexEnabled ? Forex::getCachedExchangeRate($defaultCurrencyCode, $state) : null;
148
-
149
-                                        $set('name', $currency_code->getName() ?? '');
113
+                                    ->required()
114
+                                    ->createOptionForm([
115
+                                        Forms\Components\Select::make('code')
116
+                                            ->localizeLabel()
117
+                                            ->searchable()
118
+                                            ->options(CurrencyAccessor::getAvailableCurrencies())
119
+                                            ->live()
120
+                                            ->afterStateUpdated(static function (callable $set, $state) {
121
+                                                if ($state === null) {
122
+                                                    return;
123
+                                                }
124
+
125
+                                                $currency_code = currency($state);
126
+                                                $defaultCurrencyCode = currency()->getCurrency();
127
+                                                $forexEnabled = Forex::isEnabled();
128
+                                                $exchangeRate = $forexEnabled ? Forex::getCachedExchangeRate($defaultCurrencyCode, $state) : null;
129
+
130
+                                                $set('name', $currency_code->getName() ?? '');
131
+
132
+                                                if ($forexEnabled && $exchangeRate !== null) {
133
+                                                    $set('rate', $exchangeRate);
134
+                                                }
135
+                                            })
136
+                                            ->required(),
137
+                                        Forms\Components\TextInput::make('name')
138
+                                            ->localizeLabel()
139
+                                            ->maxLength(100)
140
+                                            ->required(),
141
+                                        Forms\Components\TextInput::make('rate')
142
+                                            ->localizeLabel()
143
+                                            ->numeric()
144
+                                            ->required(),
145
+                                    ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
146
+                                        return $action
147
+                                            ->label('Add Currency')
148
+                                            ->slideOver()
149
+                                            ->modalWidth('md')
150
+                                            ->action(static function (array $data) {
151
+                                                return DB::transaction(static function () use ($data) {
152
+                                                    $code = $data['code'];
153
+                                                    $name = $data['name'];
154
+                                                    $rate = $data['rate'];
155
+
156
+                                                    return (new CreateCurrency())->create($code, $name, $rate);
157
+                                                });
158
+                                            });
159
+                                    }),
160
+                            ]),
161
+                        Forms\Components\Group::make()
162
+                            ->columns(2)
163
+                            ->columnSpanFull()
164
+                            ->schema([
165
+                                Forms\Components\TextInput::make('number')
166
+                                    ->localizeLabel('Account Number')
167
+                                    ->unique(ignoreRecord: true, modifyRuleUsing: static function (Unique $rule, $state) {
168
+                                        $companyId = Auth::user()->currentCompany->id;
150 169
 
151
-                                        if ($forexEnabled && $exchangeRate !== null) {
152
-                                            $set('rate', $exchangeRate);
153
-                                        }
170
+                                        return $rule->where('company_id', $companyId)->where('number', $state);
154 171
                                     })
172
+                                    ->maxLength(20)
173
+                                    ->validationAttribute('account number')
155 174
                                     ->required(),
156
-                                Forms\Components\TextInput::make('name')
157
-                                    ->localizeLabel()
158
-                                    ->maxLength(100)
159
-                                    ->required(),
160
-                                Forms\Components\TextInput::make('rate')
161
-                                    ->localizeLabel()
162
-                                    ->numeric()
163
-                                    ->required(),
164
-                            ])->createOptionAction(static function (Forms\Components\Actions\Action $action) {
165
-                                return $action
166
-                                    ->label('Add Currency')
167
-                                    ->slideOver()
168
-                                    ->modalWidth('md')
169
-                                    ->action(static function (array $data) {
170
-                                        return DB::transaction(static function () use ($data) {
171
-                                            $code = $data['code'];
172
-                                            $name = $data['name'];
173
-                                            $rate = $data['rate'];
174
-
175
-                                            return (new CreateCurrency())->create($code, $name, $rate);
176
-                                        });
177
-                                    });
178
-                            }),
179
-                        Forms\Components\TextInput::make('starting_balance')
180
-                            ->required()
181
-                            ->localizeLabel()
182
-                            ->dehydrated()
183
-                            ->money(static fn (Forms\Get $get) => $get('currency_code')),
175
+                                ToggleButton::make('enabled')
176
+                                    ->localizeLabel('Default'),
177
+                            ]),
184 178
                     ])->columns(),
185 179
             ]);
186 180
     }
@@ -198,10 +192,16 @@ class AccountResource extends Resource
198 192
                     ->iconPosition('after')
199 193
                     ->description(static fn (BankAccount $record) => $record->mask ?: 'N/A')
200 194
                     ->sortable(),
201
-                Tables\Columns\TextColumn::make('account.ending_balance')
195
+                Tables\Columns\TextColumn::make('account.code') // Just so I could display the balance in the table for now
202 196
                     ->localizeLabel('Current Balance')
203 197
                     ->sortable()
204
-                    ->currency(static fn (BankAccount $record) => $record->account->currency_code, true),
198
+                    ->formatStateUsing(function (BankAccount $record) {
199
+                        $accountService = app(AccountService::class);
200
+                        $startDate = $record->account->company->locale->fiscalYearStartDate();
201
+                        $endDate = $record->account->company->locale->fiscalYearEndDate();
202
+
203
+                        return $accountService->getEndingBalance($record->account, $startDate, $endDate)?->formatted();
204
+                    }),
205 205
             ])
206 206
             ->filters([
207 207
                 //

+ 0
- 188
app/Filament/Company/Resources/Setting/CategoryResource.php Voir le fichier

@@ -1,188 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Setting;
4
-
5
-use App\Enums\Accounting\AccountCategory;
6
-use App\Enums\CategoryType;
7
-use App\Filament\Company\Resources\Setting\CategoryResource\Pages;
8
-use App\Models\Accounting\Account;
9
-use App\Models\Setting\Category;
10
-use BackedEnum;
11
-use Closure;
12
-use Exception;
13
-use Filament\Facades\Filament;
14
-use Filament\Forms;
15
-use Filament\Forms\Form;
16
-use Filament\Resources\Resource;
17
-use Filament\Support\Enums\FontWeight;
18
-use Filament\Tables;
19
-use Filament\Tables\Table;
20
-use Wallo\FilamentSelectify\Components\ToggleButton;
21
-
22
-class CategoryResource extends Resource
23
-{
24
-    protected static ?string $model = Category::class;
25
-
26
-    protected static ?string $modelLabel = 'Category';
27
-
28
-    protected static ?string $navigationIcon = 'heroicon-o-folder';
29
-
30
-    protected static ?string $navigationGroup = 'Settings';
31
-
32
-    protected static ?string $slug = 'settings/categories';
33
-
34
-    public static function getModelLabel(): string
35
-    {
36
-        $modelLabel = static::$modelLabel;
37
-
38
-        return translate($modelLabel);
39
-    }
40
-
41
-    public static function getNavigationParentItem(): ?string
42
-    {
43
-        if (Filament::hasTopNavigation()) {
44
-            return translate('Finance');
45
-        }
46
-
47
-        return null;
48
-    }
49
-
50
-    public static function form(Form $form): Form
51
-    {
52
-        return $form
53
-            ->columns(1)
54
-            ->schema([
55
-                Forms\Components\TextInput::make('name')
56
-                    ->localizeLabel()
57
-                    ->required()
58
-                    ->maxLength(255)
59
-                    ->rule(static function (Forms\Get $get, Forms\Components\Component $component): Closure {
60
-                        return static function (string $attribute, $value, Closure $fail) use ($get, $component) {
61
-                            $existingCategory = Category::where('company_id', auth()->user()->currentCompany->id)
62
-                                ->where('name', $value)
63
-                                ->where('type', $get('type'))
64
-                                ->first();
65
-
66
-                            if ($existingCategory && $existingCategory->getKey() !== $component->getRecord()?->getKey()) {
67
-                                $message = translate('The :Type :record ":name" already exists.', [
68
-                                    'Type' => $existingCategory->type->getLabel(),
69
-                                    'record' => strtolower(static::getModelLabel()),
70
-                                    'name' => $value,
71
-                                ]);
72
-
73
-                                $fail($message);
74
-                            }
75
-                        };
76
-                    }),
77
-                Forms\Components\Select::make('type')
78
-                    ->localizeLabel()
79
-                    ->options(CategoryType::class)
80
-                    ->live()
81
-                    ->required()
82
-                    ->afterStateUpdated(static function (Forms\Set $set, $state, ?Category $record, string $operation) {
83
-                        $accountOptions = static::getAccountOptions($state);
84
-
85
-                        if ($operation === 'create') {
86
-                            $set('account_id', null);
87
-                        } elseif ($operation === 'edit' && $record !== null) {
88
-                            if (! array_key_exists($record->account_id, $accountOptions)) {
89
-                                $set('account_id', null);
90
-                            } else {
91
-                                $set('account_id', $record->account_id);
92
-                            }
93
-                        }
94
-                    }),
95
-                Forms\Components\Select::make('account_id')
96
-                    ->label('Account')
97
-                    ->searchable()
98
-                    ->preload()
99
-                    ->options(static function (Forms\Get $get) {
100
-                        return static::getAccountOptions($get('type'));
101
-                    }),
102
-                Forms\Components\ColorPicker::make('color')
103
-                    ->localizeLabel()
104
-                    ->required(),
105
-                ToggleButton::make('enabled')
106
-                    ->localizeLabel('Default')
107
-                    ->onLabel(Category::enabledLabel())
108
-                    ->offLabel(Category::disabledLabel()),
109
-            ]);
110
-    }
111
-
112
-    /**
113
-     * @throws Exception
114
-     */
115
-    public static function table(Table $table): Table
116
-    {
117
-        return $table
118
-            ->columns([
119
-                Tables\Columns\TextColumn::make('name')
120
-                    ->localizeLabel()
121
-                    ->weight(FontWeight::Medium)
122
-                    ->icon(static fn (Category $record) => $record->isEnabled() ? 'heroicon-o-lock-closed' : null)
123
-                    ->tooltip(static function (Category $record) {
124
-                        $tooltipMessage = translate('Default :Type :Record', [
125
-                            'Type' => $record->type->getLabel(),
126
-                            'Record' => static::getModelLabel(),
127
-                        ]);
128
-
129
-                        return $record->isEnabled() ? $tooltipMessage : null;
130
-                    })
131
-                    ->iconPosition('after')
132
-                    ->searchable()
133
-                    ->sortable(),
134
-                Tables\Columns\TextColumn::make('type')
135
-                    ->localizeLabel()
136
-                    ->sortable()
137
-                    ->searchable(),
138
-                Tables\Columns\ColorColumn::make('color')
139
-                    ->localizeLabel()
140
-                    ->copyable(),
141
-            ])
142
-            ->filters([
143
-                Tables\Filters\SelectFilter::make('type')
144
-                    ->label('Type')
145
-                    ->multiple()
146
-                    ->searchable()
147
-                    ->options(CategoryType::class),
148
-            ])
149
-            ->actions([
150
-                Tables\Actions\EditAction::make(),
151
-                Tables\Actions\DeleteAction::make(),
152
-            ])
153
-            ->bulkActions([
154
-                Tables\Actions\BulkActionGroup::make([
155
-                    Tables\Actions\DeleteBulkAction::make(),
156
-                ]),
157
-            ])
158
-            ->checkIfRecordIsSelectableUsing(static function (Category $record) {
159
-                return $record->isDisabled();
160
-            });
161
-    }
162
-
163
-    public static function getPages(): array
164
-    {
165
-        return [
166
-            'index' => Pages\ManageCategory::route('/'),
167
-        ];
168
-    }
169
-
170
-    public static function getAccountOptions($typeValue): array
171
-    {
172
-        $typeString = $typeValue instanceof BackedEnum ? $typeValue->value : $typeValue;
173
-
174
-        $accountCategory = match ($typeString) {
175
-            CategoryType::Income->value => AccountCategory::Revenue,
176
-            CategoryType::Expense->value => AccountCategory::Expense,
177
-            default => null,
178
-        };
179
-
180
-        if ($accountCategory) {
181
-            $accounts = Account::where('category', $accountCategory)->get();
182
-        } else {
183
-            $accounts = Account::all();
184
-        }
185
-
186
-        return $accounts->pluck('name', 'id')->toArray();
187
-    }
188
-}

+ 0
- 52
app/Filament/Company/Resources/Setting/CategoryResource/Pages/ManageCategory.php Voir le fichier

@@ -1,52 +0,0 @@
1
-<?php
2
-
3
-namespace App\Filament\Company\Resources\Setting\CategoryResource\Pages;
4
-
5
-use App\Enums\CategoryType;
6
-use App\Filament\Company\Resources\Setting\CategoryResource;
7
-use App\Models\Setting\Category;
8
-use App\Traits\HandlesResourceRecordCreation;
9
-use App\Traits\HandlesResourceRecordUpdate;
10
-use Filament\Actions;
11
-use Filament\Resources\Pages\ManageRecords;
12
-use Filament\Support\Enums\MaxWidth;
13
-use Filament\Tables;
14
-use Illuminate\Database\Eloquent\Model;
15
-
16
-class ManageCategory extends ManageRecords
17
-{
18
-    use HandlesResourceRecordCreation;
19
-    use HandlesResourceRecordUpdate;
20
-
21
-    protected static string $resource = CategoryResource::class;
22
-
23
-    protected function getHeaderActions(): array
24
-    {
25
-        return [
26
-            Actions\CreateAction::make()
27
-                ->using(function (array $data): Model {
28
-                    $user = auth()->user();
29
-
30
-                    $evaluatedTypes = [CategoryType::Income, CategoryType::Expense];
31
-
32
-                    return $this->handleRecordCreationWithUniqueField($data, new Category(), $user, 'type', $evaluatedTypes);
33
-                })
34
-                ->modalWidth(MaxWidth::TwoExtraLarge),
35
-        ];
36
-    }
37
-
38
-    protected function configureEditAction(Tables\Actions\EditAction $action): void
39
-    {
40
-        parent::configureEditAction($action);
41
-
42
-        $action
43
-            ->modalWidth(MaxWidth::TwoExtraLarge)
44
-            ->using(function (Model $record, array $data): Model {
45
-                $user = auth()->user();
46
-
47
-                $evaluatedTypes = [CategoryType::Income, CategoryType::Expense];
48
-
49
-                return $this->handleRecordUpdateWithUniqueField($record, $data, $user, 'type', $evaluatedTypes);
50
-            });
51
-    }
52
-}

+ 0
- 4
app/Filament/Company/Resources/Setting/DiscountResource.php Voir le fichier

@@ -160,10 +160,6 @@ class DiscountResource extends Resource
160 160
                     ->iconPosition('after')
161 161
                     ->searchable()
162 162
                     ->sortable(),
163
-                Tables\Columns\TextColumn::make('computation')
164
-                    ->localizeLabel()
165
-                    ->searchable()
166
-                    ->sortable(),
167 163
                 Tables\Columns\TextColumn::make('rate')
168 164
                     ->localizeLabel()
169 165
                     ->rate(static fn (Discount $record) => $record->computation->value)

+ 19
- 0
app/Infolists/Components/ReportEntry.php Voir le fichier

@@ -0,0 +1,19 @@
1
+<?php
2
+
3
+namespace App\Infolists\Components;
4
+
5
+use Filament\Infolists\Components\Entry;
6
+use Filament\Support\Concerns\HasDescription;
7
+use Filament\Support\Concerns\HasHeading;
8
+use Filament\Support\Concerns\HasIcon;
9
+use Filament\Support\Concerns\HasIconColor;
10
+
11
+class ReportEntry extends Entry
12
+{
13
+    use HasDescription;
14
+    use HasHeading;
15
+    use HasIcon;
16
+    use HasIconColor;
17
+
18
+    protected string $view = 'infolists.components.report-entry';
19
+}

+ 0
- 16
app/Listeners/ConfigureChartOfAccounts.php Voir le fichier

@@ -47,8 +47,6 @@ class ConfigureChartOfAccounts
47 47
                 $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
48 48
             }
49 49
         }
50
-
51
-        $this->linkCategoriesToAccounts($company);
52 50
     }
53 51
 
54 52
     private function createDefaultAccounts(Company $company, AccountSubtype $subtype, array $subtypeConfig): void
@@ -75,18 +73,4 @@ class ConfigureChartOfAccounts
75 73
             }
76 74
         }
77 75
     }
78
-
79
-    private function linkCategoriesToAccounts(Company $company): void
80
-    {
81
-        $categories = config('chart-of-accounts.category_account_map');
82
-
83
-        foreach ($categories as $categoryName => $accountName) {
84
-            $category = $company->categories()->where('name', $categoryName)->first();
85
-            $account = $company->accounts()->where('name', $accountName)->first();
86
-
87
-            if ($category && $account) {
88
-                $category->update(['account_id' => $account->id]);
89
-            }
90
-        }
91
-    }
92 76
 }

+ 0
- 2
app/Listeners/ConfigureCompanyNavigation.php Voir le fichier

@@ -12,7 +12,6 @@ use App\Filament\Company\Pages\Setting\Invoice;
12 12
 use App\Filament\Company\Pages\Setting\Localization;
13 13
 use App\Filament\Company\Resources\Banking\AccountResource;
14 14
 use App\Filament\Company\Resources\Core\DepartmentResource;
15
-use App\Filament\Company\Resources\Setting\CategoryResource;
16 15
 use App\Filament\Company\Resources\Setting\CurrencyResource;
17 16
 use App\Filament\Company\Resources\Setting\DiscountResource;
18 17
 use App\Filament\Company\Resources\Setting\TaxResource;
@@ -69,7 +68,6 @@ class ConfigureCompanyNavigation
69 68
     {
70 69
         return NavigationGroup::make('Settings')
71 70
             ->items([
72
-                ...CategoryResource::getNavigationItems(),
73 71
                 ...CurrencyResource::getNavigationItems(),
74 72
                 ...DiscountResource::getNavigationItems(),
75 73
                 ...TaxResource::getNavigationItems(),

+ 13
- 15
app/Listeners/HandleTransactionImport.php Voir le fichier

@@ -4,10 +4,10 @@ namespace App\Listeners;
4 4
 
5 5
 use App\Events\StartTransactionImport;
6 6
 use App\Models\Accounting\Account;
7
+use App\Models\Banking\BankAccount;
7 8
 use App\Models\Banking\ConnectedBankAccount;
8 9
 use App\Models\Company;
9
-use App\Services\AccountService;
10
-use App\Services\BankAccountService;
10
+use App\Services\ConnectedBankAccountService;
11 11
 use App\Services\PlaidService;
12 12
 use App\Services\TransactionService;
13 13
 use Illuminate\Support\Carbon;
@@ -17,20 +17,17 @@ class HandleTransactionImport
17 17
 {
18 18
     protected PlaidService $plaid;
19 19
 
20
-    protected BankAccountService $bankAccountService;
21
-
22
-    protected AccountService $accountService;
20
+    protected ConnectedBankAccountService $connectedBankAccountService;
23 21
 
24 22
     protected TransactionService $transactionService;
25 23
 
26 24
     /**
27 25
      * Create the event listener.
28 26
      */
29
-    public function __construct(PlaidService $plaid, BankAccountService $bankAccountService, AccountService $accountService, TransactionService $transactionService)
27
+    public function __construct(PlaidService $plaid, ConnectedBankAccountService $connectedBankAccountService, TransactionService $transactionService)
30 28
     {
31 29
         $this->plaid = $plaid;
32
-        $this->bankAccountService = $bankAccountService;
33
-        $this->accountService = $accountService;
30
+        $this->connectedBankAccountService = $connectedBankAccountService;
34 31
         $this->transactionService = $transactionService;
35 32
     }
36 33
 
@@ -53,18 +50,18 @@ class HandleTransactionImport
53 50
 
54 51
         $accessToken = $connectedBankAccount->access_token;
55 52
 
56
-        $bankAccount = $this->bankAccountService->getOrProcessBankAccount($company, $connectedBankAccount, $selectedBankAccountId);
57
-        $account = $this->accountService->getOrProcessAccount($bankAccount, $company, $connectedBankAccount);
53
+        $bankAccount = $this->connectedBankAccountService->getOrProcessBankAccountForConnectedBankAccount($company, $connectedBankAccount, $selectedBankAccountId);
54
+        $account = $this->connectedBankAccountService->getOrProcessAccountForConnectedBankAccount($bankAccount, $company, $connectedBankAccount);
58 55
 
59 56
         $connectedBankAccount->update([
60 57
             'bank_account_id' => $bankAccount->id,
61 58
             'import_transactions' => true,
62 59
         ]);
63 60
 
64
-        $this->processTransactions($startDate, $company, $connectedBankAccount, $accessToken, $account);
61
+        $this->processTransactions($company, $account, $bankAccount, $connectedBankAccount, $startDate, $accessToken);
65 62
     }
66 63
 
67
-    public function processTransactions(string $startDate, Company $company, ConnectedBankAccount $connectedBankAccount, string $accessToken, Account $account): void
64
+    public function processTransactions(Company $company, Account $account, BankAccount $bankAccount, ConnectedBankAccount $connectedBankAccount, string $startDate, string $accessToken): void
68 65
     {
69 66
         $endDate = Carbon::now()->toDateString();
70 67
         $startDate = Carbon::parse($startDate)->toDateString();
@@ -74,11 +71,12 @@ class HandleTransactionImport
74 71
         ]);
75 72
 
76 73
         if (filled($transactionsResponse->transactions)) {
77
-            $transactions = array_reverse($transactionsResponse->transactions);
74
+            $postedTransactions = array_filter($transactionsResponse->transactions, static fn ($transaction) => $transaction->pending === false);
75
+            $transactions = array_reverse($postedTransactions);
78 76
             $currentBalance = $transactionsResponse->accounts[0]->balances->current;
79 77
 
80
-            $this->transactionService->createStartingBalanceIfNeeded($company, $account, $connectedBankAccount, $transactions, $currentBalance, $startDate);
81
-            $this->transactionService->storeTransactions($company, $account, $connectedBankAccount, $transactions);
78
+            $this->transactionService->createStartingBalanceIfNeeded($company, $account, $bankAccount, $transactions, $currentBalance, $startDate);
79
+            $this->transactionService->storeTransactions($company, $bankAccount, $transactions);
82 80
         }
83 81
     }
84 82
 }

+ 0
- 2
app/Listeners/SyncAssociatedModels.php Voir le fichier

@@ -45,8 +45,6 @@ class SyncAssociatedModels
45 45
             'purchase_tax_id' => 'purchaseTax',
46 46
             'sales_discount_id' => 'salesDiscount',
47 47
             'purchase_discount_id' => 'purchaseDiscount',
48
-            'income_category_id' => 'incomeCategory',
49
-            'expense_category_id' => 'expenseCategory',
50 48
         ];
51 49
 
52 50
         foreach ($diff as $key => $value) {

+ 0
- 14
app/Listeners/SyncWithCompanyDefaults.php Voir le fichier

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\Listeners;
4 4
 
5
-use App\Enums\CategoryType;
6 5
 use App\Enums\DiscountType;
7 6
 use App\Enums\TaxType;
8 7
 use App\Events\CompanyDefaultEvent;
@@ -58,7 +57,6 @@ class SyncWithCompanyDefaults
58 57
         match ($modelName) {
59 58
             'Discount' => $this->handleDiscount($default, $type, $model->getKey()),
60 59
             'Tax' => $this->handleTax($default, $type, $model->getKey()),
61
-            'Category' => $this->handleCategory($default, $type, $model->getKey()),
62 60
             'Currency' => $default->currency_code = $model->getAttribute('code'),
63 61
             'BankAccount' => $default->bank_account_id = $model->getKey(),
64 62
             default => null,
@@ -90,16 +88,4 @@ class SyncWithCompanyDefaults
90 88
             $type === TaxType::Purchase => $default->purchase_tax_id = $key,
91 89
         };
92 90
     }
93
-
94
-    private function handleCategory($default, $type, $key): void
95
-    {
96
-        if (! in_array($type, [CategoryType::Income, CategoryType::Expense], true)) {
97
-            return;
98
-        }
99
-
100
-        match (true) {
101
-            $type === CategoryType::Income => $default->income_category_id = $key,
102
-            $type === CategoryType::Expense => $default->expense_category_id = $key,
103
-        };
104
-    }
105 91
 }

+ 15
- 1
app/Livewire/Company/Service/ConnectedAccount/ListInstitutions.php Voir le fichier

@@ -4,10 +4,12 @@ namespace App\Livewire\Company\Service\ConnectedAccount;
4 4
 
5 5
 use App\Events\PlaidSuccess;
6 6
 use App\Events\StartTransactionImport;
7
+use App\Models\Accounting\Account;
7 8
 use App\Models\Banking\BankAccount;
8 9
 use App\Models\Banking\ConnectedBankAccount;
9 10
 use App\Models\Banking\Institution;
10 11
 use App\Models\User;
12
+use App\Services\AccountService;
11 13
 use App\Services\PlaidService;
12 14
 use Filament\Actions\Action;
13 15
 use Filament\Actions\Concerns\InteractsWithActions;
@@ -36,13 +38,16 @@ class ListInstitutions extends Component implements HasActions, HasForms
36 38
 
37 39
     protected PlaidService $plaidService;
38 40
 
41
+    protected AccountService $accountService;
42
+
39 43
     public User $user;
40 44
 
41 45
     public string $modalWidth;
42 46
 
43
-    public function boot(PlaidService $plaidService): void
47
+    public function boot(PlaidService $plaidService, AccountService $accountService): void
44 48
     {
45 49
         $this->plaidService = $plaidService;
50
+        $this->accountService = $accountService;
46 51
     }
47 52
 
48 53
     public function mount(): void
@@ -57,6 +62,15 @@ class ListInstitutions extends Component implements HasActions, HasForms
57 62
             ->get();
58 63
     }
59 64
 
65
+    public function getAccountBalance(Account $account): ?string
66
+    {
67
+        $company = $account->company;
68
+        $startDate = $company->locale->fiscalYearStartDate();
69
+        $endDate = $company->locale->fiscalYearEndDate();
70
+
71
+        return $this->accountService->getEndingBalance($account, $startDate, $endDate)?->formatted();
72
+    }
73
+
60 74
     public function startImportingTransactions(): Action
61 75
     {
62 76
         return Action::make('startImportingTransactions')

+ 2
- 27
app/Models/Accounting/Account.php Voir le fichier

@@ -2,10 +2,8 @@
2 2
 
3 3
 namespace App\Models\Accounting;
4 4
 
5
-use App\Casts\MoneyCast;
6 5
 use App\Enums\Accounting\AccountCategory;
7 6
 use App\Enums\Accounting\AccountType;
8
-use App\Models\Setting\Category;
9 7
 use App\Models\Setting\Currency;
10 8
 use App\Observers\AccountObserver;
11 9
 use App\Traits\Blamable;
@@ -17,7 +15,6 @@ use Illuminate\Database\Eloquent\Factories\HasFactory;
17 15
 use Illuminate\Database\Eloquent\Model;
18 16
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
19 17
 use Illuminate\Database\Eloquent\Relations\HasMany;
20
-use Illuminate\Database\Eloquent\Relations\HasManyThrough;
21 18
 use Illuminate\Database\Eloquent\Relations\MorphTo;
22 19
 use Wallo\FilamentCompanies\FilamentCompanies;
23 20
 
@@ -39,11 +36,6 @@ class Account extends Model
39 36
         'code',
40 37
         'name',
41 38
         'currency_code',
42
-        'starting_balance',
43
-        'debit_balance',
44
-        'credit_balance',
45
-        'net_movement',
46
-        'ending_balance',
47 39
         'description',
48 40
         'active',
49 41
         'default',
@@ -56,11 +48,6 @@ class Account extends Model
56 48
     protected $casts = [
57 49
         'category' => AccountCategory::class,
58 50
         'type' => AccountType::class,
59
-        'starting_balance' => MoneyCast::class,
60
-        'debit_balance' => MoneyCast::class,
61
-        'credit_balance' => MoneyCast::class,
62
-        'net_movement' => MoneyCast::class,
63
-        'ending_balance' => MoneyCast::class,
64 51
         'active' => 'boolean',
65 52
         'default' => 'boolean',
66 53
     ];
@@ -70,11 +57,6 @@ class Account extends Model
70 57
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
71 58
     }
72 59
 
73
-    public function categories(): HasMany
74
-    {
75
-        return $this->hasMany(Category::class, 'account_id');
76
-    }
77
-
78 60
     public function subtype(): BelongsTo
79 61
     {
80 62
         return $this->belongsTo(AccountSubtype::class, 'subtype_id');
@@ -111,16 +93,9 @@ class Account extends Model
111 93
         return $this->morphTo();
112 94
     }
113 95
 
114
-    public function transactions(): HasManyThrough
96
+    public function transactions(): HasMany
115 97
     {
116
-        return $this->hasManyThrough(
117
-            Transaction::class,
118
-            JournalEntry::class,
119
-            'account_id',
120
-            'id',
121
-            'id',
122
-            'transaction_id',
123
-        );
98
+        return $this->hasMany(Transaction::class, 'account_id');
124 99
     }
125 100
 
126 101
     public function journalEntries(): HasMany

+ 9
- 10
app/Models/Accounting/Transaction.php Voir le fichier

@@ -5,7 +5,6 @@ namespace App\Models\Accounting;
5 5
 use App\Casts\MoneyCast;
6 6
 use App\Models\Banking\BankAccount;
7 7
 use App\Models\Common\Contact;
8
-use App\Models\Setting\Category;
9 8
 use App\Observers\TransactionObserver;
10 9
 use App\Traits\Blamable;
11 10
 use App\Traits\CompanyOwned;
@@ -27,8 +26,8 @@ class Transaction extends Model
27 26
 
28 27
     protected $fillable = [
29 28
         'company_id',
30
-        'category_id',
31
-        'bank_account_id', // 'account_id' => 'bank_account_id'
29
+        'account_id', // Account from Chart of Accounts (Income/Expense accounts)
30
+        'bank_account_id', // Cash or Bank Account
32 31
         'contact_id',
33 32
         'type',
34 33
         'method',
@@ -56,9 +55,14 @@ class Transaction extends Model
56 55
         return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
57 56
     }
58 57
 
59
-    public function category(): BelongsTo
58
+    public function account(): BelongsTo
60 59
     {
61
-        return $this->belongsTo(Category::class, 'category_id');
60
+        return $this->belongsTo(Account::class, 'account_id');
61
+    }
62
+
63
+    public function bankAccount(): BelongsTo
64
+    {
65
+        return $this->belongsTo(BankAccount::class, 'bank_account_id');
62 66
     }
63 67
 
64 68
     public function contact(): BelongsTo
@@ -71,11 +75,6 @@ class Transaction extends Model
71 75
         return $this->hasMany(JournalEntry::class, 'transaction_id');
72 76
     }
73 77
 
74
-    public function bankAccount(): BelongsTo
75
-    {
76
-        return $this->belongsTo(BankAccount::class, 'bank_account_id');
77
-    }
78
-
79 78
     public function createdBy(): BelongsTo
80 79
     {
81 80
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');

+ 14
- 0
app/Models/Banking/ConnectedBankAccount.php Voir le fichier

@@ -3,11 +3,13 @@
3 3
 namespace App\Models\Banking;
4 4
 
5 5
 use App\Enums\BankAccountType;
6
+use App\Models\Accounting\Account;
6 7
 use App\Traits\Blamable;
7 8
 use App\Traits\CompanyOwned;
8 9
 use Illuminate\Database\Eloquent\Casts\Attribute;
9 10
 use Illuminate\Database\Eloquent\Model;
10 11
 use Illuminate\Database\Eloquent\Relations\BelongsTo;
12
+use Illuminate\Database\Eloquent\Relations\HasOneThrough;
11 13
 use Wallo\FilamentCompanies\FilamentCompanies;
12 14
 
13 15
 class ConnectedBankAccount extends Model
@@ -61,6 +63,18 @@ class ConnectedBankAccount extends Model
61 63
         return $this->belongsTo(BankAccount::class, 'bank_account_id');
62 64
     }
63 65
 
66
+    public function account(): HasOneThrough
67
+    {
68
+        return $this->hasOneThrough(
69
+            Account::class,
70
+            BankAccount::class,
71
+            'id',
72
+            'accountable_id',
73
+            'bank_account_id',
74
+            'id'
75
+        );
76
+    }
77
+
64 78
     protected function maskedNumber(): Attribute
65 79
     {
66 80
         return Attribute::get(static function (mixed $value, array $attributes): ?string {

+ 5
- 6
app/Models/Company.php Voir le fichier

@@ -10,7 +10,6 @@ use App\Models\Common\Contact;
10 10
 use App\Models\Core\Department;
11 11
 use App\Models\History\AccountHistory;
12 12
 use App\Models\Setting\Appearance;
13
-use App\Models\Setting\Category;
14 13
 use App\Models\Setting\CompanyDefault;
15 14
 use App\Models\Setting\CompanyProfile;
16 15
 use App\Models\Setting\Currency;
@@ -91,11 +90,6 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
91 90
         return $this->hasOne(Appearance::class, 'company_id');
92 91
     }
93 92
 
94
-    public function categories(): HasMany
95
-    {
96
-        return $this->hasMany(Category::class, 'company_id');
97
-    }
98
-
99 93
     public function accountSubtypes(): HasMany
100 94
     {
101 95
         return $this->hasMany(AccountSubtype::class, 'company_id');
@@ -153,4 +147,9 @@ class Company extends FilamentCompaniesCompany implements HasAvatar
153 147
     {
154 148
         return $this->hasMany(Tax::class, 'company_id');
155 149
     }
150
+
151
+    public function transactions(): HasMany
152
+    {
153
+        return $this->hasMany(Accounting\Transaction::class, 'company_id');
154
+    }
156 155
 }

+ 0
- 79
app/Models/Setting/Category.php Voir le fichier

@@ -1,79 +0,0 @@
1
-<?php
2
-
3
-namespace App\Models\Setting;
4
-
5
-use App\Enums\CategoryType;
6
-use App\Models\Accounting\Account;
7
-use App\Traits\Blamable;
8
-use App\Traits\CompanyOwned;
9
-use App\Traits\HasDefault;
10
-use App\Traits\SyncsWithCompanyDefaults;
11
-use Database\Factories\Setting\CategoryFactory;
12
-use Illuminate\Database\Eloquent\Factories\Factory;
13
-use Illuminate\Database\Eloquent\Factories\HasFactory;
14
-use Illuminate\Database\Eloquent\Model;
15
-use Illuminate\Database\Eloquent\Relations\BelongsTo;
16
-use Illuminate\Database\Eloquent\Relations\HasOne;
17
-use Wallo\FilamentCompanies\FilamentCompanies;
18
-
19
-class Category extends Model
20
-{
21
-    use Blamable;
22
-    use CompanyOwned;
23
-    use HasDefault;
24
-    use HasFactory;
25
-    use SyncsWithCompanyDefaults;
26
-
27
-    protected $table = 'categories';
28
-
29
-    protected $fillable = [
30
-        'company_id',
31
-        'account_id',
32
-        'name',
33
-        'type',
34
-        'color',
35
-        'enabled',
36
-        'created_by',
37
-        'updated_by',
38
-    ];
39
-
40
-    protected $casts = [
41
-        'type' => CategoryType::class,
42
-        'enabled' => 'boolean',
43
-    ];
44
-
45
-    public function company(): BelongsTo
46
-    {
47
-        return $this->belongsTo(FilamentCompanies::companyModel(), 'company_id');
48
-    }
49
-
50
-    public function account(): BelongsTo
51
-    {
52
-        return $this->belongsTo(Account::class, 'account_id');
53
-    }
54
-
55
-    public function defaultIncomeCategory(): HasOne
56
-    {
57
-        return $this->hasOne(CompanyDefault::class, 'income_category_id');
58
-    }
59
-
60
-    public function defaultExpenseCategory(): HasOne
61
-    {
62
-        return $this->hasOne(CompanyDefault::class, 'expense_category_id');
63
-    }
64
-
65
-    public function createdBy(): BelongsTo
66
-    {
67
-        return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');
68
-    }
69
-
70
-    public function updatedBy(): BelongsTo
71
-    {
72
-        return $this->belongsTo(FilamentCompanies::userModel(), 'updated_by');
73
-    }
74
-
75
-    protected static function newFactory(): Factory
76
-    {
77
-        return CategoryFactory::new();
78
-    }
79
-}

+ 0
- 15
app/Models/Setting/CompanyDefault.php Voir le fichier

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\Models\Setting;
4 4
 
5
-use App\Enums\CategoryType;
6 5
 use App\Enums\DiscountType;
7 6
 use App\Enums\TaxType;
8 7
 use App\Models\Banking\BankAccount;
@@ -31,8 +30,6 @@ class CompanyDefault extends Model
31 30
         'purchase_tax_id',
32 31
         'sales_discount_id',
33 32
         'purchase_discount_id',
34
-        'income_category_id',
35
-        'expense_category_id',
36 33
         'created_by',
37 34
         'updated_by',
38 35
     ];
@@ -76,18 +73,6 @@ class CompanyDefault extends Model
76 73
             ->where('type', DiscountType::Purchase);
77 74
     }
78 75
 
79
-    public function incomeCategory(): BelongsTo
80
-    {
81
-        return $this->belongsTo(Category::class, 'income_category_id', 'id')
82
-            ->where('type', CategoryType::Income);
83
-    }
84
-
85
-    public function expenseCategory(): BelongsTo
86
-    {
87
-        return $this->belongsTo(Category::class, 'expense_category_id', 'id')
88
-            ->where('type', CategoryType::Expense);
89
-    }
90
-
91 76
     public function createdBy(): BelongsTo
92 77
     {
93 78
         return $this->belongsTo(FilamentCompanies::userModel(), 'created_by');

+ 21
- 4
app/Models/Setting/Localization.php Voir le fichier

@@ -33,8 +33,8 @@ class Localization extends Model
33 33
         'timezone',
34 34
         'date_format',
35 35
         'time_format',
36
-        'fiscal_year_start',
37
-        'fiscal_year_end',
36
+        'fiscal_year_end_month',
37
+        'fiscal_year_end_day',
38 38
         'week_start',
39 39
         'number_format',
40 40
         'percent_first',
@@ -45,8 +45,8 @@ class Localization extends Model
45 45
     protected $casts = [
46 46
         'date_format' => DateFormat::class,
47 47
         'time_format' => TimeFormat::class,
48
-        'fiscal_year_start' => 'date',
49
-        'fiscal_year_end' => 'date',
48
+        'fiscal_year_end_month' => 'integer',
49
+        'fiscal_year_end_day' => 'integer',
50 50
         'week_start' => WeekStart::class,
51 51
         'number_format' => NumberFormat::class,
52 52
     ];
@@ -98,6 +98,23 @@ class Localization extends Model
98 98
         return strpos($formattedPercent, '%') < strpos($formattedPercent, $test);
99 99
     }
100 100
 
101
+    public function fiscalYearStartDate(): string
102
+    {
103
+        return Carbon::parse($this->fiscalYearEndDate())->subYear()->addDay()->toDateString();
104
+    }
105
+
106
+    public function fiscalYearEndDate(): string
107
+    {
108
+        $today = now();
109
+        $fiscalYearEndThisYear = Carbon::createFromDate($today->year, $this->fiscal_year_end_month, $this->fiscal_year_end_day);
110
+
111
+        if ($today->gt($fiscalYearEndThisYear)) {
112
+            return $fiscalYearEndThisYear->copy()->addYear()->toDateString();
113
+        }
114
+
115
+        return $fiscalYearEndThisYear->toDateString();
116
+    }
117
+
101 118
     public function getDateTimeFormatAttribute(): string
102 119
     {
103 120
         return $this->date_format . ' ' . $this->time_format;

+ 3
- 57
app/Observers/JournalEntryObserver.php Voir le fichier

@@ -2,7 +2,6 @@
2 2
 
3 3
 namespace App\Observers;
4 4
 
5
-use App\Enums\Accounting\AccountCategory;
6 5
 use App\Models\Accounting\Account;
7 6
 use App\Models\Accounting\JournalEntry;
8 7
 
@@ -13,60 +12,13 @@ class JournalEntryObserver
13 12
      */
14 13
     public function created(JournalEntry $journalEntry): void
15 14
     {
16
-        $account = $journalEntry->account;
17
-
18
-        $amount = $this->cleanAmount($journalEntry->amount);
19
-
20
-        if ($account) {
21
-            $this->adjustBalance($account, $journalEntry->type, $amount);
22
-        }
23
-    }
24
-
25
-    private function cleanAmount($amount): string
26
-    {
27
-        return str_replace(',', '', $amount);
28
-    }
29
-
30
-    private function adjustBalance(Account $account, $type, $amount): void
31
-    {
32
-        $debitBalance = $this->cleanAmount($account->debit_balance);
33
-        $creditBalance = $this->cleanAmount($account->credit_balance);
34
-
35
-        if ($type === 'debit') {
36
-            $account->debit_balance = bcadd($debitBalance, $amount, 2);
37
-        } elseif ($type === 'credit') {
38
-            $account->credit_balance = bcadd($creditBalance, $amount, 2);
39
-        }
40
-
41
-        $this->updateNetMovement($account);
42
-        $this->updateEndingBalance($account);
15
+        //
43 16
     }
44 17
 
45
-    private function updateNetMovement(Account $account): void
46
-    {
47
-        $debitBalance = $this->cleanAmount($account->debit_balance);
48
-        $creditBalance = $this->cleanAmount($account->credit_balance);
49
-
50
-        $netMovementStrategy = match ($account->category) {
51
-            AccountCategory::Asset, AccountCategory::Expense => bcsub($debitBalance, $creditBalance, 2),
52
-            AccountCategory::Liability, AccountCategory::Equity, AccountCategory::Revenue => bcsub($creditBalance, $debitBalance, 2),
53
-        };
54
-
55
-        $account->net_movement = $netMovementStrategy;
56
-
57
-        $account->save();
58
-    }
59 18
 
60 19
     private function updateEndingBalance(Account $account): void
61 20
     {
62
-        $startingBalance = $this->cleanAmount($account->starting_balance);
63
-        $netMovement = $this->cleanAmount($account->net_movement);
64
-
65
-        if (in_array($account->category, [AccountCategory::Asset, AccountCategory::Liability, AccountCategory::Equity], true)) {
66
-            $account->ending_balance = bcadd($startingBalance, $netMovement, 2);
67
-        }
68
-
69
-        $account->save();
21
+        //
70 22
     }
71 23
 
72 24
     /**
@@ -74,13 +26,7 @@ class JournalEntryObserver
74 26
      */
75 27
     public function deleting(JournalEntry $journalEntry): void
76 28
     {
77
-        $account = $journalEntry->account;
78
-
79
-        if ($account) {
80
-            $amount = $this->cleanAmount($journalEntry->amount);
81
-
82
-            $this->adjustBalance($account, $journalEntry->type, -$amount);
83
-        }
29
+        //
84 30
     }
85 31
 
86 32
     /**

+ 57
- 2
app/Observers/TransactionObserver.php Voir le fichier

@@ -2,6 +2,7 @@
2 2
 
3 3
 namespace App\Observers;
4 4
 
5
+use App\Models\Accounting\Account;
5 6
 use App\Models\Accounting\JournalEntry;
6 7
 use App\Models\Accounting\Transaction;
7 8
 use Illuminate\Support\Facades\DB;
@@ -13,7 +14,32 @@ class TransactionObserver
13 14
      */
14 15
     public function created(Transaction $transaction): void
15 16
     {
16
-        //
17
+        $chartAccount = $transaction->account;
18
+        $bankAccount = $transaction->bankAccount->account;
19
+
20
+        $debitAccount = $transaction->type === 'expense' ? $chartAccount : $bankAccount;
21
+        $creditAccount = $transaction->type === 'expense' ? $bankAccount : $chartAccount;
22
+
23
+        $this->createJournalEntries($transaction, $debitAccount, $creditAccount);
24
+    }
25
+
26
+    private function createJournalEntries(Transaction $transaction, Account $debitAccount, Account $creditAccount): void
27
+    {
28
+        $debitAccount->journalEntries()->create([
29
+            'company_id' => $transaction->company_id,
30
+            'transaction_id' => $transaction->id,
31
+            'type' => 'debit',
32
+            'amount' => $transaction->amount,
33
+            'description' => $transaction->description,
34
+        ]);
35
+
36
+        $creditAccount->journalEntries()->create([
37
+            'company_id' => $transaction->company_id,
38
+            'transaction_id' => $transaction->id,
39
+            'type' => 'credit',
40
+            'amount' => $transaction->amount,
41
+            'description' => $transaction->description,
42
+        ]);
17 43
     }
18 44
 
19 45
     /**
@@ -21,7 +47,36 @@ class TransactionObserver
21 47
      */
22 48
     public function updated(Transaction $transaction): void
23 49
     {
24
-        //
50
+        $changes = $transaction->getChanges();
51
+
52
+        $relevantChanges = array_intersect_key($changes, array_flip(['amount', 'description', 'account_id', 'bank_account_id', 'type']));
53
+
54
+        if (empty($relevantChanges)) {
55
+            return;
56
+        }
57
+
58
+        $chartAccount = $transaction->account;
59
+        $bankAccount = $transaction->bankAccount->account;
60
+
61
+        $journalEntries = $transaction->journalEntries;
62
+
63
+        $debitEntry = $journalEntries->where('type', 'debit')->first();
64
+        $creditEntry = $journalEntries->where('type', 'credit')->first();
65
+
66
+        $debitAccount = $transaction->type === 'expense' ? $chartAccount : $bankAccount;
67
+        $creditAccount = $transaction->type === 'expense' ? $bankAccount : $chartAccount;
68
+
69
+        $debitEntry?->update([
70
+            'account_id' => $debitAccount->id,
71
+            'amount' => $transaction->amount,
72
+            'description' => $transaction->description,
73
+        ]);
74
+
75
+        $creditEntry?->update([
76
+            'account_id' => $creditAccount->id,
77
+            'amount' => $transaction->amount,
78
+            'description' => $transaction->description,
79
+        ]);
25 80
     }
26 81
 
27 82
     /**

+ 0
- 1
app/Providers/AuthServiceProvider.php Voir le fichier

@@ -36,7 +36,6 @@ class AuthServiceProvider extends ServiceProvider
36 36
     {
37 37
         $models = [
38 38
             Setting\Currency::class,
39
-            Setting\Category::class,
40 39
             Setting\Discount::class,
41 40
             Setting\Tax::class,
42 41
             Banking\BankAccount::class,

+ 1
- 2
app/Providers/Filament/AdminPanelProvider.php Voir le fichier

@@ -2,8 +2,8 @@
2 2
 
3 3
 namespace App\Providers\Filament;
4 4
 
5
+use App\Http\Middleware\Authenticate;
5 6
 use Exception;
6
-use Filament\Http\Middleware\Authenticate;
7 7
 use Filament\Http\Middleware\DisableBladeIconComponents;
8 8
 use Filament\Http\Middleware\DispatchServingFilamentEvent;
9 9
 use Filament\Pages;
@@ -29,7 +29,6 @@ class AdminPanelProvider extends PanelProvider
29 29
         return $panel
30 30
             ->id('admin')
31 31
             ->path('admin')
32
-            ->login()
33 32
             ->colors([
34 33
                 'primary' => Color::Amber,
35 34
             ])

+ 35
- 0
app/Repositories/Accounting/AccountSubtypeRepository.php Voir le fichier

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+namespace App\Repositories\Accounting;
4
+
5
+use App\Enums\BankAccountType;
6
+use App\Models\Accounting\AccountSubtype;
7
+use App\Models\Company;
8
+use Illuminate\Database\Eloquent\ModelNotFoundException;
9
+
10
+class AccountSubtypeRepository
11
+{
12
+    public function getDefaultAccountSubtypeByType(BankAccountType $type): string
13
+    {
14
+        return match ($type) {
15
+            BankAccountType::Depository => 'Cash and Cash Equivalents',
16
+            BankAccountType::Credit => 'Short-Term Borrowings',
17
+            BankAccountType::Loan => 'Long-Term Borrowings',
18
+            BankAccountType::Investment => 'Long-Term Investments',
19
+            BankAccountType::Other => 'Other Current Assets',
20
+        };
21
+    }
22
+
23
+    public function findAccountSubtypeByNameOrFail(Company $company, $name): AccountSubtype
24
+    {
25
+        $accountSubtype = $company->accountSubtypes()
26
+            ->where('name', $name)
27
+            ->first();
28
+
29
+        if ($accountSubtype === null) {
30
+            throw new ModelNotFoundException("Account subtype '{$accountSubtype}' not found for company '{$company->name}'");
31
+        }
32
+
33
+        return $accountSubtype;
34
+    }
35
+}

+ 35
- 0
app/Repositories/Accounting/JournalEntryRepository.php Voir le fichier

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+namespace App\Repositories\Accounting;
4
+
5
+use App\Models\Accounting\Account;
6
+
7
+class JournalEntryRepository
8
+{
9
+    public function sumAmounts(Account $account, string $type, ?string $startDate = null, ?string $endDate = null): int
10
+    {
11
+        $query = $account->journalEntries()->where('type', $type);
12
+
13
+        if ($startDate && $endDate) {
14
+            $query->whereHas('transaction', static function ($query) use ($startDate, $endDate) {
15
+                $query->whereBetween('posted_at', [$startDate, $endDate]);
16
+            });
17
+        } elseif ($startDate) {
18
+            $query->whereHas('transaction', static function ($query) use ($startDate) {
19
+                $query->where('posted_at', '<', $startDate);
20
+            });
21
+        }
22
+
23
+        return $query->sum('amount');
24
+    }
25
+
26
+    public function sumDebitAmounts(Account $account, string $startDate, ?string $endDate = null): int
27
+    {
28
+        return $this->sumAmounts($account, 'debit', $startDate, $endDate);
29
+    }
30
+
31
+    public function sumCreditAmounts(Account $account, string $startDate, ?string $endDate = null): int
32
+    {
33
+        return $this->sumAmounts($account, 'credit', $startDate, $endDate);
34
+    }
35
+}

+ 35
- 0
app/Repositories/Banking/ConnectedBankAccountRepository.php Voir le fichier

@@ -0,0 +1,35 @@
1
+<?php
2
+
3
+namespace App\Repositories\Banking;
4
+
5
+use App\Models\Accounting\Account;
6
+use App\Models\Accounting\AccountSubtype;
7
+use App\Models\Banking\BankAccount;
8
+use App\Models\Banking\ConnectedBankAccount;
9
+use App\Models\Company;
10
+
11
+class ConnectedBankAccountRepository
12
+{
13
+    public function createBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount)
14
+    {
15
+        return $connectedBankAccount->bankAccount()->create([
16
+            'company_id' => $company->id,
17
+            'institution_id' => $connectedBankAccount->institution_id,
18
+            'type' => $connectedBankAccount->type,
19
+            'number' => $connectedBankAccount->mask,
20
+            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
21
+        ]);
22
+    }
23
+
24
+    public function createAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, BankAccount $bankAccount, AccountSubtype $accountSubtype): Account
25
+    {
26
+        return $bankAccount->account()->create([
27
+            'company_id' => $company->id,
28
+            'name' => $connectedBankAccount->name,
29
+            'currency_code' => $connectedBankAccount->currency_code,
30
+            'description' => $connectedBankAccount->name,
31
+            'subtype_id' => $accountSubtype->id,
32
+            'active' => true,
33
+        ]);
34
+    }
35
+}

+ 42
- 0
app/Repositories/Setting/CurrencyRepository.php Voir le fichier

@@ -0,0 +1,42 @@
1
+<?php
2
+
3
+namespace App\Repositories\Setting;
4
+
5
+use App\Models\Company;
6
+use App\Models\Setting\Currency;
7
+
8
+class CurrencyRepository
9
+{
10
+    public function ensureCurrencyExists(Company $company, string $currencyCode): Currency
11
+    {
12
+        $hasDefaultCurrency = $this->hasDefaultCurrency($company);
13
+
14
+        $currency = currency($currencyCode);
15
+
16
+        return $company->currencies()
17
+            ->firstOrCreate([
18
+                'code' => $currencyCode,
19
+            ], [
20
+                'name' => $currency->getName(),
21
+                'rate' => $currency->getRate(),
22
+                'precision' => $currency->getPrecision(),
23
+                'symbol' => $currency->getSymbol(),
24
+                'symbol_first' => $currency->isSymbolFirst(),
25
+                'decimal_mark' => $currency->getDecimalMark(),
26
+                'thousands_separator' => $currency->getThousandsSeparator(),
27
+                'enabled' => ! $hasDefaultCurrency,
28
+            ]);
29
+    }
30
+
31
+    public function getDefaultCurrency(Company $company): ?Currency
32
+    {
33
+        return $company->currencies()
34
+            ->where('enabled', true)
35
+            ->first();
36
+    }
37
+
38
+    public function hasDefaultCurrency(Company $company): bool
39
+    {
40
+        return $this->getDefaultCurrency($company) !== null;
41
+    }
42
+}

+ 76
- 0
app/Services/AccountBalancesExportService.php Voir le fichier

@@ -0,0 +1,76 @@
1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\DTO\AccountBalanceReportDTO;
6
+use App\Models\Company;
7
+use Symfony\Component\HttpFoundation\StreamedResponse;
8
+
9
+class AccountBalancesExportService
10
+{
11
+    public function exportToCsv(Company $company, AccountBalanceReportDTO $accountBalanceReport, string $startDate, string $endDate): StreamedResponse
12
+    {
13
+        // Construct the filename
14
+        $filename = $company->name . ' Account Balances ' . $startDate . ' to ' . $endDate . '.csv';
15
+
16
+        $headers = [
17
+            'Content-Type' => 'text/csv',
18
+            'Content-Disposition' => 'attachment; filename="' . $filename . '"',
19
+        ];
20
+
21
+        $callback = static function () use ($company, $startDate, $endDate, $accountBalanceReport) {
22
+            $file = fopen('php://output', 'wb');
23
+
24
+            fputcsv($file, ['Account Balances']);
25
+            fputcsv($file, [$company->name]);
26
+            fputcsv($file, ['Date Range: ' . $startDate . ' to ' . $endDate]);
27
+            fputcsv($file, []);
28
+
29
+            fputcsv($file, ['ACCOUNT CODE', 'ACCOUNT', 'STARTING BALANCE', 'DEBIT', 'CREDIT', 'NET MOVEMENT', 'ENDING BALANCE']);
30
+
31
+            foreach ($accountBalanceReport->categories as $accountCategoryName => $accountCategory) {
32
+                fputcsv($file, ['', $accountCategoryName]);
33
+
34
+                foreach ($accountCategory->accounts as $account) {
35
+                    fputcsv($file, [
36
+                        $account->accountCode,
37
+                        $account->accountName,
38
+                        $account->balance->startingBalance ?? '',
39
+                        $account->balance->debitBalance,
40
+                        $account->balance->creditBalance,
41
+                        $account->balance->netMovement,
42
+                        $account->balance->endingBalance ?? '',
43
+                    ]);
44
+                }
45
+
46
+                // Category Summary row
47
+                fputcsv($file, [
48
+                    '',
49
+                    'Total ' . $accountCategoryName,
50
+                    $accountCategory->summary->startingBalance ?? '',
51
+                    $accountCategory->summary->debitBalance,
52
+                    $accountCategory->summary->creditBalance,
53
+                    $accountCategory->summary->netMovement,
54
+                    $accountCategory->summary->endingBalance ?? '',
55
+                ]);
56
+
57
+                fputcsv($file, []);
58
+            }
59
+
60
+            // Final Row for overall totals
61
+            fputcsv($file, [
62
+                '',
63
+                'Total for all accounts',
64
+                '',
65
+                $accountBalanceReport->overallTotal->debitBalance,
66
+                $accountBalanceReport->overallTotal->creditBalance,
67
+                '',
68
+                '',
69
+            ]);
70
+
71
+            fclose($file);
72
+        };
73
+
74
+        return response()->streamDownload($callback, $filename, $headers);
75
+    }
76
+}

+ 179
- 58
app/Services/AccountService.php Voir le fichier

@@ -2,89 +2,210 @@
2 2
 
3 3
 namespace App\Services;
4 4
 
5
+use Akaunting\Money\Money;
6
+use App\DTO\AccountBalanceDTO;
7
+use App\DTO\AccountBalanceReportDTO;
8
+use App\DTO\AccountCategoryDTO;
9
+use App\DTO\AccountDTO;
10
+use App\Enums\Accounting\AccountCategory;
5 11
 use App\Models\Accounting\Account;
6
-use App\Models\Banking\BankAccount;
7
-use App\Models\Banking\ConnectedBankAccount;
8
-use App\Models\Company;
9
-use App\Models\Setting\Currency;
10
-use Illuminate\Database\Eloquent\ModelNotFoundException;
12
+use App\Models\Accounting\Transaction;
13
+use App\Repositories\Accounting\JournalEntryRepository;
14
+use App\ValueObjects\BalanceValue;
15
+use Illuminate\Database\Eloquent\Collection;
11 16
 
12 17
 class AccountService
13 18
 {
14
-    public function getOrProcessAccount(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
19
+    protected JournalEntryRepository $journalEntryRepository;
20
+
21
+    public function __construct(JournalEntryRepository $journalEntryRepository)
15 22
     {
16
-        if ($bankAccount->account()->doesntExist()) {
17
-            return $this->processNewAccount($bankAccount, $company, $connectedBankAccount);
18
-        }
23
+        $this->journalEntryRepository = $journalEntryRepository;
24
+    }
25
+
26
+    public function getDebitBalance(Account $account, string $startDate, string $endDate): BalanceValue
27
+    {
28
+        $amount = $this->journalEntryRepository->sumDebitAmounts($account, $startDate, $endDate);
29
+
30
+        return new BalanceValue($amount, $account->currency_code ?? 'USD');
31
+    }
32
+
33
+    public function getCreditBalance(Account $account, string $startDate, string $endDate): BalanceValue
34
+    {
35
+        $amount = $this->journalEntryRepository->sumCreditAmounts($account, $startDate, $endDate);
19 36
 
20
-        return $bankAccount->account;
37
+        return new BalanceValue($amount, $account->currency_code ?? 'USD');
21 38
     }
22 39
 
23
-    public function processNewAccount(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
40
+    public function getNetMovement(Account $account, string $startDate, string $endDate): BalanceValue
24 41
     {
25
-        $currencyCode = $connectedBankAccount->currency_code ?? 'USD';
42
+        $debitBalance = $this->getDebitBalance($account, $startDate, $endDate)->getValue();
43
+        $creditBalance = $this->getCreditBalance($account, $startDate, $endDate)->getValue();
44
+        $netMovement = $this->calculateNetMovementByCategory($account->category, $debitBalance, $creditBalance);
26 45
 
27
-        $currency = $this->ensureCurrencyExists($company, $currencyCode);
46
+        return new BalanceValue($netMovement, $account->currency_code ?? 'USD');
47
+    }
28 48
 
29
-        $accountSubtype = $this->getAccountSubtype($bankAccount->type->value);
49
+    public function getStartingBalance(Account $account, string $startDate): ?BalanceValue
50
+    {
51
+        if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
52
+            return null;
53
+        }
30 54
 
31
-        $accountSubtypeId = $this->resolveAccountSubtypeId($company, $accountSubtype);
55
+        $debitBalanceBefore = $this->journalEntryRepository->sumDebitAmounts($account, $startDate);
56
+        $creditBalanceBefore = $this->journalEntryRepository->sumCreditAmounts($account, $startDate);
57
+        $startingBalance = $this->calculateNetMovementByCategory($account->category, $debitBalanceBefore, $creditBalanceBefore);
32 58
 
33
-        return $bankAccount->account()->create([
34
-            'company_id' => $company->id,
35
-            'name' => $connectedBankAccount->name,
36
-            'currency_code' => $currency->code,
37
-            'description' => $connectedBankAccount->name,
38
-            'subtype_id' => $accountSubtypeId,
39
-            'active' => true,
40
-        ]);
59
+        return new BalanceValue($startingBalance, $account->currency_code ?? 'USD');
41 60
     }
42 61
 
43
-    protected function ensureCurrencyExists(Company $company, string $currencyCode): Currency
62
+    public function getEndingBalance(Account $account, string $startDate, string $endDate): ?BalanceValue
44 63
     {
45
-        $currencyRelationship = $company->currencies();
46
-
47
-        $defaultCurrency = $currencyRelationship->firstWhere('enabled', true);
48
-
49
-        $hasDefaultCurrency = $defaultCurrency !== null;
50
-
51
-        $currency_code = currency($currencyCode);
52
-
53
-        return $currencyRelationship->firstOrCreate([
54
-            'code' => $currencyCode,
55
-        ], [
56
-            'name' => $currency_code->getName(),
57
-            'rate' => $currency_code->getRate(),
58
-            'precision' => $currency_code->getPrecision(),
59
-            'symbol' => $currency_code->getSymbol(),
60
-            'symbol_first' => $currency_code->isSymbolFirst(),
61
-            'decimal_mark' => $currency_code->getDecimalMark(),
62
-            'thousands_separator' => $currency_code->getThousandsSeparator(),
63
-            'enabled' => ! $hasDefaultCurrency,
64
-        ]);
64
+        if (in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
65
+            return null;
66
+        }
67
+
68
+        $startingBalance = $this->getStartingBalance($account, $startDate)?->getValue();
69
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getValue();
70
+        $endingBalance = $startingBalance + $netMovement;
71
+
72
+        return new BalanceValue($endingBalance, $account->currency_code ?? 'USD');
65 73
     }
66 74
 
67
-    protected function getAccountSubtype(string $plaidType): string
75
+    public function calculateNetMovementByCategory(AccountCategory $category, int $debitBalance, int $creditBalance): int
68 76
     {
69
-        return match ($plaidType) {
70
-            'depository' => 'Cash and Cash Equivalents',
71
-            'credit' => 'Short-Term Borrowings',
72
-            'loan' => 'Long-Term Borrowings',
73
-            'investment' => 'Long-Term Investments',
74
-            'other' => 'Other Current Assets',
77
+        return match ($category) {
78
+            AccountCategory::Asset, AccountCategory::Expense => $debitBalance - $creditBalance,
79
+            AccountCategory::Liability, AccountCategory::Equity, AccountCategory::Revenue => $creditBalance - $debitBalance,
75 80
         };
76 81
     }
77 82
 
78
-    protected function resolveAccountSubtypeId(Company $company, string $accountSubtype): int
83
+    public function getBalances(Account $account, string $startDate, string $endDate): array
84
+    {
85
+        $debitBalance = $this->getDebitBalance($account, $startDate, $endDate)->getValue();
86
+        $creditBalance = $this->getCreditBalance($account, $startDate, $endDate)->getValue();
87
+        $netMovement = $this->getNetMovement($account, $startDate, $endDate)->getValue();
88
+
89
+        $balances = [
90
+            'debit_balance' => $debitBalance,
91
+            'credit_balance' => $creditBalance,
92
+            'net_movement' => $netMovement,
93
+        ];
94
+
95
+        if (! in_array($account->category, [AccountCategory::Expense, AccountCategory::Revenue], true)) {
96
+            $balances['starting_balance'] = $this->getStartingBalance($account, $startDate)?->getValue();
97
+            $balances['ending_balance'] = $this->getEndingBalance($account, $startDate, $endDate)?->getValue();
98
+        }
99
+
100
+        return $balances;
101
+    }
102
+
103
+    public function getBalancesFormatted(Account $account, string $startDate, string $endDate): AccountBalanceDTO
79 104
     {
80
-        $accountSubtypeId = $company->accountSubtypes()
81
-            ->where('name', $accountSubtype)
82
-            ->value('id');
105
+        $balances = $this->getBalances($account, $startDate, $endDate);
106
+        $currency = $account->currency_code ?? 'USD';
107
+
108
+        return $this->formatBalances($balances, $currency);
109
+    }
110
+
111
+    public function formatBalances(array $balances, string $currency): AccountBalanceDTO
112
+    {
113
+        foreach ($balances as $key => $balance) {
114
+            $balances[$key] = Money::{$currency}($balance)->format();
115
+        }
116
+
117
+        return new AccountBalanceDTO(
118
+            startingBalance: $balances['starting_balance'] ?? null,
119
+            debitBalance: $balances['debit_balance'],
120
+            creditBalance: $balances['credit_balance'],
121
+            netMovement: $balances['net_movement'] ?? null,
122
+            endingBalance: $balances['ending_balance'] ?? null,
123
+        );
124
+    }
83 125
 
84
-        if ($accountSubtypeId === null) {
85
-            throw new ModelNotFoundException("Account subtype '{$accountSubtype}' not found for company '{$company->name}'");
126
+    public function buildAccountBalanceReport(string $startDate, string $endDate): AccountBalanceReportDTO
127
+    {
128
+        $allCategories = $this->getAccountCategoryOrder();
129
+
130
+        $categoryGroupedAccounts = Account::whereHas('journalEntries')
131
+            ->select('id', 'name', 'currency_code', 'category', 'code')
132
+            ->get()
133
+            ->groupBy(fn (Account $account) => $account->category->getPluralLabel())
134
+            ->sortBy(static fn (Collection $groupedAccounts, string $key) => array_search($key, $allCategories, true));
135
+
136
+        $accountCategories = [];
137
+        $reportTotalBalances = [
138
+            'debit_balance' => 0,
139
+            'credit_balance' => 0,
140
+        ];
141
+
142
+        foreach ($allCategories as $categoryName) {
143
+            $accountsInCategory = $categoryGroupedAccounts[$categoryName] ?? collect();
144
+            $categorySummaryBalances = [
145
+                'debit_balance' => 0,
146
+                'credit_balance' => 0,
147
+                'net_movement' => 0,
148
+            ];
149
+
150
+            if (! in_array($categoryName, [AccountCategory::Expense->getPluralLabel(), AccountCategory::Revenue->getPluralLabel()], true)) {
151
+                $categorySummaryBalances['starting_balance'] = 0;
152
+                $categorySummaryBalances['ending_balance'] = 0;
153
+            }
154
+
155
+            $categoryAccounts = [];
156
+
157
+            foreach ($accountsInCategory as $account) {
158
+                $accountBalances = $this->getBalances($account, $startDate, $endDate);
159
+
160
+                if (array_sum($accountBalances) === 0) {
161
+                    continue;
162
+                }
163
+
164
+                foreach ($accountBalances as $accountBalanceType => $accountBalance) {
165
+                    $categorySummaryBalances[$accountBalanceType] += $accountBalance;
166
+                }
167
+
168
+                $formattedAccountBalances = $this->formatBalances($accountBalances, $account->currency_code ?? 'USD');
169
+
170
+                $categoryAccounts[] = new AccountDTO(
171
+                    $account->name,
172
+                    $account->code,
173
+                    $formattedAccountBalances,
174
+                );
175
+            }
176
+
177
+            $reportTotalBalances['debit_balance'] += $categorySummaryBalances['debit_balance'];
178
+            $reportTotalBalances['credit_balance'] += $categorySummaryBalances['credit_balance'];
179
+
180
+            $formattedCategorySummaryBalances = $this->formatBalances($categorySummaryBalances, $accountsInCategory->first()->currency_code ?? 'USD');
181
+
182
+            $accountCategories[$categoryName] = new AccountCategoryDTO(
183
+                $categoryAccounts,
184
+                $formattedCategorySummaryBalances,
185
+            );
86 186
         }
87 187
 
88
-        return $accountSubtypeId;
188
+        $formattedReportTotalBalances = $this->formatBalances($reportTotalBalances, 'USD');
189
+
190
+        return new AccountBalanceReportDTO($accountCategories, $formattedReportTotalBalances);
191
+    }
192
+
193
+    public function getAccountCategoryOrder(): array
194
+    {
195
+        return [
196
+            AccountCategory::Asset->getPluralLabel(),
197
+            AccountCategory::Liability->getPluralLabel(),
198
+            AccountCategory::Equity->getPluralLabel(),
199
+            AccountCategory::Revenue->getPluralLabel(),
200
+            AccountCategory::Expense->getPluralLabel(),
201
+        ];
202
+    }
203
+
204
+    public function getEarliestTransactionDate(): string
205
+    {
206
+        $earliestDate = Transaction::oldest('posted_at')
207
+            ->value('posted_at');
208
+
209
+        return $earliestDate ?? now()->format('Y-m-d');
89 210
     }
90 211
 }

+ 0
- 30
app/Services/BankAccountService.php Voir le fichier

@@ -1,30 +0,0 @@
1
-<?php
2
-
3
-namespace App\Services;
4
-
5
-use App\Models\Banking\BankAccount;
6
-use App\Models\Banking\ConnectedBankAccount;
7
-use App\Models\Company;
8
-
9
-class BankAccountService
10
-{
11
-    public function getOrProcessBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
12
-    {
13
-        if ($selectedBankAccountId === 'new') {
14
-            return $this->processNewBankAccount($company, $connectedBankAccount);
15
-        }
16
-
17
-        return $company->bankAccounts()->find($selectedBankAccountId);
18
-    }
19
-
20
-    protected function processNewBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount): BankAccount
21
-    {
22
-        return $connectedBankAccount->bankAccount()->create([
23
-            'company_id' => $company->id,
24
-            'institution_id' => $connectedBankAccount->institution_id,
25
-            'type' => $connectedBankAccount->type,
26
-            'number' => $connectedBankAccount->mask,
27
-            'enabled' => BankAccount::where('company_id', $company->id)->where('enabled', true)->doesntExist(),
28
-        ]);
29
-    }
30
-}

+ 0
- 45
app/Services/CompanyDefaultService.php Voir le fichier

@@ -2,10 +2,8 @@
2 2
 
3 3
 namespace App\Services;
4 4
 
5
-use App\Enums\CategoryType;
6 5
 use App\Models\Company;
7 6
 use App\Models\Setting\Appearance;
8
-use App\Models\Setting\Category;
9 7
 use App\Models\Setting\Currency;
10 8
 use App\Models\Setting\Discount;
11 9
 use App\Models\Setting\DocumentDefault;
@@ -19,7 +17,6 @@ class CompanyDefaultService
19 17
     public function createCompanyDefaults(Company $company, User $user, string $currencyCode, string $countryCode, string $language): void
20 18
     {
21 19
         DB::transaction(function () use ($company, $user, $currencyCode, $countryCode, $language) {
22
-            $this->createCategories($company, $user);
23 20
             $this->createCurrency($company, $user, $currencyCode);
24 21
             $this->createSalesTax($company, $user);
25 22
             $this->createPurchaseTax($company, $user);
@@ -31,48 +28,6 @@ class CompanyDefaultService
31 28
         }, 5);
32 29
     }
33 30
 
34
-    private function createCategories(Company $company, User $user): void
35
-    {
36
-        $incomeCategories = ['Dividends', 'Interest Earned', 'Wages', 'Sales', 'Other Income'];
37
-        $expenseCategories = ['Rent or Mortgage', 'Utilities', 'Groceries', 'Transportation', 'Other Expense'];
38
-        $otherCategories = ['Transfer', 'Other'];
39
-
40
-        $defaultIncomeCategory = 'Sales';
41
-        $defaultExpenseCategory = 'Rent or Mortgage';
42
-
43
-        $this->createCategory($company, $user, $defaultIncomeCategory, CategoryType::Income, true);
44
-        $this->createCategory($company, $user, $defaultExpenseCategory, CategoryType::Expense, true);
45
-
46
-        foreach ($incomeCategories as $incomeCategory) {
47
-            if ($incomeCategory !== $defaultIncomeCategory) {
48
-                $this->createCategory($company, $user, $incomeCategory, CategoryType::Income);
49
-            }
50
-        }
51
-
52
-        foreach ($expenseCategories as $expenseCategory) {
53
-            if ($expenseCategory !== $defaultExpenseCategory) {
54
-                $this->createCategory($company, $user, $expenseCategory, CategoryType::Expense);
55
-            }
56
-        }
57
-
58
-        foreach ($otherCategories as $otherCategory) {
59
-            $this->createCategory($company, $user, $otherCategory, CategoryType::Other);
60
-        }
61
-
62
-    }
63
-
64
-    private function createCategory(Company $company, User $user, string $name, CategoryType $type, bool $enabled = false): void
65
-    {
66
-        Category::factory()->create([
67
-            'company_id' => $company->id,
68
-            'name' => $name,
69
-            'type' => $type,
70
-            'enabled' => $enabled,
71
-            'created_by' => $user->id,
72
-            'updated_by' => $user->id,
73
-        ]);
74
-    }
75
-
76 31
     private function createCurrency(Company $company, User $user, string $currencyCode): void
77 32
     {
78 33
         Currency::factory()->forCurrency($currencyCode)->create([

+ 50
- 0
app/Services/ConnectedBankAccountService.php Voir le fichier

@@ -0,0 +1,50 @@
1
+<?php
2
+
3
+namespace App\Services;
4
+
5
+use App\Models\Accounting\Account;
6
+use App\Models\Banking\BankAccount;
7
+use App\Models\Banking\ConnectedBankAccount;
8
+use App\Models\Company;
9
+use App\Repositories\Accounting\AccountSubtypeRepository;
10
+use App\Repositories\Banking\ConnectedBankAccountRepository;
11
+
12
+class ConnectedBankAccountService
13
+{
14
+    protected AccountSubtypeRepository $accountSubtypeRepository;
15
+
16
+    protected ConnectedBankAccountRepository $connectedBankAccountRepository;
17
+
18
+    public function __construct(AccountSubtypeRepository $accountSubtypeRepository, ConnectedBankAccountRepository $connectedBankAccountRepository)
19
+    {
20
+        $this->accountSubtypeRepository = $accountSubtypeRepository;
21
+        $this->connectedBankAccountRepository = $connectedBankAccountRepository;
22
+    }
23
+
24
+    public function getOrProcessBankAccountForConnectedBankAccount(Company $company, ConnectedBankAccount $connectedBankAccount, int | string $selectedBankAccountId): BankAccount
25
+    {
26
+        if ($selectedBankAccountId === 'new') {
27
+            return $this->connectedBankAccountRepository->createBankAccountForConnectedBankAccount($company, $connectedBankAccount);
28
+        }
29
+
30
+        return $company->bankAccounts()->find($selectedBankAccountId);
31
+    }
32
+
33
+    public function getOrProcessAccountForConnectedBankAccount(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
34
+    {
35
+        if ($bankAccount->account()->doesntExist()) {
36
+            return $this->processNewAccountForBank($bankAccount, $company, $connectedBankAccount);
37
+        }
38
+
39
+        return $bankAccount->account;
40
+    }
41
+
42
+    public function processNewAccountForBank(BankAccount $bankAccount, Company $company, ConnectedBankAccount $connectedBankAccount): Account
43
+    {
44
+        $defaultAccountSubtypeName = $this->accountSubtypeRepository->getDefaultAccountSubtypeByType($bankAccount->type);
45
+
46
+        $accountSubtype = $this->accountSubtypeRepository->findAccountSubtypeByNameOrFail($company, $defaultAccountSubtypeName);
47
+
48
+        return $this->connectedBankAccountRepository->createAccountForConnectedBankAccount($company, $connectedBankAccount, $bankAccount, $accountSubtype);
49
+    }
50
+}

+ 15
- 3
app/Services/CurrencyService.php Voir le fichier

@@ -76,19 +76,31 @@ class CurrencyService implements CurrencyHandler
76 76
         $filteredCurrencies = array_keys($filteredRates);
77 77
         $missingCurrencies = array_diff($targetCurrencies, $filteredCurrencies);
78 78
 
79
-        return filled($missingCurrencies) ? null : $filteredRates;
79
+        if (filled($missingCurrencies)) {
80
+            return null;
81
+        }
82
+
83
+        return $filteredRates;
80 84
     }
81 85
 
82 86
     public function getCachedExchangeRates(string $baseCurrency, array $targetCurrencies): ?array
83 87
     {
84
-        return $this->isEnabled() ? $this->getExchangeRates($baseCurrency, $targetCurrencies) : null;
88
+        if ($this->isEnabled()) {
89
+            return $this->getExchangeRates($baseCurrency, $targetCurrencies);
90
+        }
91
+
92
+        return null;
85 93
     }
86 94
 
87 95
     public function getCachedExchangeRate(string $baseCurrency, string $targetCurrency): ?float
88 96
     {
89 97
         $rates = $this->getCachedExchangeRates($baseCurrency, [$targetCurrency]);
90 98
 
91
-        return isset($rates[$targetCurrency]) ? (float) $rates[$targetCurrency] : null;
99
+        if (isset($rates[$targetCurrency])) {
100
+            return (float) $rates[$targetCurrency];
101
+        }
102
+
103
+        return null;
92 104
     }
93 105
 
94 106
     public function updateCurrencyRatesCache(string $baseCurrency): ?array

+ 0
- 58
app/Services/PlaidService.php Voir le fichier

@@ -163,22 +163,6 @@ class PlaidService
163 163
         return 'US';
164 164
     }
165 165
 
166
-    public function createSandboxPublicToken(): object
167
-    {
168
-        return $this->createPublicToken('ins_109508', ['auth']);
169
-    }
170
-
171
-    public function createPublicToken(string $institution_id, array $initial_products, array $options = []): object
172
-    {
173
-        $data = [
174
-            'institution_id' => $institution_id,
175
-            'initial_products' => $initial_products,
176
-            'options' => (object) $options,
177
-        ];
178
-
179
-        return $this->sendRequest('sandbox/public_token/create', $data);
180
-    }
181
-
182 166
     public function createToken(string $language, string $country, array $user, array $products = []): object
183 167
     {
184 168
         $plaidLanguage = $this->getLanguage($language);
@@ -227,21 +211,6 @@ class PlaidService
227 211
         return $this->sendRequest('accounts/get', $data);
228 212
     }
229 213
 
230
-    public function getAuth($access_token): object
231
-    {
232
-        return $this->authGet($access_token);
233
-    }
234
-
235
-    public function authGet(string $access_token, array $options = []): object
236
-    {
237
-        $data = [
238
-            'access_token' => $access_token,
239
-            'options' => (object) $options,
240
-        ];
241
-
242
-        return $this->sendRequest('auth/get', $data);
243
-    }
244
-
245 214
     public function getInstitution(string $institution_id, string $country): object
246 215
     {
247 216
         $options = [
@@ -253,18 +222,6 @@ class PlaidService
253 222
         return $this->getInstitutionById($institution_id, [$plaidCountry], $options);
254 223
     }
255 224
 
256
-    public function getInstitutions(int $count, int $offset, array $country_codes, array $options = []): object
257
-    {
258
-        $data = [
259
-            'count' => $count,
260
-            'offset' => $offset,
261
-            'country_codes' => $country_codes,
262
-            'options' => (object) $options,
263
-        ];
264
-
265
-        return $this->sendRequest('institutions/get', $data);
266
-    }
267
-
268 225
     public function getInstitutionById(string $institution_id, array $country_codes, array $options = []): object
269 226
     {
270 227
         $data = [
@@ -276,21 +233,6 @@ class PlaidService
276 233
         return $this->sendRequest('institutions/get_by_id', $data);
277 234
     }
278 235
 
279
-    public function syncTransactions(string $access_token, ?string $cursor = null, int $count = 100, array $options = []): object
280
-    {
281
-        $data = [
282
-            'access_token' => $access_token,
283
-            'count' => $count,
284
-            'options' => (object) $options,
285
-        ];
286
-
287
-        if ($cursor !== null) {
288
-            $data['cursor'] = $cursor;
289
-        }
290
-
291
-        return $this->sendRequest('transactions/sync', $data);
292
-    }
293
-
294 236
     public function getTransactions(string $access_token, string $start_date, string $end_date, array $options = []): object
295 237
     {
296 238
         $data = [

+ 41
- 107
app/Services/TransactionService.php Voir le fichier

@@ -6,15 +6,14 @@ use App\Enums\Accounting\AccountCategory;
6 6
 use App\Enums\Accounting\AccountType;
7 7
 use App\Models\Accounting\Account;
8 8
 use App\Models\Accounting\Transaction;
9
-use App\Models\Banking\ConnectedBankAccount;
9
+use App\Models\Banking\BankAccount;
10 10
 use App\Models\Company;
11
-use App\Models\Setting\Category;
12 11
 use App\Scopes\CurrentCompanyScope;
13 12
 use Illuminate\Support\Carbon;
14 13
 
15 14
 class TransactionService
16 15
 {
17
-    public function createStartingBalanceIfNeeded(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, array $transactions, float $currentBalance, string $startDate): void
16
+    public function createStartingBalanceIfNeeded(Company $company, Account $account, BankAccount $bankAccount, array $transactions, float $currentBalance, string $startDate): void
18 17
     {
19 18
         if ($account->transactions()->withoutGlobalScope(CurrentCompanyScope::class)->doesntExist()) {
20 19
             $accountSign = $account->category === AccountCategory::Asset ? 1 : -1;
@@ -27,112 +26,86 @@ class TransactionService
27 26
 
28 27
             $startingBalance = bcsub($adjustedBalance, $sumOfTransactions, 2);
29 28
 
30
-            $this->createStartingBalanceTransaction($company, $account, $connectedBankAccount, (float) $startingBalance, $startDate);
29
+            $this->createStartingBalanceTransaction($company, $account, $bankAccount, (float) $startingBalance, $startDate);
31 30
         }
32 31
     }
33 32
 
34
-    public function storeTransactions(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, array $transactions): void
33
+    public function storeTransactions(Company $company, BankAccount $bankAccount, array $transactions): void
35 34
     {
36 35
         foreach ($transactions as $transaction) {
37
-            $this->storeTransaction($company, $account, $connectedBankAccount, $transaction);
36
+            $this->storeTransaction($company, $bankAccount, $transaction);
38 37
         }
39 38
     }
40 39
 
41
-    public function createStartingBalanceTransaction(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, float $startingBalance, string $startDate): void
40
+    public function createStartingBalanceTransaction(Company $company, Account $account, BankAccount $bankAccount, float $startingBalance, string $startDate): void
42 41
     {
43 42
         [$transactionType, $method] = $startingBalance >= 0 ? ['income', 'deposit'] : ['expense', 'withdrawal'];
44
-        $category = $this->getUncategorizedCategory($company, $transactionType);
45 43
         $chartAccount = $account->where('category', AccountCategory::Equity)->where('name', 'Owner\'s Equity')->first();
44
+        $postedAt = Carbon::parse($startDate)->subDay()->toDateTimeString();
46 45
 
47
-        $transactionRecord = $account->transactions()->create([
46
+        Transaction::create([
48 47
             'company_id' => $company->id,
49
-            'category_id' => $category->id,
50
-            'bank_account_id' => $connectedBankAccount->bank_account_id,
48
+            'account_id' => $chartAccount->id,
49
+            'bank_account_id' => $bankAccount->id,
51 50
             'type' => $transactionType,
52 51
             'amount' => abs($startingBalance),
53 52
             'method' => $method,
54 53
             'payment_channel' => 'other',
55
-            'posted_at' => $startDate,
54
+            'posted_at' => $postedAt,
56 55
             'description' => 'Starting Balance',
57 56
             'pending' => false,
58
-            'reviewed' => true,
57
+            'reviewed' => false,
59 58
         ]);
60
-
61
-        $this->createJournalEntries($company, $account, $transactionRecord, $chartAccount);
62 59
     }
63 60
 
64
-    public function storeTransaction(Company $company, Account $account, ConnectedBankAccount $connectedBankAccount, object $transaction): void
61
+    public function storeTransaction(Company $company, BankAccount $bankAccount, object $transaction): void
65 62
     {
66 63
         [$transactionType, $method] = $transaction->amount < 0 ? ['income', 'deposit'] : ['expense', 'withdrawal'];
67 64
         $paymentChannel = $transaction->payment_channel;
68
-        $category = $this->getCategoryFromTransaction($company, $transaction, $transactionType);
69
-        $chartAccount = $category->account ?? $this->getChartFromTransaction($company, $transactionType);
65
+        $chartAccount = $this->getAccountFromTransaction($company, $transaction, $transactionType);
70 66
         $postedAt = $transaction->datetime ?? Carbon::parse($transaction->date)->toDateTimeString();
71 67
         $description = $transaction->name;
72 68
 
73
-        $transactionRecord = $account->transactions()->create([
69
+        Transaction::create([
74 70
             'company_id' => $company->id,
75
-            'category_id' => $category->id,
76
-            'bank_account_id' => $connectedBankAccount->bank_account_id,
71
+            'account_id' => $chartAccount->id,
72
+            'bank_account_id' => $bankAccount->id,
77 73
             'type' => $transactionType,
78 74
             'amount' => abs($transaction->amount),
79 75
             'method' => $method,
80 76
             'payment_channel' => $paymentChannel,
81 77
             'posted_at' => $postedAt,
82 78
             'description' => $description,
83
-            'pending' => $transaction->pending,
79
+            'pending' => false,
84 80
             'reviewed' => false,
85 81
         ]);
86
-
87
-        $this->createJournalEntries($company, $account, $transactionRecord, $chartAccount);
88 82
     }
89 83
 
90
-    public function createJournalEntries(Company $company, Account $account, Transaction $transaction, Account $chartAccount): void
84
+    public function getAccountFromTransaction(Company $company, object $transaction, string $transactionType): Account
91 85
     {
92
-        $debitAccount = $transaction->type === 'expense' ? $chartAccount : $account;
93
-        $creditAccount = $transaction->type === 'expense' ? $account : $chartAccount;
94
-
95
-        $amount = $transaction->amount;
96
-
97
-        $debitAccount->journalEntries()->create([
98
-            'company_id' => $company->id,
99
-            'transaction_id' => $transaction->id,
100
-            'type' => 'debit',
101
-            'amount' => $amount,
102
-            'description' => $transaction->description,
103
-        ]);
104
-
105
-        $creditAccount->journalEntries()->create([
106
-            'company_id' => $company->id,
107
-            'transaction_id' => $transaction->id,
108
-            'type' => 'credit',
109
-            'amount' => $amount,
110
-            'description' => $transaction->description,
111
-        ]);
112
-    }
86
+        $accountCategory = match ($transactionType) {
87
+            'income' => AccountCategory::Revenue,
88
+            'expense' => AccountCategory::Expense,
89
+        };
113 90
 
114
-    public function getCategoryFromTransaction(Company $company, object $transaction, string $transactionType): Category
115
-    {
116
-        $companyCategories = $company->categories()
117
-            ->where('type', $transactionType)
118
-            ->whereNotIn('name', ['Other Income', 'Other Expense'])
91
+        $accounts = $company->accounts()
92
+            ->where('category', $accountCategory)
93
+            ->whereNotIn('type', [AccountType::UncategorizedRevenue, AccountType::UncategorizedExpense])
119 94
             ->get();
120 95
 
121
-        $bestMatchName = $this->findBestCategoryMatch($transaction, $companyCategories->pluck('name')->toArray());
96
+        $bestMatchName = $this->findBestAccountMatch($transaction, $accounts->pluck('name')->toArray());
122 97
 
123 98
         if ($bestMatchName === null) {
124
-            return $this->getUncategorizedCategory($company, $transactionType);
99
+            return $this->getUncategorizedAccount($company, $transactionType);
125 100
         }
126 101
 
127
-        $category = $companyCategories->firstWhere('name', $bestMatchName);
128
-
129
-        return $category ?: $this->getUncategorizedCategory($company, $transactionType);
102
+        return $accounts->firstWhere('name', $bestMatchName) ?: $this->getUncategorizedAccount($company, $transactionType);
130 103
     }
131 104
 
132
-    private function findBestCategoryMatch(object $transaction, array $userCategories): ?string
105
+    private function findBestAccountMatch(object $transaction, array $accountNames): ?string
133 106
     {
134 107
         $acceptableConfidenceLevels = ['VERY_HIGH', 'HIGH'];
135
-        $similarityThreshold = 0.7;
108
+        $similarityThreshold = 70.0;
136 109
         $plaidDetail = $transaction->personal_finance_category->detailed ?? null;
137 110
         $plaidPrimary = $transaction->personal_finance_category->primary ?? null;
138 111
         $bestMatchName = null;
@@ -140,11 +113,15 @@ class TransactionService
140 113
 
141 114
         foreach ([$plaidDetail, $plaidPrimary] as $plaidCategory) {
142 115
             if ($plaidCategory !== null && in_array($transaction->personal_finance_category->confidence_level, $acceptableConfidenceLevels, true)) {
143
-                $currentMatchPercent = 0.0;
144
-                $matchedName = $this->closestCategory($plaidCategory, $userCategories, $currentMatchPercent);
145
-                if ($currentMatchPercent >= $similarityThreshold && $currentMatchPercent > $bestMatchPercent) {
146
-                    $bestMatchPercent = $currentMatchPercent;
147
-                    $bestMatchName = $matchedName;
116
+                foreach ($accountNames as $accountName) {
117
+                    $normalizedPlaidCategory = strtolower(str_replace('_', ' ', $plaidCategory));
118
+                    $normalizedAccountName = strtolower(str_replace('_', ' ', $accountName));
119
+                    $currentMatchPercent = 0.0;
120
+                    similar_text($normalizedPlaidCategory, $normalizedAccountName, $currentMatchPercent);
121
+                    if ($currentMatchPercent >= $similarityThreshold && $currentMatchPercent > $bestMatchPercent) {
122
+                        $bestMatchPercent = $currentMatchPercent;
123
+                        $bestMatchName = $accountName; // Use and return the original account name for the best match, not the normalized one
124
+                    }
148 125
                 }
149 126
             }
150 127
         }
@@ -152,50 +129,7 @@ class TransactionService
152 129
         return $bestMatchName;
153 130
     }
154 131
 
155
-    public function closestCategory(string $input, array $categories, ?float &$percent): ?string
156
-    {
157
-        $inputNormalized = strtolower(str_replace('_', ' ', $input));
158
-        $originalToNormalized = [];
159
-        foreach ($categories as $originalCategory) {
160
-            $normalizedCategory = strtolower(str_replace('_', ' ', $originalCategory));
161
-            $originalToNormalized[$normalizedCategory] = $originalCategory;
162
-        }
163
-
164
-        $shortest = -1;
165
-        $closestNormalized = null;
166
-        foreach ($originalToNormalized as $normalizedCategory => $originalCategory) {
167
-            $lev = levenshtein($inputNormalized, $normalizedCategory);
168
-            if ($lev === 0 || $lev < $shortest || $shortest < 0) {
169
-                $closestNormalized = $normalizedCategory;
170
-                $shortest = $lev;
171
-            }
172
-        }
173
-
174
-        if ($closestNormalized !== null) {
175
-            $percent = 1.0 - ($shortest / max(strlen($inputNormalized), strlen($closestNormalized)));
176
-
177
-            return $originalToNormalized[$closestNormalized]; // return the original category name
178
-        }
179
-
180
-        $percent = 0.0;
181
-
182
-        return null;
183
-    }
184
-
185
-    public function getUncategorizedCategory(Company $company, string $transactionType): Category
186
-    {
187
-        $name = match ($transactionType) {
188
-            'income' => 'Other Income',
189
-            'expense' => 'Other Expense',
190
-        };
191
-
192
-        return $company->categories()
193
-            ->where('type', $transactionType)
194
-            ->where('name', $name)
195
-            ->firstOrFail();
196
-    }
197
-
198
-    public function getChartFromTransaction(Company $company, string $transactionType): Account
132
+    public function getUncategorizedAccount(Company $company, string $transactionType): Account
199 133
     {
200 134
         [$type, $name] = match ($transactionType) {
201 135
             'income' => [AccountType::UncategorizedRevenue, 'Uncategorized Income'],

+ 0
- 123
app/Utilities/Plaid/AccountTypeMapper.php Voir le fichier

@@ -1,123 +0,0 @@
1
-<?php
2
-
3
-namespace App\Utilities\Plaid;
4
-
5
-use App\Enums\BankAccountSubtype;
6
-use App\Enums\BankAccountType;
7
-
8
-class AccountTypeMapper
9
-{
10
-    public static function mapToEnums(string $plaidType, string $plaidSubtype): array
11
-    {
12
-        $mappedType = self::mapType($plaidType);
13
-        $mappedSubtype = self::mapSubtype($plaidType, $plaidSubtype);
14
-
15
-        return [
16
-            'type' => $mappedType,
17
-            'subtype' => $mappedSubtype,
18
-        ];
19
-    }
20
-
21
-    private static function mapType(string $plaidType): BankAccountType
22
-    {
23
-        return match ($plaidType) {
24
-            'depository' => BankAccountType::Depository,
25
-            'credit' => BankAccountType::Credit,
26
-            'loan' => BankAccountType::Loan,
27
-            'investment' => BankAccountType::Investment,
28
-            default => BankAccountType::Other,
29
-        };
30
-    }
31
-
32
-    private static function mapSubtype(string $plaidType, string $plaidSubtype): ?BankAccountSubtype
33
-    {
34
-        return match ($plaidType) {
35
-            'depository' => match ($plaidSubtype) {
36
-                'checking' => BankAccountSubtype::Checking,
37
-                'savings' => BankAccountSubtype::Savings,
38
-                'hsa' => BankAccountSubtype::HealthSavingsAccountCash,
39
-                'cd' => BankAccountSubtype::CertificateOfDeposit,
40
-                'money market' => BankAccountSubtype::MoneyMarket,
41
-                'paypal' => BankAccountSubtype::Paypal,
42
-                'prepaid' => BankAccountSubtype::Prepaid,
43
-                'cash management' => BankAccountSubtype::CashManagement,
44
-                'ebt' => BankAccountSubtype::ElectronicBenefitsTransfer,
45
-                default => BankAccountSubtype::Other,
46
-            },
47
-            'credit' => match ($plaidSubtype) {
48
-                'credit card' => BankAccountSubtype::CreditCard,
49
-                'paypal' => BankAccountSubtype::PaypalCredit,
50
-                default => BankAccountSubtype::Other,
51
-            },
52
-            'loan' => match ($plaidSubtype) {
53
-                'auto' => BankAccountSubtype::Auto,
54
-                'business' => BankAccountSubtype::Business,
55
-                'commercial' => BankAccountSubtype::Commercial,
56
-                'construction' => BankAccountSubtype::Construction,
57
-                'consumer' => BankAccountSubtype::Consumer,
58
-                'home equity' => BankAccountSubtype::HomeEquity,
59
-                'loan' => BankAccountSubtype::Loan,
60
-                'mortgage' => BankAccountSubtype::Mortgage,
61
-                'overdraft' => BankAccountSubtype::Overdraft,
62
-                'line of credit' => BankAccountSubtype::LineOfCredit,
63
-                'student' => BankAccountSubtype::Student,
64
-                default => BankAccountSubtype::Other,
65
-            },
66
-            'investment' => match ($plaidSubtype) {
67
-                '529' => BankAccountSubtype::CollegeSavings529,
68
-                '401a' => BankAccountSubtype::Retirement401a,
69
-                '401k' => BankAccountSubtype::Retirement401K,
70
-                '403B' => BankAccountSubtype::Retirement403b,
71
-                '457b' => BankAccountSubtype::DeferredCompensation457b,
72
-                'brokerage' => BankAccountSubtype::Brokerage,
73
-                'cash isa' => BankAccountSubtype::CashIndividualSavingsAccount,
74
-                'crypto exchange' => BankAccountSubtype::CryptoCurrencyExchange,
75
-                'education savings account' => BankAccountSubtype::EducationSavingsAccount,
76
-                'fixed annuity' => BankAccountSubtype::FixedAnnuity,
77
-                'gic' => BankAccountSubtype::GuaranteedInvestmentCertificate,
78
-                'health reimbursement arrangement' => BankAccountSubtype::HealthReimbursementArrangement,
79
-                'hsa' => BankAccountSubtype::HealthSavingsAccountNonCash,
80
-                'ira' => BankAccountSubtype::IndividualRetirementAccount,
81
-                'isa' => BankAccountSubtype::IndividualSavingsAccount,
82
-                'keogh' => BankAccountSubtype::KeoghPlan,
83
-                'lif' => BankAccountSubtype::LifeIncomeFund,
84
-                'life insurance' => BankAccountSubtype::LifeInsuranceAccount,
85
-                'lira' => BankAccountSubtype::LockedInRetirementAccount,
86
-                'lrif' => BankAccountSubtype::LockedInRetirementIncomeFund,
87
-                'lrsp' => BankAccountSubtype::LockedInRetirementSavingsPlan,
88
-                'mutual fund' => BankAccountSubtype::MutualFundAccount,
89
-                'non-custodial wallet' => BankAccountSubtype::CryptoCurrencyWallet,
90
-                'non-taxable brokerage account' => BankAccountSubtype::NonTaxableBrokerageAccount,
91
-                'other annuity' => BankAccountSubtype::AnnuityAccountOther,
92
-                'other insurance' => BankAccountSubtype::InsuranceAccountOther,
93
-                'pension' => BankAccountSubtype::PensionAccount,
94
-                'prif' => BankAccountSubtype::PrescribedRetirementIncomeFund,
95
-                'profit sharing plan' => BankAccountSubtype::ProfitSharingPlanAccount,
96
-                'qshr' => BankAccountSubtype::QualifyingShareAccount,
97
-                'rdsp' => BankAccountSubtype::RegisteredDisabilitySavingsPlan,
98
-                'resp' => BankAccountSubtype::RegisteredEducationSavingsPlan,
99
-                'retirement' => BankAccountSubtype::RetirementAccountOther,
100
-                'rlif' => BankAccountSubtype::RestrictedLifeIncomeFund,
101
-                'roth' => BankAccountSubtype::RothIRA,
102
-                'roth 401k' => BankAccountSubtype::Roth401k,
103
-                'rrif' => BankAccountSubtype::RegisteredRetirementIncomeFund,
104
-                'rrsp' => BankAccountSubtype::RegisteredRetirementSavingsPlan,
105
-                'sarsep' => BankAccountSubtype::SalaryReductionSEPPlan,
106
-                'sep ira' => BankAccountSubtype::SimplifiedEmployeePensionIRA,
107
-                'simple ira' => BankAccountSubtype::SavingsIncentiveMatchPlanForEmployeesIRA,
108
-                'sipp' => BankAccountSubtype::SelfInvestedPersonalPension,
109
-                'stock plan' => BankAccountSubtype::StockPlanAccount,
110
-                'tfsa' => BankAccountSubtype::TaxFreeSavingsAccount,
111
-                'trust' => BankAccountSubtype::TrustAccount,
112
-                'ugma' => BankAccountSubtype::UniformGiftToMinorsAct,
113
-                'utma' => BankAccountSubtype::UniformTransfersToMinorsAct,
114
-                'variable annuity' => BankAccountSubtype::VariableAnnuityAccount,
115
-                default => BankAccountSubtype::Other,
116
-            },
117
-
118
-            default => BankAccountSubtype::Other,
119
-
120
-        };
121
-
122
-    }
123
-}

+ 25
- 0
app/ValueObjects/BalanceValue.php Voir le fichier

@@ -0,0 +1,25 @@
1
+<?php
2
+
3
+namespace App\ValueObjects;
4
+
5
+class BalanceValue
6
+{
7
+    private int $value;
8
+    private string $currency;
9
+
10
+    public function __construct(int $value, string $currency = 'USD')
11
+    {
12
+        $this->value = $value;
13
+        $this->currency = $currency;
14
+    }
15
+
16
+    public function getValue(): int
17
+    {
18
+        return $this->value;
19
+    }
20
+
21
+    public function formatted(): string
22
+    {
23
+        return money($this->value, $this->currency)->format();
24
+    }
25
+}

+ 2
- 0
composer.json Voir le fichier

@@ -15,9 +15,11 @@
15 15
         "andrewdwallo/filament-companies": "^3.0",
16 16
         "andrewdwallo/filament-selectify": "^2.0",
17 17
         "andrewdwallo/transmatic": "^1.0",
18
+        "barryvdh/laravel-dompdf": "^2.0",
18 19
         "bezhansalleh/filament-panel-switch": "^1.0",
19 20
         "filament/filament": "^3.0",
20 21
         "filament/spatie-laravel-tags-plugin": "^3.0-stable",
22
+        "guava/filament-clusters": "^1.1",
21 23
         "guzzlehttp/guzzle": "^7.2",
22 24
         "laravel/framework": "^10.10",
23 25
         "laravel/sanctum": "^3.2",

+ 715
- 333
composer.lock
Fichier diff supprimé car celui-ci est trop grand
Voir le fichier


+ 0
- 12
config/chart-of-accounts.php Voir le fichier

@@ -409,16 +409,4 @@ return [
409 409
             ],
410 410
         ],
411 411
     ],
412
-    'category_account_map' => [
413
-        'Dividends' => 'Dividends',
414
-        'Interest Earned' => 'Interest Earned',
415
-        'Wages' => 'Salaries and Wages',
416
-        'Sales' => 'Product Sales',
417
-        'Other Income' => 'Uncategorized Income',
418
-        'Rent or Mortgage' => 'Rent or Lease Payments',
419
-        'Utilities' => 'Utilities',
420
-        'Groceries' => 'Food and Drink',
421
-        'Transportation' => 'Transportation',
422
-        'Other Expenses' => 'Uncategorized Expense',
423
-    ],
424 412
 ];

+ 2
- 1
database/factories/Setting/AppearanceFactory.php Voir le fichier

@@ -2,10 +2,11 @@
2 2
 
3 3
 namespace Database\Factories\Setting;
4 4
 
5
+use App\Models\Setting\Appearance;
5 6
 use Illuminate\Database\Eloquent\Factories\Factory;
6 7
 
7 8
 /**
8
- * @extends \Illuminate\Database\Eloquent\Factories\Factory<\App\Models\Setting\Appearance>
9
+ * @extends Factory<Appearance>
9 10
  */
10 11
 class AppearanceFactory extends Factory
11 12
 {

+ 0
- 59
database/factories/Setting/CategoryFactory.php Voir le fichier

@@ -1,59 +0,0 @@
1
-<?php
2
-
3
-namespace Database\Factories\Setting;
4
-
5
-use App\Enums\CategoryType;
6
-use App\Models\Setting\Category;
7
-use Illuminate\Database\Eloquent\Factories\Factory;
8
-
9
-/**
10
- * @extends Factory<Category>
11
- */
12
-class CategoryFactory extends Factory
13
-{
14
-    /**
15
-     * The name of the factory's corresponding model.
16
-     */
17
-    protected $model = Category::class;
18
-
19
-    /**
20
-     * Define the model's default state.
21
-     *
22
-     * @return array<string, mixed>
23
-     */
24
-    public function definition(): array
25
-    {
26
-        return [
27
-            'name' => $this->faker->word,
28
-            'type' => $this->faker->randomElement(['income', 'expense']),
29
-            'color' => $this->faker->hexColor,
30
-            'enabled' => false,
31
-        ];
32
-    }
33
-
34
-    /**
35
-     * Indicate that the category is of income type.
36
-     */
37
-    public function incomeCategory(string $name): self
38
-    {
39
-        return $this->state(function (array $attributes) use ($name) {
40
-            return [
41
-                'name' => $name,
42
-                'type' => CategoryType::Income->value,
43
-            ];
44
-        });
45
-    }
46
-
47
-    /**
48
-     * Indicate that the category is of expense type.
49
-     */
50
-    public function expenseCategory(string $name): self
51
-    {
52
-        return $this->state(function (array $attributes) use ($name) {
53
-            return [
54
-                'name' => $name,
55
-                'type' => CategoryType::Expense->value,
56
-            ];
57
-        });
58
-    }
59
-}

+ 0
- 52
database/factories/Setting/CompanyDefaultFactory.php Voir le fichier

@@ -2,11 +2,9 @@
2 2
 
3 3
 namespace Database\Factories\Setting;
4 4
 
5
-use App\Enums\CategoryType;
6 5
 use App\Faker\CurrencyCode;
7 6
 use App\Models\Company;
8 7
 use App\Models\Setting\Appearance;
9
-use App\Models\Setting\Category;
10 8
 use App\Models\Setting\CompanyDefault;
11 9
 use App\Models\Setting\Currency;
12 10
 use App\Models\Setting\Discount;
@@ -46,7 +44,6 @@ class CompanyDefaultFactory extends Factory
46 44
 
47 45
         $currencyCode = $currencyFaker->currencyCode($country);
48 46
 
49
-        $categories = $this->createCategories($company, $user);
50 47
         $currency = $this->createCurrency($company, $user, $currencyCode);
51 48
         $salesTax = $this->createSalesTax($company, $user);
52 49
         $purchaseTax = $this->createPurchaseTax($company, $user);
@@ -58,8 +55,6 @@ class CompanyDefaultFactory extends Factory
58 55
 
59 56
         $companyDefaults = [
60 57
             'company_id' => $company->id,
61
-            'income_category_id' => $categories['income_category_id'],
62
-            'expense_category_id' => $categories['expense_category_id'],
63 58
             'currency_code' => $currency->code,
64 59
             'sales_tax_id' => $salesTax->id,
65 60
             'purchase_tax_id' => $purchaseTax->id,
@@ -72,53 +67,6 @@ class CompanyDefaultFactory extends Factory
72 67
         return $this->state($companyDefaults);
73 68
     }
74 69
 
75
-    private function createCategories(Company $company, User $user): array
76
-    {
77
-        $incomeCategories = ['Salary', 'Bonus', 'Interest', 'Dividends', 'Rentals'];
78
-        $expenseCategories = ['Rent', 'Utilities', 'Food', 'Transportation', 'Entertainment'];
79
-
80
-        $shuffledCategories = [
81
-            ...array_map(static fn ($name) => ['name' => $name, 'type' => CategoryType::Income->value], $incomeCategories),
82
-            ...array_map(static fn ($name) => ['name' => $name, 'type' => CategoryType::Expense->value], $expenseCategories),
83
-        ];
84
-
85
-        shuffle($shuffledCategories);
86
-
87
-        $incomeEnabled = $expenseEnabled = false;
88
-
89
-        $enabledIncomeCategoryId = null;
90
-        $enabledExpenseCategoryId = null;
91
-
92
-        foreach ($shuffledCategories as $category) {
93
-            $enabled = false;
94
-            if (! $incomeEnabled && $category['type'] === CategoryType::Income->value) {
95
-                $enabled = $incomeEnabled = true;
96
-            } elseif (! $expenseEnabled && $category['type'] === CategoryType::Expense->value) {
97
-                $enabled = $expenseEnabled = true;
98
-            }
99
-
100
-            $categoryModel = Category::factory()->create([
101
-                'company_id' => $company->id,
102
-                'name' => $category['name'],
103
-                'type' => $category['type'],
104
-                'enabled' => $enabled,
105
-                'created_by' => $user->id,
106
-                'updated_by' => $user->id,
107
-            ]);
108
-
109
-            if ($enabled && $category['type'] === CategoryType::Income->value) {
110
-                $enabledIncomeCategoryId = $categoryModel->id;
111
-            } elseif ($enabled && $category['type'] === CategoryType::Expense->value) {
112
-                $enabledExpenseCategoryId = $categoryModel->id;
113
-            }
114
-        }
115
-
116
-        return [
117
-            'income_category_id' => $enabledIncomeCategoryId,
118
-            'expense_category_id' => $enabledExpenseCategoryId,
119
-        ];
120
-    }
121
-
122 70
     private function createCurrency(Company $company, User $user, string $currencyCode)
123 71
     {
124 72
         return Currency::factory()->forCurrency($currencyCode)->create([

+ 2
- 2
database/factories/Setting/LocalizationFactory.php Voir le fichier

@@ -47,8 +47,8 @@ class LocalizationFactory extends Factory
47 47
             'number_format' => $number_format,
48 48
             'percent_first' => $percent_first,
49 49
             'week_start' => $week_start,
50
-            'fiscal_year_start' => now()->startOfYear()->toDateString(),
51
-            'fiscal_year_end' => now()->endOfYear()->toDateString(),
50
+            'fiscal_year_end_month' => 12,
51
+            'fiscal_year_end_day' => 31,
52 52
         ]);
53 53
     }
54 54
 }

+ 0
- 5
database/migrations/2023_09_03_100000_create_accounting_tables.php Voir le fichier

@@ -46,11 +46,6 @@ return new class extends Migration
46 46
             $table->string('code')->nullable()->index();
47 47
             $table->string('name')->nullable()->index();
48 48
             $table->string('currency_code')->nullable();
49
-            $table->bigInteger('starting_balance')->default(0);
50
-            $table->bigInteger('debit_balance')->default(0);
51
-            $table->bigInteger('credit_balance')->default(0);
52
-            $table->bigInteger('net_movement')->default(0);
53
-            $table->bigInteger('ending_balance')->default(0);
54 49
             $table->text('description')->nullable();
55 50
             $table->boolean('active')->default(true);
56 51
             $table->boolean('default')->default(false);

+ 0
- 38
database/migrations/2023_09_07_193412_create_categories_table.php Voir le fichier

@@ -1,38 +0,0 @@
1
-<?php
2
-
3
-use Illuminate\Database\Migrations\Migration;
4
-use Illuminate\Database\Schema\Blueprint;
5
-use Illuminate\Support\Facades\Schema;
6
-
7
-return new class extends Migration
8
-{
9
-    /**
10
-     * Run the migrations.
11
-     */
12
-    public function up(): void
13
-    {
14
-        Schema::create('categories', function (Blueprint $table) {
15
-            $table->id();
16
-            $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->foreignId('account_id')->nullable()->constrained()->nullOnDelete();
18
-            $table->string('name')->index();
19
-            $table->string('type');
20
-            $table->string('color');
21
-            $table->boolean('enabled')->default(true);
22
-            $table->boolean('is_default')->default(false);
23
-            $table->foreignId('created_by')->nullable()->constrained('users')->nullOnDelete();
24
-            $table->foreignId('updated_by')->nullable()->constrained('users')->nullOnDelete();
25
-            $table->timestamps();
26
-
27
-            $table->unique(['company_id', 'name', 'type']);
28
-        });
29
-    }
30
-
31
-    /**
32
-     * Reverse the migrations.
33
-     */
34
-    public function down(): void
35
-    {
36
-        Schema::dropIfExists('categories');
37
-    }
38
-};

+ 0
- 2
database/migrations/2023_09_08_040159_create_company_defaults_table.php Voir le fichier

@@ -20,8 +20,6 @@ return new class extends Migration
20 20
             $table->foreignId('purchase_tax_id')->nullable()->constrained('taxes')->cascadeOnDelete();
21 21
             $table->foreignId('sales_discount_id')->nullable()->constrained('discounts')->cascadeOnDelete();
22 22
             $table->foreignId('purchase_discount_id')->nullable()->constrained('discounts')->cascadeOnDelete();
23
-            $table->foreignId('income_category_id')->nullable()->constrained('categories')->cascadeOnDelete();
24
-            $table->foreignId('expense_category_id')->nullable()->constrained('categories')->cascadeOnDelete();
25 23
             $table->foreignId('created_by')->nullable()->constrained('users')->restrictOnDelete();
26 24
             $table->foreignId('updated_by')->nullable()->constrained('users')->restrictOnDelete();
27 25
             $table->timestamps();

+ 2
- 2
database/migrations/2023_10_11_210415_create_localizations_table.php Voir le fichier

@@ -22,8 +22,8 @@ return new class extends Migration
22 22
             $table->string('timezone')->nullable();
23 23
             $table->string('date_format')->default(DateFormat::DEFAULT);
24 24
             $table->string('time_format')->default(TimeFormat::DEFAULT);
25
-            $table->date('fiscal_year_start')->default(now()->startOfYear()->toDateString());
26
-            $table->date('fiscal_year_end')->default(now()->endOfYear()->toDateString());
25
+            $table->unsignedTinyInteger('fiscal_year_end_month')->default(12);
26
+            $table->unsignedTinyInteger('fiscal_year_end_day')->default(31);
27 27
             $table->unsignedTinyInteger('week_start')->default(WeekStart::DEFAULT);
28 28
             $table->string('number_format')->default(NumberFormat::DEFAULT);
29 29
             $table->boolean('percent_first')->default(false);

+ 1
- 1
database/migrations/2024_01_01_234943_create_transactions_table.php Voir le fichier

@@ -14,7 +14,7 @@ return new class extends Migration
14 14
         Schema::create('transactions', function (Blueprint $table) {
15 15
             $table->id();
16 16
             $table->foreignId('company_id')->constrained()->cascadeOnDelete();
17
-            $table->foreignId('category_id')->nullable()->constrained()->nullOnDelete();
17
+            $table->foreignId('account_id')->nullable()->constrained()->nullOnDelete();
18 18
             $table->foreignId('bank_account_id')->nullable()->constrained()->nullOnDelete();
19 19
             $table->foreignId('contact_id')->nullable()->constrained()->nullOnDelete();
20 20
             $table->string('type'); // income, expense, other

+ 31
- 28
package-lock.json Voir le fichier

@@ -397,14 +397,14 @@
397 397
             }
398 398
         },
399 399
         "node_modules/@jridgewell/gen-mapping": {
400
-            "version": "0.3.4",
401
-            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.4.tgz",
402
-            "integrity": "sha512-Oud2QPM5dHviZNn4y/WhhYKSXksv+1xLEIsNrAbGcFzUN3ubqWRFT5gwPchNc5NuzILOU4tPBDTZ4VwhL8Y7cw==",
400
+            "version": "0.3.5",
401
+            "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.5.tgz",
402
+            "integrity": "sha512-IzL8ZoEDIBRWEzlCcRhOaCupYyN5gdIK+Q6fbFdPDg6HqX6jpkItn7DFIpW9LQzXG6Df9sA7+OKnq0qlz/GaQg==",
403 403
             "dev": true,
404 404
             "dependencies": {
405
-                "@jridgewell/set-array": "^1.0.1",
405
+                "@jridgewell/set-array": "^1.2.1",
406 406
                 "@jridgewell/sourcemap-codec": "^1.4.10",
407
-                "@jridgewell/trace-mapping": "^0.3.9"
407
+                "@jridgewell/trace-mapping": "^0.3.24"
408 408
             },
409 409
             "engines": {
410 410
                 "node": ">=6.0.0"
@@ -420,9 +420,9 @@
420 420
             }
421 421
         },
422 422
         "node_modules/@jridgewell/set-array": {
423
-            "version": "1.1.2",
424
-            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.1.2.tgz",
425
-            "integrity": "sha512-xnkseuNADM0gt2bs+BvhO0p78Mk762YnZdsuzFV018NoG1Sj1SCQvpSqa7XUaTam5vAGasABV9qXASMKnFMwMw==",
423
+            "version": "1.2.1",
424
+            "resolved": "https://registry.npmjs.org/@jridgewell/set-array/-/set-array-1.2.1.tgz",
425
+            "integrity": "sha512-R8gLRTZeyp03ymzP/6Lil/28tGeGEzhx1q2k703KGWRAI1VdvPIXdG70VJc2pAMw3NA6JKL5hhFu1sJX0Mnn/A==",
426 426
             "dev": true,
427 427
             "engines": {
428 428
                 "node": ">=6.0.0"
@@ -435,9 +435,9 @@
435 435
             "dev": true
436 436
         },
437 437
         "node_modules/@jridgewell/trace-mapping": {
438
-            "version": "0.3.23",
439
-            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.23.tgz",
440
-            "integrity": "sha512-9/4foRoUKp8s96tSkh8DlAAc5A0Ty8vLXld+l9gjKKY6ckwI8G15f0hskGmuLZu78ZlGa1vtsfOa+lnB4vG6Jg==",
438
+            "version": "0.3.25",
439
+            "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.25.tgz",
440
+            "integrity": "sha512-vNk6aEwybGtawWmy/PzwnGDOjCkLWSD2wqvjGGAgOAwCGWySYXfYoxt00IJkTF+8Lb57DwOb3Aa0o9CApepiYQ==",
441 441
             "dev": true,
442 442
             "dependencies": {
443 443
                 "@jridgewell/resolve-uri": "^3.1.0",
@@ -572,9 +572,9 @@
572 572
             "dev": true
573 573
         },
574 574
         "node_modules/autoprefixer": {
575
-            "version": "10.4.17",
576
-            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.17.tgz",
577
-            "integrity": "sha512-/cpVNRLSfhOtcGflT13P2794gVSgmPgTR+erw5ifnMLZb0UnSlkK4tquLmkd3BhA+nLo5tX8Cu0upUsGKvKbmg==",
575
+            "version": "10.4.18",
576
+            "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.18.tgz",
577
+            "integrity": "sha512-1DKbDfsr6KUElM6wg+0zRNkB/Q7WcKYAaK+pzXn+Xqmszm/5Xa9coeNdtP88Vi+dPzZnMjhge8GIV49ZQkDa+g==",
578 578
             "dev": true,
579 579
             "funding": [
580 580
                 {
@@ -591,8 +591,8 @@
591 591
                 }
592 592
             ],
593 593
             "dependencies": {
594
-                "browserslist": "^4.22.2",
595
-                "caniuse-lite": "^1.0.30001578",
594
+                "browserslist": "^4.23.0",
595
+                "caniuse-lite": "^1.0.30001591",
596 596
                 "fraction.js": "^4.3.7",
597 597
                 "normalize-range": "^0.1.2",
598 598
                 "picocolors": "^1.0.0",
@@ -697,9 +697,9 @@
697 697
             }
698 698
         },
699 699
         "node_modules/caniuse-lite": {
700
-            "version": "1.0.30001589",
701
-            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001589.tgz",
702
-            "integrity": "sha512-vNQWS6kI+q6sBlHbh71IIeC+sRwK2N3EDySc/updIGhIee2x5z00J4c1242/5/d6EpEMdOnk/m+6tuk4/tcsqg==",
700
+            "version": "1.0.30001597",
701
+            "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001597.tgz",
702
+            "integrity": "sha512-7LjJvmQU6Sj7bL0j5b5WY/3n7utXUJvAe1lxhsHDbLmwX9mdL86Yjtr+5SRCyf8qME4M7pU2hswj0FpyBVCv9w==",
703 703
             "dev": true,
704 704
             "funding": [
705 705
                 {
@@ -845,9 +845,9 @@
845 845
             "dev": true
846 846
         },
847 847
         "node_modules/electron-to-chromium": {
848
-            "version": "1.4.681",
849
-            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.681.tgz",
850
-            "integrity": "sha512-1PpuqJUFWoXZ1E54m8bsLPVYwIVCRzvaL+n5cjigGga4z854abDnFRc+cTa2th4S79kyGqya/1xoR7h+Y5G5lg==",
848
+            "version": "1.4.701",
849
+            "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.701.tgz",
850
+            "integrity": "sha512-K3WPQ36bUOtXg/1+69bFlFOvdSm0/0bGqmsfPDLRXLanoKXdA+pIWuf/VbA9b+2CwBFuONgl4NEz4OEm+OJOKA==",
851 851
             "dev": true
852 852
         },
853 853
         "node_modules/emoji-regex": {
@@ -1072,9 +1072,9 @@
1072 1072
             }
1073 1073
         },
1074 1074
         "node_modules/hasown": {
1075
-            "version": "2.0.1",
1076
-            "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.1.tgz",
1077
-            "integrity": "sha512-1/th4MHjnwncwXsIW6QMzlvYL9kG5e/CpVvLRZe4XPa8TOUNbCELqmvhDmnkNsAjwaG4+I8gJJL0JBvTTLO9qA==",
1075
+            "version": "2.0.2",
1076
+            "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz",
1077
+            "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==",
1078 1078
             "dev": true,
1079 1079
             "dependencies": {
1080 1080
                 "function-bind": "^1.1.2"
@@ -2195,10 +2195,13 @@
2195 2195
             }
2196 2196
         },
2197 2197
         "node_modules/yaml": {
2198
-            "version": "2.3.4",
2199
-            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.3.4.tgz",
2200
-            "integrity": "sha512-8aAvwVUSHpfEqTQ4w/KMlf3HcRdt50E5ODIQJBw1fQ5RL34xabzxtUlzTXVqc4rkZsPbvrXKWnABCD7kWSmocA==",
2198
+            "version": "2.4.1",
2199
+            "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.1.tgz",
2200
+            "integrity": "sha512-pIXzoImaqmfOrL7teGUBt/T7ZDnyeGBWyXQBvOVhLkWLN37GXv8NMLK406UY6dS51JfcQHsmcW5cJ441bHg6Lg==",
2201 2201
             "dev": true,
2202
+            "bin": {
2203
+                "yaml": "bin.mjs"
2204
+            },
2202 2205
             "engines": {
2203 2206
                 "node": ">= 14"
2204 2207
             }

+ 16
- 0
resources/css/filament/company/theme.css Voir le fichier

@@ -3,6 +3,22 @@
3 3
 
4 4
 @config './tailwind.config.js';
5 5
 
6
+.es-report-card {
7
+    @apply md:!grid-cols-2;
8
+
9
+    .fi-fo-component-ctn {
10
+        @apply divide-y divide-gray-200 dark:divide-white/10 !gap-0;
11
+    }
12
+
13
+    .fi-section-content-ctn {
14
+        @apply md:!col-span-1;
15
+    }
16
+
17
+    .fi-section-content {
18
+        @apply !p-0;
19
+    }
20
+}
21
+
6 22
 .choices:focus-visible {
7 23
     outline: none;
8 24
 }

+ 2
- 3
resources/data/lang/en.json Voir le fichier

@@ -246,6 +246,5 @@
246 246
     "Starting Balance": "Starting Balance",
247 247
     "Account Identification": "Account Identification",
248 248
     "Account Settings": "Account Settings",
249
-    "Financial Details": "Financial Details",
250
-    "Oh Yes": "Oh Yes"
251
-}
249
+    "Financial Details": "Financial Details"
250
+}

+ 136
- 0
resources/views/components/company/reports/account-balances.blade.php Voir le fichier

@@ -0,0 +1,136 @@
1
+<!DOCTYPE html>
2
+<html lang="en">
3
+<head>
4
+    <meta http-equiv="Content-Type" content="text/html; charset=utf-8"/>
5
+    <title>Account Balances</title>
6
+    <style>
7
+        @page {
8
+            margin: 0.25in 0.25in 1in 0.25in;
9
+        }
10
+
11
+        .header {
12
+            color: #374151;
13
+        }
14
+
15
+        .table-class th,
16
+        .table-class td {
17
+            text-align: right;
18
+            color: #374151;
19
+        }
20
+
21
+        /* Align the first column header and data to the left */
22
+        .table-class th:first-child, .table-class td:first-child {
23
+            text-align: left;
24
+        }
25
+
26
+        .header {
27
+            margin-bottom: 1rem; /* Space between header and table */
28
+        }
29
+
30
+        .header .title,
31
+        .header .company-name,
32
+        .header .date-range {
33
+            margin-bottom: 0.125rem; /* Uniform space between header elements */
34
+        }
35
+
36
+        .title { font-size: 1.5rem; }
37
+        .company-name { font-size: 1.125rem; font-weight: 600; }
38
+        .date-range { font-size: 0.875rem; }
39
+
40
+        .table-class {
41
+            width: 100%;
42
+            border-collapse: collapse;
43
+        }
44
+
45
+        .table-class th,
46
+        .table-class td {
47
+            padding: 0.75rem;
48
+            font-size: 0.75rem;
49
+            line-height: 1rem;
50
+            border-bottom: 1px solid #d1d5db; /* Gray border for all rows */
51
+        }
52
+
53
+        .category-row > td {
54
+            background-color: #f3f4f6; /* Gray background for category names */
55
+            font-weight: 600;
56
+        }
57
+
58
+        .table-body tr { background-color: #ffffff; /* White background for other rows */ }
59
+
60
+        .spacer-row > td { height: 0.75rem; }
61
+
62
+        .summary-row > td,
63
+        .table-footer-row > td {
64
+            font-weight: 600;
65
+            background-color: #ffffff; /* White background for footer */
66
+        }
67
+    </style>
68
+</head>
69
+<body>
70
+    <div class="header">
71
+        <div class="title">Account Balances</div>
72
+        <div class="company-name">{{ auth()->user()->currentCompany->name }}</div>
73
+        <div class="date-range">Date Range: {{ $startDate }} to {{ $endDate }}</div>
74
+    </div>
75
+    <table class="table-class">
76
+        <thead class="table-head" style="display: table-row-group;">
77
+            <tr>
78
+                <th>Account</th>
79
+                <th>Starting Balance</th>
80
+                <th>Debit</th>
81
+                <th>Credit</th>
82
+                <th>Net Movement</th>
83
+                <th>Ending Balance</th>
84
+            </tr>
85
+        </thead>
86
+        @foreach($accountBalanceReport->categories as $accountCategoryName => $accountCategory)
87
+            <tbody>
88
+            <tr class="category-row">
89
+                <td colspan="6">{{ $accountCategoryName }}</td>
90
+            </tr>
91
+            @foreach($accountCategory->accounts as $account)
92
+                <tr>
93
+                    <td>{{ $account->accountName }}</td>
94
+                    <td>{{ $account->balance->startingBalance ?? '' }}</td>
95
+                    <td>{{ $account->balance->debitBalance }}</td>
96
+                    <td>{{ $account->balance->creditBalance }}</td>
97
+                    <td>{{ $account->balance->netMovement }}</td>
98
+                    <td>{{ $account->balance->endingBalance ?? '' }}</td>
99
+                </tr>
100
+            @endforeach
101
+            <tr class="summary-row">
102
+                <td>Total {{ $accountCategoryName }}</td>
103
+                <td>{{ $accountCategory->summary->startingBalance ?? '' }}</td>
104
+                <td>{{ $accountCategory->summary->debitBalance }}</td>
105
+                <td>{{ $accountCategory->summary->creditBalance }}</td>
106
+                <td>{{ $accountCategory->summary->netMovement }}</td>
107
+                <td>{{ $accountCategory->summary->endingBalance ?? '' }}</td>
108
+            </tr>
109
+            <tr class="spacer-row">
110
+                <td colspan="6"></td>
111
+            </tr>
112
+            </tbody>
113
+        @endforeach
114
+        <tfoot>
115
+            <tr class="table-footer-row">
116
+                <td>Total for all accounts</td>
117
+                <td></td>
118
+                <td>{{ $accountBalanceReport->overallTotal->debitBalance }}</td>
119
+                <td>{{ $accountBalanceReport->overallTotal->creditBalance }}</td>
120
+                <td></td>
121
+                <td></td>
122
+            </tr>
123
+        </tfoot>
124
+    </table>
125
+    <script type="text/php">
126
+        if (isset($pdf)) {
127
+            $font = $fontMetrics->getFont("Inter, sans-serif", "normal");
128
+            $size = 8;
129
+
130
+            $pageText = "Page {PAGE_NUM} of {PAGE_COUNT}";
131
+            $x = 533;
132
+            $y = 820;
133
+            $pdf->page_text($x, $y, $pageText, $font, $size);
134
+        }
135
+    </script>
136
+</body>

+ 38
- 0
resources/views/components/report-entry.blade.php Voir le fichier

@@ -0,0 +1,38 @@
1
+@props([
2
+    'heading' => null,
3
+    'description' => null,
4
+    'icon' => null,
5
+    'iconColor' => 'gray',
6
+])
7
+
8
+<div class="group relative p-6 hover:bg-gray-300/5 dark:hover:bg-gray-700/5">
9
+    <span
10
+        @class([
11
+            'inline-flex rounded-lg p-3 ring-4 ring-white dark:ring-gray-900',
12
+            match ($iconColor) {
13
+                'gray' => 'fi-color-gray bg-gray-50 text-gray-700 dark:bg-gray-900 dark:text-gray-500',
14
+                default => 'fi-color-custom bg-custom-50 text-custom-700 dark:bg-custom-950 dark:text-custom-500',
15
+            },
16
+        ])
17
+        @style([
18
+            \Filament\Support\get_color_css_variables(
19
+                $iconColor,
20
+                shades: [50, 500, 700, 950],
21
+            ) => $iconColor !== 'gray',
22
+        ])
23
+    >
24
+        <x-filament::icon :icon="$icon" class="h-6 w-6" />
25
+    </span>
26
+    <div class="mt-8 pr-1">
27
+        <h3 class="text-base font-semibold leading-6 text-gray-950 dark:text-white">
28
+            {{ $heading }}
29
+        </h3>
30
+        <p class="mt-2 text-sm text-gray-500 dark:text-gray-400">
31
+            {{ $description }}
32
+        </p>
33
+    </div>
34
+    <x-filament::icon
35
+        icon="heroicon-o-arrow-up-right"
36
+        class="absolute right-6 top-6 text-gray-300 group-hover:text-gray-400 h-6 w-6"
37
+    />
38
+</div>

+ 26
- 19
resources/views/filament/company/pages/accounting/chart.blade.php Voir le fichier

@@ -47,26 +47,33 @@
47 47
 
48 48
                                 <!-- Chart Rows -->
49 49
                                 @forelse($subtype->accounts as $account)
50
-                                <tr class="es-table__row">
51
-                                    <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->code }}</td>
52
-                                    <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->name }}</td>
53
-                                    <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->description }}</td>
54
-                                    <td colspan="1" class="es-table__cell px-4 py-4">@money($account->ending_balance, $account->currency_code, true)</td>
55
-                                    <td colspan="1" class="es-table__cell px-4 py-4">
56
-                                        <div>
57
-                                            @if($account->default === false)
58
-                                                {{ ($this->editChartAction)(['chart' => $account->id]) }}
59
-                                            @endif
60
-                                        </div>
61
-                                    </td>
62
-                                </tr>
50
+                                    @php
51
+                                        $accountBalance = $this->getAccountBalance($account);
52
+                                    @endphp
53
+                                    <tr class="es-table__row">
54
+                                        <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->code }}</td>
55
+                                        <td colspan="1" class="es-table__cell px-4 py-4">{{ $account->name }}</td>
56
+                                        <td colspan="{{ $accountBalance === null ? '2' : '1' }}" class="es-table__cell px-4 py-4">{{ $account->description }}</td>
57
+                                        @if($accountBalance !== null)
58
+                                            <td colspan="1" class="es-table__cell px-4 py-4">
59
+                                                {{ $accountBalance }}
60
+                                            </td>
61
+                                        @endif
62
+                                        <td colspan="1" class="es-table__cell px-4 py-4">
63
+                                            <div>
64
+                                                @if($account->default === false)
65
+                                                    {{ ($this->editChartAction)(['chart' => $account->id]) }}
66
+                                                @endif
67
+                                            </div>
68
+                                        </td>
69
+                                    </tr>
63 70
                                 @empty
64
-                                <!-- No Accounts Available Row -->
65
-                                <tr class="es-table__row">
66
-                                    <td colspan="5" class="es-table__cell px-4 py-4 italic">
67
-                                        {{ __("You haven't added any {$subtype->name} accounts yet.") }}
68
-                                    </td>
69
-                                </tr>
71
+                                    <!-- No Accounts Available Row -->
72
+                                    <tr class="es-table__row">
73
+                                        <td colspan="5" class="es-table__cell px-4 py-4 italic">
74
+                                            {{ __("You haven't added any {$subtype->name} accounts yet.") }}
75
+                                        </td>
76
+                                    </tr>
70 77
                                 @endforelse
71 78
 
72 79
                                 <!-- Add New Account Row -->

+ 3
- 0
resources/views/filament/company/pages/reports.blade.php Voir le fichier

@@ -0,0 +1,3 @@
1
+<x-filament-panels::page>
2
+    {{ $this->reportsInfolist }}
3
+</x-filament-panels::page>

+ 75
- 0
resources/views/filament/company/pages/reports/account-balances.blade.php Voir le fichier

@@ -0,0 +1,75 @@
1
+<x-filament-panels::page>
2
+    <div class="flex flex-col gap-y-6">
3
+        <x-filament-tables::container>
4
+            <div class="p-6 divide-y divide-gray-200 dark:divide-white/5">
5
+                <form wire:submit.prevent="loadAccountBalances" class="w-full">
6
+                    <div class="flex flex-col md:flex-row items-end justify-center gap-4 md:gap-6">
7
+                        <div class="flex-grow">
8
+                            {{ $this->form }}
9
+                        </div>
10
+                        <x-filament::button type="submit" class="mt-4 md:mt-0">
11
+                            Update Report
12
+                        </x-filament::button>
13
+                    </div>
14
+                </form>
15
+            </div>
16
+            <div class="divide-y divide-gray-200 overflow-x-auto dark:divide-white/10 dark:border-t-white/10">
17
+                <table class="w-full table-auto divide-y divide-gray-200 text-start dark:divide-white/5">
18
+                    <thead class="divide-y divide-gray-200 dark:divide-white/5">
19
+                        <tr class="bg-gray-50 dark:bg-white/5">
20
+                            <x-filament-tables::header-cell>Account</x-filament-tables::header-cell>
21
+                            <x-filament-tables::header-cell alignment="end">Starting Balance</x-filament-tables::header-cell>
22
+                            <x-filament-tables::header-cell alignment="end">Debit</x-filament-tables::header-cell>
23
+                            <x-filament-tables::header-cell alignment="end">Credit</x-filament-tables::header-cell>
24
+                            <x-filament-tables::header-cell alignment="end">Net Movement</x-filament-tables::header-cell>
25
+                            <x-filament-tables::header-cell alignment="end">Ending Balance</x-filament-tables::header-cell>
26
+                        </tr>
27
+                    </thead>
28
+                    @foreach($accountBalanceReport->categories as $accountCategoryName => $accountCategory)
29
+                        <tbody class="divide-y divide-gray-200 whitespace-nowrap dark:divide-white/5">
30
+                        <tr class="bg-gray-50 dark:bg-white/5">
31
+                            <x-filament-tables::cell colspan="6">
32
+                                <div class="px-3 py-2 text-sm font-medium text-gray-950 dark:text-white">{{ $accountCategoryName }}</div>
33
+                            </x-filament-tables::cell>
34
+                        </tr>
35
+                        @foreach($accountCategory->accounts as $account)
36
+                            <x-filament-tables::row>
37
+                                <x-filament-tables::cell><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->accountName }}</div></x-filament-tables::cell>
38
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->startingBalance ?? '' }}</div></x-filament-tables::cell>
39
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->debitBalance }}</div></x-filament-tables::cell>
40
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->creditBalance }}</div></x-filament-tables::cell>
41
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->netMovement }}</div></x-filament-tables::cell>
42
+                                <x-filament-tables::cell class="text-right"><div class="px-3 py-4 text-sm leading-6 text-gray-950 dark:text-white">{{ $account->balance->endingBalance ?? '' }}</div></x-filament-tables::cell>
43
+                            </x-filament-tables::row>
44
+                        @endforeach
45
+                        <x-filament-tables::row>
46
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total {{ $accountCategoryName }}</div></x-filament-tables::cell>
47
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->startingBalance ?? '' }}</div></x-filament-tables::cell>
48
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->debitBalance }}</div></x-filament-tables::cell>
49
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->creditBalance }}</div></x-filament-tables::cell>
50
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->netMovement }}</div></x-filament-tables::cell>
51
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountCategory->summary->endingBalance ?? '' }}</div></x-filament-tables::cell>
52
+                        </x-filament-tables::row>
53
+                        <x-filament-tables::row>
54
+                            <x-filament-tables::cell colspan="6">
55
+                                <div class="px-3 py-2 invisible">Hidden Text</div>
56
+                            </x-filament-tables::cell>
57
+                        </x-filament-tables::row>
58
+                        </tbody>
59
+                    @endforeach
60
+                    <tfoot>
61
+                        <tr class="bg-gray-50 dark:bg-white/5">
62
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">Total for all accounts</div></x-filament-tables::cell>
63
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
64
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountBalanceReport->overallTotal->debitBalance }}</div></x-filament-tables::cell>
65
+                            <x-filament-tables::cell class="text-right"><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white">{{ $accountBalanceReport->overallTotal->creditBalance }}</div></x-filament-tables::cell>
66
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
67
+                            <x-filament-tables::cell><div class="px-3 py-2 text-sm leading-6 font-semibold text-gray-950 dark:text-white"></div></x-filament-tables::cell>
68
+                        </tr>
69
+                    </tfoot>
70
+                </table>
71
+            </div>
72
+            <div class="es-table__footer-ctn border-t border-gray-200"></div>
73
+        </x-filament-tables::container>
74
+    </div>
75
+</x-filament-panels::page>

+ 8
- 0
resources/views/infolists/components/report-entry.blade.php Voir le fichier

@@ -0,0 +1,8 @@
1
+<x-dynamic-component :component="$getEntryWrapperView()" :entry="$entry">
2
+    <x-report-entry
3
+        :heading="$getHeading()"
4
+        :description="$getDescription()"
5
+        :icon="$getIcon()"
6
+        :iconColor="$getIconColor()"
7
+    />
8
+</x-dynamic-component>

+ 2
- 2
resources/views/livewire/company/service/connected-account/list-institutions.blade.php Voir le fichier

@@ -1,6 +1,6 @@
1 1
 <div>
2 2
     <div class="grid grid-cols-1 gap-4">
3
-        @forelse($this->connectedInstitutions as $institution) {{-- Group connected accounts by institution --}}
3
+        @forelse($this->connectedInstitutions as $institution)
4 4
             <section class="connected-account-section overflow-hidden rounded-xl bg-white shadow-sm ring-1 ring-gray-950/5 dark:bg-gray-900 dark:ring-white/10">
5 5
                 <header class="connected-account-header bg-primary-300/10 px-6 py-4 flex flex-col sm:flex-row sm:items-center gap-3">
6 6
                     @if($institution->logo_url === null)
@@ -38,7 +38,7 @@
38 38
 
39 39
                             @if($connectedBankAccount->bankAccount?->account)
40 40
                                 <div class="account-balance flex text-base leading-6 text-gray-700 dark:text-gray-200 space-x-1">
41
-                                    <strong>@money($connectedBankAccount->bankAccount->account->ending_balance, $connectedBankAccount->bankAccount->account->currency_code, true)</strong>
41
+                                    <strong>{{ $this->getAccountBalance($connectedBankAccount->bankAccount->account) }}</strong>
42 42
                                     <p>{{ $connectedBankAccount->bankAccount->account->currency_code }}</p>
43 43
                                 </div>
44 44
                             @endif

Chargement…
Annuler
Enregistrer