chenxi 5fe92bd05c 商品成本:订单侧不取套餐/服务占位编码 TCXJ、FUWU
订单原始数据里的 TCXJ(套餐)、FUWU(服务)不是实物商品,商品成本同步时直接剔除
(SQL 层过滤 + 组合装拆出的组件里同样剔除),页面口径说明同步补充。
2026-10-20 11:53:03 +08:00

182 lines
14 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

@php
$rows = $productCostRows ?? null;
$filters = $productCostFilters ?? [];
$summary = $productCostSummary ?? [];
$months = $productCostMonths ?? [];
$platforms = $productCostPlatforms ?? [];
$warehouses = $productCostWarehouses ?? [];
$error = $productCostError ?? null;
$money = static fn (mixed $value): string => $value === null ? '-' : number_format((float) $value, 2);
$text = static fn (mixed $value): string => $value === null || $value === '' ? '-' : (string) $value;
$qty = static function (mixed $value): string {
if ($value === null) {
return '-';
}
$formatted = number_format((float) $value, 2, '.', ',');
return str_contains($formatted, '.')
? rtrim(rtrim($formatted, '0'), '.')
: $formatted;
};
@endphp
<div class="product-cost-page">
<style>
.product-cost-page { color: var(--vip-content-ink, #171717); font-size: .8125rem; }
.product-cost-toolbar { display:flex; flex-wrap:wrap; align-items:flex-end; gap:.75rem; padding:1rem; margin-bottom:1rem; border:1px solid var(--vip-border,#D8D2C7); border-radius:.5rem; background:var(--vip-surface,#fff); }
.product-cost-field { display:flex; flex-direction:column; gap:.35rem; min-width:12rem; }
.product-cost-field label { color:var(--vip-content-muted,#595959); font-size:.75rem; font-weight:600; }
.product-cost-field input,.product-cost-field select { height:2.25rem; padding:0 .65rem; border:1px solid var(--vip-border,#D8D2C7); border-radius:.5rem; background:#fff; color:var(--vip-content-ink,#171717); }
.product-cost-field input:focus,.product-cost-field select:focus { border-color:var(--vip-content-accent-dark,#856632); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); }
.product-cost-check { display:flex; align-items:center; gap:.4rem; height:2.25rem; color:var(--vip-content-muted,#595959); font-size:.75rem; }
.product-cost-actions { display:flex; gap:.5rem; }
.product-cost-button { display:inline-flex; align-items:center; gap:.35rem; height:2.25rem; padding:0 .85rem; border:1px solid var(--vip-border,#D8D2C7); border-radius:.5rem; background:#fff; color:var(--vip-content-ink,#171717); text-decoration:none; font-weight:600; }
.product-cost-button.primary { border-color:var(--vip-content-accent-dark,#856632); background:var(--vip-content-accent-dark,#856632); color:#fff; }
.product-cost-alert { padding:.75rem 1rem; margin-bottom:1rem; border:1px solid var(--vip-danger,#B80000); border-radius:.5rem; background:#fff5f5; color:var(--vip-danger,#B80000); }
.product-cost-summary { display:flex; flex-wrap:wrap; gap:1rem; padding:.75rem 1rem; margin-bottom:1rem; border:1px solid var(--vip-border,#D8D2C7); border-radius:.5rem; background:var(--vip-content-accent-soft,#F7F0DE); }
.product-cost-summary .item { display:flex; flex-direction:column; gap:.15rem; }
.product-cost-summary .item span { color:var(--vip-content-muted,#595959); font-size:.7rem; }
.product-cost-summary .item b { font-size:.95rem; font-variant-numeric:tabular-nums; }
.product-cost-summary .item.is-warn b { color:var(--vip-danger,#B80000); }
.product-cost-note { margin-bottom:.75rem; color:var(--vip-content-muted,#595959); font-size:.75rem; line-height:1.6; }
.product-cost-table-wrap { overflow:auto; border:1px solid var(--vip-border,#D8D2C7); border-radius:.5rem; background:#fff; }
.product-cost-table { width:100%; min-width:78rem; border-collapse:separate; border-spacing:0; font-size:.75rem; }
.product-cost-table th,.product-cost-table td { padding:.55rem .65rem; border-right:1px solid var(--vip-border,#D8D2C7); border-bottom:1px solid var(--vip-border,#D8D2C7); white-space:nowrap; vertical-align:top; }
.product-cost-table th { position:sticky; top:0; z-index:1; background:var(--vip-content-accent-soft,#F7F0DE); text-align:left; font-weight:700; }
.product-cost-table tr:hover td { background:#fcfaf4; }
.product-cost-table .num { text-align:right; font-variant-numeric:tabular-nums; }
.product-cost-table .missing { color:var(--vip-danger,#B80000); }
.product-cost-empty { padding:4rem 1rem; text-align:center; color:var(--vip-content-muted,#595959); }
.product-cost-pagination { display:flex; justify-content:space-between; padding:.75rem 1rem; color:var(--vip-content-muted,#595959); }
.product-cost-pagination a { color:var(--vip-content-accent-dark,#856632); margin-left:.75rem; }
</style>
<form method="GET" class="product-cost-toolbar" aria-label="商品成本筛选">
<div class="product-cost-field">
<label for="product-cost-month">月份</label>
<select id="product-cost-month" name="month">
@forelse ($months as $option)
<option value="{{ $option }}" @selected(($filters['month'] ?? '') === $option)>{{ $option }}</option>
@empty
<option value="{{ $filters['month'] ?? '' }}">{{ $filters['month'] ?? '暂无数据' }}</option>
@endforelse
</select>
</div>
<div class="product-cost-field">
<label for="product-cost-platform">平台</label>
<select id="product-cost-platform" name="platform">
<option value="">全部</option>
@foreach ($platforms as $option)
<option value="{{ $option }}" @selected(($filters['platform'] ?? '') === $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<div class="product-cost-field">
<label for="product-cost-store">店铺名称</label>
<input id="product-cost-store" name="store_name" value="{{ $filters['store_name'] ?? '' }}" placeholder="支持包含查询">
</div>
<div class="product-cost-field">
<label for="product-cost-order">线上单号</label>
<input id="product-cost-order" name="online_order_no" value="{{ $filters['online_order_no'] ?? '' }}" placeholder="支持包含查询">
</div>
<div class="product-cost-field">
<label for="product-cost-code">商品编码</label>
<input id="product-cost-code" name="product_code" value="{{ $filters['product_code'] ?? '' }}" placeholder="支持包含查询">
</div>
<div class="product-cost-field">
<label for="product-cost-warehouse">仓库简称</label>
<select id="product-cost-warehouse" name="warehouse_short_name">
<option value="">全部</option>
@foreach ($warehouses as $option)
<option value="{{ $option }}" @selected(($filters['warehouse_short_name'] ?? '') === $option)>{{ $option }}</option>
@endforeach
</select>
</div>
<label class="product-cost-check">
<input type="checkbox" name="missing_price" value="1" @checked(($filters['missing_price'] ?? '') === '1')>
只看未匹配商品单价
</label>
<label class="product-cost-check">
<input type="checkbox" name="include_excluded" value="1" @checked(($filters['include_excluded'] ?? '') === '1')>
包含「-不看」仓
</label>
<div class="product-cost-actions">
<button class="product-cost-button primary" type="submit"><span class="material-icons-outlined" aria-hidden="true">search</span>查询</button>
<a class="product-cost-button" href="{{ route('business.financial-operations.product-costs.export', array_merge(['company_id' => company_id()], request()->except('page'))) }}" aria-label="导出当前筛选结果" title="导出当前筛选结果"><span class="material-icons-outlined" aria-hidden="true">download</span>导出</a>
<a class="product-cost-button" href="{{ route('business.pages.show', ['section' => $section['slug'], 'page' => $page['slug']]) }}"><span class="material-icons-outlined" aria-hidden="true">refresh</span>重置</a>
</div>
</form>
@if ($error)
<div class="product-cost-alert" role="alert">{{ $error }}</div>
@endif
<div class="product-cost-note">
口径:以「业绩明细」的账单行为基数(发生时间 / 店铺名称 / 线上单号),按「店铺 + 线上订单号」展开订单侧商品行,
只取收入金额有数据且不为 0 的账单行,一行 = 一个发货仓库的一个商品;发货仓库取订单的聚水潭仓库名称,仓库简称查仓库映射表,
商品单价按「仓库简称 + 商品编码」取「商品月加权平均单价」里的加权平均价;匹配不到时退回「线上代发价格」
(采购单价优先、其次钉钉代发单价),仍匹配不到时用「普通商品资料」里的商品成本价兜底;商品成本 = 商品数量 × 商品单价。
拆单子单重复列出的同一批商品只算一次,同一单分多仓发货的按子单实际仓库分行;虚拟条码(X0000x)不计入;
套餐/服务占位编码 TCXJ、FUWU 不取数(组合装里拆出的这两个编码同样剔除)。
定制配件这类明细行,聚水潭把配件件数写进了「商品数量」(例如数量 9524、金额 2000.04,实际单价 0.21),
乘成品的月加权/代发单价会放大几百倍;成本单价高于该行明细单价 3 倍时判为「数量与单价口径不一致」,成本留空、不计入合计。
仓库映射表里标记为「-不看」的仓库(次品 / 销退 / 虚拟 / 信誉订单 / 京东云仓等)默认不展示,
需要核对时可以勾选「包含『-不看』仓」。
</div>
<div class="product-cost-summary">
<div class="item"><span>明细行数</span><b>{{ number_format((int) ($summary['rows'] ?? 0)) }}</b></div>
<div class="item"><span>商品数量合计(已匹配单价)</span><b>{{ $qty($summary['quantity_total'] ?? null) }}</b></div>
<div class="item"><span>商品成本合计(已匹配单价)</span><b>{{ $money($summary['cost_total'] ?? null) }}</b></div>
<div class="item {{ ($summary['missing_price_rows'] ?? 0) > 0 ? 'is-warn' : '' }}"><span>未匹配商品单价行数</span><b>{{ number_format((int) ($summary['missing_price_rows'] ?? 0)) }}</b></div>
<div class="item"><span>未匹配单价的商品数量</span><b>{{ $qty($summary['missing_price_quantity'] ?? null) }}</b></div>
<div class="item"><span>其中用线上代发价格兜底</span><b>{{ number_format((int) ($summary['fallback_price_rows'] ?? 0)) }}</b></div>
<div class="item"><span>其中用商品资料成本价兜底</span><b>{{ number_format((int) ($summary['catalog_price_rows'] ?? 0)) }}</b></div>
<div class="item"><span>数量与单价口径不一致(不计成本)</span><b>{{ number_format((int) ($summary['unit_mismatch_rows'] ?? 0)) }}</b></div>
<div class="item {{ ($summary['missing_warehouse_rows'] ?? 0) > 0 ? 'is-warn' : '' }}"><span>未匹配仓库简称行数</span><b>{{ number_format((int) ($summary['missing_warehouse_rows'] ?? 0)) }}</b></div>
<div class="item"><span>已隐藏的「-不看」仓行数</span><b>{{ number_format((int) ($summary['excluded_rows'] ?? 0)) }}</b></div>
</div>
<div class="product-cost-table-wrap">
<table class="product-cost-table">
<thead><tr>
<th>发生时间</th>
<th>店铺名称</th>
<th>线上单号</th>
<th>发货仓库</th>
<th>仓库简称</th>
<th>商品编码</th>
<th class="num">商品数量</th>
<th class="num">商品单价</th>
<th class="num">商品成本</th>
</tr></thead>
<tbody>
@forelse ($rows ?? [] as $row)
<tr>
<td>{{ $text(substr((string) ($row->occurred_date ?? ''), 0, 10)) }}</td>
<td>{{ $text($row->store_name) }}</td>
<td>{{ $text($row->online_order_no) }}</td>
<td>{{ $text($row->warehouse) }}</td>
<td class="{{ ($row->warehouse_short_name ?? '') === '' ? 'missing' : '' }}">{{ $text($row->warehouse_short_name) }}</td>
<td>{{ $text($row->product_code) }}</td>
<td class="num">{{ $qty($row->product_quantity) }}</td>
<td class="num {{ $row->unit_price === null ? 'missing' : '' }}">{{ $money($row->unit_price) }}</td>
<td class="num {{ $row->product_cost === null ? 'missing' : '' }}">{{ $money($row->product_cost) }}</td>
</tr>
@empty
<tr><td colspan="9"><div class="product-cost-empty">
{{ $error ? '镜像读取失败,请稍后重试。' : '暂无符合条件的商品成本数据;生成完成后会自动显示(store-receipt:product-cost:sync)。' }}
</div></td></tr>
@endforelse
</tbody>
</table>
<div class="product-cost-pagination">
<span>当前页 {{ $rows?->count() ?? 0 }} 条 · 第 {{ $rows?->currentPage() ?? 1 }} 页 · 共 {{ $rows?->total() ?? 0 }} 条</span>
<span>
@if ($rows && ! $rows->onFirstPage())<a href="{{ $rows->previousPageUrl() }}">上一页</a>@endif
@if ($rows && $rows->hasMorePages())<a href="{{ $rows->nextPageUrl() }}">下一页</a>@endif
</span>
</div>
</div>
</div>