224 lines
9.4 KiB
PHP
224 lines
9.4 KiB
PHP
<?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,
|
||
];
|
||
}
|
||
}
|