При работе со сложными периодическими расчетами в платформе 1С:Предприятие 7.7 разработчики часто сталкиваются с поведением, которое кажется нелогичным: при записи записи в журнал расчетов пользовательские реквизиты (например, «Тариф», «Количество») успешно сохраняются в базе данных, а системное поле Результат остается пустым или сбрасывается в ноль. При этом отладка показывает, что расчет внутри кода происходит корректно и переменная содержит нужное числовое значение.
В этой статье мы подробно разберем, почему возникает эта проблема, как функционирует расчетный механизм 1С 7.7, и рассмотрим три надежных способа исправления этой ошибки.
---
Для того чтобы понять суть проблемы, нам необходимо проанализировать архитектуру компоненты «Расчет» в 1С 7.7. В отличие от обычных справочников или документов, ЖурналРасчетов управляется специализированным расчетным движком платформы.
Реквизиты журнала расчетов делятся на две категории:
Результат, Рассчитана, Фиксирована, ПериодРегистрации и другие. Эти поля жестко завязаны на внутреннюю логику расчетного механизма.Когда вы выполняете в коде конструкцию:
ЖрнКв.УстановитьРеквизит("Результат", Результат);
ЖрнКв.УстановитьРеквизит("Рассчитана", 1);
ЖрнКв.Записать();
значение действительно записывается во временный буфер и даже может физически попасть в базу данных на короткое время. Однако, как только управление возвращается к документу или обработке проведения, система инициализирует общий расчет записей журнала расчетов (например, вызывается метод Рассчитать()).
В этот момент движок расчетов берет каждую запись, обращается к назначенному ей Виду Расчета (в нашем случае — «ПоКоличеству») и запускает его собственный Модуль расчета. Если этот модуль пуст, не содержит формулы или рассчитывает значение по-другому, платформа вычисляет пустой результат и перезаписывает им ваше сохраненное вручную значение. При этом «Тариф» и «Количество» остаются нетронутыми, так как расчетный движок их не обрабатывает.
---
В концепции 1С 7.7 расчеты не должны производиться в модуле проведения документа или во внешних глобальных процедурах. Правильный подход — разделить подготовку данных и сам расчет. Рассмотрим этот процесс по шагам.
Шаг 1: В глобальной процедуре или модуле документа мы только подготавливаем исходные данные для расчета. Мы записываем в журнал расчетов «Тариф» и «Количество», но сам результат не вычисляем и не пишем:
// В глобальном модуле или модуле документа
Процедура глЗаполнитьИсходныеДанныеТКО(ЛицСчет, ЖрнКв) Экспорт
Тариф = ЖрнКв.ВидТарифа.Тариф.Получить(глНачПериода);
КолПроп = ЛицСчет.КолПроп.Получить(глНачПериода);
// Записываем только исходные параметры для расчета
ЖрнКв.УстановитьРеквизит("Тариф", Тариф);
ЖрнКв.УстановитьРеквизит("Количество", КолПроп);
// Снимаем признак расчета, чтобы движок знал, что запись нужно пересчитать
ЖрнКв.УстановитьРеквизит("Рассчитана", 0);
ЖрнКв.Записать();
КонецПроцедуры
Шаг 2: Открываем в Конфигураторе свойства нашего вида расчета (в данном случае ПоКоличеству) и переходим в его Модуль расчета. Пишем там лаконичную и понятную платформе формулу:
// Модуль расчета вида расчета "ПоКоличеству"
Тариф = ПолучитьРеквизит("Тариф");
Количество = ПолучитьРеквизит("Количество");
// Записываем результат напрямую в системную переменную расчета
Результат = Тариф * Количество;
При таком подходе, когда система вызовет метод ЖрнКв.Рассчитать(), платформа сама обратится к модулю расчета, возьмет подготовленные вами «Тариф» и «Количество», перемножит их и запишет итоговое значение в поле Результат, установив флаг Рассчитана = 1.
---
Если проект слишком сложный, логика расчетов запутана и переносить код в модули расчета видов расчета сейчас нецелесообразно, можно принудительно запретить расчетному движку перезаписывать наше значение.
Для этого в платформе 1С 7.7 существует специальный системный флаг — Фиксирована. Если установить его значение в 1, движок расчетов при проходе по журналу расчетов будет полностью игнорировать эту запись, считая ее результат окончательным и не подлежащим автоматическому пересчету.
Давайте посмотрим, как модифицировать код исходной процедуры с использованием этого флага:
// Модифицированный фрагмент кода с фиксацией результата
Если ЖрнКв.ВидРасч = ВидРасчета.ПоКоличеству Тогда
Тариф = ЖрнКв.ВидТарифа.Тариф.Получить(глНачПериода);
КолПроп = ЛицСчет.КолПроп.Получить(глНачПериода);
Результат = КолПроп * Тариф;
ЖрнКв.УстановитьРеквизит("Тариф", Тариф);
ЖрнКв.УстановитьРеквизит("Количество", КолПроп);
ЖрнКв.УстановитьРеквизит("Результат", Результат);
// Обязательные системные реквизиты для ручной фиксации:
ЖрнКв.УстановитьРеквизит("Рассчитана", 1);
ЖрнКв.УстановитьРеквизит("Фиксирована", 1); // Запрещаем пересчет движком!
ЖрнКв.Записать();
КонецЕсли;
Важное предупреждение: Использование флага Фиксирована решает локальную проблему, но лишает вас преимуществ автоматического перерасчета. Если в будущем изменятся исходные данные (например, тариф задним числом), система не сможет автоматически пересчитать эту строку журнала расчетов, так как она заблокирована.
---
Проанализируем еще одну критическую ошибку, которая присутствует в исходном коде автора темы. В коде выполняется обход записей журнала расчетов с помощью цикла Пока ... ПолучитьЗапись() = 1, и внутри этого же цикла вызывается метод Записать() на том же самом объекте журнала:
// ВНИМАНИЕ: Так писать код в 1С 7.7 крайне не рекомендуется!
Пока ЖрнКв.ПолучитьЗапись() = 1 Цикл
...
ЖрнКв.Записать();
КонецЦикла;
В архитектуре баз данных DBF и SQL, используемых в 1С 7.7, позиционирование на записи (курсор выборки) происходит динамически. Модификация и сохранение текущей записи непосредственно в цикле выборки сбивает внутренний указатель платформы. Это приводит к непредсказуемым результатам: записи могут пропускаться, обрабатываться дважды или записываться с искажением системных полей (таких как Результат).
Для того чтобы избежать этой проблемы, мы должны использовать второй вспомогательный объект журнала расчетов, спозиционированный на ту же запись. Давайте перепишем процедуру правильно:
Процедура глРассчитатьЛицСчетТКО(ЛицСчет) Экспорт
Перем Сальдо, ВидСчетчика;
// Создаем основной объект для чтения и выборки записей
ЖрнКв = СоздатьОбъект("ЖурналРасчетов.КвартплатаТКО");
// Создаем вспомогательный объект исключительно для записи данных
ЖрнКвЗапись = СоздатьОбъект("ЖурналРасчетов.КвартплатаТКО");
ЖрнКв.ВыбратьЗаписиПоОбъекту(ЛицСчет.ТекущийЭлемент(), глНачПериода, глКонПериода);
Сальдо = глРассчитатьСальдоТКО(ЛицСчет);
Пока ЖрнКв.ПолучитьЗапись() = 1 Цикл
// Проверяем вид документа-регистратора
Если ЖрнКв.Документ.Вид() = "РасчетКвартплатыТКО" Тогда
// Исправляем синтаксическую ошибку автора (добавляем пропущенное "Если")
Если ЖрнКв.ВидРасч.ВходитВГруппу(ГруппаРасчетов.Начисления) = 1 Тогда
Если ЖрнКв.ВидРасч = ВидРасчета.ПоКоличеству Тогда
Тариф = ЖрнКв.ВидТарифа.Тариф.Получить(глНачПериода);
КолПроп = ЛицСчет.КолПроп.Получить(глНачПериода);
Результат = КолПроп * Тариф;
// Позиционируем объект записи на текущую запись выборки
ЖрнКвЗапись.НайтиЗапись(ЖрнКв.ТекущаяЗапись());
// Производим изменения через вспомогательный объект
ЖрнКвЗапись.УстановитьРеквизит("Тариф", Тариф);
ЖрнКвЗапись.УстановитьРеквизит("Количество", КолПроп);
ЖрнКвЗапись.УстановитьРеквизит("Результат", Результат);
ЖрнКвЗапись.УстановитьРеквизит("Рассчитана", 1);
ЖрнКвЗапись.Записать();
КонецЕсли;
КонецЕсли;
Если ЖрнКв.ВидРасч.ВходитВГруппу(ГруппаРасчетов.Сальдо) = 1 Тогда
Стр = ""; Кол = "";
Сальдо.НайтиЗначение(ЖрнКв.ВидТарифа, Стр, Кол);
// Позиционируем объект записи
ЖрнКвЗапись.НайтиЗапись(ЖрнКв.ТекущаяЗапись());
Если Стр = 0 Тогда
ЖрнКвЗапись.УстановитьРеквизит("Результат", 0);
ЖрнКвЗапись.УстановитьРеквизит("Рассчитана", 1);
ЖрнКвЗапись.Записать();
Иначе
Сальдо.ПолучитьСтрокуПоНомеру(Стр);
ЖрнКвЗапись.УстановитьРеквизит("Результат", Сальдо.СальдоК);
ЖрнКвЗапись.УстановитьРеквизит("Рассчитана", 1);
ЖрнКвЗапись.Записать();
КонецЕсли;
КонецЕсли;
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Использование раздельных объектов для обхода базы данных и непосредственной перезаписи реквизитов гарантирует стабильность работы СУБД, исключает появление взаимных блокировок транзакций и предотвращает порчу системных индексов журнала расчетов.