Merge remote-tracking branch 'origin/main' into feature/limeng-reship-courier-rates
# Conflicts: # docs/配件补发扣款.md # modules/EcommerceFinance/View/AccessoryReshipPageData.php # resources/views/business/pages/courier-payables/accessory-reship-deductions.blade.php
This commit is contained in:
commit
040ae99084
@ -27,3 +27,5 @@
|
||||
Excel导入列为快递代码、快递公司、创建时间、创建人、扣款比率(%)。原创建时间和创建人保留。导入前整表校验,写入及操作日志使用同一事务;相同代码、名称、比例再次导入跳过,冲突整批回滚,不自动覆盖或恢复已删除记录。
|
||||
|
||||
命令行导入:`php artisan accessory-reship:import-courier-configs <xlsx文件路径> --company=<公司ID>`。增加 `--dry-run` 仅校验,不写入。上传文件及真实配置不进入Git。
|
||||
|
||||
首发快递、首发单号、所属月份以独立三列显示,多候选按同一顺序对应。支持交易单号、补发单号、补发快递、补发日期、原始/处理后条码、补发运费、匹配状态、首发快递、首发单号、所属月份及可扣金额筛选;文本和金额支持包含匹配。筛选覆盖全部业务月记录,汇总、分类数量、分页和CSV导出沿用相同条件。多个首发筛选必须由同一候选满足;清空筛选保留业务月份。
|
||||
|
||||
@ -41,7 +41,7 @@ class PayablesFinalRegistry
|
||||
* - insert:锚点列之后插入的列(锚点列名 => 列定义);
|
||||
* - append:追加到末尾的列。
|
||||
* 货款对账在「月份」后补入库单号、采购单号,末尾补未发出、对账人;
|
||||
* 退货对账去掉「快递名称」,在「备注」后插入 wms 配货单号,在「标记多标签」后补创建人。
|
||||
* 退货对账去掉「快递名称」,在「标题」后插入 wms 配货单号,在「标记多标签」后补创建人。
|
||||
*/
|
||||
private const SUPPLIER_FIELD_PATCHES = [
|
||||
'payment-reconciliation' => [
|
||||
@ -57,7 +57,7 @@ class PayablesFinalRegistry
|
||||
'return-reconciliation' => [
|
||||
'remove' => ['express_name'],
|
||||
'insert' => [
|
||||
'remark' => ['wms_pick_no' => ['label' => 'wms配货单号', 'type' => 'text']],
|
||||
'title' => ['wms_pick_no' => ['label' => 'wms配货单号', 'type' => 'text']],
|
||||
'labels' => ['creator' => ['label' => '创建人', 'type' => 'text']],
|
||||
],
|
||||
],
|
||||
|
||||
@ -50,9 +50,9 @@ class AccessoryReshipPageData
|
||||
public function matchesFilters(array $row, array $filters): bool
|
||||
{
|
||||
$values = [
|
||||
'trade_no' => $row['trade_no'], 'express_no' => $row['express_no'],
|
||||
'courier' => $row['express_name1'] ?: ($row['express_name2'] ?? ''),
|
||||
'ship_date' => substr((string) $row['ship_time'], 0, 10),
|
||||
'trade_no' => $row['trade_no'], 'express_no' => ($row['express_no'] ?? ''),
|
||||
'courier' => ($row['express_name1'] ?? '') ?: ($row['express_name2'] ?? ''),
|
||||
'ship_date' => substr((string) ($row['ship_time'] ?? ''), 0, 10),
|
||||
'barcode' => $row['barcode'], 'normalized_barcode' => $row['normalized_barcode'],
|
||||
'freight' => $row['freight_cents'] === null ? '待核实' : number_format($row['freight_cents'] / 100, 2, '.', ''),
|
||||
'deduction_amount' => $row['deduction_cents'] === null ? '待核实' : number_format($row['deduction_cents'] / 100, 2, '.', ''),
|
||||
@ -79,7 +79,7 @@ class AccessoryReshipPageData
|
||||
}
|
||||
$filters = $this->filters();
|
||||
$page = max(1, (int) request('page', 1));
|
||||
$summary = ['all' => 0, 'matched' => 0, 'unmatched' => 0, 'amount' => 0, 'invalid_amount' => 0, 'ambiguous' => 0, 'ready' => 0, 'missing_config' => 0, 'conflicting_rates' => 0, 'selection_required' => 0, 'zero_rate' => 0];
|
||||
$summary = ['all' => 0, 'matched' => 0, 'unmatched' => 0, 'amount' => 0, 'invalid_amount' => 0, 'ambiguous' => 0, 'ready' => 0, 'missing_config' => 0, 'selection_required' => 0, 'zero_rate' => 0];
|
||||
$items = [];
|
||||
$total = 0;
|
||||
$error = null;
|
||||
|
||||
@ -15,13 +15,13 @@
|
||||
<div class="reship-heading"><div><h2>配件补发扣款</h2><p>按交易单号及条形码匹配首发快递,核算配件补发运费。</p></div><span class="reship-tag">快递应付</span></div>
|
||||
<form method="GET" class="reship-toolbar">
|
||||
<label>业务月份<input type="month" name="month" value="{{ $filters['month'] }}" min="2000-01" required></label>
|
||||
<input type="hidden" name="match_status" value="{{ $filters['match_status'] }}">
|
||||
@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="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 (['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>
|
||||
<button type="submit" class="reship-primary">查询并匹配</button>
|
||||
<a class="reship-button" href="{{ request()->url() . '?' . http_build_query(['month'=>$filters['month'], 'match_status'=>$filters['match_status']]) }}">清空筛选</a>
|
||||
<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
|
||||
<span class="reship-range">匹配范围:{{ $startMonth }} 至 {{ \Carbon\Carbon::createFromFormat('!Y-m', $filters['month'])->format('Y年n月') }}</span>
|
||||
</form>
|
||||
|
||||
@ -152,7 +152,7 @@ class SupplierPayableFinalPagesTest extends FeatureTestCase
|
||||
|
||||
$this->assertSame([
|
||||
'记录标识', '月份', '发货时间', '明细供应商', '条形码', '商品名称', '发货量', '单价', '金额', '预估单价',
|
||||
'预估金额', '供应商条码', '备注', 'wms配货单号', '标题', '仓库名称', '快递单号', '标记多标签', '创建人', '对账人',
|
||||
'预估金额', '供应商条码', '备注', '标题', 'wms配货单号', '仓库名称', '快递单号', '标记多标签', '创建人', '对账人',
|
||||
], $header);
|
||||
$this->assertNotContains('快递名称', $header);
|
||||
} finally {
|
||||
@ -164,8 +164,8 @@ class SupplierPayableFinalPagesTest extends FeatureTestCase
|
||||
{
|
||||
$file = UploadedFile::fake()->createWithContent(
|
||||
'return-reconciliation.csv',
|
||||
"\xEF\xBB\xBF月份,发货时间,明细供应商,条形码,商品名称,发货量,单价,金额,预估单价,预估金额,供应商条码,备注,wms配货单号,标题,仓库名称,快递单号,标记多标签,创建人,对账人\n"
|
||||
. "2026-09,2026-09-01,退货供应商,BC-1,商品甲,3,10,30,9,27,SB-1,备注甲,WMS-1,标题甲,华南仓,SF-9,多标签,创建人甲,对账人乙\n",
|
||||
"\xEF\xBB\xBF月份,发货时间,明细供应商,条形码,商品名称,发货量,单价,金额,预估单价,预估金额,供应商条码,备注,标题,wms配货单号,仓库名称,快递单号,标记多标签,创建人,对账人\n"
|
||||
. "2026-09,2026-09-01,退货供应商,BC-1,商品甲,3,10,30,9,27,SB-1,备注甲,标题甲,WMS-1,华南仓,SF-9,多标签,创建人甲,对账人乙\n",
|
||||
);
|
||||
|
||||
$this->loginAs()
|
||||
@ -192,6 +192,7 @@ class SupplierPayableFinalPagesTest extends FeatureTestCase
|
||||
|
||||
$export = $this->loginAs()->get(route('business.supplier-payables-final.return-reconciliation.export'));
|
||||
$export->assertOk();
|
||||
$this->assertStringContainsString('备注,标题,wms配货单号', $export->streamedContent());
|
||||
$this->assertStringContainsString('标记多标签,创建人,对账人', $export->streamedContent());
|
||||
$this->assertStringNotContainsString('快递名称', $export->streamedContent());
|
||||
}
|
||||
|
||||
@ -31,7 +31,7 @@ class AccessoryReshipPageDataTest extends TestCase
|
||||
private function configs(): AccessoryReshipCourierStore
|
||||
{
|
||||
$store = Mockery::mock(AccessoryReshipCourierStore::class);
|
||||
$store->shouldReceive('activeRates')->andReturn(['快递' => ['courier_code' => 'TEST', 'rate_basis_points' => 5000]]);
|
||||
$store->shouldReceive('activeRates')->andReturn(['快递' => ['courier_code' => 'TEST', 'rate_basis_points' => 5000], '顺丰' => ['courier_code' => 'SF', 'rate_basis_points' => 10000]]);
|
||||
return $store;
|
||||
}
|
||||
|
||||
@ -64,4 +64,32 @@ class AccessoryReshipPageDataTest extends TestCase
|
||||
$this->assertSame(105, $data['reshipSummary']['ready']);
|
||||
$this->assertSame(0, $data['reshipSummary']['ambiguous']);
|
||||
}
|
||||
|
||||
public function test_filters_search_all_rows_and_keep_summary_pagination_consistent(): void
|
||||
{
|
||||
Container::getInstance()->instance('request', Request::create('/?month=2026-09&match_status=all&courier=补发&original_month=2026-08&page=2'));
|
||||
$accessories = [];
|
||||
for ($i = 0; $i < 105; $i++) $accessories[] = ['trade_no'=>'001', 'express_no'=>'BF'.$i, 'express_name1'=>'补发快递', 'ship_time'=>'2026-09-15', 'barcode'=>'SKU-1', 'freight'=>'0.10'];
|
||||
$accessories[] = array_replace($accessories[0], ['trade_no'=>'NO-MATCH']);
|
||||
$repo = Mockery::mock(ShippingOrderRepository::class);
|
||||
$repo->shouldReceive('accessoryReshipBatches')->andReturn((function () use ($accessories) { yield ['accessories'=>$accessories, 'originals'=>[['trade_no'=>'001','barcode'=>'SKU','express_no'=>'SF001','express_name1'=>'顺丰','ship_time'=>'2026-08-01','warehouse_name'=>'成品仓']]]; })());
|
||||
$data = (new AccessoryReshipPageData($repo, new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections()))->page();
|
||||
$this->assertNull($data['reshipError']);
|
||||
$this->assertCount(50, $data['reshipRows']);
|
||||
$this->assertSame(105, $data['reshipRows']->total());
|
||||
$this->assertSame(105, $data['reshipSummary']['all']);
|
||||
$this->assertSame(1050, $data['reshipSummary']['amount']);
|
||||
}
|
||||
|
||||
public function test_filters_require_same_candidate_and_support_fields_and_unknown_amount(): void
|
||||
{
|
||||
$data = new AccessoryReshipPageData(Mockery::mock(ShippingOrderRepository::class), new AccessoryReshipMatcher, $this->configs(), new AccessoryReshipRateCalculator, $this->selections());
|
||||
$row = ['trade_no'=>'T001','express_no'=>'BF001','express_name1'=>'','express_name2'=>'补发快递','ship_time'=>'2026-09-15','barcode'=>'SKU-1','normalized_barcode'=>'SKU','freight_cents'=>1230,'deduction_cents'=>1230,'matches'=>[['courier'=>'顺丰','express_no'=>'SF001','month'=>'2026-08'],['courier'=>'圆通','express_no'=>'YT001','month'=>'2026-09']]];
|
||||
$this->assertTrue($data->matchesFilters($row, ['trade_no'=>'T001','express_no'=>'bf001','courier'=>'补发','ship_date'=>'2026-09-15','barcode'=>'SKU-1','normalized_barcode'=>'SKU','freight'=>'12.30','deduction_amount'=>'12.30','original_courier'=>'顺丰','original_express_no'=>'sf001','original_month'=>'2026-08']));
|
||||
$this->assertFalse($data->matchesFilters($row, ['original_courier'=>'顺丰','original_express_no'=>'YT001']));
|
||||
$this->assertFalse($data->matchesFilters($row, ['trade_no'=>'missing']));
|
||||
$row['freight_cents'] = $row['deduction_cents'] = null;
|
||||
$this->assertTrue($data->matchesFilters($row, ['freight'=>'待核实','deduction_amount'=>'待核实']));
|
||||
$this->assertFalse($data->matchesFilters($row, ['deduction_amount'=>'0.00']));
|
||||
}
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user