[FIX] 修正訂單詳情發票狀態勾稽與列印作廢按鈕

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) <noreply@anthropic.com>
This commit is contained in:
sky121113 2026-06-16 09:57:37 +08:00
parent 07e8a6887c
commit d3e455fc82
7 changed files with 85 additions and 11 deletions

View File

@ -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 待補開。

View File

@ -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
{

View File

@ -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",

View File

@ -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",

View File

@ -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": "僅待開立/失敗的發票可查詢",

View File

@ -282,28 +282,48 @@
</svg>
</div>
@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
<div class="relative flex flex-col sm:flex-row sm:items-center justify-between gap-6">
<div class="space-y-3">
<div class="flex items-center gap-3">
<p class="text-xl font-black text-slate-800 dark:text-white font-mono tracking-tighter">{{ $order->invoice->invoice_no }}</p>
<x-status-badge status="active" :label="__('Valid')" size="xs" />
<p class="text-xl font-black text-slate-800 dark:text-white font-mono tracking-tighter">{{ $order->invoice->invoice_no ?: '---' }}</p>
<x-status-badge :color="$invBadge['color']" :label="$invBadge['label']" size="xs" />
</div>
<div class="flex flex-col gap-1">
<p class="text-[10px] font-black text-slate-400 dark:text-slate-500 uppercase tracking-widest">{{ __('Issued At') }}</p>
<p class="text-sm font-bold text-slate-600 dark:text-slate-400 font-mono tracking-tight">
{{ $order->invoice->invoice_date }} {{ $order->invoice->invoice_time }}
{{ optional($order->invoice->invoice_date)->format('Y-m-d') ?: '---' }}
</p>
</div>
</div>
@if($invStatus === 'issued')
<div class="flex items-center gap-3">
<button class="flex-1 sm:flex-none px-6 py-3 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold text-xs border border-slate-200 dark:border-slate-700 hover:bg-indigo-500 hover:text-white hover:border-indigo-500 transition-all duration-300 shadow-sm">
{{ __('Print Invoice') }}
</button>
<button class="flex-1 sm:flex-none px-6 py-3 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold text-xs border border-slate-200 dark:border-slate-700 hover:bg-rose-500 hover:text-white hover:border-rose-500 transition-all duration-300 shadow-sm">
{{ __('Void Invoice') }}
</button>
<form method="POST" action="{{ route('admin.sales.invoices.print', $order->invoice) }}" target="_blank">
@csrf
<button type="submit" class="flex-1 sm:flex-none px-6 py-3 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold text-xs border border-slate-200 dark:border-slate-700 hover:bg-indigo-500 hover:text-white hover:border-indigo-500 transition-all duration-300 shadow-sm">
{{ __('Print Invoice') }}
</button>
</form>
<form method="POST" action="{{ route('admin.sales.invoices.void', $order->invoice) }}"
onsubmit="return confirm('{{ __('Void this invoice?') }}')">
@csrf
<button type="submit" class="flex-1 sm:flex-none px-6 py-3 rounded-xl bg-white dark:bg-slate-800 text-slate-600 dark:text-slate-300 font-bold text-xs border border-slate-200 dark:border-slate-700 hover:bg-rose-500 hover:text-white hover:border-rose-500 transition-all duration-300 shadow-sm">
{{ __('Void Invoice') }}
</button>
</form>
</div>
@endif
</div>
</div>
</section>

View File

@ -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');