From d3532bf0dada1360c6d6ddfc4a6cff7d48347d3c Mon Sep 17 00:00:00 2001
From: limeng <1131177064@qq.com>
Date: Sun, 15 Nov 2026 13:32:46 +0800
Subject: [PATCH] =?UTF-8?q?fix:=20=E7=A5=A5=E5=92=8C=E4=B8=AD=E9=80=9A?=
=?UTF-8?q?=E5=92=8C=E6=9F=8F=E5=8D=A1=E9=A1=BA=E4=B8=B0=E5=B9=B2=E9=85=8D?=
=?UTF-8?q?=E9=85=8D=E4=BB=B6=E8=A1=A5=E5=8F=91=E5=85=8D=E6=89=A3=E6=AC=BE?=
=?UTF-8?q?=20(#1182)?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
.../Business/AccessoryReshipDeductions.php | 4 +-
.../AccessoryReshipRateCalculator.php | 4 +-
.../accessory-reship-deductions.blade.php | 2 +-
.../AccessoryReshipDeductionsTest.php | 84 +++++++++++++++++++
tests/Unit/AccessoryReshipPageDataTest.php | 23 ++++-
5 files changed, 113 insertions(+), 4 deletions(-)
create mode 100644 tests/Feature/Business/AccessoryReshipDeductionsTest.php
diff --git a/app/Http/Controllers/Business/AccessoryReshipDeductions.php b/app/Http/Controllers/Business/AccessoryReshipDeductions.php
index 95364911..5f6ec606 100644
--- a/app/Http/Controllers/Business/AccessoryReshipDeductions.php
+++ b/app/Http/Controllers/Business/AccessoryReshipDeductions.php
@@ -36,7 +36,9 @@ class AccessoryReshipDeductions extends Controller
fputcsv($file, ['交易单号','补发快递单号','补发快递','补发时间','原始条形码','处理后条形码','补发运费','匹配状态','首发快递公司(候选)','首发快递单号(候选)','首发所属月份(候选)','扣款比例(%)','扣款金额','说明']);
foreach ($data->rows($filters) as $row) {
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']];
+ $status = $row['deduction_state'] === 'excluded_courier' ? '不扣款'
+ : ($row['deduction_state'] === 'ready' ? '可扣款' : ($row['status'] === 'matched' ? '已匹配待核对' : '未匹配不扣款'));
+ $values = [$row['trade_no'], $row['express_no'], ($row['express_name1'] ?: ($row['express_name2'] ?? '')), $row['ship_time'], $row['barcode'], $row['normalized_barcode'], $row['freight'], $status, 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;
return preg_match('/^[=+\-@\t\r\n]/u', $value) ? "'" . $value : $value;
diff --git a/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php b/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php
index 5b7d6fcc..74f596b0 100644
--- a/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php
+++ b/modules/EcommerceFinance/Services/AccessoryReshipRateCalculator.php
@@ -6,6 +6,8 @@ use Illuminate\Validation\ValidationException;
class AccessoryReshipRateCalculator
{
+ public const EXCLUDED_RESHIP_COURIERS = ['外采快递', '线下快递', '祥和中通', '柏卡顺丰干配配件'];
+
/** 界面及文本导入均以百分数为单位;数据库保存万分比整数。 */
public static function basisPoints(string $percent): int
{
@@ -45,7 +47,7 @@ class AccessoryReshipRateCalculator
$row['deduction_state'] = 'unmatched';
$row['deduction_note'] = '三个月内未匹配首发快递单号';
$reshipCourier = trim((string) (($row['express_name1'] ?? '') ?: ($row['express_name2'] ?? '')));
- if (in_array($reshipCourier, ['外采快递', '线下快递'], true)) {
+ if (in_array($reshipCourier, self::EXCLUDED_RESHIP_COURIERS, true)) {
$row['deduction_cents'] = 0;
$row['deduction_state'] = 'excluded_courier';
$row['deduction_note'] = '补发快递为' . $reshipCourier . ',不参与扣款';
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 fcddebcd..7beef44c 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
@@ -31,7 +31,7 @@
@endforeach
按配置比例扣款合计{{ $reshipError ? '—' : '¥ ' . number_format($summary['amount'] / 100, 2) }}
- 匹配规则与扣款口径
配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为外采快递或线下快递不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。
本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。
+ 匹配规则与扣款口径
配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为{{ implode('、', \Modules\EcommerceFinance\Services\AccessoryReshipRateCalculator::EXCLUDED_RESHIP_COURIERS) }}不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。
本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。
@if ($reshipError){{ $reshipError }}
@endif
@if ($summary['ambiguous'] || $summary['invalid_amount'] || $summary['missing_config'] || $summary['zero_rate'])待选择扣款快递 {{ $summary['selection_required'] }} 单;运费异常 {{ $summary['invalid_amount'] }} 单;待配置 {{ $summary['missing_config'] }} 单,0%不扣款 {{ $summary['zero_rate'] }} 单。待核算项不计入合计。
@endif
diff --git a/tests/Feature/Business/AccessoryReshipDeductionsTest.php b/tests/Feature/Business/AccessoryReshipDeductionsTest.php
new file mode 100644
index 00000000..859b8b93
--- /dev/null
+++ b/tests/Feature/Business/AccessoryReshipDeductionsTest.php
@@ -0,0 +1,84 @@
+ $courier) {
+ $accessories[] = [
+ 'trade_no' => 'T001', 'express_no' => 'BF-TEST-' . $i,
+ 'express_name1' => $courier, 'express_name2' => '',
+ 'ship_time' => '2026-11-10', 'barcode' => 'SKU-1', 'freight' => '10.00',
+ ];
+ }
+ $original = [
+ 'trade_no' => 'T001', 'express_no' => 'FIRST001', 'express_name1' => '首发快递',
+ 'ship_time' => '2026-10-01', 'barcode' => 'SKU', 'warehouse_name' => '成品仓',
+ ];
+ $shipping = \Mockery::mock(ShippingOrderRepository::class);
+ $shipping->shouldReceive('accessoryReshipBatches')->with('2026-11-01', '2026-12-01', '2026-09-01')
+ ->andReturnUsing(static function () use ($accessories, $original) {
+ yield ['accessories' => $accessories, 'originals' => [$original]];
+ });
+ $configs = \Mockery::mock(AccessoryReshipCourierStore::class);
+ $configs->shouldReceive('activeRates')->andReturn(['首发快递' => ['courier_code' => 'TEST', 'rate_basis_points' => 5000]]);
+ $selections = \Mockery::mock(AccessoryReshipSelectionStore::class);
+ $selections->shouldReceive('forMonth')->andReturn([]);
+ $this->app->instance(ShippingOrderRepository::class, $shipping);
+ $this->app->instance(AccessoryReshipCourierStore::class, $configs);
+ $this->app->instance(AccessoryReshipSelectionStore::class, $selections);
+ $this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
+ }
+
+ protected function pageUrl(array $filters = []): string
+ {
+ return route('business.pages.show', [
+ 'company_id' => $this->company->id, 'section' => 'courier-payables',
+ 'page' => 'accessory-reship-deductions', 'month' => '2026-11',
+ ] + $filters);
+ }
+
+ public function test_page_totals_and_export_exclude_both_couriers_from_deductions(): void
+ {
+ $this->mockShipments();
+ $this->loginAs()->get($this->pageUrl(['match_status' => 'all']))->assertOk()
+ ->assertSee('BF-TEST-0')->assertSee('BF-TEST-1')->assertSee('BF-TEST-2')
+ ->assertSee('补发快递为祥和中通,不参与扣款')
+ ->assertSee('补发快递为柏卡顺丰干配配件,不参与扣款')
+ ->assertSee('¥ 5.00');
+
+ $csv = $this->get(route('business.accessory-reship-deductions.export', [
+ 'company_id' => $this->company->id, 'month' => '2026-11', 'match_status' => 'all',
+ ]))->assertOk()->streamedContent();
+ $records = array_map('str_getcsv', explode("\n", trim($csv)));
+ $this->assertCount(4, $records);
+ foreach ([1, 2] as $i) {
+ $this->assertSame('不扣款', $records[$i][7]);
+ $this->assertSame('0.00', $records[$i][12]);
+ }
+ $this->assertSame('可扣款', $records[3][7]);
+ $this->assertSame('5.00', $records[3][12]);
+
+ $this->get($this->pageUrl())->assertOk()->assertSee('BF-TEST-2')
+ ->assertDontSee('BF-TEST-0')->assertDontSee('BF-TEST-1')->assertSee('¥ 5.00');
+ $readyCsv = $this->get(route('business.accessory-reship-deductions.export', [
+ 'company_id' => $this->company->id, 'month' => '2026-11',
+ ]))->assertOk()->streamedContent();
+ $this->assertStringContainsString('BF-TEST-2', $readyCsv);
+ $this->assertStringNotContainsString('BF-TEST-0', $readyCsv);
+ $this->assertStringNotContainsString('BF-TEST-1', $readyCsv);
+
+ $this->get($this->pageUrl(['match_status' => 'all', 'deduction_state' => 'excluded_courier']))
+ ->assertOk()->assertSee('BF-TEST-0')->assertSee('BF-TEST-1')
+ ->assertDontSee('BF-TEST-2')->assertSee('¥ 0.00');
+ }
+}
diff --git a/tests/Unit/AccessoryReshipPageDataTest.php b/tests/Unit/AccessoryReshipPageDataTest.php
index f1b6ba6a..8b2d4fa7 100644
--- a/tests/Unit/AccessoryReshipPageDataTest.php
+++ b/tests/Unit/AccessoryReshipPageDataTest.php
@@ -27,7 +27,7 @@ class AccessoryReshipPageDataTest extends TestCase
$calculator = new AccessoryReshipRateCalculator;
$base = ['status' => 'matched', 'freight_cents' => 1000, 'matches' => [['courier' => '首发快递']]];
$rates = ['首发快递' => ['rate_basis_points' => 5000]];
- foreach (['外采快递', '线下快递'] as $name) {
+ foreach (['外采快递', '线下快递', '祥和中通', '柏卡顺丰干配配件'] as $name) {
foreach (['express_name1', 'express_name2'] as $field) {
$row = $calculator->apply($base + [$field => ' ' . $name . ' '], $rates);
$this->assertSame('excluded_courier', $row['deduction_state']);
@@ -48,6 +48,27 @@ class AccessoryReshipPageDataTest extends TestCase
}
}
+ public function test_exclusions_override_saved_selection_and_require_exact_reship_courier_name(): void
+ {
+ $calculator = new AccessoryReshipRateCalculator;
+ $base = ['status' => 'matched', 'freight_cents' => 1234, 'matches' => [['courier' => '祥和中通'], ['courier' => '首发快递']]];
+ $rates = ['祥和中通' => ['rate_basis_points' => 10000], '首发快递' => ['rate_basis_points' => 10000]];
+ foreach (['祥和中通', '柏卡顺丰干配配件'] as $name) {
+ foreach ([null, '首发快递'] as $selection) {
+ foreach ([$rates, []] as $configs) {
+ $row = $calculator->apply($base + ['express_name1' => $name], $configs, $selection);
+ $this->assertSame('excluded_courier', $row['deduction_state']);
+ $this->assertSame(0, $row['deduction_cents']);
+ }
+ }
+ }
+ foreach (['正常补发快递', '祥和中通其他', '柏卡顺丰干配'] as $name) {
+ $row = $calculator->apply($base + ['express_name1' => $name, 'express_name2' => '祥和中通'], $rates, '祥和中通');
+ $this->assertSame('ready', $row['deduction_state']);
+ $this->assertSame(1234, $row['deduction_cents']);
+ }
+ }
+
private function selections(): AccessoryReshipSelectionStore
{
$store = Mockery::mock(AccessoryReshipSelectionStore::class);