Как распределить пересортицу по партиям по ФИФО с помощью запроса в 1С?

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

При ведении партионного учета на счете 10 нередко возникает ситуация, когда при инвентаризации выявляется пересортица: на остатках числится одна номенклатурная позиция, распределенная по нескольким партиям, а фактически необходимо перенести эти остатки на другие характеристики или номенклатурные позиции (например, костюмы разных размеров) с соблюдением очередности списания партий по методу ФИФО. Обычно такую задачу решают циклом на встроенном языке 1С, однако рассмотрим, как можно реализовать распределение исключительно средствами языка запросов, какие математические алгоритмы для этого применяются и с какими ограничениями платформы придется столкнуться.

Постановка задачи и логика распределения

Проанализируем исходные данные задачи. Пусть на складе числится исходная позиция в количестве 100 штук, распределенная по пяти партиям поступления:

  1. Партия 1 — 10 шт.
  2. Партия 2 — 20 шт.
  3. Партия 3 — 20 шт.
  4. Партия 4 — 20 шт.
  5. Партия 5 — 30 шт.

По результатам инвентаризации выявлена пересортица, которую требуется распределить:

  1. Костюм 42р — 20 шт.
  2. Костюм 43р — 30 шт.
  3. Костюм 44р — 50 шт.

Согласно принципу ФИФО, первые 20 шт. (Костюм 42р) должны забрать Партию 1 (10 шт.) и часть Партии 2 (10 шт.). Следующие 30 шт. (Костюм 43р) забирают оставшиеся 10 шт. из Партии 2 и 20 шт. из Партии 3. Оставшиеся 50 шт. (Костюм 44р) распределяются на Партию 4 (20 шт.) и Партию 5 (30 шт.).

Решение через расчет нарастающих итогов во временных таблицах

Рассмотрим практическую реализацию запроса, предложенную в обсуждении. Суть метода заключается в нумерации строк с помощью функции АВТОНОМЕРЗАПИСИ(), вычислении нарастающих сумм для партий источников и для строк получателей через самосоединение, а затем сведении полученных цепочек воедино.

Посмотрим на пример пакетного запроса:


ВЫБРАТЬ
	"Партия0" КАК Ссылка,
	1 КАК Кол,
	"МехКрокодила" КАК Аналитика1
ПОМЕСТИТЬ мТаб

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия1",
	9,
	"МехКрокодила"

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия2",
	20,
	"МехКрокодила"

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия3",
	20,
	"МехКрокодила"

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия4",
	20,
	"МехКрокодила"

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия5",
	30,
	"МехКрокодила"

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Партия6",
	30,
	"МехКрокодила"
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	"Шуба1" КАК Ссылка,
	20 КАК Кол
ПОМЕСТИТЬ мТабПолучатель

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Шуба2",
	30

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	"Шуба3",
	50
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТаб.Ссылка,
	мТаб.Кол,
	мТаб.Аналитика1,
	АВТОНОМЕРЗАПИСИ() КАК НН
ПОМЕСТИТЬ мТаб1
ИЗ
	мТаб КАК мТаб
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТаб1.Аналитика1,
	мТаб11.НН,
	мТаб1.НН КАК Отбор
ПОМЕСТИТЬ мТаб2
ИЗ
	мТаб1 КАК мТаб1
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТаб1 КАК мТаб11
		ПО мТаб1.Аналитика1 = мТаб11.Аналитика1
			И мТаб1.НН <= мТаб11.НН
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	СУММА(мТаб1.Кол) КАК Кол,
	мТаб1.Аналитика1,
	мТаб2.НН
ПОМЕСТИТЬ мТаб3
ИЗ
	мТаб2 КАК мТаб2
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТаб1 КАК мТаб1
		ПО мТаб2.Отбор = мТаб1.НН

СГРУППИРОВАТЬ ПО
	мТаб2.НН,
	мТаб1.Аналитика1
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТабПолучатель.Ссылка,
	мТабПолучатель.Кол,
	АВТОНОМЕРЗАПИСИ() КАК НН
ПОМЕСТИТЬ мТабПолучатель1
ИЗ
	мТабПолучатель КАК мТабПолучатель
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТабПолучатель11.НН КАК НН,
	мТабПолучатель1.НН КАК отбор
ПОМЕСТИТЬ мТабПолучатель2
ИЗ
	мТабПолучатель1 КАК мТабПолучатель1
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПолучатель1 КАК мТабПолучатель11
		ПО мТабПолучатель1.НН <= мТабПолучатель11.НН
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	СУММА(мТабПолучатель1.Кол) КАК Кол,
	мТабПолучатель2.НН
ПОМЕСТИТЬ мТабПолучатель3
ИЗ
	мТабПолучатель1 КАК мТабПолучатель1
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПолучатель2 КАК мТабПолучатель2
		ПО мТабПолучатель1.НН = мТабПолучатель2.отбор

СГРУППИРОВАТЬ ПО
	мТабПолучатель2.НН
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТаб3.НН КАК Материал,
	мТаб3.Кол КАК КолМатериал,
	мТабПолучатель3.НН КАК Изделие,
	мТабПолучатель3.Кол КАК КолИзделие
ПОМЕСТИТЬ мТабПеребор
ИЗ
	мТаб3 КАК мТаб3
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТаб1 КАК мТаб1
		ПО мТаб3.НН = мТаб1.НН
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПолучатель3 КАК мТабПолучатель3
		ПО (мТаб3.Кол - мТаб1.Кол < мТабПолучатель3.Кол)
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТабПеребор.Изделие,
	МАКСИМУМ(мТабПеребор.Материал) КАК Материал,
	СУММА(ВЫБОР
			КОГДА мТабПеребор.КолМатериал = мТабПеребор.КолИзделие
				ТОГДА 1
			ИНАЧЕ 0
		КОНЕЦ) КАК Корректировка
ПОМЕСТИТЬ мТабПеребор1
ИЗ
	мТабПеребор КАК мТабПеребор

СГРУППИРОВАТЬ ПО
	мТабПеребор.Изделие
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТабПеребор11.Изделие КАК Изделие,
	мТабПеребор1.Материал + мТабПеребор1.Корректировка КАК Материал
ПОМЕСТИТЬ мТабПеребор2
ИЗ
	мТабПеребор1 КАК мТабПеребор1
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПеребор1 КАК мТабПеребор11
		ПО (мТабПеребор1.Изделие = мТабПеребор11.Изделие - 1)

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
	1,
	0
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
	мТабПолучатель1.Ссылка,
	мТабПолучатель1.Кол,
	мТаб1.Ссылка КАК Партия,
	мТаб1.Аналитика1
ПОМЕСТИТЬ мТабИтого
ИЗ
	мТабПеребор КАК мТабПеребор
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПеребор2 КАК мТабПеребор2
		ПО мТабПеребор.Изделие = мТабПеребор2.Изделие
			И мТабПеребор.Материал >= мТабПеребор2.Материал
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТаб1 КАК мТаб1
		ПО мТабПеребор.Материал = мТаб1.НН
		ВНУТРЕННЕЕ СОЕДИНЕНИЕ мТабПолучатель1 КАК мТабПолучатель1
		ПО мТабПеребор.Изделие = мТабПолучатель1.НН
;

Математический метод пересечения отрезков

Более универсальным и классическим математическим способом распределения сущностей «многие ко многим» в запросах является метод пересечения полуинтервалов. Разберем его по шагам:

  1. Формирование координат источника: Для каждой партии источника рассчитывается интервал на числовой оси: [ОстатокДо, ОстатокПосле], где ОстатокПосле — это нарастающий итог количества по текущую партию включительно, а ОстатокДо = ОстатокПосле - КоличествоПартии.
  2. Формирование координат получателя: Аналогичный интервал строится для новых позиций (получателей): [ТребуетсяДо, ТребуетсяПосле].
  3. Поиск пересечений: Таблицы соединяются по условию пересечения двух интервалов:
    ОстатокДо < ТребуетсяПосле И ОстатокПосле > ТребуетсяДо.
  4. Расчет количества: Объем списания с конкретной партии под конкретного получателя равен длине отрезка их пересечения. На языке запросов это вычисляется как разница между минимумом правых границ и максимумом левых границ:
    
    (ВЫБОР КОГДА ОстатокПосле < ТребуетсяПосле ТОГДА ОстатокПосле ИНАЧЕ ТребуетсяПосле КОНЕЦ)
    - 
    (ВЫБОР КОГДА ОстатокДо > ТребуетсяДо ТОГДА ОстатокДо ИНАЧЕ ТребуетсяДо КОНЕЦ)
    

Такой подход избавляет от необходимости подбирать искусственные корректировки и одинаково стабильно работает как при дроблении одной партии на несколько позиций, так и при объединении нескольких партий в одну.

Ограничения и подводные камни запросного метода

Несмотря на техническую реализуемость, чисто запросный подход несет в себе серьезные архитектурные риски:

  1. Квадратичная вычислительная сложность: В языке запросов 1С отсутствуют стандартные оконные функции SQL вида SUM(...) OVER (ORDER BY ...). Эмуляция нарастающих итогов через самосоединение по условию <= порождает сложность $O(N^2)$. Если партий или строк пересорта станет много, нагрузка на СУБД резко возрастет, переполнится tempdb и возникнет риск зависания системы.
  2. Недетерминированность нумерации: Функция АВТОНОМЕРЗАПИСИ() во временных таблицах может выдавать непредсказуемый порядок строк в зависимости от планов выполнения СУБД, если исходные данные не упорядочены строго по уникальным полям. Нарушение порядка приведет к сбою логики ФИФО.
  3. Проблема копеек и себестоимости: На счете 10 ведется не только количественный, но и суммовой учет. При пропорциональном распределении сумм запросом неизбежно возникнут погрешности округления. Запрос не позволяет выполнить балансировку остатка (отнести накопившиеся копейки строго на последнюю списываемую партию), что приведет к «зависанию» копеечных суммовых остатков на счетах без процедурной постобработки.

Рекомендуемые альтернативы

Для практического применения в рабочих базах рекомендуется использовать следующие проверенные варианты:

  1. Процедурная постобработка на встроенном языке: Запросом выбираются только остатки партий (с сортировкой по дате документа партии). Далее в коде модуля организуется обход с двумя указателями. Такой алгоритм работает с линейной сложностью $O(N + M)$, потребляет минимум памяти и позволяет с точностью до копейки распределить себестоимость.
  2. Типовой документ «Комплектация номенклатуры»: В конфигурации «1С:Бухгалтерия» штатным механизмом для подобной операции является разукомплектация. Документ формирует движения по списанию исходных партий по ФИФО, а расчет себестоимости новых позиций корректно закрывается регламентной процедурой закрытия месяца («Корректировка стоимости номенклатуры»).
  3. Цепочка «Инвентаризация товаров» — «Списание» — «Оприходование»: Отражение пересортицы через фиксацию недостачи старой номенклатуры по текущей себестоимости и постановку на баланс нужных позиций.
← На главную