增加进销存报表筛选导出 (#219)
This commit is contained in:
parent
4e0dcc7f2b
commit
d8622dfd3f
@ -494,21 +494,7 @@ class Pages extends Controller
|
||||
{
|
||||
$page = max(1, request()->integer('page', 1));
|
||||
$perPage = min(max((int) setting('default.list_limit', 25), 1), 100);
|
||||
$month = trim((string) request('month', now()->startOfMonth()->subMonth()->format('Y-m')));
|
||||
if (preg_match('/^(\d{4})-(\d{2})$/', $month, $matches) !== 1
|
||||
|| ! checkdate((int) $matches[2], 1, (int) $matches[1])) {
|
||||
$month = now()->startOfMonth()->subMonth()->format('Y-m');
|
||||
}
|
||||
$minimumMonth = substr(ThirdPartyWarehouseStorageMirrorRepository::START_DATE, 0, 7);
|
||||
if ($month < $minimumMonth) {
|
||||
$month = $minimumMonth;
|
||||
}
|
||||
$filters = [
|
||||
'month' => $month,
|
||||
'product_code' => mb_substr(trim((string) request('product_code', '')), 0, 100),
|
||||
'warehouse_provider' => mb_substr(trim((string) request('warehouse_provider', '')), 0, 191),
|
||||
'category' => mb_substr(trim((string) request('category', '')), 0, 191),
|
||||
];
|
||||
$filters = $this->inventoryReportFilters(request());
|
||||
$error = null;
|
||||
|
||||
try {
|
||||
@ -545,6 +531,27 @@ class Pages extends Controller
|
||||
]), $error];
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function inventoryReportFilters(Request $request): array
|
||||
{
|
||||
$month = trim((string) $request->query('month', now()->startOfMonth()->subMonth()->format('Y-m')));
|
||||
if (preg_match('/^(\d{4})-(\d{2})$/', $month, $matches) !== 1
|
||||
|| ! checkdate((int) $matches[2], 1, (int) $matches[1])) {
|
||||
$month = now()->startOfMonth()->subMonth()->format('Y-m');
|
||||
}
|
||||
$minimumMonth = substr(ThirdPartyWarehouseStorageMirrorRepository::START_DATE, 0, 7);
|
||||
if ($month < $minimumMonth) {
|
||||
$month = $minimumMonth;
|
||||
}
|
||||
|
||||
return [
|
||||
'month' => $month,
|
||||
'product_code' => mb_substr(trim((string) $request->query('product_code', '')), 0, 100),
|
||||
'warehouse_provider' => mb_substr(trim((string) $request->query('warehouse_provider', '')), 0, 191),
|
||||
'category' => mb_substr(trim((string) $request->query('category', '')), 0, 191),
|
||||
];
|
||||
}
|
||||
|
||||
private function isStoreReceivingAccountPage(string $section, string $page): bool
|
||||
{
|
||||
return $section === 'fund-management' && $page === 'store-receiving-accounts';
|
||||
@ -1050,6 +1057,45 @@ class Pages extends Controller
|
||||
]);
|
||||
}
|
||||
|
||||
public function exportInventoryReport(Request $request): StreamedResponse
|
||||
{
|
||||
abort_unless(
|
||||
$this->thirdPartyWarehouseStorage->available()
|
||||
&& $this->thirdPartyWarehouseStorage->allWarehousesReady(),
|
||||
503,
|
||||
);
|
||||
|
||||
$filters = $this->inventoryReportFilters($request);
|
||||
$productInventory = collect(config('business_navigation'))->firstWhere('slug', 'product-inventory');
|
||||
$inventoryPage = collect($productInventory['pages'] ?? [])->firstWhere('slug', 'inventory-report');
|
||||
$columns = $inventoryPage['directory']['fields'] ?? [];
|
||||
|
||||
return response()->streamDownload(function () use ($filters, $columns): void {
|
||||
$csv = fopen('php://output', 'w');
|
||||
if ($csv === false) {
|
||||
throw new RuntimeException('无法创建进销存报表导出流。');
|
||||
}
|
||||
|
||||
fputs($csv, chr(0xEF) . chr(0xBB) . chr(0xBF));
|
||||
fputcsv($csv, array_column($columns, 'label'));
|
||||
foreach ($this->thirdPartyWarehouseStorage->exportMonthlyInventoryReportRows($filters) as $row) {
|
||||
fputcsv($csv, array_map(function (array $column) use ($row): mixed {
|
||||
$value = data_get($row, $column['name']);
|
||||
|
||||
return $column['name'] === 'product_code'
|
||||
? $this->csvIdentifier($value)
|
||||
: (($column['kind'] ?? null) === 'integer'
|
||||
? ($value === null || $value === '' ? '' : $value)
|
||||
: $this->csvText($value));
|
||||
}, $columns));
|
||||
}
|
||||
fclose($csv);
|
||||
}, '进销存报表_' . str_replace('-', '', $filters['month']) . '_' . now()->format('Ymd_His') . '.csv', [
|
||||
'Content-Type' => 'text/csv; charset=UTF-8',
|
||||
'Cache-Control' => 'private, no-store',
|
||||
]);
|
||||
}
|
||||
|
||||
/** @return array<string,string> */
|
||||
private function thirdPartyWarehouseStorageFilters(Request $request): array
|
||||
{
|
||||
|
||||
@ -238,14 +238,20 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
* @param array<string,mixed> $filters
|
||||
* @return array{data:list<object>,total:int,state:array<string,mixed>,sync:array<string,mixed>}
|
||||
*/
|
||||
public function paginateMonthlyInventoryReport(array $filters, int $page, int $perPage): array
|
||||
public function paginateMonthlyInventoryReport(
|
||||
array $filters,
|
||||
int $page,
|
||||
int $perPage,
|
||||
bool $includeTotal = true,
|
||||
?array $state = null,
|
||||
): array
|
||||
{
|
||||
$state = $this->requireReadyState();
|
||||
$state ??= $this->requireReadyState();
|
||||
if ((int) ($state['scope_version'] ?? 1) < 2) {
|
||||
throw new RuntimeException('inventory_report_all_warehouses_not_ready');
|
||||
}
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 100);
|
||||
$perPage = min(max($perPage, 1), 5000);
|
||||
$period = $this->period([
|
||||
'dimension' => 'month',
|
||||
'period' => $filters['month'] ?? '',
|
||||
@ -296,7 +302,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
$candidates->whereIn('pairs.sku_code', $categorySkus);
|
||||
}
|
||||
|
||||
$total = (clone $candidates)->count();
|
||||
$total = $includeTotal ? (clone $candidates)->count() : 0;
|
||||
$pagePairs = (clone $candidates)
|
||||
->orderBy('pairs.wms_co_id')->orderBy('pairs.sku_code')
|
||||
->forPage($page, $perPage);
|
||||
@ -413,6 +419,46 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $filters
|
||||
* @return Generator<int,object>
|
||||
*/
|
||||
public function exportMonthlyInventoryReportRows(array $filters, int $batchSize = 2000): Generator
|
||||
{
|
||||
$batchSize = min(max($batchSize, 1), 5000);
|
||||
$page = 1;
|
||||
$exported = 0;
|
||||
$total = null;
|
||||
$state = $this->requireReadyState();
|
||||
|
||||
while (true) {
|
||||
$result = $this->paginateMonthlyInventoryReport(
|
||||
$filters,
|
||||
$page,
|
||||
$batchSize,
|
||||
$total === null,
|
||||
$state,
|
||||
);
|
||||
if ($total === null) {
|
||||
$total = (int) ($result['total'] ?? 0);
|
||||
}
|
||||
|
||||
$rows = $result['data'] ?? [];
|
||||
if ($rows === []) {
|
||||
return;
|
||||
}
|
||||
foreach ($rows as $row) {
|
||||
yield $row;
|
||||
}
|
||||
|
||||
$exported += count($rows);
|
||||
if ($exported >= $total || count($rows) < $batchSize) {
|
||||
return;
|
||||
}
|
||||
$page++;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param array<string,mixed> $filters
|
||||
* @return array{count:int,opening_qty:float,closing_qty:float}
|
||||
|
||||
@ -473,6 +473,9 @@
|
||||
<div class="installation-filter-actions">
|
||||
<button type="submit" class="biz-btn biz-btn-primary">查询</button>
|
||||
<a href="{{ request()->url() }}" class="biz-btn biz-btn-ghost">清除</a>
|
||||
<a href="{{ route('business.product-inventory.inventory-report.export', request()->except('page')) }}" class="biz-btn biz-btn-ghost" aria-label="导出当前筛选结果" title="导出当前筛选结果">
|
||||
<span class="material-icons-outlined" aria-hidden="true">download</span>导出
|
||||
</a>
|
||||
</div>
|
||||
</form>
|
||||
@endif
|
||||
|
||||
@ -129,6 +129,10 @@ Route::get('workspace/product-inventory/online-daifa-price/export', 'Business\Pa
|
||||
->middleware('business.page:product-inventory,online-daifa-price')
|
||||
->name('business.product-inventory.online-daifa-price.export');
|
||||
|
||||
Route::get('workspace/product-inventory/inventory-report/export', 'Business\Pages@exportInventoryReport')
|
||||
->middleware('business.page:product-inventory,inventory-report')
|
||||
->name('business.product-inventory.inventory-report.export');
|
||||
|
||||
Route::prefix('workspace/supplier-payables/product-prices')
|
||||
->name('business.supplier-payables.product-prices.')
|
||||
->middleware('business.page:supplier-payables,product-prices')
|
||||
|
||||
@ -14,6 +14,81 @@ use Tests\Feature\FeatureTestCase;
|
||||
|
||||
class PagesTest extends FeatureTestCase
|
||||
{
|
||||
public function testInventoryReportProvidesAFilteredExportButton(): void
|
||||
{
|
||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('paginateMonthlyInventoryReport')->once()->with(
|
||||
[
|
||||
'month' => '2026-08',
|
||||
'product_code' => '121630-49',
|
||||
'warehouse_provider' => '',
|
||||
'category' => '',
|
||||
],
|
||||
1,
|
||||
Mockery::type('int'),
|
||||
)->andReturn([
|
||||
'data' => [(object) ['product_code' => '121630-49']],
|
||||
'total' => 1,
|
||||
'state' => ['status' => 'succeeded'],
|
||||
'sync' => ['code' => 'healthy'],
|
||||
]);
|
||||
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
|
||||
|
||||
$response = $this->loginAs()->get(route('business.pages.show', [
|
||||
'section' => 'product-inventory',
|
||||
'page' => 'inventory-report',
|
||||
'month' => '2026-08',
|
||||
'product_code' => '121630-49',
|
||||
]));
|
||||
|
||||
$response->assertOk()
|
||||
->assertSeeText('导出')
|
||||
->assertSee(e(route('business.product-inventory.inventory-report.export', [
|
||||
'month' => '2026-08',
|
||||
'product_code' => '121630-49',
|
||||
])), false);
|
||||
}
|
||||
|
||||
public function testInventoryReportExportUsesCurrentFiltersAndPageColumnOrder(): void
|
||||
{
|
||||
$filters = [
|
||||
'month' => '2026-08',
|
||||
'product_code' => '121630-49',
|
||||
'warehouse_provider' => '测试仓',
|
||||
'category' => '家具',
|
||||
];
|
||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||
$repository->shouldReceive('available')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('allWarehousesReady')->once()->andReturnTrue();
|
||||
$repository->shouldReceive('exportMonthlyInventoryReportRows')->once()->with($filters)
|
||||
->andReturn((static function (): \Generator {
|
||||
yield (object) [
|
||||
'owner' => '常州简尚家居有限公司',
|
||||
'month' => '2026-08-01',
|
||||
'warehouse_provider' => '测试仓',
|
||||
'warehouse' => '测试仓',
|
||||
'product_code' => '121630-49',
|
||||
'category' => '家具',
|
||||
'opening_quantity' => 10,
|
||||
'closing_quantity' => 12,
|
||||
];
|
||||
})());
|
||||
$this->app->instance(ThirdPartyWarehouseStorageMirrorRepository::class, $repository);
|
||||
|
||||
$response = $this->loginAs()->get(route(
|
||||
'business.product-inventory.inventory-report.export',
|
||||
$filters,
|
||||
));
|
||||
|
||||
$response->assertOk()->assertDownload();
|
||||
$content = $response->streamedContent();
|
||||
$this->assertStringStartsWith(chr(0xEF) . chr(0xBB) . chr(0xBF), $content);
|
||||
$this->assertStringContainsString('货主,月份,仓储方,仓库,商品编码,分类,期初数量,期末数量', $content);
|
||||
$this->assertStringContainsString("'121630-49", $content);
|
||||
}
|
||||
|
||||
public function testInventoryReportDoesNotFallBackToLegacyRowsBeforeAllWarehouseMirrorIsReady(): void
|
||||
{
|
||||
$repository = Mockery::mock(ThirdPartyWarehouseStorageMirrorRepository::class);
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user