argument('month'))->startOfMonth(); $connection = (string) config('upstream_mirrors.connection', 'business_mirror'); $rowsTable = 'product_monthly_weighted_average_rows'; $now = now(); $inventory = $this->inventoryAggregate($connection, $month); $jiangxiPurchase = $this->jiangxiDeliveryPurchase($connection, $month); $nanchangOpening = $this->nanchangOpeningQuantity($connection, $month); $this->info('进销存(排除不看)合计 ' . count($inventory) . ' 个 仓库简称+商品编码'); DB::connection($connection)->table($rowsTable)->where('report_month', $month->toDateString())->delete(); $payload = []; $baseRows = 0; $deliveryRows = 0; $mirrorRows = 0; $file = (string) ($this->option('file') ?? ''); if ($file !== '') { foreach ($this->readBaseFile($file) as $row) { $payload[$this->rowKey($month, $row['warehouse_short_name'], $row['product_code'])] = [ 'report_month' => $month->toDateString(), 'row_key' => $this->rowKey($month, $row['warehouse_short_name'], $row['product_code']), 'barcode_warehouse' => $row['barcode_warehouse'], 'warehouse_short_name' => $row['warehouse_short_name'], 'product_code' => $row['product_code'], 'opening_unit_price' => $row['opening_unit_price'], 'previous_pricing_status' => $row['previous_pricing_status'], 'base_source' => 'base', 'imported_at' => $now, ]; $baseRows++; } } elseif ($this->option('from-previous')) { $previous = $month->subMonthNoOverflow(); foreach (DB::connection($connection)->table($rowsTable) ->where('report_month', $previous->toDateString()) ->get(['barcode_warehouse', 'warehouse_short_name', 'product_code', 'pricing_status', 'weighted_average_unit_price']) as $row) { $key = $this->rowKey($month, $row->warehouse_short_name, $row->product_code); $payload[$key] = [ 'report_month' => $month->toDateString(), 'row_key' => $key, 'barcode_warehouse' => $row->barcode_warehouse, 'warehouse_short_name' => $row->warehouse_short_name, 'product_code' => $row->product_code, 'opening_unit_price' => $row->weighted_average_unit_price, 'previous_pricing_status' => $row->pricing_status, 'base_source' => 'base', 'imported_at' => $now, ]; $baseRows++; } } else { $this->error('请给出 --file=表1.xlsx(首月)或 --from-previous(滚动月份)。'); return self::FAILURE; } // 代发仓:没有库存,但必须出现在这张表里,采购数量取销售出库。 foreach ($inventory as $key => $item) { $rowKey = $this->rowKey($month, $item['warehouse_short_name'], $item['product_code']); if (isset($payload[$rowKey])) { continue; } $isDelivery = $this->isDeliveryWarehouse($item['warehouse_short_name']); $payload[$rowKey] = [ 'report_month' => $month->toDateString(), 'row_key' => $rowKey, // 条码+仓库 = 商品编码 + 仓库简称 'barcode_warehouse' => $item['product_code'] . $item['warehouse_short_name'], 'warehouse_short_name' => $item['warehouse_short_name'], 'product_code' => $item['product_code'], 'opening_unit_price' => null, 'previous_pricing_status' => null, 'base_source' => $isDelivery ? 'delivery' : 'mirror', 'imported_at' => $now, ]; $isDelivery ? $deliveryRows++ : $mirrorRows++; } // 南昌樵舍仓的采购数量取自"简尚-江西仓送货主仓",这些条码也要一并纳入行维度, // 否则只存在于江西仓送货的单据会因为表里没有对应行而丢掉采购数量。 foreach (array_keys($jiangxiPurchase) as $code) { $rowKey = $this->rowKey($month, '南昌樵舍仓', $code); if (isset($payload[$rowKey])) { continue; } $payload[$rowKey] = [ 'report_month' => $month->toDateString(), 'row_key' => $rowKey, 'barcode_warehouse' => $code . '南昌樵舍仓', 'warehouse_short_name' => '南昌樵舍仓', 'product_code' => $code, 'opening_unit_price' => null, 'previous_pricing_status' => null, 'base_source' => 'mirror', 'imported_at' => $now, ]; $mirrorRows++; } $matched = 0; foreach ($payload as $key => $row) { $item = $inventory[$row['warehouse_short_name'] . "\0" . $row['product_code']] ?? null; // 代发仓判定不能只看 base_source(滚动月份会丢标记),按仓库简称再兜一层。 $isDelivery = $row['base_source'] === 'delivery' || str_contains((string) $row['warehouse_short_name'], '代发仓'); $isNanchang = $row['warehouse_short_name'] === '南昌樵舍仓'; // 原始数量只取"主仓"的期初数量(仓库名称含主仓字样)。 $openingQuantity = $isDelivery ? 0.0 : (float) ($item['opening_quantity'] ?? 0); if ($isNanchang) { // 南昌樵舍仓是特殊仓库:期初数量取"三方-南昌樵舍仓主仓"对应商品的期初数量。 $openingQuantity = (float) ($nanchangOpening[$row['product_code']] ?? 0); } $purchaseQuantity = $isDelivery ? abs((float) ($item['sales_out'] ?? 0)) : ($isNanchang // 南昌樵舍仓是特殊仓库:采购数量取"简尚-江西仓送货主仓"对应商品的采购进仓数量。 ? (float) ($jiangxiPurchase[$row['product_code']] ?? 0) : (float) ($item['purchase_in'] ?? 0)); if ($item !== null) { $matched++; } $payload[$key]['product_name'] = $item['category'] ?? null; $payload[$key]['opening_quantity'] = $openingQuantity; $payload[$key]['purchase_quantity'] = $purchaseQuantity; $payload[$key]['opening_original_amount'] = $row['opening_unit_price'] === null ? null : round($openingQuantity * (float) $row['opening_unit_price'], 2); } foreach (array_chunk($payload, 1000, true) as $chunk) { DB::connection($connection)->table($rowsTable)->insert(array_values($chunk)); } DB::connection($connection)->table('product_monthly_weighted_average_states')->updateOrInsert( ['report_month' => $month->toDateString()], [ 'status' => 'base_imported', 'base_filename' => $file !== '' ? basename($file) : null, 'base_rows' => $baseRows, 'delivery_rows' => $deliveryRows, 'matched_rows' => $matched, 'last_error' => null, 'updated_at' => $now, 'created_at' => $now, ], ); $this->info(sprintf( '%s 基数导入完成:基表 %d 行 + 代发仓 %d 行,合计 %d 行,其中 %d 行在进销存总表匹配到数据。', $month->format('Y-m'), $baseRows, $deliveryRows, count($payload), $matched, )); return self::SUCCESS; } /** @return array */ private function inventoryAggregate(string $connection, CarbonImmutable $month): array { $generation = DB::connection($connection)->table('inventory_report_authoritative_months') ->where('source_month', $month->toDateString()) ->value('active_generation'); if (! $generation) { throw new \RuntimeException($month->format('Y-m') . ' 的进销存总表尚未导入。'); } $mappingTable = (new InventoryWarehouseNameMapping)->getConnection()->getDatabaseName() . '.' . DB::connection((new InventoryWarehouseNameMapping)->getConnectionName())->getTablePrefix() . (new InventoryWarehouseNameMapping)->getTable(); $sql = "select m.warehouse_short_name as short_name, r.product_code as product_code, sum(case when r.warehouse like '%主仓%' then r.opening_quantity else 0 end) as opening_quantity, sum(r.purchase_in) as purchase_in, sum(r.sales_out) as sales_out, max(r.category) as category from inventory_report_authoritative_rows r join {$mappingTable} m on m.warehouse_provider = r.warehouse_provider and m.warehouse = r.warehouse and m.company_id = 1 and m.deleted_at is null where r.generation = ? and m.warehouse_short_name not like '%不看%' and r.warehouse like '%主仓%' and m.warehouse_short_name <> '南昌樵舍仓' group by m.warehouse_short_name, r.product_code"; $aggregate = []; foreach (DB::connection($connection)->select($sql, [$generation]) as $row) { $aggregate[$row->short_name . "\0" . mb_strtoupper(trim((string) $row->product_code))] = [ 'warehouse_short_name' => (string) $row->short_name, // 商品编码统一按大写建索引,避免与表1/普通商品资料大小写不一致导致匹配不上。 'product_code' => mb_strtoupper(trim((string) $row->product_code)), 'opening_quantity' => (string) $row->opening_quantity, 'purchase_in' => (string) $row->purchase_in, 'sales_out' => (string) $row->sales_out, 'category' => (string) ($row->category ?? ''), ]; } return $aggregate; } /** @return \Generator */ private function readBaseFile(string $file): \Generator { $reader = IOFactory::createReaderForFile($file); $reader->setReadDataOnly(true); $spreadsheet = $reader->load($file); $sheet = $spreadsheet->getSheetByName('7月份期初基数') ?? $spreadsheet->getSheet(0); foreach ($sheet->getRowIterator(3) as $row) { $cells = []; foreach ($row->getCellIterator('A', 'P') as $cell) { $cells[] = $cell->getFormattedValue(); } $shortName = trim((string) ($cells[2] ?? '')); $productCode = trim((string) ($cells[4] ?? '')); if ($shortName === '' || $productCode === '') { continue; } $price = trim((string) ($cells[7] ?? '')); $previousStatus = trim((string) ($cells[15] ?? '')); yield [ // 条码+仓库统一为「仓库简称 + 商品编码」,不再沿用表1 里的单元格写法。 'barcode_warehouse' => $shortName . $productCode, 'warehouse_short_name' => $shortName, 'product_code' => mb_strtoupper($productCode), // 原始价格保留源表精度(不提前舍入),保证加权平均单价与手工表一致;页面展示仍按两位。 'opening_unit_price' => $price === '' ? null : (float) str_replace(',', '', $price), 'previous_pricing_status' => $previousStatus === '' ? null : $previousStatus, ]; } } private function isDeliveryWarehouse(string $shortName): bool { return str_contains($shortName, '代发'); } /** * 南昌樵舍仓的采购数量取"简尚-江西仓送货主仓"对应商品的采购进仓数量。 * * @return array */ private function jiangxiDeliveryPurchase(string $connection, CarbonImmutable $month): array { $generation = DB::connection($connection)->table('inventory_report_authoritative_months') ->where('source_month', $month->toDateString()) ->value('active_generation'); if (! $generation) { return []; } $map = []; foreach (DB::connection($connection)->table('inventory_report_authoritative_rows') ->where('generation', $generation) ->where('warehouse', '简尚-江西仓送货主仓') ->selectRaw('product_code, sum(purchase_in) as quantity') ->groupBy('product_code') ->get() as $row) { $map[mb_strtoupper(trim((string) $row->product_code))] = (string) $row->quantity; } return $map; } /** * 南昌樵舍仓的期初数量取"三方-南昌樵舍仓主仓"对应商品的期初数量。 * * @return array */ private function nanchangOpeningQuantity(string $connection, CarbonImmutable $month): array { $generation = DB::connection($connection)->table('inventory_report_authoritative_months') ->where('source_month', $month->toDateString()) ->value('active_generation'); if (! $generation) { return []; } $map = []; foreach (DB::connection($connection)->table('inventory_report_authoritative_rows') ->where('generation', $generation) ->where('warehouse', '三方-南昌樵舍仓主仓') ->selectRaw('product_code, sum(opening_quantity) as quantity') ->groupBy('product_code') ->get() as $row) { $map[mb_strtoupper(trim((string) $row->product_code))] = (string) $row->quantity; } return $map; } private function rowKey(CarbonImmutable $month, string $shortName, string $productCode): string { return sha1($month->format('Y-m') . '|' . $shortName . '|' . $productCode); } }