Почему появляется неверное развернутое сальдо по счету 60 в 1С при включении валютной суммы и как это исправить?

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

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

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

Выясняем причину: в чем кроется техническая суть проблемы?

Для того чтобы понять природу этой ошибки, нам необходимо заглянуть под капот платформы 1С и СУБД (будь то Microsoft SQL Server или PostgreSQL). В типовом плане счетов «1С:Бухгалтерии» для счета 60.01 (и большинства других субсчетов расчетов) признак валютного учета изначально выключен. Это означает, что система не должна хранить информацию о валюте для проводок по этому счету.

На уровне базы данных в таблице регистра бухгалтерии Хозрасчетный под каждое измерение выделяется соответствующая колонка. Для валютных счетов в полях ВалютаДт и ВалютаКт записывается ссылка на элемент справочника Валюты. Для невалютных счетов эти колонки должны оставаться абсолютно пустыми. На языке баз данных отсутствие значения обозначается как специальный маркер NULL.

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

Для платформы «1С:Предприятие 8» и SQL-сервера значения NULL (полное отсутствие данных в ячейке таблицы) и ПустаяСсылка (существующая, но незаполненная ссылка на объект справочника) — это два принципиально разных значения. Они занимают разный объем памяти и обрабатываются оптимизатором СУБД по-разному.

Когда мы формируем Оборотно-сальдовую ведомость с выключенным флагом «Валютная сумма», отчет группирует итоги только по ключевым субконто (Контрагенты, Договоры, Документы расчетов). В этот момент СУБД складывает все рублевые суммы вместе, и они успешно взаимозачитываются («схлопываются»), выдавая нулевой остаток.

Но как только мы устанавливаем флаг «Валютная сумма», платформа 1С автоматически добавляет в запрос группировку по разрезу Валюта. В результате СУБД разбивает наши рублевые проводки на две изолированные группы:

  1. Проводки, в которых поле валюты имеет значение NULL.
  2. Проводки, в которых в поле валюты записана ПустаяСсылка.

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

Почему в базе данных появляются пустые ссылки вместо NULL?

Проанализируем основные сценарии, при которых в регистр бухгалтерии проникает эта системная коллизия:

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

Шаг 1. Локализуем проблему с помощью консоли запросов

Прежде чем приступать к исправлению данных, нам необходимо выявить все документы, которые содержат некорректные записи в регистре. Для этого воспользуемся внешней обработкой «Консоль запросов». Нам нужно составить запрос к регистру бухгалтерии Хозрасчетный, который выберет проводки по невалютным счетам, где поле валюты заполнено пустой ссылкой (то есть значение не равно NULL).

Рассмотрим структуру этого запроса:


ВЫБРАТЬ
    Хозрасчетный.Регистратор КАК ДокументРегистратор,
    Хозрасчетный.Период КАК ДатаПроводки,
    Хозрасчетный.НомерСтроки КАК НомерСтроки,
    Хозрасчетный.СчетДт КАК СчетДебета,
    Хозрасчетный.СчетКт КАК СчетКредита,
    Хозрасчетный.ВалютаДт КАК ВалютаДебета,
    Хозрасчетный.ВалютаКт КАК ВалютаКредита
ИЗ
    РегистрБухгалтерии.Хозрасчетный КАК Хозрасчетный
ГДЕ
    (НЕ Хозрасчетный.СчетДт.Валютный И Хозрасчетный.ВалютаДт ЕСТЬ НЕ NULL)
    ИЛИ
    (НЕ Хозрасчетный.СчетКт.Валютный И Хозрасчетный.ВалютаКт ЕСТЬ НЕ NULL)

Проанализируем этот запрос. Мы обращаемся к таблице регистра и накладываем жесткий фильтр. Нас интересуют строки, где у счета дебета снят признак Валютный, но при этом поле ВалютаДт содержит любое заполненное значение (включая пустую ссылку, так как она не является NULL). Аналогичное условие накладывается на счет кредита и поле ВалютаКт. Выполнив этот запрос, мы получим точный список документов-нарушителей.

Шаг 2. Исправляем проводки с помощью внешней обработки

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

Важное правило: записывать набор записей необходимо строго с выключенным флагом обмена данными (НаборЗаписей.ОбменДанными.Загрузка = Ложь). В этом случае платформа самостоятельно проанализирует свойства счетов и принудительно преобразует очищенные поля в NULL на уровне базы данных.

Создадим внешнюю обработку и напишем следующий алгоритм в модуле объекта:


Процедура ИсправитьВалютноеСальдо() Экспорт

    // Шаг 1. Находим все регистраторы с некорректными записями валюты
    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ РАЗЛИЧНЫЕ
        |   Хозрасчетный.Регистратор КАК Регистратор
        |ИЗ
        |   РегистрБухгалтерии.Хозрасчетный КАК Хозрасчетный
        |ГДЕ
        |   (НЕ Хозрасчетный.СчетДт.Валютный И Хозрасчетный.ВалютаДт ЕСТЬ НЕ NULL)
        |   ИЛИ
        |   (НЕ Хозрасчетный.СчетКт.Валютный И Хозрасчетный.ВалютаКт ЕСТЬ НЕ NULL)";
        
    РезультатЗапроса = Запрос.Выполнить();
    Если РезультатЗапроса.Пустой() Тогда
        Сообщить("Проблемных записей в регистре бухгалтерии не обнаружено.");
        Возврат;
    КонецЕсли;
    
    Выборка = РезультатЗапроса.Выбрать();
    КоличествоИсправленных = 0;
    
    // Шаг 2. Обходим каждый документ и корректируем его движения
    Пока Выборка.Следующий() Цикл
        
        НаборЗаписей = РегистрыБухгалтерии.Хозрасчетный.СоздатьНаборЗаписей();
        НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
        НаборЗаписей.Прочитать();
        
        Модифицирован = Ложь;
        
        Для Каждого Запись Из НаборЗаписей Цикл
            
            // Обрабатываем Дебет
            Если НЕ Запись.СчетДт.Пустая() Тогда
                Если НЕ Запись.СчетДт.Валютный И Запись.ВалютаДт <> Неопределено Тогда
                    Запись.ВалютаДт = Неопределено;
                    Модифицирован = Истина;
                КонецЕсли;
            КонецЕсли;
            
            // Обрабатываем Кредит
            Если НЕ Запись.СчетКт.Пустая() Тогда
                Если НЕ Запись.СчетКт.Валютный И Запись.ВалютаКт <> Неопределено Тогда
                    Запись.ВалютаКт = Неопределено;
                    Модифицирован = Истина;
                КонецЕсли;
            КонецЕсли;
            
        КонецЦикла;
        
        // Шаг 3. Записываем набор, если были изменения
        Если Модифицирован Тогда
            НаборЗаписей.ОбменДанными.Загрузка = Ложь; // Позволяет платформе сделать приведение к NULL
            Попытка
                НаборЗаписей.Записать();
                КоличествоИсправленных = КоличествоИсправленных + 1;
            Исключение
                Сообщить("Не удалось записать движения по документу: " + СогласоватьИмяДокумента(Выборка.Регистратор) + ". Ошибка: " + ОписаниеОшибки());
            КонецПопытки;
        КонецЕсли;
        
    КонецЦикла;
    
    Сообщить("Успешно исправлено документов: " + Строка(КоличествоИсправленных));

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

Функция СогласоватьИмяДокумента(Ссылка)
    Возврат Строка(Ссылка) + " от " + Формат(Ссылка.Дата, "ДФ=dd.MM.yyyy");
КонецФункции

Запустив данную обработку, мы полностью очистим регистр от паразитных пустых ссылок в полях валюты для всех рублевых операций. После этого откроем Оборотно-сальдовую ведомость, переформируем ее с включенным показателем «Валютная сумма» и убедимся, что развернутое сальдо по счету 60 полностью исчезло.

Шаг 3. Предотвращаем появление проблемы в будущем

Для того чтобы заблокировать повторное появление пустых ссылок в невалютных счетах (например, при регулярных обменах или некорректной работе сторонних обработок), мы можем внедрить защитный код непосредственно в логику работы конфигурации. Лучше всего сделать это с помощью механизма расширений, перехватив событие ПередЗаписью регистра бухгалтерии Хозрасчетный.

Создадим расширение и добавим в модуль набора записей регистра следующий код:


&Перед("ПередЗаписью")
Процедура ЗащитаОтПустыхВалют_ПередЗаписью(Отказ, Замещение)
    
    // Если запись инициирована системными механизмами удаления, пропускаем
    Если ЭтотОбъект.Количество() = 0 Тогда
        Возврат;
    КонецЕсли;
    
    Для Каждого Запись Из ЭтотОбъект Цикл
        
        // Корректируем Дебет
        Если НЕ Запись.СчетДт.Пустая() Тогда
            Если НЕ Запись.СчетДт.Валютный Тогда
                Если Запись.ВалютаДт <> Неопределено Тогда
                    Запись.ВалютаДт = Неопределено;
                КонецЕсли;
            Иначе
                // Если счет валютный, а значение не заполнено, подставим пустую ссылку для порядка
                Если Запись.ВалютаДт = Неопределено Тогда
                    Запись.ВалютаДт = Справочники.Валюты.ПустаяСсылка();
                КонецЕсли;
            КонецЕсли;
        КонецЕсли;
        
        // Корректируем Кредит
        Если НЕ Запись.СчетКт.Пустая() Тогда
            Если НЕ Запись.СчетКт.Валютный Тогда
                Если Запись.ВалютаКт <> Неопределено Тогда
                    Запись.ВалютаКт = Неопределено;
                КонецЕсли;
            Иначе
                Если Запись.ВалютаКт = Неопределено Тогда
                    Запись.ВалютаКт = Справочники.Валюты.ПустаяСсылка();
                КонецЕсли;
            КонецЕсли;
        КонецЕсли;
        
    КонецЦикла;
    
КонецПроцедуры

Данный обработчик принудительно проверяет каждую проводку непосредственно перед сохранением в базу данных. Если транзакция пытается записать непустую структуру в поле валюты для невалютного счета, система сбрасывает ее в значение Неопределено, что на уровне СУБД запишется как правильный NULL. И наоборот, для валютных счетов при отсутствии значения будет установлена корректная типизированная пустая ссылка.

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

← На главную