hasMany(AccountingEntry::class, $this->getAccountingForeignKey()); } /** * Get the foreign key name for accounting entries */ protected function getAccountingForeignKey(): string { return 'finance_document_id'; } /** * Get debit entries for this model */ public function debitEntries(): HasMany { return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_DEBIT); } /** * Get credit entries for this model */ public function creditEntries(): HasMany { return $this->accountingEntries()->where('entry_type', AccountingEntry::ENTRY_TYPE_CREDIT); } /** * Validate that debit and credit entries balance */ public function validateBalance(): bool { $debitTotal = $this->debitEntries()->sum('amount'); $creditTotal = $this->creditEntries()->sum('amount'); return bccomp((string) $debitTotal, (string) $creditTotal, 2) === 0; } /** * Generate accounting entries for this model * This creates the double-entry bookkeeping records */ public function generateAccountingEntries(array $entries): void { // Delete existing entries $this->accountingEntries()->delete(); // Create new entries foreach ($entries as $entry) { $this->accountingEntries()->create([ 'chart_of_account_id' => $entry['chart_of_account_id'], 'entry_type' => $entry['entry_type'], 'amount' => $entry['amount'], 'entry_date' => $entry['entry_date'] ?? $this->getAccountingDate(), 'description' => $entry['description'] ?? $this->getAccountingDescription(), ]); } } /** * Auto-generate simple accounting entries based on account type * For basic income/expense transactions */ public function autoGenerateAccountingEntries(): void { $chartOfAccountId = $this->getAccountingChartOfAccountId(); // Only auto-generate if chart_of_account_id is set if (! $chartOfAccountId) { return; } $entries = []; $entryDate = $this->getAccountingDate(); $description = $this->getAccountingDescription(); $amount = $this->getAccountingAmount(); // Get account to determine type $account = ChartOfAccount::find($chartOfAccountId); if (! $account) { return; } if ($account->account_type === 'income') { // Income: Debit Cash, Credit Income Account $entries[] = [ 'chart_of_account_id' => $this->getCashAccountId(), 'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT, 'amount' => $amount, 'entry_date' => $entryDate, 'description' => '收入 - '.$description, ]; $entries[] = [ 'chart_of_account_id' => $chartOfAccountId, 'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT, 'amount' => $amount, 'entry_date' => $entryDate, 'description' => $description, ]; } elseif ($account->account_type === 'expense') { // Expense: Debit Expense Account, Credit Cash $entries[] = [ 'chart_of_account_id' => $chartOfAccountId, 'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT, 'amount' => $amount, 'entry_date' => $entryDate, 'description' => $description, ]; $entries[] = [ 'chart_of_account_id' => $this->getCashAccountId(), 'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT, 'amount' => $amount, 'entry_date' => $entryDate, 'description' => '支出 - '.$description, ]; } if (! empty($entries)) { $this->generateAccountingEntries($entries); } } /** * Get the cash account ID using config */ protected function getCashAccountId(): int { static $cashAccountId = null; if ($cashAccountId === null) { $cashCode = config('accounting.account_codes.cash', '1101'); $cashAccount = ChartOfAccount::where('account_code', $cashCode)->first(); $cashAccountId = $cashAccount ? $cashAccount->id : 1; } return $cashAccountId; } /** * Get the bank account ID using config */ protected function getBankAccountId(): int { static $bankAccountId = null; if ($bankAccountId === null) { $bankCode = config('accounting.account_codes.bank', '1201'); $bankAccount = ChartOfAccount::where('account_code', $bankCode)->first(); $bankAccountId = $bankAccount ? $bankAccount->id : 2; } return $bankAccountId; } /** * Get description for accounting entries * Override in model if needed */ protected function getAccountingDescription(): string { return $this->description ?? $this->title ?? 'Transaction'; } /** * Get date for accounting entries * Override in model if needed */ protected function getAccountingDate() { return $this->submitted_at ?? $this->created_at ?? now(); } /** * Get chart of account ID for auto-generation * Override in model if needed */ protected function getAccountingChartOfAccountId(): ?int { return $this->chart_of_account_id ?? null; } /** * Get amount for accounting entries * Override in model if needed */ protected function getAccountingAmount(): float { return (float) ($this->amount ?? 0); } }