use app\jobs\SendWhatsappMessageJob;
use DateTime;
use DateTimeZone;
+use Throwable;
use Yii;
use yii\console\Controller;
use yii\console\ExitCode;
return ExitCode::OK;
}
- public function actionAutoplannogrammaCalculate()
+ public function actionAutoplannogrammaCalculate(): void
{
- $date = new \DateTime();
+ $date = new DateTime();
$date->modify('+2 months');
-
$planDate = $date->format('Y-m-01');
- $month = $date->format('m');
- $year = $date->format('Y');
+ $month = (int)$date->format('m');
+ $year = (int)$date->format('Y');
$service = new AutoPlannogrammaService();
- $writeOffsForecast = $service->getWeeklyProductsWriteoffsForecast($month, $year, 2);
- $forecast = $service->getWeeklyBouquetProductsForecast($month, $year, 2);
-
- foreach ($forecast as $item) {
-
- $this->stdout('прогноз', BaseConsole::FG_GREEN);
- $this->stdout(print_r($item, true), BaseConsole::FG_GREEN);
- break;
- }
-
- foreach ($writeOffsForecast as $item) {
- $this->stdout('списание', BaseConsole::FG_GREEN);
- $this->stdout(print_r($item, true), BaseConsole::FG_GREEN);
- break;
- }
+ $stores = CityStore::find()->where(['visible' => CityStore::IS_VISIBLE])->all();
$this->stdout("Начало расчетов автопланограммы для $planDate\n", BaseConsole::FG_GREEN);
- $service = new AutoPlannogrammaService();
- $stores = CityStore::findAll(['visible' => CityStore::IS_VISIBLE]);
-
foreach ($stores as $store) {
$this->stdout("Начало расчетов автопланограммы для магазина ID: {$store->id} ({$store->name})\n", BaseConsole::FG_YELLOW);
try {
- $forecast = $service->calculateFullForecastForWeek([
+ $forecastParams = [
'month' => $month,
'year' => $year,
'type' => AutoPlannogrammaService::TYPE_SALES,
'subcategory' => null,
'species' => null,
'plan_date' => $planDate
- ]);
+ ];
+ $forecast = $service->calculateFullForecastForWeek($forecastParams);
$writeOffsForecast = $service->getWeeklyProductsWriteoffsForecast($month, $year, $store->id);
- $forecast = $service->getWeeklyBouquetProductsForecast($month, $year, $store->id);
-
+ $salesForecast = $service->getWeeklyBouquetProductsSalesForecast($month, $year, $store->id);
$this->stdout("Рассчитана автопланограмма для магазина {$store->name}\n", BaseConsole::FG_GREEN);
- } catch (\Throwable $e) {
- $this->stderr("Ошибка при расчёте прогноза: {$e->getMessage()}\n", BaseConsole::FG_RED);
- Yii::error("Ошибка при расчёте прогноза: " . $e->getMessage(), __METHOD__);
- continue;
- }
- $writeOffsForecastMap = [];
- foreach ($writeOffsForecast as $itemForecast) {
- $itemWeek = $itemForecast['week'];
- $itemGuid = $itemForecast['product_id'];
- $writeoffsForecast = $itemForecast['forecast'];
- $writeOffsForecastMap[$itemWeek][$itemGuid] = $writeoffsForecast;
-
- }
-
- $existingRecords = [];
- $records = Autoplannogramma::find()
- ->where([
- 'month' => $month,
- 'year' => $year,
- 'store_id' => $store->id,
- ])
- ->andWhere(['week' => array_unique(array_column($forecast, 'week'))])
- ->all();
- foreach ($records as $record) {
- $existingRecords[$record->week . '_' . $record->product_id] = $record;
- }
- foreach ($forecast as $item) {
- $key = $item['week'] . '_' . $item['product_id'];
- $model = $existingRecords[$key] ?? null;
- $this->stderr(implode("\n", $item), BaseConsole::FG_GREEN);
- if (!$model) {
- $model = new Autoplannogramma();
- $model->month = $month;
- $model->year = $year;
- $model->week = $item['week'];
- $model->product_id = $item['product_id'];
- $model->store_id = $item['store_id'];
- $model->is_archive = false;
- $model->capacity_type = 1;
- $model->quantity = $item['forecast_week_pieces'];
- if (isset($writeOffsForecastMap[$item['week']]) && isset($writeOffsForecastMap[$item['week']][$item['product_id']])) {
- $model->writeoffs_forecast = $writeOffsForecastMap[$item['week']][$item['product_id']];
- } else {
- $model->writeoffs_forecast = 0;
+ $existingRecords = Autoplannogramma::find()
+ ->where([
+ 'month' => $month,
+ 'year' => $year,
+ 'store_id' => $store->id,
+ 'week' => array_unique(array_column($forecast, 'week'))
+ ])
+ ->indexBy(fn($record) => $record->week . '_' . $record->product_id)
+ ->all();
+
+ foreach ($forecast as $item) {
+ $key = $item['week'] . '_' . $item['product_id'];
+ $model = $existingRecords[$key] ?? new Autoplannogramma();
+ $productId = $item['product_id'];
+ $week = $item['week'];
+ $quantity = (float)($item['forecast_week_pieces'] ?? 0);
+
+ $this->stderr(implode("\n", $item), BaseConsole::FG_GREEN);
+
+ $details = [];
+ $total = $quantity;
+
+ if (!empty($writeOffsForecast[$productId][$week]['writeOffs'])) {
+ $writeOffs = $writeOffsForecast[$productId][$week]['writeOffs'];
+ $details['writeOffs']['groups'] = $writeOffs;
+ $total += is_array($writeOffs) ? array_sum($writeOffs) : (float)$writeOffs;
}
- }
-
-
- $needsUpdate = $model->quantity_forecast != $item['forecast_week_pieces'];
+ foreach (['offline', 'online', 'marketplace'] as $type) {
+ $data = $salesForecast[$productId][$week][$type] ?? null;
+ if ($data) {
+ $details[$type]['groups'] = [];
+ if (isset($data['share'])) {
+ $share = (float)$data['share'];
+ $details[$type]['groups']['share'] = $share;
+ $details[$type]['groups']['quantity'] = round($quantity * $share, 2);
+ $total += $details[$type]['groups']['quantity'];
+ unset($data['share']);
+ }
+ if (!empty($data)) {
+ $details[$type]['groups'] = array_merge($details[$type]['groups'], $data);
+ $total += is_array($data) ? array_sum($data) : (float)$data;
+ }
+ }
+ }
- if ($needsUpdate) {
- $model->quantity_forecast = $item['forecast_week_pieces'];
- }
+ $needsUpdate = !$model->isNewRecord && (
+ $model->calculate !== $quantity ||
+ round((float)$model->total, 2) !== round($total, 2) ||
+ json_encode($model->details, JSON_UNESCAPED_UNICODE) !== json_encode($details, JSON_UNESCAPED_UNICODE)
+ );
- if (!$model->save()) {
- $errors = [];
- foreach ($model->getErrors() as $attr => $attrErrors) {
- foreach ($attrErrors as $error) {
- $errors[] = "$attr: $error";
+ if ($model->isNewRecord || $needsUpdate) {
+ $model->setAttributes([
+ 'month' => $month,
+ 'year' => $year,
+ 'week' => $week,
+ 'product_id' => $productId,
+ 'store_id' => $store->id,
+ 'is_archive' => false,
+ 'capacity_type' => 1,
+ 'details' => json_encode($details, JSON_UNESCAPED_UNICODE),
+ 'calculate' => $quantity,
+ 'modify' => $quantity,
+ 'total' => round($total, 2)
+ ]);
+
+ if (!$model->save()) {
+ $errors = implode('; ', array_map(
+ fn($attr, $attrErrors) => "$attr: " . implode(', ', $attrErrors),
+ array_keys($model->getErrors()),
+ $model->getErrors()
+ ));
+ $this->stderr("Ошибка при сохранении модели: $errors\n", BaseConsole::FG_RED);
+ Yii::error("Ошибка сохранения Autoplannogramma: $errors", __METHOD__);
}
}
- $errorMessage = implode('; ', $errors);
- $this->stderr("Ошибка при сохранении модели: $errorMessage\n", BaseConsole::FG_RED);
- Yii::error("Ошибка сохранения Autoplannogramma: $errorMessage", __METHOD__);
}
+
+ $this->stdout("Сохранена автопланограмма для магазина {$store->name}\n", BaseConsole::FG_GREEN);
+ } catch (Throwable $e) {
+ $this->stderr("Ошибка при расчёте прогноза: {$e->getMessage()}\n", BaseConsole::FG_RED);
+ Yii::error("Ошибка при расчёте прогноза: " . $e->getMessage(), __METHOD__);
+ continue;
}
- $this->stdout("Сохранена автопланограмма для магазина {$store->name}\n", BaseConsole::FG_GREEN);
}
+
$this->stdout("Расчет и сохранение автопланограммы завершены\n", BaseConsole::FG_GREEN);
}
}
namespace yii_app\records;
-use Product;
use Yii;
use yii\behaviors\BlameableBehavior;
use yii\behaviors\TimestampBehavior;
* @property string|null $product_id GUID продукта
* @property int|null $store_id ID магазина
* @property int|null $capacity_type Тип планограммы
- * @property int|null $quantity Количество
- * @property int|null $quantity_forecast Количество рассчитанное
- * @property int|null $writeoffs_forecast Количество списано
+ * @property float|null $calculate Суммарное расчетное значение
+ * @property float|null $modify Значение проставленное закупщиком
+ * @property float|null $total Расчетное значение продаж
+ * @property array|null $details Детализация итоговой суммы (JSON)
* @property bool|null $is_archive Архивная ли запись?
* @property bool|null $auto_forecast Значение спрогнозировано?
* @property string|null $created_at Дата создания
*/
class Autoplannogramma extends \yii\db\ActiveRecord
{
- /**
- * {@inheritdoc}
- */
public static function tableName()
{
return 'autoplannogramma';
}
- /**
- * {@inheritdoc}
- */
public function rules()
{
return [
[['week', 'month', 'year', 'store_id', 'capacity_type', 'created_by', 'updated_by'], 'integer'],
[['is_archive', 'auto_forecast'], 'boolean'],
- [[ 'quantity', 'quantity_forecast', 'writeoffs_forecast'], 'number'],
+ [['calculate', 'modify', 'total'], 'number'],
+ [['details'], 'safe'],
[['created_at', 'updated_at'], 'safe'],
[['auto_forecast'], 'default', 'value' => true],
[['product_id'], 'string', 'max' => 255],
'class' => TimestampBehavior::class,
'createdAtAttribute' => 'created_at',
'updatedAtAttribute' => 'updated_at',
- 'value' => new Expression('NOW()')
+ 'value' => new Expression('NOW()'),
],
[
'class' => BlameableBehavior::class,
];
}
- /**
- * {@inheritdoc}
- */
public function attributeLabels()
{
return [
'product_id' => 'GUID продукта',
'store_id' => 'ID магазина',
'capacity_type' => 'Тип планограммы',
- 'quantity' => 'Количество',
- 'quantity_forecast' => 'Количество рассчитанное',
- 'writeoffs_forecast' => 'Количество списано',
+ 'calculate' => 'Суммарное расчетное значение',
+ 'modify' => 'Значение проставленное закупщиком',
+ 'total' => 'Расчетное значение продаж',
+ 'details' => 'Детализация итоговой суммы',
'is_archive' => 'Архивная ли запись?',
'auto_forecast' => 'Значение спрогнозировано?',
'created_at' => 'Дата создания',
];
}
- public function getProducts() {
+ public function getProducts()
+ {
return $this->hasMany(Products1cNomenclature::class, ['id' => 'product_id']);
}
}
return $pricesMap;
}
- public function getWeeklyBouquetProductsForecast($month, $year, $storeId = null, $weekNumber = null)
+
+ public function getWeeklyBouquetProductsSalesForecast($month, $year, $storeId = null, $weekNumber = null)
{
$matrixGroups = ArrayHelper::map(
MatrixBouquetForecast::find()->select(['group'])->distinct()->asArray()->all(),
}
}
- $grouped = [];
$grouped = [];
$salesShares = [];
->indexBy('store_id')
->asArray()
->all();
+
if ($plans) {
foreach ($plans as $storeId => $plan) {
$total = $plan['total_sales_plan'];
$guid = (string)$item['product_guid'];
$group = (string)$item['matrix_group'];
$type = (string)$item['type'];
+ $week = (string)$item['week'];
$forecastValue = (float)$item['week_forecast'];
if (isset($salesShares[$storeItem]) && isset($salesShares[$storeItem][$type])) {
- $grouped[$storeItem][$type]['share'] = $salesShares[$storeItem][$type];
+ $grouped[$week][$type]['share'] = $salesShares[$storeItem][$type];
}
- $grouped[$storeItem][$guid][$type][$group] = $forecastValue;
+ $grouped[$guid][$week][$type][$group] = $forecastValue;
}
return $grouped;
'category' => $forecast['category'] ?? '',
'subcategory' => $forecast['subcategory'] ?? '',
'species' => $forecast['species'] ?? '',
- 'product_id' => $forecast['product_id'] ?? '',
+ 'product_guid' => $forecast['product_id'] ?? '',
'name' => $forecast['name'] ?? '',
'price' => $forecast['price'] ?? '',
'goal' => $forecast['goal'] ?? 0,
}
}
- return $weeksProductForecast;
+ $grouped = [];
+ foreach ($weeksProductForecast as $item) {
+ $productId = $item['product_guid'];
+ $week = $item['week'];
+ $type = 'writeOffs';
+ $forecastValue = $item['forecast'];
+ $grouped[$productId][$week][$type] = $forecastValue;
+
+ }
+
+ return $grouped;
}
}
\ No newline at end of file