Как запретить запись и проведение документа в 1С с помощью расширения?

Программист 1С v8.3 (Управляемые формы)
← На главную

При разработке функционала в 1С часто возникает необходимость установить дополнительные проверки и запретить запись или проведение документа при определенных условиях. Эта задача особенно актуальна при работе с расширениями, когда требуется добавить специфическую логику, не затрагивая типовую конфигурацию. Рассмотрим подробнее, как корректно реализовать такой запрет, избегая распространенных ошибок.

Понимание события «ПередЗаписью» и директив расширения

Для запрета записи документа мы будем работать с событием ПередЗаписью, которое вызывается непосредственно перед тем, как объект документа будет сохранен в базу данных. Это идеальный момент для проведения всех необходимых проверок.

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

  1. &Перед: Эта директива указывает, что наш код должен быть выполнен перед вызовом оригинальной процедуры или функции из типовой конфигурации. Для нашей задачи, когда мы хотим установить Отказ = Истина (чтобы отменить запись), директива &Перед является наиболее подходящей. Она позволяет нам прервать выполнение стандартной логики записи до ее начала.
  2. &После: Директива &После означает, что наш код будет выполнен после того, как отработает оригинальная процедура. Если мы попытаемся установить Отказ = Истина в обработчике с директивой &После, это часто бывает неэффективно, потому что стандартная процедура записи уже могла завершиться или уже приняла решение о возможности записи. Нам нужно отменить запись до ее совершения, а не после. Именно использование &После вместо &Перед часто приводит к тому, что документ записывается, несмотря на наши условия.
  3. &Вместо: Эта директива позволяет полностью заменить типовую процедуру нашим кодом. Используется, когда нам нужна абсолютная гибкость и мы готовы взять на себя всю ответственность за логику записи.
  4. &СКонтролем: Выполняет типовую процедуру, а затем наш код, но предоставляет механизмы контроля.

Вывод: Для запрета записи документа путем установки параметра Отказ всегда используем директиву &Перед.

Корректная сигнатура процедуры «ПередЗаписью» в расширении

Одной из наиболее частых причин ошибки "несоответствие типа возвращаемого параметра" при работе с расширениями является неверное определение параметров в обработчике события ПередЗаписью. Стандартная процедура ПередЗаписью в модуле объекта документа имеет следующую сигнатуру:


Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
    // Ваш код
КонецПроцедуры

Давайте разберем каждый параметр:

  1. Отказ (Булево): Это основной параметр, который мы будем изменять. Если мы установим Отказ = Истина, запись документа будет отменена. Этот параметр передается по ссылке, что позволяет нашему обработчику изменять его значение.
  2. РежимЗаписи (РежимЗаписиДокумента): Этот параметр указывает, в каком режиме происходит запись документа (например, Запись или Проведение). Он передается по значению, то есть мы можем его читать, но не изменять.
  3. РежимПроведения (РежимПроведенияДокумента): Этот параметр уточняет режим проведения (например, Оперативный или Неоперативный). Также передается по значению.

Распространенная ошибка: Некоторые разработчики по ошибке пытаются передать в процедуру сам объект документа как первый параметр (например, Процедура РасшЗапрет_ПередЗаписью(Объект, Отказ, РежимЗаписи)). Это происходит из-за путаницы с другими обработчиками, такими как ОбработкаПроведения, где объект документа действительно передается. Однако для ПередЗаписью в модуле объекта документа сам документ доступен через неявную переменную ЭтотОбъект.

Когда система пытается вызвать наш обработчик &Перед("ПередЗаписью") с некорректной сигнатурой, она сталкивается с тем, что типы переданных параметров не совпадают с ожидаемыми. Например, если мы объявим процедуру так: Процедура РасшЗапрет_ПередЗаписью(Объект, Отказ, РежимЗаписи), то при вызове система попытается передать ей оригинальные параметры: Отказ (булево) в Объект, РежимЗаписи (перечисление) в Отказ, и РежимПроведения (перечисление) в РежимЗаписи. Очевидно, что Булево не соответствует типу Объект, а РежимЗаписиДокумента не соответствует типу Булево, что и приводит к ошибке "несоответствие типа возвращаемого параметра" или "несоответствие типов параметров".

Важно: Всегда используйте точную сигнатуру (Отказ, РежимЗаписи, РежимПроведения) для обработчика ПередЗаписью в модуле объекта.

Пример правильного обработчика в модуле объекта расширения

Предположим, у нас есть документ "ЗаказКлиента" и мы хотим запретить его запись при определенных условиях. Код в модуле объекта расширения будет выглядеть так:


// В модуле объекта документа "ЗаказКлиента" расширения
&Перед("ПередЗаписью")
Процедура РасшЗапрет_ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)

    // Вызываем процедуру в общем модуле для основной логики проверки
    // Передаем текущий объект документа и параметр Отказ
    Расш_ПроверкиДокументов.ПроверитьЗаказПередЗаписью(ЭтотОбъект, Отказ);

    // Если Отказ установлен в Истина внутри ПроверитьЗаказПередЗаписью,
    // то запись документа будет прервана.
    // Сообщение пользователю также должно быть выведено внутри ПроверитьЗаказПередЗаписью.

КонецПроцедуры

Обратите внимание, что мы используем ЭтотОбъект для доступа к текущему документу. Параметр Отказ передается по ссылке в нашу вспомогательную процедуру в общем модуле, что позволяет ей влиять на процесс записи.

Логика валидации: проверка данных нового и существующего документа

Когда мы запрещаем запись, нам необходимо проверить данные документа. Эти данные могут быть как для нового документа (который еще не записан в базу), так и для существующего (в который внесли изменения). Наша логика должна корректно обрабатывать оба сценария.

Доступ к данным документа

Внутри процедуры ПередЗаписью (или в процедуре общего модуля, куда мы передаем ЭтотОбъект) текущий документ доступен как объект. Мы можем напрямую обращаться к его реквизитам и табличным частям:


// Пример доступа к реквизитам и табличным частям
Если ЭтотОбъект.Организация.ИНН = "1234567890" Тогда
    Для Каждого СтрокаТабЧасти Из ЭтотОбъект.Товары Цикл
        Если СтрокаТабЧасти.Номенклатура.ВидНоменклатуры.Наименование = "Продукция Ветис" Тогда
            // Условие выполнено
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

Различие между новым и существующим документом

Для определения, является ли документ новым или уже существующим в базе, мы можем использовать свойство ЭтоНовый объекта документа. Свойство ЭтоНовый будет Истина для документа, который еще ни разу не был записан в базу данных и не имеет постоянной ссылки.


Если ЭтотОбъект.ЭтоНовый Тогда
    // Документ новый
Иначе
    // Документ уже существует в базе
КонецЕсли;

Аналогичную проверку можно сделать с помощью ЗначениеЗаполнено(Ссылка). Если ЗначениеЗаполнено(Ссылка) возвращает Ложь, это означает, что у документа нет сформированной ссылки, то есть он новый.


Если НЕ ЗначениеЗаполнено(ЭтотОбъект.Ссылка) Тогда
    // Документ новый
Иначе
    // Документ уже существует в базе
КонецЕсли;

Внимание: Если ваше условие запрета записи применимо как к новым, так и к уже существующим документам, то условие НЕ ЗначениеЗаполнено(Ссылка) в проверке запрета может быть излишним, как это было в сообщениях форума. Убедитесь, что логика охватывает все необходимые сценарии.

Использование запросов для валидации текущих (несохраненных) данных

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

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


// В общем модуле, процедура ПроверитьЗаказПередЗаписью
Процедура ПроверитьЗаказПередЗаписью(ДокументОбъект, Отказ) Экспорт

    // Создаем запрос
    Запрос = Новый Запрос;

    // Определяем временную таблицу "втТовары" и заполняем ее данными из табличной части "Товары" документа
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    тзТовары.Номенклатура КАК Номенклатура,
    |    тзТовары.Количество КАК Количество,
    |    тзТовары.Сумма КАК Сумма
    |ПОМЕСТИТЬ втТовары
    |ИЗ
    |    &тзТовары КАК тзТовары
    |;
    |
    ////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    втТовары.Номенклатура.Наименование КАК НаименованиеНоменклатуры,
    |    втТовары.Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры,
    |    СУММА(втТовары.Количество) КАК ОбщееКоличество
    |ИЗ
    |    втТовары КАК втТовары
    |ГДЕ
    |    втТовары.Номенклатура.ВидНоменклатуры.Наименование = &НаименованиеВидаНоменклатурыЗапрещенной
    |
    |СГРУППИРОВАТЬ ПО
    |    втТовары.Номенклатура.Наименование,
    |    втТовары.Номенклатура.ВидНоменклатуры
    |
    |ИМЕЮЩИЕ
    |    СУММА(втТовары.Количество) > 10"; // Например, запрет, если общее количество такого товара > 10

    // Передаем табличную часть "Товары" документа в параметр запроса
    Запрос.УстановитьПараметр("тзТовары", ДокументОбъект.Товары.Выгрузить());
    Запрос.УстановитьПараметр("НаименованиеВидаНоменклатурыЗапрещенной", "Продукция Ветис");

    РезультатЗапроса = Запрос.Выполнить();

    Если НЕ РезультатЗапроса.Пустой() Тогда
        Отказ = Истина; // Устанавливаем отказ записи
        ВызватьИсключение НСтр("ru = 'Нельзя записать документ, так как общее количество запрещенной продукции превышает лимит!'");
    КонецЕсли;

    // Дополнительные проверки, не требующие запроса
    Если ДокументОбъект.СуммаДокумента < 1000 И ДокументОбъект.Организация.ИНН = "9876543210" Тогда
        Отказ = Истина;
        ВызватьИсключение НСтр("ru = 'Для данной организации сумма документа не может быть меньше 1000!'");
    КонецЕсли;

КонецПроцедуры

В этом примере мы делаем следующее:

  1. Создаем временную таблицу втТовары и наполняем ее данными из табличной части Товары текущего документа, используя метод Выгрузить(). Это позволяет запросу работать с актуальными, еще не записанными в базу данными.
  2. Во втором блоке запроса мы уже работаем с этой временной таблицей, выполняя необходимые группировки и условия (например, если общая сумма или количество определенных товаров превышает лимит).
  3. Если запрос возвращает какие-либо данные (т.е. РезультатЗапроса.Пустой() является Ложь), это означает, что условие запрета выполнено.

Остановка операции записи: параметр Отказ и ВызватьИсключение

Для предотвращения записи документа у нас есть два основных механизма:

  1. Установка Отказ = Истина;:

    Это основной и рекомендуемый способ. Когда в процедуре ПередЗаписью мы устанавливаем Отказ в Истина, система понимает, что запись должна быть прервана. При этом пользователю обычно выводится информационное сообщение, которое мы можем добавить с помощью Сообщить("Текст сообщения"); или, что чаще, вместе с ВызватьИсключение.

    Преимущество: Это мягкий способ отмены, который позволяет системе корректно завершить процесс, не вызывая немедленного прерывания выполнения всего кода, и обеспечивает более контролируемое поведение. Другие обработчики ПередЗаписью (если они есть) также увидят установленный Отказ.

  2. Использование ВызватьИсключение "Текст сообщения";:

    Оператор ВызватьИсключение немедленно прерывает выполнение текущей процедуры и выводит сообщение пользователю. Это более "жесткий" способ остановить запись.

    Преимущество: Гарантирует немедленное прекращение выполнения и отображение сообщения. В некоторых случаях это может быть предпочтительнее для того, чтобы пользователь сразу увидел критическое сообщение и не продолжал работу, не исправив ошибку.

    Рекомендация: Часто эти два механизма используются в связке. Мы устанавливаем Отказ = Истина; и сразу же после этого вызываем ВызватьИсключение "Сообщение";. Это обеспечивает и корректную отмену записи, и немедленное информирование пользователя.

Пример общего модуля с логикой проверки

Для лучшей структуризации кода и возможности переиспользования рекомендуется выносить сложную логику проверки в отдельные процедуры общих модулей. Эти процедуры можно вызывать из обработчика события в модуле объекта расширения.


// В новом или существующем общем модуле расширения, например, "Расш_ПроверкиДокументов"
// Убедитесь, что модуль имеет свойство "Сервер" и "Вызов сервера"
&НаСервере
Процедура ПроверитьЗаказПередЗаписью(ДокументОбъект, Отказ) Экспорт

    // Проверка 1: Запрет на запись, если количество определенной номенклатуры > 0
    // и определенная организация
    ЗапретУстановлен = Ложь;

    // Пример условия: если в табличной части "Товары" есть номенклатура с видом "Продукция Ветис"
    // и документ оформлен для организации с ИНН "1234567890"
    Если ДокументОбъект.Организация.ИНН = "1234567890" Тогда
        Для Каждого СтрокаТовара Из ДокументОбъект.Товары Цикл
            // Проверяем, существует ли ссылка (для избежания ошибки при работе с незаписанной номенклатурой)
            Если ЗначениеЗаполнено(СтрокаТовара.Номенклатура) И СтрокаТовара.Номенклатура.ВидНоменклатуры.Наименование = "Продукция Ветис" Тогда
                ЗапретУстановлен = Истина;
                Прервать; // Выходим из цикла, условие выполнено
            КонецЕсли;
        КонецЦикла;
    КонецЕсли;

    Если ЗапретУстановлен Тогда
        Отказ = Истина; // Устанавливаем отказ
        ВызватьИсключение НСтр("ru = 'Нельзя записать документ: запрещено использование продукции Ветис для данной организации!'");
    КонецЕсли;

    // Проверка 2: Запрет, если сумма документа меньше 500 для всех новых документов
    Если ДокументОбъект.ЭтоНовый И ДокументОбъект.СуммаДокумента < 500 Тогда
        Отказ = Истина;
        ВызватьИсключение НСтр("ru = 'Сумма нового документа не может быть меньше 500!'");
    КонецЕсли;

    // Проверка 3: Более сложная проверка с запросом, как обсуждалось ранее
    Запрос = Новый Запрос;
    Запрос.Текст = 
    "ВЫБРАТЬ
    |    тзТовары.Номенклатура КАК Номенклатура,
    |    тзТовары.Количество КАК Количество
    |ПОМЕСТИТЬ втНоменклатураСКоличеством
    |ИЗ
    |    &ТЗТовары КАК тзТовары
    |;
    |
    ////////////////////////////////////////////////////////////////////////////////
    |ВЫБРАТЬ
    |    втНоменклатураСКоличеством.Номенклатура.ВидНоменклатуры КАК ВидНоменклатуры,
    |    СУММА(втНоменклатураСКоличеством.Количество) КАК ОбщееКоличество
    |ИЗ
    |    втНоменклатураСКоличеством КАК втНоменклатураСКоличеством
    |ГДЕ
    |    втНоменклатураСКоличеством.Номенклатура.ВидНоменклатуры.Наименование = &ЗапрещенныйВидНоменклатуры
    |
    |СГРУППИРОВАТЬ ПО
    |    втНоменклатураСКоличеством.Номенклатура.ВидНоменклатуры
    |
    |ИМЕЮЩИЕ
    |    СУММА(втНоменклатураСКоличеством.Количество) > 100"; // Если общее количество такого вида > 100

    Запрос.УстановитьПараметр("ТЗТовары", ДокументОбъект.Товары.Выгрузить());
    Запрос.УстановитьПараметр("ЗапрещенныйВидНоменклатуры", "Опасные товары");

    РезультатЗапроса = Запрос.Выполнить();

    Если НЕ РезультатЗапроса.Пустой() Тогда
        Отказ = Истина;
        ВызватьИсключение НСтр("ru = 'Нельзя записать документ: общее количество опасных товаров превышает допустимый лимит!'");
    КонецЕсли;

КонецПроцедуры

В этом примере:

  1. Процедура ПроверитьЗаказПередЗаписью объявлена с директивой &НаСервере, так как она будет работать с серверными объектами (запросы, объекты документов).
  2. Она принимает ДокументОбъект (ссылка на текущий документ) и Отказ (параметр, который будет изменен).
  3. Внутри процедуры выполняется несколько проверок, использующих как прямой доступ к реквизитам объекта, так и запросы с временными таблицами.
  4. При обнаружении запрещающего условия устанавливается Отказ = Истина; и выводится сообщение с помощью ВызватьИсключение.

Шаги по внедрению решения

  1. Создайте расширение: Откройте конфигуратор, перейдите в меню "Конфигурация" -> "Расширения конфигурации" -> "Добавить".
  2. Добавьте изменяемый объект: В расширении найдите нужный документ (например, "ЗаказКлиента"), откройте его свойства и убедитесь, что он добавлен в список изменяемых объектов расширения (через правый клик -> "Добавить в расширение").
  3. Перейдите в модуль объекта документа: В расширении откройте модуль объекта вашего документа (например, "ЗаказКлиента" -> "Модуль объекта").
  4. Добавьте обработчик: Вставьте код обработчика &Перед("ПередЗаписью"), который будет вызывать вашу процедуру из общего модуля, как показано выше.
  5. Создайте (или используйте существующий) общий модуль: В расширении создайте новый общий модуль (например, "Расш_ПроверкиДокументов") или добавьте существующий общий модуль в расширение (если он уже содержит вспомогательные функции). Установите свойства модуля Сервер и Вызов сервера в Истина.
  6. Реализуйте логику проверки: В общем модуле напишите процедуру, содержащую всю необходимую логику валидации, как показано в примерах.
  7. Протестируйте: Запустите 1С в режиме предприятия и тщательно протестируйте сценарии, при которых документ должен быть запрещен к записи, а также сценарии, когда запись разрешена.

Важность отладки

Если вы сталкиваетесь с неожиданным поведением или ошибками, используйте отладчик 1С. Установите точки останова в вашем обработчике ПередЗаписью и в процедуре общего модуля. Пошагово пройдитесь по коду, проверяя значения параметров (Отказ, РежимЗаписи, РежимПроведения), значения реквизитов документа (ЭтотОбъект), результаты запросов и условия. Это позволит точно определить, на каком этапе происходит ошибка или почему условие запрета не срабатывает.

Запомните:

  1. Используйте &Перед для установки Отказ = Истина.
  2. Соблюдайте точную сигнатуру (Отказ, РежимЗаписи, РежимПроведения) для процедуры ПередЗаписью.
  3. Работайте с актуальными данными документа, используя ЭтотОбъект или временные таблицы в запросах.
  4. Используйте Отказ = Истина; в сочетании с ВызватьИсключение для надежного запрета и информирования пользователя.

Следуя этим рекомендациям, вы сможете эффективно и надежно реализовать запрет записи и проведения документов с помощью расширений в 1С.

← На главную