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