diff --git a/docs/配件补发扣款.md b/docs/配件补发扣款.md index e3b14869..68dbd406 100644 --- a/docs/配件补发扣款.md +++ b/docs/配件补发扣款.md @@ -27,3 +27,5 @@ Excel导入列为快递代码、快递公司、创建时间、创建人、扣款比率(%)。原创建时间和创建人保留。导入前整表校验,写入及操作日志使用同一事务;相同代码、名称、比例再次导入跳过,冲突整批回滚,不自动覆盖或恢复已删除记录。 命令行导入:`php artisan accessory-reship:import-courier-configs --company=<公司ID>`。增加 `--dry-run` 仅校验,不写入。上传文件及真实配置不进入Git。 + +首发快递、首发单号、所属月份以独立三列显示,多候选按同一顺序对应。支持交易单号、补发单号、补发快递、补发日期、原始/处理后条码、补发运费、匹配状态、首发快递、首发单号、所属月份及可扣金额筛选;文本和金额支持包含匹配。筛选覆盖全部业务月记录,汇总、分类数量、分页和CSV导出沿用相同条件。多个首发筛选必须由同一候选满足;清空筛选保留业务月份。 diff --git a/modules/EcommerceFinance/Services/PayablesFinalRegistry.php b/modules/EcommerceFinance/Services/PayablesFinalRegistry.php index e5467670..98c9e9bb 100644 --- a/modules/EcommerceFinance/Services/PayablesFinalRegistry.php +++ b/modules/EcommerceFinance/Services/PayablesFinalRegistry.php @@ -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']], ], ], diff --git a/modules/EcommerceFinance/View/AccessoryReshipPageData.php b/modules/EcommerceFinance/View/AccessoryReshipPageData.php index 06f9afdd..0019b53b 100644 --- a/modules/EcommerceFinance/View/AccessoryReshipPageData.php +++ b/modules/EcommerceFinance/View/AccessoryReshipPageData.php @@ -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; 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 9d30a067..13d7f429 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 @@ -15,13 +15,13 @@

配件补发扣款

按交易单号及条形码匹配首发快递,核算配件补发运费。

快递应付
- @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 匹配范围:{{ $startMonth }} 至 {{ \Carbon\Carbon::createFromFormat('!Y-m', $filters['month'])->format('Y年n月') }}
diff --git a/tests/Feature/Business/SupplierPayableFinalPagesTest.php b/tests/Feature/Business/SupplierPayableFinalPagesTest.php index f107b3b1..40ed0237 100644 --- a/tests/Feature/Business/SupplierPayableFinalPagesTest.php +++ b/tests/Feature/Business/SupplierPayableFinalPagesTest.php @@ -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()); } diff --git a/tests/Unit/AccessoryReshipPageDataTest.php b/tests/Unit/AccessoryReshipPageDataTest.php index 885007e9..807feb81 100644 --- a/tests/Unit/AccessoryReshipPageDataTest.php +++ b/tests/Unit/AccessoryReshipPageDataTest.php @@ -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'])); + } }