From d3e455fc82659e624b8b76e1e0a90a60991d24d6 Mon Sep 17 00:00:00 2001 From: sky121113 Date: Tue, 16 Jun 2026 09:57:37 +0800 Subject: [PATCH] =?UTF-8?q?[FIX]=20=E4=BF=AE=E6=AD=A3=E8=A8=82=E5=96=AE?= =?UTF-8?q?=E8=A9=B3=E6=83=85=E7=99=BC=E7=A5=A8=E7=8B=80=E6=85=8B=E5=8B=BE?= =?UTF-8?q?=E7=A8=BD=E8=88=87=E5=88=97=E5=8D=B0=E4=BD=9C=E5=BB=A2=E6=8C=89?= =?UTF-8?q?=E9=88=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 訂單詳情發票徽章改為動態讀取 invoice->status,依 pending/issued/failed/void 顯示對應狀態與顏色,不再寫死「有效」。 2. 移除不存在的 invoice_time 欄位引用,開立時間改以 invoice_date 格式化顯示。 3. 作廢按鈕接上既有 admin.sales.invoices.void 路由,僅於 issued 狀態顯示並加上確認對話框。 4. 新增列印發票功能:EcpayInvoiceService::invoicePrint() 呼叫綠界 B2CInvoice/InvoicePrint 取得列印頁網址,SalesController::printInvoice() 加守衛後 302 導向,並新增 admin.sales.invoices.print 路由,按鈕以新分頁開啟。 5. 補齊三語系 key(Failed to get invoice print URL、Only issued invoices can be printed),完成 zh_TW/en/ja 對齊排序。 Co-Authored-By: Claude Opus 4.8 (1M context) --- .../Controllers/Admin/SalesController.php | 27 +++++++++++++ app/Services/Invoice/EcpayInvoiceService.php | 20 ++++++++++ lang/en.json | 2 + lang/ja.json | 2 + lang/zh_TW.json | 2 + .../partials/order-detail-panel.blade.php | 40 ++++++++++++++----- routes/web.php | 3 +- 7 files changed, 85 insertions(+), 11 deletions(-) diff --git a/app/Http/Controllers/Admin/SalesController.php b/app/Http/Controllers/Admin/SalesController.php index a157214..8b68215 100644 --- a/app/Http/Controllers/Admin/SalesController.php +++ b/app/Http/Controllers/Admin/SalesController.php @@ -1145,6 +1145,33 @@ class SalesController extends Controller // ===================== 電子發票對帳/補開/作廢 ===================== + /** + * 列印發票:向綠界取得列印頁網址(InvoicePrint)後 302 導向。 + * 該網址自綠界回應起 1 小時內有效,前端以 target="_blank" 於新分頁開啟。 + */ + public function printInvoice(Invoice $invoice, \App\Services\Invoice\EcpayInvoiceService $ecpay) + { + // 僅已開立且有發票號者可列印 + if ($invoice->status !== Invoice::STATUS_ISSUED || empty($invoice->invoice_no)) { + return back()->with('error', __('Only issued invoices can be printed')); + } + $machine = $invoice->machine; + if (!$machine || !$ecpay->configForMachine($machine)) { + return back()->with('error', __('This machine has no ECPay invoice settings')); + } + $date = $invoice->invoice_date + ? Carbon::parse($invoice->invoice_date)->format('Y-m-d') + : ''; + + $result = $ecpay->invoicePrint($machine, $invoice->invoice_no, $date); + $url = $result['InvoiceHtml'] ?? ''; + if ($result === null || (string) ($result['RtnCode'] ?? '') !== '1' || empty($url)) { + return back()->with('error', __('Failed to get invoice print URL')); + } + + return redirect()->away($url); + } + /** * 手動向綠界查詢單張發票真實狀態(GetIssue)。 * pending 掉包時用此確認:已開→補登 issued;查無→標 failed 待補開。 diff --git a/app/Services/Invoice/EcpayInvoiceService.php b/app/Services/Invoice/EcpayInvoiceService.php index 369bb0e..d372026 100644 --- a/app/Services/Invoice/EcpayInvoiceService.php +++ b/app/Services/Invoice/EcpayInvoiceService.php @@ -123,6 +123,26 @@ class EcpayInvoiceService return $this->post('/B2CInvoice/Issue', $cfg, $invoicePayload); } + /** + * 取得發票列印頁網址(InvoicePrint)。 + * 綠界回傳 Data.InvoiceHtml 為可列印頁 URL(自呼叫起 1 小時內有效)。 + * 回傳 decode 後陣列(含 RtnCode/InvoiceHtml)或 null(金鑰缺/連線失敗)。 + */ + public function invoicePrint(Machine $machine, string $invoiceNo, string $invoiceDate): ?array + { + $cfg = $this->configForMachine($machine); + if (!$cfg) { + return null; + } + return $this->post('/B2CInvoice/InvoicePrint', $cfg, [ + 'MerchantID' => $cfg['merchant_id'], + 'InvoiceNo' => $invoiceNo, + 'InvoiceDate' => $invoiceDate, + 'PrintStyle' => 1, + 'IsShowingDetail' => 1, + ]); + } + /** 作廢(Invalid)。 */ public function invalid(Machine $machine, string $invoiceNo, string $invoiceDate, string $reason = '作廢'): ?array { diff --git a/lang/en.json b/lang/en.json index b3730b6..24fd147 100644 --- a/lang/en.json +++ b/lang/en.json @@ -737,6 +737,7 @@ "External URL": "External URL", "Failed": "Failed", "Failed to fetch machine data.": "Failed to fetch machine data.", + "Failed to get invoice print URL": "Failed to get invoice print URL", "Failed to load permissions": "Failed to load permissions", "Failed to load preview": "Failed to load preview", "Failed to load tab content": "Failed to load tab content", @@ -1382,6 +1383,7 @@ "Online Status": "Online Status", "Only .xlsx files are supported (Max 5MB)": "Only .xlsx files are supported (Max 5MB)", "Only .xlsx, .xls, .csv files are supported (Max 5MB)": "Only .xlsx, .xls, .csv files are supported (Max 5MB)", + "Only issued invoices can be printed": "Only issued invoices can be printed", "Only issued invoices can be voided": "Only issued invoices can be voided", "Only machines under the same company can be cloned for security.": "Only machines under the same company can be cloned for security.", "Only pending/failed invoices can be queried": "Only pending/failed invoices can be queried", diff --git a/lang/ja.json b/lang/ja.json index d948b65..9ba9716 100644 --- a/lang/ja.json +++ b/lang/ja.json @@ -737,6 +737,7 @@ "External URL": "External URL", "Failed": "失敗", "Failed to fetch machine data.": "機器データの取得に失敗しました。", + "Failed to get invoice print URL": "領収書印刷URLの取得に失敗しました", "Failed to load permissions": "権限の読み込みに失敗しました", "Failed to load preview": "プレビューの読み込みに失敗しました", "Failed to load tab content": "タブ内容の読み込みに失敗しました", @@ -1382,6 +1383,7 @@ "Online Status": "オンラインステータス", "Only .xlsx files are supported (Max 5MB)": "Only .xlsx files are supported (Max 5MB)", "Only .xlsx, .xls, .csv files are supported (Max 5MB)": ".xlsx, .xls, .csv ファイルのみサポートされています (最大 5MB)", + "Only issued invoices can be printed": "発行済みの領収書のみ印刷できます", "Only issued invoices can be voided": "Only issued invoices can be voided", "Only machines under the same company can be cloned for security.": "セキュリティのため、同じ会社の機器設定のみコピーできます。", "Only pending/failed invoices can be queried": "Only pending/failed invoices can be queried", diff --git a/lang/zh_TW.json b/lang/zh_TW.json index 0d515e7..ef72f1b 100644 --- a/lang/zh_TW.json +++ b/lang/zh_TW.json @@ -737,6 +737,7 @@ "External URL": "外連 URL", "Failed": "失敗", "Failed to fetch machine data.": "無法取得機台資料。", + "Failed to get invoice print URL": "取得發票列印網址失敗", "Failed to load permissions": "載入權限失敗", "Failed to load preview": "載入預覽失敗", "Failed to load tab content": "載入分頁內容失敗", @@ -1382,6 +1383,7 @@ "Online Status": "在線狀態", "Only .xlsx files are supported (Max 5MB)": "僅支援 .xlsx 檔案 (最大 5MB)", "Only .xlsx, .xls, .csv files are supported (Max 5MB)": "僅支援 .xlsx, .xls, .csv 檔案 (最大 5MB)", + "Only issued invoices can be printed": "僅已開立的發票可列印", "Only issued invoices can be voided": "僅已開立的發票可作廢", "Only machines under the same company can be cloned for security.": "基於安全性,僅限複製同公司旗下的機台設定。", "Only pending/failed invoices can be queried": "僅待開立/失敗的發票可查詢", diff --git a/resources/views/admin/sales/partials/order-detail-panel.blade.php b/resources/views/admin/sales/partials/order-detail-panel.blade.php index f625654..085fb7b 100644 --- a/resources/views/admin/sales/partials/order-detail-panel.blade.php +++ b/resources/views/admin/sales/partials/order-detail-panel.blade.php @@ -282,28 +282,48 @@ + @php + // 以發票狀態機顯示(pending/issued/failed/void);舊資料無 status 時以發票號回推 + $invStatus = $order->invoice->status ?: (empty($order->invoice->invoice_no) ? 'failed' : 'issued'); + $invStatusMap = [ + 'pending' => ['label' => __('Pending'), 'color' => 'amber'], + 'issued' => ['label' => __('Issued'), 'color' => 'emerald'], + 'failed' => ['label' => __('Failed'), 'color' => 'rose'], + 'void' => ['label' => __('Void'), 'color' => 'slate'], + ]; + $invBadge = $invStatusMap[$invStatus] ?? ['label' => $invStatus, 'color' => 'slate']; + @endphp
-

{{ $order->invoice->invoice_no }}

- +

{{ $order->invoice->invoice_no ?: '---' }}

+

{{ __('Issued At') }}

- {{ $order->invoice->invoice_date }} {{ $order->invoice->invoice_time }} + {{ optional($order->invoice->invoice_date)->format('Y-m-d') ?: '---' }}

- + + @if($invStatus === 'issued')
- - +
+ @csrf + +
+
+ @csrf + +
+ @endif
diff --git a/routes/web.php b/routes/web.php index 8b2cd2b..1b708cf 100644 --- a/routes/web.php +++ b/routes/web.php @@ -142,7 +142,8 @@ Route::middleware(['auth', 'auth.session', 'verified', 'tenant.access'])->prefix Route::prefix('sales')->name('sales.')->group(function () { Route::get('/', [App\Http\Controllers\Admin\SalesController::class, 'index'])->name('index'); - // 電子發票對帳/補開/作廢 + // 電子發票對帳/補開/作廢/列印 + Route::post('/invoices/{invoice}/print', [App\Http\Controllers\Admin\SalesController::class, 'printInvoice'])->name('invoices.print'); Route::post('/invoices/{invoice}/reconcile', [App\Http\Controllers\Admin\SalesController::class, 'reconcileInvoice'])->name('invoices.reconcile'); Route::post('/invoices/{invoice}/reissue', [App\Http\Controllers\Admin\SalesController::class, 'reissueInvoice'])->name('invoices.reissue'); Route::post('/invoices/{invoice}/void', [App\Http\Controllers\Admin\SalesController::class, 'voidInvoice'])->name('invoices.void');