Du kannst nicht mehr als 25 Themen auswählen Themen müssen mit entweder einem Buchstaben oder einer Ziffer beginnen. Sie können Bindestriche („-“) enthalten und bis zu 35 Zeichen lang sein.

ChartOfAccountsService.php 6.1KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142
  1. <?php
  2. namespace App\Services;
  3. use App\Enums\Accounting\AccountType;
  4. use App\Enums\Banking\BankAccountType;
  5. use App\Models\Accounting\Account;
  6. use App\Models\Accounting\AccountSubtype;
  7. use App\Models\Accounting\Adjustment;
  8. use App\Models\Banking\BankAccount;
  9. use App\Models\Company;
  10. use Exception;
  11. class ChartOfAccountsService
  12. {
  13. private string $currencyCode;
  14. public function createChartOfAccounts(Company $company, string $currencyCode = 'USD'): void
  15. {
  16. $this->currencyCode = $currencyCode;
  17. $chartOfAccounts = config('chart-of-accounts.default');
  18. // Always create a non-recoverable "Purchase Tax" adjustment, even without an account
  19. $this->createAdjustmentForAccount($company, 'tax', 'purchase', false);
  20. foreach ($chartOfAccounts as $type => $subtypes) {
  21. foreach ($subtypes as $subtypeName => $subtypeConfig) {
  22. $subtype = $company->accountSubtypes()
  23. ->createQuietly([
  24. 'multi_currency' => $subtypeConfig['multi_currency'] ?? false,
  25. 'inverse_cash_flow' => $subtypeConfig['inverse_cash_flow'] ?? false,
  26. 'category' => AccountType::from($type)->getCategory()->value,
  27. 'type' => $type,
  28. 'name' => $subtypeName,
  29. 'description' => $subtypeConfig['description'] ?? 'No description available.',
  30. ]);
  31. try {
  32. $this->createDefaultAccounts($company, $subtype, $subtypeConfig);
  33. } catch (Exception $e) {
  34. // Log the error
  35. logger()->alert('Failed to create a company with its defaults, blocking critical business functionality.', [
  36. 'error' => $e->getMessage(),
  37. 'userId' => $company->owner->id,
  38. 'companyId' => $company->id,
  39. ]);
  40. throw $e;
  41. }
  42. }
  43. }
  44. }
  45. private function createDefaultAccounts(Company $company, AccountSubtype $subtype, array $subtypeConfig): void
  46. {
  47. if (isset($subtypeConfig['accounts']) && is_array($subtypeConfig['accounts'])) {
  48. $baseCode = $subtypeConfig['base_code'];
  49. $defaultCurrencyCode = $this->currencyCode;
  50. if (empty($defaultCurrencyCode)) {
  51. throw new Exception('No default currency available for creating accounts.');
  52. }
  53. foreach ($subtypeConfig['accounts'] as $accountName => $accountDetails) {
  54. // Create the Account without directly setting bank_account_id
  55. /** @var Account $account */
  56. $account = $company->accounts()->createQuietly([
  57. 'subtype_id' => $subtype->id,
  58. 'category' => $subtype->type->getCategory()->value,
  59. 'type' => $subtype->type->value,
  60. 'code' => $baseCode++,
  61. 'name' => $accountName,
  62. 'currency_code' => $defaultCurrencyCode,
  63. 'description' => $accountDetails['description'] ?? 'No description available.',
  64. 'default' => true,
  65. 'created_by' => $company->owner->id,
  66. 'updated_by' => $company->owner->id,
  67. ]);
  68. // Check if we need to create a BankAccount for this Account
  69. if ($subtypeConfig['multi_currency'] && isset($subtypeConfig['bank_account_type'])) {
  70. $bankAccount = $this->createBankAccountForMultiCurrency($company, $subtypeConfig['bank_account_type']);
  71. // Associate the BankAccount with the Account
  72. $bankAccount->account()->associate($account);
  73. $bankAccount->saveQuietly();
  74. }
  75. if (isset($subtypeConfig['adjustment_category'], $subtypeConfig['adjustment_type'], $subtypeConfig['adjustment_recoverable'])) {
  76. $adjustment = $this->createAdjustmentForAccount($company, $subtypeConfig['adjustment_category'], $subtypeConfig['adjustment_type'], $subtypeConfig['adjustment_recoverable']);
  77. // Associate the Adjustment with the Account
  78. $adjustment->account()->associate($account);
  79. $adjustment->name = $account->name;
  80. $adjustment->description = $account->description;
  81. $adjustment->saveQuietly();
  82. }
  83. }
  84. }
  85. }
  86. private function createBankAccountForMultiCurrency(Company $company, string $bankAccountType): BankAccount
  87. {
  88. $noDefaultBankAccount = $company->bankAccounts()->where('enabled', true)->doesntExist();
  89. return $company->bankAccounts()->createQuietly([
  90. 'type' => BankAccountType::from($bankAccountType) ?? BankAccountType::Other,
  91. 'enabled' => $noDefaultBankAccount,
  92. 'created_by' => $company->owner->id,
  93. 'updated_by' => $company->owner->id,
  94. ]);
  95. }
  96. private function createAdjustmentForAccount(Company $company, string $category, string $type, bool $recoverable): Adjustment
  97. {
  98. $defaultRate = match ([$category, $type]) {
  99. ['tax', 'sales'], ['tax', 'purchase'] => '8',
  100. ['discount', 'sales'], ['discount', 'purchase'] => '5',
  101. default => '0',
  102. };
  103. if ($category === 'tax' && $type === 'purchase' && $recoverable === false) {
  104. $name = 'Purchase Tax';
  105. $description = 'This tax is non-recoverable and is included as part of the total cost of the purchase. The tax amount is embedded into the associated expense or asset account based on the type of purchase.';
  106. }
  107. return $company->adjustments()->createQuietly([
  108. 'name' => $name ?? null,
  109. 'description' => $description ?? null,
  110. 'category' => $category,
  111. 'type' => $type,
  112. 'recoverable' => $recoverable,
  113. 'rate' => $defaultRate,
  114. 'computation' => 'percentage',
  115. 'created_by' => $company->owner->id,
  116. 'updated_by' => $company->owner->id,
  117. ]);
  118. }
  119. }