CaiWuXiTong/app/Console/Commands/ComputeProductMonthlyWeightedAverage.php

224 lines
9.4 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 Carbon\CarbonImmutable;
use Illuminate\Console\Command;
use Illuminate\Support\Facades\DB;
/**
* 按取数逻辑计算某个月的更改价格、定价状态、采购单价、期初金额、入库金额和加权平均单价。
*/
class ComputeProductMonthlyWeightedAverage extends Command
{
protected $signature = 'product:monthly-weighted-average:compute {month : 报表月份,例如 2026-07}';
protected $description = '按取数逻辑计算商品月加权平均单价';
public function handle(): int
{
$month = CarbonImmutable::parse($this->argument('month'))->startOfMonth();
$connection = (string) config('upstream_mirrors.connection', 'business_mirror');
$table = 'product_monthly_weighted_average_rows';
$deadline = $month->addMonthNoOverflow()->day(2)->startOfDay();
$query = DB::connection($connection)->table($table)->where('report_month', $month->toDateString());
$codes = (clone $query)->distinct()->pluck('product_code')->map(fn ($code) => trim((string) $code))->unique()->values()->all();
$this->info('待计算 SKU:' . count($codes));
$cost = $this->costPrices($connection, $codes);
$work = $this->workPlatformPrices($connection, $codes);
$change = $this->priceChanges($connection, $codes);
$total = (clone $query)->count();
$computed = 0;
$now = now();
for ($offset = 0; $offset < $total; $offset += 1000) {
$rows = (clone $query)->orderBy('row_key')->offset($offset)->limit(1000)
->get([
'row_key', 'barcode_warehouse', 'warehouse_short_name', 'product_code', 'base_source', 'imported_at',
'opening_quantity', 'opening_unit_price', 'previous_pricing_status', 'purchase_quantity',
]);
$updates = [];
foreach ($rows as $row) {
// 普通商品资料与进销存的商品编码大小写不一致,统一按大写匹配。
$code = mb_strtoupper(trim((string) $row->product_code));
$costPrice = is_array($cost) ? ($cost[$code] ?? null) : null;
$workPrice = is_array($work) ? ($work[$code] ?? null) : null;
$changePrice = is_array($change) ? ($change[$code] ?? null) : null;
$payload = $this->calculate(
$row,
$costPrice,
$workPrice,
$changePrice,
$deadline,
);
$updates[] = array_merge([
'report_month' => $month->toDateString(),
'row_key' => $row->row_key,
'barcode_warehouse' => $row->barcode_warehouse,
'warehouse_short_name' => $row->warehouse_short_name,
'product_code' => $row->product_code,
'base_source' => $row->base_source,
'imported_at' => $row->imported_at,
'computed_at' => $now,
], $payload);
$computed++;
}
if ($updates !== []) {
DB::connection($connection)->table($table)->upsert(
$updates,
['report_month', 'row_key'],
['adjusted_unit_price', 'opening_amount', 'purchase_unit_price', 'purchase_amount', 'weighted_average_unit_price', 'pricing_status', 'computed_at'],
);
}
}
DB::connection($connection)->table('product_monthly_weighted_average_states')->updateOrInsert(
['report_month' => $month->toDateString()],
['status' => 'computed', 'computed_rows' => $computed, 'last_computed_at' => $now, 'updated_at' => $now, 'created_at' => $now],
);
$this->info($month->format('Y-m') . " 计算完成:{$computed} 行");
return self::SUCCESS;
}
/**
* @return array<string,float>
*/
private function costPrices(string $connection, array $codes): array
{
$map = [];
foreach (array_chunk($codes, 2000) as $chunk) {
foreach (DB::connection($connection)->table('ops_jst_sku')
->whereIn('sku_id', $chunk)->whereNotNull('cost_price')->get(['sku_id', 'cost_price']) as $row) {
$map[mb_strtoupper(trim((string) $row->sku_id))] = (float) $row->cost_price;
}
}
return $map;
}
/**
* @return array<string,array{price:float,effect:?string}>
*/
private function workPlatformPrices(string $connection, array $codes): array
{
$map = [];
$now = now();
foreach (array_chunk($codes, 2000) as $chunk) {
$rows = DB::connection($connection)->table('js_pm_price_adjustment')
->whereIn('skuCode', $chunk)
->whereNotNull('purchasePrice')
->where(function ($query) use ($now): void {
$query->whereNull('effectTime')->orWhere('effectTime', '<=', $now);
})
->orderBy('effectTime')
->get(['skuCode', 'purchasePrice', 'effectTime']);
foreach ($rows as $row) {
$map[mb_strtoupper(trim((string) $row->skuCode))] = [
'price' => (float) $row->purchasePrice,
'effect' => $row->effectTime === null ? null : (string) $row->effectTime,
];
}
}
return $map;
}
/**
* @return array<string,array{price:float,time:?string}>
*/
private function priceChanges(string $connection, array $codes): array
{
$map = [];
foreach (array_chunk($codes, 2000) as $chunk) {
$rows = DB::connection($connection)->table('js_pm_price_adjustment_log')
->whereIn('skucode', $chunk)
->whereNotNull('afterPrice')
->orderBy('effectStartTime')
->get(['skucode', 'afterPrice', 'effectStartTime']);
foreach ($rows as $row) {
$map[mb_strtoupper(trim((string) $row->skucode))] = [
'price' => (float) $row->afterPrice,
'time' => $row->effectStartTime === null ? null : (string) $row->effectStartTime,
];
}
}
return $map;
}
/**
* @param array{price:float,effect:?string}|null $work
* @param array{price:float,time:?string}|null $change
* @return array<string,mixed>
*/
private function calculate(object $row, ?float $cost, ?array $work, ?array $change, CarbonImmutable $deadline): array
{
$openingQuantity = $row->opening_quantity === null ? 0.0 : (float) $row->opening_quantity;
$purchaseQuantity = $row->purchase_quantity === null ? 0.0 : (float) $row->purchase_quantity;
$openingUnitPrice = $row->opening_unit_price === null ? null : (float) $row->opening_unit_price;
$previousStatus = trim((string) ($row->previous_pricing_status ?? ''));
$changePrice = $change['price'] ?? null;
$workPrice = $work['price'] ?? null;
// 更改价格 + 定价状态(表2 第10条)
if ($openingUnitPrice !== null) {
if ($previousStatus !== '预估价') {
$adjusted = $openingUnitPrice;
$status = $previousStatus !== '' ? $previousStatus : '已定价';
} elseif ($changePrice !== null) {
$adjusted = $changePrice;
$status = '已定价';
} else {
$adjusted = $openingUnitPrice;
$status = '预估价';
}
} elseif ($changePrice !== null) {
$adjusted = $changePrice;
$status = '已定价';
} elseif ($cost !== null) {
$adjusted = $cost;
$status = $workPrice !== null ? '已定价' : '未定价';
} else {
$adjusted = null;
$status = '未定价';
}
// 采购单价(表2 第13条):变价后价格优先,变价时间晚于次月2号取工作平台价格,兜底聚水潭成本价
$changeTime = $change['time'] ?? null;
$lateChange = $changeTime !== null && CarbonImmutable::parse($changeTime)->greaterThanOrEqualTo($deadline);
$purchaseUnitPrice = match (true) {
$changePrice !== null && ! $lateChange => $changePrice,
$lateChange && $workPrice !== null => $workPrice,
$changePrice !== null => $changePrice,
$workPrice !== null => $workPrice,
$cost !== null => $cost,
default => null,
};
$openingAmount = $adjusted === null ? null : round($openingQuantity * $adjusted, 2);
$purchaseAmount = $purchaseUnitPrice === null ? null : round($purchaseQuantity * $purchaseUnitPrice, 2);
$denominator = $openingQuantity + $purchaseQuantity;
if ($denominator === 0.0) {
$weighted = $adjusted === null ? null : round($adjusted, 2);
} else {
$weighted = round((($openingAmount ?? 0) + ($purchaseAmount ?? 0)) / $denominator, 2);
}
return [
'adjusted_unit_price' => $adjusted === null ? null : round($adjusted, 2),
'opening_amount' => $openingAmount,
'purchase_unit_price' => $purchaseUnitPrice === null ? null : round($purchaseUnitPrice, 2),
'purchase_amount' => $purchaseAmount,
'weighted_average_unit_price' => $weighted,
'pricing_status' => $status,
];
}
}