修复仓储核算明细查看并支持导出 #199
49
app/Exports/ThirdPartyWarehouseStorageDetailExport.php
Normal file
49
app/Exports/ThirdPartyWarehouseStorageDetailExport.php
Normal 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);
|
||||
}
|
||||
}
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
{
|
||||
|
||||
@ -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>
|
||||
|
||||
@ -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,
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user