Как избежать ошибки "Данные были изменены или удалены другим пользователем" при программном изменении табличной части документа в 1С:ЗУП?

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

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

Анализируем исходную проблему

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

Исходный код, представленный автором, выглядел следующим образом:


Для Каждого СтрокаТЧ ИЗ Объект.Состав Цикл
    Если МассивИностранцев.Найти(Объект.Состав[ИндексСтроки].ФизическоеЛицо) <> Неопределено Тогда
        Объект.Состав.Удалить(СтрокаТЧ);
        ВсегоСтрок = ВсегоСтрок - 1;
    Иначе
        ИндексСтроки = ИндексСтроки + 1;
    КонецЕсли;
КонецЦикла;
ДокументОбъект.Записать();
ДокументОбъект.Прочитать();

После выполнения этого кода на сервере и возврата на клиентскую часть, при попытке пользователя записать документ на форме (например, нажав кнопку "Записать" или "Провести"), возникала ошибка: "Данные были изменены или удалены другим пользователем. Нужно перечитать форму программно.".

Выясняем причину возникновения ошибки

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

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

Вызов ДокументОбъект.Прочитать() после ДокументОбъект.Записать() на сервере обновляет сам серверный объект, но не обновляет данные в клиентской форме. Форма по-прежнему работает со своим кешем данных, который не был обновлен из базы. Дополнительно об этой проблеме можно почитать в статье о программном заполнении табличной части документа в открытой форме.

Решение 1: Корректное удаление строк из табличной части

Для автоматизации подобных задач часто используется специальный инструмент для гибкой настройки автоматического заполнения реквизитов и табличных частей — есть универсальный конструктор заполнения реквизитов и табличных частей.

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

Сначала рассмотрим предложенную логику удаления: "Добавил реквизит 'Иностранцы' тип Булево, если он = Истина Тогда удаляем всех кроме иностранцев, если нет - удаляем Только Иностранцев. Список иностранцев берется из созданного регистра сведений."

Предположим, МассивИностранцев содержит список ФизическихЛиц, которые являются иностранцами.

Скорректированная логика удаления строк внутри цикла с учетом описания автора и правила обхода с конца:


// Предположим, что Объект.Иностранцы - это реквизит типа Булево в документе.
// МассивИностранцев - это массив ФизическихЛиц, полученный из регистра сведений.

Для ИндексСтроки = Объект.Состав.Количество() - 1 По 0 Шаг -1 Цикл
    СтрокаТЧ = Объект.Состав[ИндексСтроки];
            
    // Проверяем, является ли текущее ФизическоеЛицо иностранцем
    ЕстьВМассивеИностранцев = (МассивИностранцев.Найти(СтрокаТЧ.ФизическоеЛицо) <> Неопределено);

    // Если реквизит "Иностранцы" = Истина: удаляем всех КРОМЕ иностранцев (т.е. удаляем не-иностранцев)
    Если Объект.Иностранцы И НЕ ЕстьВМассивеИностранцев Тогда
        Объект.Состав.Удалить(СтрокаТЧ);
    // Если реквизит "Иностранцы" = Ложь: удаляем ТОЛЬКО иностранцев
    ИначеЕсли НЕ Объект.Иностранцы И ЕстьВМассивеИностранцев Тогда
        Объект.Состав.Удалить(СтрокаТЧ);
    КонецЕсли;
КонецЦикла;

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

Решение 2: Запись формы на клиенте после серверных изменений

Основная причина ошибки, как мы выяснили, заключается в том, что после серверной записи объекта в базу данных (ДокументОбъект.Записать()), клиентская форма не обновляется и пытается сохранить свою устаревшую версию. Чтобы синхронизировать форму с изменениями, необходимо после внесения всех серверных модификаций записать форму на клиенте.

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

Предположим, ваша клиентская процедура, вызываемая по кнопке "Заполнить", и соответствующая серверная функция выглядят так:


&НаКлиенте
Процедура ЗаполнитьИОчиститьТабличнуюЧасть()

    // Вызываем серверную процедуру, которая выполнит заполнение и удаление строк.
    // Важно: эта серверная процедура также выполняет Объект.Записать() в базу данных.
    ВыполнитьСервернуюОбработкуЗаполненияИУдаления();

    // После возврата с сервера, данные в форме могут быть устаревшими
    // относительно того, что было записано в базу данных.
    // Чтобы избежать ошибки "Данные были изменены...", мы должны
    // программно записать форму на клиенте. Этот метод обновит данные
    // формы из Объекта, а затем запишет его, выполняя все проверки
    // и обработчики, связанные с формой.
    ЭтаФорма.Записать(); 
    // Если код находится в модуле формы, можно использовать просто Записать();
    // Записать(); 
КонецПроцедуры

&НаСервере
Процедура ВыполнитьСервернуюОбработкуЗаполненияИУдаления()
    // Здесь должен быть ваш код по заполнению табличной части
    // Например:
    // Объект.Состав.Очистить();
    // ... логика заполнения табличной части "Состав" ...

    // Получаем массив иностранцев из регистра сведений
    МассивИностранцев = ПолучитьМассивИностранцевИзРегистраСведений(); // Ваша функция

    // Корректное удаление строк согласно логике
    Для ИндексСтроки = Объект.Состав.Количество() - 1 По 0 Шаг -1 Цикл
        СтрокаТЧ = Объект.Состав[ИндексСтроки];
        ЕстьВМассивеИностранцев = (МассивИностранцев.Найти(СтрокаТЧ.ФизическоеЛицо) <> Неопределено);

        Если Объект.Иностранцы И НЕ ЕстьВМассивеИностранцев Тогда
            Объект.Состав.Удалить(СтрокаТЧ);
        ИначеЕсли НЕ Объект.Иностранцы И ЕстьВМассивеИностранцев Тогда
            Объект.Состав.Удалить(СтрокаТЧ);
        КонецЕсли;
    КонецЦикла;

    // Теперь мы записываем изменения в базу данных напрямую.
    // Это создает новую версию объекта в БД.
    Объект.Записать();
    // Метод Прочитать() здесь нужен, чтобы обновить сам Объект на сервере
    // после записи, если мы планируем продолжить с ним работать на сервере.
    // На состояние формы он не влияет напрямую, ее нужно обновлять на клиенте
    // с помощью ЭтаФорма.Записать() или Прочитать(), если записи не было на сервере.
    Объект.Прочитать(); // Обновляем сам серверный объект, но не форму!
КонецПроцедуры

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

Итак, ключевым шагом является вызов ЭтаФорма.Записать() (или просто Записать(), если вы находитесь в модуле формы) на клиенте после того, как серверная процедура изменила и записала объект в базу данных. Это позволяет форме корректно обновить свои данные, учесть все изменения и записать документ, выполнив все необходимые обработчики и проверки.

Отличие Объект.Записать() от Записать()/ЭтаФорма.Записать()

Важно понимать разницу между этими методами, чтобы избежать подобных ошибок в будущем:

  1. Объект.Записать():
    • Этот метод вызывается на сервере.
    • Он предназначен для прямой записи программно созданного или измененного объекта (например, ДокументОбъект, СправочникОбъект) в базу данных.
    • При его выполнении не вызываются обработчики событий формы, такие как ПередЗаписьюНаСервере или ПриЗаписиНаСервере модуля формы. Он работает непосредственно с объектом данных, а не с его представлением на форме.
    • Используется, когда необходимо записать объект без контекста формы (например, в фоновом задании, при пакетной обработке, или когда объект был создан полностью программно).
  2. Записать() (в модуле формы) / ЭтаФорма.Записать() (в общем модуле, если передан контекст формы):
    • Эти методы используются на клиенте (или на сервере в контексте формы, но инициируются с клиента) для записи объекта, который редактируется в данный момент в форме.
    • При их вызове платформа выполняет полный цикл записи, включая:
      • Проверку заполнения реквизитов (событие ОбработкаПроверкиЗаполнения, что важно учитывать при настройке проверки заполнения реквизитов объектов).
      • Вызов событий формы (ПередЗаписьюНаСервере, ПриЗаписиНаСервере модуля формы).
      • Актуализацию данных формы и ее реквизитов, синхронизируя их с базой данных.
      • Обработку управляемых блокировок.
    • Использование Записать() или ЭтаФорма.Записать() гарантирует, что все бизнес-правила и логика, реализованные в модуле формы, будут учтены при сохранении документа, а также синхронизирует состояние формы с базой данных, предотвращая ошибки версионирования.

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

Дополнительные аспекты для 1С:ЗУП и документа "Ведомость в банк"

Конфигурация 1С:Зарплата и Управление Персоналом (ЗУП) имеет свои особенности, особенно когда речь идет о расчетных документах. Документ "Ведомость в банк" часто связан с другими табличными частями, которые формируются автоматически при заполнении или пересчете документа.

Как справедливо отметил участник форума, при программном изменении основной табличной части Объект.Состав необходимо убедиться, что все связанные табличные части также актуализированы или очищены при необходимости. Например, это могут быть табличные части, содержащие данные по НДФЛ или взносам. Если вы удаляете строки из Объект.Состав, но соответствующие записи в других табличных частях остаются, это может привести к некорректным расчетам или ошибкам при проведении документа в дальнейшем.

Рекомендуем тщательно изучить структуру документа "Ведомость в банк" и его обработчики заполнения (например, процедуры ЗаполнитьЗарплата(), ЗаполнитьНДФЛ()), чтобы понять, какие еще табличные части могут быть затронуты вашими изменениями и требуют аналогичной корректировки (например, очистки с помощью метода Очистить() перед повторным заполнением или удалением отдельных строк).

Механизмы блокировок в 1С:Предприятии

Чтобы глубже понять природу ошибки "Данные были изменены или удалены другим пользователем", рассмотрим, как платформа 1С управляет конкурентным доступом к данным:

  1. Управляемые блокировки: Это основной механизм блокировок в современных конфигурациях 1С, работающих в режиме управляемого приложения. Платформа самостоятельно управляет блокировками данных, позволяя нескольким пользователям работать с одними и теми же таблицами, блокируя только те записи, которые реально изменяются. Менеджер управляемых блокировок 1С понимает структуру данных (например, измерения регистров, реквизиты документов) и блокирует данные более точно, не блокируя всю таблицу целиком. Это повышает параллельность работы и уменьшает количество конфликтов.
  2. Объектная пессимистическая блокировка: При редактировании данных в формах платформа автоматически устанавливает блокировку на объект, указанный в качестве основного реквизита формы. Эта блокировка не позволяет другим сеансам (или даже текущему сеансу) изменять данные данного объекта, пока блокировка не будет снята (например, при записи или отмене редактирования). Ошибка "Данные были изменены или удалены другим пользователем" часто является результатом того, что объект был изменен "в обход" этой объектной блокировки, то есть напрямую через Объект.Записать() на сервере, когда форма считала, что у нее эксклюзивное право на изменение и "помнила" предыдущую версию объекта.
  3. Транзакционные блокировки: Эти блокировки работают на уровне СУБД (например, PostgreSQL, MS SQL Server) и обеспечивают атомарность и изоляцию операций в рамках транзакции. 1С использует транзакции для гарантированной записи данных.

Когда вы записываете объект через Объект.Записать() на сервере, это происходит в рамках серверной транзакции и обновляет данные в базе. Форма же, пытаясь записать свой "старый" объект, обнаруживает, что версия данных в базе изменилась, и это воспринимается как конфликт, вызванный "другим пользователем". Вызов ЭтаФорма.Записать() решает эту проблему, поскольку он инициирует полный цикл обновления и записи данных формы, согласуя ее состояние с текущей базой данных и корректно работая с механизмом версионирования объектов.

Заключение

Мы выяснили, что ошибка "Данные были изменены или удалены другим пользователем" при программном изменении табличной части документа в 1С:ЗУП возникает из-за рассинхронизации между состоянием объекта в базе данных и представлением этого объекта в клиентской форме. Ключевое решение состоит в том, чтобы после всех программных изменений на сервере, которые записывают объект в базу данных, вызвать метод ЭтаФорма.Записать() на клиенте. Этот подход гарантирует, что форма обновит свои данные и выполнит все необходимые шаги для корректной записи документа. Также мы рассмотрели важные практики по безопасному удалению строк из табличных частей (обход с конца) и необходимость учета связанных табличных частей в сложных документах, таких как "Ведомость в банк" в 1С:ЗУП.

← На главную