]);
}
+ public function action1_1()
+ {
+ $request = Yii::$app->request;
+
+ $filters = [
+ 'category' => $request->get('category'),
+ 'store_id' => $request->get('store_id'),
+ 'year' => $request->get('year'),
+ 'month' => $request->get('month'),
+ 'type' => $request->get('type'),
+ ];
+
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => [],
+ 'pagination' => ['pageSize' => 100],
+ ]);
+
+ // Обработка даты на год и месяц
+ if (!empty($filters['year']) && !empty($filters['month'])) {
+ $filters['plan_date'] = $filters['year'] . '-' . str_pad($filters['month'], 2, '0', STR_PAD_LEFT) . '-01';
+
+ $service = new AutoPlannogrammaService();
+ $data = $service->getUnmarkedProductsComponents($filters['store_id'], $filters['month'], $filters['year'], $filters['type']);
+
+ $flatData = array_filter($data, function ($row) use ($filters) {
+ foreach ($filters as $key => $value) {
+ if (empty($value)) continue;
+ if (!isset($row[$key])) continue;
+
+ if (stripos((string)$row[$key], (string)$value) === false) {
+ return false;
+ }
+ }
+ return true;
+ });
+
+ $dataProvider = new ArrayDataProvider([
+ 'allModels' => array_values($flatData),
+ 'pagination' => ['pageSize' => 100],
+ ]);
+ }
+
+ return $this->render('1_1', [
+ 'dataProvider' => $dataProvider,
+ 'filters' => $filters,
+ ]);
+ }
+
public function action2()
{
$request = Yii::$app->request;
use yii\db\Expression;
use yii\db\Query;
use yii\helpers\ArrayHelper;
+use yii_app\records\BouquetComposition;
use yii_app\records\CityStore;
+use yii_app\records\CityStoreParams;
+use yii_app\records\ExportImportTable;
+use yii_app\records\PricesDynamic;
+use yii_app\records\Products1c;
+use yii_app\records\Products1cNomenclature;
+use yii_app\records\Sales;
+use yii_app\records\SalesProducts;
use yii_app\records\SalesWriteOffsPlan;
+use yii_app\records\WriteOffs;
+use yii_app\records\WriteOffsProducts;
class AutoPlannogrammaService
{
return array_values($filtered);
}
+
+ /**
+ * @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, $type = 'sales'): array
+ {
+ $region = CityStoreParams::find()
+ ->where(['store_id' => $storeId])
+ ->one()->address_region;
+
+ if (!$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);
+ $unmarkedProducts = [];
+
+ 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'])
+ ->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' => '']])
+ ->andWhere(['nom.category' => null])
+ ->asArray()
+ ->all();
+ $unmarkedProducts = $salesProducts;
+ } else {
+ $storeJoinCondition = $type === self::TYPE_WRITE_OFFS ? 'ex.export_val = w.store_id' : 'ex.export_val = s.store_id_1c';
+ $writeOffsProducts = WriteOffs::find()
+ ->alias('w')
+ ->select([
+ 'write_off_id' => 'w.id',
+ 'write_off_date' => 'w.date',
+ 'product_id' => 'wp.product_id',
+ 'type' => 'p1c.type',
+ 'components' => 'p1c.components',
+ 'product_name' => 'p1c.name',
+ 'store_id' => 'ex.entity_id',
+ ])
+ ->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(['nom.category' => null])
+ ->andWhere(['<>', 'p1c.components', ''])
+ ->asArray()
+ ->all();
+ //var_dump($writeOffsProducts); die();
+ $unmarkedProducts = $writeOffsProducts;
+ }
+
+ //var_dump( $salesProducts); die();
+
+ $components = [];
+ $rows = [];
+ foreach ($unmarkedProducts as $up) {
+
+ $js = trim($up['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' => $up['id'],
+ 'sale_date' => $up['date'],
+ 'product_id' => $up['product_id'],
+ 'product_name'=> $up['name'],
+ 'component_guid' => $guid,
+ 'quantity' => $qty,
+ 'type' => $type,
+ 'operation' => $up['operation'] ?? '',
+ ];
+ }
+ }
+
+ 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[] = [
+ 'store_id' => $storeId,
+ '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,
+ 'month' => $month,
+ 'year' => $year,
+ 'type' => $r['type'],
+ 'operation' => $r['operation'] ?? '',
+ ];
+ }
+
+ return $result;
+ }
+
+
+ public function sumUnmarkedProductsComponentsByCategory(array $items, string $type): array
+ {
+ $aggregated = [];
+
+ foreach ($items as $row) {
+ $storeId = $row['store_id'];
+ $category = $row['component_category'];
+ $month = $row['month'];
+ $year = $row['year'];
+ $operation = $row['operation'] ?? null;
+
+ $cost = (float)$row['cost'];
+
+ if (
+ $type === AutoPlannogrammaService::TYPE_SALES
+ || ($row['type'] ?? null) === AutoPlannogrammaService::TYPE_SALES
+ ) {
+ if ($operation === 'Возврат') {
+ $cost = -$cost;
+ }
+
+ }
+
+ $key = implode('|', [$storeId, $category, $year, $month]);
+
+ if (!isset($aggregated[$key])) {
+ $aggregated[$key] = [
+ 'store_id' => $storeId,
+ 'category' => $category,
+ 'sum' => 0.0,
+ 'month' => $month,
+ 'year' => $year,
+ 'type' => $type,
+ ];
+ }
+
+ $aggregated[$key]['sum'] += $cost;
+ }
+
+ return array_values($aggregated);
+ }
+
+
}
\ No newline at end of file
--- /dev/null
+<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_category_share)") ?></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(['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/1'], ['class' => 'btn btn-default']) ?>
+ </div>
+ </div>
+
+ <?php ActiveForm::end(); ?>
+</div>
+
+<?= GridView::widget([
+ 'dataProvider' => $dataProvider,
+ 'columns' => [
+ [
+ 'attribute' => 'store_id',
+ 'label' => 'Магазин',
+ 'value' => function ($row) {
+ return CityStore::findOne($row['store_id'])->name ?? null;
+ },
+ ],
+ [
+ 'attribute' => 'sale_id',
+ 'label' => '№ чека',
+ ],
+ [
+ 'attribute' => 'sale_date',
+ 'label' => 'Дата покупки',
+ 'format' => ['datetime', 'php:Y-m-d H:i'],
+ ],
+ [
+ 'attribute' => 'product_name',
+ 'label' => 'Товар',
+ ],
+ [
+ 'attribute' => 'component_guid',
+ 'label' => 'GUID компонента',
+ ],
+ [
+ 'attribute' => 'component_name',
+ 'label' => 'Название компонента',
+ ],
+ [
+ 'attribute' => 'component_category',
+ 'label' => 'Категория компонента',
+ ],
+ [
+ 'attribute' => 'quantity',
+ 'label' => 'Количество',
+ 'format' => 'integer',
+ ],
+ [
+ 'attribute' => 'price',
+ 'label' => 'Цена',
+ 'format' => ['decimal', 2],
+ ],
+ [
+ 'attribute' => 'cost',
+ 'label' => 'Стоимость',
+ 'format' => ['decimal', 2],
+ ],
+ ],
+]); ?>
+