use yii_app\records\PricesDynamic;
use yii_app\records\Products1c;
use yii_app\records\Products1cNomenclature;
+ use yii_app\records\CityStoreParams;
+ use yii_app\records\StoreDynamic;
+use yii_app\records\SalesWriteOffsPlan;
use yii_app\services\AutoPlannogrammaService;
use yii_app\services\StorePlanService;
]);
}
-
-
+ public function actionGetSubcategories(string $category, int $year, int $week): array
+ {
+ Yii::$app->response->format = Response::FORMAT_JSON;
+
+ $data = Autoplannogramma::find()
+ ->alias('a')
+ ->leftJoin('products_1c_nomenclature p', 'a.product_id = p.id')
+ ->where(['p.category' => $category])
+ ->andWhere(['a.year' => $year])
+ ->andWhere(['a.week' => $week])
+ ->select([
+ 'p.subcategory as name',
+ new \yii\db\Expression("CASE WHEN COUNT(a.id) > 0 THEN 1 ELSE 0 END AS hasData")
+ ])
+ ->groupBy('p.subcategory')
+ ->asArray()
+ ->all();
+
+ return $data;
+ }
++
+ public function actionWeeklyBouquetProductsForecast()
+ {
+ Yii::$app->response->format = \yii\web\Response::FORMAT_JSON;
+
+ $request = Yii::$app->request;
+ $storeIdRequest = $request->get('storeId', null);
+ $monthRequest = $request->get('month');
+ $yearRequest = $request->get('year');
+ $weekRequest = $request->get('week');
+
+ if (!$monthRequest || !$yearRequest || $weekRequest === null) {
+ return ['success' => false, 'message' => 'Нет параметров'];
+ }
+
+ $service = new AutoPlannogrammaService();
+ $result = $service->getWeeklyBouquetProductsForecast($monthRequest, $yearRequest, $storeIdRequest);
+
+ if (!is_array($result)) {
+ return ['success' => false, 'message' => 'Ошибка структуры данных'];
+ }
+
+ $grouped = [];
+ $salesShares = [];
+
+ $plans = SalesWriteOffsPlan::find()
+ ->where(['month' => $monthRequest, 'year' => $yearRequest])
+ ->indexBy('store_id')
+ ->asArray()
+ ->all();
+ if ($plans) {
+ foreach ($plans as $storeId => $plan) {
+ $total = $plan['total_sales_plan'];
+ $offline = $plan['offline_sales_plan'];
+ $online = $plan['online_sales_shop_plan'];
+ $market = $plan['online_sales_marketplace_plan'];
+ $salesShares[$storeId]['offline'] = round($offline / $total, 4);
+ $salesShares[$storeId]['online'] = round($online / $total, 4);
+ $salesShares[$storeId]['marketplace'] = round($market / $total, 4);
+ }
+ }
+
+ foreach ($result as $item) {
+ $weekItem = (int) $item['week'];
+ $storeItem = (int) $item['store_id'];
+ $guid = (string) $item['product_guid'];
+ $group = (string) $item['matrix_group'];
+ $type = (string) $item['type'];
+ $forecastValue = (float) $item['week_forecast'];
+ if (isset($salesShares[$storeItem]) && isset($salesShares[$storeItem][$type])) {
+ $grouped[$weekItem][$storeItem][$type]['share'] = $salesShares[$storeItem][$type];
+ }
+
+ $grouped[$weekItem][$storeItem][$guid][$type][$group] = $forecastValue;
+ }
+
+
+ if ($weekRequest !== null) {
+ $week = (int) $weekRequest;
+ $grouped = isset($grouped[$week]) ? [$week => $grouped[$week]] : [];
+ }
+
+ return [
+ 'success' => true,
+ 'data' => $grouped,
+ ];
+
+
+ }
}
return $pricesMap;
}
- /* public function getWeeklyBouquetProductsForecast($month, $year, $storeId)
- public function getWeeklyBouquetProductsForecast($month, $year, $storeId = null, $weekNumber = null)
++ /* public function getWeeklyBouquetProductsForecast($month, $year, $storeId = null, $weekNumber = null)
{
$matrixGroups = ArrayHelper::map(
MatrixBouquetForecast::find()->select(['group'])->distinct()->asArray()->all(),
];
}
}
- return $weeklyForecasts;
+ return $weeksProductForecast;
+ }*/
+
+ public function getWeeklyBouquetProductsForecast($month, $year, $storeId = null, $weekNumber = null)
+ {
+ $matrixGroups = ArrayHelper::map(
+ MatrixBouquetForecast::find()->select(['group'])->distinct()->asArray()->all(),
+ 'group',
+ 'group'
+ );
+
+ $date = $year . '-' . str_pad($month, 2, '0', STR_PAD_LEFT) . '-01';
+
+ $result = StorePlanService::getBouquetSpiecesMonthGoalFromForecast($month, $year, $storeId, $matrixGroups);
+ $weekShares = $this->getHistoricalSpeciesShareByWeek($date);
+ $flatData = $result['flatData'];
+
+ $weekIndex = [];
+ foreach ($weekShares as $shareRow) {
+ $key = implode('|', [
+ $shareRow['store_id'],
+ $shareRow['category'],
+ $shareRow['subcategory'],
+ $shareRow['species'],
+ ]);
+ $weekIndex[$key][] = $shareRow;
+ }
+
+ $weeklyForecasts = [];
+
+ foreach ($flatData as $item) {
+ $key = implode('|', [
+ $item['store_id'],
+ $item['category'],
+ $item['subcategory'],
+ $item['species'],
+ ]);
+
+ if (!isset($weekIndex[$key])) {
+ continue;
+ }
+
+ foreach ($weekIndex[$key] as $weekShare) {
+ if ($weekNumber !== null && (int)$weekShare['week'] !== (int)$weekNumber) {
+ continue; // фильтрация по неделе
+ }
+
+ $weeklyForecasts[] = [
+ 'store_id' => $item['store_id'],
+ 'category' => $item['category'],
+ 'subcategory' => $item['subcategory'],
+ 'species' => $item['species'],
+ 'type' => $item['type'],
+ 'product_guid' => $item['product_guid'],
+ 'week_forecast' => round($item['full_forecast'] * $weekShare['share'], 2),
+ 'full_forecast' => $item['full_forecast'],
+ 'week' => $weekShare['week'],
+ 'matrix_group' => $item['matrix_group'],
+ 'month' => $item['month'],
+ 'year' => $item['year'],
+ ];
+ }
+ }
+
+ $grouped = [];
+ $salesShares = [];
+
+ $plans = SalesWriteOffsPlan::find()
+ ->where(['month' => $month, 'year' => $year])
+ ->indexBy('store_id')
+ ->asArray()
+ ->all();
+ if ($plans) {
+ foreach ($plans as $storeId => $plan) {
+ $total = $plan['total_sales_plan'];
+ $offline = $plan['offline_sales_plan'];
+ $online = $plan['online_sales_shop_plan'];
+ $market = $plan['online_sales_marketplace_plan'];
+ $salesShares[$storeId]['offline'] = round($offline / $total, 4);
+ $salesShares[$storeId]['online'] = round($online / $total, 4);
+ $salesShares[$storeId]['marketplace'] = round($market / $total, 4);
+ }
+ }
+ foreach ($weeklyForecasts as $item) {
+ $storeItem = (int)$item['store_id'];
+ $guid = (string)$item['product_guid'];
+ $group = (string)$item['matrix_group'];
+ $type = (string)$item['type'];
+ $forecastValue = (float)$item['week_forecast'];
+ if (isset($salesShares[$storeItem]) && isset($salesShares[$storeItem][$type])) {
+ $grouped[$storeItem][$type]['share'] = $salesShares[$storeItem][$type];
+ }
+ $grouped[$storeItem][$guid][$type][$group] = $forecastValue;
+ }
+
+ return $grouped;
+ }
+
+ public function getWeeklyProductsWriteoffsForecast($month, $year, $weeklySalesForecast, $storeId = null, $weekNumber = null)
+ {
+ $weeksProductForecast = [];
+ $filters = [];
+
+ $dateFrom = date("Y-m-d 00:00:00", strtotime(sprintf('%04d-%02d-01', $year, $month)));
+ $monthYear = date("m-Y", strtotime($dateFrom));
+ $filters['store_id'] = $storeId;
+ $filters['type'] = self::TYPE_WRITE_OFFS;
+ $filters['plan_date'] = $dateFrom;
+
+ $monthSpeciesGoals = $this->calculateFullGoalChain($filters);
+ $monthSpeciesGoalsMap = [];
+ foreach ($monthSpeciesGoals as $monthSpeciesGoal) {
+ $monthSpeciesGoalsMap[$monthSpeciesGoal['store_id']]
+ [$monthSpeciesGoal['category']]
+ [$monthSpeciesGoal['subcategory']]
+ [$monthSpeciesGoal['species']] = $monthSpeciesGoal['goal'];
+ }
+ $productsWriteoffsShare = $this->calculateWriteOffShareByMonth($weeklySalesForecast, $month, $year);
+ $productsWriteoffsShareMap = [];
+ foreach ($productsWriteoffsShare as $productWriteoffs) {
+ $productsWriteoffsShareMap[$productWriteoffs['store_id']][$productWriteoffs['product_id']] = $productWriteoffs['share'];
+ }
+
+ $weeksShareResult = $this->getHistoricalWeeklySpeciesShare($monthYear, $filters, null, 'writeOffs');
+ $weeksData = $this->calculateWeeklySpeciesGoals($weeksShareResult, $monthSpeciesGoals);
+
+ foreach ($weeksData as $r) {
+ $forecasts = $this->calculateWeekForecastSpeciesProducts($r['category'], $r['subcategory'], $r['species'], $r['store_id'], $r['weekly_goal']);
+
+ foreach ($forecasts as $forecast) {
+ $productWriteoffsForecastSpeciesShare = $productsWriteoffsShareMap[$r['store_id']][$forecast['product_id']] ?? 0;
+ $productWriteoffsForecast = $productWriteoffsForecastSpeciesShare * $forecast['forecast'];
+ $weeksProductForecast[] = [
+ 'category' => $forecast['category'] ?? '',
+ 'subcategory' => $forecast['subcategory'] ?? '',
+ 'species' => $forecast['species'] ?? '',
+ 'product_id' => $forecast['product_id'] ?? '',
+ 'name' => $forecast['name'] ?? '',
+ 'price' => $forecast['price'] ?? '',
+ 'goal' => $forecast['goal'] ?? 0,
+ 'forecast' => $productWriteoffsForecast ?? 0,
+ 'week' => $r['week'],
+ ];
+ }
+ }
+
+ return $weeksProductForecast;
+ }
+
+ /**
+ * Рассчитывает долю списания (write-offs) каждого товара за заданный месяц
+ * на основании исторических данных за два предыдущих года.
+ *
+ * @param array $productList Список товаров:
+ * [
+ * [
+ * 'week' => int,
+ * 'store_id' => int,
+ * 'category' => string,
+ * 'subcategory' => string,
+ * 'species' => string,
+ * 'product_id' => string,
+ * 'forecast_month_pieces' => float,
+ * 'forecast_week_pieces' => float,
+ * ], …
+ * ]
+ * @param string $monthYear Формат 'YYYY-MM'
+ * @return array [
+ * [
+ * 'store_id' => int,
+ * 'month' => int,
+ * 'year' => int,
+ * 'category' => string,
+ * 'subcategory'=> string,
+ * 'species' => string,
+ * 'product_id' => string,
+ * 'writeoff_qty' => float,
+ * 'species_qty' => float,
+ * 'share' => float, // доля
+ * ], …
+ * ]
+ */
+ public function calculateWriteOffShareByMonth(array $productList, int $month, int $year ): array
+ {
+ $year = $year ?? (int)date("Y");
+ $month = $month ?? (int)date("m", strtotime( "+ 2 month" . date('Y-m-01')));
+
+ $storeIds = array_unique(array_column($productList, 'store_id'));
+ if (empty($storeIds)) {
+ return [];
+ }
+
+ $productQtyMap = []; // [store][cat][sub][spec][pid] => qty
+ $speciesQtyMap = []; // [store][cat][sub][spec] => total qty
+
+ $storeJoinCondition = 'ex.export_val = w.store_id';
+
+ for ($offset = 2; $offset >= 1; $offset--) {
+ $histYear = $year - $offset;
+ $start = sprintf('%04d-%02d-01 00:00:00', $histYear, $month);
+ $end = date('Y-m-d 23:59:59', strtotime("$start +1 month -1 second"));
+
+ $productsWriteOffs = (new Query())
+ ->select([
+ 'store_id' => 'ex.entity_id',
+ 'category' => 'p1c.category',
+ 'subcategory' => 'p1c.subcategory',
+ 'species' => 'p1c.species',
+ 'product_id' => 'wp.product_id',
+ 'qty' => new \yii\db\Expression('SUM(wp.quantity)'),
+ ])
+ ->from(['w' => 'write_offs'])
+ ->innerJoin(['wp' => 'write_offs_products'], 'wp.write_offs_id = w.id')
+ ->leftJoin('export_import_table ex', $storeJoinCondition)
+ ->leftJoin('products_1c_nomenclature p1c', 'p1c.id = wp.product_id')
+ ->leftJoin('products_1c p1', 'p1.id = wp.product_id')
+
+ ->andWhere(['ex.entity_id' => $storeIds])
+ ->andWhere(['>=', 'w.date', $start])
+ ->andWhere(['<=', 'w.date', $end])
+
+ ->andWhere(['p1.components' => ''])
+ ->andWhere(['not in', 'p1c.category', ['', 'букет', 'сборка', 'сервис']])
+ ->groupBy(['ex.entity_id','p1c.category','p1c.subcategory','p1c.species','wp.product_id'])
+ ->all();
+
+ foreach ($productsWriteOffs as $productsWriteOffsItem) {
+ $sid = (int)$productsWriteOffsItem['store_id'];
+ $cat = $productsWriteOffsItem['category'];
+ $sub = $productsWriteOffsItem['subcategory'];
+ $spec = $productsWriteOffsItem['species'];
+ $pid = $productsWriteOffsItem['product_id'];
+ $qty = (float)$productsWriteOffsItem['qty'];
+
+
+ $productQtyMap[$sid][$cat][$sub][$spec][$pid] =
+ ($productQtyMap[$sid][$cat][$sub][$spec][$pid] ?? 0.0) + $qty;
+
+ $speciesQtyMap[$sid][$cat][$sub][$spec] =
+ ($speciesQtyMap[$sid][$cat][$sub][$spec] ?? 0.0) + $qty;
+ }
+ }
+
+ $seen = [];
+ $result = [];
+ foreach ($productList as $item) {
+ $pid = $item['product_id'];
+ $sid = $item['store_id'];
+ $cat = $item['category'];
+ $sub = $item['subcategory'];
+ $spec = $item['species'];
+
+ $key = "{$sid}|{$cat}|{$sub}|{$spec}|{$pid}";
+ if (isset($seen[$key])) {
+ continue;
+ }
+ $seen[$key] = true;
+
+ $productsQty = $productQtyMap[$sid][$cat][$sub][$spec][$pid] ?? 0.0;
+ $speciesQty = $speciesQtyMap[$sid][$cat][$sub][$spec] ?? 0.0;
+ $share = $speciesQty > 0
+ ? round($productsQty / $speciesQty, 4)
+ : 0.0;
+
+ $result[] = [
+ 'store_id' => $sid,
+ 'month' => $month,
+ 'year' => $year,
+ 'category' => $cat,
+ 'subcategory' => $sub,
+ 'species' => $spec,
+ 'product_id' => $pid,
+ 'writeoff_qty' => $productsQty,
+ 'species_qty' => $speciesQty,
+ 'share' => $share,
+ ];
+ }
+
+ return $result;
}
-
}