282 lines
13 KiB
PHP
282 lines
13 KiB
PHP
<?php
|
||
|
||
namespace Tests\Feature\Business;
|
||
|
||
use App\Http\Livewire\Business\ThirdPartyWarehouseQuotes as WarehouseQuoteComponent;
|
||
use App\Models\Auth\Permission;
|
||
use App\Models\Business\ThirdPartyWarehouseQuote;
|
||
use App\Models\Business\ThirdPartyWarehouseStorageSettlement;
|
||
use Illuminate\Database\Eloquent\ModelNotFoundException;
|
||
use Livewire\Livewire;
|
||
use Maatwebsite\Excel\Facades\Excel;
|
||
use Tests\Feature\FeatureTestCase;
|
||
|
||
class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase
|
||
{
|
||
public function test_quote_management_contains_warehouse_quote_entry_and_formula_definitions(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->createQuote();
|
||
|
||
$this->loginAs()
|
||
->get(route('business.pages.show', [
|
||
'section' => 'courier-payables',
|
||
'page' => 'quote-management',
|
||
'quote_view' => 'warehouse',
|
||
]))
|
||
->assertOk()
|
||
->assertSeeText('快递报价')
|
||
->assertSeeText('仓储报价')
|
||
->assertSeeText('录入新报价')
|
||
->assertSeeText('南昌樵舍仓')
|
||
->assertSeeText('入库管理费(元/吨)')
|
||
->assertSeeText('出库费(元/单)')
|
||
->assertSeeText('每托折算件数(件/托)')
|
||
->assertSeeText('每托占地面积(㎡/托)')
|
||
->assertSeeText('仓储面积单价(元/㎡·月)')
|
||
->assertSeeText('Σ(普通商品资料重量kg ÷ 1000 ×(加工进仓+加工出仓+采购进仓+采购退货+其它退货+销售退货+盘点+调拨入))× 入库管理费单价')
|
||
->assertSeeText('Σ(销售出仓 + 其它出库 + 调拨出)× 出库费单价')
|
||
->assertSeeText('系统数据为唯一核算来源,不读取仓库提供、核对或差异列')
|
||
->assertSeeText($this->user->name)
|
||
->assertSee('data-quote-panel="warehouse"', false);
|
||
}
|
||
|
||
public function test_warehouse_quote_store_creates_a_new_effective_version(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$existing = $this->createQuote();
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->set('warehouseName', '南昌樵舍仓')
|
||
->set('inboundRatePerTon', '22.5')
|
||
->set('outboundRatePerOrder', '0.9')
|
||
->set('piecesPerPallet', '24')
|
||
->set('palletAreaSqm', '3.2')
|
||
->set('storageRatePerSqmMonth', '11')
|
||
->set('effectiveAt', '2026-09-01')
|
||
->set('remark', '九月新报价')
|
||
->call('save')
|
||
->assertHasNoErrors()
|
||
->assertSet('message', '仓储报价已保存,新报价不会覆盖历史版本。');
|
||
|
||
$this->assertDatabaseCount('business_third_party_warehouse_quotes', 2);
|
||
$this->assertDatabaseHas('business_third_party_warehouse_quotes', [
|
||
'company_id' => company_id(),
|
||
'warehouse_name' => '南昌樵舍仓',
|
||
'inbound_rate_per_ton' => 22.5,
|
||
'outbound_rate_per_order' => 0.9,
|
||
'pieces_per_pallet' => 24,
|
||
'pallet_area_sqm' => 3.2,
|
||
'storage_rate_per_sqm_month' => 11,
|
||
'effective_at' => '2026-09-01',
|
||
'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE,
|
||
'created_by' => user_id(),
|
||
]);
|
||
$this->assertSame('20.0000', (string) $existing->fresh()->inbound_rate_per_ton);
|
||
}
|
||
|
||
public function test_warehouse_quote_validation_rejects_zero_divisors(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->set('piecesPerPallet', '0')
|
||
->set('palletAreaSqm', '0')
|
||
->call('save')
|
||
->assertHasErrors(['piecesPerPallet' => 'gt', 'palletAreaSqm' => 'gt']);
|
||
}
|
||
|
||
public function test_warehouse_quote_validation_rejects_a_blank_warehouse_name(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->set('warehouseName', ' ')
|
||
->call('save')
|
||
->assertHasErrors(['warehouseName' => 'required']);
|
||
|
||
$this->assertDatabaseCount('business_third_party_warehouse_quotes', 0);
|
||
}
|
||
|
||
public function test_quote_status_action_is_company_scoped(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$quote = ThirdPartyWarehouseQuote::withoutGlobalScopes()->create([
|
||
'company_id' => company_id() + 999,
|
||
'warehouse_name' => '其他公司仓库',
|
||
'inbound_rate_per_ton' => 20,
|
||
'outbound_rate_per_order' => 0.85,
|
||
'pieces_per_pallet' => 23,
|
||
'pallet_area_sqm' => 3,
|
||
'storage_rate_per_sqm_month' => 10.5,
|
||
'effective_at' => '2026-08-01',
|
||
'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE,
|
||
]);
|
||
|
||
$this->loginAs();
|
||
$this->expectException(ModelNotFoundException::class);
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->call('setStatus', $quote->id, ThirdPartyWarehouseQuote::STATUS_INACTIVE);
|
||
}
|
||
|
||
public function test_daily_sku_storage_detail_can_be_opened_and_switched_by_date(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$quote = $this->createQuote();
|
||
$this->createSettlement($quote, [
|
||
['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => 0, 'closing' => 100, 'storage_fee' => 4.417952],
|
||
['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-B', 'product_name' => '商品B', 'supplier' => '供应商乙', 'category' => '柜类', 'unit' => '件', 'opening' => 50, 'net_movement' => 10, 'closing' => 60, 'storage_fee' => 2.650771],
|
||
['date' => '2026-08-02', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => -10, 'closing' => 90, 'storage_fee' => 3.976157],
|
||
]);
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->call('viewSettlementDetails', '2026-08')
|
||
->assertSet('settlementDetailDate', '2026-08-01')
|
||
->assertDispatched('warehouse-settlement-details-opened')
|
||
->assertSee('每日商品仓储费明细')
|
||
->assertSee('导出整月商品明细')
|
||
->assertSee('160.00')
|
||
->assertSee('SKU-A')
|
||
->assertSee('SKU-B')
|
||
->set('settlementDetailDate', '2026-08-02')
|
||
->assertSee('90.00')
|
||
->assertSee('3.976157')
|
||
->assertDontSee('SKU-B');
|
||
}
|
||
|
||
public function test_legacy_settlement_explains_that_recalculation_is_required(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$quote = $this->createQuote();
|
||
$this->createSettlement($quote, []);
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->call('viewSettlementDetails', '2026-08')
|
||
->assertDispatched('warehouse-settlement-details-opened')
|
||
->assertSee('原存档没有每日商品明细,请重新核算该月份后查看和导出');
|
||
}
|
||
|
||
public function test_daily_sku_storage_details_can_be_exported_for_the_whole_month(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$quote = $this->createQuote();
|
||
$dailySku = [
|
||
['date' => '2026-08-01', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => 0, 'closing' => 100, 'storage_fee' => 4.417952],
|
||
['date' => '2026-08-02', 'owner' => '甲', 'sku_code' => 'SKU-A', 'product_name' => '商品A', 'supplier' => '供应商甲', 'category' => '桌类', 'unit' => '件', 'opening' => 100, 'net_movement' => -10, 'closing' => 90, 'storage_fee' => 3.976157],
|
||
];
|
||
$this->createSettlement($quote, $dailySku);
|
||
Excel::fake();
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->call('exportSettlementDetails', '2026-08');
|
||
|
||
Excel::assertDownloaded('南昌樵舍仓每日商品仓储费明细_2026-08.xlsx', function ($export) use ($dailySku): bool {
|
||
return $export->title() === '每日商品仓储费'
|
||
&& count($export->array()) === count($dailySku)
|
||
&& $export->array()[1][0] === '2026-08-02';
|
||
});
|
||
}
|
||
|
||
public function test_legacy_settlement_does_not_export_an_empty_detail_file(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$quote = $this->createQuote();
|
||
$this->createSettlement($quote, []);
|
||
Excel::fake();
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->call('exportSettlementDetails', '2026-08')
|
||
->assertSet('settlementDetailPeriod', '2026-08')
|
||
->assertDispatched('warehouse-settlement-details-opened')
|
||
->assertSee('原存档没有每日商品明细,请重新核算该月份后查看和导出');
|
||
|
||
$this->assertSame([], $this->readExcelFakeDownloads());
|
||
}
|
||
|
||
public function test_settlement_history_detail_and_export_are_company_scoped(): void
|
||
{
|
||
$this->user->attachPermission(Permission::where('name', 'read-admin-panel')->firstOrFail());
|
||
$this->loginAs();
|
||
$quote = $this->createQuote();
|
||
$this->createSettlement($quote, [], company_id() + 999, '2026-07');
|
||
|
||
Livewire::test(WarehouseQuoteComponent::class)
|
||
->assertDontSee('2026-07')
|
||
->call('viewSettlementDetails', '2026-07')
|
||
->assertSet('settlementDetailMessage', '未找到 2026-07 的核算存档。')
|
||
->call('exportSettlement', '2026-07')
|
||
->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。')
|
||
->call('exportSettlementDetails', '2026-07')
|
||
->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。');
|
||
}
|
||
|
||
private function createQuote(): ThirdPartyWarehouseQuote
|
||
{
|
||
return ThirdPartyWarehouseQuote::query()->create([
|
||
'company_id' => company_id(),
|
||
'warehouse_name' => '南昌樵舍仓',
|
||
'inbound_rate_per_ton' => 20,
|
||
'outbound_rate_per_order' => 0.85,
|
||
'pieces_per_pallet' => 23,
|
||
'pallet_area_sqm' => 3,
|
||
'storage_rate_per_sqm_month' => 10.5,
|
||
'effective_at' => '2026-08-01',
|
||
'status' => ThirdPartyWarehouseQuote::STATUS_ACTIVE,
|
||
'remark' => '八月合同报价',
|
||
'created_by' => $this->user->id,
|
||
'updated_by' => $this->user->id,
|
||
]);
|
||
}
|
||
|
||
/** @return array<string,mixed> */
|
||
private function readExcelFakeDownloads(): array
|
||
{
|
||
$fake = Excel::getFacadeRoot();
|
||
$property = new \ReflectionProperty($fake, 'downloads');
|
||
$property->setAccessible(true);
|
||
|
||
return $property->getValue($fake);
|
||
}
|
||
|
||
/** @param list<array<string,mixed>> $dailySku */
|
||
private function createSettlement(
|
||
ThirdPartyWarehouseQuote $quote,
|
||
array $dailySku,
|
||
?int $companyId = null,
|
||
string $period = '2026-08',
|
||
): void
|
||
{
|
||
ThirdPartyWarehouseStorageSettlement::query()->create([
|
||
'company_id' => $companyId ?? company_id(),
|
||
'warehouse_id' => WarehouseQuoteComponent::WAREHOUSE_ID,
|
||
'warehouse_name' => '南昌樵舍仓',
|
||
'period' => $period,
|
||
'quote_id' => $quote->id,
|
||
'inbound_rate_per_ton' => 20,
|
||
'outbound_rate_per_order' => 0.85,
|
||
'pieces_per_pallet' => 23,
|
||
'pallet_area_sqm' => 3,
|
||
'storage_rate_per_sqm_month' => 10.5,
|
||
'days_in_month' => 31,
|
||
'inbound_tons' => 0,
|
||
'outbound_qty' => 0,
|
||
'inbound_fee' => 0,
|
||
'outbound_fee' => 0,
|
||
'storage_fee' => array_sum(array_column($dailySku, 'storage_fee')),
|
||
'memo_fee' => 0,
|
||
'loss_fee' => 0,
|
||
'compensation_fee' => 0,
|
||
'total_fee' => array_sum(array_column($dailySku, 'storage_fee')),
|
||
'detail_json' => ['daily' => [], 'per_sku' => [], 'daily_sku' => $dailySku],
|
||
'calculated_by' => user_id(),
|
||
]);
|
||
}
|
||
}
|