diff --git a/app/Http/Controllers/Business/AccessoryReshipDeductions.php b/app/Http/Controllers/Business/AccessoryReshipDeductions.php index a9b8a3b4..95364911 100644 --- a/app/Http/Controllers/Business/AccessoryReshipDeductions.php +++ b/app/Http/Controllers/Business/AccessoryReshipDeductions.php @@ -35,7 +35,7 @@ class AccessoryReshipDeductions extends Controller fwrite($file, "\xEF\xBB\xBF"); fputcsv($file, ['交易单号','补发快递单号','补发快递','补发时间','原始条形码','处理后条形码','补发运费','匹配状态','首发快递公司(候选)','首发快递单号(候选)','首发所属月份(候选)','扣款比例(%)','扣款金额','说明']); foreach ($data->rows($filters) as $row) { - if ($filters['match_status'] !== 'all' && $row['status'] !== $filters['match_status']) continue; + if (!AccessoryReshipPageData::inCategory($row, $filters['match_status'])) continue; $values = [$row['trade_no'], $row['express_no'], ($row['express_name1'] ?: ($row['express_name2'] ?? '')), $row['ship_time'], $row['barcode'], $row['normalized_barcode'], $row['freight'], $row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? '已匹配待核对' : '未匹配不扣款'), implode(' / ', array_column($row['matches'], 'courier')), implode(' / ', array_column($row['matches'], 'express_no')), implode(' / ', array_column($row['matches'], 'month')), $row['deduction_rate_basis_points'] === null ? '' : number_format($row['deduction_rate_basis_points'] / 100, 2, '.', '') . '%', $row['deduction_cents'] === null ? '' : number_format($row['deduction_cents'] / 100, 2, '.', ''), $row['deduction_note']]; fputcsv($file, array_map(static function ($value) { $value = (string) $value; diff --git a/app/Http/Controllers/Business/CourierStockQuoteEntries.php b/app/Http/Controllers/Business/CourierStockQuoteEntries.php index beb94014..a2c58aca 100644 --- a/app/Http/Controllers/Business/CourierStockQuoteEntries.php +++ b/app/Http/Controllers/Business/CourierStockQuoteEntries.php @@ -166,7 +166,7 @@ class CourierStockQuoteEntries extends Controller $headers = ['Cache-Control' => 'private, no-store', 'X-Content-Type-Options' => 'nosniff']; if ($preview) { $mime = $attachment['mime_type'] ?? $disk->mimeType($attachment['path']); - abort_unless(in_array($mime, ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true), 415, '此格式请下载原文件查看。'); + abort_unless(in_array($mime, ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true), 415, '此格式请下载原文件查看。'); return $disk->response($attachment['path'], $attachment['name'], [...$headers, 'Content-Type' => $mime]); } diff --git a/app/Models/Business/CourierQuoteVersion.php b/app/Models/Business/CourierQuoteVersion.php index 74916822..05bd7336 100644 --- a/app/Models/Business/CourierQuoteVersion.php +++ b/app/Models/Business/CourierQuoteVersion.php @@ -61,6 +61,20 @@ class CourierQuoteVersion extends Model return $this->status === self::STATUS_VOIDED; } + /** 存量报价审核后按执行日期生效,不以补签或历史签署资料为前置条件。 */ + public function isApprovedStockQuote(): bool + { + return $this->status === self::STATUS_APPROVED + && $this->change?->status === CourierQuoteChange::STATUS_APPROVED + && $this->change->isStockQuote(); + } + + public function signatureLabel(): string + { + return $this->isApprovedStockQuote() && !in_array($this->signature_status, ['历史签署资料已归档', '已签署并归档'], true) + ? '存量报价,审核生效(无需签章)' : (string) $this->signature_status; + } + public function change(): BelongsTo { return $this->belongsTo(CourierQuoteChange::class, 'source_change_id'); diff --git a/app/Support/CourierQuoteChangeService.php b/app/Support/CourierQuoteChangeService.php index cd9f578b..7f6d64e1 100644 --- a/app/Support/CourierQuoteChangeService.php +++ b/app/Support/CourierQuoteChangeService.php @@ -142,7 +142,7 @@ final class CourierQuoteChangeService $hasHistoricalFiles = $change->isStockQuote() && collect(CourierStockQuoteEntry::query()->findOrFail(data_get($change->quote_payload, 'stock_quote_entry_id'))->files)->contains('kind', 'signed'); if ($change->isStockQuote() && $historicalDocumentsConfirmed && ! $hasHistoricalFiles) { - throw ValidationException::withMessages(['stock_signature_verified' => '尚未上传历史签署资料,请补充资料或保留待确认/待补签状态。']); + throw ValidationException::withMessages(['stock_signature_verified' => '尚未上传历史签署资料,请补充资料或取消历史资料核验;不影响存量报价审核生效。']); } $historicalSigned = $hasHistoricalFiles && $historicalDocumentsConfirmed; $values = $source['values']; @@ -182,7 +182,7 @@ final class CourierQuoteChangeService 'values_json' => json_encode($values, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'formulas_json' => json_encode($formulas, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR), 'status' => 'approved', - 'signature_status' => $change->isStockQuote() ? ($historicalSigned ? '历史签署资料已归档' : '待确认/待补签') : '未签署', + 'signature_status' => $change->isStockQuote() ? ($historicalSigned ? '历史签署资料已归档' : '存量报价,审核生效(无需签章)') : '未签署', 'created_by' => $change->created_by, 'approved_by' => $userId, ]); diff --git a/app/Support/CourierRequoteService.php b/app/Support/CourierRequoteService.php index c2ff6478..5bc9dbbb 100644 --- a/app/Support/CourierRequoteService.php +++ b/app/Support/CourierRequoteService.php @@ -370,7 +370,7 @@ final class CourierRequoteService { return $versions ->filter(static fn (CourierQuoteVersion $version): bool => ! $version->isVoided() - && self::isSignedVersion($version) + && ($version->isApprovedStockQuote() || self::isSignedVersion($version)) && ($includeTemporary || ! $version->isTemporary()) && $version->coversDate($date)) ->sortBy([ diff --git a/app/Support/QuoteManagementSnapshot.php b/app/Support/QuoteManagementSnapshot.php index 9a1090cb..cbd3353e 100644 --- a/app/Support/QuoteManagementSnapshot.php +++ b/app/Support/QuoteManagementSnapshot.php @@ -165,11 +165,12 @@ final class QuoteManagementSnapshot 'column_count' => $columnCount, 'formula_count' => $this->formulaCount($formulas), 'status' => $this->versionLifecycleStatus($version), - 'signature_status' => $version->signature_status, + 'signature_status' => $version->signatureLabel(), 'values' => $values, 'formulas' => $formulas, 'source_change_id' => $version->source_change_id, 'signed_files' => $this->signedFiles($version), + 'stock_originals' => $this->stockOriginals($version), 'signed_at' => $version->signature?->completed_at?->format('Y-m-d H:i') ?? $version->signature?->archived_at?->format('Y-m-d H:i'), ]; @@ -205,6 +206,24 @@ final class QuoteManagementSnapshot return $files; } + private function stockOriginals(CourierQuoteVersion $version): array + { + if (!$version->change?->isStockQuote() || !($entry = $version->change->stockEntry)) return []; + $files = []; + foreach ($entry->files as $index => $file) { + if (($file['kind'] ?? '') !== 'signed') continue; + // 早期手动上传未记录 MIME;入口按扩展名提供,响应仍按实际 MIME 白名单校验。 + $previewable = isset($file['mime_type']) + ? in_array($file['mime_type'], ['application/pdf', 'image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'], true) + : in_array(strtolower(pathinfo($file['name'], PATHINFO_EXTENSION)), ['pdf', 'jpg', 'jpeg', 'png', 'gif', 'webp', 'bmp'], true); + $files[] = [ + 'name' => $file['name'], 'previewable' => $previewable, + 'url' => route('business.courier-stock-quotes.download', [$entry, $index, 'preview' => $previewable ? 1 : 0]), + ]; + } + return $files; + } + private function decodeMatrix(?string $json): array { if ($json === null || trim($json) === '') { @@ -224,7 +243,7 @@ final class QuoteManagementSnapshot return '已作废'; } - if (! $this->isSigned($version)) { + if (! $this->isExecutable($version)) { return '待生效(待签章)'; } @@ -235,10 +254,10 @@ final class QuoteManagementSnapshot return $version->isTemporary() ? '生效中(临时)' : '生效中'; } - /** 回签单已归档(以签署记录为准,版本上的文字状态可能滞后)。 */ - private function isSigned(CourierQuoteVersion $version): bool + /** 存量报价审核生效;其他报价仍要求回签归档。 */ + private function isExecutable(CourierQuoteVersion $version): bool { - return in_array($version->signature_status, ['已签署并归档', '历史签署资料已归档'], true) + return $version->isApprovedStockQuote() || in_array($version->signature_status, ['已签署并归档', '历史签署资料已归档'], true) || $version->signature?->status === \App\Models\Business\CourierQuoteSignature::STATUS_ARCHIVED; } @@ -261,7 +280,7 @@ final class QuoteManagementSnapshot : 'name:' . $version->company_name)) as $group) { $current = CourierRequoteService::resolveAt($group, $today); $base = CourierRequoteService::resolveAt($group, $today, false); - $started = $group->filter(fn (CourierQuoteVersion $version): bool => ! $version->isVoided() && $this->isSigned($version) + $started = $group->filter(fn (CourierQuoteVersion $version): bool => ! $version->isVoided() && $this->isExecutable($version) && $version->effective_date !== null && $version->effective_date->lte($today)); foreach ($started as $version) { if ($current !== null && $version->id === $current->id) { diff --git a/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php b/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php index b5b30e97..5b7d6fcc 100644 --- a/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php +++ b/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php @@ -44,6 +44,13 @@ class AccessoryReshipRateCalculator $row['deduction_rate_basis_points'] = null; $row['deduction_state'] = 'unmatched'; $row['deduction_note'] = '三个月内未匹配首发快递单号'; + $reshipCourier = trim((string) (($row['express_name1'] ?? '') ?: ($row['express_name2'] ?? ''))); + if (in_array($reshipCourier, ['外采快递', '线下快递'], true)) { + $row['deduction_cents'] = 0; + $row['deduction_state'] = 'excluded_courier'; + $row['deduction_note'] = '补发快递为' . $reshipCourier . ',不参与扣款'; + return $row; + } if ($row['status'] === 'unmatched') { return $row; } @@ -79,6 +86,10 @@ class AccessoryReshipRateCalculator $row['deduction_cents'] = self::amount($row['freight_cents'], $rate); $row['deduction_state'] = $rate === 0 ? 'zero_rate' : 'ready'; $row['deduction_note'] = $rate === 0 ? '扣款比例0%,不扣款' : '按所选首发快递配置比例核算,仅计一次'; + if ($rate > 0 && $row['deduction_cents'] === 0) { + $row['deduction_state'] = 'zero_amount'; + $row['deduction_note'] = '扣款金额为0,不扣款'; + } } return $row; } diff --git a/modules/EcommerceFinance/View/AccessoryReshipPageData.php b/modules/EcommerceFinance/View/AccessoryReshipPageData.php index 0019b53b..1a6fe40f 100644 --- a/modules/EcommerceFinance/View/AccessoryReshipPageData.php +++ b/modules/EcommerceFinance/View/AccessoryReshipPageData.php @@ -23,8 +23,8 @@ class AccessoryReshipPageData { $month = request()->query('month', now()->format('Y-m')); abort_unless(is_string($month) && preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $month) && (int) substr($month, 0, 4) >= 2000, 422, '请选择有效业务月份。'); - $status = request()->query('match_status', 'matched'); - $filters = ['month' => $month, 'match_status' => in_array($status, ['matched', 'unmatched', 'all'], true) ? $status : 'matched']; + $status = request()->query('match_status', 'ready'); + $filters = ['month' => $month, 'match_status' => in_array($status, ['ready', 'matched', 'unmatched', 'all'], true) ? $status : 'ready']; foreach (['trade_no','express_no','courier','ship_date','barcode','normalized_barcode','freight','deduction_state','original_courier','original_express_no','original_month','deduction_amount'] as $field) { $value = request()->query($field, ''); abort_unless(is_string($value) && mb_strlen($value) <= 200, 422, '筛选条件格式无效。'); @@ -93,7 +93,7 @@ class AccessoryReshipPageData if ($row['deduction_state'] === 'selection_required') $summary['ambiguous']++; if (isset($summary[$row['deduction_state']])) $summary[$row['deduction_state']]++; } - if ($filters['match_status'] !== 'all' && $filters['match_status'] !== $row['status']) continue; + if (!self::inCategory($row, $filters['match_status'])) continue; if ($total >= ($page - 1) * 50 && count($items) < 50) $items[] = $row; $total++; } @@ -113,4 +113,11 @@ class AccessoryReshipPageData 'reshipRows' => new LengthAwarePaginator($items, $total, 50, $page, ['path' => request()->url(), 'query' => request()->except('page')]), ]; } + + public static function inCategory(array $row, string $category): bool + { + return $category === 'all' || ($category === 'ready' + ? $row['deduction_state'] === 'ready' && $row['deduction_cents'] > 0 + : $row['status'] === $category); + } } diff --git a/resources/views/business/pages/courier-payables/accessory-reship-deductions.blade.php b/resources/views/business/pages/courier-payables/accessory-reship-deductions.blade.php index 13d7f429..ace6ec5d 100644 --- a/resources/views/business/pages/courier-payables/accessory-reship-deductions.blade.php +++ b/resources/views/business/pages/courier-payables/accessory-reship-deductions.blade.php @@ -1,7 +1,7 @@ @php $filters = $reshipFilters ?? []; $summary = $reshipSummary ?? []; - $labels = ['matched' => '已匹配项', 'unmatched' => '未匹配项', 'all' => '全部配件单']; + $labels = ['ready' => '可扣款', 'matched' => '已匹配项', 'unmatched' => '未匹配项', 'all' => '全部配件单']; $startMonth = \Carbon\Carbon::createFromFormat('!Y-m', ($filters['month'] ?? now()->format('Y-m')))->subMonths(2)->format('Y年n月'); @endphp
@@ -18,11 +18,11 @@ @foreach (['trade_no'=>'交易单号','express_no'=>'补发快递单号','courier'=>'补发快递','ship_date'=>'补发日期','barcode'=>'原始条形码','normalized_barcode'=>'处理后条形码','freight'=>'补发运费','original_courier'=>'首发快递','original_express_no'=>'首发单号','original_month'=>'所属月份','deduction_amount'=>'可扣金额'] as $field => $label) @endforeach - - + + 清空筛选 - @if (!$reshipError)导出筛选结果@endif + @if (!$reshipError)导出当前列表 导出可扣款清单@endif 匹配范围:{{ $startMonth }} 至 {{ \Carbon\Carbon::createFromFormat('!Y-m', $filters['month'])->format('Y年n月') }}
@@ -31,7 +31,7 @@ @endforeach
按配置比例扣款合计{{ $reshipError ? '—' : '¥ ' . number_format($summary['amount'] / 100, 2) }}
-
匹配规则与扣款口径

配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。仅匹配到首发快递单号的记录列为可扣款,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。

本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。

+
匹配规则与扣款口径

配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为外采快递或线下快递不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。

本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。

@if ($reshipError)@endif @if ($summary['ambiguous'] || $summary['invalid_amount'] || $summary['missing_config'] || $summary['zero_rate'])
待选择扣款快递 {{ $summary['selection_required'] }} 单;运费异常 {{ $summary['invalid_amount'] }} 单;待配置 {{ $summary['missing_config'] }} 单,0%不扣款 {{ $summary['zero_rate'] }} 单。待核算项不计入合计。
@endif
@@ -44,7 +44,7 @@ @foreach (['交易单号','补发快递单号','补发快递','补发日期','原始条形码','处理后条形码','补发运费','匹配状态','首发快递','首发单号','所属月份','扣款比例','可扣金额','说明'] as $heading){{ $heading }}@endforeach @forelse ($reshipRows as $row) - {{ $row['trade_no'] }}{{ $row['express_no'] }}{{ $row['express_name1'] ?: $row['express_name2'] }}{{ substr((string) $row['ship_time'], 0, 10) }}{{ $row['barcode'] }}{{ $row['normalized_barcode'] }}{{ $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2) }}{{ $row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? ($row['deduction_state'] === 'zero_rate' ? '比例0%不扣款' : '待核算') : '未匹配不扣款') }} + {{ $row['trade_no'] }}{{ $row['express_no'] }}{{ $row['express_name1'] ?: $row['express_name2'] }}{{ substr((string) $row['ship_time'], 0, 10) }}{{ $row['barcode'] }}{{ $row['normalized_barcode'] }}{{ $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2) }}{{ in_array($row['deduction_state'], ['excluded_courier', 'zero_amount'], true) ? '不扣款' : ($row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? ($row['deduction_state'] === 'zero_rate' ? '比例0%不扣款' : '待核算') : '未匹配不扣款')) }} @if (count($row['courier_candidates']) > 1)
@csrf diff --git a/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php b/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php index 9cb8e9db..7ae666d2 100644 --- a/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php +++ b/resources/views/business/pages/courier-payables/partials/stock-quote-review.blade.php @@ -22,7 +22,7 @@ @if(collect($stockEntry->files)->contains('kind', 'signed'))

@endif -

未核验签署依据时,审核后仍为“待确认/待补签”。历史资料归档与本系统电子签署分别记录。

+

存量报价审核通过后按实际执行日期生效,无需签章。历史签署资料可另行核验归档,不影响报价生效。

@endif diff --git a/resources/views/business/pages/courier-payables/quote-change-show.blade.php b/resources/views/business/pages/courier-payables/quote-change-show.blade.php index 3cfe18ba..5230df4e 100644 --- a/resources/views/business/pages/courier-payables/quote-change-show.blade.php +++ b/resources/views/business/pages/courier-payables/quote-change-show.blade.php @@ -88,7 +88,7 @@
追溯核算
{{ $change->recalculationStatusLabel() }}
{{ $stockEntry ? '录入范围' : '变价明细' }}
{{ $stockEntry ? $stockEntry->source_range : ($requoteDiff ? ($requoteDiff['same_structure'] ? count($requoteDiff['cells']) . ' 处价格变动' : '计费方式调整') : $change->items->count() . ' 项') }}
新报价版本
{{ $change->version?->version_code ?? '尚未生成' }}
-
签署状态
{{ $change->version?->signature_status ?? '未进入签署' }}
+
签署状态
{{ $change->version?->signatureLabel() ?? '未进入签署' }}
变更依据与说明
{{ $change->summary }}
diff --git a/resources/views/business/pages/courier-payables/quote-management.blade.php b/resources/views/business/pages/courier-payables/quote-management.blade.php index a40cea8c..1f4bd7d2 100644 --- a/resources/views/business/pages/courier-payables/quote-management.blade.php +++ b/resources/views/business/pages/courier-payables/quote-management.blade.php @@ -849,6 +849,7 @@
原工作表内容 · 只读
+
@@ -1114,6 +1115,16 @@ setDetail('range', text(quote.source_range, '-')); setDetail('date', text(quote.period_label || quote.effective_date, 'Excel 未标注')); setDetail('formulas', String(quote.formula_count)); + var originals = root.querySelector('[data-stock-original-links]'); + originals.replaceChildren(); + (quote.stock_originals || []).forEach(function (file, index, files) { + var link = create('a', 'quote-button', (file.previewable ? '预览签署原件' : '下载签署原件') + (files.length > 1 ? ' ' + (index + 1) : '')); + link.href = file.url; + link.title = file.name; + link.target = '_blank'; + link.rel = 'noopener noreferrer'; + originals.appendChild(link); + }); var signedBox = root.querySelector('[data-quote-signed]'); var files = quote.signed_files || {}; signedBox.hidden = !files.signed; diff --git a/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php b/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php index f36cb4d2..4c74ba7b 100644 --- a/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php +++ b/resources/views/business/pages/courier-payables/stock-quote-entry.blade.php @@ -69,7 +69,7 @@

支持 xlsx、xls,单文件不超过 20 MB。原始内容和公式会保留。

多工作表文件请填写本次报价所在的准确工作表名称。

最多 10 个,每个不超过 20 MB。

-

上传资料后由审核人核验;缺少有效签署依据时标记为待确认/待补签。

+

上传资料后由审核人核验归档;存量报价审核后按执行日期生效,无需签章。

diff --git a/tests/Feature/Business/CourierStockQuoteBatchTest.php b/tests/Feature/Business/CourierStockQuoteBatchTest.php index 36bb81bd..de14a2e9 100644 --- a/tests/Feature/Business/CourierStockQuoteBatchTest.php +++ b/tests/Feature/Business/CourierStockQuoteBatchTest.php @@ -364,7 +364,7 @@ class CourierStockQuoteBatchTest extends FeatureTestCase $reviewer->companies()->attach($this->company->id); $changes = app(CourierQuoteChangeService::class); $approved = $changes->approve($entry->change, (int) $reviewer->id); - $this->assertSame('待确认/待补签', $approved->version->signature_status); + $this->assertSame('存量报价,审核生效(无需签章)', $approved->version->signature_status); $change = $changes->create([ 'source_quote_id' => $approved->version->version_code, 'change_type' => '费率调整', 'effective_date' => '2026-10-16', 'summary' => '基于存量版本调价,原件继续保留', diff --git a/tests/Feature/Business/CourierStockQuoteEntryTest.php b/tests/Feature/Business/CourierStockQuoteEntryTest.php index bfae5f2c..24947b53 100644 --- a/tests/Feature/Business/CourierStockQuoteEntryTest.php +++ b/tests/Feature/Business/CourierStockQuoteEntryTest.php @@ -75,12 +75,18 @@ class CourierStockQuoteEntryTest extends FeatureTestCase $this->assertDatabaseCount('business_courier_quote_versions', 0); $this->loginAs($reviewer, $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); $version = CourierQuoteVersion::firstOrFail(); - $this->assertSame('待确认/待补签', $version->signature_status); + $this->assertSame('存量报价,审核生效(无需签章)', $version->signature_status); $this->assertSame('EXP-001', $version->courier_expcode); $this->assertSame(CourierQuoteChange::RECALCULATION_NOT_REQUIRED, $entry->change->fresh()->recalculation_status); $this->assertSame('2025-12-01', $version->effective_date->toDateString()); $this->assertDatabaseCount('business_courier_quote_signatures', 0); - $this->assertSame('待生效(待签章)', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); + $this->assertSame('生效中', app(QuoteManagementSnapshot::class)->load()['quotes'][0]['status']); + $this->assertSame($version->id, \App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())->id); + // 已上线旧记录无需改库或重新审核,也按存量审核规则生效。 + $version->update(['signature_status' => '待确认/待补签']); + $snapshot = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; + $this->assertSame('生效中', $snapshot['status']); + $this->assertSame('存量报价,审核生效(无需签章)', $snapshot['signature_status']); } public function test_historical_documents_require_reviewer_verification_and_remain_distinct_from_esign(): void @@ -110,6 +116,30 @@ class CourierStockQuoteEntryTest extends FeatureTestCase $this->assertDatabaseCount('business_courier_quote_versions', 0); } + public function test_approved_stock_quote_exposes_all_originals_without_requiring_signature_verification(): void + { + $entry = $this->createEntry(['signed_files' => [ + UploadedFile::fake()->createWithContent('signed-original.pdf', "%PDF-1.4\n1 0 obj\n<< /Type /Catalog >>\nendobj\n%%EOF"), + UploadedFile::fake()->image('签署原件二.png'), + ]]); + $this->loginAs()->post(route('business.courier-quote-changes.submit', $entry->change)); + $this->loginAs($this->reviewer(), $this->company)->post(route('business.courier-quote-changes.approve', $entry->change), ['stock_rules_confirmed' => '1'])->assertRedirect(); + $quote = app(QuoteManagementSnapshot::class)->load()['quotes'][0]; + $this->assertSame('生效中', $quote['status']); + $this->assertCount(2, $quote['stock_originals']); + $this->assertSame([], $quote['signed_files']); + $this->loginAs()->get(route('business.pages.show', ['section' => 'courier-payables', 'page' => 'quote-management'])) + ->assertOk()->assertSee('data-stock-original-links', false)->assertSee('预览签署原件'); + foreach ($quote['stock_originals'] as $original) { + $this->assertTrue($original['previewable']); + $response = $this->loginAs()->get($original['url'])->assertOk()->assertHeader('X-Content-Type-Options', 'nosniff'); + $this->assertStringStartsWith('inline;', $response->headers->get('Content-Disposition')); + } + $version = CourierQuoteVersion::firstOrFail(); + $version->update(['status' => CourierQuoteVersion::STATUS_VOIDED]); + $this->assertNull(\App\Support\CourierRequoteService::resolveAt(collect([$version->fresh()]), now())); + } + public function test_same_code_and_type_cannot_be_imported_twice_but_same_names_with_different_codes_can(): void { $this->createEntry(); diff --git a/tests/Unit/AccessoryReshipPageDataTest.php b/tests/Unit/AccessoryReshipPageDataTest.php index 807feb81..f1b6ba6a 100644 --- a/tests/Unit/AccessoryReshipPageDataTest.php +++ b/tests/Unit/AccessoryReshipPageDataTest.php @@ -22,6 +22,32 @@ require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/Services/Accessory class AccessoryReshipPageDataTest extends TestCase { + public function test_deductible_category_excludes_special_reship_couriers_and_nonpositive_amounts(): void + { + $calculator = new AccessoryReshipRateCalculator; + $base = ['status' => 'matched', 'freight_cents' => 1000, 'matches' => [['courier' => '首发快递']]]; + $rates = ['首发快递' => ['rate_basis_points' => 5000]]; + foreach (['外采快递', '线下快递'] as $name) { + foreach (['express_name1', 'express_name2'] as $field) { + $row = $calculator->apply($base + [$field => ' ' . $name . ' '], $rates); + $this->assertSame('excluded_courier', $row['deduction_state']); + $this->assertSame(0, $row['deduction_cents']); + $this->assertFalse(AccessoryReshipPageData::inCategory($row, 'ready')); + $this->assertTrue(AccessoryReshipPageData::inCategory($row, 'all')); + } + } + $ready = $calculator->apply($base + ['express_name1' => '正常快递'], $rates); + $this->assertTrue(AccessoryReshipPageData::inCategory($ready, 'ready')); + $this->assertSame(500, $ready['deduction_cents']); + foreach ([0, 1, null] as $freight) { + $row = $calculator->apply(array_replace($base, ['freight_cents' => $freight]), ['首发快递' => ['rate_basis_points' => 1]]); + $this->assertFalse(AccessoryReshipPageData::inCategory($row, 'ready')); + } + foreach (['zero_rate', 'selection_required', 'missing_config', 'invalid_amount', 'unmatched'] as $state) { + $this->assertFalse(AccessoryReshipPageData::inCategory(array_replace($ready, ['deduction_state' => $state]), 'ready')); + } + } + private function selections(): AccessoryReshipSelectionStore { $store = Mockery::mock(AccessoryReshipSelectionStore::class);