Как реализовать автоматический расчет и списание ингредиентов на заказанные блюда в 1С?

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

Разработка решений для сферы общественного питания (рестораны, кафе, столовые) на платформе 1С всегда связана с решением классической задачи: как правильно списать сырье и ингредиенты со склада, когда клиент заказывает готовое блюдо. Готовое блюдо (например, «Борщ» или «Салат Цезарь») состоит из множества составляющих (мясо, овощи, соусы), учет которых ведется на складе в килограммах, штуках или литрах. В то же время клиент заказывает блюда порционно.

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

Методология учета: почему нельзя списывать ингредиенты напрямую «Заказом» в реальном бизнесе?

Прежде чем перейти к написанию кода, проанализируем ситуацию с точки зрения бизнес-логики. На форумах часто возникает спор: должен ли документ ЗаказПокупателя производить фактическое списание остатков из регистра накопления? В реальном производстве — нет.

Рассмотрим причины, почему прямое списание заказом считается методологической ошибкой:

  1. Планы и факты: Заказ — это плановый документ. Клиент может изменить состав заказа, отказаться от блюда или попросить заменить один ингредиент на другой (например, убрать лук). Если продукты спишутся в момент проведения заказа, остатки на складе перестанут соответствовать реальности.
  2. Процесс приготовления: В общепите блюда готовятся порционно или крупными партиями на всю смену. Списание сырья обычно оформляется документами класса ВыпускПродукции, ОтчетПроизводстваЗаСмену или при закрытии кассовой смены на основании чеков общепита.

Тем не менее, в рамках учебных задач (курсовых работ) или в упрощенных системах автоматизации часто требуется реализовать списание «здесь и сейчас» прямо при оформлении заказа. Мы рассмотрим техническую реализацию именно такой схемы, попутно исправив критические ошибки программирования.

Анализ кодовых ошибок: почему код автора не работает?

Посмотрим на исходный код, который попытался написать автор темы для автоматического заполнения состава ингредиентов:


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

В этом небольшом фрагменте кода скрыто сразу три серьезных проблемы, которые разберем подробнее:

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

Ошибка 2. Запрос в цикле (Главный антипаттерн платформы 1С).
Конструкция Для Каждого... Цикл Запрос.Выполнить() обращается к базе данных на каждой итерации. Если в заказе будет 10 или 20 блюд, сервер сделает 10 или 20 отдельных транзакций к СУБД. Это критически снижает производительность системы. Запрос всегда должен быть один, а данные для него нужно передавать пакетно.

Ошибка 3. Отсутствие расчета итогового количества.
Код просто переносит норму расхода на одну порцию из справочника в документ. Если клиент заказал 5 порций блюда, программа все равно запишет расход как для 1 порции, так как в коде отсутствует операция умножения количества блюд на норму расхода ингредиента.

Решаем задачу правильно: пошаговая инструкция

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

Шаг 1. Подготовка структуры метаданных

Для корректной работы нашего алгоритма структура объектов в конфигураторе должна выглядеть следующим образом:

  1. Справочник Номенклатура (или Продукты):
    • Имеет табличную часть Состав (колонки: Сырье с типом ссылки на этот же справочник, и Количество с типом Число). Это наша технологическая карта блюда.
  2. Документ ЗаказПокупателя:
    • Табличная часть СоставЗаказа (колонки: Блюдо с типом СправочникСсылка.Номенклатура, и Количество с типом Число).
    • Вторая табличная часть Состав (колонки: Сырье с типом СправочникСсылка.Номенклатура, и Количество с типом Число). Сюда будут сворачиваться все ингредиенты.

Шаг 2. Написание оптимизированного заполнения состава на сервере

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

Рассмотрим реализацию этой процедуры на сервере:


&НаСервере
Процедура ЗаполнитьИнгредиентыНаСервере()
    
    // 1. Очищаем табличную часть получателя ингредиентов
    Объект.Состав.Очистить();
    
    // Проверим, заполнена ли исходная таблица заказа
    Если Объект.СоставЗаказа.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    // 2. Выгружаем данные ТЧ документа в Таблицу Значений для передачи в параметры запроса
    ТаблицаЗаказа = Объект.СоставЗаказа.Выгрузить();
    
    // 3. Создаем и настраиваем запрос
    Запрос = Новый Запрос;
    Запрос.УстановитьПараметр("ТаблицаЗаказа", ТаблицаЗаказа);
    
    // Запрос выполняет соединение ТЗ с табличной частью справочника "Номенклатура"
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    ВТ_Заказ.Блюдо КАК Блюдо,
        |    ВТ_Заказ.Количество КАК КоличествоБлюда
        |ПОМЕСТИТЬ ВТ_ИсходныйЗаказ
        |ИЗ
        |    &ТаблицаЗаказа КАК ВТ_Заказ
        |;
        |
        |////////////////////////////////////////////////////////////////////////////////
        |ВЫБРАТЬ
        |    Рецепты.Сырье КАК Сырье,
        |    СУММА(ВТ_ИсходныйЗаказ.КоличествоБлюда * Рецепты.Количество) КАК Количество
        |ИЗ
        |    ВТ_ИсходныйЗаказ КАК ВТ_ИсходныйЗаказ
        |    ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Номенклатура.Состав КАК Рецепты
        |        ПО ВТ_ИсходныйЗаказ.Блюдо = Рецепты.Ссылка
        |ГРУППИРОВАТЬ ПО
        |    Рецепты.Сырье";
    
    // 4. Выполняем запрос и выгружаем результат в ТЧ "Состав"
    РезультатЗапроса = Запрос.Выполнить();
    Если Не РезультатЗапроса.Пустой() Тогда
        Объект.Состав.Загрузить(РезультатЗапроса.Выгрузить());
    КонецЕсли;
    
КонецПроцедуры

Проанализируем работу запроса:

Шаг 3. Проведение документа по регистру накопления остатков

Заполнение табличной части на форме — это лишь визуальное представление для пользователя. Чтобы продукты реально списались со склада, документ должен сформировать движения по регистру накопления (например, по регистру ОстаткиМатериалов).

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


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

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

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

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

← На главную