修复仓储核算明细查看并支持导出 #199

Merged
limeng merged 1 commits from fix/limeng-warehouse-settlement-details into main 2026-09-17 15:47:13 +08:00
5 changed files with 157 additions and 46 deletions

View File

@ -0,0 +1,49 @@
<?php
declare(strict_types=1);
namespace App\Exports;
use Maatwebsite\Excel\Concerns\FromArray;
use Maatwebsite\Excel\Concerns\WithHeadings;
use Maatwebsite\Excel\Concerns\WithTitle;
class ThirdPartyWarehouseStorageDetailExport implements FromArray, WithHeadings, WithTitle
{
/** @param list<array<string,mixed>> $rows */
public function __construct(private readonly array $rows)
{
}
public function title(): string
{
return '每日商品仓储费';
}
/** @return array<int,string> */
public function headings(): array
{
return [
'日期', '货主', '商品编码', '商品名称', '供应商', '分类', '单位',
'上日结余', '当日进出净额', '当日库存结余', '当天仓储费',
];
}
/** @return array<int,array<int,mixed>> */
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->rows);
}
}

View File

@ -31,7 +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'] ?? []),
new ThirdPartyWarehouseStorageDetailExport($this->result['daily_sku'] ?? []),
];
}
}
@ -186,43 +186,3 @@ class StorageSettlementSkuSheet implements FromArray, WithHeadings, WithTitle
], $this->perSku);
}
}
class StorageSettlementDailySkuSheet implements FromArray, WithHeadings, WithTitle
{
/** @param list<array<string,mixed>> $dailySku */
public function __construct(private readonly array $dailySku)
{
}
public function title(): string
{
return '每日商品仓储费';
}
/** @return array<int,string> */
public function headings(): array
{
return [
'日期', '货主', '商品编码', '商品名称', '供应商', '分类', '单位',
'上日结余', '当日进出净额', '当日库存结余', '当天仓储费',
];
}
/** @return array<int,array<int,mixed>> */
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);
}
}

View File

@ -4,6 +4,7 @@ namespace App\Http\Livewire\Business;
use App\Business\ThirdPartyWarehouseQuoteRepository;
use App\Business\ThirdPartyWarehouseStorageFeeCalculator;
use App\Exports\ThirdPartyWarehouseStorageDetailExport;
use App\Exports\ThirdPartyWarehouseStorageSettlementExport;
use App\Models\Business\ThirdPartyWarehouseQuote;
use App\Models\Business\ThirdPartyWarehouseStorageSettlement;
@ -181,6 +182,30 @@ class ThirdPartyWarehouseQuotes extends Component
return Excel::download(new ThirdPartyWarehouseStorageSettlementExport($result), $fileName);
}
public function exportSettlementDetails(string $period): ?BinaryFileResponse
{
$this->ensureAuthenticated();
abort_unless(preg_match('/^\d{4}-(0[1-9]|1[0-2])$/', $period) === 1, 422);
$settlement = $this->settlementForPeriod($period);
if ($settlement === null) {
$this->settlementError = '未找到 ' . $period . ' 的核算存档,请先核算。';
return null;
}
$dailySku = ($settlement->detail_json ?: [])['daily_sku'] ?? [];
if ($dailySku === []) {
$this->showMissingSettlementDetails($period);
return null;
}
$fileName = $settlement->warehouse_name . '每日商品仓储费明细_' . $period . '.xlsx';
return Excel::download(new ThirdPartyWarehouseStorageDetailExport($dailySku), $fileName);
}
public function viewSettlementDetails(string $period): void
{
$this->ensureAuthenticated();
@ -194,6 +219,7 @@ class ThirdPartyWarehouseQuotes extends Component
$settlement = $this->settlementForPeriod($period);
if ($settlement === null) {
$this->settlementDetailMessage = '未找到 ' . $period . ' 的核算存档。';
$this->dispatch('warehouse-settlement-details-opened');
return;
}
@ -201,7 +227,7 @@ class ThirdPartyWarehouseQuotes extends Component
$detail = $settlement->detail_json ?: [];
$dailySku = $detail['daily_sku'] ?? [];
if ($dailySku === []) {
$this->settlementDetailMessage = $period . ' 的原存档没有每日商品明细,请重新核算该月份后查看。';
$this->showMissingSettlementDetails($period);
return;
}
@ -210,6 +236,7 @@ class ThirdPartyWarehouseQuotes extends Component
sort($dates);
$this->settlementDetailDates = $dates;
$this->settlementDetailDate = $dates[0] ?? '';
$this->dispatch('warehouse-settlement-details-opened');
}
public function closeSettlementDetails(): void
@ -327,6 +354,16 @@ class ThirdPartyWarehouseQuotes extends Component
->first();
}
private function showMissingSettlementDetails(string $period): void
{
$this->settlementPeriod = $period;
$this->settlementDetailPeriod = $period;
$this->settlementDetailDate = '';
$this->settlementDetailDates = [];
$this->settlementDetailMessage = $period . ' 的原存档没有每日商品明细,请重新核算该月份后查看和导出。';
$this->dispatch('warehouse-settlement-details-opened');
}
/** @return array<string,array<int,string>> */
protected function rules(): array
{

View File

@ -4,7 +4,12 @@
};
@endphp
<div class="warehouse-quote-page" data-warehouse-quote-page>
<div
class="warehouse-quote-page"
data-warehouse-quote-page
x-data
x-on:warehouse-settlement-details-opened.window="$nextTick(() => document.getElementById('warehouse-settlement-detail')?.scrollIntoView({ behavior: 'smooth', block: 'start' }))"
>
<style>
.warehouse-quote-page { display:grid; width:100%; min-width:0; max-width:100%; gap:1rem; }
.warehouse-quote-intro,.warehouse-quote-form,.warehouse-quote-history { width:100%; min-width:0; max-width:100%; border:1px solid var(--quote-border); border-radius:.5rem; background:#fff; }
@ -250,7 +255,7 @@
<td>
<div class="warehouse-settlement-actions">
<button class="quote-action" type="button" wire:click="viewSettlementDetails('{{ $item['period'] }}')" wire:loading.attr="disabled" wire:target="viewSettlementDetails">查看明细</button>
<button class="quote-action" type="button" wire:click="exportSettlement('{{ $item['period'] }}')" wire:loading.attr="disabled" wire:target="exportSettlement">导出</button>
<button class="quote-action" type="button" wire:click="exportSettlement('{{ $item['period'] }}')" wire:loading.attr="disabled" wire:target="exportSettlement">导出核算</button>
</div>
</td>
</tr>
@ -261,7 +266,7 @@
</table>
</div>
@if ($settlementDetailPeriod)
<section class="warehouse-settlement-detail" aria-labelledby="warehouse-settlement-detail-title">
<section class="warehouse-settlement-detail" id="warehouse-settlement-detail" aria-labelledby="warehouse-settlement-detail-title">
<div class="warehouse-settlement-detail__head">
<div class="warehouse-settlement-detail__title">
<span class="material-icons-outlined" aria-hidden="true">inventory_2</span>
@ -271,6 +276,11 @@
</div>
</div>
<div class="warehouse-settlement-detail__controls">
@if ($settlementDetailDates !== [])
<button class="quote-button" type="button" wire:click="exportSettlementDetails('{{ $settlementDetailPeriod }}')" wire:loading.attr="disabled" wire:target="exportSettlementDetails">
<span class="material-icons-outlined" aria-hidden="true">download</span><span>导出整月商品明细</span>
</button>
@endif
@if ($settlementDetailDates !== [])
<div class="warehouse-quote-field">
<label for="warehouse-settlement-detail-date">库存日期</label>

View File

@ -8,6 +8,7 @@ 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
@ -135,7 +136,9 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase
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')
@ -154,7 +157,47 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase
Livewire::test(WarehouseQuoteComponent::class)
->call('viewSettlementDetails', '2026-08')
->assertSee('原存档没有每日商品明细,请重新核算该月份后查看');
->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
@ -169,6 +212,8 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase
->call('viewSettlementDetails', '2026-07')
->assertSet('settlementDetailMessage', '未找到 2026-07 的核算存档。')
->call('exportSettlement', '2026-07')
->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。')
->call('exportSettlementDetails', '2026-07')
->assertSet('settlementError', '未找到 2026-07 的核算存档,请先核算。');
}
@ -190,6 +235,16 @@ class ThirdPartyWarehouseQuotePageTest extends FeatureTestCase
]);
}
/** @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,