Files
usher-manage-stack/app/Models/Income.php
Gbanyan 642b879dd4 Add membership fee system with disability discount and fix document permissions
Features:
- Implement two fee types: entrance fee and annual fee (both NT$1,000)
- Add 50% discount for disability certificate holders
- Add disability certificate upload in member profile
- Integrate disability verification into cashier approval workflow
- Add membership fee settings in system admin

Document permissions:
- Fix hard-coded role logic in Document model
- Use permission-based authorization instead of role checks

Additional features:
- Add announcements, general ledger, and trial balance modules
- Add income management and accounting entries
- Add comprehensive test suite with factories
- Update UI translations to Traditional Chinese

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-01 09:56:01 +08:00

447 lines
12 KiB
PHP

<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
use Illuminate\Database\Eloquent\Relations\BelongsTo;
use Illuminate\Database\Eloquent\Relations\HasMany;
use Illuminate\Support\Facades\DB;
class Income extends Model
{
use HasFactory;
// 收入類型常數
const TYPE_MEMBERSHIP_FEE = 'membership_fee'; // 會費收入
const TYPE_ENTRANCE_FEE = 'entrance_fee'; // 入會費收入
const TYPE_DONATION = 'donation'; // 捐款收入
const TYPE_ACTIVITY = 'activity'; // 活動收入
const TYPE_GRANT = 'grant'; // 補助收入
const TYPE_INTEREST = 'interest'; // 利息收入
const TYPE_OTHER = 'other'; // 其他收入
// 狀態常數
const STATUS_PENDING = 'pending'; // 待確認
const STATUS_CONFIRMED = 'confirmed'; // 已確認
const STATUS_CANCELLED = 'cancelled'; // 已取消
// 付款方式常數
const PAYMENT_METHOD_CASH = 'cash';
const PAYMENT_METHOD_BANK_TRANSFER = 'bank_transfer';
const PAYMENT_METHOD_CHECK = 'check';
protected $fillable = [
'income_number',
'title',
'description',
'income_date',
'amount',
'income_type',
'chart_of_account_id',
'payment_method',
'bank_account',
'payer_name',
'receipt_number',
'transaction_reference',
'attachment_path',
'member_id',
'status',
'recorded_by_cashier_id',
'recorded_at',
'confirmed_by_accountant_id',
'confirmed_at',
'cashier_ledger_entry_id',
'notes',
];
protected $casts = [
'income_date' => 'date',
'amount' => 'decimal:2',
'recorded_at' => 'datetime',
'confirmed_at' => 'datetime',
];
/**
* Boot 方法 - 自動產生收入編號
*/
protected static function boot()
{
parent::boot();
static::creating(function ($income) {
if (empty($income->income_number)) {
$income->income_number = self::generateIncomeNumber();
}
if (empty($income->recorded_at)) {
$income->recorded_at = now();
}
});
}
/**
* 產生收入編號 INC-2025-0001
*/
public static function generateIncomeNumber(): string
{
$year = date('Y');
$prefix = "INC-{$year}-";
$lastIncome = self::where('income_number', 'like', "{$prefix}%")
->orderBy('income_number', 'desc')
->first();
if ($lastIncome) {
$lastNumber = (int) substr($lastIncome->income_number, -4);
$newNumber = $lastNumber + 1;
} else {
$newNumber = 1;
}
return $prefix . str_pad($newNumber, 4, '0', STR_PAD_LEFT);
}
// ========== 關聯 ==========
/**
* 會計科目
*/
public function chartOfAccount(): BelongsTo
{
return $this->belongsTo(ChartOfAccount::class);
}
/**
* 關聯會員
*/
public function member(): BelongsTo
{
return $this->belongsTo(Member::class);
}
/**
* 記錄的出納人員
*/
public function recordedByCashier(): BelongsTo
{
return $this->belongsTo(User::class, 'recorded_by_cashier_id');
}
/**
* 確認的會計人員
*/
public function confirmedByAccountant(): BelongsTo
{
return $this->belongsTo(User::class, 'confirmed_by_accountant_id');
}
/**
* 關聯的出納日記帳
*/
public function cashierLedgerEntry(): BelongsTo
{
return $this->belongsTo(CashierLedgerEntry::class);
}
/**
* 會計分錄
*/
public function accountingEntries(): HasMany
{
return $this->hasMany(AccountingEntry::class);
}
// ========== 狀態查詢 ==========
/**
* 是否待確認
*/
public function isPending(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 是否已確認
*/
public function isConfirmed(): bool
{
return $this->status === self::STATUS_CONFIRMED;
}
/**
* 是否已取消
*/
public function isCancelled(): bool
{
return $this->status === self::STATUS_CANCELLED;
}
/**
* 是否可以被會計確認
*/
public function canBeConfirmed(): bool
{
return $this->status === self::STATUS_PENDING;
}
/**
* 是否可以被取消
*/
public function canBeCancelled(): bool
{
return $this->status === self::STATUS_PENDING;
}
// ========== 業務方法 ==========
/**
* 會計確認收入
*/
public function confirmByAccountant(User $accountant): void
{
if (!$this->canBeConfirmed()) {
throw new \Exception('此收入無法確認');
}
DB::transaction(function () use ($accountant) {
// 1. 更新收入狀態
$this->update([
'status' => self::STATUS_CONFIRMED,
'confirmed_by_accountant_id' => $accountant->id,
'confirmed_at' => now(),
]);
// 2. 產生出納日記帳記錄
$ledgerEntry = $this->createCashierLedgerEntry();
// 3. 產生會計分錄
$this->generateAccountingEntries();
});
}
/**
* 取消收入
*/
public function cancel(): void
{
if (!$this->canBeCancelled()) {
throw new \Exception('此收入無法取消');
}
$this->update([
'status' => self::STATUS_CANCELLED,
]);
}
/**
* 建立出納日記帳記錄
*/
protected function createCashierLedgerEntry(): CashierLedgerEntry
{
$bankAccount = $this->bank_account ?? 'Main Account';
$balanceBefore = CashierLedgerEntry::getLatestBalance($bankAccount);
$ledgerEntry = CashierLedgerEntry::create([
'entry_date' => $this->income_date,
'entry_type' => CashierLedgerEntry::ENTRY_TYPE_RECEIPT,
'payment_method' => $this->payment_method,
'bank_account' => $bankAccount,
'amount' => $this->amount,
'balance_before' => $balanceBefore,
'balance_after' => $balanceBefore + $this->amount,
'receipt_number' => $this->receipt_number,
'transaction_reference' => $this->transaction_reference,
'recorded_by_cashier_id' => $this->recorded_by_cashier_id,
'recorded_at' => now(),
'notes' => "收入確認:{$this->title} ({$this->income_number})",
]);
$this->update(['cashier_ledger_entry_id' => $ledgerEntry->id]);
return $ledgerEntry;
}
/**
* 產生會計分錄
*/
protected function generateAccountingEntries(): void
{
// 借方:資產帳戶(現金或銀行存款)
$assetAccountId = $this->getAssetAccountId();
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $assetAccountId,
'entry_type' => AccountingEntry::ENTRY_TYPE_DEBIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
// 貸方:收入科目
AccountingEntry::create([
'income_id' => $this->id,
'chart_of_account_id' => $this->chart_of_account_id,
'entry_type' => AccountingEntry::ENTRY_TYPE_CREDIT,
'amount' => $this->amount,
'entry_date' => $this->income_date,
'description' => "收入:{$this->title} ({$this->income_number})",
]);
}
/**
* 根據付款方式取得資產帳戶 ID
*/
protected function getAssetAccountId(): int
{
$accountCode = match ($this->payment_method) {
self::PAYMENT_METHOD_BANK_TRANSFER => '1201', // 銀行存款
self::PAYMENT_METHOD_CHECK => '1201', // 銀行存款
default => '1101', // 現金
};
return ChartOfAccount::where('account_code', $accountCode)->value('id') ?? 1;
}
// ========== 文字取得 ==========
/**
* 取得收入類型文字
*/
public function getIncomeTypeText(): string
{
return match ($this->income_type) {
self::TYPE_MEMBERSHIP_FEE => '會費收入',
self::TYPE_ENTRANCE_FEE => '入會費收入',
self::TYPE_DONATION => '捐款收入',
self::TYPE_ACTIVITY => '活動收入',
self::TYPE_GRANT => '補助收入',
self::TYPE_INTEREST => '利息收入',
self::TYPE_OTHER => '其他收入',
default => '未知',
};
}
/**
* 取得狀態文字
*/
public function getStatusText(): string
{
return match ($this->status) {
self::STATUS_PENDING => '待確認',
self::STATUS_CONFIRMED => '已確認',
self::STATUS_CANCELLED => '已取消',
default => '未知',
};
}
/**
* 取得付款方式文字
*/
public function getPaymentMethodText(): string
{
return match ($this->payment_method) {
self::PAYMENT_METHOD_CASH => '現金',
self::PAYMENT_METHOD_BANK_TRANSFER => '銀行轉帳',
self::PAYMENT_METHOD_CHECK => '支票',
default => '未知',
};
}
/**
* 取得狀態標籤屬性
*/
public function getStatusLabelAttribute(): string
{
return $this->getStatusText();
}
// ========== 收入類型與科目對應 ==========
/**
* 取得收入類型對應的預設會計科目代碼
*/
public static function getDefaultAccountCode(string $incomeType): string
{
return match ($incomeType) {
self::TYPE_MEMBERSHIP_FEE => '4101',
self::TYPE_ENTRANCE_FEE => '4102',
self::TYPE_DONATION => '4201',
self::TYPE_ACTIVITY => '4402',
self::TYPE_GRANT => '4301',
self::TYPE_INTEREST => '4401',
self::TYPE_OTHER => '4901',
default => '4901',
};
}
/**
* 取得收入類型對應的預設會計科目 ID
*/
public static function getDefaultAccountId(string $incomeType): ?int
{
$accountCode = self::getDefaultAccountCode($incomeType);
return ChartOfAccount::where('account_code', $accountCode)->value('id');
}
/**
* 靜態方法:取得收入類型文字標籤
*/
public static function getIncomeTypeLabel(string $incomeType): string
{
return match ($incomeType) {
self::TYPE_MEMBERSHIP_FEE => '會費收入',
self::TYPE_ENTRANCE_FEE => '入會費收入',
self::TYPE_DONATION => '捐款收入',
self::TYPE_ACTIVITY => '活動收入',
self::TYPE_GRANT => '補助收入',
self::TYPE_INTEREST => '利息收入',
self::TYPE_OTHER => '其他收入',
default => '未知',
};
}
// ========== 查詢範圍 ==========
/**
* 篩選待確認的收入
*/
public function scopePending($query)
{
return $query->where('status', self::STATUS_PENDING);
}
/**
* 篩選已確認的收入
*/
public function scopeConfirmed($query)
{
return $query->where('status', self::STATUS_CONFIRMED);
}
/**
* 篩選特定收入類型
*/
public function scopeOfType($query, string $type)
{
return $query->where('income_type', $type);
}
/**
* 篩選特定會員
*/
public function scopeForMember($query, int $memberId)
{
return $query->where('member_id', $memberId);
}
/**
* 篩選日期範圍
*/
public function scopeDateRange($query, $startDate, $endDate)
{
return $query->whereBetween('income_date', [$startDate, $endDate]);
}
}