修复存量报价审核生效、原件预览和配件补发可扣款清单 #377
@ -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;
|
||||
|
||||
@ -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]);
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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,
|
||||
]);
|
||||
|
||||
@ -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([
|
||||
|
||||
@ -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) {
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
<div class="reship-page">
|
||||
@ -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)
|
||||
<label>{{ $label }}<input type="{{ $field === 'ship_date' ? 'date' : ($field === 'original_month' ? 'month' : 'text') }}" name="{{ $field }}" value="{{ $filters[$field] ?? '' }}" placeholder="{{ in_array($field, ['freight','deduction_amount']) ? '金额或待核实' : '筛选' . $label }}" maxlength="200"></label>
|
||||
@endforeach
|
||||
<label>匹配状态<select name="match_status">@foreach (['all'=>'全部状态','matched'=>'已匹配','unmatched'=>'未匹配不扣款'] as $value => $label)<option value="{{ $value }}" @selected($filters['match_status'] === $value)>{{ $label }}</option>@endforeach</select></label>
|
||||
<label>扣款状态<select name="deduction_state"><option value="">全部状态</option>@foreach (['ready'=>'可扣款','unmatched'=>'未匹配不扣款','missing_config'=>'待配置比例','selection_required'=>'待选择扣款快递','zero_rate'=>'比例0%不扣款','invalid_amount'=>'运费待核实'] as $value => $label)<option value="{{ $value }}" @selected(($filters['deduction_state'] ?? '') === $value)>{{ $label }}</option>@endforeach</select></label>
|
||||
<label>匹配状态<select name="match_status">@foreach (['ready'=>'可扣款','all'=>'全部状态','matched'=>'已匹配','unmatched'=>'未匹配不扣款'] as $value => $label)<option value="{{ $value }}" @selected($filters['match_status'] === $value)>{{ $label }}</option>@endforeach</select></label>
|
||||
<label>扣款状态<select name="deduction_state"><option value="">全部状态</option>@foreach (['ready'=>'可扣款','unmatched'=>'未匹配不扣款','missing_config'=>'待配置比例','selection_required'=>'待选择扣款快递','excluded_courier'=>'补发快递不参与扣款','zero_amount'=>'金额0不扣款','zero_rate'=>'比例0%不扣款','invalid_amount'=>'运费待核实'] as $value => $label)<option value="{{ $value }}" @selected(($filters['deduction_state'] ?? '') === $value)>{{ $label }}</option>@endforeach</select></label>
|
||||
<button type="submit" class="reship-primary">查询并匹配</button>
|
||||
<a class="reship-button" href="{{ request()->url() . '?' . http_build_query(['month'=>$filters['month'], 'match_status'=>'all']) }}">清空筛选</a>
|
||||
@if (!$reshipError)<a class="reship-button" href="{{ route('business.accessory-reship-deductions.export', $filters) }}">导出筛选结果</a>@endif
|
||||
@if (!$reshipError)<a class="reship-button" href="{{ route('business.accessory-reship-deductions.export', $filters) }}">导出当前列表</a> <a class="reship-button" href="{{ route('business.accessory-reship-deductions.export', array_merge($filters, ['match_status'=>'ready', 'deduction_state'=>''])) }}">导出可扣款清单</a>@endif
|
||||
<span class="reship-range">匹配范围:{{ $startMonth }} 至 {{ \Carbon\Carbon::createFromFormat('!Y-m', $filters['month'])->format('Y年n月') }}</span>
|
||||
</form>
|
||||
<div class="reship-metrics">
|
||||
@ -31,7 +31,7 @@
|
||||
@endforeach
|
||||
<div><span>按配置比例扣款合计</span><strong>{{ $reshipError ? '—' : '¥ ' . number_format($summary['amount'] / 100, 2) }}</strong></div>
|
||||
</div>
|
||||
<details class="reship-rules"><summary>匹配规则与扣款口径</summary><p>配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。仅匹配到首发快递单号的记录列为可扣款,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。</p><p>本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。</p></details>
|
||||
<details class="reship-rules"><summary>匹配规则与扣款口径</summary><p>配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为外采快递或线下快递不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。</p><p>本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。</p></details>
|
||||
@if ($reshipError)<div class="reship-error" role="alert">{{ $reshipError }}</div>@endif
|
||||
@if ($summary['ambiguous'] || $summary['invalid_amount'] || $summary['missing_config'] || $summary['zero_rate'])<div class="reship-notice" role="status">待选择扣款快递 {{ $summary['selection_required'] }} 单;运费异常 {{ $summary['invalid_amount'] }} 单;待配置 {{ $summary['missing_config'] }} 单,0%不扣款 {{ $summary['zero_rate'] }} 单。待核算项不计入合计。</div>@endif
|
||||
<section class="reship-panel" data-finance-table-surface>
|
||||
@ -44,7 +44,7 @@
|
||||
@foreach (['交易单号','补发快递单号','补发快递','补发日期','原始条形码','处理后条形码','补发运费','匹配状态','首发快递','首发单号','所属月份','扣款比例','可扣金额','说明'] as $heading)<th>{{ $heading }}</th>@endforeach
|
||||
</tr></thead><tbody>
|
||||
@forelse ($reshipRows as $row)
|
||||
<tr><td>{{ $row['trade_no'] }}</td><td>{{ $row['express_no'] }}</td><td>{{ $row['express_name1'] ?: $row['express_name2'] }}</td><td>{{ substr((string) $row['ship_time'], 0, 10) }}</td><td class="reship-barcode">{{ $row['barcode'] }}</td><td>{{ $row['normalized_barcode'] }}</td><td class="reship-money">{{ $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2) }}</td><td><span class="reship-status {{ $row['status'] }}">{{ $row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? ($row['deduction_state'] === 'zero_rate' ? '比例0%不扣款' : '待核算') : '未匹配不扣款') }}</span></td><td>
|
||||
<tr><td>{{ $row['trade_no'] }}</td><td>{{ $row['express_no'] }}</td><td>{{ $row['express_name1'] ?: $row['express_name2'] }}</td><td>{{ substr((string) $row['ship_time'], 0, 10) }}</td><td class="reship-barcode">{{ $row['barcode'] }}</td><td>{{ $row['normalized_barcode'] }}</td><td class="reship-money">{{ $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2) }}</td><td><span class="reship-status {{ $row['status'] }}">{{ in_array($row['deduction_state'], ['excluded_courier', 'zero_amount'], true) ? '不扣款' : ($row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? ($row['deduction_state'] === 'zero_rate' ? '比例0%不扣款' : '待核算') : '未匹配不扣款')) }}</span></td><td>
|
||||
@if (count($row['courier_candidates']) > 1)
|
||||
<form data-reship-selection action="{{ route('business.accessory-reship-deductions.selection') }}" method="POST">
|
||||
@csrf
|
||||
|
||||
@ -22,7 +22,7 @@
|
||||
@if(collect($stockEntry->files)->contains('kind', 'signed'))
|
||||
<p><label><input type="checkbox" form="quote-approval-form" name="stock_signature_verified" value="1"> 我已核验历史签署资料与本报价一致,按历史签署资料归档</label></p>
|
||||
@endif
|
||||
<p class="quote-review__hint">未核验签署依据时,审核后仍为“待确认/待补签”。历史资料归档与本系统电子签署分别记录。</p>
|
||||
<p class="quote-review__hint">存量报价审核通过后按实际执行日期生效,无需签章。历史签署资料可另行核验归档,不影响报价生效。</p>
|
||||
</div>
|
||||
@endif
|
||||
</div>
|
||||
|
||||
@ -88,7 +88,7 @@
|
||||
<div><dt>追溯核算</dt><dd>{{ $change->recalculationStatusLabel() }}</dd></div>
|
||||
<div><dt>{{ $stockEntry ? '录入范围' : '变价明细' }}</dt><dd>{{ $stockEntry ? $stockEntry->source_range : ($requoteDiff ? ($requoteDiff['same_structure'] ? count($requoteDiff['cells']) . ' 处价格变动' : '计费方式调整') : $change->items->count() . ' 项') }}</dd></div>
|
||||
<div><dt>新报价版本</dt><dd>{{ $change->version?->version_code ?? '尚未生成' }}</dd></div>
|
||||
<div><dt>签署状态</dt><dd>{{ $change->version?->signature_status ?? '未进入签署' }}</dd></div>
|
||||
<div><dt>签署状态</dt><dd>{{ $change->version?->signatureLabel() ?? '未进入签署' }}</dd></div>
|
||||
</dl>
|
||||
<div class="quote-review__summary"><strong>变更依据与说明</strong><br>{{ $change->summary }}</div>
|
||||
</div>
|
||||
|
||||
@ -849,6 +849,7 @@
|
||||
<div class="quote-modal__title" id="quote-detail-title" data-quote-detail="title"></div>
|
||||
<div class="quote-modal__subtitle">原工作表内容 · 只读</div>
|
||||
</div>
|
||||
<div data-stock-original-links style="display:flex;flex-wrap:wrap;justify-content:flex-end;gap:8px;margin-left:auto"></div>
|
||||
<button class="quote-modal__close" type="button" aria-label="关闭报价明细" title="关闭" data-quote-close>
|
||||
<span class="material-icons-outlined" aria-hidden="true">close</span>
|
||||
</button>
|
||||
@ -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;
|
||||
|
||||
@ -69,7 +69,7 @@
|
||||
<div class="stock-quote__field"><label for="source_quote">{{ $entry ? '替换当前报价 Excel(可选)' : '原报价 Excel(必填)' }}</label><input class="stock-quote__control" id="source_quote" name="source_quote" type="file" accept=".xlsx,.xls" @required(!$entry)><p class="stock-quote__hint">支持 xlsx、xls,单文件不超过 20 MB。原始内容和公式会保留。</p></div>
|
||||
<div class="stock-quote__field"><label for="sheet_name">工作表名称</label><input class="stock-quote__control" id="sheet_name" name="sheet_name" value="{{ old('sheet_name', $entry?->sheet_name) }}" maxlength="255" placeholder="文件只有一张工作表时可留空"><p class="stock-quote__hint">多工作表文件请填写本次报价所在的准确工作表名称。</p></div>
|
||||
<div class="stock-quote__field"><label for="confirmation_files">报价确认单、合同及其他依据(可选)</label><input class="stock-quote__control" id="confirmation_files" name="confirmation_files[]" type="file" multiple accept=".pdf,.doc,.docx,.jpg,.jpeg,.png,.xlsx,.xls,.eml,.msg"><p class="stock-quote__hint">最多 10 个,每个不超过 20 MB。</p></div>
|
||||
<div class="stock-quote__field"><label for="signed_files">历史签署资料(可选)</label><input class="stock-quote__control" id="signed_files" name="signed_files[]" type="file" multiple accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"><p class="stock-quote__hint">上传资料后由审核人核验;缺少有效签署依据时标记为待确认/待补签。</p></div>
|
||||
<div class="stock-quote__field"><label for="signed_files">历史签署资料(可选)</label><input class="stock-quote__control" id="signed_files" name="signed_files[]" type="file" multiple accept=".pdf,.doc,.docx,.jpg,.jpeg,.png"><p class="stock-quote__hint">上传资料后由审核人核验归档;存量报价审核后按执行日期生效,无需签章。</p></div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
@ -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' => '基于存量版本调价,原件继续保留',
|
||||
|
||||
@ -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();
|
||||
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user