Для внедренцев, которые работают с типовыми или собственными конфигурациями – и тех, кто готовится к Аттестации на 1С:Специалист по платформе.
В статье мы разберем:
- как правильно использовать управляемые блокировки при оперативном и неоперативном проведении документов
- к чему может привести отсутствие блокировок
- как не совершать ошибок, которые обнаружатся не сразу и могут иметь серьезные последствия :)
Время на прочтение – 20 минут.
Итак, две методики контроля остатков в 1С:Предприятии 8.3
Давайте начнем с того, что обозначения “старая методика” и “новая методика” достаточно условны. В самом деле, если “новая методика” используется с 2010 года – она уже не очень новая :)
Однако, мы еще раз вынуждены на этом остановиться, потому что различать эти подходы нужно и это имеет критическое значение.
“Старая методика” – это подход к контролю остатков, который использовался со времен «1С:Предприятие 8.0».
C 2010 года, с развитием платформы и добавлением новых возможностей с «1С:Предприятие 8.2» – применяется “новая методика” (однако – не везде).
В чем разница?
Принципиальная разница – в моменте контроля остатков:
- В “старой” методике остатки контролируются ДО записи движений в регистры.
Сначала проверяем остатки, если остатков “не хватает” (будут возникать отрицательные остатки) – проводить документ не будем - В “новой” методике – контроль происходит ПОСЛЕ записи движений, то есть постфактум.
Если после проведения образовались отрицательные остатки – нужно «откатить» транзакцию, то есть отменить проведение документа.
Детально преимущества и недостатки новой методики раскрыты в отдельной статье, поэтому ограничимся лишь общим тезисом – новая методика более оптимальна с точки зрения производительности и масштабируемости.
Ok, значит, старая методика ушла в прошлое и это удел УТ 10.3?
Нет, это не совсем так.
Новую методику можно использовать, когда при списании товаров все необходимые данные есть в документе и их не нужно вычислять.
Например, когда количество для списания известно из табличной части документа. Проблема возникает с себестоимостью, ведь её нужно рассчитать до записи в регистр, то есть выполнить запрос к базе данных.
Поэтому новая методика может успешно применяться в случае, если данные по количеству и себестоимости хранятся в отдельных регистрах.
Например, вот так:
Однако встречаются конфигурации, где и количество, и стоимость учитываются на одном регистре. И вот здесь-то обоснованно остается работать старая методика контроля остатков!
Вот пример одного регистра и для количества, и для себестоимости:
А что насчет типовых конфигураций? Там ведь только новая методика, верно?
Не всегда!
Вот, например, в «1C:Управление торговлей 11.3» есть 2 регистра:
При проведении документов отгрузки регистр «Себестоимость товаров» не заполняется вообще. Данные в этот регистр попадают только при выполнении регламентных операций по закрытию месяца.
В УТ 11 используется новая методика, так как все данные для проведения документов можно получить, не обращаясь к контролируемым регистрам.
Что касается «1C:Бухгалтерии», то там и количество, и себестоимость хранятся в одном регистре бухгалтерии, на соответствующих счетах БУ.
Поэтому в БП 3.0 используется старая методика.
В рамках текущей статьи мы разберем блокировки и для старой, и для новой методики контроля остатков.
Про оперативное проведение документов
В этом простом вопросе часто встречаются заблуждения.
Оперативное проведение можно анализировать при контроле остатков, но не обязательно.
Оперативное проведение – это возможность документа регистрировать возникающие события здесь и сейчас, то есть в реальном времени.
Настраивается оно с помощью специального свойства документа:
Что значит «регистрировать здесь и сейчас»? Платформа для оперативно проводимых документов выполняет ряд действий:
- Документам, проводимым сегодня, присваивается текущее время
- Если два документа проводятся одновременно, каждый будет иметь свое время (то есть система разнесет документы по разным секундам)
- Документы нельзя будет провести будущей датой.
Но главное другое – система передает признак оперативности проведения документа в обработку проведения:
Далее, в зависимости от значения этого параметра можно организовать запрос для получения остатков.
Для оперативно проводимых документов можно не указывать параметр в запросе, будут получаться актуальные остатки на 31.12.3999 год:
Актуальные остатки хранятся в системе и получаются максимально быстро (остатки на другие даты в большинстве случаев получаются расчетным путем).
Таким образом оперативное проведение можно принять и для старой, и для новой методики контроля остатков.
В УТ 11 документам, списывающим номенклатуру, запрещено проводиться оперативно. Например, это документы «Реализация товаров и услуг», «Сборка товаров», «Перемещение товаров», «Внутреннее потребление товаров» и другие.
Почему так сделано?
В системе контроль остатков всегда выполняется на актуальный момент времени (параметр Период в запросе не задается). А отсутствие оперативного проведения позволяет вводить документы будущим числом, такая задача часто требуется клиентам.
Контроль остатков по новой методике – без блокировок
Коротко рассмотрим алгоритм контроля остатков при проведении документа «Реализация товаров и услуг» на модельной конфигурации.
Есть два регистра:
- Свободные остатки – для количественного учета
- Себестоимость товаров – для учета себестоимости
Для контроля остатков товаров достаточно работы с регистром «Свободные остатки».
Код обработки проведения будет выглядеть таким образом:
Запрос = Новый Запрос;
// 1. Инициализация менеджера временных таблиц
#Область Область1
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
#КонецОбласти
// 2. Запрос, группирующий данные табличной части
#Область Область2
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТовары.Номенклатура КАК Номенклатура,
| СУММА(РеализацияТовары.Количество) КАК Количество,
| МИНИМУМ(РеализацияТовары.НомерСтроки) КАК НомерСтроки
|ПОМЕСТИТЬ ТоварыДокумента
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
|ГДЕ
| РеализацияТовары.Ссылка = &Ссылка
|
|СГРУППИРОВАТЬ ПО
| РеализацияТовары.Номенклатура
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| &Дата КАК Период,
| ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход) КАК ВидДвижения,
| ТоварыДокумента.Номенклатура КАК Номенклатура,
| ТоварыДокумента.Количество КАК Количество
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("Дата", Дата);
РезультатЗапроса = Запрос.Выполнить();
#КонецОбласти
// 3. Загрузка таблицы значений в набор записей
#Область Область3
Движения.СвободныеОстатки.Загрузить(РезультатЗапроса.Выгрузить());
#КонецОбласти
// 4. Запись движений в БД
#Область Область4
Движения.СвободныеОстатки.Записывать = Истина;
Движения.Записать();
#КонецОбласти
// 5. Запрос, получающий отрицательные остатки
#Область Область5
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыДокумента.НомерСтроки КАК НомерСтроки,
| -СвободныеОстаткиОстатки.КоличествоОстаток КАК Дефицит
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрНакопления.СвободныеОстатки.Остатки(
| &МоментВремени,
| Номенклатура В
| (ВЫБРАТЬ
| ТоварыДокумента.Номенклатура КАК Номенклатура
| ИЗ
| ТоварыДокумента КАК ТоварыДокумента)) КАК СвободныеОстаткиОстатки
| ПО ТоварыДокумента.Номенклатура = СвободныеОстаткиОстатки.Номенклатура
|ГДЕ
| СвободныеОстаткиОстатки.КоличествоОстаток < 0";
#КонецОбласти
// 6. Определение момента времени для контроля остатков
#Область Область6
МоментКонтроляОстатков =
?(Режим = РежимПроведенияДокумента.Оперативный,
Неопределено,
Новый Граница(МоментВремени(), ВидГраницы.Включая));
Запрос.УстановитьПараметр("МоментВремени", МоментКонтроляОстатков);
РезультатЗапроса = Запрос.Выполнить();
#КонецОбласти
// 7. Если запрос не пустой, значит образовались отрицательные остатки
#Область Область7
Если НЕ РезультатЗапроса.Пустой() Тогда
Отказ = Истина;
ВыборкаОшибки = РезультатЗапроса.Выбрать();
Пока ВыборкаОшибки.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара в количестве: "+ВыборкаОшибки.Дефицит;
Сообщение.Поле = "Товары["+(ВыборкаОшибки.НомерСтроки-1)+"].Количество";
Сообщение.УстановитьДанные(ЭтотОбъект);
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
#КонецОбласти
// 8. Если есть ошибки, то возвращаемся из обработчика события
#Область Область8
Если Отказ Тогда
Возврат;
КонецЕсли;
#КонецОбласти
КонецПроцедуры
Рассмотрим ключевые точки алгоритма контроля остатков.
1. Инициализация менеджера временных таблиц
Менеджер будет необходим, чтобы созданная в запросе временная таблица была доступна и в следующих запросах.
Таким образом, данные табличной части получаются один раз, сохраняются во временную таблицу и далее используются многократно.
2. Запрос, группирующий данные табличной части
В запросе выбираются сгруппированные данные табличной части.
Обратите внимание, что выбирается и номер строки документа – он потребуется для контекстной привязки сообщения об ошибке. Для номера строки используется агрегатная функция МИНИМУМ() – то есть сообщение будет привязано к первой строке, где встречается указанная номенклатура.
В первом запросе пакета создается временная таблица. Во втором запросе выбираются данные временной таблицы и добавляются 2 поля, необходимые для каждой записи регистра – Период и ВидДвижения.
3. Загрузка таблицы значений в набор записей
Одной командой происходит загрузка движений в набор записей.
Плюсы такого подхода:
- Не нужно выполнять предварительную очистку, то есть использовать метод Очистить()
- Не нужно организовывать цикл по выборке или табличной части.
4. Запись движений в БД
Запись можно было бы выполнить одной командой (вместо двух) – Движения.СвободныеОстатки.Записать().
И в нашем случае, когда записывается один регистр, разницы не будет никакой.
Но более универсальным является такой подход:
- Вначале установить флаг Записывать у необходимых наборов записей регистров
- Затем вызывать метод Записать() коллекции Движения, который записывает в БД все наборы с установленным флагом Записывать
После выполнения команды «Движения.Записать()» флаг Записывать у всех наборов сбросится в Ложь.
Также нужно помнить, что в конце транзакции (после ОбработкиПроведения) система автоматически запишет в БД только те наборы записей, у которых флаг Записывать установлен в значение Истина.
Метод Записать() коллекции Движения записывает наборы записей в одинаковой последовательности даже для разных документов.
Запись же движений вручную может привести к проблемам.
Приведем пример.
Если в документе «Реализация» выполнить запись так:
...
Движения.СебестоимостьТоваров.Записать();
А в документе «Перемещение товаров» изменить порядок:
...
Движения. СвободныеОстатки .Записать();
То это может привести к взаимоблокировке документов на пересекающихся наборах номенклатуры.
Приведенный подход записи движений можно использовать, если указано соответствующее значение записи движений в свойствах документа:
5. Запрос, получающий отрицательные остатки
В запросе выбираются отрицательные остатки по номенклатуре из документа.
Отрицательный остаток – это и есть нехватка (дефицит) товара.
Если бы мы не планировали делать привязку сообщений к полям документа, запрос можно сильно упросить – будут получаться данные из одной таблицы (остатков регистра).
6. Определение момента времени для контроля остатков
Вот здесь нам пригодилось оперативное проведение.
Если документ проводится оперативно, то момент для получения остатков – Неопределено, что означает получение актуальных остатков.
Если это неоперативное проведение, то мы получаем момент времени «после» документа – чтобы учесть только что сделанные движения.
Именно в этом и заключается выигрыш оперативно проводимых документов.
7. Если запрос не пустой, значит, образовались отрицательные остатки
В цикле обходим все отрицательные остатки и выводим сообщение привязанной к строкам табличной части.
Вот так будет выглядеть диагностическое сообщение:
8. Если есть ошибки, то возвращаемся из обработчика события
Если была хоть одна ошибка – выходим из процедуры.
Поскольку нет смысла продолжать проведение, транзакция всё равно не будет зафиксирована (а дальше у нас будет разработан код по списанию партий).
Реализация списания себестоимости по партиям
После того, как проверка остатков прошла успешно, можно приступать к списанию партий.
Код для списания по FIFO будет таким:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если РежимЗаписи = РежимЗаписиДокумента.Проведение
И НЕ ЭтотОбъект.ЭтоНовый()
И ЭтотОбъект.Проведен Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Документ.Дата КАК Дата
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документ
|ГДЕ
| Документ.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДокумент = РезультатЗапроса.Выбрать();
ВыборкаДокумент.Следующий();
ЭтотОбъект.ДополнительныеСвойства.Вставить("СтараяДатаДокумента", ВыборкаДокумент.Дата);
Иначе
ЭтотОбъект.ДополнительныеСвойства.Вставить("ДатаДокументаСдвинутаВперед", Ложь);
КонецЕсли;
КонецПроцедуры
Процедура ПриЗаписи(Отказ)
Если НЕ ЭтотОбъект.ДополнительныеСвойства.Свойство("ДатаДокументаСдвинутаВперед") Тогда
ЭтотОбъект.ДополнительныеСвойства.Вставить("ДатаДокументаСдвинутаВперед",
ЭтотОбъект.Дата>ЭтотОбъект.ДополнительныеСвойства.СтараяДатаДокумента);
Сообщить(ЭтотОбъект.ДополнительныеСвойства.ДатаДокументаСдвинутаВперед);
КонецЕсли;
КонецПроцедуры
Процедура ОбработкаПроведения(Отказ, Режим)
Запрос = Новый Запрос;
// 1. Инициализация менеджера временных таблиц
#Область Область1
...
#КонецОбласти
// 2. Запрос, группирующий данные табличной части
#Область Область2
...
#КонецОбласти
// 3. Загрузка таблицы значений в набор записей
#Область Область3
...
#КонецОбласти
// 4. Запись движений в БД
#Область Область4
...
#КонецОбласти
// 5. Запрос, получающий отрицательные остатки
#Область Область5
...
#КонецОбласти
// 6. Определение момента времени для контроля остатков
#Область Область6
...
#КонецОбласти
// 7. Если запрос не пустой, значит образовались отрицательные остатки
#Область Область7
...
#КонецОбласти
// 8. Если есть ошибки, то возвращаемся из обработчика события
#Область Область8
...
#КонецОбласти
// II. Подготовка наборов записей регистра "Себестоимость товаров"
#Область ОбластьII
Если ДополнительныеСвойства.ДатаДокументаСдвинутаВперед Тогда
Движения.СебестоимостьТоваров.Записывать = Истина;
Движения.СебестоимостьТоваров.Очистить();
Движения.Записать();
КонецЕсли;
Движения.СебестоимостьТоваров.Записывать = Истина;
#КонецОбласти
// III. Запрос получающий остатки партий для списания по FIFO
#Область ОбластьIII
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыДокумента.Номенклатура КАК Номенклатура,
| ТоварыДокумента.Количество КАК Количество,
| ТоварыДокумента.НомерСтроки КАК НомерСтроки,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
| ЕСТЬNULL(Остатки.СуммаОстаток, 0) КАК СуммаОстаток,
| Остатки.Партия КАК Партия
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров.Остатки(
| &МоментВремени,
| Номенклатура В
| (ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура
| ИЗ
| ТоварыДокумента КАК Т)) КАК Остатки
| ПО ТоварыДокумента.Номенклатура = Остатки.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
| Остатки.Партия.МоментВремени
|ИТОГИ
| МАКСИМУМ(Количество),
| СУММА(КоличествоОстаток)
|ПО
| Номенклатура";
МоментКонтроляОстатков =
?(Режим = РежимПроведенияДокумента.Оперативный,
Неопределено,
Новый Граница(МоментВремени(), ВидГраницы.Исключая));
Запрос.УстановитьПараметр("МоментВремени", МоментКонтроляОстатков);
РезультатЗапроса = Запрос.Выполнить();
#КонецОбласти
// IV. Цикл по номенклатуре документа
#Область ОбластьIV
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаНоменклатура.Следующий() Цикл
// V. Получим количество для списания
ОсталосьСписать = ВыборкаНоменклатура.Количество;
// VI. Цикл по партиям по FIFO
ВыборкаПартии = ВыборкаНоменклатура.Выбрать();
Пока ВыборкаПартии.Следующий() И ОсталосьСписать>0 Цикл
// VII. Проверка на нулевой остаток
Если ВыборкаПартии.КоличествоОстаток=0 Тогда
Продолжить;
КонецЕсли;
Движение = Движения.СебестоимостьТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаПартии.Номенклатура;
Движение.Партия = ВыборкаПартии.Партия;
// VIII. Расчет количества и суммы для списания
Движение.Количество = Мин(ОсталосьСписать, ВыборкаПартии.КоличествоОстаток);
Движение.Сумма = Движение.Количество*
ВыборкаПартии.СуммаОстаток/ВыборкаПартии.КоличествоОстаток;
// IX. Уменьшим количество для списания
ОсталосьСписать = ОсталосьСписать - Движение.Количество;
КонецЦикла;
КонецЦикла;
#КонецОбласти
КонецПроцедуры
Разберем ключевые точки алгоритма списания партий по FIFO.
I. Анализ смещения даты документа вперед
Здесь мы понимаем, сдвигается ли дата проведенного документа вперед. Эта информация будет полезна ниже, при очистке движений.
Для анализа сдвига даты документа потребуется 2 события:
- Перед записью – для получения старой даты документа и проверки режима проведения документа
- При записи – для получения новой даты документа
Данные между событиями передаем через специальную коллекцию объекта – «ДополнительныеСвойства». Она существует пока текущая версия объекта находится в памяти, то есть доступна для всех событий при проведении.
Почему в БП не нужно задействовать «При записи»?
Всё просто – документы отгрузки в бухгалтерии не могут проводиться оперативно. А это значит, что время документа не будет принимать оперативную отметку (если документ перепроводится текущим днем), поэтому и старую и новую дату документа можно получить в событии «Перед записью».
II. Подготовка наборов записей регистра «Себестоимость товаров»
Для документа установлен режим удаления движений – “При отмене проведения”:
Таким образом, есть вероятность, что при перепроведении мы можем учесть движения самого документа. НО произойдет это только в случае сдвига даты документа вперед. То есть очистку движений имеет смысл делать только при сдвиге даты документа вперед.
Приведем пример:
- Остаток мониторов LG на момент проведения документов – 10 шт.
- Проводится документ, который списывает 8 шт.
- В этом же документе время увеличивается на 1 минуту, перепроводим
Если не будет удаления старых движений, то система сообщит о нехватке 6 мониторов, поскольку текущие движения документа уже списали 8 из 10 имеющихся мониторов.
Но это неправильно: ситуации изменения «неоперативных» документов (вчерашних и более ранних) они не учтут.
То есть проблема «нехватки 6 мониторов» (см. выше) будет в этом случае решена только для документов изменяемых сегодняшним числом.
III. Запрос, получающий остатки партий для списания по FIFO
В запросе обращаемся к остаткам по партиям, при этом накладываем итоги по номенклатуре.
На уровне итогов получается количество из документа – МАКСИМУМ(Количество) и остаток партии – СУММА(КоличествоОстаток).
Если движения по регистрам “СвободныеОстатки” и “СебестоимостьТоваров” по количеству делаются синхронно (и приход, и расход), то такой ситуации возникнуть не может. На это мы и будем закладываться при списании партий.
IV. Цикл по номенклатуре документа
Благодаря итогам в запросе во внешнем цикле обходим номенклатуру из документа.
V. Получим количество для списания
Запомним, какое количество нужно списать. Далее это количество будет уменьшаться.
VI. Цикл по партиям по FIFO
Вложенный цикл будет содержать партии по текущей номенклатуре.
VII. Проверка на нулевой остаток
Далее будет производиться деление на количественный остаток. Деление на ноль приведет к ошибке во время исполнения.
Поэтому принимаем решение, что такие ошибочные партии будем пропускать. При желании можно выдать диагностику пользователю.
VIII. Расчет количества и суммы для списания
Количество для списания — это минимальное значение между остатком партии и тем, что осталось списать.
Сумма рассчитывается элементарной пропорцией.
То есть НЕ нужно делать дополнительных проверок (иногда дают такой совет) на то, что списывается все количество. Этот совет даже имеет своё название – «проблема копеек».
А тем, кто дает вредные советы имеет смысл заглянуть в конфигурацию «1С:Бухгалтерия 8». Там (о, ужас!) нет проверки на то, что списывается партия целиком :)
Вот скрин общего модуля «Учет товаров», метод «СписатьОстаткиТоваров»:
IX. Уменьшим количество для списания
Нужно понять, сколько еще осталось списать. Для этого вычтем количество из движения регистра.
Зачем нужны управляемые блокировки?
Вот мы и дошли до управляемых блокировок.
Казалось бы, представленные выше алгоритмы работают, как часы. Можете сами их потестировать (ссылки на выгрузки баз в конце статьи).
Но при реальной многопользовательской эксплуатации начнутся проблемы, причем, как это часто бывает, проблемы будут обнаружены не сразу…
Приведем пример наиболее типичной проблемы при списании товара, когда 2 пользователя практически одновременно пытаются списать товар (оформить продажу):
В этом примере два пользователя почти одновременно проводят продажу товаров – документ №2 начал проводиться чуть позже документа 1.
При получении остатка система сообщает, что остаток 10 шт., и оба документа успешно проводятся. Печальный итог – на складе минус 5 мониторов LG.
Но при этом контроль остатков работает! То есть, если документ №2 будет проводиться после окончания проведения документа №1, система не проведет документ №2:
Это очень опасное рассуждение.
Даже, два пользователя могут проводить документы практически одновременно, например, если один из них выполняет групповое проведение документов.
Кроме этого, нельзя быть застрахованным от увеличения количества пользователей. Если бизнес пойдет «в гору», то нужны будут новые продажники, кладовщики, логисты и так далее. Поэтому нужно сразу создавать решения, которые будут устойчиво работать в многопользовательской среде.
Как решить проблему при параллельном проведении документов?
Решение простое – заблокировать мониторы LG в момент времени Т1, так чтобы другие транзакции не могли обратиться к остаткам по этому товару.
Тогда в момент времени Т2 система будет ждать, когда монитор LG будет разблокирован. И после этого система получит актуальный остаток товаров и будет выполнено (или не выполнено) списание товаров.
Существует 2 типа блокировок:
- Объектные
- Транзакционные.
Если говорить просто, то объектные блокировки не позволяют интерактивно изменить двум пользователям один объект (элемент справочника или документ).
А транзакционные блокировки позволяют программно оперировать актуальными данными при выполнении движений по регистрам.
В этой статье нас будут интересовать именно транзакционные блокировки, далее просто блокировки.
Когда нужно накладывать блокировки?
Задача установки блокировок становится актуальной, как только в базе начинает работать более одного пользователя.
Блокировки нужно устанавливать в транзакциях, а когда возникают транзакции? Правильно, самый частый случай – проведение документов.
То есть блокировки нужно накладывать при проведении всех документов?
Ни в коем случае. Устанавливать блокировки «на всякий случай» точно не стоит. Ведь сами по себе блокировки снижают параллельность работы пользователей (масштабируемость системы).
Блокировки нужно накладывать на ресурсы (строки таблицы), которые читаются и изменяются в транзакциях. Например, при проведении документов.
В примере выше таким ресурсом является остаток по товару. Система должна была заблокировать остаток с момента получения данных об остатке (Т1) до окончания транзакции (Т3).
Пример, когда не нужно накладывать блокировку – проведение документа «Поступление товаров». В этом случае нет никакой конкуренции за ресурсы (остатки, …), поэтому блокировка будет вредна: она уменьшит масштабируемость системы.
Автоматические и управляемые блокировки
Здесь мы не будем вдаваться в теорию (это тема отдельной статьи), а скажем лишь, что управляемые блокировки являются более оптимальными.
Вместо теории можем привести пруф – все современные типовые конфигурации работают на управляемых блокировках.
Поэтому в нашей модельной конфигурации будет выбран соответствующий режим:
Управляемые блокировки в новой технологии контроля остатков
Блокировку будем накладывать на регистр “Свободные остатки” и только на номенклатурные позиции, встречающиеся в документе.
Причем правильный вариант наложения блокировки – как можно позднее.
В новой методике контроля остатков это нужно сделать перед записью (или в момент записи) движений в регистр “Свободные остатки”, чтобы другие транзакции не смогли изменить этот разделяемый ресурс.
Блокировку можно накладывать вручную (программным образом) и чуть позже мы покажем, как это делается.
Но дополнительный бонус новой технологии контроля остатков в том, что для блокировки разделяемых ресурсов нужна лишь одна строка кода.
Нужно просто установить свойство БлокироватьДляИзменения у набора записей регистра:
// 3. Загрузка таблицы значений в набор записей
#Область Область3
Движения.СвободныеОстатки.Загрузить(РезультатЗапроса.Выгрузить());
#КонецОбласти
// 3.1. Блокировка остатков регистра
#Область Область3_1
Движения.СвободныеОстатки.БлокироватьДляИзменения = Истина;
#КонецОбласти
// 4. Запись движений в БД
#Область Область4
Движения.СвободныеОстатки.Записывать = Истина;
Движения.Записать();
#КонецОбласти
...
В результате 2 транзакции не смогут изменять свободные остатки по одной номенклатуре.
Но для нашей статье принципиально следующее – система установит блокировку на комбинацию записываемых в регистр данных. А детально работу свойства БлокироватьДляИзменения мы рассмотрим в отдельной статье.
Кстати, в типовой УТ 11 не так-то просто найти установку свойства БлокироватьДляИзменения для регистра “Свободные остатки”. Дело в том, что это выполняется в модуле набора записей регистра, в событии “Перед записью”.
Вот и всё, одной строкой кода была обеспечена корректная работа системы!
Почему? Такая блокировка являлась бы излишней (а это определенная нагрузка на сервер 1С), поскольку движения в регистры “Свободные остатки” и “Себестоимость товаров” выполняются всегда синхронно, то есть последовательно друг за другом.
Поэтому, заблокировав товары из “Свободных остатков”, мы не допустим другие транзакции до этих товаров и в регистре “Себестоимость товаров”.
Но для старой методики контроля остатков блокировка будет накладываться по-другому. Для начала разберем алгоритм партионного списания для этого случая.
Старая методика контроля остатков
Напомним, что старая методика может применяться, если количество и стоимость учитываются в одном регистре.
Пусть это будет регистр “Себестоимость товаров”:
Тогда алгоритм проведения документа “Реализация товаров” будет выглядеть вот так:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если РежимЗаписи = РежимЗаписиДокумента.Проведение
И НЕ ЭтотОбъект.ЭтоНовый()
И ЭтотОбъект.Проведен Тогда
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Документ.Дата КАК Дата
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документ
|ГДЕ
| Документ.Ссылка = &Ссылка";
Запрос.УстановитьПараметр("Ссылка", ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДокумент = РезультатЗапроса.Выбрать();
ВыборкаДокумент.Следующий();
ЭтотОбъект.ДополнительныеСвойства.Вставить("СтараяДатаДокумента", ВыборкаДокумент.Дата);
Иначе
ЭтотОбъект.ДополнительныеСвойства.Вставить("ДатаДокументаСдвинутаВперед", Ложь);
КонецЕсли;
КонецПроцедуры
Процедура ПриЗаписи(Отказ)
Если НЕ ЭтотОбъект.ДополнительныеСвойства.Свойство("ДатаДокументаСдвинутаВперед") Тогда
ЭтотОбъект.ДополнительныеСвойства.Вставить("ДатаДокументаСдвинутаВперед",
ЭтотОбъект.Дата>ЭтотОбъект.ДополнительныеСвойства.СтараяДатаДокумента);
Сообщить(ЭтотОбъект.ДополнительныеСвойства.ДатаДокументаСдвинутаВперед);
КонецЕсли;
КонецПроцедуры
Процедура ОбработкаПроведения(Отказ, Режим)
// 2. Удаление "старых" движений документа
Если ДополнительныеСвойства.ДатаДокументаСдвинутаВперед Тогда
Движения.СебестоимостьТоваров.Записывать = Истина;
Движения.СебестоимостьТоваров.Очистить();
Движения.Записать();
КонецЕсли;
// 3. Установка флага для записи движений в конце транзакции
Движения.СебестоимостьТоваров.Записывать = Истина;
// 4. Запрос, получающий остатки по партиям на момент времени документа
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТовары.Номенклатура КАК Номенклатура,
| СУММА(РеализацияТовары.Количество) КАК Количество,
| МИНИМУМ(РеализацияТовары.НомерСтроки) КАК НомерСтроки
|ПОМЕСТИТЬ ТоварыДокумента
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
|ГДЕ
| РеализацияТовары.Ссылка = &Ссылка
|
|СГРУППИРОВАТЬ ПО
| РеализацияТовары.Номенклатура
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ТоварыДокумента.Номенклатура КАК Номенклатура,
| ТоварыДокумента.Количество КАК Количество,
| ТоварыДокумента.НомерСтроки КАК НомерСтроки,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
| ЕСТЬNULL(Остатки.СуммаОстаток, 0) КАК СуммаОстаток,
| Остатки.Партия КАК Партия
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров.Остатки(
| &МоментВремени,
| Номенклатура В
| (ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура
| ИЗ
| ТоварыДокумента КАК Т)) КАК Остатки
| ПО ТоварыДокумента.Номенклатура = Остатки.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
| Остатки.Партия.МоментВремени
|ИТОГИ
| МАКСИМУМ(Количество),
| СУММА(КоличествоОстаток)
|ПО
| НомерСтроки";
МоментКонтроляОстатков =
?(Режим = РежимПроведенияДокумента.Оперативный,
Неопределено,
Новый Граница(МоментВремени(), ВидГраницы.Исключая));
Запрос.УстановитьПараметр("МоментВремени", МоментКонтроляОстатков);
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаНоменклатура = РезультатЗапроса.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
// 5. Цикл по номенклатуре - проверяем достаточность количества для списания
Пока ВыборкаНоменклатура.Следующий() Цикл
ДефицитНоменклатуры = ВыборкаНоменклатура.Количество - ВыборкаНоменклатура.КоличествоОстаток;
Если ДефицитНоменклатуры>0 Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара в количестве: "+ДефицитНоменклатуры;
Сообщение.Поле = "Товары["+(ВыборкаНоменклатура.НомерСтроки-1)+"].Количество";
Сообщение.УстановитьДанные(ЭтотОбъект);
Сообщение.Сообщить();
Отказ = Истина;
КонецЕсли;
Если Отказ Тогда
Продолжить;
КонецЕсли;
// 6. Получим количество для списания
ОсталосьСписать = ВыборкаНоменклатура.Количество;
ВыборкаПартии = ВыборкаНоменклатура.Выбрать();
// 7. Цикл по партиям
Пока ВыборкаПартии.Следующий() И ОсталосьСписать>0 Цикл
// 8. Проверка на ноль (дальше будет выполняться деление)
Если ВыборкаПартии.КоличествоОстаток=0 Тогда
Продолжить;
КонецЕсли;
Движение = Движения.СебестоимостьТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаПартии.Номенклатура;
Движение.Партия = ВыборкаПартии.Партия;
// 9. Расчет количества для списания
Движение.Количество = Мин(ОсталосьСписать, ВыборкаПартии.КоличествоОстаток);
// 10. Расчет суммы списания
Движение.Сумма = Движение.Количество*
ВыборкаПартии.СуммаОстаток/ВыборкаПартии.КоличествоОстаток;
// 11. Уменьшим количество для списания
ОсталосьСписать = ОсталосьСписать - Движение.Количество;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Комментарии по ключевым моментам алгоритма.
1. Обработчик события «Перед записью»
Здесь мы понимаем, сдвигается ли дата проведенного документа вперед. Эта информация будет полезна ниже, при очистке движений.
2. Удаление «старых» движений документа
Напомним, что для документа установлен режим удаления движений – “При отмене проведения”.
Поэтому позаботиться об очистке движений должен разработчик.
3. Установка флага для записи движений в конце транзакции
Благодаря этому сформированные движения запишутся в момент окончания транзакции.
4. Запрос, получающий остатки по партиям на момент времени документа
Запрос состоит из двух пакетов.
В первом получаем сгруппированные данные табличной части. Номер строки выбираем, чтобы была возможность привязать сообщение об ошибке к конкретной строке документа.
Во втором запросе обращаемся к остаткам по партиям, при этом накладываем итоги по номеру строки. Таким образом, возможен контроль остатков на верхнем уровне (без анализа партий).
5. Цикл по номенклатуре - проверяем достаточность количества
Именно здесь мы проверяем достаточность товаров.
Если их не хватает, то параметр “Отказ” выставляется в “Истину” и дальнейший анализ партий не будет выполняться.
6. Получим количество для списания
В отдельной переменной запомним количество для списания. Далее в цикле по партиям оно будет уменьшаться.
7. Цикл по партиям
Организуем списание по партиям.
Причем количества партий гарантированно будет хватать – проверка на достаточность количества была выше.
8. Проверка на ноль (дальше будет выполняться деление)
Страховка от деления на ноль – очень важное дело :)
9. Расчет количества для списания
Количество для списания – это минимальное значение между остатком партии и тем, что осталось списать.
10. Расчет суммы списания
Сумма рассчитывается пропорцией.
11. Уменьшим количество для списания
Нужно понять, сколько еще осталось списать.
Блокировки в старой методике контроля остатков
С помощью блокировки мы как бы говорим системе, что только эта транзакция может работать с этим списком товаров (накладываем блокировку). Если другая транзакция будет пытаться наложить блокировку хоть на один из ранее заблокированных товаров, она попадет в очередь ожидания. Через некоторое время (тайм-аут) будет сделана еще одна попытка наложить блокировку.
Сейчас мы не будем предельно точно расписывать механику работы системы при ожидании на блокировке. Здесь важно другое – блокировки не позволят читать неактуальные остатки из-за параллельной работы пользователей. То есть транзакции будут выстраиваться в очередь для получения актуальных остатков.
Но при этом транзакции по непересекающемуся набору товаров будут выполняться параллельно.
В какой момент нужно накладывать блокировку?
Наша задача – получить актуальные остатки. Поэтому блокировку нужно накладывать ДО запроса, получающего остатки.
То есть в нашем коде запрос нужно разбить на два:
- В первом получается уникальный набор товаров, помещается во временную таблицу
- Во втором будут получаться остатки.
И между этими запросами должна быть наложена блокировка.
Вот так изменится код:
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ
| РеализацияТовары.Номенклатура КАК Номенклатура,
| СУММА(РеализацияТовары.Количество) КАК Количество,
| МИНИМУМ(РеализацияТовары.НомерСтроки) КАК НомерСтроки
|ПОМЕСТИТЬ ТоварыДокумента
|ИЗ
| Документ.РеализацияТоваровУслуг.Товары КАК РеализацияТовары
|ГДЕ
| РеализацияТовары.Ссылка = &Ссылка
|
|СГРУППИРОВАТЬ ПО
| РеализацияТовары.Номенклатура
|
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ТоварыДокумента.Номенклатура КАК Номенклатура
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
// II. Установка блокировки
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.СебестоимостьТоваров");
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
ЭлементБлокировки.ИсточникДанных = РезультатЗапроса;
ЭлементБлокировки.ИспользоватьИзИсточникаДанных("Номенклатура", "Номенклатура");
Блокировка.Заблокировать();
// III. Выполняем запрос к остаткам партий
Запрос.Текст =
"ВЫБРАТЬ
| ТоварыДокумента.Номенклатура КАК Номенклатура,
| ТоварыДокумента.Количество КАК Количество,
| ТоварыДокумента.НомерСтроки КАК НомерСтроки,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК КоличествоОстаток,
| ЕСТЬNULL(Остатки.СуммаОстаток, 0) КАК СуммаОстаток,
| Остатки.Партия КАК Партия
|ИЗ
| ТоварыДокумента КАК ТоварыДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СебестоимостьТоваров.Остатки(
| &МоментВремени,
| Номенклатура В
| (ВЫБРАТЬ
| Т.Номенклатура КАК Номенклатура
| ИЗ
| ТоварыДокумента КАК Т)) КАК Остатки
| ПО ТоварыДокумента.Номенклатура = Остатки.Номенклатура
|
|УПОРЯДОЧИТЬ ПО
| Партия
|ИТОГИ
| МАКСИМУМ(Количество),
| СУММА(КоличествоОстаток)
|ПО
| НомерСтроки
|АВТОУПОРЯДОЧИВАНИЕ";
Прокомментируем ключевые моменты.
I. Инициализация менеджера временных таблиц
Созданную временную таблицу мы будем использовать во втором запросе. Чтобы была такая возможность, нужно создать менеджер временных таблиц.
II. Установка блокировки
К этому мы шли всю статью.
Товары для блокировки берутся из результата запроса. Далее сопоставляются поле из регистра и поле запроса (в нашем случае оба называются – Номенклатура).
III. Выполняем запрос к остаткам партий
Теперь можно выполнять запрос, получающий остатки. В этот момент времени гарантированно только одна транзакция работает с остатками по конкретному набору товаров.
Подведение итогов
Установку управляемых блокировок нужно обязательно выполнять на конкурирующие ресурсы, которые используются в транзакциях (в частности при проведении документов).
Мы рассмотрели установку блокировок для старой и новой методики контроля остатков:
- В новой методике используется свойство БлокироватьДляИзменения набора записей регистра
- В старой – запросом получаются данные из табличной части и программно устанавливается блокировка до получения остатков
Новая методика используется в УТ 11, старая – в БП 3.0.
Но это не значит, что в УТ 11 не накладываются блокировки программным образом. Например, для регистров взаиморасчетов используется блокировка через объект “Блокировка данных”.
И не забывайте про блокировки на экзамене “1С:Специалист” по платформе! :)
Выгрузки ИБ и PDF-версия статьи для участников группы ВКонтакте
Мы ведем группу ВКонтакте – http://vk.com/kursypo1c.
Если Вы еще не вступили в нее – сделайте это сейчас, и в блоке ниже (на этой странице) появятся ссылки на скачивание материалов.
Вы можете скачать эту статью в формате PDF по следующей ссылке: Ссылка доступна для зарегистрированных пользователей)
Ссылка доступна для зарегистрированных пользователей)
ИБ разрабатывались на платформе 1С:Предприятие 8.3.9.1850.
Логически странный подход (с функциональной точке зрения, а не устойчивости программной реализации), задним числом документ можно провести, а будущей датой нет
Добрый день!
Так реализовано на уровне платформы. Предполагаю, что это может быть связано с тем, чтобы вводить в систему уже свершившиеся факты.
Если в конфигураторе для документа запретить оперативное проведение, то документ будущей датой проведется.
Приветствую! Помогите прояснить момент, когда две старых методики проведения одновременно, при записи пустых движений, блокируют две разные номенклатуры удаляемых записей, а затем пытаются установить явные блокировки на противоположную номенклатуру. Как в данном случае обходится без взаимной блокировки?
Добрый день.
Описанная ситуация возможна, если стоит режим удаления движений Удалять автоматически.
Если режим удаления Удалять автоматически при отмене проведения, тогда очистка и блокировка движений будет в момент записи новых движений.
Что бы подстраховаться можно явной блокировкой сразу блокировать и старые и новые данные.
Добрый день!
Не могу понять вот этот момент:
«Похожий прием используется в типовой «1С:Бухгалтерии 8». Но там используется одно событие «Перед записью».
Почему в БП не нужно задействовать «При записи»?
Всё просто – документы отгрузки в бухгалтерии не могут проводиться оперативно. А это значит, что время документа не будет принимать оперативную отметку (если документ перепроводится текущим днем), поэтому и старую и новую дату документа можно получить в событии «Перед записью».
Почему и как, если у нас не оперативный режим, можно получить две даты (до изменения и после). Почему мы тут не используем ПриЗаписи()?
Здравствуйте.
Документ, для которого разрешено оперативное проведение, может проводиться оперативно. При этом, если документ проводится текущей датой, то его время изменяется на текущее. Изменяется оно как раз во время записи документа (с проведением). Поэтому узнать, какое время будет у оперативно проводимого документа, можно только после его записи. Событие ПриЗаписи как раз и вызывается после записи документа, а значит, в нём уже известно окончательное время документа.
Если для документа оперативное проведение запрещено, значит, документ всегда проводится неоперативно. А потому при записи его время не изменяется. А значит, ещё до записи документа можно получить и обработать его время. До начала записи документа срабатывает событие ПередЗаписью. Использование обработчика ПриЗаписи в этом варианте не требуется, т.к. все необходимые действие уже выполнены в обработчике ПередЗаписью.
О, дошло, спасибо за разъяснение!
Здравствуйте, а почему в старой методике блокировка ставится после удаления старых движений их регистра? Ведь если кто-то изменит данные после того, как мы удалили движения, а потом мы прочитаем данные, может оказаться так, что ресурсы, по которым мы хотим записать движения в регистр, были записаны при проведении другого документа.
Приведу пример: В таблице остатков у нас есть стулья в количестве 10 штук. Мы хотим перепровести документ “Расходная накладная №1”, где на данный момент указано движение по стульям в количестве двух штук. Нам нужно добавить в этот документ ещё одну запись, например по столам, которых у нас много. Добавляем, записываем, проводим….и в тот момент времени когда у нас удаляются старые движения по стульям в количестве 2 штук перед блокировкой. Другой пользователь читает и списывает стулья в количестве 12 штук. В итоге наш документ с двумя стульями и каким-то количеством столов не проводится. Потому что они уже списаны другим пользователем
Здравствуйте.
Удаление старых движений нужно для того, чтобы документ при выборке остатков регистра не “видел” свои старые движения, которые в итоге будут автоматически удалены (замещены) при записи новых, и учёт которых в остатках привел бы к искаженным данным и, как следствие, к ошибке.
При этом система автоматически установит блокировку на те записи регистра, которые соответствуют позициям номенклатуры из удаляемых движений.
В другом документе, который проводится одновременно с данным, также задействован механизм блокировок: перед тем, как прочитать данные (старая методика) или перед тем, как их записать (новая методика) документ попытается установить блокировку по данным регистра по нужным ему позициям номенклатуры. И есть данные регистра заблокированы, будет ожидать, пока блокировка не будет снята, т.е. до окончания транзакции (окончания обработки проведения) того документа, который заблокировал данные.
В Вашем примере.
1. Удаляются старые движений документа “Накладная 1”. При этом будут заблокированы данные регистра по стульям.
2. Другой документ, “Накладная 2”, пытается прочитать данные из регистра по стульям. Предположим, что в нём используется старая методика. Значит, перед чтением он попытается заблокировать (исключительная блокировка) данные регистра по стульям. Но это ему не удастся, т.е. эти данные уже заблокированы документом “Накладная 1”.
3. Как только документ “Накладная 1” закончил обработку проведения, данные регистра по стульям разблокировались, и документ “Накладная 2” продолжит проведение. При этом важно, что документ “Накладная 2” получит информацию об остатках, которые получились после перепроведения документа “Накладная 1”, т.е. будет оперировать количеством стульев, равным 10. А потому остатков для его проведения окажется недостаточно, обработка проведения будет отменена, и ошибки (отрицательные остатки регистра) не произойдет.
Что же до ситуации, когда между удалением старых движений и установкой блокировки “вклинится” другой документ, то в этом случай ошибки не произойдет. Дело в том, что в другом документе также работает механизм блокировок: перед тем как считать данные, необходимые для формирования движений, документ заблокирует эти данные; блокировка будет активна до окончания транзакции (до окончания обработки проведения). Т.е. документ сможет прочитать данные, сформировать и записать движения, и никакой другой документ “вклиниться” в этот процесс не сможет.
Механизм блокировок как раз и обеспечивает, что данные, прочитанные из регистра запросом, не будут изменены кем-то другим до полного завершения формирования движения по регистру.
1. Ну вот такой Сценарий:
>Пользователь 1 удалил старые движения(блокировка снята)
>Пользователь 2 читает данные(блокировка1 наложена)
>Пользователь 2 изменил данные(блокировка1 всё наложена)
>Пользователь 2 завершил обработку проведения(блокировка1 снята)
>Пользователь 1 читает данные(блокировка2 наложена)….
Тут блокировки не конфликтуют…
2.Разрешите уточнить: Метод записать() во время удаления движений устанавливает блокировку до конца обработки проведения или транзакция завершится при переходе к следующей строке кода после “Дживжения.РегистрОстатков.Записать()” и блокировка снимается. Если по окончанию обработки проведения, тогда у меня вопрос. А зачем мы вообще вручную устанавливаем блокировку, если регистр уже заблокирован методом “записать()”?
1. В Вашем примере действие в пункте
>Пользователь 2 читает данные(блокировка1 наложена)
произойдет только после того, как завершится транзакция “Пользователя 1”.
Дело в том, что после действия
>Пользователь 1 удалил старые движения
данные регистра были заблокированы по тем записям, которые соответствуют удаленным. В приведенном ранее примере, будут заблокированы все записи регистра, в которых измерение “Номенклатура” содержит значение “Стул”
Поэтому последовательность будет такая:
>Пользователь 1 удалил старые движения (блокировка установлена)
>Пользователь 2 пытается установить блокировку для чтения данных, но не может, т.к. данные заблокированы Пользователем 1. Пользователь 2 ожидает окончания блокировки Пользователя 1
>Пользователь 1 устанавливает явную (дополнительную) блокировку – по номенклатуре из документа
>Пользователь 1 читает данные (блокировка сохраняется)
>Пользователь 1 записывает данные в регистр (блокировка сохраняется)
>Пользователь 1 заканчивает обработку проведения (транзакцию записи), блокировка снимается
>Пользователь 2 устанавливает явную блокировку по номенклатуре из документа
>Пользователь 2 читает данные (блокировка сохраняется)
>Пользователь 2 записывает данные в регистр (блокировка сохраняется)
>Пользователь 2 заканчивает обработку проведения (транзакцию записи), блокировка снимается
2. Блокировка устанавливается до окончания транзакции. Т.е. до окончания обработки проведения. Однажды установленная в процедуре ОбработкаПроведения() будет активна до конца этой процедуры.
А зачем мы вообще вручную устанавливаем блокировку, если регистр уже заблокирован методом “записать()”?
При старой методике блокировка устанавливается явным образом на те записи, которые критичны для правильности получения данных и формирования движений. Эти данные нужно получить ДО записи в регистр и нужно также обеспечить их неизменность до окончания обработки. В данном случае метод Записать() с блокировкой не поможет, т.к. он выполняется ПОСЛЕ выборки данных из регистра. Что же касается записи в регистр при очистке старых движений, то блокировка при этом устанавливается только на те записи, которые соответствуют удаляемым записям (в нашем примере – по тем же позициям номенклатуры), а при формировании движений могут быть затронуты совсем другие записи (в нашем примере – при перепроведении с изменением в документе могут быть совсем другие позиции номенклатуры). Кроме того, документ может и не перепроводится, в проводиться впервые, но данные регистра нужно блокировать в любом случае.
При новой методике как раз и используют блокировки, устанавливаемую системой при записи автоматически – т.к. нужно заблокировать именно записи регистра по номенклатуре из документа. При этом также заблокируются и записи с номенклатурой из старых, удаляемых (замещаемых при записи) движений. Перед записью устанавливают свойство “БлокироватьДляИзменения” в значение “Истина” – для того чтобы заблокировать все записи по данной номенклатуре, без учета разделителей итогов (если для регистра включено разделение итогов). Подробнее о свойстве “БлокироватьДляИзменения” можно прочитать в этой статье, а также в этой.
Всё стало разложено по своим местам. Огромное спасибо!
“Почему так сделано?
В системе контроль остатков всегда выполняется на актуальный момент времени (параметр Период в запросе не задается). А отсутствие оперативного проведения позволяет вводить документы будущим числом, такая задача часто требуется клиентам.”
Интересная логика, а нельзя было эту возможность неоперативного управления обыграть Правами? Кому надо неоперативно давать право на документ “Интерактивное проведение неоперативное”. Получается такая брешь в безопасности в типовой из коробки((
Здравствуйте.
Нет, такой вариант не сработает.
Если для документа включить возможность оперативного проведения, то возможность проведения документа будущей датой будет недоступна. Возможность оперативного проведения и возможность проведения будущей датой взаимоисключаются на уровне системы. Поэтому ограничением прав эту задачу решить не получится. Если для документа разрешено оперативное проведение, то можно ограничить пользователю права, чтобы он всегда проводил документы оперативно, либо дать возможность их неоперативного проведения. Но в любом случае возможности проведения документов будущей датой у пользователя не будет.
Добрый день , при изменении Документа Заказ поставщику на неоперативное проведение , какие последствия будет для работы ERP ? Будут ли проблемы при обновлении ?
Здравствуйте.
Представляется, что негативными последствиями такого решения может быть некоторое замедление проведения документов “Заказ поставщику” – за счет того, что не будет использоваться более быстрый способ получения остатков при оперативном проведении. Программной доработки, скорее всего, не потребуется, т.к. даже документ, которому разрешено оперативное проведение, может быть проведен и в неоперативном режиме, поэтому обработка такой ситуации должна быть предусмотрена в программном коде. При запрете оперативного проведения для документа алгоритм будет просто всегда выбирать ветку с неоперативным проведением.
Другим последствием запрета оперативного проведения будет являться возможность проводить документ будущей датой. Соответственно, текущие остатки регистров, по которым выполняет движения этот документ, также могут теперь содержать остатки будущей даты (а не остатки на текущий момент, как в изначальном варианте). Соответственно, следует проверить, имеются ли другие документа, которые выполняют движений по этим регистрам и для которых предусмотрена возможность оперативного проведения, проверить корректность получения остатков для этих документов (возможно, они интерпретируют текущие остатки регистра именно как остатки на текущий момент) и при необходимости внести правки. Кроме того, следует выяснить, какие проверки производятся при проведении документа “Заказ поставщику”, какие остатки при этом используются – убедиться что эти эти проверки будут корректно отрабатывать при проведении документа будущей датой, и при необходимости внести правки.
Если же проведение документа будущей датой не требуется для решения задач и запрещение оперативного проведения документа выполняется по иной причине, для упрощения можно эту возможность программно запретить.
Особых проблем при обновлении не будет. Конечно, автоматически обновить уже не получится, придется вносить доработки (измененное свойство документа, и, возможно, некоторые доработки в коде) в новую конфигурацию при обновлении. Но так как изменения небольшие, это не должно представлять каких-то трудностей.
Если НЕ ЭтотОбъект.ДополнительныеСвойства.Свойство(“ДатаДокументаСдвинутаВперед”)Тогда
ЭтотОбъект.ДополнительныеСвойства.Вставить(“ДатаДокументаСдвинутаВперед”,
ЭтотОбъект.Дата>ЭтотОбъект.ДополнительныеСвойства.СтараяДатаДокумента);
Сообщить(ЭтотОбъект.ДополнительныеСвойства.ДатаДокументаСдвинутаВперед);
КонецЕсли;
“НЕ” в условии точно нужно?
Здравствуйте.
В условии нет ошибки.
При перепроведении документа свойство “ДатаДокументаСдвинутаВперед” не устанавливается (оно устанавливается только при проведении нового документа или при записи документа без проведения). Именно эту ситуацию (перепроведение документа) здесь и обрабатывают: устанавливают значение свойства “ДатаДокументаСдвинутаВперед”, сравнивая старую дату документа и новую (которая, возможно, изменилась при записи документа в режиме оперативного проведения).
Спасибо. Наличие свойства по ключу, и получение значения перепутал.
“То есть НЕ нужно делать дополнительных проверок (иногда дают такой совет) на то, что списывается все количество. Этот совет даже имеет своё название – «проблема копеек».”
Не понял проблемы. Можно подробнее?
Добрый день!
Суть проблемы копеек в том, что в регистре будет ноль по ресурсу Количество и ненулеовое значение по ресурсу Сумма.
Собственно в статье сказано, что в текущей методике такой проблемы не будет.
Посмотрел статью https://xn—-1-bedvffifm4g.xn--p1ai/courses/dev-att/general-methods-startpage/sjlllysinr-chapter-12/ .
Сделал вывод: если писать не Сумма Остаток / Количество Остаток * Количество Списания, а Количество списания / Количество остаток x Сумма остаток, то проблемы нет.
Вопрос: зачем таки проверять, что количество списывается все?
Добрый день!
Вы правы, проблемы не будет, можно применять на практике :)
Приведенный подход имеет исторические корни, которые в том числе касаются оптимизации.
Операция сравнения выполняется достаточно быстро.
Операции умножения и деления – кратно дольше.
Списание остатка “под ноль” довольно частотная операция. Поэтому с помощью такого подхода в общем случае мы делаем проведение документа чуть быстрее.
Для новой методики написано так: “На самом деле при свойство БлокироватьДляИзменения не устанавливает управляемую блокировку, оно лишь выключает режим разделения итогов регистра при записи.”
А если в нашем регистре Свободные остатки выключен режим разделения итогов, то в этой записи не будет смысла? Получается ни ручные блокировки не нужны, ни эта строчка не нужна?
Здравствуйте.
Да, в данном случае, если по регистру СвободныеОстатки формирование движений и контроль остатков выполняется по “новой” методике (т.е. сначала запись движений в базу данных, а затем проверка результата), и для этого регистра в конфигураторе выключено использование разделение итогов, то команду
БлокироватьДляИзменения = Истина
можно убрать – она не окажет никакого эффекта: и без этой команды блокировка будет установлена системой автоматически по всем требуемым записям регистра в момент записи набора записей регистра в базу данных (т.е. при выполнении команды Движения.Записать() или Движения.ИмяРегистра.Записать()) .
Явная установка блокировки (с помощью объекта БлокировкаДанных) здесь также не потребуется.
…”Для оперативно проводимых документов можно не указывать параметр в запросе, будут получаться актуальные остатки на 31.12.3999 год:”…
Это грубейшая ошибка, допустим есть два вида документов, которые двигают один и тот же регистр: Вид А (Оперативное проведение разрешено) и Вид Б (Оперативное проведение запрещено)
Таким образом в системе существуют проведенные документы типа Б в сколь угодно отдаленном будущем(вплоть до 3999…)
А теперь подумайте какие остатки Вы получите при оперативном проведении документа типа А, если не укажите параметр или укажите НЕОПРЕДЕЛЕНО
Добрый день!
Предлагаю находится в контексте решаемой задачи. Мы ведь не делаем доработку типовой (где документы проводятся неоперативно).
Мы разрабатываем собственное решение, где документы проводятся оперативно.
Но в целом ремарка хорошая :) Однако не будем усложнять статью, а оставим эту информацию в комментариях.
Какие упущения вы видите в том, чтобы вот этот код, посвященный определению условия очистки движений регистра себестоимости :
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
Если РежимЗаписи = РежимЗаписиДокумента.Проведение
И НЕ ЭтотОбъект.ЭтоНовый()
И ЭтотОбъект.Проведен Тогда
Запрос = Новый Запрос;
Запрос.Текст =
“ВЫБРАТЬ
| Документ.Дата КАК Дата
|ИЗ
| Документ.РеализацияТоваровУслуг КАК Документ
|ГДЕ
| Документ.Ссылка = &Ссылка”;
Запрос.УстановитьПараметр(“Ссылка”, ЭтотОбъект.Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДокумент = РезультатЗапроса.Выбрать();
ВыборкаДокумент.Следующий();
ЭтотОбъект.ДополнительныеСвойства.Вставить(“СтараяДатаДокумента”, ВыборкаДокумент.Дата);
Иначе
ЭтотОбъект.ДополнительныеСвойства.Вставить(“ДатаДокументаСдвинутаВперед”, Ложь);
КонецЕсли;
КонецПроцедуры
Процедура ПриЗаписи(Отказ)
Если НЕ ЭтотОбъект.ДополнительныеСвойства.Свойство(“ДатаДокументаСдвинутаВперед”) Тогда
ЭтотОбъект.ДополнительныеСвойства.Вставить(“ДатаДокументаСдвинутаВперед”,
ЭтотОбъект.Дата>ЭтотОбъект.ДополнительныеСвойства.СтараяДатаДокумента);
Сообщить(ЭтотОбъект.ДополнительныеСвойства.ДатаДокументаСдвинутаВперед);
КонецЕсли;
КонецПроцедуры
заменить вот этим:
Процедура ПередЗаписью(…)
ДополнительныеСвойства.Вставить(“Момент”, МоментВремени()); //здесь нет обращений к БД
//единственные накладные расходы, мы всякий раз пишем в ДополнительныеСвойства свойство, //не думаю, что это сильно скажется на памяти сервера 1С
КонецПроцедуры
Процедура ОбработкаПроведения(…)
Сдвиг = ДополнительныеСвойства.Момент.Сравнить(МоментВремени()) < 0;
Если Сдвиг Тогда
//Чистим движения
КонецЕсли;
КонецПроцедуры
Упущение вот какое – если документ не записан (создан и проводится впервые), то момент времени для него неопределен.
Соответственно код будет работать некорректно.
Согласен, в коде еще нужно условие поставить
Процедура ПередЗаписью(…)
Если ЭтоНовый() И РежимЗаписиДокумента.Проведение Тогда
ДополнительныеСвойства.Вставить(“Момент”, МоментВремени());
КонецЕсли;
КонецПроцедуры
Сдвиг = ДополнительныеСвойства.Свойство(“Момент”) И ДополнительныеСвойства.Момент.Сравнить(МоментВремени()) < 0;
Если Сдвиг Тогда
//Чистим движения
КонецЕсли;
Получается немного больше кода, но все равно менее громоздко чем ваш вариант.
Нет, вы не поняли. Момент времени не определен, но заполнен началом дня от даты документа.
Посмотрите в отладчике и вопрос снимется :)