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