mirror of
https://github.com/henry4682/finance_app.git
synced 2026-07-16 00:10:00 +00:00
buid new app
This commit is contained in:
258
app/Http/Controllers/InvoiceImportController.php
Normal file
258
app/Http/Controllers/InvoiceImportController.php
Normal file
@@ -0,0 +1,258 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Expense;
|
||||
use App\Models\Category;
|
||||
use App\Models\CategoryRule;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class InvoiceImportController extends Controller
|
||||
{
|
||||
// ── 自動偵測格式 ──
|
||||
private function detectFormat(array $firstRow): string
|
||||
{
|
||||
// AndroMoney 第一行是 "Windows Excel", "理財幫手AndroMoney", 日期
|
||||
if (isset($firstRow[1]) && str_contains($firstRow[1], 'AndroMoney')) {
|
||||
return 'andromoney';
|
||||
}
|
||||
return 'invoice';
|
||||
}
|
||||
|
||||
// ── 取得或建立分類 ──
|
||||
private function findOrCreateCategory(string $name, ?int $parentId = null): int
|
||||
{
|
||||
$category = Category::firstOrCreate(
|
||||
[
|
||||
'user_id' => auth()->id(),
|
||||
'name' => $name,
|
||||
'parent_id' => $parentId,
|
||||
],
|
||||
[
|
||||
'color' => $this->randomColor(),
|
||||
]
|
||||
);
|
||||
return $category->id;
|
||||
}
|
||||
|
||||
private function randomColor(): string
|
||||
{
|
||||
$colors = [
|
||||
'#EF4444',
|
||||
'#F59E0B',
|
||||
'#10B981',
|
||||
'#3B82F6',
|
||||
'#8B5CF6',
|
||||
'#EC4899',
|
||||
'#14B8A6',
|
||||
'#F97316',
|
||||
];
|
||||
return $colors[array_rand($colors)];
|
||||
}
|
||||
|
||||
// ── 發票格式匯入 ──
|
||||
private function importInvoice($handle, $rules): array
|
||||
{
|
||||
$imported = 0;
|
||||
$updated = 0;
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 8) continue;
|
||||
|
||||
$invoiceNumber = trim($row[2] ?? '');
|
||||
$date = trim($row[1] ?? '');
|
||||
$amount = trim($row[3] ?? '0');
|
||||
$sellerName = trim($row[7] ?? '');
|
||||
$itemName = trim($row[13] ?? '');
|
||||
|
||||
if (empty($invoiceNumber)) continue;
|
||||
if ((float)$amount < 0) {
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$formattedDate = null;
|
||||
if (strlen($date) === 8) {
|
||||
$formattedDate = substr($date, 0, 4) . '-'
|
||||
. substr($date, 4, 2) . '-'
|
||||
. substr($date, 6, 2);
|
||||
}
|
||||
|
||||
$exists = Expense::where('user_id', auth()->id())
|
||||
->where('invoice_number', $invoiceNumber)
|
||||
->exists();
|
||||
|
||||
if ($exists) {
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
$categoryId = $this->guessCategory($sellerName, $rules);
|
||||
|
||||
$expense = Expense::updateOrCreate(
|
||||
[
|
||||
'user_id' => auth()->id(),
|
||||
'external_id' => $invoiceNumber,
|
||||
],
|
||||
[
|
||||
'type' => 'expense',
|
||||
'amount' => (float)$amount,
|
||||
'date' => $formattedDate,
|
||||
'seller_name' => $sellerName,
|
||||
'item_name' => $itemName,
|
||||
'invoice_number' => $invoiceNumber,
|
||||
'note' => $sellerName,
|
||||
'category_id' => $categoryId,
|
||||
]
|
||||
);
|
||||
if ($expense->wasRecentlyCreated) {
|
||||
$imported++;
|
||||
} else {
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
return [$imported, $updated];
|
||||
}
|
||||
|
||||
// ── AndroMoney 格式匯入 ──
|
||||
private function importAndroMoney($handle): array
|
||||
{
|
||||
$imported = 0;
|
||||
$updated = 0;
|
||||
|
||||
// 跳過標題列(第二行)
|
||||
fgetcsv($handle);
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
// 轉換 Big5 → UTF-8
|
||||
$row = array_map(fn($val) => mb_convert_encoding($val, 'UTF-8', 'Big5'), $row);
|
||||
|
||||
if (count($row) < 6) continue;
|
||||
// 欄位對應
|
||||
// A=0:id, B=1:幣別, C=2:金額, D=3:分類, E=4:子分類
|
||||
// F=5:日期, G=6:付款, H=7:收款, I=8:備註, M=12:商家, N=13:uid
|
||||
$amount = trim($row[2] ?? '0');
|
||||
$catName = trim($row[3] ?? '');
|
||||
$subCatName = trim($row[4] ?? '');
|
||||
$date = trim($row[5] ?? '');
|
||||
$payOut = trim($row[6] ?? '');
|
||||
$payIn = trim($row[7] ?? '');
|
||||
$note = trim($row[8] ?? '');
|
||||
$sellerName = trim($row[13] ?? '');
|
||||
|
||||
// 過濾掉看起來像 uid 的值(32碼以上的hex字串)
|
||||
if (preg_match('/^[0-9a-f]{20,}$/i', $note)) {
|
||||
$note = '';
|
||||
}
|
||||
|
||||
if (empty($amount) || empty($date)) continue;
|
||||
if ((float)$amount == 0) continue;
|
||||
|
||||
// 日期格式轉換 20260106 → 2026-01-06
|
||||
$formattedDate = null;
|
||||
if (strlen($date) === 8 && is_numeric($date)) {
|
||||
$formattedDate = substr($date, 0, 4) . '-'
|
||||
. substr($date, 4, 2) . '-'
|
||||
. substr($date, 6, 2);
|
||||
}
|
||||
|
||||
// 日期無效就跳過
|
||||
if (!$formattedDate) {
|
||||
$updated++;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 判斷收入/支出
|
||||
// 付款欄有值 = 支出,收款欄有值 = 收入
|
||||
$type = !empty($payIn) ? 'income' : 'expense';
|
||||
|
||||
// 取得或建立主分類
|
||||
$categoryId = null;
|
||||
if (!empty($catName)) {
|
||||
$categoryId = $this->findOrCreateCategory($catName);
|
||||
}
|
||||
|
||||
// 取得或建立子分類(掛在主分類下)
|
||||
$subcategoryId = null;
|
||||
if (!empty($subCatName) && $categoryId) {
|
||||
$subcategoryId = $this->findOrCreateCategory($subCatName, $categoryId);
|
||||
}
|
||||
|
||||
// uid 是 row[12]
|
||||
$uid = trim($row[12] ?? '');
|
||||
|
||||
$expense = Expense::updateOrCreate(
|
||||
[
|
||||
'user_id' => auth()->id(),
|
||||
'external_id' => $uid ?: null,
|
||||
],
|
||||
[
|
||||
'type' => $type,
|
||||
'amount' => abs((float)$amount),
|
||||
'date' => $formattedDate,
|
||||
'category_id' => $categoryId,
|
||||
'subcategory_id' => $subcategoryId,
|
||||
'note' => $note ?: $sellerName,
|
||||
'seller_name' => $sellerName,
|
||||
]
|
||||
);
|
||||
|
||||
if ($expense->wasRecentlyCreated) {
|
||||
$imported++;
|
||||
} else {
|
||||
$updated++;
|
||||
}
|
||||
}
|
||||
|
||||
return [$imported, $updated];
|
||||
}
|
||||
|
||||
// ── 自動分類(發票用)──
|
||||
private function guessCategory($sellerName, $rules)
|
||||
{
|
||||
foreach ($rules as $rule) {
|
||||
if (str_contains($sellerName, $rule->keyword)) {
|
||||
return $rule->category_id;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
public function index()
|
||||
{
|
||||
return inertia('Expenses/Import');
|
||||
}
|
||||
|
||||
public function store(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'file' => 'required|file|mimes:csv,txt',
|
||||
]);
|
||||
|
||||
$file = $request->file('file');
|
||||
$handle = fopen($file->getPathname(), 'r');
|
||||
|
||||
// 讀第一行偵測格式
|
||||
$firstRow = fgetcsv($handle);
|
||||
$format = $this->detectFormat($firstRow);
|
||||
|
||||
$rules = CategoryRule::with('category')
|
||||
->where('user_id', auth()->id())
|
||||
->get();
|
||||
|
||||
if ($format === 'andromoney') {
|
||||
[$imported, $updated] = $this->importAndroMoney($handle);
|
||||
} else {
|
||||
[$imported, $updated] = $this->importInvoice($handle, $rules);
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$formatName = $format === 'andromoney' ? 'AndroMoney' : '發票';
|
||||
|
||||
return redirect()->route('expenses.index')->with([
|
||||
'message' => "【{$formatName}】匯入完成!新增 {$imported} 筆,更新 {$updated} 筆",
|
||||
]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user