修正进销存月报按已确认进出仓明细取数
This commit is contained in:
parent
d9b42f9cda
commit
0143dfe693
@ -314,47 +314,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
$page = max($page, 1);
|
||||
$perPage = min(max($perPage, 1), 5000);
|
||||
$period = $this->inventoryReportPeriod((string) ($filters['month'] ?? ''));
|
||||
$authoritative = $this->monthlyInventoryAuthoritativeQuery($period, $filters);
|
||||
if ($authoritative !== null) {
|
||||
$total = $includeTotal ? $this->boundedCount(clone $authoritative) : 0;
|
||||
$authoritativePage = (clone $authoritative)
|
||||
->orderBy('warehouse_provider')
|
||||
->orderBy('warehouse')
|
||||
->orderBy('product_code')
|
||||
->forPage($page, $perPage);
|
||||
$rows = $this->boundedGet($authoritativePage)
|
||||
->map(static fn (object $row): object => (object) [
|
||||
'owner' => $row->owner,
|
||||
'month' => $row->report_month,
|
||||
'warehouse_provider' => $row->warehouse_provider,
|
||||
'warehouse' => $row->warehouse,
|
||||
'product_code' => $row->product_code,
|
||||
'category' => $row->category,
|
||||
'opening_quantity' => $row->opening_quantity,
|
||||
'closing_quantity' => $row->closing_quantity,
|
||||
'processing_in' => $row->processing_in,
|
||||
'processing_out' => $row->processing_out,
|
||||
'purchase_in' => $row->purchase_in,
|
||||
'purchase_return' => $row->purchase_return,
|
||||
'other_in' => $row->other_in,
|
||||
'other_return' => $row->other_return,
|
||||
'sales_out' => $row->sales_out,
|
||||
'sales_return' => $row->sales_return,
|
||||
'stocktake' => $row->stocktake,
|
||||
'transfer_out' => $row->transfer_out,
|
||||
'transfer_in' => $row->transfer_in,
|
||||
'opening' => $row->opening,
|
||||
'other_out' => $row->other_out,
|
||||
])
|
||||
->all();
|
||||
|
||||
return [
|
||||
'data' => $rows,
|
||||
'total' => $total,
|
||||
'state' => $state,
|
||||
'sync' => $this->syncStatus($state),
|
||||
];
|
||||
}
|
||||
// 月报的进出数量统一来自聚水潭进出仓明细;权威月报只提供同月的期初维度。
|
||||
$headerGeneration = $this->generation($state, 'active_header_generation');
|
||||
$itemGeneration = $this->generation($state, 'active_item_generation');
|
||||
$inventoryGeneration = $this->generation($state, 'active_inventory_generation');
|
||||
@ -362,7 +322,9 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
throw new RuntimeException('inventory_report_mirror_not_ready');
|
||||
}
|
||||
|
||||
$authority = $this->previousMonthlyInventoryAuthority($period);
|
||||
$currentAuthority = $this->currentMonthlyInventoryAuthority($period);
|
||||
$authority = $currentAuthority ?? $this->previousMonthlyInventoryAuthority($period);
|
||||
$authorityIsCurrentMonth = $currentAuthority !== null;
|
||||
$resolveRollForward = fn (): ?array => $authority === null
|
||||
? null
|
||||
: $this->monthlyInventoryRollForward(
|
||||
@ -376,6 +338,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
$perPage,
|
||||
$includeTotal,
|
||||
$state,
|
||||
$authorityIsCurrentMonth,
|
||||
);
|
||||
$rolledForward = $includeTotal && $perPage <= 100 && $authority !== null
|
||||
? $this->rememberMonthlyInventoryReport(
|
||||
@ -687,6 +650,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
int $perPage,
|
||||
bool $includeTotal,
|
||||
array $state,
|
||||
bool $authorityIsCurrentMonth = false,
|
||||
): ?array {
|
||||
try {
|
||||
$latestProducts = $this->connection->table(self::PRODUCT_TABLE)
|
||||
@ -710,6 +674,9 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
'authority.warehouse_provider',
|
||||
'authority.warehouse',
|
||||
);
|
||||
$authorityOpeningColumn = $authorityIsCurrentMonth
|
||||
? 'authority.opening_quantity'
|
||||
: 'authority.closing_quantity';
|
||||
$authorityCandidates = (clone $authorityRows)
|
||||
->leftJoin(self::WAREHOUSE_TABLE . ' as authority_warehouse', function ($join): void {
|
||||
$join->on('authority_warehouse.name', '=', 'authority.warehouse_provider');
|
||||
@ -721,7 +688,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
'authority.owner AS owner, authority.warehouse_provider AS warehouse_provider, '
|
||||
. 'authority.warehouse AS source_warehouse, authority_warehouse.wms_co_id AS wms_co_id, '
|
||||
. $authorityWarehouseIdSql . ' AS wh_id, authority.product_code AS sku_code, '
|
||||
. 'authority.category AS category, authority.closing_quantity AS opening_qty',
|
||||
. 'authority.category AS category, ' . $authorityOpeningColumn . ' AS opening_qty',
|
||||
$authorityWarehouseIdBindings,
|
||||
);
|
||||
$inventoryDimensions = $this->connection->table(self::SUBWAREHOUSE_INVENTORY_TABLE . ' as inventory')
|
||||
@ -775,9 +742,10 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
|
||||
$authorityEnd = Carbon::parse($authority['source_month'])
|
||||
->addMonth()->startOfMonth()->format('Y-m-d 00:00:00');
|
||||
$movementStart = $authorityIsCurrentMonth ? $period['start'] : $authorityEnd;
|
||||
$movementWarehouseIdSql = 'COALESCE(NULLIF(h.wh_id, 0), 1)';
|
||||
$movementPairs = $this->confirmedMovements($headerGeneration, $itemGeneration, null)
|
||||
->where('h.occurred_at', '>=', $authorityEnd)
|
||||
->where('h.occurred_at', '>=', $movementStart)
|
||||
->where('h.occurred_at', '<', $period['end'])
|
||||
->where('i.qty', '!=', 0)
|
||||
->where('i.qty_type', '!=', 0)
|
||||
@ -810,15 +778,18 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
->whereColumn('prior.product_code', 'movement_pair.sku_code')
|
||||
->whereColumn('prior.warehouse_provider', 'movement_warehouse.name')
|
||||
->whereRaw('prior.warehouse = ' . $movementWarehouseSql, $movementWarehouseBindings);
|
||||
})->whereNotExists(function (Builder $snapshot) use ($inventoryGeneration): void {
|
||||
$snapshot->selectRaw('1')
|
||||
->from(self::SUBWAREHOUSE_INVENTORY_TABLE . ' as matching_inventory')
|
||||
->where('matching_inventory.generation', $inventoryGeneration)
|
||||
->where('matching_inventory.qty', '!=', 0)
|
||||
->whereColumn('matching_inventory.wms_co_id', 'movement_pair.wms_co_id')
|
||||
->whereRaw('COALESCE(NULLIF(matching_inventory.wh_id, 0), 1) = movement_pair.wh_id')
|
||||
->whereColumn('matching_inventory.sku_code', 'movement_pair.sku_code');
|
||||
});
|
||||
if (! $authorityIsCurrentMonth) {
|
||||
$movementDimensions->whereNotExists(function (Builder $snapshot) use ($inventoryGeneration): void {
|
||||
$snapshot->selectRaw('1')
|
||||
->from(self::SUBWAREHOUSE_INVENTORY_TABLE . ' as matching_inventory')
|
||||
->where('matching_inventory.generation', $inventoryGeneration)
|
||||
->where('matching_inventory.qty', '!=', 0)
|
||||
->whereColumn('matching_inventory.wms_co_id', 'movement_pair.wms_co_id')
|
||||
->whereRaw('COALESCE(NULLIF(matching_inventory.wh_id, 0), 1) = movement_pair.wh_id')
|
||||
->whereColumn('matching_inventory.sku_code', 'movement_pair.sku_code');
|
||||
});
|
||||
}
|
||||
$movementCount = clone $movementDimensions;
|
||||
if (trim((string) ($filters['category'] ?? '')) === '') {
|
||||
$movementCount->whereExists(function (Builder $product): void {
|
||||
@ -844,11 +815,16 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
array_merge([$owner], $movementWarehouseBindings),
|
||||
);
|
||||
|
||||
$sources = [
|
||||
[$authorityCandidates, $authorityCandidates, 'authority.warehouse_provider', 'authority.product_code'],
|
||||
[$inventoryDimensions, $inventoryDimensions, 'w.name', 'inventory.sku_code'],
|
||||
[$movementDimensions, $movementCount, 'movement_warehouse.name', 'movement_pair.sku_code'],
|
||||
];
|
||||
$sources = $authorityIsCurrentMonth
|
||||
? [
|
||||
[$authorityCandidates, $authorityCandidates, 'authority.warehouse_provider', 'authority.product_code'],
|
||||
[$movementDimensions, $movementCount, 'movement_warehouse.name', 'movement_pair.sku_code'],
|
||||
]
|
||||
: [
|
||||
[$authorityCandidates, $authorityCandidates, 'authority.warehouse_provider', 'authority.product_code'],
|
||||
[$inventoryDimensions, $inventoryDimensions, 'w.name', 'inventory.sku_code'],
|
||||
[$movementDimensions, $movementCount, 'movement_warehouse.name', 'movement_pair.sku_code'],
|
||||
];
|
||||
$counts = [];
|
||||
$total = 0;
|
||||
if ($includeTotal) {
|
||||
@ -924,7 +900,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
$movementRows = collect();
|
||||
if ($movementPairs->isNotEmpty()) {
|
||||
$movements = $this->confirmedMovements($headerGeneration, $itemGeneration, null)
|
||||
->where('h.occurred_at', '>=', $authorityEnd)
|
||||
->where('h.occurred_at', '>=', $movementStart)
|
||||
->where('h.occurred_at', '<', $period['end'])
|
||||
->where(function (Builder $pairs) use ($movementPairs): void {
|
||||
foreach ($movementPairs as $pair) {
|
||||
@ -1037,7 +1013,7 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
'category' => trim((string) ($filters['category'] ?? '')),
|
||||
];
|
||||
|
||||
return 'inventory-report:monthly-page:v6:' . hash('sha256', (string) json_encode([
|
||||
return 'inventory-report:monthly-page:v10:' . hash('sha256', (string) json_encode([
|
||||
'period' => $period,
|
||||
'filters' => $normalizedFilters,
|
||||
'authority' => $authority,
|
||||
@ -1082,6 +1058,25 @@ class ThirdPartyWarehouseStorageMirrorRepository
|
||||
}
|
||||
}
|
||||
|
||||
/** @param array{start:string,end:string} $period */
|
||||
private function currentMonthlyInventoryAuthority(array $period): ?array
|
||||
{
|
||||
$month = Carbon::parse($period['start'])->startOfMonth();
|
||||
$state = $this->connection->table(self::INVENTORY_REPORT_AUTHORITATIVE_STATE_TABLE)
|
||||
->where('dataset_key', self::INVENTORY_REPORT_AUTHORITATIVE_DATASET_KEY)
|
||||
->whereNotNull('active_generation')
|
||||
->whereDate('source_month', '=', $month->toDateString())
|
||||
->first();
|
||||
if ($state === null || trim((string) $state->active_generation) === '') {
|
||||
return null;
|
||||
}
|
||||
|
||||
return [
|
||||
'generation' => (string) $state->active_generation,
|
||||
'source_month' => $month->toDateString(),
|
||||
];
|
||||
}
|
||||
|
||||
/** @param array{start:string,end:string} $period */
|
||||
private function previousMonthlyInventoryAuthority(array $period): ?array
|
||||
{
|
||||
|
||||
@ -173,9 +173,20 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
public function test_inventory_report_prefers_authoritative_month_rows_with_exact_opening_quantities(): void
|
||||
{
|
||||
$this->markReady();
|
||||
$this->insertProduct('SKU-AUTH');
|
||||
$this->insertProduct('SKU-AUTH', ['category' => '权威分类']);
|
||||
$this->insertInventory('SKU-AUTH', 999, '2026-09-01 00:00:00');
|
||||
$this->insertMovement('SKU-AUTH', '采购进仓', 500, 1, '2026-08-20 10:00:00');
|
||||
$this->insertMovement('SKU-AUTH', '采购进仓', 500, 1, '2026-08-20 10:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertMovement('SKU-AUTH', '销售出仓', 2, -1, '2026-08-21 10:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertMovement('SKU-AUTH', '采购进仓', 90, 1, '2026-09-01 00:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertInventoryReportAuthoritativeRow([
|
||||
'warehouse_provider' => '简尚-崔桥工厂仓',
|
||||
'warehouse' => '简尚-崔桥工厂仓进货仓',
|
||||
@ -197,8 +208,9 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(1, $result['total']);
|
||||
$this->assertSame('简尚-崔桥工厂仓进货仓', $row->warehouse);
|
||||
$this->assertSame(14.0, (float) $row->opening_quantity);
|
||||
$this->assertSame(20.0, (float) $row->closing_quantity);
|
||||
$this->assertSame(6.0, (float) $row->purchase_in);
|
||||
$this->assertSame(512.0, (float) $row->closing_quantity);
|
||||
$this->assertSame(500.0, (float) $row->purchase_in);
|
||||
$this->assertSame(-2.0, (float) $row->sales_out);
|
||||
}
|
||||
|
||||
public function test_inventory_report_rolls_previous_month_closing_into_next_month_opening(): void
|
||||
@ -321,6 +333,19 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
}
|
||||
// 按确认的进出仓明细口径汇总日期、类型、仓储方、仓库、商品和带符号数量。
|
||||
$this->insertMovement('CTG008616N1152V01', '销售出仓', 1, -1, '2026-08-24 09:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertMovement('CTG008616N1152V01', '销售出仓', 2, -1, '2026-08-24 09:30:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertMovement('CTG008616N1152V01', '销售出仓', 999, -1, '2026-08-24 10:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
]);
|
||||
$this->insertMovement('CTG008616N1152V01', '采购进仓', 999, 1, '2026-08-25 10:00:00', [
|
||||
'warehouse_id' => 15038426,
|
||||
'wh_id' => 3,
|
||||
@ -338,8 +363,9 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
$this->assertSame(2, $august['total']);
|
||||
$this->assertSame(0.0, (float) $rows['简尚-崔桥工厂仓进货仓']->opening_quantity);
|
||||
$this->assertSame(110.0, (float) $rows['简尚-崔桥工厂仓进货仓']->purchase_in);
|
||||
$this->assertSame(-1002.0, (float) $rows['简尚-崔桥工厂仓进货仓']->sales_out);
|
||||
$this->assertSame(0.0, (float) $rows['简尚-崔桥工厂仓进货仓']->transfer_out);
|
||||
$this->assertSame(110.0, (float) $rows['简尚-崔桥工厂仓进货仓']->closing_quantity);
|
||||
$this->assertSame(-892.0, (float) $rows['简尚-崔桥工厂仓进货仓']->closing_quantity);
|
||||
|
||||
$secondPage = $this->repository->paginateMonthlyInventoryReport($filters, 2, 1);
|
||||
$this->assertSame(2, $secondPage['total']);
|
||||
@ -349,7 +375,7 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
array_merge($filters, ['category' => '测试分类']), 1, 25,
|
||||
);
|
||||
$this->assertSame(2, $categorized['total']);
|
||||
$this->assertSame(110.0, (float) collect($categorized['data'])
|
||||
$this->assertSame(-892.0, (float) collect($categorized['data'])
|
||||
->firstWhere('warehouse', '简尚-崔桥工厂仓进货仓')->closing_quantity);
|
||||
|
||||
$exported = iterator_to_array($this->repository->exportMonthlyInventoryReportRows($filters, 1));
|
||||
@ -360,8 +386,8 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase
|
||||
array_merge($filters, ['month' => '2026-09']), 1, 25,
|
||||
);
|
||||
$nextRow = collect($september['data'])->firstWhere('warehouse', '简尚-崔桥工厂仓进货仓');
|
||||
$this->assertSame(110.0, (float) $nextRow->opening_quantity);
|
||||
$this->assertSame(110.0, (float) $nextRow->closing_quantity);
|
||||
$this->assertSame(-892.0, (float) $nextRow->opening_quantity);
|
||||
$this->assertSame(-892.0, (float) $nextRow->closing_quantity);
|
||||
}
|
||||
|
||||
public function test_inventory_report_product_code_filter_is_exact(): void
|
||||
|
||||
@ -33,7 +33,7 @@ class ThirdPartyWarehouseStorageUpstreamRepositoryTest extends TestCase
|
||||
$this->assertStringContainsString('newer.[Id] > h.[Id]', $query->selectCalls[0]['sql']);
|
||||
$this->assertStringContainsString('ORDER BY h.[Id]', $query->selectCalls[0]['sql']);
|
||||
$this->assertStringContainsString('h.[io_date] >= ?', $query->selectCalls[0]['sql']);
|
||||
$this->assertSame([100, 200, '2026-08-01'], $query->selectCalls[0]['bindings']);
|
||||
$this->assertSame([100, 200, '2026-07-01'], $query->selectCalls[0]['bindings']);
|
||||
}
|
||||
|
||||
public function test_items_are_parameterized_and_split_into_bounded_io_id_batches(): void
|
||||
@ -108,7 +108,7 @@ class ThirdPartyWarehouseStorageUpstreamRepositoryTest extends TestCase
|
||||
$this->assertStringNotContainsString('[modified] < ? AND [io_date] >= ?', $query->selectCalls[0]['sql']);
|
||||
$this->assertStringNotContainsString('[wms_co_id] = ?', $query->selectCalls[0]['sql']);
|
||||
$this->assertSame('2026-09-11 03:20:00.000', $query->selectCalls[0]['bindings'][500]);
|
||||
$this->assertSame('2026-08-01', $query->selectCalls[0]['bindings'][501]);
|
||||
$this->assertSame('2026-07-01', $query->selectCalls[0]['bindings'][501]);
|
||||
$this->assertSame([1, 2], array_column($rows, 'source_id'));
|
||||
|
||||
$repository->latestHeadersForIoIds([], '2026-09-11 03:20:00.000');
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user