$filters['species']
);
// var_dump($result); die();
-
+ $withoutHistoryResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
+ $filters['store_id'],
+ $filters['month'],
+ $filters['year'],
+ $result['without_history']
+ );
$productSalesShare = StorePlanService::calculateProductSalesShare(
$filters['store_id'],
$filters['month'],
);
- var_dump( $productSalesShare); die();
+ var_dump( $withoutHistoryResults); die();
$flatData = array_filter($data, function ($row) use ($filters) {
foreach ($filters as $key => $value) {
if (empty($value)) continue;
'pagination' => ['pageSize' => 100],
]);
}
- return $this->render('8', [
+ return $this->render('81', [
'dataProvider' => $dataProvider,
'filters' => $filters,
]);
$species
);
// var_dump($result); die();
- $weightedResults = StorePlanService::calculateWeightedSalesForProductsWithoutHistory(
+ $weightedResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
$storeId,
$selectedMonth,
$selectedYear,
use yii\db\Query;
use yii\helpers\ArrayHelper;
use yii_app\records\CityStore;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1cNomenclature;
+use yii_app\records\SalesProducts;
use yii_app\records\SalesWriteOffsPlan;
class AutoPlannogrammaService
// ——————— WEIGHTED SALES ————————
$t2 = hrtime(true);
- $weightedResults = StorePlanService::calculateMedianSalesForProductsWithoutHistory(
+ $medianResults = StorePlanService::calculateMedianSalesForProductsWithoutHistoryExtended(
$storeId, $month, $year, $productsWithoutHistory
);
$dur = (hrtime(true) - $t2) / 1e6;
Yii::warning("calculateMedianSalesForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
- if (empty($weightedResults)) {
+ if (empty($medianResults)) {
continue;
}
// ——————— COST CALCULATION ————————
$t3 = hrtime(true);
$costs = StorePlanService::calculateCostForProductsWithoutHistory(
- $storeId, $month, $year, $weightedResults
+ $storeId, $month, $year, $medianResults
);
$dur = (hrtime(true) - $t3) / 1e6;
Yii::warning( "calculateCostForProductsWithoutHistory for store {$storeId}: {$dur} ms\n");
return $result;
}
+ /**
+ * @param array|int $storeIds
+ * @param string $dateFrom формат 'Y-m-d H:i:s'
+ * @param string $dateTo формат 'Y-m-d H:i:s'
+ * @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(array|int $storeIds, string $dateFrom, string $dateTo, int $regionId = 52, $typeFilter = null): array
+ {
+ $salesProducts = SalesProducts::find()
+ ->alias('sp')
+ ->innerJoinWith([
+ 'sale s',
+ 'product_1c p1c',
+ 'product.nomenclature nom',
+ ])
+ ->andWhere(['s.store_id' => $storeIds])
+ ->andWhere(['between', 's.date', $dateFrom, $dateTo])
+ ->andWhere(['nom.category' => null]);
+
+ if ($typeFilter) {
+ $salesProducts->andWhere(['p1c.type' => $typeFilter]);
+ }
+ $salesProducts->all();
+
+ $components = [];
+ $rows = [];
+ foreach ($salesProducts as $sp) {
+ /** @var SalesProducts $sp */
+ $sale = $sp->check_id;
+ $product = $sp->product_id;
+ $js = trim($product->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' => $sale->id,
+ 'sale_date' => $sale->date,
+ 'product_id' => $product->id,
+ 'product_name'=> $product->name,
+ 'component_guid' => $guid,
+ 'quantity' => $qty,
+ ];
+ }
+ }
+ if (empty($rows)) {
+ return [];
+ }
+ $guids = array_keys($components);
+
+ $nomenclatures = Products1cNomenclature::find()
+ ->andWhere(['guid' => $guids])
+ ->indexBy('guid')
+ ->all();
+
+ $priceDynamics = PricesDynamic::find()
+ ->andWhere(['region_id' => $regionId])
+ ->andWhere(['product_id' => array_values( ArrayHelper::getColumn($nomenclatures, 'id') )])
+ ->andWhere(['<=', 'date_from', $dateTo])
+ ->andWhere(['>=', 'date_to', $dateFrom])
+ ->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;
+ }
}
\ No newline at end of file
/**
- * Ð\9cеÑ\82од вÑ\8bÑ\87иÑ\81лÑ\8fеÑ\82 взвеÑ\88енное знаÑ\87ение пÑ\80одаж длÑ\8f Ñ\82оваÑ\80ов без иÑ\81Ñ\82оÑ\80ии.
+ * Метод вычисляет значение продаж для товаров без истории.
*
* @param int $storeId Идентификатор магазина.
* @param string $selectedMonth Выбранный месяц в формате "mm" (целевой месяц).
*
* @return array Возвращает массив, где ключ – GUID товара, а значение – рассчитанное взвешенное значение продаж.
*/
- public static function calculateWeightedSalesForProductsWithoutHistory($storeId, $selectedMonth, $selectedYear, $productsWithoutHistory)
+ public static function calculateMedianSalesForProductsWithoutHistoryExtended($storeId, $selectedMonth, $selectedYear, $productsWithoutHistory)
{
$t0 = hrtime(true);
$targetDate = strtotime("{$selectedYear}-{$selectedMonth}-01");
$periods = self::getPeriods($targetDate, 3);
- $weightedResults = [];
+ $medianSalesResults = [];
$initTime = (hrtime(true) - $t0) / 1e6; // миллисекунды
Yii::warning( "Init (periods): {$initTime} ms\n");
foreach ($productsWithoutHistory as $product) {
$t1 = hrtime(true);
$similarProductIds = self::getSimilarProductIDs($guid);
if (empty($similarProductIds)) {
- $weightedResults[$guid] = 0;
+ $medianSalesResults[$guid] = 0;
continue;
}
$dur = (hrtime(true) - $t1) / 1e6;
$t3 = hrtime(true);
$median3 = self::calculateMedianSalesOverPeriods($storeId, $similarProductIds, $periods);
- $weightedResults[$guid] = [
+ $medianSalesResults[$guid] = [
'weightedValue' => $median3,
'medianSales' => $medianSales,
'salesValues' => $salesValuesForEachMonth,
$totalTime = (hrtime(true) - $t0) / 1e6;
Yii::warning( "Total calculateWeightedSalesForProductsWithoutHistory: {$totalTime} ms\n");
- return $weightedResults;
+ return $medianSalesResults;
}
/**
* @param int $storeId
* @param int $selectedMonth
* @param int $selectedYear
- * @param array $productsWithoutHistory массив ['guid' => …]
+ * @param array $medianProductsWithoutHistory массив ['guid' => …]
* @return array плоский массив строк:
* [
* [
int $storeId,
int $selectedMonth,
int $selectedYear,
- array $productsWithoutHistory
+ array $medianProductsWithoutHistory
): array
{
$accumulator = [];
-
- foreach ($productsWithoutHistory as $guid => $info) {
+ // var_dump($productsWithoutHistory); die();
+ foreach ($medianProductsWithoutHistory as $guid => $info) {
$quantity = (float)$info;
if ($quantity <= 0) {
continue;
--- /dev/null
+<?php
+?>
+ <div class="filter-form" style="margin-bottom: 20px;">
+ <?php use kartik\date\DatePicker;
+ use kartik\grid\GridView;
+ use kartik\select2\Select2;
+ use yii\helpers\ArrayHelper;
+ use yii\helpers\Html;
+ use yii\widgets\ActiveForm;
+ use yii_app\records\CityStore;
+ use yii_app\records\Products1cNomenclature;
+ ?>
+ <h1 class="ms-3 mb-4"><?= Html::encode("Расчет доли товара без истории (month_nohistory_sale_frcst)") ?></h1>
+ <?php
+ $form = ActiveForm::begin(['method' => 'get']); ?>
+ <div class="row p-3">
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['category' => $filters['category'] ?? '']), 'category')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('category')->distinct()->asArray()->all(),
+ 'category',
+ 'category'
+ ),
+ 'options' => ['placeholder' => 'Категория', 'name' => 'category'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Категория') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['subcategory' => $filters['subcategory'] ?? '']), 'subcategory')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('subcategory')->distinct()->asArray()->all(),
+ 'subcategory',
+ 'subcategory'
+ ),
+ 'options' => ['placeholder' => 'Подкатегория', 'name' => 'subcategory'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Подкатегория') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['species' => $filters['species'] ?? '']), 'species')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ Products1cNomenclature::find()->select('species')->distinct()->asArray()->all(),
+ 'species',
+ 'species'
+ ),
+ 'options' => ['placeholder' => 'Тип товара', 'name' => 'species'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Товар') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['store_id' => $filters['store_id'] ?? '']), 'store_id')->widget(Select2::class, [
+ 'data' => ArrayHelper::map(
+ CityStore::findAll(['visible' => CityStore::IS_VISIBLE]),
+ 'id',
+ 'name'
+ ),
+ 'options' => ['placeholder' => 'Магазин', 'name' => 'store_id'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('Магазин') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['month' => $filters['month'] ?? '']), 'month')->dropDownList(\yii_app\helpers\DateHelper::MONTH_NUMBER_NAMES, [
+ 'prompt' => 'Месяц',
+ 'name' => 'month',
+ ])->label('Плановый месяц') ?>
+ </div>
+
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['year' => $filters['year'] ?? '']), 'year')->dropDownList(['2025' => 2025, '2026' => 2026], [
+ 'prompt' => 'Год',
+ 'name' => 'year',
+ ])->label('Плановый год') ?>
+ </div>
+ <div class="col-md">
+ <?= $form->field(new \yii\base\DynamicModel(['type' => $filters['type'] ?? '']), 'type')->widget(Select2::class, [
+ 'data' => [
+ 'writeOffs' => 'Списания',
+ 'sales' => 'Продажи'
+ ],
+ 'options' => ['placeholder' => 'Тип', 'name' => 'type'],
+ 'pluginOptions' => ['allowClear' => true],
+ ])->label('По дефолту продажи!') ?>
+ </div>
+ <div class="col-md">
+ <?= Html::submitButton('Фильтровать', ['class' => 'btn btn-primary']) ?>
+ </div>
+ <div class="col-md">
+ <?= Html::a('Сбросить', ['auto-plannogramma/8'], ['class' => 'btn btn-default']) ?>
+ </div>
+ </div>
+
+ <?php ActiveForm::end(); ?>
+ </div>
+
+
+
+<?php
+$columns = [
+ ['attribute' => 'store_id', 'label' => 'Магазин', 'value' => function ($data) {
+ return CityStore::findOne($data['store_id'])->name ?? null;
+ }],
+ ['attribute' => 'category', 'label' => 'Категория'],
+ ['attribute' => 'subcategory', 'label' => 'Подкатегория'],
+ ['attribute' => 'species', 'label' => 'Тип'],
+ ['attribute' => 'goal', 'label' => 'Сумма', 'format' => ['decimal', 2]],
+];
+
+
+?>
+<?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'columns' => $columns,
+]); ?>
+<?php