Du kan inte välja fler än 25 ämnen Ämnen måste starta med en bokstav eller siffra, kan innehålla bindestreck ('-') och vara max 35 tecken långa.

ChartOfAccountsService.php 6.1KB

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