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

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

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

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

Постановка задачи

Представим, что у нас есть документ «Реализация товаров и услуг», который делает движения по счетам учета ТМЦ (например, 41.01). Нам необходимо в программном коде получить остаток товара на складе на текущий момент (или на конец дня), но при этом проигнорировать списание, сделанное этим конкретным документом.

Просто получить остатки на момент «до документа» (используя Граница) не всегда подходит, так как после этого документа могли быть введены другие документы, движения которых мы хотим учесть. Нам нужно «вырезать» из потока времени влияние только одного регистратора.

Мы рассмотрим два принципиально разных способа решения:

  1. Метод временной отмены в транзакции — эмуляция отмены проведения.
  2. Математический метод — корректировка результата запроса путем вычитания движений.

Способ 1: Использование транзакции и отката изменений

Этот способ основан на механизме транзакций базы данных. Транзакция позволяет выполнить ряд действий и, если они не зафиксированы, полностью отменить их, вернув базу в исходное состояние. Важнейшая особенность платформы 1С заключается в том, что переменные встроенного языка (ТаблицыЗначений, Структуры, Массивы), заполненные внутри транзакции, не очищаются при ее отмене. Откатываются только изменения в базе данных.

Давайте разберем алгоритм действий по шагам:

  1. Начинаем транзакцию.
  2. Получаем набор записей регистра бухгалтерии по нашему документу.
  3. Очищаем этот набор (или отключаем активность записей) и записываем его. Теперь для базы данных движений нет.
  4. Выполняем запрос к виртуальной таблице остатков. Платформа вернет данные без учета нашего документа.
  5. Выгружаем результат запроса в переменную (например, в ТаблицаЗначений).
  6. Отменяем транзакцию. Движения документа в базе восстанавливаются.
  7. Работаем с сохраненной таблицей значений.

Рассмотрим, как это выглядит в коде. Предположим, у нас есть ссылка на документ в переменной СсылкаНаДокумент.


// 1. Начинаем транзакцию
НачатьТранзакцию();

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

// Теперь в переменной ТаблицаРеальныхОстатков у нас данные БЕЗ учета документа
// Можно обрабатывать их дальше

Преимущества метода:

Недостатки и риски:

Способ 2: Математический метод (Запрос с исключением)

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

В терминах запросов 1С это означает:

  1. Выбрать общие остатки из таблицы РегистрБухгалтерии.Хозрасчетный.Остатки.
  2. Выбрать обороты (движения) только по нужному документу из таблицы РегистрБухгалтерии.Хозрасчетный.Обороты (или основной таблицы движений).
  3. Инвертировать знаки движений документа (умножить на -1). То есть, если документ списал товар (Кредит), мы должны добавить это количество обратно, чтобы узнать, сколько было бы без списания.
  4. Сгруппировать результаты.

Давайте посмотрим, как реализовать это в одном запросе, используя оператор ОБЪЕДИНИТЬ ВСЕ.


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

Запрос.УстановитьПараметр("МоментВремени", Новый Граница(КонецДня(ТекущаяДата()), ВидГраницы.Включая)); // или просто дата
Запрос.УстановитьПараметр("Организация", Организация);
Запрос.УстановитьПараметр("Счет", СчетУчета);
Запрос.УстановитьПараметр("ИсключаемыйДокумент", СсылкаНаДокумент);

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

Разберем логику запроса:

  1. Первая часть (до ОБЪЕДИНИТЬ ВСЕ) получает реальные остатки из базы. Если документ проведен, его движения уже «сидят» в этих цифрах.
  2. Вторая часть выбирает движения только нашего документа. Виртуальная таблица Обороты с периодичностью Запись позволяет отфильтровать данные по конкретному Регистратор.
  3. Мы берем выражение СуммаОборотДт - СуммаОборотКт. Это «вклад» документа в остаток. Если документ увеличил остаток, число будет положительным. Если уменьшил — отрицательным.
  4. Ставим знак «минус» перед выражением. Это математически аннулирует влияние документа.
  5. Суммируем (сворачиваем) таблицу.

Преимущества:

Важный нюанс: Партионный учет и себестоимость

Нужно быть очень осторожным при использовании второго (математического) метода, если речь идет о сложных механизмах расчета себестоимости, таких как ФИФО (скользящая оценка) или ЛИФО.

Если в вашей конфигурации (например, БП 3.0) на счете 41 ведется партионный учет (субконто «Партии»), то простое вычитание сумм может дать некорректную себестоимость единицы товара.

Почему так происходит? Представьте ситуацию:

  1. Была Партия 1 (дешевая) и Партия 2 (дорогая).
  2. Наш исключаемый документ списал Партию 1.
  3. Следующий документ списал Партию 2.

Если мы математически «вернем» количество и сумму документа, то остаток по количеству будет верным. Но структура партий в базе уже изменилась — следующие документы «подтянули» другие партии. Математика вернет нам суммы списанных партий, но не пересчитает влияние этого на последующие документы.

Поэтому:

Использование объекта «Граница»

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

Достаточно запросить остатки на момент этого документа, но с видом границы Исключая.


// Получаем момент времени документа
МоментДокумента = СсылкаНаДокумент.МоментВремени();

// Создаем границу, ИСКЛЮЧАЮЩУЮ этот момент
ГраницаРасчета = Новый Граница(МоментДокумента, ВидГраницы.Исключая);

Запрос.УстановитьПараметр("Момент", ГраницаРасчета);
// Запрос к Остаткам...

Этот код вернет остатки ровно перед тем, как документ сделал свои движения. Это работает только если нам не нужно учитывать документы, которые стоят на оси времени после нашего исключаемого документа.

Выводы

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

  1. Математический метод (Запрос с UNION ALL) — рекомендуется как основной. Он не блокирует базу и работает быстро. Идеален для складского учета (в количестве) и взаиморасчетов.
  2. Метод транзакции — используйте его только в исключительных случаях, если логика получения остатков настолько сложна, что её невозможно воспроизвести запросом (например, задействованы сложные алгоритмы программного кода при расчете). Помните о риске блокировок.

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

← На главную