307 lines
11 KiB
PHP
307 lines
11 KiB
PHP
<?php
|
||
|
||
namespace App\Http\Controllers;
|
||
|
||
use App\Models\BankReconciliation;
|
||
use App\Models\CashierLedgerEntry;
|
||
use App\Support\AuditLogger;
|
||
use Illuminate\Http\Request;
|
||
use Illuminate\Support\Facades\DB;
|
||
use Illuminate\Support\Facades\Storage;
|
||
|
||
class BankReconciliationController extends Controller
|
||
{
|
||
/**
|
||
* Display a listing of bank reconciliations
|
||
*/
|
||
public function index(Request $request)
|
||
{
|
||
$query = BankReconciliation::query()
|
||
->with([
|
||
'preparedByCashier',
|
||
'reviewedByAccountant',
|
||
'approvedByManager'
|
||
])
|
||
->orderByDesc('reconciliation_month');
|
||
|
||
// Filter by status
|
||
if ($request->filled('reconciliation_status')) {
|
||
$query->where('reconciliation_status', $request->reconciliation_status);
|
||
}
|
||
|
||
// Filter by month
|
||
if ($request->filled('month')) {
|
||
$query->whereYear('reconciliation_month', '=', substr($request->month, 0, 4))
|
||
->whereMonth('reconciliation_month', '=', substr($request->month, 5, 2));
|
||
}
|
||
|
||
$reconciliations = $query->paginate(15);
|
||
|
||
return view('admin.bank-reconciliations.index', [
|
||
'reconciliations' => $reconciliations,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Show the form for creating a new bank reconciliation
|
||
*/
|
||
public function create(Request $request)
|
||
{
|
||
// Check authorization
|
||
$this->authorize('prepare_bank_reconciliation');
|
||
|
||
// Default to current month
|
||
$month = $request->input('month', now()->format('Y-m'));
|
||
|
||
// Get system book balance from cashier ledger
|
||
$systemBalance = CashierLedgerEntry::getLatestBalance();
|
||
|
||
return view('admin.bank-reconciliations.create', [
|
||
'month' => $month,
|
||
'systemBalance' => $systemBalance,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Store a newly created bank reconciliation
|
||
*/
|
||
public function store(Request $request)
|
||
{
|
||
// Check authorization
|
||
$this->authorize('prepare_bank_reconciliation');
|
||
|
||
$validated = $request->validate([
|
||
'reconciliation_month' => ['required', 'date_format:Y-m'],
|
||
'bank_statement_balance' => ['required', 'numeric'],
|
||
'bank_statement_date' => ['required', 'date'],
|
||
'bank_statement_file' => ['nullable', 'file', 'max:10240'],
|
||
'system_book_balance' => ['required', 'numeric'],
|
||
'outstanding_checks' => ['nullable', 'array'],
|
||
'outstanding_checks.*.amount' => ['required', 'numeric', 'min:0'],
|
||
'outstanding_checks.*.check_number' => ['nullable', 'string'],
|
||
'outstanding_checks.*.description' => ['nullable', 'string'],
|
||
'deposits_in_transit' => ['nullable', 'array'],
|
||
'deposits_in_transit.*.amount' => ['required', 'numeric', 'min:0'],
|
||
'deposits_in_transit.*.date' => ['nullable', 'date'],
|
||
'deposits_in_transit.*.description' => ['nullable', 'string'],
|
||
'bank_charges' => ['nullable', 'array'],
|
||
'bank_charges.*.amount' => ['required', 'numeric', 'min:0'],
|
||
'bank_charges.*.description' => ['nullable', 'string'],
|
||
'notes' => ['nullable', 'string'],
|
||
]);
|
||
|
||
DB::beginTransaction();
|
||
try {
|
||
// Handle bank statement file upload
|
||
$statementPath = null;
|
||
if ($request->hasFile('bank_statement_file')) {
|
||
$statementPath = $request->file('bank_statement_file')->store('bank-statements', 'local');
|
||
}
|
||
|
||
// Create reconciliation record
|
||
$reconciliation = new BankReconciliation([
|
||
'reconciliation_month' => $validated['reconciliation_month'] . '-01',
|
||
'bank_statement_balance' => $validated['bank_statement_balance'],
|
||
'bank_statement_date' => $validated['bank_statement_date'],
|
||
'bank_statement_file_path' => $statementPath,
|
||
'system_book_balance' => $validated['system_book_balance'],
|
||
'outstanding_checks' => $validated['outstanding_checks'] ?? [],
|
||
'deposits_in_transit' => $validated['deposits_in_transit'] ?? [],
|
||
'bank_charges' => $validated['bank_charges'] ?? [],
|
||
'prepared_by_cashier_id' => $request->user()->id,
|
||
'prepared_at' => now(),
|
||
'notes' => $validated['notes'] ?? null,
|
||
]);
|
||
|
||
// Calculate adjusted balance
|
||
$reconciliation->adjusted_balance = $reconciliation->calculateAdjustedBalance();
|
||
|
||
// Calculate discrepancy
|
||
$reconciliation->discrepancy_amount = $reconciliation->calculateDiscrepancy();
|
||
|
||
// Set status based on discrepancy
|
||
if ($reconciliation->hasDiscrepancy()) {
|
||
$reconciliation->reconciliation_status = BankReconciliation::STATUS_DISCREPANCY;
|
||
} else {
|
||
$reconciliation->reconciliation_status = BankReconciliation::STATUS_PENDING;
|
||
}
|
||
|
||
$reconciliation->save();
|
||
|
||
AuditLogger::log('bank_reconciliation.created', $reconciliation, $validated);
|
||
|
||
DB::commit();
|
||
|
||
$message = '銀行調節表已建立。';
|
||
if ($reconciliation->hasDiscrepancy()) {
|
||
$message .= ' 發現差異金額:NT$ ' . number_format($reconciliation->discrepancy_amount, 2);
|
||
}
|
||
|
||
return redirect()
|
||
->route('admin.bank-reconciliations.show', $reconciliation)
|
||
->with('status', $message);
|
||
|
||
} catch (\Exception $e) {
|
||
DB::rollBack();
|
||
return redirect()
|
||
->back()
|
||
->withInput()
|
||
->with('error', '建立銀行調節表時發生錯誤:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Display the specified bank reconciliation
|
||
*/
|
||
public function show(BankReconciliation $bankReconciliation)
|
||
{
|
||
$bankReconciliation->load([
|
||
'preparedByCashier',
|
||
'reviewedByAccountant',
|
||
'approvedByManager'
|
||
]);
|
||
|
||
// Get outstanding items summary
|
||
$summary = $bankReconciliation->getOutstandingItemsSummary();
|
||
|
||
return view('admin.bank-reconciliations.show', [
|
||
'reconciliation' => $bankReconciliation,
|
||
'summary' => $summary,
|
||
]);
|
||
}
|
||
|
||
/**
|
||
* Accountant reviews the bank reconciliation
|
||
*/
|
||
public function review(Request $request, BankReconciliation $bankReconciliation)
|
||
{
|
||
// Check authorization
|
||
$this->authorize('review_bank_reconciliation');
|
||
|
||
// Check if can be reviewed
|
||
if (!$bankReconciliation->canBeReviewed()) {
|
||
return redirect()
|
||
->route('admin.bank-reconciliations.show', $bankReconciliation)
|
||
->with('error', '此銀行調節表無法覆核。');
|
||
}
|
||
|
||
$validated = $request->validate([
|
||
'review_notes' => ['nullable', 'string'],
|
||
]);
|
||
|
||
DB::beginTransaction();
|
||
try {
|
||
$bankReconciliation->update([
|
||
'reviewed_by_accountant_id' => $request->user()->id,
|
||
'reviewed_at' => now(),
|
||
]);
|
||
|
||
AuditLogger::log('bank_reconciliation.reviewed', $bankReconciliation, $validated);
|
||
|
||
DB::commit();
|
||
|
||
return redirect()
|
||
->route('admin.bank-reconciliations.show', $bankReconciliation)
|
||
->with('status', '銀行調節表已完成會計覆核。');
|
||
|
||
} catch (\Exception $e) {
|
||
DB::rollBack();
|
||
return redirect()
|
||
->back()
|
||
->with('error', '覆核銀行調節表時發生錯誤:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Manager approves the bank reconciliation
|
||
*/
|
||
public function approve(Request $request, BankReconciliation $bankReconciliation)
|
||
{
|
||
// Check authorization
|
||
$this->authorize('approve_bank_reconciliation');
|
||
|
||
// Check if can be approved
|
||
if (!$bankReconciliation->canBeApproved()) {
|
||
return redirect()
|
||
->route('admin.bank-reconciliations.show', $bankReconciliation)
|
||
->with('error', '此銀行調節表無法核准。');
|
||
}
|
||
|
||
DB::beginTransaction();
|
||
try {
|
||
// Determine final status
|
||
$finalStatus = $bankReconciliation->hasDiscrepancy()
|
||
? BankReconciliation::STATUS_DISCREPANCY
|
||
: BankReconciliation::STATUS_COMPLETED;
|
||
|
||
$bankReconciliation->update([
|
||
'approved_by_manager_id' => $request->user()->id,
|
||
'approved_at' => now(),
|
||
'reconciliation_status' => $finalStatus,
|
||
]);
|
||
|
||
AuditLogger::log('bank_reconciliation.approved', $bankReconciliation, [
|
||
'approved_by' => $request->user()->name,
|
||
'final_status' => $finalStatus,
|
||
]);
|
||
|
||
DB::commit();
|
||
|
||
$message = '銀行調節表已核准。';
|
||
if ($finalStatus === BankReconciliation::STATUS_DISCREPANCY) {
|
||
$message .= ' 請注意:仍有差異需要處理。';
|
||
}
|
||
|
||
return redirect()
|
||
->route('admin.bank-reconciliations.show', $bankReconciliation)
|
||
->with('status', $message);
|
||
|
||
} catch (\Exception $e) {
|
||
DB::rollBack();
|
||
return redirect()
|
||
->back()
|
||
->with('error', '核准銀行調節表時發生錯誤:' . $e->getMessage());
|
||
}
|
||
}
|
||
|
||
/**
|
||
* Download bank statement file
|
||
*/
|
||
public function downloadStatement(BankReconciliation $bankReconciliation)
|
||
{
|
||
if (!$bankReconciliation->bank_statement_file_path) {
|
||
abort(404, '找不到銀行對帳單檔案');
|
||
}
|
||
|
||
if (!Storage::disk('local')->exists($bankReconciliation->bank_statement_file_path)) {
|
||
abort(404, '銀行對帳單檔案不存在');
|
||
}
|
||
|
||
return Storage::disk('local')->download($bankReconciliation->bank_statement_file_path);
|
||
}
|
||
|
||
/**
|
||
* Export reconciliation to PDF
|
||
*/
|
||
public function exportPdf(BankReconciliation $bankReconciliation)
|
||
{
|
||
// Check authorization
|
||
$this->authorize('view_cashier_ledger');
|
||
|
||
$bankReconciliation->load([
|
||
'preparedByCashier',
|
||
'reviewedByAccountant',
|
||
'approvedByManager'
|
||
]);
|
||
|
||
$summary = $bankReconciliation->getOutstandingItemsSummary();
|
||
|
||
// Generate PDF (you would need to implement PDF generation library like DomPDF or TCPDF)
|
||
// For now, return a view that can be printed
|
||
return view('admin.bank-reconciliations.pdf', [
|
||
'reconciliation' => $bankReconciliation,
|
||
'summary' => $summary,
|
||
]);
|
||
}
|
||
}
|