Вычитаем месяцы из даты: встроенный язык, запросы и СКД

· 9 мин

Содержание

Отнять от даты один или несколько месяцев в 1С 8.3 проще всего функцией ДобавитьМесяц с отрицательным аргументом: ДобавитьМесяц(Дата, -1). Она есть и в 8.3, и в 8.2, вызывается из любого модуля и сама разбирается с разной длиной месяцев.

Та же задача во встроенном языке, в языке запросов и в СКД решается разными функциями. Ниже — как вычесть месяц от текущей даты, как получить начало и конец прошлого месяца и почему нельзя отнимать месяц вычитанием секунд.

В официальной документации нет функций с именами ДатаДоб() или НачМесяца(). Рабочие имена встроенного языка — ДобавитьМесяц(), НачалоМесяца(), КонецМесяца(). Их и используйте.

Главный инструмент: функция ДобавитьМесяц

ДобавитьМесяц(<Дата>, <ЧислоМесяцев>) сдвигает дату на указанное число месяцев. Чтобы отнять — передайте отрицательное число.

// минус один месяц от текущей даты сеанса
ПрошлыйМесяц = ДобавитьМесяц(ТекущаяДатаСеанса(), -1);

// минус три месяца от конкретной даты
Квартал = ДобавитьМесяц(Дата(2026, 1, 15), -3); // 15.10.2025

Главное достоинство функции — она сама обрабатывает «неудобные» даты. Если в месяце-получателе нужного числа нет, возвращается последний день этого месяца:

// 31 марта минус месяц = 29 февраля (2024 — високосный)
Результат = ДобавитьМесяц(Дата(2024, 3, 31), -1); // 29.02.2024

Не пытайтесь воспроизвести это руками через Число() и Дата() — логика «последнего дня» уже встроена, свой код только добавит ошибок.

Функция доступна и на клиенте, и на сервере, и в общих модулях — отдельное подключение не нужно. Число месяцев может быть любым целым: -1 отнимает один месяц, -18 — полтора года, положительное число прибавляет. Возвращается значение того же типа «Дата», что и аргумент, поэтому результат сразу можно сравнивать с другими датами или записывать в реквизит.

Время в результате сохраняется от исходной даты. Если нужны «чистые» сутки без часов и минут, оберните результат в НачалоДня:

ДатаБезВремени = НачалоДня(ДобавитьМесяц(ДокументДата, -1));

Несколько частых формулировок одной задачи и готовый код:

Что нужноКод встроенного языка
Отнять 1 месяцДобавитьМесяц(Дата, -1)
Отнять 6 месяцев (полгода)ДобавитьМесяц(Дата, -6)
Отнять годДобавитьМесяц(Дата, -12)
Вычесть 2 годаДобавитьМесяц(Дата, -24)

Начало и конец прошлого месяца

Для отчётов обычно нужна не произвольная дата минус месяц, а границы прошлого месяца целиком. Их дают НачалоМесяца и КонецМесяца.

// первый день прошлого месяца, 00:00:00
НачалоПрошлого = НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1));

// последний день прошлого месяца, 23:59:59
КонецПрошлого = КонецМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1));

Порядок вызовов не важен — результат совпадёт, если сначала сдвинуть, а потом взять границу, или наоборот. Но сдвиг первым читается понятнее: «от сегодняшней даты шагнули на месяц назад, потом взяли начало месяца».

КонецМесяца возвращает время 23:59:59. Если используете эту дату как верхнюю границу периода в отчёте, учитывайте секунды: документ, проведённый в 23:59:58, в период попадёт, а созданный ровно в полночь следующих суток — уже нет.

Полезные производные для стандартных периодов:

  • 📅 начало прошлого квартала — НачалоКвартала(ДобавитьМесяц(Дата, -3));
  • 📅 начало прошлого года — НачалоГода(ДобавитьМесяц(Дата, -12));
  • 📅 ровно месяц назад с сохранением числа — ДобавитьМесяц(Дата, -1) без обёрток.
Где вам нужно отнять месяц?

Вычитание месяца в языке запросов и СКД

Внутри запроса функции встроенного языка недоступны — там свои имена. Отнять месяц помогает ДОБАВИТЬКДАТЕ.

ВЫБРАТЬ
    Продажи.Период КАК Период,
    ДОБАВИТЬКДАТЕ(Продажи.Период, МЕСЯЦ, -1) КАК ПредыдущийМесяц
ИЗ
    РегистрНакопления.Продажи КАК Продажи

Синтаксис — ДОБАВИТЬКДАТЕ(<Дата>, <ВидИнтервала>, <Количество>). Вид интервала пишется без кавычек: МЕСЯЦ, ДЕНЬ, ГОД, КВАРТАЛ, НЕДЕЛЯ, ЧАС, МИНУТА, СЕКУНДА. Отрицательное количество отнимает.

Границы периода в запросе дают НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА:

// первый день прошлого месяца
НАЧАЛОПЕРИОДА(ДОБАВИТЬКДАТЕ(&Дата, МЕСЯЦ, -1), МЕСЯЦ)

// последний день прошлого месяца
КОНЕЦПЕРИОДА(ДОБАВИТЬКДАТЕ(&Дата, МЕСЯЦ, -1), МЕСЯЦ)

В СКД то же выражение можно задать прямо в поле вычисляемого ресурса или в пользовательском поле-выражении — движок СКД использует синтаксис языка запросов, поэтому код из примеров выше переносится без изменений.

Соответствие функций в двух языках — чтобы не путать имена при переносе кода между модулем и запросом:

ЗадачаВстроенный языкЯзык запросов и СКД
Сдвиг на месяцыДобавитьМесяц(Д, -N)ДОБАВИТЬКДАТЕ(Д, МЕСЯЦ, -N)
Начало месяцаНачалоМесяца(Д)НАЧАЛОПЕРИОДА(Д, МЕСЯЦ)
Конец месяцаКонецМесяца(Д)КОНЕЦПЕРИОДА(Д, МЕСЯЦ)
Разница в месяцахсчитается вручнуюРАЗНОСТЬДАТ(Д1, Д2, МЕСЯЦ)

Если дата минус месяц нужна как параметр отбора всего запроса, посчитайте её во встроенном языке и передайте через УстановитьПараметр("ДатаОт", НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1))). Это читабельнее, чем городить ДОБАВИТЬКДАТЕ вокруг &ТекущаяДата в каждом условии.

Почему нельзя отнимать месяц секундами и циклами

Дата в 1С хранится как число секунд, поэтому велик соблазн отнять «тридцать дней» вычитанием. Так делать нельзя: месяцы разной длины.

// НЕВЕРНО: 30 суток в секундах
Неправильно = Дата(2026, 3, 31) - 30 * 86400; // получится 01.03.2026, а не февраль

В примере от 31 марта вычли 30 суток и получили 1 марта — тот же месяц, а не предыдущий. Для 31-дневных месяцев сдвиг не попадёт в нужную точку, для февраля промахнётся ещё сильнее.

Второй распространённый приём — вычитать месяц в цикле «по одному». Он тоже лишний: ДобавитьМесяц принимает любое отрицательное число за один вызов.

// Лишний цикл
Дата = ИсходнаяДата;
Для Сч = 1 По 6 Цикл
    Дата = ДобавитьМесяц(Дата, -1);
КонецЦикла;

// То же самое одной строкой
Дата = ДобавитьМесяц(ИсходнаяДата, -6);

Вычитание секунд оправдано только для фиксированных интервалов — «минус сутки», «минус час». Для сдвига по календарю (месяц, квартал, год) всегда берите ДобавитьМесяц (год — это -12 месяцев) или ДОБАВИТЬКДАТЕ.

Граничные случаи: пустая дата, минимум и конец месяца

В 1С нет отрицательных дат. Диапазон дат — от 01.01.0001 до 31.12.9999. Пустая дата хранится как 01.01.0001 00:00:00 — это и есть «нулевое» значение реквизита типа «Дата».

Из этого следуют практичные правила:

  • 🔹 Проверяйте пустую дату до вычитания. ДобавитьМесяц('00010101', -1) упрётся в нижнюю границу. Перед сдвигом ставьте проверку Если ЗначениеЗаполнено(Дата) Тогда.
  • 🔹 Не уходите ниже минимума. Вычитание большого числа месяцев от ранней даты даст ошибку выхода за диапазон, а не «отрицательную» дату.
  • 🔹 Помните про перенос на последний день. Для 29, 30 и 31 числа результат может «съехать» на конец более короткого месяца — это не ошибка, а штатное поведение ДобавитьМесяц.
Если ЗначениеЗаполнено(ДатаНачисления) Тогда
    ДатаПрошлая = ДобавитьМесяц(ДатаНачисления, -1);
Иначе
    ДатаПрошлая = Дата(1, 1, 1); // оставляем пустой
КонецЕсли;

ТекущаяДата() на сервере в управляемом приложении возвращает дату компьютера сервера и считается устаревшей. В клиент-серверной работе берите ТекущаяДатаСеанса() — она учитывает часовой пояс сеанса.

Практические примеры: отчётность, зарплата, аналитика

Вычитание месяца чаще всего нужно, чтобы взять предыдущий период. Несколько готовых фрагментов под реальные задачи.

Период прошлого месяца для отчёта. Пользователь открыл отчёт — по умолчанию подставляем прошлый месяц целиком:

База = ТекущаяДатаСеанса();
ПериодОтчета.ДатаНачала  = НачалоМесяца(ДобавитьМесяц(База, -1));
ПериодОтчета.ДатаОкончания = КонецМесяца(ДобавитьМесяц(База, -1));

Дата месяц назад от даты документа. Например, срок, отсчитанный назад от даты начисления:

КонтрольнаяДата = ДобавитьМесяц(Начисление.Дата, -1);

Сравнение «месяц к месяцу» в запросе. Берём выручку текущего и предыдущего месяца одним запросом через ДОБАВИТЬКДАТЕ в условии соединения — без двух отдельных выборок.

Диапазон за полгода назад. Для анализа динамики:

Конец  = КонецМесяца(ТекущаяДатаСеанса());
Начало = НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -5)); // 6 месяцев включительно

Обратите внимание на -5, а не -6: чтобы захватить шесть месяцев вместе с текущим, отступаем на пять назад. Пересчитайте на своих датах, если нужен другой край диапазона.

Расчётный период «месяц назад» в зарплатных расчётах. Когда нужна граница периода, предшествующего текущему месяцу начисления, отступаем от начала месяца начисления:

НачалоТекущего = НачалоМесяца(МесяцНачисления);
НачалоПредыдущего = ДобавитьМесяц(НачалоТекущего, -1);
КонецПредыдущего  = КонецМесяца(НачалоПредыдущего);

Такой приём универсален: сначала приводим произвольную дату к началу месяца, затем сдвигаем на нужное число месяцев. Так результат не зависит от того, какое число месяца было в исходной дате, — а это частый источник расхождений, когда документы в периоде датированы по-разному.

Если вычисляете границу периода для отбора в отчёте или для подбора данных, приводите дату к началу или концу месяца явно. Иначе документы, оформленные в первой половине месяца, и документы конца месяца попадут в разные периоды из-за сохранённого в дате времени.

Производительность при работе с датами

На единичных вычислениях скорость неважна. Разница видна в циклах по тысячам строк и в обработках документов.

  • ⚡ Выносите постоянные даты из цикла. Если граница периода не меняется, посчитайте ДобавитьМесяц один раз до Для … Цикл, а не на каждой итерации.
  • ⚡ Сдвигайте дату в запросе, а не в коде после выборки. ДОБАВИТЬКДАТЕ внутри запроса отрабатывает на стороне СУБД по всему набору сразу. Перебирать результат и вызывать ДобавитьМесяц в цикле медленнее.
  • ⚡ Не пересчитывайте одно и то же. НачалоМесяца(ДобавитьМесяц(...)) в условии отбора считайте один раз в переменную и подставляйте её.
// Хорошо: один расчёт до цикла
ГраницаПериода = НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1));
Для Каждого Строка Из Таблица Цикл
    Если Строка.Дата >= ГраницаПериода Тогда
        // ...
    КонецЕсли;
КонецЦикла;

Для больших выборок правило простое: календарную арифметику по датам старайтесь делать в запросе, а в коде оставлять только готовые границы.

Сколько месяцев между двумя датами

Обратная задача к вычитанию — посчитать, на сколько месяцев одна дата отстоит от другой. Нужна для стажа, для числа месяцев аренды, для периодов начислений.

В языке запросов для этого есть РАЗНОСТЬДАТ(<Дата1>, <Дата2>, <Тип>). Тип интервала — такой же набор, как у ДОБАВИТЬКДАТЕ:

ВЫБРАТЬ
    РАЗНОСТЬДАТ(Договор.ДатаНачала, Договор.ДатаОкончания, МЕСЯЦ) КАК Месяцев
ИЗ
    Справочник.Договоры КАК Договор

Во встроенном языке готовой функции «разница в месяцах» нет — считаем через год и месяц каждой даты:

МесяцевВсего = (Год(ДатаКонца) - Год(ДатаНачала)) * 12
             + (Месяц(ДатаКонца) - Месяц(ДатаНачала));

Проверьте логику на примере: с 01.03.2025 по 01.10.2026 формула даёт (2026 - 2025) * 12 + (10 - 3) = 12 + 7 = 19 месяцев. Если важно учитывать неполные месяцы (число второй даты меньше числа первой), добавьте поправку на день после сравнения чисел.

Для стажа и расчётных периодов, где нужны и годы, и месяцы, и дни одновременно, удобнее хранить границы и считать разницу в запросе: РАЗНОСТЬДАТ сразу вернёт целое число интервалов выбранного типа.

Частые вопросы

Чем `ДобавитьМесяц` лучше вычитания секунд?
Месяцы разной длины, поэтому вычитание фиксированного числа секунд даёт неверную дату. ДобавитьМесяц сдвигает по календарю и сам обрабатывает конец месяца.
Почему 31 марта минус месяц даёт 29 февраля, а не 28 или 31?
Если в месяце-получателе нужного числа нет, функция возвращает последний день этого месяца. В високосном году февраль заканчивается 29-го, поэтому результат — 29.02.
Как отнять месяц прямо в запросе или в СКД?
Функцией ДОБАВИТЬКДАТЕ(Поле, МЕСЯЦ, -1). Для начала и конца периода — НАЧАЛОПЕРИОДА и КОНЕЦПЕРИОДА.
Как получить весь прошлый месяц для отчёта?
НачалоМесяца(ДобавитьМесяц(ТекущаяДатаСеанса(), -1)) для начала и КонецМесяца(...) для конца. Конец месяца возвращает время 23:59:59.
Можно ли уйти в отрицательную дату?
Нет. Минимальная дата в 1С — 01.01.0001, пустая дата хранится этим же значением. Перед вычитанием проверяйте дату на заполненность.