В процессе разработки складского функционала на платформе 1С:Предприятие 8 часто возникает задача не просто зафиксировать поступление или выдачу ТМЦ, но и обеспечить жесткий контроль за тем, чтобы сотрудники не возвращали больше оборудования, чем им было выдано ранее. Рассмотрим подробно, как правильно спроектировать архитектуру регистров и реализовать программный контроль остатков в документах выдачи и возврата.
Одной из главных ошибок начинающих разработчиков является попытка уместить все движения в один регистр накопления (например, только в РегистрОстатка). Как справедливо отмечено в обсуждении, «пихать всё в один регистр — путь в никуда». Для корректного учета нам понадобятся две независимые сущности:
Склад, Оборудование.РегистрХранения) — регистр остатков, который фиксирует, какое оборудование числится за конкретным подотчетным лицом. Основные измерения: Сотрудник, Оборудование.Разберем логику работы документов в этой связке:
ОборудованиеНаСкладах.ОборудованиеНаСкладах (с контролем остатка на складе) и одновременно «Приход» в ОборудованиеВЭксплуатации.ОборудованиеВЭксплуатации (с контролем остатка у конкретного сотрудника) и «Приход» в ОборудованиеНаСкладах.Перед тем как выдать монитор или ноутбук сотруднику, мы должны убедиться, что это оборудование физически есть на выбранном складе. Проанализируем стандартную методику контроля, реализованную в процедуре ОбработкаПроведения.
Процедура ОбработкаПроведения(Отказ, Режим)
// 1. Подготовка движений
Движения.ОборудованиеНаСкладах.Записывать = Истина;
Движения.ОборудованиеВЭксплуатации.Записывать = Истина;
// Сначала записываем пустые наборы или очищаем существующие
Движения.Записать();
// 2. Формируем запрос для проверки остатков на складе
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ВыдачаТаблица.Оборудование КАК Оборудование,
| СУММА(ВыдачаТаблица.Количество) КАК Количество
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| Документ.Выдача.Таблица КАК ВыдачаТаблица
|ГДЕ
| ВыдачаТаблица.Ссылка = &Ссылка
|СГРУППИРОВАТЬ ПО
| ВыдачаТаблица.Оборудование
|;
|
|ВЫБРАТЬ
| ВТ_Товары.Оборудование КАК Оборудование,
| ВТ_Товары.Количество КАК КоличествоВДокументе,
| ЕСТЬNULL(Остатки.КоличествоОстаток, 0) КАК ОстатокНаСкладе
|ИЗ
| ВТ_Товары КАК ВТ_Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОборудованиеНаСкладах.Остатки(&МоментВремени,
| Склад = &Склад И Оборудование В (ВЫБРАТЬ Оборудование ИЗ ВТ_Товары)) КАК Остатки
| ПО ВТ_Товары.Оборудование = Остатки.Оборудование";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
Запрос.УстановитьПараметр("Склад", Склад);
Запрос.УстановитьПараметр("МоментВремени", МоментВремени());
// 3. Анализ результата и формирование движений
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Если Выборка.ОстатокНаСкладе < Выборка.КоличествоВДокументе Тогда
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара " + Выборка.Оборудование + ". Остаток: " + Выборка.ОстатокНаСкладе;
Сообщение.Сообщить();
Отказ = Истина;
КонецЕсли;
Если Не Отказ Тогда
// Движение по складу (минус)
ДвижениеСклад = Движения.ОборудованиеНаСкладах.ДобавитьРасход();
ДвижениеСклад.Период = Дата;
ДвижениеСклад.Склад = Склад;
ДвижениеСклад.Оборудование = Выборка.Оборудование;
ДвижениеСклад.Количество = Выборка.КоличествоВДокументе;
// Движение по эксплуатации (плюс сотруднику)
ДвижениеЭксп = Движения.ОборудованиеВЭксплуатации.ДобавитьПриход();
ДвижениеЭксп.Период = Дата;
ДвижениеЭксп.Сотрудник = Сотрудник;
ДвижениеЭксп.Оборудование = Выборка.Оборудование;
ДвижениеЭксп.Количество = Выборка.КоличествоВДокументе;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Теперь перейдем к ключевому вопросу: как не позволить Ивану вернуть 5 мониторов, если мы выдавали ему только 2? Основная проблема кода, представленного в теме форума, заключалась в том, что в запросе к виртуальной таблице Остатки не передавался фильтр по измерению Сотрудник. Из-за этого система проверяла общий остаток оборудования у всех сотрудников сразу.
Рассмотрим правильный алгоритм проведения возврата с использованием «старой» методики (запись с последующей проверкой на отрицательный остаток), так как она наиболее наглядна для понимания процесса.
Шаг 1. Формирование движений расхода. Сначала мы записываем в регистр эксплуатации «Расход», как будто сотрудник уже вернул оборудование.
Шаг 2. Запись движений. Вызываем Движения.Записать(). В этот момент физические таблицы итогов обновляются.
Шаг 3. Проверка на минус. Мы выполняем запрос к остаткам именно по ЭТОМУ сотруднику. Если после нашего документа остаток стал отрицательным — значит, сотрудник пытается вернуть лишнее.
Процедура ОбработкаПроведения(Отказ, Режим)
// Очищаем старые движения и готовимся к записи новых
Движения.РегистрХранения.Очистить();
Движения.РегистрОстатка.Очистить();
// 1. Создаем движения расхода у сотрудника
Для Каждого ТекСтрока Из ТаблицаОборудования Цикл
Движение = Движения.РегистрХранения.ДобавитьРасход();
Движение.Период = Дата;
Движение.Сотрудник = Сотрудник; // Важно!
Движение.Оборудование = ТекСтрока.Оборудование;
Движение.Количество = ТекСтрока.Количество;
КонецЦикла;
// 2. Записываем движения в базу данных, чтобы увидеть результат в виртуальных таблицах
Движения.РегистрХранения.Записывать = Истина;
Движения.Записать();
// 3. Проверяем, не ушел ли остаток у сотрудника в минус
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.Оборудование КАК Оборудование,
| ПРЕДСТАВЛЕНИЕССЫЛКИ(Остатки.Оборудование) КАК ОборудованиеПредставление,
| -Остатки.КоличествоОстаток КАК Дефицит
|ИЗ
| РегистрНакопления.РегистрХранения.Остатки(&МоментВремени,
| Сотрудник = &Сотрудник И Оборудование В (ВЫБРАТЬ Оборудование ИЗ Документ.Возврат.Таблица ГДЕ Ссылка = &Ссылка)) КАК Остатки
|ГДЕ
| Остатки.КоличествоОстаток < 0";
// Используем Границу, чтобы включить движения текущего документа в проверку
ГраницаКонтроля = Новый Граница(МоментВремени(), ВидГраницы.Включая);
Запрос.УстановитьПараметр("МоментВремени", ГраницаКонтроля);
Запрос.УстановитьПараметр("Сотрудник", Сотрудник);
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
Отказ = Истина;
ВыборкаОшибки = РезультатЗапроса.Выбрать();
Пока ВыборкаОшибки.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Сотрудник " + Сотрудник + " не может вернуть " + ВыборкаОшибки.ОборудованиеПредставление +
". Превышение на " + ВыборкаОшибки.Дефицит + " шт.";
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
// 4. Если всё хорошо, делаем приход на склад
Если Не Отказ Тогда
Движения.РегистрОстатка.Записывать = Истина;
Для Каждого ТекСтрока Из ТаблицаОборудования Цикл
Движение = Движения.РегистрОстатка.ДобавитьПриход();
Движение.Период = Дата;
Движение.Склад = Склад;
Движение.Оборудование = ТекСтрока.Оборудование;
Движение.Количество = ТекСтрока.Количество;
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Проанализируем ситуацию подробнее. В реальных высоконагруженных системах описанный выше способ («старая» методика) может приводить к избыточным блокировкам. Выясним причины и способы улучшения:
.Остатки(). Это критично для производительности СУБД, так как сервер 1С отсекает лишние данные на этапе получения из SQL, а не фильтрует всю таблицу целиком.Граница с видом Включая. Если передать просто Дата, вы рискуете получить остатки на начало секунды, не учитывая текущий документ.БлокировкаДанных по соответствующим измерениям регистра.Сотрудник и Оборудование в конфигураторе установлены как Измерения. Если сделать их реквизитами, получение остатков в их разрезе станет невозможным или крайне медленным.Рассмотренный подход с использованием двух регистров является «золотым стандартом» в типовых конфигурациях 1С (таких как ERP или УТ 11). Он позволяет четко разделять ответственность: склад отвечает за наличие коробок на полках, а регистр эксплуатации — за персональную ответственность сотрудников.