*/
private static function getPeriods($baseDate, $count, $withWeeks = false, $withWeights = false)
{
- $date = new \DateTime($baseDate);
- $dateFrom = $date->modify('-3 month')->format('Y-m');
+ $date = new \DateTime(date('Y-m-d', $baseDate));
+ $dateFrom = strtotime($date->modify('-2 month')->format('Y-m'));
$periods = [];
for ($i = 1; $i <= $count; $i++) {
$timestamp = strtotime("-{$i} month", $dateFrom);
return $weightedResults;
}
+ /**
+ * Возвращает среднюю цену $productId для месяца, отстоящего на $offset месяцев
+ * от заданного $year-$month, в контексте $storeId.
+ *
+ * Если целевой месяц март 2025, при $offset = 2 будет рассчитана цена за январь 2025.
+ *
+ * @param string $productId
+ * @param int $year
+ * @param int $month
+ * @param int $storeId
+ * @param int $offset количество месяцев назад (по умолчанию 2)
+ * @return float
+ */
+ public static function getPriceForProductAtOffsetMonth(string $productId, int $year, int $month, int $storeId, int $offset = 2): float
+ {
+ $date = new \DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
+ $target = $date->modify(sprintf('-%d months', $offset));
+ $tYear = (int)$target->format('Y');
+ $tMonth = (int)$target->format('m');
+
+ $monthStart = sprintf('%04d-%02d-01', $tYear, $tMonth);
+ $daysInMonth = cal_days_in_month(CAL_GREGORIAN, $tMonth, $tYear);
+ $monthEnd = sprintf('%04d-%02d-%02d', $tYear, $tMonth, $daysInMonth);
+
+ $priceRecords = PricesDynamic::find()
+ ->where(['product_id' => $productId])
+ ->andWhere(['<=', 'date_from', $monthEnd])
+ ->andWhere([
+ 'or',
+ ['>=', 'date_to', $monthStart],
+ ['date_to' => '2100-01-01 03:00:00+03']
+ ])
+ ->all();
+
+ // определяем регион по городу
+ $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
+ if ($cityId == 1342) {
+ $region = BouquetComposition::REGION_NN;
+ } elseif ($cityId == 1) {
+ $region = BouquetComposition::REGION_MSK;
+ } else {
+ $region = null;
+ }
+
+ // ->andWhere(['or', ['region_id' => $region], ['region_id' => null]])
+
+ if (empty($priceRecords)) {
+ return 0.0;
+ }
+
+ $total = 0;
+ $count = 0;
+ foreach ($priceRecords as $rec) {
+ if (isset($rec->price)) {
+ $total += $rec->price;
+ $count++;
+ }
+ }
+
+ return $count > 0 ? ($total / $count) : 0.0;
+ }
+
+
+ /**
+ * Рассчитывает итоговые стоимости для товаров без истории, группируя суммы по
+ * category/subcategory/species.
+ *
+ * @param int $storeId
+ * @param int $selectedMonth
+ * @param int $selectedYear
+ * @param array $productsWithoutHistory массив ['guid' => …]
+ * @return array плоский массив строк:
+ * [
+ * [
+ * 'store_id' => …,
+ * 'category' => …,
+ * 'subcategory' => …,
+ * 'species' => …,
+ * 'month' => …,
+ * 'year' => …,
+ * 'sum' => …,
+ * ],
+ * …
+ * ]
+ */
+ public static function calculateCostForProductsWithoutHistory(
+ int $storeId,
+ int $selectedMonth,
+ int $selectedYear,
+ array $weightedProductsWithoutHistory
+ ): array
+ {
+
+ $accumulator = [];
+
+ foreach ($weightedProductsWithoutHistory as $guid => $info) {
+ $quantity = (float)$info['weightedValue'];
+ if ($quantity <= 0) {
+ continue;
+ }
+
+ // цена за два месяца назад
+ $price = self::getPriceForProductAtOffsetMonth(
+ $guid,
+ $selectedYear,
+ $selectedMonth,
+ $storeId,
+ 2
+ );
+
+ $cost = $quantity * $price;
+
+ /** @var Products1cNomenclature $nom */
+ $nom = Products1cNomenclature::findOne($guid);
+ $cat = $nom->category ?? '';
+ $sub = $nom->subcategory ?? '';
+ $sp = $nom->species ?? '';
+
+ $key = implode('|', [$cat, $sub, $sp]);
+ if (!isset($accumulator[$key])) {
+ $accumulator[$key] = [
+ 'store_id' => $storeId,
+ 'category' => $cat,
+ 'subcategory' => $sub,
+ 'species' => $sp,
+ 'month' => $selectedMonth,
+ 'year' => $selectedYear,
+ 'sum' => 0.0,
+ ];
+ }
+ $accumulator[$key]['sum'] += $cost;
+ }
+
+ return array_values($accumulator);
+ }
+
+
+ /**
+ * Общий расчёт плана для заданной категории товаров без истории.
+ *
+ * @param int $storeId
+ * @param string $yearMonth строка "YYYY-MM", например "2025-03"
+ * @param string $category
+ * @param string|null $subcategory
+ * @param string|null $species
+ * @return array [
+ * [
+ * 'store_id' => …,
+ * 'category' => …,
+ * 'subcategory' => …,
+ * 'species' => …,
+ * 'month' => …,
+ * 'year' => …,
+ * 'sum' => …,
+ * ],
+ * …
+ * ]
+ *
+ */
+ public static function calculateSpeciesForecastForProductsWithoutHistory(
+ int $storeId,
+ string $month,
+ string $year,
+ string $category,
+ ?string $subcategory,
+ ?string $species
+ ): array {
+
+ $histResult = self::calculateHistoricalShare(
+ $storeId,
+ $month,
+ $year,
+ $category,
+ $subcategory,
+ $species
+ );
+ $productsWithoutHistory = $histResult['without_history'] ?? [];
+
+ if (empty($productsWithoutHistory)) {
+ return [];
+ }
+
+ $weightedResults = self::calculateWeightedSalesForProductsWithoutHistory(
+ $storeId,
+ $month,
+ $year,
+ $productsWithoutHistory
+ );
+
+ if (empty($weightedResults)) {
+ return [];
+ }
+
+ $costs = self::calculateCostForProductsWithoutHistory(
+ $storeId,
+ $month,
+ $year,
+ $weightedResults
+ );
+
+ return $costs;
+ }
/**
* Получает идентификаторы товаров, похожих на указанный товар,