При разработке информационных систем на платформе 1С:Предприятие 8.3 часто возникает задача по автоматическому расчету даты доставки, которая зависит от различных параметров, таких как зона и категория доставки. Эта функция помогает повысить точность планирования и автоматизировать рутинные операции. В данном руководстве мы подробно рассмотрим, как реализовать подобный механизм на управляемых формах, учтем типичные ошибки и предложим оптимальные решения, включая работу с производственным календарем и гибкое управление правилами через регистры сведений.
Для начала рассмотрим базовый подход, основанный на условной логике в обработчиках событий формы документа. Нам потребуется определить, в каком месте кода мы будем проверять условия и выполнять расчет. Обычно для таких задач используются события формы документа, например, ПриИзменении для реквизитов "Зона" и "Категория" или ПередЗаписью всего документа.
Выбор обработчика события зависит от того, когда должна обновляться дата доставки:
ПриИзменении для соответствующих полей. Этот подход удобен для пользователя, так как он сразу видит результат.ПередЗаписью формы или модуля объекта. Этот вариант может быть более производительным при большом количестве проверок, но менее интерактивным для пользователя.В нашем случае, для обеспечения интерактивности, предпочтительнее использовать ПриИзменении для полей "Зона" и "КатегорияДоставки", а также можно дополнить логику в ПередЗаписьюНаСервере для окончательной проверки перед сохранением.
В обработчике события мы будем использовать конструкцию Если...ИначеЕсли...КонецЕсли для проверки различных комбинаций зоны и категории доставки.
Пример базовой реализации в обработчике ПриИзменении:
&НаКлиенте
Процедура ЗонаПриИзменении(Элемент)
ОбновитьДатуДоставки();
КонецПроцедуры
&НаКлиенте
Процедура КатегорияДоставкиПриИзменении(Элемент)
ОбновитьДатуДоставки();
КонецПроцедуры
&НаКлиенте
Процедура ОбновитьДатуДоставки()
Если Объект.Зона.Пустая() ИЛИ Объект.КатегорияДоставки.Пустая() Тогда
Объект.ДатаДоставкиОриентир = Неопределено;
Возврат;
КонецЕсли;
// Предполагаем, что Зона и КатегорияДоставки - это ссылочные типы (справочники)
// и их значения сравниваются по ссылкам или по их строковому представлению, если это строка.
// Если Зона - это ссылка на справочник, то сравнение будет Зона = Справочники.ЗоныДоставки.НайтиПоНаименованию("000000001")
// Для примера, как если бы сравнивались строковые представления или коды
// Предположим, что Зона имеет наименование "000000001" или "000000002"
// А КатегорияДоставки имеет наименование "Стандарт" или "Экспресс"
Если Лев(Объект.Зона.Наименование, 9) = "000000001" И Лев(Объект.КатегорияДоставки.Наименование, 8) = "Стандарт" Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата() + 60*60*24 * 1; // Текущая дата + 1 день
ИначеЕсли Лев(Объект.Зона.Наименование, 9) = "000000002" И Лев(Объект.КатегорияДоставки.Наименование, 8) = "Стандарт" Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата() + 60*60*24 * 2; // Текущая дата + 2 дня
ИначеЕсли Лев(Объект.Зона.Наименование, 25) = "Внутригородская доставка" И Лев(Объект.КатегорияДоставки.Наименование, 8) = "Экспресс" Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата(); // Текущая дата
Иначе
Объект.ДатаДоставкиОриентир = ТекущаяДата(); // По умолчанию, если нет совпадений
КонецЕсли;
КонецПроцедуры
В этом примере мы используем функцию ТекущаяДата(), которая возвращает текущую дату и время. Для добавления дней к дате мы прибавляем количество секунд в одном дне, умноженное на количество необходимых дней. Один день содержит 60 секунд * 60 минут * 24 часа = 86400 секунд. Поэтому, чтобы прибавить N дней, мы пишем ТекущаяДата() + N*86400.
В процессе работы с полями типа "ссылка" (например, на элементы справочников "ЗоныДоставки" или "КатегорииДоставки") часто возникают ошибки, связанные с некорректным сравнением значений. Рассмотрим их подробнее.
Как было подмечено в обсуждении на форуме, одной из распространенных ошибок является попытка сравнить строковое представление ссылки (например, наименование или код) с фактической строкой. Если реквизит Объект.Зона является ссылочным типом (например, элемент справочника "Зоны доставки"), то прямое сравнение Объект.Зона = "000000001" будет некорректным. Правильнее сравнивать по наименованию, коду или по самой ссылке, предварительно получив ее.
Объект.Зона.Наименование = "000000001". Однако это может быть ненадежно, если наименования могут меняться или не являются уникальными.Объект.Зона.Код = "000000001". Более надежно, если коды уникальны и неизменны.НайтиПоНаименованию() или НайтиПоКоду():
ЗначениеЗоны1 = Справочники.ЗоныДоставки.НайтиПоНаименованию("000000001");
Если ЗначениеЗоны1 <> Неопределено И Объект.Зона = ЗначениеЗоны1 Тогда
// ...
КонецЕсли;
Лев(): Если реквизит Объект.Зона или Объект.КатегорияДоставки является строкой или вы хотите сравнить часть наименования, как в примерах из форума, то важно правильно указывать длину подстроки. Ошибка Лев(Объект.Зона,2)="Внутригородская доставка" очевидна, так как функция Лев() вернет только первые два символа, которые никак не могут быть равны полной строке. Правильно будет указать длину, равную или большую длины сравниваемой строки: Лев(Объект.Зона.Наименование, 25) = "Внутригородская доставка".При работе на управляемых формах необходимо учитывать контекст выполнения кода: &НаКлиенте или &НаСервере. Если вам нужно получить данные из базы (например, из справочников или регистров) или выполнить сложные вычисления, требующие доступа к серверной части, используйте серверные вызовы. В нашем примере, если Зона и КатегорияДоставки — это ссылки, и их значения сравниваются через наименования, доступные на клиенте, то код может быть на клиенте. Однако, если требуется получить ссылку через НайтиПоНаименованию, это уже серверная операция.
Рекомендуется вынести логику получения данных и сложный расчет на сервер, используя процедуры с директивой &НаСервере или &НаСервереБезКонтекста.
// Модуль формы документа
&НаКлиенте
Процедура ЗонаПриИзменении(Элемент)
ОбновитьДатуДоставкиНаСервере();
КонецПроцедуры
&НаКлиенте
Процедура КатегорияДоставкиПриИзменении(Элемент)
ОбновитьДатуДоставкиНаСервере();
КонецПроцедуры
&НаСервере
Процедура ОбновитьДатуДоставкиНаСервере()
Если Объект.Зона.Пустая() ИЛИ Объект.КатегорияДоставки.Пустая() Тогда
Объект.ДатаДоставкиОриентир = Неопределено;
Возврат;
КонецЕсли;
// Пример сравнения по ссылке на серверной стороне
ЗначениеЗоны1 = Справочники.ЗоныДоставки.НайтиПоКоду("000000001");
ЗначениеЗоны2 = Справочники.ЗоныДоставки.НайтиПоКоду("000000002");
КатегорияСтандарт = Справочники.КатегорииДоставки.НайтиПоНаименованию("Стандарт");
КатегорияЭкспресс = Справочники.КатегорииДоставки.НайтиПоНаименованию("Экспресс");
ВнутригородскаяДоставка = Справочники.ЗоныДоставки.НайтиПоНаименованию("Внутригородская доставка");
Если Объект.Зона = ЗначениеЗоны1 И Объект.КатегорияДоставки = КатегорияСтандарт Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата() + 86400; // Текущая дата + 1 день
ИначеЕсли Объект.Зона = ЗначениеЗоны2 И Объект.КатегорияДоставки = КатегорияСтандарт Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата() + 86400 * 2; // Текущая дата + 2 дня
ИначеЕсли Объект.Зона = ВнутригородскаяДоставка И Объект.КатегорияДоставки = КатегорияЭкспресс Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата(); // Текущая дата
Иначе
Объект.ДатаДоставкиОриентир = ТекущаяДата(); // По умолчанию
КонецЕсли;
КонецПроцедуры
Простое прибавление дней к текущей дате не учитывает выходные и праздничные дни, что может привести к некорректным срокам доставки. Например, если доставка занимает 1 день, и он выпадает на субботу, а выгрузка должна произойти в рабочий день, то реальная дата доставки будет понедельник. Для корректного учета нам понадобится использовать производственный календарь.
ДеньНедели()Функция ДеньНедели(Дата) позволяет определить день недели (1 - понедельник, 7 - воскресенье). Проверка ДеньНедели(Дата) = 6 или = 7 поможет исключить стандартные выходные (суббота, воскресенье). Однако этот подход не учитывает государственные праздники, которые могут выпадать на будние дни, а также переносы рабочих и выходных дней, характерные для производственного календаря. Поэтому такой метод является лишь частичным решением.
Пример кода с ДеньНедели() (частичное решение):
&НаКлиенте
Процедура ПередЗаписью(Отказ, ПараметрыЗаписи)
Если Лев(Объект.Зона,25)="Внутригородская доставка" И Лев(Объект.КатегорияДоставки,8)="Экспресс" Тогда
Объект.ДатаДоставкиОриентир = ТекущаяДата()+86400*2; // Изначально прибавляем 2 дня
Пока ДеньНедели(Объект.ДатаДоставкиОриентир) >= 6 Цикл // Пока это суббота или воскресенье
Объект.ДатаДоставкиОриентир = Объект.ДатаДоставкиОриентир - 86400; // Отнимаем один день (движемся назад)
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Как видим, этот код пытается "сдвинуть" дату, если она попала на выходной, но движется назад, что может быть неверно для "даты доставки". Правильнее было бы двигаться вперед до ближайшего рабочего дня.
РегламентированныйПроизводственныйКалендарьНаиболее точное решение для учета нерабочих дней — это использование регистра сведений РегламентированныйПроизводственныйКалендарь. Этот регистр содержит информацию о видах дней (рабочий, выходной, предпраздничный, праздничный) для каждого дня года. Для его корректной работы необходимо, чтобы календарь был заполнен в вашей конфигурации. Во многих типовых конфигурациях 1С он заполняется автоматически или импортируется.
Разберем алгоритм получения следующего рабочего дня и примеры функций из форума, адаптированные для общего модуля.
// В Общем модуле, например "УправлениеДатойДоставки"
// Свойство: Сервер, ВызовСервера
Функция ПолучитьВидДня(Дата) Экспорт
ПеречисленияВидыДней = ОбщиеФункции.ЗначениеПеречисления("ВидыДнейПроизводственногоКалендаря"); // Если есть такой справочник
// Если перечисление прямо доступно, то можно так:
// Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РегламентированныйПроизводственныйКалендарь.ВидДня
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
|ГДЕ
| РегламентированныйПроизводственныйКалендарь.ДатаКалендаря = &ДатаКалендаря";
Запрос.УстановитьПараметр("ДатаКалендаря", НачалоДня(Дата));
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.ВидДня;
Иначе
// Если для даты нет записи, возможно, календарь не заполнен или это далекое будущее/прошлое.
// Можно считать по умолчанию рабочим днем или вывести предупреждение.
// Для простоты, если нет записи, считаем рабочим.
Возврат Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий;
КонецЕсли;
КонецФункции // ПолучитьВидДня
// В Общем модуле, например "УправлениеДатойДоставки"
// Свойство: Сервер, ВызовСервера
Функция РегламентированныйПроизводственныйКалендарьЗаполнен(Дата) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК КоличествоДней
|ИЗ
| РегистрСведений.РегламентированныйПроизводственныйКалендарь КАК РегламентированныйПроизводственныйКалендарь
|ГДЕ
| РегламентированныйПроизводственныйКалендарь.Год = &Год";
Запрос.УстановитьПараметр("Год", Год(Дата));
Выборка = Запрос.Выполнить().Выбрать();
Выборка.Следующий();
Если Выборка.КоличествоДней > 0 Тогда
Возврат Истина;
Иначе
#Если Клиент Тогда
Сообщить("Производственный календарь не заполнен на " + Год(Дата) + " год! Обратитесь к администратору базы данных!", СтатусСообщения.ОченьВажное);
#КонецЕсли
Возврат Ложь;
КонецЕсли;
КонецФункции
// В Общем модуле, например "УправлениеДатойДоставки"
// Свойство: Сервер, ВызовСервера
Функция ПолучитьРабочуюДатуСУчетомДней(НачальнаяДата, КоличествоДней) Экспорт
ТекущаяРабочаяДата = НачалоДня(НачальнаяДата);
СчетчикРабочихДней = 0;
ДлительностьСуток = (60*60*24);
Если НЕ РегламентированныйПроизводственныйКалендарьЗаполнен(ТекущаяРабочаяДата) Тогда
// Если календарь не заполнен, можно выдать ошибку или вернуться к простому расчету
Возврат НачальнаяДата + ДлительностьСуток * КоличествоДней;
КонецЕсли;
Пока СчетчикРабочихДней < КоличествоДней Цикл
ТекущаяРабочаяДата = ТекущаяРабочаяДата + ДлительностьСуток; // Переходим к следующему дню
ВидДня = ПолучитьВидДня(ТекущаяРабочаяДата);
Если ВидДня = Перечисления.ВидыДнейПроизводственногоКалендаря.Рабочий ИЛИ
ВидДня = Перечисления.ВидыДнейПроизводственногоКалендаря.Предпраздничный Тогда
СчетчикРабочихДней = СчетчикРабочихДней + 1;
КонецЕсли;
КонецЦикла;
Возврат ТекущаяРабочаяДата;
КонецФункции // ПолучитьРабочуюДатуСУчетомДней
Теперь в нашем обработчике на форме мы можем вызвать эту функцию, чтобы получить корректную дату доставки:
// В Общем модуле "УправлениеДатойДоставки"
// Свойство: ВызовСервера
&НаСервере
Процедура ОбновитьДатуДоставкиНаСервере()
// ... (Код определения значений Зоны и Категории, как в предыдущем примере)
КоличествоДнейДоставки = 0;
Если Объект.Зона = ЗначениеЗоны1 И Объект.КатегорияДоставки = КатегорияСтандарт Тогда
КоличествоДнейДоставки = 1;
ИначеЕсли Объект.Зона = ЗначениеЗоны2 И Объект.КатегорияДоставки = КатегорияСтандарт Тогда
КоличествоДнейДоставки = 2;
ИначеЕсли Объект.Зона = ВнутригородскаяДоставка И Объект.КатегорияДоставки = КатегорияЭкспресс Тогда
КоличествоДнейДоставки = 0; // Доставка в тот же день
Иначе
КоличествоДнейДоставки = 0; // По умолчанию
КонецЕсли;
Объект.ДатаДоставкиОриентир = УправлениеДатойДоставки.ПолучитьРабочуюДатуСУчетомДней(ТекущаяДата(), КоличествоДнейДоставки);
КонецПроцедуры
Такой подход обеспечивает высокую точность расчетов, учитывая все официальные выходные и праздничные дни, а также их переносы.
Жесткое прописывание правил Если...ИначеЕсли... непосредственно в коде имеет существенный недостаток: любое изменение правил (например, добавление новой зоны, изменение срока доставки) требует вмешательства программиста и модификации конфигурации. Это негибко и затратно. Гораздо более предпочтительным решением является вынесение этих правил в регистр сведений.
Создадим регистр сведений с типом хранения "Независимый", "Периодичность - Непериодический".
ЗонаДоставки (Тип: СправочникСсылка.ЗоныДоставки)КатегорияДоставки (Тип: СправочникСсылка.КатегорииДоставки)СпособДоставки (Тип: СправочникСсылка.СпособыДоставки)Приоритет (Тип: Число) — для случаев, когда для одной комбинации Зона+Категория может быть несколько правил.КоличествоДнейДоставки (Тип: Число)УчитыватьВыходные (Тип: Булево) — флаг, указывающий, нужно ли для данного правила учитывать производственный календарь.Создадим функцию в общем модуле, которая будет искать соответствующее правило в регистре и возвращать количество дней доставки. Эта функция должна быть серверной.
// В Общем модуле, например "УправлениеДатойДоставки"
// Свойство: Сервер, ВызовСервера
Функция ПолучитьПравилоДоставки(Зона, Категория) Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| СрокиДоставки.КоличествоДнейДоставки,
| СрокиДоставки.УчитыватьВыходные
|ИЗ
| РегистрСведений.СрокиДоставки КАК СрокиДоставки
|ГДЕ
| СрокиДоставки.ЗонаДоставки = &Зона
| И СрокиДоставки.КатегорияДоставки = &Категория";
Запрос.УстановитьПараметр("Зона", Зона);
Запрос.УстановитьПараметр("Категория", Категория);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка; // Возвращаем структуру с КоличествоДнейДоставки и УчитыватьВыходные
Иначе
// Если правило не найдено, можно вернуть значение по умолчанию или Неопределено
Возврат Неопределено;
КонецЕсли;
КонецФункции
Теперь в обработчике формы мы вызываем эту функцию и используем полученные данные:
// В Модуле формы документа
&НаСервере
Процедура ОбновитьДатуДоставкиНаСервере()
Если Объект.Зона.Пустая() ИЛИ Объект.КатегорияДоставки.Пустая() Тогда
Объект.ДатаДоставкиОриентир = Неопределено;
Возврат;
КонецЕсли;
Правило = УправлениеДатойДоставки.ПолучитьПравилоДоставки(Объект.Зона, Объект.КатегорияДоставки);
Если Правило <> Неопределено Тогда
КоличествоДнейДоставки = Правило.КоличествоДнейДоставки;
Если Правило.УчитыватьВыходные Тогда
Объект.ДатаДоставкиОриентир = УправлениеДатойДоставки.ПолучитьРабочуюДатуСУчетомДней(ТекущаяДата(), КоличествоДнейДоставки);
Иначе
Объект.ДатаДоставкиОриентир = ТекущаяДата() + (60*60*24 * КоличествоДнейДоставки);
КонецЕсли;
Иначе
// Если правило не найдено, устанавливаем дату по умолчанию или сообщаем пользователю
Сообщить("Для выбранной зоны и категории доставки правило не найдено. Дата доставки установлена по умолчанию.", СтатусСообщения.Важное);
Объект.ДатаДоставкиОриентир = ТекущаяДата();
КонецЕсли;
КонецПроцедуры
Реализация автоматического расчета даты доставки в 1С — это комплексная задача, требующая внимательного подхода. Мы рассмотрели различные аспекты: от базовой условной логики до продвинутых механизмов учета нерабочих дней и гибкого управления правилами через регистры сведений.
Основные рекомендации:
РегламентированныйПроизводственныйКалендарь.Следуя этим рекомендациям, вы сможете создать надежный и гибкий механизм расчета даты доставки, который будет эффективно работать в вашей системе 1С.