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>
This commit is contained in:
@@ -26,11 +26,6 @@ class FinanceDocumentController extends Controller
|
||||
$query->where('status', $request->status);
|
||||
}
|
||||
|
||||
// Filter by request type
|
||||
if ($request->filled('request_type')) {
|
||||
$query->where('request_type', $request->request_type);
|
||||
}
|
||||
|
||||
// Filter by amount tier
|
||||
if ($request->filled('amount_tier')) {
|
||||
$query->where('amount_tier', $request->amount_tier);
|
||||
@@ -79,7 +74,6 @@ class FinanceDocumentController extends Controller
|
||||
'member_id' => ['nullable', 'exists:members,id'],
|
||||
'title' => ['required', 'string', 'max:255'],
|
||||
'amount' => ['required', 'numeric', 'min:0'],
|
||||
'request_type' => ['required', 'in:expense_reimbursement,advance_payment,purchase_request,petty_cash'],
|
||||
'description' => ['nullable', 'string'],
|
||||
'attachment' => ['nullable', 'file', 'max:10240'], // 10MB max
|
||||
]);
|
||||
@@ -95,7 +89,6 @@ class FinanceDocumentController extends Controller
|
||||
'submitted_by_user_id' => $request->user()->id,
|
||||
'title' => $validated['title'],
|
||||
'amount' => $validated['amount'],
|
||||
'request_type' => $validated['request_type'],
|
||||
'description' => $validated['description'] ?? null,
|
||||
'attachment_path' => $attachmentPath,
|
||||
'status' => FinanceDocument::STATUS_PENDING,
|
||||
@@ -115,17 +108,13 @@ class FinanceDocumentController extends Controller
|
||||
|
||||
// Send email notification to finance cashiers
|
||||
$cashiers = User::role('finance_cashier')->get();
|
||||
if ($cashiers->isEmpty()) {
|
||||
// Fallback to old cashier role for backward compatibility
|
||||
$cashiers = User::role('cashier')->get();
|
||||
}
|
||||
foreach ($cashiers as $cashier) {
|
||||
Mail::to($cashier->email)->queue(new FinanceDocumentSubmitted($document));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.index')
|
||||
->with('status', '財務申請單已提交。申請類型:' . $document->getRequestTypeText() . ',金額級別:' . $document->getAmountTierText());
|
||||
->with('status', '報銷申請單已提交。金額級別:' . $document->getAmountTierText());
|
||||
}
|
||||
|
||||
public function show(FinanceDocument $financeDocument)
|
||||
@@ -133,13 +122,19 @@ class FinanceDocumentController extends Controller
|
||||
$financeDocument->load([
|
||||
'member',
|
||||
'submittedBy',
|
||||
// 新工作流程 relationships
|
||||
'approvedBySecretary',
|
||||
'approvedByChair',
|
||||
'approvedByBoardMeeting',
|
||||
'requesterConfirmedBy',
|
||||
'cashierConfirmedBy',
|
||||
'accountantRecordedBy',
|
||||
// Legacy relationships
|
||||
'approvedByCashier',
|
||||
'approvedByAccountant',
|
||||
'approvedByChair',
|
||||
'rejectedBy',
|
||||
'chartOfAccount',
|
||||
'budgetItem',
|
||||
'approvedByBoardMeeting',
|
||||
'paymentOrderCreatedByAccountant',
|
||||
'paymentVerifiedByCashier',
|
||||
'paymentExecutedByCashier',
|
||||
@@ -159,72 +154,48 @@ class FinanceDocumentController extends Controller
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Check if user has any finance approval permissions
|
||||
$isCashier = $user->hasRole('finance_cashier') || $user->hasRole('cashier');
|
||||
$isAccountant = $user->hasRole('finance_accountant') || $user->hasRole('accountant');
|
||||
$isChair = $user->hasRole('finance_chair') || $user->hasRole('chair');
|
||||
// 新工作流程:秘書長 → 理事長 → 董理事會
|
||||
$isSecretary = $user->hasRole('secretary_general');
|
||||
$isChair = $user->hasRole('finance_chair');
|
||||
$isBoardMember = $user->hasRole('finance_board_member');
|
||||
$isAdmin = $user->hasRole('admin');
|
||||
|
||||
// Determine which level of approval based on current status and user role
|
||||
if ($financeDocument->canBeApprovedByCashier() && $isCashier) {
|
||||
// 秘書長審核(第一階段)
|
||||
if ($financeDocument->canBeApprovedBySecretary($user) && ($isSecretary || $isAdmin)) {
|
||||
$financeDocument->update([
|
||||
'approved_by_cashier_id' => $user->id,
|
||||
'cashier_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_CASHIER,
|
||||
'approved_by_secretary_id' => $user->id,
|
||||
'secretary_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_SECRETARY,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_cashier', $financeDocument, [
|
||||
AuditLogger::log('finance_document.approved_by_secretary', $financeDocument, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $financeDocument->amount_tier,
|
||||
]);
|
||||
|
||||
// Send email notification to accountants
|
||||
$accountants = User::role('finance_accountant')->get();
|
||||
if ($accountants->isEmpty()) {
|
||||
$accountants = User::role('accountant')->get();
|
||||
}
|
||||
foreach ($accountants as $accountant) {
|
||||
Mail::to($accountant->email)->queue(new FinanceDocumentApprovedByCashier($financeDocument));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '出納已審核通過。已送交會計審核。');
|
||||
}
|
||||
|
||||
if ($financeDocument->canBeApprovedByAccountant() && $isAccountant) {
|
||||
$financeDocument->update([
|
||||
'approved_by_accountant_id' => $user->id,
|
||||
'accountant_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_ACCOUNTANT,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_accountant', $financeDocument, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $financeDocument->amount_tier,
|
||||
]);
|
||||
|
||||
// For small amounts, approval is complete (no chair needed)
|
||||
// 小額:審核完成
|
||||
if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_SMALL) {
|
||||
// 通知申請人審核已完成,可以領款
|
||||
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '會計已審核通過。小額申請審核完成,可以製作付款單。');
|
||||
->with('status', '秘書長已核准。小額申請審核完成,申請人可向出納領款。');
|
||||
}
|
||||
|
||||
// For medium and large amounts, send to chair
|
||||
// 中額/大額:送交理事長
|
||||
$chairs = User::role('finance_chair')->get();
|
||||
if ($chairs->isEmpty()) {
|
||||
$chairs = User::role('chair')->get();
|
||||
}
|
||||
foreach ($chairs as $chair) {
|
||||
Mail::to($chair->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '會計已審核通過。已送交理事長審核。');
|
||||
->with('status', '秘書長已核准。已送交理事長審核。');
|
||||
}
|
||||
|
||||
if ($financeDocument->canBeApprovedByChair() && $isChair) {
|
||||
// 理事長審核(第二階段:中額或大額)
|
||||
if ($financeDocument->canBeApprovedByChair($user) && ($isChair || $isAdmin)) {
|
||||
$financeDocument->update([
|
||||
'approved_by_chair_id' => $user->id,
|
||||
'chair_approved_at' => now(),
|
||||
@@ -234,25 +205,147 @@ class FinanceDocumentController extends Controller
|
||||
AuditLogger::log('finance_document.approved_by_chair', $financeDocument, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $financeDocument->amount_tier,
|
||||
'requires_board_meeting' => $financeDocument->requires_board_meeting,
|
||||
]);
|
||||
|
||||
// For large amounts, notify that board meeting approval is still needed
|
||||
if ($financeDocument->requires_board_meeting && !$financeDocument->board_meeting_approved_at) {
|
||||
// 中額:審核完成
|
||||
if ($financeDocument->amount_tier === FinanceDocument::AMOUNT_TIER_MEDIUM) {
|
||||
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '理事長已審核通過。大額申請仍需理事會核准。');
|
||||
->with('status', '理事長已核准。中額申請審核完成,申請人可向出納領款。');
|
||||
}
|
||||
|
||||
// For medium amounts or large amounts with board approval, complete
|
||||
// 大額:送交董理事會
|
||||
$boardMembers = User::role('finance_board_member')->get();
|
||||
foreach ($boardMembers as $member) {
|
||||
Mail::to($member->email)->queue(new FinanceDocumentApprovedByAccountant($financeDocument));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '理事長已核准。大額申請需送交董理事會審核。');
|
||||
}
|
||||
|
||||
// 董理事會審核(第三階段:大額)
|
||||
if ($financeDocument->canBeApprovedByBoard($user) && ($isBoardMember || $isAdmin)) {
|
||||
$financeDocument->update([
|
||||
'board_meeting_approved_by_id' => $user->id,
|
||||
'board_meeting_approved_at' => now(),
|
||||
'status' => FinanceDocument::STATUS_APPROVED_BOARD,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.approved_by_board', $financeDocument, [
|
||||
'approved_by' => $user->name,
|
||||
'amount_tier' => $financeDocument->amount_tier,
|
||||
]);
|
||||
|
||||
Mail::to($financeDocument->submittedBy->email)->queue(new FinanceDocumentFullyApproved($financeDocument));
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '審核流程完成。會計可以製作付款單。');
|
||||
->with('status', '董理事會已核准。審核流程完成,申請人可向出納領款。');
|
||||
}
|
||||
|
||||
abort(403, 'You are not authorized to approve this document at this stage.');
|
||||
abort(403, '您無權在此階段審核此文件。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 出帳確認(雙重確認:申請人 + 出納)
|
||||
*/
|
||||
public function confirmDisbursement(Request $request, FinanceDocument $financeDocument)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$isRequester = $financeDocument->submitted_by_user_id === $user->id;
|
||||
$isCashier = $user->hasRole('finance_cashier');
|
||||
$isAdmin = $user->hasRole('admin');
|
||||
|
||||
// 申請人確認
|
||||
if ($isRequester && $financeDocument->canRequesterConfirmDisbursement($user)) {
|
||||
$financeDocument->update([
|
||||
'requester_confirmed_at' => now(),
|
||||
'requester_confirmed_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.requester_confirmed_disbursement', $financeDocument, [
|
||||
'confirmed_by' => $user->name,
|
||||
]);
|
||||
|
||||
// 檢查是否雙重確認完成
|
||||
if ($financeDocument->isDisbursementComplete()) {
|
||||
$financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]);
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '出帳確認完成。等待會計入帳。');
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '申請人已確認領款。等待出納確認。');
|
||||
}
|
||||
|
||||
// 出納確認
|
||||
if (($isCashier || $isAdmin) && $financeDocument->canCashierConfirmDisbursement()) {
|
||||
$financeDocument->update([
|
||||
'cashier_confirmed_at' => now(),
|
||||
'cashier_confirmed_by_id' => $user->id,
|
||||
]);
|
||||
|
||||
AuditLogger::log('finance_document.cashier_confirmed_disbursement', $financeDocument, [
|
||||
'confirmed_by' => $user->name,
|
||||
]);
|
||||
|
||||
// 檢查是否雙重確認完成
|
||||
if ($financeDocument->isDisbursementComplete()) {
|
||||
$financeDocument->update(['disbursement_status' => FinanceDocument::DISBURSEMENT_COMPLETED]);
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '出帳確認完成。等待會計入帳。');
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '出納已確認出帳。等待申請人確認。');
|
||||
}
|
||||
|
||||
abort(403, '您無權確認此出帳。');
|
||||
}
|
||||
|
||||
/**
|
||||
* 入帳確認(會計)
|
||||
*/
|
||||
public function confirmRecording(Request $request, FinanceDocument $financeDocument)
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
$isAccountant = $user->hasRole('finance_accountant');
|
||||
$isAdmin = $user->hasRole('admin');
|
||||
|
||||
if (!$financeDocument->canAccountantConfirmRecording()) {
|
||||
abort(403, '此文件尚未完成出帳確認,無法入帳。');
|
||||
}
|
||||
|
||||
if (!$isAccountant && !$isAdmin) {
|
||||
abort(403, '只有會計可以確認入帳。');
|
||||
}
|
||||
|
||||
$financeDocument->update([
|
||||
'accountant_recorded_at' => now(),
|
||||
'accountant_recorded_by_id' => $user->id,
|
||||
'recording_status' => FinanceDocument::RECORDING_COMPLETED,
|
||||
]);
|
||||
|
||||
// 自動產生會計分錄
|
||||
$financeDocument->autoGenerateAccountingEntries();
|
||||
|
||||
AuditLogger::log('finance_document.accountant_confirmed_recording', $financeDocument, [
|
||||
'confirmed_by' => $user->name,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '會計已確認入帳。財務流程完成。');
|
||||
}
|
||||
|
||||
public function reject(Request $request, FinanceDocument $financeDocument)
|
||||
@@ -269,9 +362,12 @@ class FinanceDocumentController extends Controller
|
||||
}
|
||||
|
||||
// Check if user has permission to reject
|
||||
$canReject = $user->hasRole('finance_cashier') || $user->hasRole('cashier') ||
|
||||
$user->hasRole('finance_accountant') || $user->hasRole('accountant') ||
|
||||
$user->hasRole('finance_chair') || $user->hasRole('chair');
|
||||
$canReject = $user->hasRole('admin') ||
|
||||
$user->hasRole('secretary_general') ||
|
||||
$user->hasRole('finance_cashier') ||
|
||||
$user->hasRole('finance_accountant') ||
|
||||
$user->hasRole('finance_chair') ||
|
||||
$user->hasRole('finance_board_member');
|
||||
|
||||
if (!$canReject) {
|
||||
abort(403, '您無權駁回此文件。');
|
||||
@@ -295,7 +391,7 @@ class FinanceDocumentController extends Controller
|
||||
|
||||
return redirect()
|
||||
->route('admin.finance.show', $financeDocument)
|
||||
->with('status', '財務申請單已駁回。');
|
||||
->with('status', '報銷申請單已駁回。');
|
||||
}
|
||||
|
||||
public function download(FinanceDocument $financeDocument)
|
||||
|
||||
Reference in New Issue
Block a user