From: fomichev Date: Tue, 10 Jun 2025 15:30:26 +0000 (+0300) Subject: Рефакторинг цен X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=913a6a151ef97d1c97e329515b29d8b0c14892e9;p=erp24_rep%2Fyii-erp24%2F.git Рефакторинг цен --- diff --git a/erp24/controllers/AutoPlannogrammaController.php b/erp24/controllers/AutoPlannogrammaController.php index 791d0196..23e6fdbd 100644 --- a/erp24/controllers/AutoPlannogrammaController.php +++ b/erp24/controllers/AutoPlannogrammaController.php @@ -866,7 +866,35 @@ class AutoPlannogrammaController extends BaseController // Обработка даты на год и месяц if (!empty($filters['year']) && !empty($filters['month'])) { $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01'; + $periodEnd = (new \DateTime())->format('Y-m-d H:i:s'); // «сейчас» + $periodStart = (new \DateTime())->modify('-20 days')->format('Y-m-d H:i:s'); // «сейчас – 2 дня» + $prices = PricesDynamic::find() + + ->where(['product_id' => ['2b5a5fcd-03d4-11ef-84e6-ac1f6b1b7573', '2b72702a-792f-11e8-9edd-1c6f659fb563']]) + + ->andWhere(['<=', 'date_from', $periodEnd]) + ->andWhere(['>=', 'date_to', $periodStart]) + ->orderBy(['date_from' => SORT_ASC]) + ->asArray() + ->all(); + + $pricesMap = []; + foreach ($prices as $price) { + if ($price['product_id'] == '2b72702a-792f-11e8-9edd-1c6f659fb563') { + if ( empty($pricesMap[$price['product_id']][$price['region_id']])) { + $pricesMap[$price['product_id']][$price['region_id']][] = $price['price']; + } else { + $pricesMap[$price['product_id']][$price['region_id']][0] = min($price['price'], $pricesMap[$price['product_id']][$price['region_id']][0]); + } + } else { + if ($price['active'] !== 1) { + continue; + } + $pricesMap[$price['product_id']][$price['region_id']][] = $price['price']; + } + } + var_dump($prices);die(); $service = new AutoPlannogrammaService(); //$goals = $service->calculateFullGoalChain($filters); @@ -912,6 +940,8 @@ class AutoPlannogrammaController extends BaseController $productSalesShare ); + + /* $matrixForecast = MatrixBouquetForecast::find() ->where(['year' => $filters['year'], 'month' => $filters['month']]) ->asArray() @@ -1345,7 +1375,6 @@ class AutoPlannogrammaController extends BaseController $productForecastSpecies = $service->calculateProductSalesBySpecies($salesProductForecastShare, $cleanedSpeciesGoals); - $weeklySales = $service->getHistoricalSpeciesShareByWeek($filters['plan_date'], $filters); $weeklySalesForecast = $service->calculateWeeklyProductForecastPieces($productForecastSpecies, $weeklySales); @@ -1703,6 +1732,7 @@ class AutoPlannogrammaController extends BaseController foreach ($weeksData as $r) { $forecasts = $service->calculateWeekForecastSpeciesProducts($r['category'], $r['subcategory'], $r['species'], $r['store_id'], $r['weekly_goal']); + foreach ($forecasts as $forecast) { $weeksProductForecast[] = [ 'category' => $forecast['category'] ?? '', diff --git a/erp24/services/AutoPlannogrammaService.php b/erp24/services/AutoPlannogrammaService.php index cd70178f..6095c773 100644 --- a/erp24/services/AutoPlannogrammaService.php +++ b/erp24/services/AutoPlannogrammaService.php @@ -29,6 +29,7 @@ class AutoPlannogrammaService public const TYPE_WRITE_OFFS = 'writeOffs'; // Тип операции: списания private const CATEGORY_LOOKBACK_MONTHS = 3; // Период для анализа категорий (месяцы) private const LOOKBACK_MONTHS = 2; // Отступаемый шаг от плановой даты перед расчетами + const SPECIAL_PRODUCT = '2b72702a-792f-11e8-9edd-1c6f659fb563'; /** * Получение списка видимых магазинов @@ -1508,28 +1509,22 @@ class AutoPlannogrammaService ? BouquetComposition::REGION_MSK : BouquetComposition::REGION_NN; } - $priceRecords = PricesDynamic::find() - ->select(['product_id', 'price']) - ->where(['product_id' => $productsIds]) - ->andWhere(['active' => 1]) - ->andWhere(['region_id' => $region]) - ->indexBy('product_id') - ->asArray() - ->all(); - foreach ($priceRecords as $id => $record) { - if ($goal == 0 || (int)$record['price'] == 0) { + $pricesMap = self::buildPricesMap($productsIds, self::SPECIAL_PRODUCT, $region); + + foreach ($pricesMap as $id => $price) { + if ($goal == 0 || (float)$price == 0) { $forecast = 0; } else { - $forecast = round(max($goal / (float)$record['price'], 1), 0); + $forecast = round(max($goal / (float)$price, 1), 0); } $speciesProductForecast[] = [ 'category' => $category, 'subcategory' => $subcategory, 'species' => $species, - 'product_id' => $record['product_id'], + 'product_id' => $id, 'name' => $products[$id]['name'], - 'price' => $record['price'] ?? $goal ?? 1, + 'price' => $price ?? $goal ?? 1, 'goal' => $goal ?? 0, 'forecast' => $forecast @@ -1670,7 +1665,6 @@ class AutoPlannogrammaService $year, $goals, $productSalesShare - ); @@ -1723,142 +1717,6 @@ class AutoPlannogrammaService return $result; } - /** - * @param array|int $storeIds - * @param int $month - * @param int $year - * @param int $regionId - * @return array список строк с полями: - * sale_id, sale_date, product_id, product_name, - * component_guid, component_name, component_category, - * quantity, price, cost - */ - public function getUnmarkedProductsComponents(int $storeId, string $month, string $year, int $regionId = null, $typeFilter = null): array - { - $storeParams = CityStoreParams::find() - ->where(['store_id' => $storeId]) - ->one(); - - $region = $storeParams?->address_region; - - if (!$region && !$regionId) { - $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar(); - $region = $cityId == 1 - ? BouquetComposition::REGION_MSK - : BouquetComposition::REGION_NN; - } - - $monthStart = sprintf('%04d-%02d-01 00:00:00', $year, $month); - $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $month, $year); - $monthEnd = sprintf('%04d-%02d-%02d 23:59:59', $year, $month, $daysInMonth); - $salesProducts = Sales::find() - ->alias('s') - ->select(['s.id', 's.date', 'sp.product_id', 'p1c.type', 'p1c.components' , 'p1c.name']) - ->innerJoin( - ['sp' => SalesProducts::tableName()], - 's.id = sp.check_id' - ) - ->innerJoin( - ['p1c' => Products1c::tableName()], - 'p1c.id = sp.product_id' - ) - ->leftJoin( - ['nom' => Products1cNomenclature::tableName()], - 'nom.id = sp.product_id' - ) - ->andWhere(['s.store_id' => $storeId]) - ->andWhere(['between', 's.date', $monthStart, $monthEnd]) - ->andWhere(['not', ['p1c.components' => '']]) - ->andWhere(['nom.category' => null]) - ->asArray() - ->all(); - - $components = []; - $rows = []; - foreach ($salesProducts as $sp) { - /** @var SalesProducts $sp */ - $js = trim($sp['components']); - if ($js === '' || $js[0] !== '{') { - continue; - } - $data = @json_decode($js, true); - if (!is_array($data)) { - continue; - } - foreach ($data as $guid => $qty) { - $qty = (int)$qty; - if ($qty <= 0) { - continue; - } - - $components[$guid] = true; - - $rows[] = [ - 'sale_id' => $sp['id'], - 'sale_date' => $sp['date'], - 'product_id' => $sp['product_id'], - 'product_name'=> $sp['name'], - 'component_guid' => $guid, - 'quantity' => $qty, - ]; - } - } - - if (empty($rows)) { - return []; - } - $guids = array_keys($components); - - $nomenclatures = Products1cNomenclature::find() - ->andWhere(['id' => $guids]) - ->indexBy('id') - ->all(); - - $priceDynamics = PricesDynamic::find() - ->andWhere(['region_id' => $region]) - ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )]) - ->orderBy(['date_from' => SORT_DESC]) - ->all(); - - $pricesByProduct = []; - foreach ($priceDynamics as $pd) { - /** @var PricesDynamic $pd */ - $pid = $pd->product_id; - $pricesByProduct[$pid][] = $pd; - } - - $result = []; - foreach ($rows as $r) { - $guid = $r['component_guid']; - $n = $nomenclatures[$guid] ?? null; - $pid = $n?->id; - $price = 0; - if ($pid && isset($pricesByProduct[$pid])) { - foreach ($pricesByProduct[$pid] as $pd) { - if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) { - $price = $pd->price; - break; - } - } - } - $cost = $r['quantity'] * $price; - - $result[] = [ - 'sale_id' => $r['sale_id'], - 'sale_date' => $r['sale_date'], - 'product_id' => $r['product_id'], - 'product_name' => $r['product_name'], - 'component_guid' => $guid, - 'component_name' => $n?->name, - 'component_category' => $n?->category, - 'quantity' => $r['quantity'], - 'price' => $price, - 'cost' => $cost, - ]; - } - - return $result; - } public function calculateProductForecastInPiecesProductsWithHistory( int $storeId, @@ -1887,13 +1745,8 @@ class AutoPlannogrammaService ? BouquetComposition::REGION_MSK : BouquetComposition::REGION_NN; } - $priceRecords = PricesDynamic::find() - ->where([ - 'product_id' => array_keys($productSalesShare), - 'region_id' => $region, - 'active' => 1, - ]) - ->indexBy('product_id')->asArray()->all(); + + $pricesMap = self::buildPricesMap(array_keys($productSalesShare), self::SPECIAL_PRODUCT, $region); foreach ($productSalesShare as $productId => $data) { $share = $data['share'] ?? 0.0; @@ -1902,7 +1755,7 @@ class AutoPlannogrammaService continue; } - if (!$priceRecords[$productId] || $priceRecords[$productId]['price'] <= 0) { + if (!$pricesMap[$productId] || $pricesMap[$productId] <= 0) { continue; } @@ -1910,7 +1763,7 @@ class AutoPlannogrammaService $cat = $data['category']; $sub = $data['subcategory']; $spec = $data['species']; - $price = $priceRecords[$productId]['price']; + $price = $pricesMap[$productId]; if ( ! isset( $goalsMap[$storeId], @@ -2066,28 +1919,18 @@ class AutoPlannogrammaService ->all(); $products = ArrayHelper::getColumn($productShares, 'product_id'); - $prices = PricesDynamic::find() - ->select(['product_id', 'price', 'region_id']) - ->where(['product_id' => $products]) - ->andWhere(['active' => 1]) - ->asArray() - ->all(); + $pricesMap = self::buildPricesMap($products, self::SPECIAL_PRODUCT); - $pricesMap = []; - foreach ($prices as $price) { - $pricesMap[$price['product_id']][$price['region_id']][] = $price['price']; - } foreach ($productShares as $shareItem) { $storeId = $shareItem['store_id']; $region = $regions[$storeId]['address_region'] ?? BouquetComposition::REGION_NN; - $priceList = $pricesMap[$shareItem['product_id']][$region] ?? null; - $price = is_array($priceList) && count($priceList) > 0 - ? $priceList[0] ?? 1 - : 1; - + if (!$pricesMap[$shareItem['product_id']] || $pricesMap[$shareItem['product_id']][$region] <= 0) { + continue; + } + $price = $pricesMap[$shareItem['product_id']][$region]; $key = implode('|', [ $shareItem['store_id'], @@ -2283,7 +2126,7 @@ class AutoPlannogrammaService $price = 0; $dailyPrices = []; foreach ($pricesByProduct[$productId] ?? [] as $priceRecordForProduct) { - if ($productId == '2b72702a-792f-11e8-9edd-1c6f659fb563') { + if ($productId == self::SPECIAL_PRODUCT) { $saleDay = (new \DateTime($componentDataRecord['sale_date']))->format('Y-m-d'); $fromDay = (new \DateTime($priceRecordForProduct->date_from))->modify('-1 day')->format('Y-m-d'); $toDay = (new \DateTime($priceRecordForProduct->date_to ))->modify('+1 day')->format('Y-m-d'); @@ -2863,4 +2706,82 @@ class AutoPlannogrammaService return array_values($filtered); } + + /** + * Строит карту цен за последние 20 дней. + * + * @param array $productIds Список product_id для выборки + * @param string $specialProductId GUID товара, для которого нужен min + * @param int|null $regionId Если указан — один регион, иначе — несколько + * @return array + */ + public static function buildPricesMap( + array $productIds, + string $specialProductId, + ?int $regionId = null + ): array { + $periodEnd = (new \DateTime())->format('Y-m-d H:i:s'); + $periodStart = (new \DateTime())->modify('-20 days')->format('Y-m-d H:i:s'); + + + $query = PricesDynamic::find() + ->select(['product_id', 'region_id', 'price', 'active', 'date_from', 'date_to']) + ->where(['product_id' => $productIds]) + ->andWhere(['<=', 'date_from', $periodEnd]) + ->andWhere(['>=', 'date_to', $periodStart]) + ->orderBy(['date_from' => SORT_ASC]); + + if ($regionId !== null) { + $query->andWhere(['region_id' => $regionId]); + } + + $priceRecords = $query->asArray()->all(); + $pricesMap = []; + $multiRegion = $regionId === null; + + foreach ($priceRecords as $priceRecord) { + $productId = $priceRecord['product_id']; + $price = (float)$priceRecord['price']; + + $regionId = $multiRegion + ? $priceRecord['region_id'] + : null; + + if ($productId === $specialProductId) { + if ($multiRegion) { + if (!isset($pricesMap[$productId][$regionId])) { + $pricesMap[$productId][$regionId] = [$price]; + } else { + $pricesMap[$productId][$regionId] = min( + $pricesMap[$productId][$regionId], + $price + ); + } + } else { + if (!isset($pricesMap[$productId])) { + $pricesMap[$productId] = [$price]; + } else { + $pricesMap[$productId] = min( + $pricesMap[$productId], + $price + ); + } + } + continue; + } + + if (isset($priceRecord['active']) && $priceRecord['active'] !== 1) { + continue; + } + + if ($multiRegion) { + $pricesMap[$productId][$regionId] = $price; + } else { + $pricesMap[$productId] = $price; + } + } + + return $pricesMap; + } + } \ No newline at end of file