}
}
+ public static function calculateSales($store_id, $year, $month) {
+ $monthStart = date("Y-m-d 00:00:00", strtotime($year . '-' . $month . '-1'));
+ $monthEnd = date("Y-m-t 23:59:59", strtotime($year . '-' . $month . '-1'));
+
+ foreach (range(1, 5) as $ind) {
+ $weekStart = date("Y-m-d 00:00:00", strtotime("+" . (($ind - 1) * 7) . ' days', strtotime($monthStart)));
+ $weekEnd = date("Y-m-d 23:59:59", strtotime("+" . ($ind * 7 - 1) . ' days', strtotime($monthStart)));
+ if ($weekEnd > $monthEnd) {
+ $weekEnd = $monthEnd;
+ }
+
+ $sales = Sales::find()
+ ->where(['between', 'date', $weekStart, $weekEnd])
+ ->andWhere(['store_id' => $store_id])
+ ->andWhere(['operation' => Sales::OPERATION_SALE])
+ ->asArray()->all();
+ $salesIds = ArrayHelper::getColumn($sales, 'id');
+
+ // Ищем чеки-возврат на текущие чеки
+ $returnSales = Sales::find()->where(['operation' => Sales::OPERATION_RETURN, 'sales_check' => $salesIds])->all();
+ $returnSalesIds = ArrayHelper::getColumn($returnSales, 'sales_check');
+
+ // Offline sales
+ $salesOffline = Sales::find()->select(['SUM(summ) as total'])
+ ->where(['between', 'date', $weekStart, $weekEnd])
+ ->andWhere(['order_id' => ['', '0']])
+ ->andWhere(['store_id' => $store_id])
+ ->andWhere(['operation' => Sales::OPERATION_SALE])
+ ->andWhere(['NOT IN', 'id', $returnSalesIds])
+ ->asArray()->one();
+
+ // Online sales
+ $salesOnline = Sales::find()->select(['SUM(summ) as total'])
+ ->where(['between', 'date', $weekStart, $weekEnd])
+ ->andWhere(['NOT IN', 'order_id', ['', '0']])
+ ->andWhere(['store_id' => $store_id])
+ ->andWhere(['operation' => Sales::OPERATION_SALE])
+ ->andWhere(['NOT IN', 'id', $returnSalesIds])
+ ->asArray()->one();
+
+ $motivation = Motivation::find()->where(['store_id' => $store_id, 'year' => $year, 'month' => $month])->one();
+ $motivationValueGroup = MotivationValueGroup::find()->where(['alias' => 'week' . $ind])->one();
+ foreach (['Оффлайн продажи', 'Онлайн продажи'] as $topicInd => $topic) {
+ $motivationCostsItem = MotivationCostsItem::find()->where(['name' => $topic])->one();
+ /** @var $motivationCostsItem MotivationCostsItem */
+ if ($motivation) {
+ $motivationValue = MotivationValue::find()->where(['motivation_id' => $motivation->id,
+ 'motivation_group_id' => $motivationValueGroup->id, 'value_id' => $motivationCostsItem->code])->one();
+ if (!$motivationValue) {
+ $motivationValue = new MotivationValue;
+ $motivationValue->motivation_id = $motivation->id;
+ $motivationValue->motivation_group_id = $motivationValueGroup->id;
+ $motivationValue->value_id = $motivationCostsItem->code;
+ $motivationValue->value_type = $motivationCostsItem->data_type;
+ }
+ $motivationValue->value_float = [$salesOffline, $salesOnline][$topicInd]['total'];
+ $motivationValue->save();
+ if ($motivationValue->getErrors()) {
+ throw new \Exception(Json::encode($motivationValue->getErrors()));
+ }
+ }
+ }
+ }
+ }
++
+ /**
+ * Получение записей по фактическому количеству смен в магазине за указанный период.
+ *
+ * @param string $startDate Дата начала периода в формате 'YYYY-MM-DD'.
+ * @param string $endDate Дата окончания периода в формате 'YYYY-MM-DD'.
+ * @param int $storeId Идентификатор магазина.
+ *
+ * @return array|null Возвращает массив записей из модели TimetableFactModel, соответствующих условиям,
+ * или null, если записи не найдены.
+ */
+ public static function getTimetableFactRecordsByDateAndStore($startDate, $endDate, $storeId)
+ {
+
+
+ // Делаем запрос к TimetableFactModel
+ $records = TimetableFactModel::find()
+ ->where(['store_id' => $storeId])
+ ->andWhere(['between', 'date_shift', $startDate, $endDate])
+ ->all();
+
+ return $records;
+ }
+
+ /**
+ * Получение суммы отпускных для магазина за определенный период.
+ *
+ * Этот метод выполняет поиск записей в таблице Timetable с `slot_type_id` равным 2,
+ * которые соответствуют указанному магазину и попадают в заданный период.
+ * Для каждой записи находится самая последняя запись о платеже сотрудника,
+ * и её значение используется для расчета суммы отпускных.
+ *
+ * @param string $startDate Дата начала периода в формате 'YYYY-MM-DD'.
+ * @param string $endDate Дата окончания периода в формате 'YYYY-MM-DD'.
+ * @param int $storeId Идентификатор магазина.
+ *
+ * @return float Возвращает общую сумму отпускных за указанный период.
+ * Если записей не найдено, возвращается 0.0.
+ */
+ public static function getVacationsSum($startDate, $endDate, $storeId):float
+ {
+ // Делаем запрос к таблице Timetable для получения записей с slot_type_id = 2 (Timetable::TIMESLOT_VACATION)
+ $records = Timetable::find()
+
+ ->where(['store_id' => $storeId])
+ ->andWhere(['between', 'date', $startDate, $endDate])
+ ->andWhere(['slot_type_id' => Timetable::TIMESLOT_VACATION])
+ ->all();
+
+ // Проверяем, есть ли записи
+ if (empty($records)) {
+ return 0.0; // Возвращаем 0, если записей нет
+ }
+
+ $dailyPayments = self::getEmployeePayments($endDate);
+
+ $vacationsSum = 0.0;
+ foreach ($records as $record) {
+ $dailyPayment = isset($dailyPayments[$record->admin_id]) ? $dailyPayments[$record->admin_id] : 0.0;
+
+
+ $vacationsSum += $dailyPayment;
+
+ }
+
+ return $vacationsSum;
+ }
+
+ /**
+ * Получение суммы всех недель для определенного `value_id` из таблицы `motivation_value`.
+ *
+ * Метод выполняет запрос к модели `MotivationValue`, чтобы получить все записи,
+ * соответствующие указанным `motivation_id` и `value_id`, и принадлежащие одной из
+ * определенных групп мотивации. Затем метод суммирует значения поля `value_float`
+ * из всех полученных записей.
+ *
+ * @param int $id Идентификатор мотивации (`motivation_id`).
+ * @param int $valueId Идентификатор значения (`value_id`).
+ *
+ * @return float Возвращает сумму всех значений `value_float` для заданного `value_id`.
+ * Если записи не найдены, возвращается 0.
+ */
+ public static function getMonthSum($id, $valueId)
+ {
+ $aliases = ['week1', 'week2', 'week3', 'week4', 'week5'];
+ $groupIds = MotivationCostsItem::find()
+ ->select('id')
+ ->where(['alias' => $aliases])
+ ->column();
+ // Запрос к модели MotivationValue для получения записей с заданными условиями
+ $records = MotivationValue::find()
+ ->where(['motivation_id' => $id, 'value_id' => $valueId])
+ ->andWhere(['in', 'motivation_group_id', $groupIds])
+ ->all();
+
+ // Получаем массив значений value_float
+ $values = array_map(function($record) {
+ return $record->value_float;
+ }, $records);
+
+ // Суммируем значения value_float
+ $totalSum = array_sum($values);
+
+ return $totalSum;
+ }
+
+ /**
+ * Создает или обновляет запись в таблице `motivation_value`.
+ *
+ * Метод пытается найти существующую запись в таблице `motivation_value` по заданным
+ * `motivation_id`, `group_id` и `value_id`. Если запись не найдена, создается новая.
+ * В зависимости от типа значения (`value_type`), метод устанавливает соответствующее поле
+ * (`value_float`, `value_int`, `value_string`) и сохраняет запись.
+ * Если сохранение успешно, возвращается ID записи. В случае ошибки возвращается `false`.
+ *
+ * @param int $motivationId Идентификатор мотивации.
+ * @param string $groupAlias Алиас группы мотивации.
+ * @param int $valueId Идентификатор значения.
+ * @param string $valueType Тип значения, который может быть 'float', 'int' или 'string'.
+ * @param string $value_string Значение, которое нужно сохранить (string).
+ * @param int $value_int Значение, которое нужно сохранить (int).
+ * @param float $value_float Значение, которое нужно сохранить (float).
+ *
+ * @return int|false Возвращает ID сохраненной или обновленной записи, либо `false` в случае ошибки.
+ * @throws \InvalidArgumentException Если передан некорректный тип значения.
+ */
+ public static function saveOrUpdateMotivationValue($motivationId, $groupAlias, $valueId, $valueType, $value)
+ {
+
+ // Найти id по алиасу в таблице MotivationValueGroup
+ $group = MotivationValueGroup::findOne(['alias' => $groupAlias]);
+
+ if ($group === null) {
+ throw new \InvalidArgumentException("Неверный алиас группы: $groupAlias");
+ }
+
+ $groupId = $group->id;
+ // Найти существующую запись
+ $motivationValue = MotivationValue::findOne([
+ 'motivation_id' => $motivationId,
+ 'motivation_group_id' => $groupId,
+ 'value_id' => $valueId,
+ ]);
+
+ // Если запись не найдена, создать новую
+ if ($motivationValue === null) {
+ $motivationValue = new MotivationValue();
+ $motivationValue->motivation_id = $motivationId;
+ $motivationValue->motivation_group_id = $groupId;
+ $motivationValue->value_id = $valueId;
+ $motivationValue->value_type = $valueType;
+ }
+
+ // Установить значение в зависимости от типа
+ switch ($valueType) {
+ case 'float':
+ $motivationValue->value_float = $value;
+ break;
+ case 'int':
+ $motivationValue->value_int = $value;
+ break;
+ case 'string':
+ $motivationValue->value_string = $value;
+ break;
+ default:
+ throw new \InvalidArgumentException("Неправильное значение типа: $valueType");
+ }
+
+ // Сохранить запись и вернуть id или false
+ if ($motivationValue->save()) {
+ return $motivationValue->id;
+ } else {
+ Yii::error("Не удалось сохранить значение: " . json_encode($motivationValue->errors));
+ return false;
+ }
+ }
+
+ /**
+ * Определяет номер недели в месяце для указанной даты.
+ *
+ * Метод вычисляет номер недели в месяце на основе дня месяца, переданного в объекте `DateTime`.
+ * Недели считаются следующим образом:
+ * - 1-я неделя: с 1-го по 7-й день месяца.
+ * - 2-я неделя: с 8-го по 14-й день месяца.
+ * - 3-я неделя: с 15-го по 21-й день месяца.
+ * - 4-я неделя: с 22-го по 28-й день месяца.
+ * - 5-я неделя: с 29-го дня месяца и до конца месяца.
+ *
+ * @param string $date Строка даты в формате 'Y-m-d', для которой нужно определить номер недели.
+ *
+ * @return int Номер недели в месяце (от 1 до 5).
+ */
+ public static function getWeekOfMonth($date)
+ {
+ // Преобразуем строку даты в день месяца
+ $dayOfMonth = intval(date('j', strtotime($date)));
+
+ // Определяем номер недели в месяце
+ if ($dayOfMonth <= 7) {
+ return 1;
+ } elseif ($dayOfMonth <= 14) {
+ return 2;
+ } elseif ($dayOfMonth <= 21) {
+ return 3;
+ } elseif ($dayOfMonth <= 28) {
+ return 4;
+ } else {
+ return 5;
+ }
+ }
+
+ /**
+ * Возвращает дату начала указанной недели месяца для заданной даты.
+ *
+ * Метод определяет начало недели в месяце на основе номера недели, переданного в параметре `$weekOfMonth`.
+ * Возвращается объект `DateTime`, представляющий первый день этой недели. Если номер недели выходит за пределы
+ * допустимых значений (1-5), по умолчанию возвращается начало первой недели месяца.
+ *
+ * @param string $date Строка даты в формате 'Y-m-d', представляющая дату, для которой определяется неделя.
+ * @param int $weekOfMonth Номер недели в месяце (от 1 до 5).
+ *
+ * @return string Дата начала указанной недели месяца в формате 'Y-m-d'.
+ */
+ public static function getStartOfWeek($date, $weekOfMonth)
+ {
+ // Извлекаем год и месяц из строки даты
+ $year = date('Y', strtotime($date));
+ $month = date('m', strtotime($date));
+
+ switch ($weekOfMonth) {
+ case 1:
+ return sprintf("%s-%s-01", $year, $month);
+ case 2:
+ return sprintf("%s-%s-08", $year, $month);
+ case 3:
+ return sprintf("%s-%s-15", $year, $month);
+ case 4:
+ return sprintf("%s-%s-22", $year, $month);
+ case 5:
+ return sprintf("%s-%s-29", $year, $month);
+ default:
+ throw new \InvalidArgumentException("Некорректное значение для недели месяца: $weekOfMonth");
+ }
+ }
+
+ /**
+ * Вычисление общей суммы фонда оплаты труда (ФОТ) - зарплаты и отпускных для магазина за указанный период.
+ *
+ * Этот метод сначала получает записи о сменах сотрудников за указанный период и сумму отпускных.
+ * Затем для каждой записи определяется соответствующая дневная зарплата, и она добавляется к общей сумме.
+ * Если в записи уже указана зарплата за смену, она используется вместо дневной зарплаты.
+ * В конце к общей сумме зарплат добавляется сумма отпускных.
+ *
+ * @param string $startDate Дата начала периода в формате 'YYYY-MM-DD'.
+ * @param string $endDate Дата окончания периода в формате 'YYYY-MM-DD'.
+ * @param int $storeId Идентификатор магазина.
+ *
+ * @return float Возвращает общую сумму фонда оплаты труда за указанный период,
+ * включая зарплаты и отпускные.
+ */
+ public static function calculateTotalSalary($startDate, $endDate, $storeId):float
+ {
+ $records = self::getTimetableFactRecordsByDateAndStore($startDate, $endDate, $storeId);
+ $vacationSum = self::getVacationsSum($startDate, $endDate, $storeId);
+ $dailyPayments = self::getEmployeePayments($endDate);
+ $totalSalary = 0.0;
+ foreach ($records as $record) {
+ $dailyPayment = isset($dailyPayments[$record->admin_id]) ? $dailyPayments[$record->admin_id] : 0.0;
+
+ if (!empty($record->salary_shift)) {
+ $totalSalary += $record->salary_shift;
+ } else {
+ $totalSalary += $dailyPayment;
+ }
+ }
+
+ return $totalSalary + $vacationSum;
+ }
+
+
+ /**
+ * @param $currentDate
+ * @return array
+ */
+ public static function getEmployeePayments($currentDate): array
+ {
+ // Запрос для получения всех записей, но только с учетом последней даты для каждого admin_id
+ $employeePayments = EmployeePayment::find()
+ ->where(['<=', 'date', $currentDate])
+ ->orderBy(['admin_id' => SORT_ASC, 'date' => SORT_DESC])
+ ->all();
+
+ // Преобразование результатов в массив admin_id => daily_payment
+ $dailyPayments = [];
+ foreach ($employeePayments as $payment) {
+ if (!isset($dailyPayments[$payment->admin_id])) {
+ $dailyPayments[$payment->admin_id] = $payment->daily_payment;
+ }
+ }
+ return $dailyPayments;
+ }
}