fix: 祥和中通和柏卡顺丰干配配件补发免扣款 (#1182)
This commit is contained in:
parent
e60095e64d
commit
d3532bf0da
@ -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;
|
||||
|
||||
@ -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 . ',不参与扣款';
|
||||
|
||||
@ -31,7 +31,7 @@
|
||||
@endforeach
|
||||
<div><span>按配置比例扣款合计</span><strong>{{ $reshipError ? '—' : '¥ ' . number_format($summary['amount'] / 100, 2) }}</strong></div>
|
||||
</div>
|
||||
<details class="reship-rules"><summary>匹配规则与扣款口径</summary><p>配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为外采快递或线下快递不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。</p><p>本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。</p></details>
|
||||
<details class="reship-rules"><summary>匹配规则与扣款口径</summary><p>配件仓库:常州简尚家居有限公司。配件条码取第一个英文逗号前的内容,再去掉最后一个“-”及其后内容。首发仓库排除该仓库,首发多个条码拆分后按完整条码匹配。补发快递为{{ implode('、', \Modules\EcommerceFinance\Services\AccessoryReshipRateCalculator::EXCLUDED_RESHIP_COURIERS) }}不参与扣款。可扣款列表仅包含已确定首发归属、已配置比例且扣款金额大于0的记录,扣款金额=补发运费×首发快递配置比例,按分四舍五入。未配置比例暂不核算,0%不扣款。</p><p>本页生成扣款核对清单,尚未记入应付账。首发快递名称相同自动任选一条,无需核对;名称不同请选择需要扣款的快递,仅计一次;运费缺失或异常需核实;重复匹配不会新增扣款记录。</p></details>
|
||||
@if ($reshipError)<div class="reship-error" role="alert">{{ $reshipError }}</div>@endif
|
||||
@if ($summary['ambiguous'] || $summary['invalid_amount'] || $summary['missing_config'] || $summary['zero_rate'])<div class="reship-notice" role="status">待选择扣款快递 {{ $summary['selection_required'] }} 单;运费异常 {{ $summary['invalid_amount'] }} 单;待配置 {{ $summary['missing_config'] }} 单,0%不扣款 {{ $summary['zero_rate'] }} 单。待核算项不计入合计。</div>@endif
|
||||
<section class="reship-panel" data-finance-table-surface>
|
||||
|
||||
84
tests/Feature/Business/AccessoryReshipDeductionsTest.php
Normal file
84
tests/Feature/Business/AccessoryReshipDeductionsTest.php
Normal file
@ -0,0 +1,84 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Feature\Business;
|
||||
|
||||
use App\Models\Auth\Permission;
|
||||
use Modules\EcommerceFinance\Repositories\ShippingOrderRepository;
|
||||
use Modules\EcommerceFinance\Services\AccessoryReshipCourierStore;
|
||||
use Modules\EcommerceFinance\Services\AccessoryReshipSelectionStore;
|
||||
use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class AccessoryReshipDeductionsTest extends FeatureTestCase
|
||||
{
|
||||
protected function mockShipments(): void
|
||||
{
|
||||
$accessories = [];
|
||||
foreach (['祥和中通', '柏卡顺丰干配配件', '正常补发快递'] as $i => $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');
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user