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