[FEAT] 優化會計報表:新增稅額、發票日期與付款方式等會計專用欄位並支援 CSV 完整匯出
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
All checks were successful
ERP-Deploy-Demo / deploy-demo (push) Successful in 1m0s
This commit is contained in:
@@ -69,14 +69,25 @@ class AccountingReportController extends Controller
|
|||||||
}
|
}
|
||||||
|
|
||||||
$exportData = $allRecords->map(function ($record) {
|
$exportData = $allRecords->map(function ($record) {
|
||||||
|
$taxAmount = (float)($record['tax_amount'] ?? 0);
|
||||||
|
$totalAmount = (float)($record['amount'] ?? 0);
|
||||||
|
$untaxedAmount = $totalAmount - $taxAmount;
|
||||||
|
|
||||||
return [
|
return [
|
||||||
$record['date'],
|
$record['date'],
|
||||||
$record['source'],
|
$record['source'],
|
||||||
$record['category'],
|
$record['category'],
|
||||||
$record['item'],
|
$record['item'],
|
||||||
$record['reference'],
|
$record['reference'],
|
||||||
$record['invoice_number'],
|
$record['invoice_date'] ?? '-',
|
||||||
$record['amount'],
|
$record['invoice_number'] ?? '-',
|
||||||
|
$untaxedAmount,
|
||||||
|
$taxAmount,
|
||||||
|
$totalAmount,
|
||||||
|
$record['payment_method'] ?? '-',
|
||||||
|
$record['payment_note'] ?? '-',
|
||||||
|
$record['remarks'] ?? '-',
|
||||||
|
$record['status'] ?? '-',
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -91,7 +102,11 @@ class AccountingReportController extends Controller
|
|||||||
// BOM for Excel compatibility with UTF-8
|
// BOM for Excel compatibility with UTF-8
|
||||||
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
fprintf($file, chr(0xEF).chr(0xBB).chr(0xBF));
|
||||||
|
|
||||||
fputcsv($file, ['日期', '來源', '類別', '項目', '參考單號', '發票號碼', '金額']);
|
fputcsv($file, [
|
||||||
|
'日期', '來源', '類別', '項目', '參考單號',
|
||||||
|
'發票日期', '發票號碼', '未稅金額', '稅額', '總金額',
|
||||||
|
'付款方式', '付款備註', '內部備註', '狀態'
|
||||||
|
]);
|
||||||
|
|
||||||
foreach ($exportData as $row) {
|
foreach ($exportData as $row) {
|
||||||
fputcsv($file, $row);
|
fputcsv($file, $row);
|
||||||
|
|||||||
@@ -19,23 +19,48 @@ class FinanceService implements FinanceServiceInterface
|
|||||||
|
|
||||||
public function getAccountingReportData(string $start, string $end): array
|
public function getAccountingReportData(string $start, string $end): array
|
||||||
{
|
{
|
||||||
// 1. 獲取採購單資料
|
// 1. 獲取應付帳款資料 (已付款)
|
||||||
$purchaseOrders = $this->procurementService->getPurchaseOrdersByDate($start, $end)
|
$accountPayables = \App\Modules\Finance\Models\AccountPayable::where('status', \App\Modules\Finance\Models\AccountPayable::STATUS_PAID)
|
||||||
->map(function ($po) {
|
->whereNotNull('paid_at')
|
||||||
|
->whereBetween('paid_at', [$start, $end])
|
||||||
|
->get();
|
||||||
|
|
||||||
|
// 取得供應商資料 (Manual Hydration)
|
||||||
|
$vendorIds = $accountPayables->pluck('vendor_id')->unique()->filter()->toArray();
|
||||||
|
$vendorsMap = $this->procurementService->getVendorsByIds($vendorIds)->keyBy('id');
|
||||||
|
|
||||||
|
// 付款方式對映
|
||||||
|
$paymentMethodMap = [
|
||||||
|
'cash' => '現金',
|
||||||
|
'bank_transfer' => '銀行轉帳',
|
||||||
|
'check' => '支票',
|
||||||
|
'credit_card' => '信用卡',
|
||||||
|
];
|
||||||
|
|
||||||
|
$payableRecords = $accountPayables->map(function ($ap) use ($vendorsMap, $paymentMethodMap) {
|
||||||
|
$vendorName = isset($vendorsMap[$ap->vendor_id]) ? $vendorsMap[$ap->vendor_id]->name : '未知廠商';
|
||||||
|
$mappedPaymentMethod = $paymentMethodMap[$ap->payment_method] ?? $ap->payment_method;
|
||||||
return [
|
return [
|
||||||
'id' => 'PO-' . $po->id,
|
'id' => 'AP-' . $ap->id,
|
||||||
'date' => Carbon::parse($po->created_at)->timezone(config('app.timezone'))->toDateString(),
|
'date' => Carbon::parse($ap->paid_at)->timezone(config('app.timezone'))->toDateString(),
|
||||||
'source' => '採購單',
|
'source' => '應付帳款',
|
||||||
'category' => '進貨支出',
|
'category' => '進貨支出',
|
||||||
'item' => $po->vendor->name ?? '未知廠商',
|
'item' => $vendorName,
|
||||||
'reference' => $po->code,
|
'reference' => $ap->document_number,
|
||||||
'invoice_number' => $po->invoice_number,
|
'invoice_date' => $ap->invoice_date ? $ap->invoice_date->format('Y-m-d') : null,
|
||||||
'amount' => (float)$po->grand_total,
|
'invoice_number' => $ap->invoice_number,
|
||||||
|
'amount' => (float)$ap->total_amount,
|
||||||
|
'tax_amount' => (float)$ap->tax_amount,
|
||||||
|
'status' => $ap->status,
|
||||||
|
'payment_method' => $mappedPaymentMethod,
|
||||||
|
'payment_note' => $ap->payment_note,
|
||||||
|
'remarks' => $ap->remarks,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
// 2. 獲取公共事業費 (注意:目前資料表欄位為 transaction_date)
|
// 2. 獲取公共事業費 (已繳費)
|
||||||
$utilityFees = UtilityFee::whereBetween('transaction_date', [$start, $end])
|
$utilityFees = UtilityFee::where('payment_status', UtilityFee::STATUS_PAID)
|
||||||
|
->whereBetween('transaction_date', [$start, $end])
|
||||||
->get()
|
->get()
|
||||||
->map(function ($fee) {
|
->map(function ($fee) {
|
||||||
return [
|
return [
|
||||||
@@ -45,12 +70,18 @@ class FinanceService implements FinanceServiceInterface
|
|||||||
'category' => $fee->category,
|
'category' => $fee->category,
|
||||||
'item' => $fee->description ?: $fee->category,
|
'item' => $fee->description ?: $fee->category,
|
||||||
'reference' => '-',
|
'reference' => '-',
|
||||||
|
'invoice_date' => null,
|
||||||
'invoice_number' => $fee->invoice_number,
|
'invoice_number' => $fee->invoice_number,
|
||||||
'amount' => (float)$fee->amount,
|
'amount' => (float)$fee->amount,
|
||||||
|
'tax_amount' => 0.0,
|
||||||
|
'status' => $fee->payment_status,
|
||||||
|
'payment_method' => null,
|
||||||
|
'payment_note' => null,
|
||||||
|
'remarks' => $fee->description,
|
||||||
];
|
];
|
||||||
});
|
});
|
||||||
|
|
||||||
$allRecords = $purchaseOrders->concat($utilityFees)
|
$allRecords = $payableRecords->concat($utilityFees)
|
||||||
->sortByDesc('date')
|
->sortByDesc('date')
|
||||||
->values();
|
->values();
|
||||||
|
|
||||||
@@ -58,7 +89,7 @@ class FinanceService implements FinanceServiceInterface
|
|||||||
'records' => $allRecords,
|
'records' => $allRecords,
|
||||||
'summary' => [
|
'summary' => [
|
||||||
'total_amount' => $allRecords->sum('amount'),
|
'total_amount' => $allRecords->sum('amount'),
|
||||||
'purchase_total' => $purchaseOrders->sum('amount'),
|
'payable_total' => $payableRecords->sum('amount'),
|
||||||
'utility_total' => $utilityFees->sum('amount'),
|
'utility_total' => $utilityFees->sum('amount'),
|
||||||
'record_count' => $allRecords->count(),
|
'record_count' => $allRecords->count(),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -8,7 +8,7 @@ import {
|
|||||||
Calendar,
|
Calendar,
|
||||||
Filter,
|
Filter,
|
||||||
TrendingDown,
|
TrendingDown,
|
||||||
Package,
|
Wallet,
|
||||||
Pocket,
|
Pocket,
|
||||||
RotateCcw,
|
RotateCcw,
|
||||||
FileText
|
FileText
|
||||||
@@ -29,6 +29,7 @@ import Pagination from "@/Components/shared/Pagination";
|
|||||||
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
import { SearchableSelect } from "@/Components/ui/searchable-select";
|
||||||
import { Can } from "@/Components/Permission/Can";
|
import { Can } from "@/Components/Permission/Can";
|
||||||
import { Checkbox } from "@/Components/ui/checkbox";
|
import { Checkbox } from "@/Components/ui/checkbox";
|
||||||
|
import { StatusBadge } from "@/Components/shared/StatusBadge";
|
||||||
|
|
||||||
interface Record {
|
interface Record {
|
||||||
id: string;
|
id: string;
|
||||||
@@ -37,8 +38,14 @@ interface Record {
|
|||||||
category: string;
|
category: string;
|
||||||
item: string;
|
item: string;
|
||||||
reference: string;
|
reference: string;
|
||||||
invoice_number?: string;
|
invoice_date?: string | null;
|
||||||
|
invoice_number?: string | null;
|
||||||
amount: number | string;
|
amount: number | string;
|
||||||
|
tax_amount: number | string;
|
||||||
|
status?: string;
|
||||||
|
payment_method?: string | null;
|
||||||
|
payment_note?: string | null;
|
||||||
|
remarks?: string | null;
|
||||||
}
|
}
|
||||||
|
|
||||||
interface PageProps {
|
interface PageProps {
|
||||||
@@ -52,7 +59,7 @@ interface PageProps {
|
|||||||
};
|
};
|
||||||
summary: {
|
summary: {
|
||||||
total_amount: number;
|
total_amount: number;
|
||||||
purchase_total: number;
|
payable_total: number;
|
||||||
utility_total: number;
|
utility_total: number;
|
||||||
record_count: number;
|
record_count: number;
|
||||||
};
|
};
|
||||||
@@ -273,10 +280,10 @@ export default function AccountingReport({ records, summary, filters }: PageProp
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-orange-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
<div className="flex items-center gap-3 px-4 py-4 bg-white rounded-xl border-l-4 border-l-orange-500 shadow-sm border border-gray-100 transition-all hover:bg-gray-50">
|
||||||
<Package className="h-6 w-6 text-orange-500 shrink-0" />
|
<Wallet className="h-6 w-6 text-orange-500 shrink-0" />
|
||||||
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
<div className="flex flex-1 items-baseline justify-between gap-2 min-w-0">
|
||||||
<span className="text-sm text-gray-500 font-medium shrink-0">採購支出</span>
|
<span className="text-sm text-gray-500 font-medium shrink-0">應付帳款</span>
|
||||||
<span className="text-xl font-bold text-gray-900 truncate">$ {Number(summary.purchase_total).toLocaleString()}</span>
|
<span className="text-xl font-bold text-gray-900 truncate">$ {Number(summary.payable_total).toLocaleString()}</span>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -305,13 +312,16 @@ export default function AccountingReport({ records, summary, filters }: PageProp
|
|||||||
<TableHead className="w-[120px] text-center">來源</TableHead>
|
<TableHead className="w-[120px] text-center">來源</TableHead>
|
||||||
<TableHead className="w-[140px] text-center">類別</TableHead>
|
<TableHead className="w-[140px] text-center">類別</TableHead>
|
||||||
<TableHead className="px-6">項目詳細</TableHead>
|
<TableHead className="px-6">項目詳細</TableHead>
|
||||||
<TableHead className="w-[180px] text-right px-6">金額</TableHead>
|
<TableHead className="w-[160px] text-center">付款方式 / 備註</TableHead>
|
||||||
|
<TableHead className="w-[100px] text-center">狀態</TableHead>
|
||||||
|
<TableHead className="w-[120px] text-right">稅額</TableHead>
|
||||||
|
<TableHead className="w-[150px] text-right px-6">總金額</TableHead>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
</TableHeader>
|
</TableHeader>
|
||||||
<TableBody>
|
<TableBody>
|
||||||
{records.data.length === 0 ? (
|
{records.data.length === 0 ? (
|
||||||
<TableRow>
|
<TableRow>
|
||||||
<TableCell colSpan={6}>
|
<TableCell colSpan={7}>
|
||||||
<div className="flex flex-col items-center justify-center space-y-2 py-8 text-gray-400">
|
<div className="flex flex-col items-center justify-center space-y-2 py-8 text-gray-400">
|
||||||
<FileText className="h-10 w-10 opacity-20" />
|
<FileText className="h-10 w-10 opacity-20" />
|
||||||
<p>此日期區間內無支出紀錄</p>
|
<p>此日期區間內無支出紀錄</p>
|
||||||
@@ -333,7 +343,7 @@ export default function AccountingReport({ records, summary, filters }: PageProp
|
|||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell className="text-center">
|
<TableCell className="text-center">
|
||||||
<Badge variant="secondary" className={
|
<Badge variant="secondary" className={
|
||||||
record.source === '採購單'
|
record.source === '應付帳款'
|
||||||
? 'bg-orange-50 text-orange-700 border-orange-100'
|
? 'bg-orange-50 text-orange-700 border-orange-100'
|
||||||
: 'bg-blue-50 text-blue-700 border-blue-100'
|
: 'bg-blue-50 text-blue-700 border-blue-100'
|
||||||
}>
|
}>
|
||||||
@@ -348,11 +358,47 @@ export default function AccountingReport({ records, summary, filters }: PageProp
|
|||||||
<TableCell>
|
<TableCell>
|
||||||
<div className="flex flex-col">
|
<div className="flex flex-col">
|
||||||
<span className="font-medium text-gray-900">{record.item}</span>
|
<span className="font-medium text-gray-900">{record.item}</span>
|
||||||
{record.invoice_number && (
|
{(record.invoice_number || record.invoice_date) && (
|
||||||
<span className="text-xs text-gray-400">發票:{record.invoice_number}</span>
|
<span className="text-xs text-gray-400 mt-0.5">
|
||||||
|
發票:{record.invoice_number || '-'}
|
||||||
|
{record.invoice_date && ` (${record.invoice_date})`}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
{record.remarks && (
|
||||||
|
<span className="text-xs text-gray-500 mt-0.5 truncate max-w-[200px]" title={record.remarks}>
|
||||||
|
備註:{record.remarks}
|
||||||
|
</span>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
<div className="flex flex-col items-center">
|
||||||
|
<span className="text-sm text-gray-700">{record.payment_method || '-'}</span>
|
||||||
|
{record.payment_note && (
|
||||||
|
<span className="text-xs text-gray-400 truncate max-w-[120px]" title={record.payment_note}>
|
||||||
|
{record.payment_note}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-center">
|
||||||
|
{record.status === 'paid' ? (
|
||||||
|
<StatusBadge variant="success">已付款</StatusBadge>
|
||||||
|
) : record.status === 'pending' ? (
|
||||||
|
<StatusBadge variant="warning">待付款</StatusBadge>
|
||||||
|
) : record.status === 'overdue' ? (
|
||||||
|
<StatusBadge variant="destructive">已逾期</StatusBadge>
|
||||||
|
) : record.status === 'draft' ? (
|
||||||
|
<StatusBadge variant="neutral">草稿</StatusBadge>
|
||||||
|
) : record.status === 'approved' ? (
|
||||||
|
<StatusBadge variant="info">已核准</StatusBadge>
|
||||||
|
) : (
|
||||||
|
<StatusBadge variant="neutral">{record.status || '-'}</StatusBadge>
|
||||||
|
)}
|
||||||
|
</TableCell>
|
||||||
|
<TableCell className="text-right text-gray-600">
|
||||||
|
{record.tax_amount ? `$ ${Number(record.tax_amount).toLocaleString()}` : '-'}
|
||||||
|
</TableCell>
|
||||||
<TableCell className="text-right font-bold text-gray-900 px-4">
|
<TableCell className="text-right font-bold text-gray-900 px-4">
|
||||||
$ {Number(record.amount).toLocaleString()}
|
$ {Number(record.amount).toLocaleString()}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
|
|||||||
Reference in New Issue
Block a user