Начинающие разработчики в платформе 1С:Предприятие часто сталкиваются с интуитивным, но ошибочным желанием: добавить в справочник Товары реквизит Количество и уменьшать его значение при проведении каждого документа расхода. Разберем детально, почему такой подход ведет к критическим ошибкам в системе, как устроена правильная архитектура учета остатков и как реализовать ее по шагам.
Проанализируем ключевые архитектурные проблемы, которые неизбежно возникнут при попытке списывать количество прямо из справочника:
Рассмотрим, как выглядит изменение реквизита справочника напрямую в модуле документа, и выясним причину, почему так делать не рекомендуется:
Процедура ПередЗаписью(Отказ, РежимЗаписи, РежимПроведения)
СпрОбъект = ЭтотОбъект.Товар.ПолучитьОбъект();
Если РежимЗаписи = РежимЗаписиДокумента.Проведение И ЭтотОбъект.Проведен = Ложь Тогда
СпрОбъект.Количество = СпрОбъект.Количество - ЭтотОбъект.Количество;
СпрОбъект.Записать();
ИначеЕсли РежимЗаписи = РежимЗаписиДокумента.ОтменаПроведения Тогда
СпрОбъект.Количество = СпрОбъект.Количество + ЭтотОбъект.Количество;
СпрОбъект.Записать();
КонецЕсли;
КонецПроцедуры
Посмотрим на недостатки данного кода: если документ перезаписать без проведения несколько раз или изменить в нем номенклатуру, алгоритм начнет накапливать системные погрешности. Поэтому стандартная парадигма 1С всегда строится по схеме: Документы -> Регистры -> Отчеты.
Разберем по шагам правильное создание структуры складского учета в конфигураторе:
Товары. Хранит исключительно статическую информацию о позиции (наименование, артикул, единицу измерения). Никаких реквизитов остатков в нем быть не должно.ОстаткиТоваров. Устанавливаем вид регистра: Остатки.
Товар (тип СправочникСсылка.Товары).
Количество (тип Число, длина 15, точность 3).ПриходТовара (или ПоступлениеТоваров) и документ РасходТовара (или РеализацияТоваров). Для каждого документа на вкладке Движения отмечаем регистр накопления ОстаткиТоваров.Рассмотрим заполнение движений в модуле документа расхода в обработчике ОбработкаПроведения. Движения формируются в контексте транзакции проведения:
Процедура ОбработкаПроведения(Отказ, Режим)
// Регистрируем необходимость записи движений по регистру
Движения.ОстаткиТоваров.Записывать = Истина;
// Формируем расходное движение
Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Товар = ЭтотОбъект.Товар;
Движение.Количество = ЭтотОбъект.Количество;
КонецПроцедуры
Для приходного документа используется метод ДобавитьПриход(). Платформа самостоятельно управляет итогами регистра: пересчитывает актуальные остатки и очищает записи при отмене проведения документа.
Проанализируем ситуацию, когда необходимо запретить списание товара, если его физически нет на складе. Для этого используется механизм контроля остатков с блокировкой данных:
Процедура ОбработкаПроведения(Отказ, Режим)
// Устанавливаем управляемую блокировку на данные регистра
Блокировка = Новый БлокировкаДанных;
ЭлементБлокировки = Блокировка.Добавить("РегистрНакопления.ОстаткиТоваров");
ЭлементБлокировки.УстановитьЗначение("Товар", ЭтотОбъект.Товар);
ЭлементБлокировки.Режим = РежимБлокировкиДанных.Исключительный;
Блокировка.Заблокировать();
// Записываем движение расхода
Движения.ОстаткиТоваров.Записывать = Истина;
Движение = Движения.ОстаткиТоваров.ДобавитьРасход();
Движение.Период = Дата;
Движение.Товар = ЭтотОбъект.Товар;
Движение.Количество = ЭтотОбъект.Количество;
Движения.ОстаткиТоваров.Записать();
// Проверяем, не ушел ли остаток в минус после проведения
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваровОстатки.Товар КАК Товар,
| ОстаткиТоваровОстатки.КоличествоОстаток КАК КоличествоОстаток
|ИЗ
| РегистрНакопления.ОстаткиТоваров.Остатки(&МоментВремени, Товар = &Товар) КАК ОстаткиТоваровОстатки
|ГДЕ
| ОстаткиТоваровОстатки.КоличествоОстаток < 0";
Запрос.УстановитьПараметр("МоментВремени", Новый Граница(МоментВремени(), ГраницаВключаяПоследнееЗначение));
Запрос.УстановитьПараметр("Товар", ЭтотОбъект.Товар);
Результат = Запрос.Выполнить();
Если НЕ Результат.Пустой() Тогда
Отказ = Истина;
Выборка = Результат.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщение = Новый СообщениеПользователю;
Сообщение.Текст = "Недостаточно товара " + Выборка.Товар + ". Дефицит: " + (-Выборка.КоличествоОстаток);
Сообщение.Сообщить();
КонецЦикла;
КонецЕсли;
КонецПроцедуры
Если пользователю необходимо видеть текущий остаток прямо в форме списка справочника Товары, сам реквизит в структуру объекта не добавляется. Для этого настраивается свойство ДинамическийСписок формы списка: включается произвольный запрос, в котором таблица Справочник.Товары связывается левым соединением с виртуальной таблицей РегистрНакопления.ОстаткиТоваров.Остатки().
Также важно разделять складские документы и документы намерений: например, документ Заказ не списывает физический остаток, а резервирует товар через отдельный регистр ЗаказыПокупателей. Фактическое списание со склада оформляется отдельным документом отгрузки.