CaiWuXiTong/app/Console/Commands/ImportProductMonthlyWeightedAverageBase.php

239 lines
11 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<?php
namespace App\Console\Commands;
use App\Models\Business\InventoryWarehouseNameMapping;
use Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
use PhpOffice\PhpSpreadsheet\IOFactory;
/**
* 商品月加权平均单价:导入某个月的期初基数。
*
* 基数来源两类:
* 1) 首个可用月份(7月)来自财务提供的《商品月末加权平均单价》表1,包含仓库简称、商品编码和上月加权平均单价;
* 2) 其余月份由上月算出的加权平均单价滚动生成(--from-previous)。
* 无论哪一类,都会并入进销存总表里"代发仓"的商品(代发仓没有库存,采购数量取销售出库)。
*/
class ImportProductMonthlyWeightedAverageBase extends Command
{
protected $signature = 'product:monthly-weighted-average:base
{month : 报表月份,例如 2026-07}
{--file= : 期初基数 Excel(表1),首月使用}
{--from-previous : 用上月的加权平均单价作为本月原始价格}';
protected $description = '导入/生成商品月加权平均单价的期初基数';
public function handle(): int
{
$month = CarbonImmutable::parse($this->argument('month'))->startOfMonth();
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$rowsTable = 'product_monthly_weighted_average_rows';
$now = now();
$inventory = $this->inventoryAggregate($connection, $month);
$this->info('进销存(排除不看)合计 ' . count($inventory) . ' 个 仓库简称+商品编码');
DB::connection($connection)->table($rowsTable)->where('report_month', $month->toDateString())->delete();
$payload = [];
$baseRows = 0;
$deliveryRows = 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) {
if (! $this->isDeliveryWarehouse($item['warehouse_short_name'])) {
continue;
}
$rowKey = $this->rowKey($month, $item['warehouse_short_name'], $item['product_code']);
if (isset($payload[$rowKey])) {
continue;
}
$payload[$rowKey] = [
'report_month' => $month->toDateString(),
'row_key' => $rowKey,
'barcode_warehouse' => $item['warehouse_short_name'] . $item['product_code'],
'warehouse_short_name' => $item['warehouse_short_name'],
'product_code' => $item['product_code'],
'opening_unit_price' => null,
'previous_pricing_status' => null,
'base_source' => 'delivery',
'imported_at' => $now,
];
$deliveryRows++;
}
$matched = 0;
foreach ($payload as $key => $row) {
$item = $inventory[$row['warehouse_short_name'] . "\0" . $row['product_code']] ?? null;
$isDelivery = $row['base_source'] === 'delivery';
$openingQuantity = $isDelivery ? 0.0 : (float) ($item['opening_quantity'] ?? 0);
$purchaseQuantity = $isDelivery
? abs((float) ($item['sales_out'] ?? 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<string,array{warehouse_short_name:string,product_code:string,opening_quantity:string,purchase_in:string,sales_out:string,category:string}> */
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(r.opening_quantity) 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 '%不看%'
group by m.warehouse_short_name, r.product_code";
$aggregate = [];
foreach (DB::connection($connection)->select($sql, [$generation]) as $row) {
$aggregate[$row->short_name . "\0" . $row->product_code] = [
'warehouse_short_name' => (string) $row->short_name,
'product_code' => (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<int,array{barcode_warehouse:string,warehouse_short_name:string,product_code:string,opening_unit_price:?float}> */
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 [
'barcode_warehouse' => trim((string) ($cells[1] ?? '')) ?: $shortName . $productCode,
'warehouse_short_name' => $shortName,
'product_code' => $productCode,
'opening_unit_price' => $price === '' ? null : round((float) str_replace(',', '', $price), 2),
'previous_pricing_status' => $previousStatus === '' ? null : $previousStatus,
];
}
}
private function isDeliveryWarehouse(string $shortName): bool
{
return str_contains($shortName, '代发');
}
private function rowKey(CarbonImmutable $month, string $shortName, string $productCode): string
{
return sha1($month->format('Y-m') . '|' . $shortName . '|' . $productCode);
}
}