From: Vladimir Fomichev Date: Tue, 29 Jul 2025 15:53:19 +0000 (+0300) Subject: Актуализация номенклатуры X-Git-Url: https://gitweb.erp-flowers.ru/?a=commitdiff_plain;h=ab8d2026089e6b5e49b2da5257cc50a275a1d5ac;p=erp24_rep%2Fyii-erp24%2F.git Актуализация номенклатуры --- diff --git a/erp24/controllers/Products1cNomenclatureActualityController.php b/erp24/controllers/Products1cNomenclatureActualityController.php new file mode 100644 index 00000000..b1af12b3 --- /dev/null +++ b/erp24/controllers/Products1cNomenclatureActualityController.php @@ -0,0 +1,349 @@ + [ + 'class' => VerbFilter::className(), + 'actions' => [ + 'delete' => ['POST'], + ], + ], + ] + ); + } + + /** + * Lists all Products1cNomenclatureActuality models. + * + * @return string + */ + public function actionIndex() + { + $filter = new \yii\base\DynamicModel([ + 'category','subcategory','species', + 'type','color','sort','size', + 'date_from','date_to', + ]); + $filter->addRule([ + 'category','subcategory','species', + 'type','color','sort','size', + 'date_from','date_to', + ], 'safe'); + $filter->load(Yii::$app->request->get()); + + + if (Yii::$app->request->isPost && $post = Yii::$app->request->post('actuality', [])) { + $this->processBatchActuality($post); + Yii::$app->session->setFlash('success', 'Данные по актуальности успешно сохранены.'); + return $this->refresh(); + } + + + $emptyQuery = Products1cNomenclature::find()->where('0=1'); + $dataProvider = new \yii\data\ActiveDataProvider([ + 'query' => $emptyQuery, + 'pagination' => ['pageSize' => 50], + 'sort' => ['defaultOrder'=>['name'=>SORT_ASC]], + ]); + + + $filtersUsed = array_filter([ + $filter->category, + $filter->subcategory, + $filter->species, + $filter->type, + $filter->color, + $filter->sort, + $filter->size, + $filter->date_from, + $filter->date_to, + ]); + if ($filtersUsed) { + $query = Products1cNomenclature::find()->alias('n'); + + + foreach (['category','subcategory','species'] as $attr) { + if ($filter->$attr) { + $query->andWhere(["n.$attr" => $filter->$attr]); + } + } + + + if ($filter->date_from || $filter->date_to) { + $query->innerJoin( + Products1cNomenclatureActuality::tableName() . ' a', + 'a.guid = n.id AND a.active = 1' + ); + if ($filter->date_from) { + $df = \DateTime::createFromFormat('Y-m', $filter->date_from) + ->format('Y-m-01 00:00:00'); + $query->andWhere(['>=', 'a.date_from', $df]); + } + if ($filter->date_to) { + $dt = \DateTime::createFromFormat('Y-m', $filter->date_to); + $dt->modify('last day of this month')->setTime(23,59,59); + $query->andWhere(['<=', 'a.date_end', $dt->format('Y-m-d H:i:s')]); + } + } + + + $attrMap = [ + 'type' => ['type','тип'], + 'color' => ['цвет','color'], + 'sort' => ['sort','сорт'], + 'size' => ['size','размер'], + ]; + foreach (['type','color','sort','size'] as $attr) { + if ($filter->$attr) { + + $propIds = Products1cPropType::find() + ->select('id') + ->andWhere(['name' => $attrMap[$attr]]) + ->column(); + + $productIds = Products1cAdditionalCharacteristics::find() + ->select('product_id') + ->distinct() + ->andWhere(['property_id' => $propIds, 'value' => $filter->$attr]) + ->column(); + + + if (empty($productIds)) { + $query->andWhere('0=1'); + break; + } + + $query->andWhere(['n.id' => $productIds]); + } + } + + // 4.4) Подставляем в провайдер + $dataProvider->query = $query; + } + + $categories = Products1cNomenclature::find()->select('category')->distinct()->column(); + $subcategories = Products1cNomenclature::find()->select('subcategory')->distinct(); + $subcategories->andWhere(['not', ['subcategory' => null]]); + $subcategories->andWhere(['<>', 'subcategory', '']); + if ($filter->category) { + $subcategories->andWhere(['category' => $filter->category]); + } + $subcategories = $subcategories->column(); + + $species = Products1cNomenclature::find() + ->select('species') + ->distinct() + ->andWhere(['not', ['species' => null]]) + ->andWhere(['<>', 'species', '']); + + if ($filter->subcategory) { + $species->andWhere(['subcategory' => $filter->subcategory]); + } + + $species = $species->column(); + + $lists = []; + $attrMap = [ + 'type' => ['type','тип'], + 'color' => ['цвет','color'], + 'sort' => ['sort','сорт'], + 'size' => ['size','размер'], + ]; + foreach (['type','color','sort','size'] as $attr) { + $propIds = Products1cPropType::find() + ->select('id') + ->andWhere(['name'=>$attrMap[$attr]]) + ->column(); + $lists[$attr] = Products1cAdditionalCharacteristics::find() + ->select('value')->distinct() + ->where(['property_id'=>$propIds]) + ->column(); + } + + return $this->render('index', [ + 'filter' => $filter, + 'dataProvider' => $dataProvider, + 'categories' => array_combine($categories,$categories), + 'subcategories' => array_combine($subcategories,$subcategories), + 'species' => array_combine($species, $species), + 'types' => array_combine($lists['type'], $lists['type']), + 'colors' => array_combine($lists['color'], $lists['color']), + 'sorts' => array_combine($lists['sort'], $lists['sort']), + 'sizes' => array_combine($lists['size'], $lists['size']), + ]); + } + + + + /** + * Обработка массового сохранения диапазонов актуальности. + * Если из/до нет или невалидны — пропускаем. + * Закрываем старую запись и создаем новую при изменении диапазона. + * @param array $post + */ + protected function processBatchActuality(array $post) + { + $userId = Yii::$app->user->id; + $now = date('Y-m-d H:i:s'); + + foreach ($post as $row) { + if (empty($row['from']) || empty($row['to'])) { + continue; + } + + $fromDt = \DateTime::createFromFormat('Y-m', $row['from']); + if (!$fromDt) { + continue; + } + $newFrom = $fromDt->format('Y-m-01 00:00:00'); + + $toDt = \DateTime::createFromFormat('Y-m', $row['to']); + if (!$toDt) { + continue; + } + $toDt->modify('last day of this month')->setTime(23, 59, 59); + $newTo = $toDt->format('Y-m-d H:i:s'); + + + $warehouseNN = !empty($row['warehouse_nn']); + $warehouseMS = !empty($row['warehouse_msk']); + $supplier = $row['supplier'] ?? null; + $plantation = $row['plantation'] ?? null; + + + $old = Products1cNomenclatureActuality::find() + ->andWhere(['guid' => $row['guid'], 'active' => 1]) + ->one(); + + if ($old) { + if ($old->date_from === $newFrom && $old->date_end === $newTo) { + continue; + } + + $old->active = 0; + $old->updated_by = $userId; + $old->updated_at = $now; + $old->save(false); + } + + + $new = new Products1cNomenclatureActuality([ + 'guid' => $row['guid'], + 'date_from' => $newFrom, + 'date_end' => $newTo, + 'active' => 1, + 'created_at' => $now, + 'created_by' => $userId, + ]); + $new->save(false); + } + } + + /** + * Displays a single Products1cNomenclatureActuality model. + * @param int $id ID + * @return string + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionView($id) + { + return $this->render('view', [ + 'model' => $this->findModel($id), + ]); + } + + /** + * Creates a new Products1cNomenclatureActuality model. + * If creation is successful, the browser will be redirected to the 'view' page. + * @return string|\yii\web\Response + */ + public function actionCreate() + { + $model = new Products1cNomenclatureActuality(); + + if ($this->request->isPost) { + if ($model->load($this->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } + } else { + $model->loadDefaultValues(); + } + + return $this->render('create', [ + 'model' => $model, + ]); + } + + /** + * Updates an existing Products1cNomenclatureActuality model. + * If update is successful, the browser will be redirected to the 'view' page. + * @param int $id ID + * @return string|\yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionUpdate($id) + { + $model = $this->findModel($id); + + if ($this->request->isPost && $model->load($this->request->post()) && $model->save()) { + return $this->redirect(['view', 'id' => $model->id]); + } + + return $this->render('update', [ + 'model' => $model, + ]); + } + + /** + * Deletes an existing Products1cNomenclatureActuality model. + * If deletion is successful, the browser will be redirected to the 'index' page. + * @param int $id ID + * @return \yii\web\Response + * @throws NotFoundHttpException if the model cannot be found + */ + public function actionDelete($id) + { + $this->findModel($id)->delete(); + + return $this->redirect(['index']); + } + + /** + * Finds the Products1cNomenclatureActuality model based on its primary key value. + * If the model is not found, a 404 HTTP exception will be thrown. + * @param int $id ID + * @return Products1cNomenclatureActuality the loaded model + * @throws NotFoundHttpException if the model cannot be found + */ + protected function findModel($id) + { + if (($model = Products1cNomenclatureActuality::findOne(['id' => $id])) !== null) { + return $model; + } + + throw new NotFoundHttpException('The requested page does not exist.'); + } +} diff --git a/erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php b/erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php new file mode 100644 index 00000000..bb74e8c7 --- /dev/null +++ b/erp24/migrations/m250729_101715_create_products_1c_nomenclature_actuality_table.php @@ -0,0 +1,43 @@ +db->getTableSchema(self::TABLE_NAME); + + if (!isset($tableSchema)) { + $this->createTable(self::TABLE_NAME, [ + 'id' => $this->primaryKey(), + 'guid' => $this->string()->notNull(), + 'date_from' => $this->dateTime()->notNull()->comment('Дата и время начала активности'), + 'date_end' => $this->dateTime()->null()->comment('Дата и время окончания активности'), + 'active' => $this->tinyInteger()->notNull()->defaultValue(1)->comment('Активность записи'), + 'created_at' => $this->dateTime()->notNull()->comment('Дата создания'), + 'updated_at' => $this->dateTime()->null()->comment('Дата обновления'), + 'created_by' => $this->integer()->notNull()->comment('ИД создателя'), + 'updated_by' => $this->integer()->null()->comment('ИД редактировавшего'), + ]); + } + } + + /** + * {@inheritdoc} + */ + public function safeDown() + { + $tableSchema = $this->db->getTableSchema(self::TABLE_NAME); + if (isset($tableSchema)) { + $this->dropTable(self::TABLE_NAME); + } + } +} diff --git a/erp24/records/Products1cNomenclature.php b/erp24/records/Products1cNomenclature.php index 57f39e04..5342425f 100644 --- a/erp24/records/Products1cNomenclature.php +++ b/erp24/records/Products1cNomenclature.php @@ -3,6 +3,7 @@ namespace yii_app\records; use Yii; +use yii\db\ActiveQuery; /** * This is the model class for table "products_1c_nomenclature". @@ -65,4 +66,35 @@ class Products1cNomenclature extends \yii\db\ActiveRecord 'color' => 'Цвет', ]; } + + /** + * Relation: все записи актуальности для этой номенклатуры + */ + public function getActualities(): ActiveQuery + { + return $this->hasMany( + Products1cNomenclatureActuality::class, + ['guid' => 'id'] + ); + } + + /** + * Relation: единственная (первыей) активная запись актуальности + */ + public function getActiveActuality(): ActiveQuery + { + return $this->hasOne( + Products1cNomenclatureActuality::class, + ['guid' => 'id'] + )->andWhere(['active' => 1]); + } + + /** + * Проверяет, есть ли для этой номенклатуры активная запись + * @return bool + */ + public function hasActiveActuality(): bool + { + return (bool)$this->getActiveActuality()->exists(); + } } diff --git a/erp24/records/Products1cNomenclatureActuality.php b/erp24/records/Products1cNomenclatureActuality.php new file mode 100644 index 00000000..cce087ed --- /dev/null +++ b/erp24/records/Products1cNomenclatureActuality.php @@ -0,0 +1,66 @@ + null], + [['active'], 'default', 'value' => 1], + [['guid', 'date_from', 'created_at', 'created_by'], 'required'], + [['date_from', 'date_end', 'created_at', 'updated_at'], 'safe'], + [['active', 'created_by', 'updated_by'], 'default', 'value' => null], + [['active', 'created_by', 'updated_by'], 'integer'], + [['guid'], 'string', 'max' => 255], + ]; + } + + /** + * {@inheritdoc} + */ + public function attributeLabels() + { + return [ + 'id' => 'ID', + 'guid' => 'Guid', + 'date_from' => 'Date From', + 'date_end' => 'Date End', + 'active' => 'Active', + 'created_at' => 'Created At', + 'updated_at' => 'Updated At', + 'created_by' => 'Created By', + 'updated_by' => 'Updated By', + ]; + } + +} diff --git a/erp24/records/Products1cNomenclatureActualitySearch.php b/erp24/records/Products1cNomenclatureActualitySearch.php new file mode 100644 index 00000000..bddbc6b1 --- /dev/null +++ b/erp24/records/Products1cNomenclatureActualitySearch.php @@ -0,0 +1,175 @@ +load()` method. + * + * @return ActiveDataProvider + */ + public function search($params, $formName = null) + { + $query = Products1cNomenclatureActuality::find(); + + // add conditions that should always apply here + + $dataProvider = new ActiveDataProvider([ + 'query' => $query, + ]); + + $this->load($params, $formName); + + if (!$this->validate()) { + // uncomment the following line if you do not want to return any records when validation fails + // $query->where('0=1'); + return $dataProvider; + } + + // grid filtering conditions + $query->andFilterWhere([ + 'id' => $this->id, + 'date_from' => $this->date_from, + 'date_end' => $this->date_end, + 'active' => $this->active, + 'created_at' => $this->created_at, + 'updated_at' => $this->updated_at, + 'created_by' => $this->created_by, + 'updated_by' => $this->updated_by, + ]); + + $query->andFilterWhere(['ilike', 'guid', $this->guid]); + + return $dataProvider; + } + + public static function getCategoryList(): array + { + $rows = Products1cNomenclature::find() + ->select('category') + ->distinct() + ->where(['not', ['category'=>null]]) + ->orderBy('category') + ->asArray() + ->all(); + + return ArrayHelper::map($rows, 'category', 'category'); + } + + public static function getTypeList(): array + { + $typeList = Products1cPropType::find() + ->select(['id']) + ->andWhere(['name' => ['type', 'тип']]) + ->column(); + $additionalCharacteristic = Products1cAdditionalCharacteristics::find() + ->select(['value'])->distinct()->where(['property_id' => $typeList ])->asArray()->all(); + + return ArrayHelper::map($additionalCharacteristic, 'value', 'value'); + } + + public static function getColorList(): array + { + $typeList = Products1cPropType::find() + ->select(['id']) + ->andWhere(['name' => ['цвет', 'color']]) + ->column(); + $additionalCharacteristic = Products1cAdditionalCharacteristics::find() + ->select(['value'])->distinct()->where(['property_id' => $typeList])->asArray()->all(); + + return ArrayHelper::map($additionalCharacteristic, 'value', 'value'); + } + + public static function getSubcategoryList(): array + { + $rows = Products1cNomenclature::find() + ->select('subcategory') + ->distinct() + ->where(['not', ['subcategory'=>null]]) + ->orderBy('subcategory') + ->asArray() + ->all(); + + return ArrayHelper::map($rows, 'subcategory', 'subcategory'); + } + + public static function getSortList(): array + { + $typeList = Products1cPropType::find() + ->select(['id']) + ->andWhere(['name' => ['sort', 'сорт']]) + ->column(); + $additionalCharacteristic = Products1cAdditionalCharacteristics::find() + ->select(['value'])->distinct()->where(['property_id' => $typeList])->asArray()->all(); + + return ArrayHelper::map($additionalCharacteristic, 'value', 'value'); + } + + public static function getSpeciesList(): array + { + $rows = Products1cNomenclature::find() + ->select('species') + ->distinct() + ->where(['not', ['species'=>null]]) + ->orderBy('species') + ->asArray() + ->all(); + + return ArrayHelper::map($rows, 'species', 'species'); + } + + public static function getSizeList(): array + { + $typeList = Products1cPropType::find() + ->select(['id']) + ->andWhere(['name' => ['size', 'размер']]) + ->column(); + $additionalCharacteristic = Products1cAdditionalCharacteristics::find() + ->select(['value'])->distinct()->where(['property_id' => $typeList])->asArray()->all(); + + return ArrayHelper::map($additionalCharacteristic, 'value', 'value'); + } +} diff --git a/erp24/views/products1c-nomenclature-actuality/index.php b/erp24/views/products1c-nomenclature-actuality/index.php new file mode 100644 index 00000000..270e4f26 --- /dev/null +++ b/erp24/views/products1c-nomenclature-actuality/index.php @@ -0,0 +1,304 @@ +title = 'Актуализация номенклатуры'; +$this->params['breadcrumbs'][] = $this->title; + +// Список месяцев-годов для выпадающих списков +function monthList() +{ + $list = []; + $start = new DateTime('2021-01'); + $end = new DateTime('2025-12'); + while ($start <= $end) { + $key = $start->format('Y-m'); + $list[$key] = $start->format('Y‑m'); + $start->modify('+1 month'); + } + return $list; +} + +$months = monthList(); +?> + +
+ +

title) ?>

+ + + 'get', + 'action' => ['index'], + 'options' => ['class' => 'mb-4'], + ]); ?> + +
+ + +
+
+
+
+ field($filter, 'category', ['options' => ['class' => 'w-90']])->dropDownList( + $categories, + ['prompt' => 'Категория', 'id' => 'filter-category'] + )->label(false) ?> + +
+ +
+
+
+
+
+ field($filter, 'type', ['options' => ['class' => 'w-90']])->dropDownList( + $types, + ['prompt' => 'Тип', 'id' => 'filter-type'] + )->label(false) ?> + +
+ +
+
+
+
+
+ field($filter, 'color', ['options' => ['class' => 'w-90']])->dropDownList( + $colors, + ['prompt' => 'Цвет', 'id' => 'filter-color'] + )->label(false) ?> + +
+ +
+
+
+
+
+
+
+ field($filter, 'subcategory', ['options' => ['class' => 'w-90']])->dropDownList( + $subcategories, + ['prompt' => 'Подкатегория', 'id' => 'filter-subcategory', 'class' => 'w-100'] + )->label(false) ?> + +
+ +
+
+
+
+
+ field($filter, 'sort', ['options' => ['class' => 'w-90']])->dropDownList( + $sorts, + ['prompt' => 'Сорт', 'id' => 'filter-sort'] + )->label(false) ?> +
+ +
+
+
+
+
+
+
+
+ field($filter, 'species', ['options' => ['class' => 'w-90']])->dropDownList( + $species, + ['prompt' => 'Вид', 'id' => 'filter-species'] + )->label(false) ?> +
+ +
+
+
+
+
+ field($filter, 'size', ['options' => ['class' => 'w-90']])->dropDownList( + $sizes, + ['prompt' => 'Размер', 'id' => 'filter-size'] + )->label(false) ?> +
+ +
+
+
+
+
+
+ + +
+
Актуальность ассортимента
+
+
+ field($filter, 'date_from', ['options' => ['class' => 'w-100']]) + ->dropDownList($months, ['prompt' => 'Выбрать дату от', 'id' => 'filter-date-from']) + ->label(false) ?> +
+ +
+
+
+
+
+ field($filter, 'date_to', ['options' => ['class' => 'w-100']]) + ->dropDownList($months, ['prompt' => 'Выбрать дату до', 'id' => 'filter-date-to']) + ->label(false) ?> +
+ +
+
+
+
+ + +
+
+ +
+ 'form-select', 'id' => 'filter-supplier', 'prompt' => 'Поставщик']) ?> +
+ +
+
+
+
+ +
+ 'form-select', 'id' => 'filter-plantation', 'prompt' => 'Плантация']) ?> +
+ +
+
+
+
+ 'btn btn-primary']) ?> +
+
+ +
+ + + + + + + + 'actuality-form']); ?> +
+ 'btn btn-success']) ?> +
+ $dataProvider, + 'responsive' => false, + 'hover' => true, + 'floatHeader' => false, + 'tableOptions' => ['class' => 'table table-bordered'], + 'containerOptions' => ['style' => 'overflow:auto; max-height:500px;'], + 'rowOptions' => function($model) { + return $model->hasActiveActuality() ? ['class' => 'table-success'] : []; + }, + 'columns' => [ + [ + 'attribute' => 'name', + 'label' => 'Наименование', + 'format' => 'raw', + 'contentOptions' => ['style'=>'min-width:200px;'], + 'value' => function ($m) { + return Html::encode($m->name); + } + ], + [ + 'label' => 'Актуальность ассортимента', + 'format' => 'raw', + 'contentOptions' => ['style'=>'white-space:nowrap; min-width:200px;'], + 'value' => function ($m, $k, $i) use ($months) { + $active = $m->getActiveActuality()->one(); + $from = $active ? (new \DateTime($active->date_from))->format('Y-m') : null; + $to = $active ? (new \DateTime($active->date_end))->format('Y-m') : null; + return Html::hiddenInput("actuality[$i][guid]", $m->id) + . Html::tag('div', + Html::dropDownList("actuality[$i][from]", $from, $months, [ + 'class'=>'form-select form-select-sm me-1', + 'prompt'=>'от' + ]) + . Html::dropDownList("actuality[$i][to]", $to, $months, [ + 'class'=>'form-select form-select-sm', + 'prompt'=>'до' + ]), + ['class'=>'d-flex align-items-center'] + ); + } + ], + [ + 'label' => 'Склад NN', + 'format' => 'raw', + 'contentOptions' => ['style'=>'width:60px; text-align:center;'], + 'value' => function ($m, $k, $i) { + return Html::checkbox("actuality[$i][warehouse_nn]", false); + } + ], + [ + 'label' => 'Склад MSK', + 'format' => 'raw', + 'contentOptions' => ['style'=>'width:60px; text-align:center;'], + 'value' => function ($m, $k, $i) { + return Html::checkbox("actuality[$i][warehouse_msk]", false); + } + ], + [ + 'label' => 'Поставщик/Плантация', + 'format' => 'text', + 'contentOptions' => ['style'=>'min-width:150px;'], + 'value' => function ($m) { + return '–'; + } + ], + ], + ]); ?> + + + + + +
+ += from); + }); + if (to.val() < from) { + to.val(from); + } +}); +$('.clear-btn').on('click', function(){ + var target = $(this).data('target'); + $('#' + target).val(null).trigger('change'); +}); +JS; +$this->registerJs($js); +?>