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