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

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

На предприятиях по производству алкогольной продукции, в частности в виноделии и производстве коньяков, возникает специфическая задача: необходимо распределять готовые купажи по бочкам. Эта операция имеет свои особенности, включая уникальность кода каждого купажа, запрет на их смешивание в одной бочке, необходимость сначала доливать уже начатые бочки, а затем использовать новые, а также критическую важность обеспечения одновременной работы нескольких операторов без коллизий, таких как «заливка с горкой» в одну и ту же бочку.

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

1. Понимание сущностей и их структуры в 1С

Для начала, мы определим основные объекты, которые нам потребуются в 1С, и их характеристики:

  1. Справочник "Бочки" или "Емкости":
  2. Справочник "Купажи" или "Номенклатура (с видом Купаж)":
  3. Регистр накопления "СодержимоеБочек" (или аналогичный):

2. Алгоритм распределения купажей по бочкам

Теперь, когда основные сущности определены, разработаем алгоритм, который будет работать по кнопке "Распределить" в документе разлива или распределения. Алгоритм должен учитывать следующие правила:

  1. Приоритет заполнения: Сначала заполняем неполные бочки с тем же купажом.
  2. Использование новых бочек: Если неполные бочки закончились, берем пустые.
  3. Один купаж в бочке: Купажи смешивать нельзя.
  4. Защита от "горки": Нельзя налить больше, чем позволяет объем бочки.
  5. Параллельная работа: Несколько операторов должны иметь возможность работать одновременно, не мешая друг другу.

Рассмотрим пошаговое описание алгоритма:

  1. Итерация по купажам: Мы начинаем обработку с каждого купажа, который указан в документе распределения. Для каждого купажа из документа:

    1. Поиск и заполнение неполных бочек:
      • Выполняем запрос к РегиструНакопления.СодержимоеБочек.Остатки. Запрос должен выбрать все бочки, которые содержат текущий купаж и при этом не заполнены до своего максимального объема (ОбъемКупажаОстаток < Бочки.Объем).
      • Важно: На этом этапе мы также должны исключить бочки, которые в данный момент заблокированы другими операторами (подробнее о блокировках ниже).
      • Полученную выборку бочек отсортируем. Наиболее эффективным будет сортировка по возрастанию свободного объема (Бочки.Объем - ОбъемКупажаОстаток), чтобы сначала заполнить бочки, которые почти полные.
      • Итерируем по отсортированным неполным бочкам:
        • Для каждой бочки определяем доступный свободный объем.
        • Наливаем в нее часть купажа из документа: либо весь оставшийся объем купажа, либо столько, сколько поместится в эту бочку (меньшее из двух значений).
        • Уменьшаем объем купажа, который нужно распределить.
        • Если бочка заполнилась (достигла своего максимального объема), переходим к следующей неполной бочке.
        • Если весь купаж из документа распределен, прекращаем цикл для текущего купажа.
    2. Использование новых (пустых) бочек:
      • Если после заполнения всех неполных бочек у нас еще остался объем текущего купажа, необходимо использовать пустые бочки.
      • Выполняем запрос к Справочнику.Бочки, чтобы найти все бочки, которые на данный момент считаются пустыми (т.е. отсутствуют в РегистреНакопления.СодержимоеБочек.Остатки) и не заблокированы другими операторами.
      • Также можем отсортировать эти бочки по объему (например, по возрастанию, чтобы сначала заполнить меньшие бочки, если это имеет значение для производственного процесса).
      • Итерируем по найденным пустым бочкам:
        • Наливаем в бочку объем купажа: либо весь оставшийся объем купажа, либо полный объем бочки (меньшее из двух значений).
        • Уменьшаем объем купажа, который нужно распределить.
        • Если купаж из документа распределен, прекращаем цикл.
    3. Контроль остатка: После обработки всех доступных неполных и пустых бочек, проверяем, остался ли еще нераспределенный объем купажа. Если да, то необходимо вывести сообщение пользователю о том, что не хватило емкостей, или предложить взять еще пустые бочки.

3. Механизм блокировки для параллельной работы операторов

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

Мы рекомендуем использовать отдельный РегистрСведений "ЗаблокированныеЕмкости".

  1. Структура регистра сведений "ЗаблокированныеЕмкости":
    • Измерения:
      • Емкость (тип: Ссылка на Справочник.Бочки) – уникально идентифицирует заблокированную бочку.
      • ДокументБлокировки (тип: Ссылка на тип документа, который осуществляет распределение, например, ДокументСсылка.РаспределениеКупажей) – указывает, какой именно документ заблокировал данную бочку.
    • Ресурсы: Могут быть необязательны, но можно добавить ДатаВремяБлокировки для контроля "зависших" блокировок.
  2. Принцип работы блокировки:
    1. Перед выбором бочки: Когда алгоритм распределения (описанный выше) выбирает очередную бочку для заполнения (будь то неполная или пустая), он должен сначала проверить, не заблокирована ли она.
    2. Попытка блокировки: Оператор, чей документ пытается "занять" бочку, должен предпринять попытку записать запись в РегистрСведений.ЗаблокированныеЕмкости для этой конкретной бочки, указав в ДокументБлокировки ссылку на свой документ.
      • Если запись успешно создана, это означает, что бочка свободна, и она теперь заблокирована данным документом. Оператор может продолжать работу с этой бочкой.
      • Если попытка записи вызывает исключение (например, из-за нарушения уникальности по измерению Емкость, если у регистра установлен ключ уникальности, или из-за конфликта при попытке добавить запись, когда она уже есть), это означает, что бочка уже заблокирована другим документом/оператором. В этом случае текущий алгоритм должен пропустить эту бочку и перейти к следующей доступной.
    3. Снятие блокировки:
      • После успешного проведения документа распределения, все записи в РегистреСведений.ЗаблокированныеЕмкости, связанные с этим документом, должны быть удалены. Это происходит в модуле проведения документа.
      • Если оператор закрыл документ, не проведя его, или сеанс работы завершился аварийно, записи о блокировке могут "зависнуть". Для решения этой проблемы рекомендуется создать регламентное задание, которое будет периодически проверять РегистрСведений.ЗаблокированныеЕмкости. Если ДокументБлокировки давно неактивен, не проведен, или его сеанс пользователя отсутствует, регламентное задание может очищать такие "зависшие" записи, освобождая бочки.

4. Сквозная прослеживаемость для РАР

Для предприятий, работающих с алкогольной продукцией, прослеживаемость является ключевым требованием РАР, особенно для возврата акцизов. Если купажи, сделанные из своего винограда, позволяют вернуть акциз, а из покупного – нет, то необходимо обеспечить четкую фиксацию этой информации.

Рекомендуется использовать механизм серий номенклатуры в 1С. Каждая серия может быть уникальным идентификатором для конкретной партии купажа и содержать следующие данные:

При поступлении купажа в систему ему присваивается серия, а при розливе в бочки эта серия фиксируется в РегистреНакопления.СодержимоеБочек в качестве дополнительной аналитики, что позволяет в любой момент узнать историю содержимого каждой бочки.

5. Технические объекты и методы 1С для реализации

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

Примерный алгоритм на 1С (псевдокод)

Представим себе, что у нас есть документ Документ.РаспределениеКупажей с табличной частью КупажиКРаспределению, где указаны Купаж и Объем.


// В модуле объекта документа "РаспределениеКупажей" или в отдельной обработке

Процедура РаспределитьКупажиПоБочкам()

    // Начинаем транзакцию для обеспечения атомарности
    НачатьТранзакцию();

    Попытка
        Для Каждого СтрокаКупаж Из ЭтотОбъект.КупажиКРаспределению Цикл

            ТекущийКупаж = СтрокаКупаж.Купаж;
            ОбъемКРаспределению = СтрокаКупаж.Объем;

            // 1. Сначала заполняем неполные бочки с этим купажом
            // Запрос для неполных бочек, не заблокированных другими документами
            ЗапросНеполные = Новый Запрос;
            ЗапросНеполные.Текст =
                "ВЫБРАТЬ
                |    СодержимоеБочекОстатки.Бочка КАК Бочка,
                |    СодержимоеБочекОстатки.ОбъемКупажаОстаток КАК ОбъемВБочке,
                |    Бочки.Объем КАК ОбъемБочки
                |ИЗ
                |    РегистрНакопления.СодержимоеБочек.Остатки(
                |            ,
                |            Купаж = &Купаж) КАК СодержимоеБочекОстатки
                |        ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Бочки КАК Бочки
                |        ПО СодержимоеБочекОстатки.Бочка = Бочки.Ссылка
                |ГДЕ
                |    СодержимоеБочекОстатки.ОбъемКупажаОстаток < Бочки.Объем // Неполные
                |    И НЕ СодержимоеБочекОстатки.Бочка В
                |            (ВЫБРАТЬ
                |                ЗаблокированныеЕмкости.Емкость
                |            ИЗ
                |                РегистрСведений.ЗаблокированныеЕмкости КАК ЗаблокированныеЕмкости
                |            ГДЕ
                |                ЗаблокированныеЕмкости.ДокументБлокировки <> &СсылкаТекущегоДокумента) // Не заблокированы другим
                |
                |УПОРЯДОЧИТЬ ПО
                |    (Бочки.Объем - СодержимоеБочекОстатки.ОбъемКупажаОстаток) ВОЗРАСТАНИЮ"; // Сначала наименее свободные

            ЗапросНеполные.УстановитьПараметр("Купаж", ТекущийКупаж);
            ЗапросНеполные.УстановитьПараметр("СсылкаТекущегоДокумента", ЭтотОбъект.Ссылка);

            ВыборкаНеполные = ЗапросНеполные.Выполнить().Выбрать();

            Пока ОбъемКРаспределению > 0 И ВыборкаНеполные.Следующий() Цикл
                БочкаТекущая = ВыборкаНеполные.Бочка;
                ОбъемВБочке = ВыборкаНеполные.ОбъемВБочке;
                ОбъемБочки = ВыборкаНеполные.ОбъемБочки;

                СвободныйОбъем = ОбъемБочки - ОбъемВБочке;
                Если СвободныйОбъем > 0 Тогда
                    ОбъемЗаливки = Мин(ОбъемКРаспределению, СвободныйОбъем);

                    // *********** Попытка блокировки конкретной бочки через РС ***********
                    Если Не УстановитьБлокировкуБочки(БочкаТекущая, ЭтотОбъект.Ссылка) Тогда
                        Продолжить; // Бочка заблокирована другим оператором, переходим к следующей
                    КонецЕсли;

                    // Создаем движения по РегиструНакопления "СодержимоеБочек"
                    Движение = ЭтотОбъект.Движения.СодержимоеБочек.Добавить();
                    Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
                    Движение.Период = ЭтотОбъект.Дата;
                    Движение.Бочка = БочкаТекущая;
                    Движение.Купаж = ТекущийКупаж;
                    Движение.ОбъемКупажа = ОбъемЗаливки;

                    ОбъемКРаспределению = ОбъемКРаспределению - ОбъемЗаливки;
                КонецЕсли;
            КонецЦикла;

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

                ЗапросПустые.УстановитьПараметр("Купаж", ТекущийКупаж); // Учитываем, что пустая бочка не должна содержать *другой* купаж
                ЗапросПустые.УстановитьПараметр("СсылкаТекущегоДокумента", ЭтотОбъект.Ссылка);

                ВыборкаПустые = ЗапросПустые.Выполнить().Выбрать();

                Пока ОбъемКРаспределению > 0 И ВыборкаПустые.Следующий() Цикл
                    БочкаТекущая = ВыборкаПустые.Бочка;
                    ОбъемБочки = ВыборкаПустые.ОбъемБочки;

                    // *********** Попытка блокировки конкретной бочки через РС ***********
                    Если Не УстановитьБлокировкуБочки(БочкаТекущая, ЭтотОбъект.Ссылка) Тогда
                        Продолжить; // Бочка заблокирована другим оператором, переходим к следующей
                    КонецЕсли;

                    ОбъемЗаливки = Мин(ОбъемКРаспределению, ОбъемБочки);

                    // Создаем движения по РегиструНакопления "СодержимоеБочек"
                    Движение = ЭтотОбъект.Движения.СодержимоеБочек.Добавить();
                    Движение.ВидДвижения = ВидДвиженияНакопления.Приход;
                    Движение.Период = ЭтотОбъект.Дата;
                    Движение.Бочка = БочкаТекущая;
                    Движение.Купаж = ТекущийКупаж;
                    Движение.ОбъемКупажа = ОбъемЗаливки;

                    ОбъемКРаспределению = ОбъемКРаспределению - ОбъемЗаливки;
                КонецЦикл;
            КонецЕсли;

            // Если купаж все еще остался, возможно, нужна новая бочка или сообщение об ошибке
            Если ОбъемКРаспределению > 0 Тогда
                Сообщить("Не удалось полностью распределить купаж " + ТекущийКупаж.Наименование + ". Остаток: " + ОбъемКРаспределению, СтатусСообщения.Важное);
                // Можно записать в отдельную ТЧ документа нераспределенный остаток
            КонецЕсли;

        КонецЦикла; // Для Каждого СтрокаКупаж Из ДокументРаспределение.Купажи

        ЗафиксироватьТранзакцию();
        Сообщить("Распределение успешно выполнено.");

    Исключение
        ОтменитьТранзакцию();
        Сообщить("Ошибка при распределении: " + ОписаниеОшибки(), СтатусСообщения.Ошибка);
        // Важно: в случае ошибки в транзакции, все блокировки РС также будут отменены
        // Если же блокировка производилась вне транзакции, ее придется снимать отдельно
    КонецПопытки;

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

// Вспомогательная функция для установки блокировки бочки в РегистреСведений
Функция УстановитьБлокировкуБочки(СсылкаБочки, СсылкаДокумента) Экспорт
    Попытка
        НоваяЗапись = РегистрыСведений.ЗаблокированныеЕмкости.СоздатьМенеджерЗаписи();
        НоваяЗапись.Емкость = СсылкаБочки;
        НоваяЗапись.ДокументБлокировки = СсылкаДокумента;
        НоваяЗапись.Записать(); // Если запись с такой Емкостью уже есть, вызовет исключение при попытке записать.
        Возврат Истина; // Блокировка установлена успешно
    Исключение
        // Сообщить("Бочка " + СсылкаБочки.Наименование + " уже заблокирована другим документом."); // Для отладки
        Возврат Ложь; // Бочка уже заблокирована
    КонецПопытки;
КонецФункции

// Процедура для снятия блокировок, вызывается при проведении документа или отмене проведения
Процедура СнятьБлокировкиБочек(СсылкаДокумента) Экспорт
    НаборЗаписей = РегистрыСведений.ЗаблокированныеЕмкости.СоздатьНаборЗаписей();
    НаборЗаписей.Отбор.ДокументБлокировки.Установить(СсылкаДокумента);
    НаборЗаписей.Очистить(); // Удаляем все записи, относящиеся к текущему документу
    НаборЗаписей.Записать();
КонецПроцедуры

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

    Запрос.УстановитьПараметр("ДатаДокумента", ТекущаяДата() - 60*60*24); // Документы старше суток

    Выборка = Запрос.Выполнить().Выбрать();
    НаборЗаписей = РегистрыСведений.ЗаблокированныеЕмкости.СоздатьНаборЗаписей();

    Пока Выборка.Следующий() Цикл
        // Дополнительная проверка, например, активен ли пользователь, создавший документ
        // (может быть сложно, но полезно для предотвращения ошибочной разблокировки)
        // Для простоты здесь просто очищаем старые непроведенные
        НаборЗаписей.Отбор.ДокументБлокировки.Установить(Выборка.ДокументБлокировки);
        НаборЗаписей.Очистить();
        НаборЗаписей.Записать();
    КонецЦикла;
КонецПроцедуры

Заключение

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

← На главную