diff --git a/app/Business/ThirdPartyWarehouseStorageFeeCalculator.php b/app/Business/ThirdPartyWarehouseStorageFeeCalculator.php index 73f8875b..b1fe3902 100644 --- a/app/Business/ThirdPartyWarehouseStorageFeeCalculator.php +++ b/app/Business/ThirdPartyWarehouseStorageFeeCalculator.php @@ -37,7 +37,7 @@ class ThirdPartyWarehouseStorageFeeCalculator } /** - * @return array{summary:array,daily:list>,per_sku:list>} + * @return array{summary:array,daily:list>,per_sku:list>,daily_sku:list>} */ public function calculate(int $warehouseId, string $warehouseName, string $period): array { @@ -63,6 +63,10 @@ class ThirdPartyWarehouseStorageFeeCalculator $daily = []; /** @var array> $skus */ $skus = []; + $dailySku = []; + $storagePerPiecePerDay = $days > 0 && $piecesPerPallet > 0 + ? $palletArea * $sqmRate / $piecesPerPallet / $days + : 0.0; foreach ($this->mirror->exportRows(['dimension' => 'month', 'period' => $period], 5000) as $row) { $date = trim((string) ($row['date'] ?? '')); @@ -90,6 +94,20 @@ class ThirdPartyWarehouseStorageFeeCalculator $daily[$date]['inbound_qty'] += $inboundQty; $daily[$date]['outbound_qty'] += $outboundQty; + $dailySku[] = [ + 'date' => $date, + 'owner' => (string) ($row['owner'] ?? ''), + 'sku_code' => $code, + 'product_name' => (string) ($row['product_name'] ?? ''), + 'supplier' => (string) ($row['supplier'] ?? ''), + 'category' => (string) ($row['category'] ?? ''), + 'unit' => (string) ($row['unit'] ?? ''), + 'opening' => round($opening, 2), + 'net_movement' => round($closing - $opening, 2), + 'closing' => round($closing, 2), + 'storage_fee' => round($closing * $storagePerPiecePerDay, 6), + ]; + if (! isset($skus[$code])) { $skus[$code] = [ 'owner' => (string) ($row['owner'] ?? ''), @@ -198,6 +216,7 @@ class ThirdPartyWarehouseStorageFeeCalculator ], 'daily' => $dailyRows, 'per_sku' => $perSku, + 'daily_sku' => $dailySku, ]; } } diff --git a/app/Exports/ThirdPartyWarehouseStorageSettlementExport.php b/app/Exports/ThirdPartyWarehouseStorageSettlementExport.php index 0b7eb648..f158198c 100644 --- a/app/Exports/ThirdPartyWarehouseStorageSettlementExport.php +++ b/app/Exports/ThirdPartyWarehouseStorageSettlementExport.php @@ -11,13 +11,14 @@ use Maatwebsite\Excel\Concerns\WithMultipleSheets; use Maatwebsite\Excel\Concerns\WithTitle; /** - * 三方仓仓储费核算明细导出:汇总 / 进销存数据汇总(每日) / 进销存(逐商品) 三张表。 + * 三方仓仓储费核算明细导出:汇总 / 进销存数据汇总(每日) / 进销存(逐商品) / + * 每日商品仓储费四张表。 * 数据来自 ThirdPartyWarehouseStorageFeeCalculator 的核算结果(或存档快照)。 */ class ThirdPartyWarehouseStorageSettlementExport implements WithMultipleSheets { /** - * @param array{summary:array,daily:list>,per_sku:list>} $result + * @param array{summary:array,daily:list>,per_sku:list>,daily_sku?:list>} $result */ public function __construct(private readonly array $result) { @@ -30,6 +31,7 @@ class ThirdPartyWarehouseStorageSettlementExport implements WithMultipleSheets new StorageSettlementSummarySheet($this->result['summary']), new StorageSettlementDailySheet($this->result['summary'], $this->result['daily']), new StorageSettlementSkuSheet($this->result['per_sku']), + new StorageSettlementDailySkuSheet($this->result['daily_sku'] ?? []), ]; } } @@ -184,3 +186,43 @@ class StorageSettlementSkuSheet implements FromArray, WithHeadings, WithTitle ], $this->perSku); } } + +class StorageSettlementDailySkuSheet implements FromArray, WithHeadings, WithTitle +{ + /** @param list> $dailySku */ + public function __construct(private readonly array $dailySku) + { + } + + public function title(): string + { + return '每日商品仓储费'; + } + + /** @return array */ + public function headings(): array + { + return [ + '日期', '货主', '商品编码', '商品名称', '供应商', '分类', '单位', + '上日结余', '当日进出净额', '当日库存结余', '当天仓储费', + ]; + } + + /** @return array> */ + public function array(): array + { + return array_map(static fn (array $row): array => [ + $row['date'], + $row['owner'], + $row['sku_code'], + $row['product_name'], + $row['supplier'], + $row['category'], + $row['unit'], + (float) $row['opening'], + (float) $row['net_movement'], + (float) $row['closing'], + (float) $row['storage_fee'], + ], $this->dailySku); + } +} diff --git a/app/Http/Livewire/Business/ThirdPartyWarehouseQuotes.php b/app/Http/Livewire/Business/ThirdPartyWarehouseQuotes.php index 083d837e..6e7a992c 100644 --- a/app/Http/Livewire/Business/ThirdPartyWarehouseQuotes.php +++ b/app/Http/Livewire/Business/ThirdPartyWarehouseQuotes.php @@ -45,6 +45,15 @@ class ThirdPartyWarehouseQuotes extends Component public ?string $settlementError = null; + public ?string $settlementDetailPeriod = null; + + public string $settlementDetailDate = ''; + + /** @var list */ + public array $settlementDetailDates = []; + + public ?string $settlementDetailMessage = null; + public function mount(): void { $this->effectiveAt = now()->toDateString(); @@ -130,7 +139,11 @@ class ThirdPartyWarehouseQuotes extends Component 'loss_fee' => $summary['loss_fee'], 'compensation_fee' => $summary['compensation_fee'], 'total_fee' => $summary['total_fee'], - 'detail_json' => ['daily' => $result['daily'], 'per_sku' => $result['per_sku']], + 'detail_json' => [ + 'daily' => $result['daily'], + 'per_sku' => $result['per_sku'], + 'daily_sku' => $result['daily_sku'], + ], 'calculated_by' => (int) user_id(), ], ); @@ -144,6 +157,7 @@ class ThirdPartyWarehouseQuotes extends Component $this->ensureAuthenticated(); $settlement = ThirdPartyWarehouseStorageSettlement::query() + ->where('company_id', (int) company_id()) ->where('warehouse_id', self::WAREHOUSE_ID) ->where('period', $period) ->first(); @@ -154,11 +168,12 @@ class ThirdPartyWarehouseQuotes extends Component return null; } - $detail = $settlement->detail_json ?: ['daily' => [], 'per_sku' => []]; + $detail = $settlement->detail_json ?: ['daily' => [], 'per_sku' => [], 'daily_sku' => []]; $result = [ 'summary' => $this->summaryFromSettlement($settlement), 'daily' => $detail['daily'] ?? [], 'per_sku' => $detail['per_sku'] ?? [], + 'daily_sku' => $detail['daily_sku'] ?? [], ]; $fileName = $settlement->warehouse_name . '仓储费用_' . $period . '.xlsx'; @@ -166,11 +181,51 @@ class ThirdPartyWarehouseQuotes extends Component return Excel::download(new ThirdPartyWarehouseStorageSettlementExport($result), $fileName); } + public function viewSettlementDetails(string $period): void + { + $this->ensureAuthenticated(); + abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422); + + $this->settlementDetailPeriod = $period; + $this->settlementDetailDate = ''; + $this->settlementDetailDates = []; + $this->settlementDetailMessage = null; + + $settlement = $this->settlementForPeriod($period); + if ($settlement === null) { + $this->settlementDetailMessage = '未找到 ' . $period . ' 的核算存档。'; + + return; + } + + $detail = $settlement->detail_json ?: []; + $dailySku = $detail['daily_sku'] ?? []; + if ($dailySku === []) { + $this->settlementDetailMessage = $period . ' 的原存档没有每日商品明细,请重新核算该月份后查看。'; + + return; + } + + $dates = array_values(array_unique(array_column($dailySku, 'date'))); + sort($dates); + $this->settlementDetailDates = $dates; + $this->settlementDetailDate = $dates[0] ?? ''; + } + + public function closeSettlementDetails(): void + { + $this->settlementDetailPeriod = null; + $this->settlementDetailDate = ''; + $this->settlementDetailDates = []; + $this->settlementDetailMessage = null; + } + public function render(): View { $data = $this->repository()->pageData(); $history = ThirdPartyWarehouseStorageSettlement::query() + ->where('company_id', (int) company_id()) ->where('warehouse_id', self::WAREHOUSE_ID) ->orderByDesc('period') ->limit(24) @@ -184,12 +239,15 @@ class ThirdPartyWarehouseQuotes extends Component 'calculated_at' => optional($row->updated_at)->format('Y-m-d H:i'), ]) ->all(); + $settlementDetail = $this->settlementDetailData(); return view('business.pages.courier-payables.partials.third-party-warehouse-quotes', [ 'warehouseQuotes' => $data['rows'], 'warehouseQuoteSummary' => $data['summary'], 'warehouseQuotesReady' => $data['ready'], 'settlementHistory' => $history, + 'settlementDetailRows' => $settlementDetail['rows'], + 'settlementDetailSummary' => $settlementDetail['summary'], ]); } @@ -219,6 +277,56 @@ class ThirdPartyWarehouseQuotes extends Component ]; } + /** + * 明细行只参与当前服务端渲染,不进入 Livewire 公共快照,避免日期切换时回传整张表。 + * + * @return array{rows:list>,summary:array|null} + */ + private function settlementDetailData(): array + { + $empty = ['rows' => [], 'summary' => null]; + + if ($this->settlementDetailPeriod === null + || ! in_array($this->settlementDetailDate, $this->settlementDetailDates, true)) { + return $empty; + } + + $settlement = $this->settlementForPeriod($this->settlementDetailPeriod); + if ($settlement === null) { + return $empty; + } + + $detail = $settlement->detail_json ?: []; + $rows = array_values(array_filter( + $detail['daily_sku'] ?? [], + fn (array $row): bool => ($row['date'] ?? null) === $this->settlementDetailDate, + )); + usort($rows, static fn (array $left, array $right): int => strcmp( + (string) ($left['sku_code'] ?? ''), + (string) ($right['sku_code'] ?? ''), + )); + + return [ + 'rows' => $rows, + 'summary' => [ + 'sku_count' => count($rows), + 'opening' => array_sum(array_column($rows, 'opening')), + 'net_movement' => array_sum(array_column($rows, 'net_movement')), + 'closing' => array_sum(array_column($rows, 'closing')), + 'storage_fee' => array_sum(array_column($rows, 'storage_fee')), + ], + ]; + } + + private function settlementForPeriod(string $period): ?ThirdPartyWarehouseStorageSettlement + { + return ThirdPartyWarehouseStorageSettlement::query() + ->where('company_id', (int) company_id()) + ->where('warehouse_id', self::WAREHOUSE_ID) + ->where('period', $period) + ->first(); + } + /** @return array> */ protected function rules(): array { diff --git a/resources/views/business/pages/courier-payables/partials/third-party-warehouse-quotes.blade.php b/resources/views/business/pages/courier-payables/partials/third-party-warehouse-quotes.blade.php index 22c16d7e..ed7f631c 100644 --- a/resources/views/business/pages/courier-payables/partials/third-party-warehouse-quotes.blade.php +++ b/resources/views/business/pages/courier-payables/partials/third-party-warehouse-quotes.blade.php @@ -26,10 +26,10 @@ .warehouse-quote-field { display:grid; min-width:0; gap:.35rem; } .warehouse-quote-field--wide { grid-column:span 2; } .warehouse-quote-field label { color:var(--quote-muted); font-size:.75rem; font-weight:600; } - .warehouse-quote-field input,.warehouse-quote-field textarea { width:100%; min-width:0; border:1px solid var(--quote-border); border-radius:.5rem; background:#fff; color:var(--quote-ink); font:inherit; } - .warehouse-quote-field input { height:34px; padding:0 .625rem; } + .warehouse-quote-field input,.warehouse-quote-field select,.warehouse-quote-field textarea { width:100%; min-width:0; border:1px solid var(--quote-border); border-radius:.5rem; background:#fff; color:var(--quote-ink); font:inherit; } + .warehouse-quote-field input,.warehouse-quote-field select { height:34px; padding:0 .625rem; } .warehouse-quote-field textarea { min-height:34px; padding:.5rem .625rem; resize:vertical; } - .warehouse-quote-field input:focus,.warehouse-quote-field textarea:focus { border-color:var(--quote-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); } + .warehouse-quote-field input:focus,.warehouse-quote-field select:focus,.warehouse-quote-field textarea:focus { border-color:var(--quote-accent); outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); } .warehouse-quote-formulas { display:grid; grid-template-columns:repeat(3,minmax(0,1fr)); gap:1px; margin:0 1rem 1rem; overflow:hidden; border:1px solid var(--quote-border); border-radius:.5rem; background:var(--quote-border); } .warehouse-quote-formula { min-width:0; padding:.7rem .75rem; background:#FCFAF5; } .warehouse-quote-formula dt { color:var(--quote-muted); font-size:.7rem; font-weight:600; } @@ -66,8 +66,34 @@ .warehouse-quote-card dt { color:var(--quote-muted); font-size:.7rem; font-weight:600; } .warehouse-quote-card dd { margin:.2rem 0 0; font-size:1rem; font-weight:700; font-variant-numeric:tabular-nums; } .warehouse-quote-card--total dd { color:var(--quote-accent); } + .warehouse-settlement-actions { display:flex; align-items:center; gap:.65rem; } + .warehouse-settlement-detail { border-top:1px solid var(--quote-border); } + .warehouse-settlement-detail__head { display:flex; align-items:flex-end; justify-content:space-between; gap:1rem; padding:1rem; } + .warehouse-settlement-detail__title { display:flex; min-width:0; align-items:center; gap:.55rem; } + .warehouse-settlement-detail__title .material-icons-outlined { color:var(--quote-accent); font-size:20px; } + .warehouse-settlement-detail__controls { display:flex; align-items:flex-end; gap:.65rem; } + .warehouse-settlement-detail__close { display:grid; width:34px; height:34px; place-items:center; border:1px solid var(--quote-border); border-radius:.5rem; background:#fff; color:var(--quote-muted); cursor:pointer; } + .warehouse-settlement-detail__close:hover { border-color:var(--quote-accent); background:#FCFAF5; color:var(--quote-ink); } + .warehouse-settlement-detail__close:focus-visible { outline:none; box-shadow:0 0 0 3px rgba(133,102,50,.2); } + .warehouse-settlement-detail__close .material-icons-outlined { font-size:18px; } + .warehouse-settlement-detail__summary { display:grid; grid-template-columns:repeat(5,minmax(0,1fr)); border-top:1px solid var(--quote-border); border-bottom:1px solid var(--quote-border); background:#FCFAF5; } + .warehouse-settlement-detail__metric { min-width:0; padding:.65rem 1rem; border-left:1px solid var(--quote-border); } + .warehouse-settlement-detail__metric:first-child { border-left:0; } + .warehouse-settlement-detail__metric dt { color:var(--quote-muted); font-size:.7rem; font-weight:600; } + .warehouse-settlement-detail__metric dd { margin:.15rem 0 0; font-size:.875rem; font-weight:600; font-variant-numeric:tabular-nums; } + .warehouse-settlement-detail__table-wrap { max-height:min(32rem,65vh); overflow:auto; } + .warehouse-settlement-detail__table { min-width:76rem; } + .warehouse-settlement-detail__table th { position:sticky; top:0; z-index:1; } + .warehouse-settlement-detail__notice { margin:0; padding:1rem; border-top:1px solid var(--quote-border); color:var(--quote-muted); } @media (max-width:1050px){ .warehouse-quote-cards { grid-template-columns:repeat(2,minmax(0,1fr)); } } - @media (max-width:640px){ .warehouse-quote-cards { grid-template-columns:1fr; } } + @media (max-width:760px){ .warehouse-settlement-detail__summary { grid-template-columns:repeat(2,minmax(0,1fr)); } .warehouse-settlement-detail__metric { border-top:1px solid var(--quote-border); } } + @media (max-width:640px){ + .warehouse-quote-cards,.warehouse-settlement-detail__summary { grid-template-columns:1fr; } + .warehouse-settlement-detail__head { align-items:flex-start; flex-direction:column; } + .warehouse-settlement-detail__controls { width:100%; } + .warehouse-settlement-detail__controls .warehouse-quote-field { flex:1; max-width:none; } + .warehouse-settlement-detail__metric { border-left:0; } + }
@@ -170,7 +196,7 @@

仓储费核算

-
选择结算月份,一键按当期生效报价核算并存档,可导出核算明细(汇总/每日/逐商品)
+
选择结算月份,一键按当期生效报价核算并存档,可查看和导出每日商品仓储费
@if ($settlementError) @@ -188,6 +214,9 @@ 核算仓储费 @if ($settlementSummary) + @@ -219,7 +248,10 @@ {{ number_format($item['total_fee'], 2) }} {{ $item['calculated_at'] }} - +
+ + +
@empty @@ -228,6 +260,73 @@
+ @if ($settlementDetailPeriod) +
+
+
+ +
+

每日商品仓储费明细

+
{{ $settlementDetailPeriod }} · 上月结转库存按商品编码逐日延续
+
+
+
+ @if ($settlementDetailDates !== []) +
+ + +
+ @endif + +
+
+ @if ($settlementDetailMessage) +

{{ $settlementDetailMessage }}

+ @elseif ($settlementDetailSummary) +
+
商品编码数
{{ number_format($settlementDetailSummary['sku_count']) }}
+
上日结余
{{ number_format($settlementDetailSummary['opening'], 2) }}
+
当日进出净额
{{ number_format($settlementDetailSummary['net_movement'], 2) }}
+
当日库存结余
{{ number_format($settlementDetailSummary['closing'], 2) }}
+
当天仓储费
{{ number_format($settlementDetailSummary['storage_fee'], 4) }}
+
+
+ + + + + + + + + + @forelse ($settlementDetailRows as $row) + + + + + + + + + + + + @empty + + @endforelse + +
商品编码商品名称供应商分类单位上日结余当日进出净额当日库存结余当天仓储费
{{ $row['sku_code'] }}{{ $row['product_name'] }}{{ $row['supplier'] }}{{ $row['category'] }}{{ $row['unit'] }}{{ number_format($row['opening'], 2) }}{{ number_format($row['net_movement'], 2) }}{{ number_format($row['closing'], 2) }}{{ number_format($row['storage_fee'], 6) }}
该日期没有商品库存明细。
+
+ @endif +
+ @endif
diff --git a/tests/Feature/Business/ThirdPartyWarehouseQuotePageTest.php b/tests/Feature/Business/ThirdPartyWarehouseQuotePageTest.php index 6f93701a..2c880eaa 100644 --- a/tests/Feature/Business/ThirdPartyWarehouseQuotePageTest.php +++ b/tests/Feature/Business/ThirdPartyWarehouseQuotePageTest.php @@ -5,6 +5,7 @@ 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 Tests\Feature\FeatureTestCase; @@ -120,6 +121,57 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase ->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') + ->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') + ->assertSee('原存档没有每日商品明细,请重新核算该月份后查看'); + } + + 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 的核算存档,请先核算。'); + } + private function createQuote(): ThirdPartyWarehouseQuote { return ThirdPartyWarehouseQuote::query()->create([ @@ -137,4 +189,38 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase 'updated_by' => $this->user->id, ]); } + + /** @param list> $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(), + ]); + } } diff --git a/tests/Feature/Business/ThirdPartyWarehouseStorageFeeCalculatorTest.php b/tests/Feature/Business/ThirdPartyWarehouseStorageFeeCalculatorTest.php index f1c6b76b..cf58011e 100644 --- a/tests/Feature/Business/ThirdPartyWarehouseStorageFeeCalculatorTest.php +++ b/tests/Feature/Business/ThirdPartyWarehouseStorageFeeCalculatorTest.php @@ -4,6 +4,7 @@ namespace Tests\Feature\Business; use App\Business\ThirdPartyWarehouseProductWeightRepository; use App\Business\ThirdPartyWarehouseStorageFeeCalculator; +use App\Exports\ThirdPartyWarehouseStorageSettlementExport; use App\Models\Business\ThirdPartyWarehouseQuote; use Mockery; use Modules\EcommerceFinance\Repositories\ThirdPartyWarehouseStorageMirrorRepository; @@ -67,6 +68,24 @@ class ThirdPartyWarehouseStorageFeeCalculatorTest extends FeatureTestCase $this->assertSame(round(46.0 + 8.5 + $expectedStorage, 2), (float) $summary['total_fee']); $this->assertCount(2, $result['daily']); $this->assertCount(2, $result['per_sku']); + $this->assertCount(3, $result['daily_sku']); + + $firstDay = collect($result['daily_sku'])->where('date', '2026-08-01'); + $carriedStock = $firstDay->firstWhere('sku_code', 'A'); + $this->assertSame(100.0, (float) $carriedStock['opening']); + $this->assertSame(0.0, (float) $carriedStock['net_movement']); + $this->assertSame(100.0, (float) $carriedStock['closing']); + $this->assertEqualsWithDelta(100 / 23 * 3 * 10.5 / 31, $carriedStock['storage_fee'], 0.000001); + $this->assertEqualsWithDelta( + $result['daily'][0]['storage_fee'], + $firstDay->sum('storage_fee'), + 0.0001, + ); + + $sheets = (new ThirdPartyWarehouseStorageSettlementExport($result))->sheets(); + $this->assertCount(4, $sheets); + $this->assertSame('每日商品仓储费', $sheets[3]->title()); + $this->assertSame('A', $sheets[3]->array()[0][2]); } public function test_requires_effective_quote(): void