diff --git a/modules/EcommerceFinance/Repositories/ThirdPartyWarehouseStorageMirrorRepository.php b/modules/EcommerceFinance/Repositories/ThirdPartyWarehouseStorageMirrorRepository.php index 5ef5bd60..f8b0260b 100644 --- a/modules/EcommerceFinance/Repositories/ThirdPartyWarehouseStorageMirrorRepository.php +++ b/modules/EcommerceFinance/Repositories/ThirdPartyWarehouseStorageMirrorRepository.php @@ -638,9 +638,9 @@ class ThirdPartyWarehouseStorageMirrorRepository } /** - * The month immediately after an authoritative report starts from that report's closing - * quantity. Current inventory is deliberately ignored because it cannot reproduce the - * historical opening balance of each subwarehouse. + * Every later month starts from the latest authoritative month closing quantity and + * rolls confirmed movements forward to the requested month. Current inventory is + * deliberately ignored because it cannot reproduce historical opening balances. * * @param array{start:string,end:string} $period * @param array $filters @@ -765,13 +765,15 @@ class ThirdPartyWarehouseStorageMirrorRepository ]; } + $authorityEnd = Carbon::parse($authority['source_month']) + ->addMonth()->startOfMonth()->format('Y-m-d 00:00:00'); $movements = $this->confirmedMovements($headerGeneration, $itemGeneration, null) - ->where('h.occurred_at', '>=', $period['start']) + ->where('h.occurred_at', '>=', $authorityEnd) ->where('h.occurred_at', '<', $period['end']) ->whereIn('h.wms_co_id', $pairRows->pluck('wms_co_id')->filter()->unique()->values()->all()) ->whereIn('i.sku_code', $pairRows->pluck('sku_code')->unique()->values()->all()) ->select([ - 'h.wms_co_id', 'h.io_type', 'i.sku_code', + 'h.wms_co_id', 'h.io_type', 'h.occurred_at', 'i.sku_code', 'i.qty as movement_qty', 'i.qty_type as movement_qty_type', ])->selectRaw('COALESCE(NULLIF(h.wh_id, 0), 1) AS wh_id'); $signed = '(COALESCE(mv.movement_qty, 0) * COALESCE(mv.movement_qty_type, 0))'; @@ -780,16 +782,22 @@ class ThirdPartyWarehouseStorageMirrorRepository $join->on('mv.wms_co_id', '=', 'a.wms_co_id') ->on('mv.wh_id', '=', 'a.wh_id') ->on('mv.sku_code', '=', 'a.sku_code'); - })->groupBy([ + })->groupBy([ 'a.owner', 'a.warehouse_provider', 'a.source_warehouse', 'a.wms_co_id', 'a.wh_id', 'a.sku_code', 'a.category', 'a.opening_qty', - ])->select([ - 'a.owner', 'a.warehouse_provider', 'a.source_warehouse', 'a.wms_co_id', - 'a.wh_id', 'a.sku_code', 'a.category', 'a.opening_qty', - ]); + ])->select([ + 'a.owner', 'a.warehouse_provider', 'a.source_warehouse', 'a.wms_co_id', + 'a.wh_id', 'a.sku_code', 'a.category', 'a.opening_qty', + ]); + $monthly->selectRaw( + 'a.opening_qty + COALESCE(SUM(CASE WHEN mv.occurred_at >= ? AND mv.occurred_at < ? ' + . 'THEN ' . $signed . ' ELSE 0 END), 0) AS rolled_opening_qty', + [$authorityEnd, $period['start']], + ); foreach (self::MOVEMENT_COLUMNS as $column => $type) { - $expression = 'COALESCE(SUM(CASE WHEN mv.io_type = ? THEN ' . $signed . ' ELSE 0 END), 0)'; - $monthly->selectRaw($expression . ' AS ' . $column, [$type]); + $expression = 'COALESCE(SUM(CASE WHEN mv.occurred_at >= ? AND mv.occurred_at < ? ' + . 'AND mv.io_type = ? THEN ' . $signed . ' ELSE 0 END), 0)'; + $monthly->selectRaw($expression . ' AS ' . $column, [$period['start'], $period['end'], $type]); } $rows = $monthly->orderBy('a.warehouse_provider')->orderBy('a.wh_id')->orderBy('a.sku_code') @@ -801,7 +809,7 @@ class ThirdPartyWarehouseStorageMirrorRepository trim((string) $row->warehouse_provider), (int) $row->wh_id, ); - $closingQuantity = (float) $row->opening_qty; + $closingQuantity = (float) $row->rolled_opening_qty; foreach (array_keys(self::MOVEMENT_COLUMNS) as $column) { $closingQuantity += (float) $row->{$column}; } @@ -813,7 +821,7 @@ class ThirdPartyWarehouseStorageMirrorRepository 'warehouse' => $warehouse, 'product_code' => $row->sku_code, 'category' => $row->category, - 'opening_quantity' => $row->opening_qty, + 'opening_quantity' => $row->rolled_opening_qty, 'closing_quantity' => $closingQuantity, 'processing_in' => $row->processing_in, 'processing_out' => $row->processing_out, @@ -847,11 +855,11 @@ class ThirdPartyWarehouseStorageMirrorRepository /** @param array{start:string,end:string} $period */ private function previousMonthlyInventoryAuthority(array $period): ?array { - $previousMonth = Carbon::parse($period['start'])->subMonth()->startOfMonth()->toDateString(); $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', $previousMonth) + ->whereDate('source_month', '<', Carbon::parse($period['start'])->startOfMonth()) + ->orderByDesc('source_month') ->first(); if ($state === null || trim((string) $state->active_generation) === '') { return null; @@ -859,7 +867,7 @@ class ThirdPartyWarehouseStorageMirrorRepository return [ 'generation' => (string) $state->active_generation, - 'source_month' => $previousMonth, + 'source_month' => Carbon::parse($state->source_month)->startOfMonth()->toDateString(), ]; } diff --git a/tests/Unit/EcommerceFinance/ThirdPartyWarehouseStorageMirrorRepositoryTest.php b/tests/Unit/EcommerceFinance/ThirdPartyWarehouseStorageMirrorRepositoryTest.php index b71ffe18..b5a30710 100644 --- a/tests/Unit/EcommerceFinance/ThirdPartyWarehouseStorageMirrorRepositoryTest.php +++ b/tests/Unit/EcommerceFinance/ThirdPartyWarehouseStorageMirrorRepositoryTest.php @@ -239,6 +239,37 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase $this->assertSame(5.0, (float) $rows['简尚-崔桥工厂仓主仓']->transfer_in); } + public function test_inventory_report_rolls_multiple_months_from_the_july_authoritative_baseline(): void + { + $this->markReady(); + $this->insertProduct('SKU-ROLL-MULTI'); + $this->insertInventory('SKU-ROLL-MULTI', 999, '2026-09-11 03:21:00'); + $this->insertInventoryReportAuthoritativeRow([ + 'source_month' => '2026-07-01', + 'report_month' => '2026-07-01', + 'warehouse_provider' => '三方-南昌樵舍仓', + 'warehouse' => '三方-南昌樵舍仓主仓', + 'product_code' => 'SKU-ROLL-MULTI', + 'opening_quantity' => 10, + 'closing_quantity' => 12, + ]); + $this->insertMovement('SKU-ROLL-MULTI', '采购进仓', 3, 1, '2026-08-02 10:00:00'); + $this->insertMovement('SKU-ROLL-MULTI', '销售出仓', 2, -1, '2026-09-03 10:00:00'); + + $result = $this->repository->paginateMonthlyInventoryReport([ + 'month' => '2026-09', + 'product_code' => 'SKU-ROLL-MULTI', + 'warehouse_provider' => '', + 'category' => '', + ], 1, 25); + $row = $result['data'][0]; + + $this->assertSame(1, $result['total']); + $this->assertSame(15.0, (float) $row->opening_quantity); + $this->assertSame(13.0, (float) $row->closing_quantity); + $this->assertSame(-2.0, (float) $row->sales_out); + } + public function test_inventory_report_product_code_filter_is_exact(): void { $this->markReady(); @@ -971,16 +1002,18 @@ class ThirdPartyWarehouseStorageMirrorRepositoryTest extends TestCase private function insertInventoryReportAuthoritativeRow(array $overrides = []): void { $generation = '55555555-5555-5555-5555-555555555555'; + $sourceMonth = (string) ($overrides['source_month'] ?? '2026-08-01'); + unset($overrides['source_month']); DB::connection('business_mirror')->table('inventory_report_authoritative_states')->insert([ 'dataset_key' => 'jiansun.inventory-report.authoritative-month', 'status' => 'succeeded', 'active_generation' => $generation, - 'source_month' => '2026-08-01', + 'source_month' => $sourceMonth, ]); DB::connection('business_mirror')->table('inventory_report_authoritative_rows')->insert(array_merge([ 'generation' => $generation, 'row_key' => hash('sha256', 'authoritative-row'), - 'report_month' => '2026-08-01', + 'report_month' => $sourceMonth, 'owner' => '常州简尚家居有限公司', 'warehouse_provider' => '仓储方', 'warehouse' => '子仓',