feat: 新增山东河北供应商配件运费扣除明细
This commit is contained in:
parent
d24eefb687
commit
28b46f593c
@ -3265,6 +3265,7 @@ class Pages extends Controller
|
||||
'board-deductions' => $this->contentForBoardDeductions(),
|
||||
'supplier-payables-report' => $this->contentForSupplierPayablesReport(),
|
||||
'supplier-management' => app(\Modules\EcommerceFinance\View\SupplierManagementPageData::class)->page(),
|
||||
'accessory-freight-deductions' => app(\Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData::class)->page(),
|
||||
'payable-cloud-warehouse-vehicle-fee' => $this->contentForDingPayableWorkflows(),
|
||||
'online-daifa-price' => $this->contentForOnlineDaifaPrice(),
|
||||
'product-costs' => $this->contentForProductCosts(),
|
||||
|
||||
41
app/Http/Controllers/Business/SupplierAccessoryFreight.php
Normal file
41
app/Http/Controllers/Business/SupplierAccessoryFreight.php
Normal file
@ -0,0 +1,41 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Business;
|
||||
|
||||
use Illuminate\Routing\Controller;
|
||||
use Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher;
|
||||
use Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData;
|
||||
|
||||
class SupplierAccessoryFreight extends Controller
|
||||
{
|
||||
public function export(SupplierAccessoryFreightPageData $data)
|
||||
{
|
||||
$filters = $data->filters();
|
||||
$columns = $data->exportColumns();
|
||||
$file = tmpfile();
|
||||
abort_if($file === false, 500, '无法创建导出文件。');
|
||||
try {
|
||||
fwrite($file, "\xEF\xBB\xBF");
|
||||
fputcsv($file, ['业务月份', '发货记录ID', '仓库', ...array_values($columns)]);
|
||||
foreach ($data->rows($filters) as $row) {
|
||||
if ($filters['state'] !== 'all' && $filters['state'] !== $row['state']) continue;
|
||||
$cells = array_merge($row, ['courier' => ($row['express_name1'] ?? '') ?: ($row['express_name2'] ?? ''),
|
||||
'freight' => $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2, '.', ''),
|
||||
'state' => SupplierAccessoryFreightMatcher::STATES[$row['state']], 'deduction' => number_format($row['deduction_cents'] / 100, 2, '.', '')]);
|
||||
$values = [$filters['month'], $row['source_id'], \Modules\EcommerceFinance\Services\AccessoryReshipMatcher::WAREHOUSE];
|
||||
foreach ($columns as $key => $label) $values[] = $cells[$key];
|
||||
fputcsv($file, array_map(static function ($value) {
|
||||
$value = (string) $value;
|
||||
// 纯数字标识保留文本,防止 Excel 截断长单号;同时阻止公式执行。
|
||||
return preg_match('/^[=+\-@\t\r\n]|^\d{12,}$/u', $value) ? "'" . $value : $value;
|
||||
}, $values));
|
||||
}
|
||||
rewind($file);
|
||||
} catch (\Throwable $exception) {
|
||||
fclose($file);
|
||||
report($exception);
|
||||
abort(503, '资料读取失败,未生成不完整的扣款明细,请稍后重试。');
|
||||
}
|
||||
return response()->streamDownload(function () use ($file) { fpassthru($file); fclose($file); }, SupplierAccessoryFreightPageData::TITLE . '-' . $filters['month'] . '.csv', ['Content-Type' => 'text/csv; charset=UTF-8']);
|
||||
}
|
||||
}
|
||||
@ -685,6 +685,7 @@ $navigation = [
|
||||
'icon' => 'local_shipping',
|
||||
'pages' => [
|
||||
['slug' => 'supplier-management', 'title' => '供应商管理'],
|
||||
['slug' => 'accessory-freight-deductions', 'title' => '新河北仓、曹县仓配件运费扣除明细'],
|
||||
['slug' => 'product-prices', 'title' => '商品价格'],
|
||||
['slug' => 'payment-reconciliation', 'title' => '货款对账'],
|
||||
['slug' => 'return-reconciliation', 'title' => '退货对账'],
|
||||
|
||||
15
docs/供应商配件运费扣除.md
Normal file
15
docs/供应商配件运费扣除.md
Normal file
@ -0,0 +1,15 @@
|
||||
# 新河北仓、曹县仓配件运费扣除明细
|
||||
|
||||
入口:供应商应付 → 新河北仓、曹县仓配件运费扣除明细。
|
||||
|
||||
按业务月份读取发货数据,仓库固定为“常州简尚家居有限公司”。沿用配件补发扣款的包裹合并及条形码规则:按 `source_id` 合并运费,按明细顺序取第一个逗号前的条形码,删除最后一个 `-` 及其后缀。
|
||||
|
||||
处理后的条形码匹配普通商品资料 `sku_id`,使用商品的 `supplier_name` 精确匹配供应商管理的 `scmname`,读取 `linkerprovience`(联系人省)。山东、山东省统一为山东;河北、河北省统一为河北。只有这两个省份扣除对应包裹全部运费,其他省份不扣,不套用快递扣款比例,也不要求匹配首发快递。
|
||||
|
||||
未匹配商品、商品未维护供应商、未匹配供应商档案、联系人省缺失、供应商或省份冲突、运费无效,均列为待核实,扣款额为零。同一条形码的多条商品记录若供应商相同,或同一供应商多份档案的省份经上述归一后一致,只计算一笔运费。零运费列为不扣款。金额按整数分汇总,页面和导出口径一致。
|
||||
|
||||
默认展示可扣款,可切换全部、不扣款、待核实;支持月份、交易单号、快递单号、供应商、处理后条形码、联系人省筛选及每页50条翻页。上方汇总统计当前文字筛选范围内全部状态。列设置支持隐藏、拖动排序、恢复默认,按用户和公司存于浏览器;CSV导出覆盖当前状态及筛选结果全部行,按可见列顺序导出,额外保留月份、发货记录ID和仓库供核对,长单号按文本保护并转义公式字符。
|
||||
|
||||
只读取现有共享镜像,不新增数据库表、不写回商品、供应商或发货数据,不自动进入供应商应付汇总或执行财务入账。页面与导出沿用供应商应付页级权限。
|
||||
|
||||
验证:`SupplierAccessoryFreightTest` 覆盖四种省名、冲突、缺失资料、异常/零运费、真实SQLite关联、翻页汇总、过滤、CSV文本与公式保护、列顺序及不完整数据错误路径;同时运行配件补发扣款测试,验证共享包裹读取重用后的行为。
|
||||
@ -69,6 +69,21 @@ class ShippingOrderRepository
|
||||
|
||||
/** 仅批量读取本月配件及其交易单号对应的三个月首发候选;复用发货数据合并口径。 */
|
||||
public function accessoryReshipBatches(string $monthStart, string $nextMonth, string $windowStart): \Generator
|
||||
{
|
||||
foreach ($this->accessoryFreightBatches($monthStart, $nextMonth) as $accessories) {
|
||||
$trades = array_values(array_unique(array_filter(array_column($accessories, 'trade_no'), static fn ($trade) => trim((string) $trade) !== '')));
|
||||
$originals = $trades === [] ? [] : $this->db->table('jst_oms_shipping')
|
||||
->where('warehouse_name', '<>', \Modules\EcommerceFinance\Services\AccessoryReshipMatcher::WAREHOUSE)
|
||||
->where('ship_time', '>=', $windowStart)->where('ship_time', '<', $nextMonth)
|
||||
->whereIn('trade_no', $trades)->whereNotNull('express_no')->where('express_no', '<>', '')
|
||||
->select(['trade_no', 'barcode', 'express_no', 'express_name1', 'express_name2', 'ship_time', 'warehouse_name'])
|
||||
->get()->map(fn ($row) => (array) $row)->all();
|
||||
yield compact('accessories', 'originals');
|
||||
}
|
||||
}
|
||||
|
||||
/** 常州配件按发货包裹合并,运费只累计一次;两个扣款明细共用同一口径。 */
|
||||
public function accessoryFreightBatches(string $monthStart, string $nextMonth): \Generator
|
||||
{
|
||||
$last = null;
|
||||
while (true) {
|
||||
@ -80,14 +95,7 @@ class ShippingOrderRepository
|
||||
$select = 'source_id, MAX(express_no) as express_no, MAX(trade_no) as trade_no, MAX(express_name1) as express_name1, MAX(express_name2) as express_name2, MAX(ship_time) as ship_time, SUM(freight) as freight, GROUP_CONCAT(DISTINCT barcode ORDER BY id SEPARATOR \',\' ) as barcode';
|
||||
$accessories = $query->groupBy('source_id')->selectRaw($select)->orderBy('source_id')->limit(1000)->get()->map(fn ($row) => (array) $row)->all();
|
||||
if ($accessories === []) return;
|
||||
$trades = array_values(array_unique(array_filter(array_column($accessories, 'trade_no'), static fn ($trade) => trim((string) $trade) !== '')));
|
||||
$originals = $trades === [] ? [] : $this->db->table('jst_oms_shipping')
|
||||
->where('warehouse_name', '<>', \Modules\EcommerceFinance\Services\AccessoryReshipMatcher::WAREHOUSE)
|
||||
->where('ship_time', '>=', $windowStart)->where('ship_time', '<', $nextMonth)
|
||||
->whereIn('trade_no', $trades)->whereNotNull('express_no')->where('express_no', '<>', '')
|
||||
->select(['trade_no', 'barcode', 'express_no', 'express_name1', 'express_name2', 'ship_time', 'warehouse_name'])
|
||||
->get()->map(fn ($row) => (array) $row)->all();
|
||||
yield compact('accessories', 'originals');
|
||||
yield $accessories;
|
||||
$last = $accessories[array_key_last($accessories)]['source_id'];
|
||||
}
|
||||
}
|
||||
|
||||
@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Repositories;
|
||||
|
||||
use Modules\EcommerceFinance\Services\AccessoryReshipMatcher;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher;
|
||||
use RuntimeException;
|
||||
|
||||
/** 只读现有共享镜像,不使用发货行的历史供应商字段代替商品资料。 */
|
||||
class SupplierAccessoryFreightRepository
|
||||
{
|
||||
public function __construct(
|
||||
private readonly ShippingOrderRepository $shipping,
|
||||
private readonly BusinessMirrorDatasetRegistry $datasets,
|
||||
private readonly GenericBusinessMirrorRepository $mirror,
|
||||
private readonly SupplierAccessoryFreightMatcher $matcher,
|
||||
) {}
|
||||
|
||||
public function rows(string $month): \Generator
|
||||
{
|
||||
$productDataset = $this->datasets->get(ProductCatalogMirrorRepository::DATASET_KEY);
|
||||
$supplierDataset = $this->datasets->get(SupplierManagementMirrorRepository::DATASET);
|
||||
if (!$this->mirror->ready($productDataset) || !$this->mirror->ready($supplierDataset)) {
|
||||
throw new RuntimeException('商品或供应商资料尚未完成首次同步');
|
||||
}
|
||||
$db = $this->mirror->connection();
|
||||
// 供应商档案数量较小,一次读取,批量商品查询避免逐单访问数据库。
|
||||
$suppliers = $db->table($supplierDataset->targetTable())->get(['scmname', 'linkerprovience'])
|
||||
->groupBy(static fn ($row) => trim((string) $row->scmname))
|
||||
->map(static fn ($group) => $group->map(static fn ($row) => (array) $row)->all());
|
||||
$start = \Carbon\Carbon::createFromFormat('!Y-m', $month);
|
||||
foreach ($this->shipping->accessoryFreightBatches($start->format('Y-m-d'), $start->copy()->addMonth()->format('Y-m-d')) as $batch) {
|
||||
$barcodes = array_values(array_unique(array_filter(array_map(static fn ($row) => AccessoryReshipMatcher::barcode((string) ($row['barcode'] ?? '')), $batch), static fn ($value) => $value !== '')));
|
||||
$products = $db->table($productDataset->targetTable())->where('sku_type', 'normal')->whereIn('sku_id', $barcodes)
|
||||
->get(['sku_id', 'name', 'supplier_name'])->groupBy(static fn ($row) => trim((string) $row->sku_id))
|
||||
->map(static fn ($group) => $group->map(static fn ($row) => (array) $row)->all());
|
||||
foreach ($batch as $row) {
|
||||
$barcode = AccessoryReshipMatcher::barcode((string) ($row['barcode'] ?? ''));
|
||||
$candidates = $products->get($barcode, []);
|
||||
$supplierName = trim((string) ($candidates[0]['supplier_name'] ?? ''));
|
||||
yield $this->matcher->match($row, $candidates, $suppliers->get($supplierName, []));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,50 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\Services;
|
||||
|
||||
class SupplierAccessoryFreightMatcher
|
||||
{
|
||||
public const STATES = ['ready' => '可扣款', 'excluded' => '不扣款', 'pending' => '待核实'];
|
||||
|
||||
public function match(array $row, array $products, array $suppliers): array
|
||||
{
|
||||
$barcode = AccessoryReshipMatcher::barcode((string) ($row['barcode'] ?? ''));
|
||||
$row = array_merge($row, ['normalized_barcode' => $barcode, 'product_name' => '', 'supplier_name' => '',
|
||||
'province' => '', 'freight_cents' => AccessoryReshipMatcher::cents($row['freight'] ?? null),
|
||||
'deduction_cents' => 0, 'state' => 'pending', 'reason' => '未匹配到商品资料']);
|
||||
if ($barcode === '' || $products === []) return $row;
|
||||
$names = array_values(array_unique(array_map(static fn ($product) => trim((string) ($product['supplier_name'] ?? '')), $products)));
|
||||
$row['product_name'] = implode(' / ', array_unique(array_column($products, 'name')));
|
||||
$row['supplier_name'] = implode(' / ', $names);
|
||||
if (count($names) !== 1 || $names[0] === '') {
|
||||
$row['reason'] = count($names) > 1 ? '同一条形码对应多个供应商' : '商品资料未维护供应商';
|
||||
return $row;
|
||||
}
|
||||
if ($suppliers === []) {
|
||||
$row['reason'] = '未匹配到供应商管理资料';
|
||||
return $row;
|
||||
}
|
||||
$provinces = array_values(array_unique(array_map(static function ($supplier) {
|
||||
$province = trim((string) ($supplier['linkerprovience'] ?? ''));
|
||||
return match ($province) { '山东省' => '山东', '河北省' => '河北', default => $province };
|
||||
}, $suppliers)));
|
||||
$row['province'] = implode(' / ', array_filter($provinces, static fn ($value) => $value !== ''));
|
||||
if (count($provinces) !== 1 || $provinces[0] === '') {
|
||||
$row['reason'] = count($provinces) > 1 ? '供应商联系人省存在冲突' : '供应商未维护联系人省';
|
||||
return $row;
|
||||
}
|
||||
if (!in_array($provinces[0], ['山东', '河北'], true)) {
|
||||
$row['state'] = 'excluded';
|
||||
$row['reason'] = '联系人省不属于山东、河北';
|
||||
return $row;
|
||||
}
|
||||
if ($row['freight_cents'] === null) {
|
||||
$row['reason'] = '运费无效,需核实';
|
||||
return $row;
|
||||
}
|
||||
$row['state'] = $row['freight_cents'] > 0 ? 'ready' : 'excluded';
|
||||
$row['deduction_cents'] = $row['freight_cents'];
|
||||
$row['reason'] = $row['freight_cents'] > 0 ? '按山东、河北供应商扣除全部配件运费' : '运费为零,不扣款';
|
||||
return $row;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,81 @@
|
||||
<?php
|
||||
|
||||
namespace Modules\EcommerceFinance\View;
|
||||
|
||||
use Illuminate\Pagination\LengthAwarePaginator;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierAccessoryFreightRepository;
|
||||
use Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher;
|
||||
|
||||
class SupplierAccessoryFreightPageData
|
||||
{
|
||||
public const TITLE = '新河北仓、曹县仓配件运费扣除明细';
|
||||
public const TEXT_FILTERS = ['trade_no' => '交易单号', 'express_no' => '快递单号', 'supplier_name' => '供应商', 'normalized_barcode' => '处理后条形码', 'province' => '联系人省'];
|
||||
public const COLUMNS = ['state' => '扣款状态', 'trade_no' => '交易单号', 'express_no' => '快递单号', 'courier' => '快递名称', 'ship_time' => '发货时间', 'barcode' => '原始条形码', 'normalized_barcode' => '处理后条形码', 'product_name' => '商品名称', 'supplier_name' => '供应商', 'province' => '联系人省', 'freight' => '配件运费(元)', 'deduction' => '扣款金额(元)', 'reason' => '说明'];
|
||||
|
||||
public function exportColumns(): array
|
||||
{
|
||||
$requested = request()->query('columns');
|
||||
if ($requested === null) return self::COLUMNS;
|
||||
abort_unless(is_string($requested) && strlen($requested) <= 500, 422, '导出列无效。');
|
||||
$keys = explode(',', $requested);
|
||||
abort_unless($keys !== [] && count($keys) === count(array_unique($keys)) && array_diff($keys, array_keys(self::COLUMNS)) === [], 422, '导出列无效。');
|
||||
return array_replace(array_flip($keys), array_intersect_key(self::COLUMNS, array_flip($keys)));
|
||||
}
|
||||
|
||||
public function __construct(private readonly SupplierAccessoryFreightRepository $repository) {}
|
||||
|
||||
public function filters(): array
|
||||
{
|
||||
$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, '请选择有效业务月份。');
|
||||
$state = request()->query('state', 'ready');
|
||||
abort_unless(is_string($state) && in_array($state, ['all', ...array_keys(SupplierAccessoryFreightMatcher::STATES)], true), 422, '扣款状态无效。');
|
||||
$filters = compact('month', 'state');
|
||||
foreach (self::TEXT_FILTERS as $key => $label) {
|
||||
$value = request()->query($key, '');
|
||||
abort_unless(is_string($value) && mb_strlen($value) <= 200, 422, '筛选条件格式无效。');
|
||||
$filters[$key] = trim($value);
|
||||
}
|
||||
$filters['province'] = match ($filters['province']) { '山东省' => '山东', '河北省' => '河北', default => $filters['province'] };
|
||||
return $filters;
|
||||
}
|
||||
|
||||
public function rows(array $filters): \Generator
|
||||
{
|
||||
foreach ($this->repository->rows($filters['month']) as $row) {
|
||||
foreach (self::TEXT_FILTERS as $key => $label) {
|
||||
if (($filters[$key] ?? '') !== '' && mb_stripos((string) ($row[$key] ?? ''), $filters[$key]) === false) continue 2;
|
||||
}
|
||||
yield $row;
|
||||
}
|
||||
}
|
||||
|
||||
public function page(): array
|
||||
{
|
||||
$filters = $this->filters();
|
||||
$page = max(1, (int) request('page', 1));
|
||||
$summary = ['all' => 0, 'ready' => 0, 'excluded' => 0, 'pending' => 0, 'amount' => 0];
|
||||
$items = [];
|
||||
$total = 0;
|
||||
$error = null;
|
||||
try {
|
||||
foreach ($this->rows($filters) as $row) {
|
||||
$summary['all']++;
|
||||
$summary[$row['state']]++;
|
||||
$summary['amount'] += $row['deduction_cents'];
|
||||
if ($filters['state'] !== 'all' && $filters['state'] !== $row['state']) continue;
|
||||
if ($total >= ($page - 1) * 50 && count($items) < 50) $items[] = $row;
|
||||
$total++;
|
||||
}
|
||||
} catch (\Throwable $exception) {
|
||||
report($exception);
|
||||
$error = '发货、商品或供应商资料暂时无法读取,或尚未完成同步,请稍后重试。';
|
||||
$summary = array_fill_keys(array_keys($summary), 0);
|
||||
$items = [];
|
||||
$total = 0;
|
||||
}
|
||||
return ['view' => 'business.pages.supplier-payables.accessory-freight-deductions',
|
||||
'freightFilters' => $filters, 'freightSummary' => $summary, 'freightError' => $error,
|
||||
'freightRows' => new LengthAwarePaginator($items, $total, 50, $page, ['path' => request()->url(), 'query' => request()->except('page')])];
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
@php
|
||||
$states = \Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher::STATES;
|
||||
$title = \Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData::TITLE;
|
||||
@endphp
|
||||
<div class="supplier-freight-page" data-preference-key="supplier-accessory-freight:{{ company_id() }}:{{ optional(user())->id }}">
|
||||
<div class="sf-heading"><p>常州配件发货运费,按商品供应商的联系人省归属核算。</p><span class="sf-tag">按月核算</span></div>
|
||||
<form method="get" class="sf-toolbar">
|
||||
<label>业务月份<input type="month" name="month" value="{{ $freightFilters['month'] }}" required></label>
|
||||
<label>扣款状态<select name="state"><option value="all" @selected($freightFilters['state'] === 'all')>全部记录</option>@foreach($states as $key => $label)<option value="{{ $key }}" @selected($freightFilters['state'] === $key)>{{ $label }}</option>@endforeach</select></label>
|
||||
@foreach(\Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData::TEXT_FILTERS as $key => $label)
|
||||
<label>{{ $label }}<input type="text" name="{{ $key }}" value="{{ $freightFilters[$key] }}" maxlength="200" placeholder="筛选{{ $label }}"></label>
|
||||
@endforeach
|
||||
<button type="submit" class="sf-primary">查询</button>
|
||||
<a class="sf-button" href="{{ request()->url() }}?month={{ $freightFilters['month'] }}">重置</a>
|
||||
<a class="sf-button" data-sf-export href="{{ route('business.supplier-accessory-freight.export', $freightFilters) }}">导出当前筛选</a>
|
||||
<details class="sf-column-settings"><summary class="sf-button">列设置</summary><div class="sf-column-menu"><p>勾选显示,拖动调整顺序</p><div data-sf-column-list>@foreach(\Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData::COLUMNS as $key => $label)<label draggable="true" data-column-key="{{ $key }}"><input type="checkbox" value="{{ $key }}" checked> {{ $label }}</label>@endforeach</div><button type="button" data-sf-reset>恢复默认</button><span data-sf-column-message role="status"></span></div></details>
|
||||
</form>
|
||||
<div class="sf-metrics">
|
||||
<div><span>筛选范围内配件发货</span><strong>{{ number_format($freightSummary['all']) }}</strong></div>
|
||||
<div><span>可扣款 / 不扣款</span><strong>{{ number_format($freightSummary['ready']) }} <small>/ {{ number_format($freightSummary['excluded']) }}</small></strong></div>
|
||||
<div><span>待核实</span><strong>{{ number_format($freightSummary['pending']) }}</strong></div>
|
||||
<div><span>可扣运费合计(元)</span><strong>{{ number_format($freightSummary['amount'] / 100, 2) }}</strong></div>
|
||||
</div>
|
||||
<details class="sf-rules"><summary>扣款规则与金额口径</summary><p>仅取所选月份仓库为“常州简尚家居有限公司”的发货包裹。取第一个逗号前的条形码,再去掉最后一个“-”及其后缀,用处理后的条形码匹配商品资料条形码,再按商品供应商名称匹配供应商管理。联系人省为山东、山东省、河北、河北省的供应商,扣除对应包裹全部运费。每个发货包裹只计一次;不需要匹配首发单。其他省份不扣,未匹配、资料缺失、供应商或省份冲突、运费异常的记录待核实。此处展示扣除明细,不自动入账。</p></details>
|
||||
@if($freightError)<div class="sf-error" role="alert">{{ $freightError }}</div>@endif
|
||||
<section class="sf-panel" aria-label="配件运费扣除明细">
|
||||
<div class="sf-table-scroll"><table><thead><tr>
|
||||
@foreach(\Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData::COLUMNS as $key => $column)<th data-column-key="{{ $key }}">{{ $column }}</th>@endforeach
|
||||
</tr></thead><tbody>
|
||||
@forelse($freightRows as $row)
|
||||
<tr><td><span class="sf-status {{ $row['state'] }}">{{ $states[$row['state']] }}</span></td><td>{{ $row['trade_no'] ?: '—' }}</td><td>{{ $row['express_no'] ?: '—' }}</td><td>{{ ($row['express_name1'] ?? '') ?: ($row['express_name2'] ?? '') ?: '—' }}</td><td>{{ $row['ship_time'] }}</td><td class="sf-barcode">{{ $row['barcode'] ?: '—' }}</td><td class="sf-barcode">{{ $row['normalized_barcode'] ?: '—' }}</td><td>{{ $row['product_name'] ?: '—' }}</td><td>{{ $row['supplier_name'] ?: '—' }}</td><td>{{ $row['province'] ?: '—' }}</td><td class="sf-money">{{ $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2) }}</td><td class="sf-money">{{ number_format($row['deduction_cents'] / 100, 2) }}</td><td>{{ $row['reason'] }}</td></tr>
|
||||
@empty
|
||||
<tr><td colspan="13" class="sf-empty">{{ $freightError ?: '当前筛选条件下暂无记录,请调整月份、状态或筛选条件。' }}</td></tr>
|
||||
@endforelse
|
||||
</tbody></table></div>
|
||||
<div class="sf-footer">当前状态共 {{ number_format($freightRows->total()) }} 条,每页50条。上方汇总包含当前文字筛选范围内的全部状态。{{ $freightRows->links() }}</div>
|
||||
</section>
|
||||
</div>
|
||||
<style>
|
||||
.supplier-freight-page{color:#171717;font-size:13px}.sf-heading{display:flex;justify-content:space-between;align-items:center;margin-bottom:16px}.sf-heading h2{font-size:16px;font-weight:600;margin:0 0 6px}.sf-heading p{font-size:12px;color:#595959;margin:0}.sf-tag,.sf-status{display:inline-block;border-radius:4px;padding:4px 8px;background:#F7F0DE;color:#856632;white-space:nowrap;font-size:12px}.sf-toolbar{display:flex;flex-wrap:wrap;align-items:flex-end;gap:12px;margin:16px 0}.sf-toolbar label{display:grid;gap:6px;font-size:12px}.sf-toolbar input,.sf-toolbar select,.sf-toolbar button,.sf-button{height:34px;border:1px solid #D8D2C7;border-radius:8px;background:#fff;color:#171717;padding:0 10px}.sf-toolbar input[type=text]{width:155px}.sf-toolbar .sf-primary{background:#856632;border-color:#856632;color:#fff}.sf-button{display:inline-flex;align-items:center;text-decoration:none}.sf-metrics{display:grid;grid-template-columns:repeat(4,minmax(0,1fr));gap:12px;margin:16px 0}.sf-metrics>div{background:#fff;border:1px solid #D8D2C7;border-radius:12px;padding:16px}.sf-metrics span{color:#595959;font-size:12px}.sf-metrics strong{display:block;font-size:22px;margin-top:8px;font-variant-numeric:tabular-nums}.sf-metrics small{font-size:15px;font-weight:400;color:#595959}.sf-metrics>div:last-child strong{color:#856632}.sf-rules{font-size:12px;color:#595959;margin:12px 0}.sf-rules summary{cursor:pointer;color:#856632}.sf-rules p{line-height:1.8;margin:8px 0}.sf-error{background:#F7F0DE;color:#B80000;padding:12px;border-radius:8px;margin:12px 0}.sf-panel{background:#fff;border:1px solid #D8D2C7;border-radius:12px;overflow:hidden}.sf-table-scroll{overflow:auto;max-height:65vh}.sf-panel table{width:100%;border-collapse:collapse;font-size:12px}.sf-panel th,.sf-panel td{padding:12px;border-bottom:1px solid #D8D2C7;text-align:left;vertical-align:top}.sf-panel th{background:#F7F0DE;color:#595959;font-weight:500;white-space:nowrap;position:sticky;top:0;z-index:1}.sf-panel td{min-width:110px;overflow-wrap:anywhere}.sf-panel .sf-barcode{min-width:200px;max-width:300px}.sf-panel .sf-money{text-align:right;white-space:nowrap;font-variant-numeric:tabular-nums}.sf-status.ready{background:#eef3e9;color:#53793E}.sf-status.excluded{background:#f1efeb;color:#595959}.sf-status.pending{color:#935F07}.sf-panel .sf-empty{text-align:center;padding:60px 16px;color:#595959}.sf-footer{padding:12px 16px;font-size:12px;color:#595959}.sf-footer nav{margin-top:12px}@media(max-width:700px){.sf-metrics{grid-template-columns:repeat(2,minmax(0,1fr))}.sf-heading{align-items:flex-start;gap:12px}.sf-tag{flex-shrink:0}.sf-heading h2{line-height:1.6}}
|
||||
</style>
|
||||
<style>.sf-column-settings{position:relative}.sf-column-settings summary{cursor:pointer;list-style:none}.sf-column-menu{position:absolute;top:40px;right:0;width:240px;padding:12px;background:#fff;border:1px solid #D8D2C7;border-radius:8px;z-index:10;box-shadow:0 4px 16px #17171714}.sf-column-menu p{font-size:12px;color:#595959;margin:0 0 8px}.sf-column-menu [data-sf-column-list]{max-height:340px;overflow:auto}.sf-column-menu label{display:flex;align-items:center;gap:6px;padding:6px;cursor:grab}.sf-column-menu input{height:auto;accent-color:#856632}.sf-column-menu button{margin-top:8px}.sf-column-menu [role=status]{display:block;font-size:12px;color:#935F07}.sf-toolbar :focus-visible{outline:2px solid #856632;outline-offset:2px}.sf-panel tbody tr:hover{background:#faf7ef}</style>
|
||||
@push('body_end')
|
||||
<script>
|
||||
(() => {
|
||||
const root = document.querySelector('.supplier-freight-page');
|
||||
if (!root) return;
|
||||
const list = root.querySelector('[data-sf-column-list]');
|
||||
const defaults = Array.from(list.children);
|
||||
const keys = defaults.map(item => item.dataset.columnKey);
|
||||
const tableRows = Array.from(root.querySelectorAll('table tr')).filter(row => row.children.length === keys.length && !row.querySelector('[colspan]'));
|
||||
const cells = tableRows.map(row => Object.fromEntries(keys.map((key, i) => [key, row.children[i]])));
|
||||
const storageKey = root.dataset.preferenceKey;
|
||||
const scrollArea = root.querySelector('.sf-table-scroll');
|
||||
const fitTable = () => scrollArea.style.maxHeight = Math.max(180, window.innerHeight - scrollArea.getBoundingClientRect().top - 80) + 'px';
|
||||
fitTable(); window.addEventListener('resize', fitTable);
|
||||
const apply = (persist = true) => {
|
||||
const order = Array.from(list.children).map(item => item.dataset.columnKey);
|
||||
const visible = Array.from(list.querySelectorAll('input:checked')).map(input => input.value);
|
||||
tableRows.forEach((row, i) => order.forEach(key => { cells[i][key].hidden = !visible.includes(key); row.append(cells[i][key]); }));
|
||||
root.querySelectorAll('td[colspan]').forEach(cell => cell.colSpan = visible.length);
|
||||
const link = root.querySelector('[data-sf-export]');
|
||||
const url = new URL(link.href); url.searchParams.set('columns', order.filter(key => visible.includes(key)).join(',')); link.href = url.toString();
|
||||
if (persist) try { localStorage.setItem(storageKey, JSON.stringify({order, visible})); } catch (_) {}
|
||||
};
|
||||
try {
|
||||
const saved = JSON.parse(localStorage.getItem(storageKey));
|
||||
if (saved && Array.isArray(saved.order) && Array.isArray(saved.visible) && new Set(saved.order).size === keys.length && saved.order.length === keys.length && saved.order.every(key => keys.includes(key)) && saved.visible.some(key => keys.includes(key))) {
|
||||
saved.order.forEach(key => list.append(defaults.find(item => item.dataset.columnKey === key)));
|
||||
list.querySelectorAll('input').forEach(input => input.checked = saved.visible.includes(input.value));
|
||||
}
|
||||
} catch (_) {}
|
||||
apply(false);
|
||||
list.addEventListener('change', event => {
|
||||
if (!list.querySelector('input:checked')) { event.target.checked = true; root.querySelector('[data-sf-column-message]').textContent = '请至少保留一列。'; return; }
|
||||
root.querySelector('[data-sf-column-message]').textContent = ''; apply();
|
||||
});
|
||||
let dragging = null;
|
||||
list.addEventListener('dragstart', event => { dragging = event.target.closest('[data-column-key]'); if (dragging) { event.dataTransfer.setData('text/plain', dragging.dataset.columnKey); event.dataTransfer.effectAllowed = 'move'; } });
|
||||
list.addEventListener('dragover', event => event.preventDefault());
|
||||
list.addEventListener('drop', event => { event.preventDefault(); const target = event.target.closest('[data-column-key]'); if (dragging && target && dragging !== target) { list.insertBefore(dragging, target); apply(); } dragging = null; });
|
||||
root.querySelector('[data-sf-reset]').addEventListener('click', () => { defaults.forEach(item => { item.querySelector('input').checked = true; list.append(item); }); apply(); });
|
||||
})();
|
||||
</script>
|
||||
@endpush
|
||||
@ -465,6 +465,10 @@ Route::get('workspace/courier-payables/accessory-reship-deductions/export', [\Ap
|
||||
->middleware('business.page:courier-payables,accessory-reship-deductions')
|
||||
->name('business.accessory-reship-deductions.export');
|
||||
|
||||
Route::get('workspace/supplier-payables/accessory-freight-deductions/export', [\App\Http\Controllers\Business\SupplierAccessoryFreight::class, 'export'])
|
||||
->middleware('business.page:supplier-payables,accessory-freight-deductions')
|
||||
->name('business.supplier-accessory-freight.export');
|
||||
|
||||
Route::get('workspace/courier-payables/shipping-data/export', 'Business\Pages@exportShippingData')
|
||||
->middleware('business.page:courier-payables,shipping-data')
|
||||
->name('business.courier-payables.shipping-data.export');
|
||||
|
||||
205
tests/Unit/SupplierAccessoryFreightTest.php
Normal file
205
tests/Unit/SupplierAccessoryFreightTest.php
Normal file
@ -0,0 +1,205 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit;
|
||||
|
||||
use Illuminate\Config\Repository;
|
||||
use Illuminate\Database\Capsule\Manager;
|
||||
use Illuminate\Foundation\Application;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Facade;
|
||||
use Modules\EcommerceFinance\Contracts\BusinessMirrorDataset;
|
||||
use Modules\EcommerceFinance\Repositories\GenericBusinessMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ProductCatalogMirrorRepository;
|
||||
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierAccessoryFreightRepository;
|
||||
use Modules\EcommerceFinance\Repositories\SupplierManagementMirrorRepository;
|
||||
use Modules\EcommerceFinance\Services\AccessoryReshipMatcher;
|
||||
use Modules\EcommerceFinance\Services\BusinessMirrorDatasetRegistry;
|
||||
use Modules\EcommerceFinance\Services\SupplierAccessoryFreightMatcher;
|
||||
use Modules\EcommerceFinance\View\SupplierAccessoryFreightPageData;
|
||||
use Mockery;
|
||||
use PHPUnit\Framework\TestCase;
|
||||
|
||||
foreach (['Contracts/BusinessMirrorDataset', 'Repositories/GenericBusinessMirrorRepository', 'Repositories/ShippingOrderRepository', 'Repositories/ProductCatalogMirrorRepository', 'Repositories/SupplierManagementMirrorRepository', 'Repositories/SupplierAccessoryFreightRepository', 'Services/BusinessMirrorDatasetRegistry', 'Services/AccessoryReshipMatcher', 'Services/SupplierAccessoryFreightMatcher', 'View/SupplierAccessoryFreightPageData'] as $file) {
|
||||
require_once dirname(__DIR__, 2) . '/modules/EcommerceFinance/' . $file . '.php';
|
||||
}
|
||||
require_once dirname(__DIR__, 2) . '/app/Http/Controllers/Business/SupplierAccessoryFreight.php';
|
||||
|
||||
class SupplierAccessoryFreightTest extends TestCase
|
||||
{
|
||||
protected function setUp(): void
|
||||
{
|
||||
$app = new Application(dirname(__DIR__, 2));
|
||||
$app->instance('config', new Repository(['app' => ['platform_timezone' => 'Asia/Shanghai']]));
|
||||
$db = new Manager($app);
|
||||
$db->addConnection(['driver' => 'sqlite', 'database' => ':memory:', 'prefix' => '']);
|
||||
$app->instance('db', $db->getDatabaseManager());
|
||||
Facade::clearResolvedInstances();
|
||||
Facade::setFacadeApplication($app);
|
||||
$app->instance('request', Request::create('/?month=2026-09&state=all'));
|
||||
$app->instance(\Illuminate\Contracts\Routing\ResponseFactory::class, new \Illuminate\Routing\ResponseFactory(
|
||||
Mockery::mock(\Illuminate\Contracts\View\Factory::class), Mockery::mock(\Illuminate\Routing\Redirector::class)
|
||||
));
|
||||
}
|
||||
|
||||
protected function tearDown(): void { Mockery::close(); parent::tearDown(); }
|
||||
|
||||
private function row(array $patch = []): array
|
||||
{
|
||||
return array_replace(['source_id' => 1, 'trade_no' => '123456789012345678', 'express_no' => 'BF001', 'express_name1' => '补发快递',
|
||||
'ship_time' => '2026-09-15', 'barcode' => 'GBGJ-SJ038024N1410V02-10,GBGJ-SJ038024N1410V02-2', 'freight' => '12.35'], $patch);
|
||||
}
|
||||
|
||||
public function test_four_province_spellings_deduct_full_freight_once(): void
|
||||
{
|
||||
foreach (['山东', '山东省', '河北', '河北省'] as $province) {
|
||||
$result = (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name' => '成品', 'supplier_name' => '供应商甲'], ['name' => '成品', 'supplier_name' => '供应商甲']], [['linkerprovience' => $province]]);
|
||||
$this->assertSame('GBGJ-SJ038024N1410V02', $result['normalized_barcode']);
|
||||
$this->assertSame('ready', $result['state']);
|
||||
$this->assertSame(1235, $result['deduction_cents']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_missing_or_ambiguous_product_supplier_province_never_deducts(): void
|
||||
{
|
||||
$product = [['name' => '成品', 'supplier_name' => '甲']];
|
||||
foreach ([ [[], []], [[['name' => '成品', 'supplier_name' => '']], []], [$product, []],
|
||||
[[...$product, ['name' => '成品', 'supplier_name' => '乙']], [['linkerprovience' => '山东']]],
|
||||
[$product, [['linkerprovience' => '']]], [$product, [['linkerprovience' => '山东'], ['linkerprovience' => '河北']]],
|
||||
[$product, [['linkerprovience' => '山东'], ['linkerprovience' => '']]] ] as [$products, $suppliers]) {
|
||||
$result = (new SupplierAccessoryFreightMatcher)->match($this->row(), $products, $suppliers);
|
||||
$this->assertSame('pending', $result['state']);
|
||||
$this->assertSame(0, $result['deduction_cents']);
|
||||
}
|
||||
}
|
||||
|
||||
public function test_other_provinces_zero_invalid_and_equivalent_duplicate_provinces(): void
|
||||
{
|
||||
$matcher = new SupplierAccessoryFreightMatcher;
|
||||
$products = [['name' => '成品', 'supplier_name' => '甲']];
|
||||
foreach (['江苏省', '山东济南', '河北省石家庄市'] as $province) {
|
||||
$result = $matcher->match($this->row(), $products, [['linkerprovience' => $province]]);
|
||||
$this->assertSame('excluded', $result['state']);
|
||||
$this->assertSame(0, $result['deduction_cents']);
|
||||
}
|
||||
foreach ([null, '', '-5', 'abc'] as $amount) {
|
||||
$result = $matcher->match($this->row(['freight' => $amount]), $products, [['linkerprovience' => '河北']]);
|
||||
$this->assertSame('pending', $result['state']);
|
||||
$this->assertSame(0, $result['deduction_cents']);
|
||||
}
|
||||
$result = $matcher->match($this->row(['freight' => '0']), $products, [['linkerprovience' => '河北']]);
|
||||
$this->assertSame('excluded', $result['state']);
|
||||
$result = $matcher->match($this->row(['freight' => '0.10']), $products, [['linkerprovience' => ' 山东 '], ['linkerprovience' => '山东省']]);
|
||||
$this->assertSame('ready', $result['state']);
|
||||
$this->assertSame(10, $result['deduction_cents']);
|
||||
}
|
||||
|
||||
public function test_repository_uses_product_supplier_not_shipping_supplier_and_ignores_combinations(): void
|
||||
{
|
||||
$connection = app('db')->connection();
|
||||
$connection->statement('CREATE TABLE products (sku_id TEXT, sku_type TEXT, name TEXT, supplier_name TEXT)');
|
||||
$connection->statement('CREATE TABLE suppliers (scmname TEXT, linkerprovience TEXT)');
|
||||
$connection->table('products')->insert([
|
||||
['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'normal', 'name' => '成品', 'supplier_name' => '商品供应商'],
|
||||
['sku_id' => 'GBGJ-SJ038024N1410V02', 'sku_type' => 'combine', 'name' => '组合', 'supplier_name' => '其他供应商'],
|
||||
]);
|
||||
$connection->table('suppliers')->insert(['scmname' => '商品供应商', 'linkerprovience' => '河北省']);
|
||||
$registry = Mockery::mock(BusinessMirrorDatasetRegistry::class);
|
||||
foreach ([ProductCatalogMirrorRepository::DATASET_KEY => 'products', SupplierManagementMirrorRepository::DATASET => 'suppliers'] as $key => $table) {
|
||||
$dataset = Mockery::mock(BusinessMirrorDataset::class);
|
||||
$dataset->shouldReceive('targetTable')->andReturn($table);
|
||||
$registry->shouldReceive('get')->with($key)->andReturn($dataset);
|
||||
}
|
||||
$mirror = Mockery::mock(GenericBusinessMirrorRepository::class);
|
||||
$mirror->shouldReceive('ready')->andReturnTrue();
|
||||
$mirror->shouldReceive('connection')->andReturn($connection);
|
||||
$shipping = Mockery::mock(ShippingOrderRepository::class);
|
||||
$shipping->shouldReceive('accessoryFreightBatches')->once()->with('2026-09-01', '2026-10-01')->andReturn((function () { yield [$this->row(['supplier_name' => '错误的历史供应商'])]; })());
|
||||
$repo = new SupplierAccessoryFreightRepository($shipping, $registry, $mirror, new SupplierAccessoryFreightMatcher);
|
||||
$rows = iterator_to_array($repo->rows('2026-09'));
|
||||
$this->assertCount(1, $rows);
|
||||
$this->assertSame('商品供应商', $rows[0]['supplier_name']);
|
||||
$this->assertSame(1235, $rows[0]['deduction_cents']);
|
||||
}
|
||||
|
||||
private function data(): SupplierAccessoryFreightPageData
|
||||
{
|
||||
$repository = Mockery::mock(SupplierAccessoryFreightRepository::class);
|
||||
$repository->shouldReceive('rows')->with('2026-09')->andReturnUsing(function () {
|
||||
$matcher = new SupplierAccessoryFreightMatcher;
|
||||
for ($i = 0; $i < 105; $i++) yield $matcher->match($this->row(['source_id' => $i, 'freight' => '0.10']), [['name' => '=危险商品', 'supplier_name' => '山东供应商']], [['linkerprovience' => '山东']]);
|
||||
yield $matcher->match($this->row(['source_id' => 200]), [], []);
|
||||
});
|
||||
return new SupplierAccessoryFreightPageData($repository);
|
||||
}
|
||||
|
||||
public function test_summary_pagination_and_filters_use_full_month_without_multiplying_amount(): void
|
||||
{
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=all&page=2'));
|
||||
$page = $this->data()->page();
|
||||
$this->assertNull($page['freightError']);
|
||||
$this->assertCount(50, $page['freightRows']);
|
||||
$this->assertSame(106, $page['freightRows']->total());
|
||||
$this->assertSame(1050, $page['freightSummary']['amount']);
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=ready&supplier_name=山东&page=3'));
|
||||
$page = $this->data()->page();
|
||||
$this->assertCount(5, $page['freightRows']);
|
||||
$this->assertSame(105, $page['freightRows']->total());
|
||||
$this->assertSame(0, $page['freightSummary']['pending']);
|
||||
$this->assertSame(1050, $page['freightSummary']['amount']);
|
||||
}
|
||||
|
||||
public function test_csv_matches_filters_preserves_long_ids_and_escapes_formulas(): void
|
||||
{
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=pending'));
|
||||
$controller = new \App\Http\Controllers\Business\SupplierAccessoryFreight;
|
||||
$response = $controller->export($this->data());
|
||||
ob_start(); $response->sendContent(); $csv = ob_get_clean();
|
||||
$this->assertStringStartsWith("\xEF\xBB\xBF", $csv);
|
||||
$this->assertSame(2, substr_count($csv, "\n"));
|
||||
$this->assertStringContainsString("'123456789012345678", $csv);
|
||||
$this->assertStringContainsString('待核实', $csv);
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=ready'));
|
||||
$response = $controller->export($this->data());
|
||||
ob_start(); $response->sendContent(); $csv = ob_get_clean();
|
||||
$this->assertStringContainsString("'=危险商品", $csv);
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=ready&columns=deduction,supplier_name'));
|
||||
$response = $controller->export($this->data());
|
||||
ob_start(); $response->sendContent(); $csv = ob_get_clean();
|
||||
$lines = explode("\n", trim(substr($csv, 3)));
|
||||
$this->assertSame(['业务月份', '发货记录ID', '仓库', '扣款金额(元)', '供应商'], str_getcsv($lines[0]));
|
||||
$this->assertCount(5, str_getcsv($lines[1]));
|
||||
}
|
||||
|
||||
public function test_filters_accept_province_alias_and_reject_invalid_month_state_and_columns(): void
|
||||
{
|
||||
app()->instance('request', Request::create('/?month=2026-09&state=all&province=山东省'));
|
||||
$this->assertSame('山东', $this->data()->filters()['province']);
|
||||
foreach (['month=2026-13', 'month[]=2026-09', 'month=1999-01', 'month=2026-09&state=unknown', 'month=2026-09&supplier_name[]=x', 'month=2026-09&columns=', 'month=2026-09&columns=state,state', 'month=2026-09&columns=password'] as $query) {
|
||||
app()->instance('request', Request::create('/?' . $query));
|
||||
try {
|
||||
$this->data()->filters();
|
||||
$this->data()->exportColumns();
|
||||
$this->fail('应拒绝无效条件');
|
||||
} catch (\Symfony\Component\HttpKernel\Exception\HttpException $exception) {
|
||||
$this->assertSame(422, $exception->getStatusCode());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function test_data_error_clears_partial_rows_and_totals(): void
|
||||
{
|
||||
$repository = Mockery::mock(SupplierAccessoryFreightRepository::class);
|
||||
$repository->shouldReceive('rows')->andReturnUsing(function () {
|
||||
yield (new SupplierAccessoryFreightMatcher)->match($this->row(), [['name'=>'商品','supplier_name'=>'甲']], [['linkerprovience'=>'河北']]);
|
||||
throw new \RuntimeException('读取失败');
|
||||
});
|
||||
$handler = Mockery::mock(\Illuminate\Contracts\Debug\ExceptionHandler::class);
|
||||
$handler->shouldReceive('report')->once();
|
||||
app()->instance(\Illuminate\Contracts\Debug\ExceptionHandler::class, $handler);
|
||||
$page = (new SupplierAccessoryFreightPageData($repository))->page();
|
||||
$this->assertNotNull($page['freightError']);
|
||||
$this->assertSame(0, $page['freightRows']->total());
|
||||
$this->assertSame(0, $page['freightSummary']['amount']);
|
||||
}
|
||||
}
|
||||
Loading…
x
Reference in New Issue
Block a user