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)@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);