]> gitweb.erp-flowers.ru Git - erp24_rep/yii-erp24/.git/commitdiff
Merge branch 'refs/heads/feature_fomichev_erp-419_calculate_cleared_species_month_goa...
authorfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 30 May 2025 12:50:31 +0000 (15:50 +0300)
committerfomichev <vladimir.fomichev@erp-flowers.ru>
Fri, 30 May 2025 12:50:31 +0000 (15:50 +0300)
# Conflicts:
# erp24/controllers/CategoryPlanController.php
# erp24/services/AutoPlannogrammaService.php
# erp24/services/StorePlanService.php

1  2 
erp24/controllers/AutoPlannogrammaController.php
erp24/controllers/CategoryPlanController.php
erp24/services/AutoPlannogrammaService.php
erp24/services/StorePlanService.php

index 34877f1e862e476d1921dd4dfcbfbc314dd70068,5ca42a255a5b7bbf7fd7bd503bbb0643a4f5ea00..dec3bf92ccde3857cdd9fd0d1c4983be070a2c10
@@@ -1072,361 -925,264 +1327,619 @@@ var_dump($yearData); die()
          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
 +    {
 +        $date = new \DateTimeImmutable(sprintf('%04d-%02d-01', $year, $month));
 +
 +
 +        $region = CityStoreParams::find()
 +            ->where(['store_id' => $storeId])
 +            ->one()->address_region;
 +
 +        if (!$regionId && !$region) {
 +            // определяем регион по городу
 +            $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
 +            if ($cityId == 1) {
 +                $region = BouquetComposition::REGION_MSK;
 +            } else {
 +                $region = 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();
 +       // var_dump( $salesProducts); die();
 +
 +        $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') )])
 +           // ->andWhere(['<=', 'date_from', $monthStart])
 +          //  ->andWhere(['>=', 'date_to', $monthEnd])
 +            ->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 static function calculateProductForecastInPiecesProductsWithHistory(
 +        int $storeId,
 +        string $month,
 +        string $category,
 +        string $subcategory,
 +        string $species,
 +        array $productSalesShare,
 +        array $speciesGoals
 +    ): array {
 +        $result = [];
 +
 +        $goal = null;
 +        foreach ($speciesGoals as $item) {
 +            if (
 +                $item['store_id'] == $storeId &&
 +                $item['category'] == $category &&
 +                $item['subcategory'] == $subcategory &&
 +                $item['species'] == $species
 +            ) {
 +                $goal = $item['goal'];
 +                break;
 +            }
 +        }
 +
 +        if ($goal === null) {
 +            return [];
 +        }
 +
 +        $region = CityStoreParams::find()
 +            ->where(['store_id' => $storeId])
 +            ->select('address_region')
 +            ->scalar();
  
 +        if (!$region) {
 +            $cityId = CityStore::find()->select('city_id')->where(['id' => $storeId])->scalar();
 +            $region = ($cityId == 1)
 +                ? BouquetComposition::REGION_MSK
 +                : BouquetComposition::REGION_NN;
 +        }
 +
 +        foreach ($productSalesShare as $productId => $data) {
 +            $share = $data['share'] ?? 0.0;
 +
 +            if ($share <= 0.0) {
 +                continue;
 +            }
 +
 +            $priceRecord = PricesDynamic::find()
 +                ->where([
 +                    'product_id' => $productId,
 +                    'region_id' => $region,
 +                    'active' => 1,
 +                ])
 +                ->one();
 +
 +            if (!$priceRecord || $priceRecord->price <= 0) {
 +                continue;
 +            }
 +
 +            $forecastSum = $goal * $share;
 +
 +            $forecastCount = $forecastSum / $priceRecord->price;
 +
 +            $result[] = [
 +                'product_id' => $productId,
 +                'goal' => $goal,
 +                'goal_share' => round($forecastSum, 2),
 +                'price' => $priceRecord->price,
 +                'forecast_pieces' => round($forecastCount),
 +                'store_id' => $storeId,
 +                'category' => $category,
 +                'subcategory' => $subcategory,
 +                'species' => $species,
 +            ];
 +        }
 +
 +        return $result;
 +    }
 +
 +    /**
 +     * Рассчитывает долю каждого товара в общем прогнозе по штукам.
 +     *
 +     * Объединяет прогнозы без истории и с историей для полного списка товаров.
 +     *
 +     * @param array $pieciesForecastProductsNoHistyory  Результат calculateSpeciesForecastForProductsWithoutHistory
 +     * @param array $pieciesForecastProductWithHistory Результат calculateProductForecastInPiecesProductsWithHistory
 +     * @return array
 +     */
 +    public function calculateProductForecastShare(
 +        array $pieciesForecastProductsNoHistyory,
 +        array $pieciesForecastProductWithHistory
 +    ): array {
 +        $shareResult = [];
 +
 +        $info = $pieciesForecastProductsNoHistyory[0] ?? null;
 +        if (!$info) {
 +            return [];
 +        }
 +
 +        $noHistoryMap = $info['forecasts'] ?? [];
 +
 +        $piecesMap = [];
 +        foreach ($pieciesForecastProductWithHistory as $item) {
 +            if (isset($item['product_id'], $item['forecast_pieces'])) {
 +                $piecesMap[$item['product_id']] = $item['forecast_pieces'];
 +            }
 +        }
 +
 +        $allProductIds = array_merge(
 +                array_keys($noHistoryMap),
 +                array_keys($piecesMap)
 +        );
 +
 +        $quantityMap = [];
 +        foreach ($allProductIds as $pid) {
 +            if (isset($piecesMap[$pid])) {
 +                $quantityMap[$pid] = $piecesMap[$pid];
 +            } elseif (isset($noHistoryMap[$pid])) {
 +                $quantityMap[$pid] = (float)$noHistoryMap[$pid];
 +            } else {
 +                $quantityMap[$pid] = 0;
 +            }
 +        }
 +
 +        $totalPieces = array_sum($quantityMap);
 +        if ($totalPieces <= 0) {
 +            return [];
 +        }
 +
 +        $storeId     = $info['store_id'];
 +        $month       = $info['month'];
 +        $year        = $info['year'];
 +        $category    = $info['category'];
 +        $subcategory = $info['subcategory'];
 +        $species     = $info['species'];
 +
 +        foreach ($quantityMap as $pid => $count) {
 +            $share = $count / $totalPieces;
 +
 +            $shareResult[] = [
 +                'store_id'       => $storeId,
 +                'month'          => $month,
 +                'year'           => $year,
 +                'category'       => $category,
 +                'subcategory'    => $subcategory,
 +                'species'        => $species,
 +                'product_id'     => $pid,
 +                'forecast_pieces'=> $count,
 +                'share'          => round($share, 4),
 +                 'history_status'  => in_array($pid,  array_keys($noHistoryMap)) ? 'No history' : 'With history'
 +            ];
 +        }
 +
 +        return $shareResult;
 +    }
 +
 +    /**
 +     * Рассчитывает продажи по каждому товару внутри вида на основе долей и очищенной цели вида.
 +     *
 +     * @param array $productShares  Результат calculateProductForecastShare
 +     * @param array $speciesGoals   Массив целей по видам с ключом 'goal'
 +     * @return array
 +     */
 +    public static function calculateProductSalesBySpecies(
 +        array $productShares,
 +        array $speciesGoals
 +    ): array {
 +        $result = [];
 +        $goalsMap = [];
 +        foreach ($speciesGoals as $item) {
 +            if (isset($item['store_id'], $item['category'], $item['subcategory'], $item['species'], $item['goal'])) {
 +                $key = implode('|', [
 +                    $item['store_id'],
 +                    $item['category'],
 +                    $item['subcategory'],
 +                    $item['species']
 +                ]);
 +                $goalsMap[$key] = $item['goal'];
 +            }
 +        }
 +
 +        foreach ($productShares as $shareItem) {
 +            $key = implode('|', [
 +                $shareItem['store_id'],
 +                $shareItem['category'],
 +                $shareItem['subcategory'],
 +                $shareItem['species']
 +            ]);
 +            if (!isset($goalsMap[$key])) {
 +                continue;
 +            }
 +            $cleanGoal     = $goalsMap[$key];
 +            $productSales = $shareItem['share'] * $cleanGoal;
 +
 +            $result[] = [
 +                'store_id'      => $shareItem['store_id'],
 +                'month'         => $shareItem['month'] ?? null,
 +                'year'          => $shareItem['year'] ?? null,
 +                'category'      => $shareItem['category'],
 +                'subcategory'   => $shareItem['subcategory'],
 +                'species'       => $shareItem['species'],
 +                'product_id'    => $shareItem['product_id'],
 +                'forecast_pieces' => $shareItem['forecast_pieces'],
 +                'share'          => round($shareItem['share'], 4),
 +                'history_status' => $shareItem['history_status'],
 +                'cleanGoal' => $cleanGoal,
 +                'product_sales' => round($productSales, 2),
 +            ];
 +        }
 +
 +        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 getProductsComponentsInCategory(int $storeId, string $month, string $year, string $type = self::TYPE_SALES): array
+     {
+         $region = CityStoreParams::find()
+             ->where(['store_id' => $storeId])
+             ->one()->address_region ?? null;
+         if (!$region) {
+             $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);
+         $componentProducts = [];
+         if ($type == self::TYPE_SALES) {
+             $salesProducts = Sales::find()
+                 ->alias('s')
+                 ->select([
+                     's.id',
+                     's.date',
+                     's.operation',
+                     'sp.product_id',
+                     'p1c.type',
+                     'p1c.components',
+                     'p1c.name',
+                     'sp.quantity AS quantity_product'
+                 ])
+                 ->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(['not', ['s.operation'   => ['Удален', 'Удаление']]])
+                 ->andWhere(['between', 's.date', $monthStart, $monthEnd])
+                 ->andWhere(['not', ['p1c.components' => '']])
+                 ->asArray()
+                 ->all();
+             $componentProducts = $salesProducts;
+         } else {
+             $writeOffsProducts = WriteOffs::find()
+                 ->alias('w')
+                 ->select([
+                     'w.id AS write_off_id',
+                     'w.date AS write_off_date',
+                     'wp.product_id',
+                     'p1c.type',
+                     'p1c.components',
+                     'p1c.name AS product_name',
+                     'ex.entity_id AS store_id',
+                     'wp.quantity AS quantity_product'
+                 ])
+                 ->innerJoin(
+                     ['wp' => WriteOffsProducts::tableName()],
+                     'wp.write_offs_id = w.id'
+                 )
+                 ->innerJoin(
+                     ['p1c' => Products1c::tableName()],
+                     'p1c.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['nom' => Products1cNomenclature::tableName()],
+                     'nom.id = wp.product_id'
+                 )
+                 ->leftJoin(
+                     ['ex' => ExportImportTable::tableName()],
+                     'ex.export_val = w.store_id'
+                 )
+                 ->andWhere(['between', 'w.date', $monthStart, $monthEnd])
+                 ->andWhere(['ex.entity_id' => $storeId])
+                 ->andWhere(['not', ['p1c.components' => '']])
+                 ->asArray()
+                 ->all();
+             $componentProducts = $writeOffsProducts;
+         }
+         $components = [];
+         $rows       = [];
+         foreach ($componentProducts as $cp) {
+             $js = trim($cp['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[] = [
+                     'record_id'        => $cp['id'] ?? $cp['write_off_id'],
+                     'sale_date'        => $cp['date'] ?? $cp['write_off_date'],
+                     'product_id'       => $cp['product_id'],
+                     'product_name'     => $cp['name'] ?? $cp['product_name'],
+                     'quantity_product' => $cp['quantity_product'],
+                     'component_guid'   => $guid,
+                     'quantity'         => $qty,
+                     'type'             => $type,
+                     'operation'        => $cp['operation'] ?? '',
+                 ];
+             }
+         }
+         if (empty($rows)) {
+             return [];
+         }
+         $guids = array_keys($components);
+         $nomenclatures = Products1cNomenclature::find()
+             ->andWhere(['id' => $guids])
+             ->andWhere(['not in', 'category', ['', 'букет', 'сборка', 'сервис']])
+             ->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) {
+             $pricesByProduct[$pd->product_id][] = $pd;
+         }
+         $result = [];
+         foreach ($rows as $r) {
+             $guid  = $r['component_guid'];
+             $n     = $nomenclatures[$guid] ?? null;
+             $pid   = $n?->id;
+             $price = 0;
+             foreach ($pricesByProduct[$pid] ?? [] as $pd) {
+                 if ($pd->date_from <= $r['sale_date'] && $pd->date_to >= $r['sale_date']) {
+                     if ($pid == '2b72702a-792f-11e8-9edd-1c6f659fb563') {
+                         $price = 8.66; //заглушка исправить
+                     } else {
+                         $price = $pd->price;
+                     }
+                     break;
+                 }
+             }
+             $cost = $r['quantity'] * $price * $r['quantity_product'];
+             $costComponent = $r['quantity'] * $price;
+             $result[] = [
+                 'store_id'             => $storeId,
+                 'record_id'            => $r['record_id'],
+                 'sale_date'            => $r['sale_date'],
+                 'product_id'           => $r['product_id'],
+                 'product_name'         => $r['product_name'],
+                 'quantity_product'     => $r['quantity_product'],
+                 'component_guid'       => $guid,
+                 'component_name'       => $n?->name,
+                 'component_category'   => $n?->category,
+                 'component_subcategory'=> $n?->subcategory,
+                 'component_species'    => $n?->species,
+                 'quantity'             => $r['quantity'],
+                 'price'                => $price,
+                 'cost'                 => $cost,
+                 'component_cost'       => $costComponent,
+                 'type'                 => $type,
+                 'operation'            => $r['operation'],
+             ];
+         }
+         return $result;
+     }
+     public function sumProductsComponentsByGroup(array $items, string $type, string $group = 'category'): array
+     {
+         $aggregated = [];
+         foreach ($items as $row) {
+             $storeId     = $row['store_id'];
+             $category    = $row['component_category'];
+             $subcategory = $row['component_subcategory'];
+             $species     = $row['component_species'];
+             $operation   = $row['operation'] ?? null;
+             $cost = (float)$row['cost'];
+             if (
+                 $type === AutoPlannogrammaService::TYPE_SALES
+                 || ($row['type'] ?? null) === AutoPlannogrammaService::TYPE_SALES
+             ) {
+                 if ($operation === 'Возврат') {
+                     $cost = -$cost;
+                 }
+             }
+             $keyParts = [$storeId, $category];
+             if ($group === 'subcategory' || $group === 'species') {
+                 $keyParts[] = $subcategory;
+             }
+             if ($group === 'species') {
+                 $keyParts[] = $species;
+             }
+             $key = implode('|', $keyParts);
+             if (!isset($aggregated[$key])) {
+                 // базовая структура
+                 $aggregated[$key] = [
+                     'store_id'  => $storeId,
+                     'category'  => $category,
+                     'sum'       => 0.0,
+                     'type'      => $type,
+                 ];
+                 if ($group === 'subcategory' || $group === 'species') {
+                     $aggregated[$key]['subcategory'] = $subcategory;
+                 }
+                 if ($group === 'species') {
+                     $aggregated[$key]['species'] = $species;
+                 }
+             }
+             $aggregated[$key]['sum'] += $cost;
+         }
+         return array_values($aggregated);
+     }
  }
index 5d3f38dd2da23f823b595be6dd588ecdefae234f,20d954b58f3fda719bd142b025d7f4c842758223..b99448cd801b55801a8e522004a19c65c94bb91d
@@@ -430,7 -433,7 +433,6 @@@ class StorePlanServic
                          $hasHistoryInAllPeriods = false;
                      }
                  }
--
                  $weeklySalesData[$periodKey] = $weekData;
              }