Загрузка данных


Занятие 13
После изучения предыдущего занятия вы уже достаточно хорошо
знакомы с языком запросов, и мы наконец-то можем приступить к
одному из самых важных занятий нашей книги – к оптимизации
документа ОказаниеУслуги и, в частности, к полному изменению его
обработчика события ОбработкаПроведения.
«Зачем это нужно?» – можете спросить вы. Тому есть три причины.
Во-первых, в обработчике события ОбработкаПроведения мы используем обращение к реквизиту ВидНоменклатуры справочника Номенклатура через точку. Такое обращение может сильно замедлить
скорость выполнения процедуры при больших объемах табличной
части документа.
Во-вторых, руководство ООО «На все руки мастер» решило наконецто завершить «эксперименты» по ручному вводу стоимости
расходуемых материалов и перейти на автоматический расчет
стоимости расходуемых материалов «по среднему».
В-третьих, при проведении документа ОказаниеУслуги необходимо
контролировать остатки расходуемых товаров на складе. Если товаров
не хватает, выдавать предупреждение и не проводить документ.
Поэтому изменения, вносимые нами в документ ОказаниеУслуги,
будут преследовать три цели:
 повышение скорости выполнения процедуры;
 автоматическое определение стоимости расходуемых материалов
при проведении документа;
 разделение алгоритма проведения документа на оперативный и
неоперативный режимы и контроль остатков в случае оперативного проведения документа.
Прежде чем мы приступим непосредственно к каким-либо действиям,
следует сказать несколько слов об особенностях хранения и использования ссылочных данных в системе «1С:Предприятие».
Теория: особенности использования
ссылочных данных
В этом разделе мы поговорим об особенностях использования
ссылочных данных, так как, используя доступ к этим данным с
помощью запросов, мы можем значительно повысить скорость
проведения документа и оптимизировать этот процесс.
Термином «ссылочные данные» мы будем обозначать данные, хранящиеся в базе данных, доступ к которым возможен при помощи
объектов встроенного языка вида Ссылка: СправочникСсылка.<имя>,
ДокументСсылка.<имя> и т.д. Для того чтобы дальнейшее изложение
было понятнее, мы построим объяснение на примере получения
ссылки на вид номенклатуры при проведении документа ОказаниеУслуги.
Не все данные, хранящиеся в базе данных, являются ссылочными. Это
связано с тем, что в модели данных «1С:Предприятия» суще-ствует
деление на данные, представляющие объектные сущности
(справочники, планы счетов, документы и т.д.), и данные, представляющие необъектные сущности (регистры сведений, регистры накопления и т.д.).
С точки зрения платформы некоторая совокупность объектных
данных определяется не только значениями своих полей, но и самим
фактом своего существования. Другими словами, удалив из базы
некоторую совокупность объектных данных, мы не сможем вернуть
систему в то же состояние, которое было до удаления. Даже если мы
заново создадим ту же самую совокупность объектных данных с теми
же самыми значениями полей, с точки зрения системы это будет
ДРУГАЯ совокупность объектных данных.
Каждую такую совокупность объектных данных, уникальную с точки
зрения системы, называют объектом базы данных.
Для того чтобы система могла отличить один объект базы данных от
другого, каждый объект базы данных (совокупность объектных
данных) имеет внутренний идентификатор. Различные объекты базы
данных всегда будут иметь разные внутренние идентификаторы. Этот
идентификатор хранится вместе с остальными данными объекта в
специальном поле Ссылка.
Необъектные данные хранятся в виде записей и с точки зрения
системы определяются исключительно значениями своих полей.
Таким образом, удалив некоторую запись и записав после этого новую,
с точно такими же значениями всех полей, мы получим то же самое
состояние базы данных, которое было до удаления.
Таким образом, поскольку мы можем однозначно указать на каждый
объект базы данных, у нас появляется возможность хранить такой
указатель в полях других таблиц базы данных, выбирать его в поле
ввода, указывать в параметрах запроса при поиске по ссылке и т.д.
Во всех этих случаях как раз и будет использоваться объект встроенного языка вида Ссылка. Фактически этот объект хранит только
внутренний идентификатор, находящийся в поле Ссылка.
Например, если взять наш документ ОказаниеУслуги, то в поле,
хранящем реквизит табличной части Номенклатура, на самом деле
находится внутренний идентификатор, указывающий на элемент
справочника Номенклатура (рис. 14.1).
Рис. 14.1. Ссылка на элемент справочника «Номенклатура»
Когда в обработчике события ОбработкаПроведения документа
ОказаниеУслуги мы присваиваем значение реквизита табличной части
Номенклатура какой-либо переменной, мы имеем дело с объектом
встроенного языка ДокументОбъект.ОказаниеУслуги.
Этот объект содержит в себе значения всех реквизитов документа и
реквизитов его табличных частей.
Поэтому обращение (листинг 14.1) приводит к тому, что мы просто
читаем данные, хранящиеся в оперативной памяти, в этом самом
объекте встроенного языка (рис. 14.2).
Листинг 14.1. Обращение к реквизиту объекта
Движение.Материал = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Однако когда мы обращаемся к виду номенклатуры как к реквизиту
того элемента справочника, ссылка на который указана в табличной
части документа (листинг 14.2), происходит буквально следующее
(рис. 14.3).
Рис. 14.2. Чтение данных из оперативной памяти
Листинг 14.2. Обращение к реквизиту ссылки
Если ТекСтрокаПереченьНоменклатуры.Номенклатура.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
Рис. 14.3. Использование кеша объектов
Поскольку в объекте ДокументОбъект.ОказаниеУслуги есть только
ссылка на элемент справочника Номенклатура и больше никаких
данных об этом элементе нет, платформа возьмет эту ссылку
и обратится по ней в кеш объектов в надежде найти там данные того
объекта, ссылка на который у нее есть.
Если кеш объектов не будет иметь нужных данных, он обратится к базе
данных с тем, чтобы прочитать все данные объекта, ссылкой на
который он обладает.
После того как все данные, хранящиеся в реквизитах нужного
элемента справочника и в реквизитах его табличных частей, будут
считаны в кеш объектов, кеш объектов вернет запрашиваемую ссылку,
хранящуюся в реквизите ВидНоменклатуры справочника Номенклатура.
Как несложно догадаться, подобное обращение к базе данных требует
большего количества времени, нежели просто чтение из опера-тивной
памяти. При интерактивном заполнении документа подобные
задержки ничтожно малы, по сравнению со скоростью работы пользователя. Однако при выполнении большого количества расчетов
(например, при проведении больших документов, содержащих
несколько тысяч строк) разница во времени может быть довольно
заметной.
Из всего вышесказанного можно сделать следующий вывод: если
алгоритм проведения документа использует только те данные,
которые присутствуют в реквизитах документа (и его табличных
частей), вполне достаточно использовать конструктор движений
документа (как это было у нас в случае с документом ПриходнаяНакладная).
Если же в алгоритме проведения требуется анализировать дополнительные реквизиты объектов, ссылки на которые содержатся в
документе, а также использовать результаты расчета итогов регистров,
следует использовать запросы для более быстрой выборки данных из
базы данных.
То же самое справедливо в отношении выполнения любых участков
программы, критичных по производительности. Механизм запросов
лучше «читает» информационную базу и может за один раз выбрать
только те данные, которые необходимы. Поэтому, например, в
типовых решениях вы практически не увидите использования объекта
встроенного языка СправочникВыборка.<имя>. Вместо этого
повсеместно используются запросы к базе данных.
Повышение скорости проведения
Первое, чем мы займемся на этом занятии, – избавимся от «вредной»
конструкции ТекСтрокаПереченьНоменклатуры.Номенклатура.ВидНоменклатуры.
В режиме «Конфигуратор»
Откроем модуль документа ОказаниеУслуги.
Напомним, как выглядит сейчас процедура проведения этого
документа (листинг 14.3).
Листинг 14.3. Процедура «ОбработкаПроведения»
Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;
Для Каждого ТекСтрокаПереченьНоменклатуры Из ПереченьНоменклатуры Цикл
Если ТекСтрокаПереченьНоменклатуры.Номенклатура.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ТекСтрокаПереченьНоменклатуры.Количество;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Стоимость = ТекСтрокаПереченьНоменклатуры.Количество
* ТекСтрокаПереченьНоменклатуры.Стоимость;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ТекСтрокаПереченьНоменклатуры.Количество;
Движение.Выручка = ТекСтрокаПереченьНоменклатуры.Сумма;
Движение.Стоимость = ТекСтрокаПереченьНоменклатуры.Стоимость
* ТекСтрокаПереченьНоменклатуры.Количество;
КонецЦикла;
Другими словами, все данные, необходимые для проведения
документа, мы получаем из самого документа, и только для определения того, чем является номенклатура (товаром или услугой), мы
обращаемся к базе данных, читая данные всего объекта Номенклатура
(листинг 14.4).
Листинг 14.4. Обращение к объекту «Номенклатура»
Если ТекСтрокаПереченьНоменклатуры.Номенклатура.ВидНоменклатуры
Забегая вперед, скажем, что это не единственные данные, которые не
содержатся в самом документе и которые в то же время будут нужны
нам для правильного его проведения.
Поэтому поступим следующим образом: все данные, связанные с
номенклатурой, которая содержится в табличной части документа, мы
будем получать с помощью запроса к базе данных. А данные,
связанные с самим документом (например, дата документа, склад), мы
по-прежнему будем получать из документа. Такой подход позволит
нам читать только нужные данные и за счет этого макси-мально
ускорить проведение документа.
Итак, запросом мы будем получать:
 номенклатуру,
 количество,
 сумму,
 стоимость.
Из документа мы возьмем следующие данные:
 дата,
 клиент
,
 мастер
,  склад.
Приступим к созданию запроса. Установим курсор перед циклом
обхода табличной части документа и из контекстного меню выберем
пункт Конструктор запроса с обработкой результата (рис. 14.4).
Подтвердим, что мы хотим создать новый запрос.
В окне конструктора запросов перейдем на закладку Таблицы и поля и
выберем таблицу ОказаниеУслугиПереченьНоменклатуры – это
табличная часть документа ОказаниеУслуги.
Рис. 14.4. Вызов конструктора запроса
Из этой таблицы нам нужны поля – Номенклатура, НоменклатураВидНоменклатуры, Количество, Сумма и Стоимость (рис. 14.5).
Рис. 14.5. Выбранные поля
Но нам нужны не все записи этой таблицы, а только те, которые
относятся к нашему документу.
Поэтому перейдем на закладку Условия и зададим условие отбора из
таблицы документа только строк проводимого документа.
Для этого перетащим поле Ссылка в список условий запроса (листинг
14.5).
Листинг 14.5. Условие отбора из таблицы документа
ОказаниеУслугиПереченьНоменклатуры.Ссылка = &Ссылка
Ссылка на этот документ будет передана в параметр запроса Ссылка
(рис. 14.6).
Рис. 14.6. Условие отбора из таблицы документа
Также следует учесть, что в табличной части документа одна и та же
номенклатура может встречаться несколько раз.
Поэтому на закладке Группировка сгруппируем наши записи по полям
Номенклатура и НоменклатураВидНоменклатуры, а рассчитывать
будем сумму значений для полей Количество и Сумма.
Благодаря этому в результате значения номенклатуры повторяться не
будут, и для каждого из них будут посчитаны суммарные значения по
полям Количество и Сумма, если в табличной части документа
содержится несколько строк с одинаковой номенклатурой.
Также в состав суммируемых полей включим и поле Стоимость. По
нему будем рассчитывать, например, функцию Максимум.
Мы подразумеваем, что для разных строк одной и той же номенклатуры стоимость будет одинаковой, поэтому функция Максимум нужна
нам лишь для того, чтобы получить одно из имеющихся значений
стоимости (рис. 14.7).
Рис. 14.7. Группировка строк таблицы документа
На закладке Объединения/Псевдонимы зададим псевдонимы для
полей Количество и Сумма – КоличествоВДокументе и СуммаВДокументе, а для поля НоменклатураВидНоменклатуры зададим
псевдоним ВидНоменклатуры просто для облегчения чтения запроса
(рис. 14.8).
Рис. 14.8. Псевдонимы полей
Нажмем ОК и посмотрим, какой текст запроса сформировал
конструктор (листинг 14.6).
Листинг 14.6. Текст запроса
//{{КОНСТРУКТОР_ЗАПРОСА_С_ОБРАБОТКОЙ_РЕЗУЛЬТАТА //
Данный фрагмент построен конструктором.
// При повторном использовании конструктора внесенные вручную изменения будут утеряны!!!
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура КАК Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК |
ВидНоменклатуры,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Количество) КАК |
КоличествоВДокументе,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Сумма) КАК СуммаВДокументе, |
МАКСИМУМ(ОказаниеУслугиПереченьНоменклатуры.Стоимость) КАК Стоимость |ИЗ
| Документ.ОказаниеУслуги.ПереченьНоменклатуры КАК
| ОказаниеУслугиПереченьНоменклатуры
|ГДЕ
| ОказаниеУслугиПереченьНоменклатуры.Ссылка = &Ссылка |
|СГРУППИРОВАТЬ ПО
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Вставить обработку выборки ВыборкаДетальныеЗаписи
КонецЦикла;
//}}КОНСТРУКТОР_ЗАПРОСА_С_ОБРАБОТКОЙ_РЕЗУЛЬТАТА
Комментарии конструктора запроса в начале и конце фрагмента
можно удалить.
Поскольку для построения запроса мы использовали Конструктор
запроса с обработкой результата, конструктор написал за нас код для
выполнения и обхода записей запроса. Прокомментируем этот код.
Как вы уже знаете, для работы с запросами используется объект
встроенного языка Запрос. Вначале создается новый объект Запрос и
помещается в переменную Запрос. Затем в свойство Текст объекта
Запрос помещается сам текст запроса (Запрос.Текст = …).
После этого устанавливается значение параметра запроса &Ссылка как
ссылка на тот документ, в модуле которого мы сейчас находимся
(листинг 14.7).
Листинг 14.7. Установка параметра запроса
Запрос.УстановитьПараметр(“Ссылка”, Ссылка);
Затем запрос выполняется (Запрос.Выполнить()), получается объект
РезультатЗапроса, и выполняется его метод Выбрать(), который
формирует выборку записей из результата запроса.
Таким образом, получается объект ВыборкаИзРезультатаЗапроса,
который помещается в переменную ВыборкаДетальныеЗаписи.
Далее, используя метод этого объекта Следующий() (ВыборкаДетальныеЗаписи.Следующий()), мы будем в цикле обходить выборку
записей запроса.
Выполняя метод выборки запроса ВыборкаДетальныеЗаписи.
Следующий(), мы на каждом шаге цикла позиционируем указатель на
следующую запись выборки, пока не будет достигнут конец выборки.
Чтобы в цикле получить значение какого-либо поля выборки из
результата запроса, мы будем обращаться к полям запроса через точку
от переменной ВыборкаДетальныеЗаписи, которая содержит текущую
строку выборки запроса. Например, так: ВыборкаДетальныеЗаписи.Номенклатура.
Теперь нам осталось перенести существовавшие ранее в этом модуле
строки, описывающие движения регистров, внутрь цикла обхода
результата запроса (листинг 14.8).
Листинг 14.8. Цикл обхода записей запроса
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
// Вставить обработку выборки ВыборкаДетальныеЗаписи
КонецЦикла;
Сначала вместо комментария «// Вставить обработку выборки
ВыборкаДетальныеЗаписи» перенесем условие проверки и весь код,
формирующий движения по регистрам ОстаткиМатериалов и СтоимостьМатериалов (листинг 14.9).
Листинг 14.9. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ТекСтрокаПереченьНоменклатуры.Номенклатура.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ТекСтрокаПереченьНоменклатуры.Количество;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Стоимость = ТекСтрокаПереченьНоменклатуры.Количество *
ТекСтрокаПереченьНоменклатуры.Стоимость;
КонецЕсли;
КонецЦикла;
В условии заменим ТекСтрокаПереченьНоменклатуры.Номенклатура на
ВыборкаДетальныеЗаписи, так как вид номенклатуры мы теперь
получаем из запроса.
В движениях также заменим ТекСтрокаПереченьНоменклатуры
на ВыборкаДетальныеЗаписи (листинг 14.10).
Листинг 14.10. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.Количество;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.Количество *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
КонецЦикла;
ПРИМЕЧАНИЕ
Для упрощения восприятия новый текст в листингах выделен жирным
шрифтом.
Не забудем, что для поля Количество мы задали псевдоним в запросе,
поэтому заменим его на КоличествоВДокументе (листинг 14.11).
Листинг 14.11. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
КонецЦикла;
Теперь перенесем формирование движений по регистру Продажи
(листинг 14.12).
Листинг 14.12. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ТекСтрокаПереченьНоменклатуры.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ТекСтрокаПереченьНоменклатуры.Количество;
Движение.Выручка = ТекСтрокаПереченьНоменклатуры.Сумма;
Движение.Стоимость = ТекСтрокаПереченьНоменклатуры.Стоимость *
ТекСтрокаПереченьНоменклатуры.Количество;
КонецЦикла;
Здесь произведем аналогичные замены. ТекСтрокаПереченьНоменклатуры заменим на ВыборкаДетальныеЗаписи (листинг 14.13).
Листинг 14.13. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ВыборкаДетальныеЗаписи.Количество;
Движение.Выручка = ВыборкаДетальныеЗаписи.Сумма;
Движение.Стоимость = ВыборкаДетальныеЗаписи.Стоимость *
ВыборкаДетальныеЗаписи.Количество;
КонецЦикла;
Поля запроса Сумма и Количество заменим на СуммаВДокументе и
КоличествоВДокументе (листинг 14.14).
Листинг 14.14. Формирование движений регистров
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
Движение.Выручка = ВыборкаДетальныеЗаписи.СуммаВДокументе;
Движение.Стоимость = ВыборкаДетальныеЗаписи.Стоимость *
ВыборкаДетальныеЗаписи.КоличествоВДокументе;
КонецЦикла;
Оставшийся цикл обхода табличной части можно удалить (листинг
14.15).
Листинг 14.15. Ненужные строки
Для Каждого ТекСтрокаПереченьНоменклатуры Из ПереченьНоменклатуры Цикл
КонецЦикла;
В результате процедура проведения примет следующий вид (листинг
14.16).
Листинг 14.16. Процедура «ОбработкаПроведения»
Процедура ОбработкаПроведения(Отказ, Режим)
Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура КАК Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК |
ВидНоменклатуры,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Количество) КАК |
КоличествоВДокументе,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Сумма) КАК СуммаВДокументе, |
МАКСИМУМ(ОказаниеУслугиПереченьНоменклатуры.Стоимость) КАК Стоимость |ИЗ
| Документ.ОказаниеУслуги.ПереченьНоменклатуры КАК
| ОказаниеУслугиПереченьНоменклатуры
|ГДЕ
| ОказаниеУслугиПереченьНоменклатуры.Ссылка = &Ссылка |
|СГРУППИРОВАТЬ ПО
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
ВыборкаДетальныеЗаписи.Стоимость;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
Движение.Выручка = ВыборкаДетальныеЗаписи.СуммаВДокументе;
Движение.Стоимость = ВыборкаДетальныеЗаписи.Стоимость *
ВыборкаДетальныеЗаписи.КоличествоВДокументе;
КонецЦикла;
КонецПроцедуры
В режиме «1С:Предприятие»
Теперь нужно запустить «1С:Предприятие» в режиме отладки,
перепровести документы Оказание услуги и проверить, что ничего не
изменилось.
Таким образом, мы выполнили первый пункт нашего «плана» – избавились в процедуре проведения от считывания всех данных объекта
Номенклатура и тем самым оптимизировали выполнение процедуры
проведения.
Автоматический расчет стоимости
Теперь приступим ко второму этапу нашего плана.
До сих пор стоимость расходуемых материалов мы вписывали в
документ Оказание услуги вручную, при его создании.
Теперь же будем определять стоимость номенклатуры по среднему:
для каждой номенклатуры делить ее общую, суммарную стоимость на
то количество этой номенклатуры, которое у нас имеется, и таким
образом получать среднюю стоимость одной единицы этой номенклатуры.
Чтобы выполнить такой расчет, нам понадобятся дополнительные
данные, которых у нас сейчас нет.
Для каждой номенклатуры из табличной части нам понадобятся:
 ее стоимость, хранящаяся в регистре СтоимостьМатериалов;
 общее ее количество на всех складах, хранящееся в регистре
ОстаткиМатериалов.
Поэтому нам нужно будет доработать наш запрос таким образом,
чтобы он получал из базы данных и эти данные тоже.
Таким образом, нам хотелось бы, чтобы запрос возвращал следующие
поля для каждой номенклатуры, которая есть в документе (рис. 14.9).
Рис. 14.9. Описание полей запроса
Первые четыре поля мы можем получить (и уже получаем) из
табличной части самого документа, а вот последние два нужно будет
получить из других таблиц базы данных:
 стоимость – из регистра СтоимостьМатериалов;
 остатки на всех складах – из регистра ОстаткиМатериалов (рис.
14.10).
Рис. 14.10. Описание полей и таблиц запроса
Это значит, что наш запрос должен содержать два левых соеди-нения
таблицы документа с другими таблицами: одно – с таблицей
РегистрНакопления.СтоимостьМатериалов.Остатки, другое –
с таблицей РегистрНакопления.ОстаткиМатериалов.Остатки.
Казалось бы, все готово для того, чтобы составить запрос.
Но обратите внимание на важную деталь: в предложенной схеме
виртуальные таблицы будут возвращать стоимость и остатки номенклатуры абсолютно для всей номенклатуры. А нас интересует только
та номенклатура, которая указана в нашем документе.
На маленькой базе эта особенность может почти не проявляться –
количество различных элементов номенклатуры в справочнике
сравнимо с количеством различных элементов номенклатуры
в документе.
Но представьте реальную базу. В справочнике Номенклатура – 15000
наименований, например. А в документе – всего 5 наименований.
Получится, что сначала виртуальная таблица остатков будет усиленно
трудиться и рассчитывать нам стоимость (или остатки) по всем 15000
наименованиям номенклатуры, а в результате, когда мы левым соединением станем соединять ее с таблицей документа, мы из этих 15000
строк стоимости (или остатков) возьмем всего лишь 5 строк – для той
номенклатуры, которая указана в документе. Остальные 14995 строк
будут просто отброшены – система напрасно их рассчитывала.
Естественно, такая расточительность непозволительна в реальных
условиях, и такой запрос является очень неоптимальным. Поэтому во
все виртуальные таблицы, которые мы будем использовать, нужно
добавить условие отбора только той номенклатуры, которая содержится в табличной части нашего документа. В этом случае стоимость
и остатки будут рассчитаны не для всей номенклатуры вообще, а
только для нужной нам номенклатуры.
В результате схема нашего запроса будет выглядеть следующим
образом (рис. 14.11).
Рис. 14.11. Схема запроса
Обратите внимание, что в обеих виртуальных таблицах используется
список номенклатуры из табличной части документа (по нему ограничиваются рассчитываемые данные).
Кроме этого, из таблиц документа тоже берутся не все данные, а
только данные, относящиеся к одному нашему конкретному
документу. Чтобы не получать этот список три раза (для документа и
в каждой виртуальной таблице заново), мы можем сформировать его
заранее и затем уже использовать в нужных нам условиях запроса.
Выполнить эту задачу нам помогут временные таблицы.
Временные таблицы – это программные объекты, которые разработчик может создать и заполнить данными, а запросы могут использовать данные временных таблиц для своих нужд. Например, для
наложения некоторого сложного условия, как в нашем случае.
Таким образом, схема нашего запроса приобретает следующий вид
(рис. 14.12).
Рис. 14.12. Схема запроса
Итак, приступим.
В режиме «Конфигуратор»
Первое, что мы сделаем, – удалим реквизит табличной части
Стоимость документа ОказаниеУслуги, который нам больше не понадобится.
Для этого откроем в конфигураторе окно редактирования объекта
конфигурации Документ ОказаниеУслуги, перейдем на закладку
Данные, раскроем список реквизитов табличной части документа,
выделим реквизит Стоимость и нажмем кнопку Удалить в командной
панели (рис. 14.13).
Рис. 14.13. Удаление реквизита табличной части
Также следует удалить соответствующее поле из таблицы ПереченьНоменклатуры, расположенной в форме.
Для этого откроем форму ФормаДокумента документа ОказаниеУслуги и в окне структуры элементов формы выделим поле таблицы
ПереченьНоменклатурыСтоимость и нажмем кнопку Удалить
в командной панели (рис. 14.14).
Рис. 14.14. Удаление поля табличной части
Теперь займемся запросом.
Временную таблицу мы сформируем с помощью того запроса,
который у нас уже написан.
Откроем модуль документа ОказаниеУслуги.
В процедуре ОбработкаПроведения() перед созданием запроса
создадим менеджер временных таблиц и укажем, что этот запрос будет
использовать созданный менеджер временных таблиц (листинг 14.17).
Листинг 14.17. Использование менеджера временных таблиц
Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;
// Создать менеджер временных таблиц
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
// Укажем, какой менеджер временных таблиц использует этот запрос
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
"ВЫБРАТЬ
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура КАК Номенклатура,
Теперь изменим запрос таким образом, чтобы он создавал временную
таблицу, которая будет храниться в нашем менеджере временных
таблиц МенеджерВТ.
Чтобы конструктор запроса смог открыть наш запрос, удалим из него
строку (поля Стоимость у нас больше нет), листинг 14.18.
Листинг 14.18. Изменение запроса
| МАКСИМУМ(ОказаниеУслугиПереченьНоменклатуры.Стоимость) КАК Стоимость
Также удалим запятую в конце предыдущей строки (листинг 14.19).
Листинг 14.19. Изменение запроса
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Сумма) КАК СуммаВДокументе
Теперь установим курсор внутрь текста запроса, например на слове
ВЫБРАТЬ, и выполним команду контекстного меню Конструктор
запроса. Существующий текст запроса будет показан в форме
конструктора запросов (рис. 14.15).
Рис. 14.15. Конструктор запроса
Чтобы результат запроса поместить во временную таблицу, перейдем
на закладку Дополнительно и отметим пункт Создание временной
таблицы.
Зададим имя временной таблицы – НоменклатураДокумента
(рис. 14.16).
Рис. 14.16. Создание временной таблицы
Нажмем ОК и посмотрим, какой текст сформировал конструктор
запроса (листинг 14.20).
Листинг 14.20. Текст запроса
"ВЫБРАТЬ
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура КАК Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК |
ВидНоменклатуры,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Количество) КАК КоличествоВДокументе, |
СУММА(ОказаниеУслугиПереченьНоменклатуры.Сумма) КАК СуммаВДокументе |ПОМЕСТИТЬ
НоменклатураДокумента
|ИЗ
| Документ.ОказаниеУслуги.ПереченьНоменклатуры КАК
| ОказаниеУслугиПереченьНоменклатуры
|ГДЕ
| ОказаниеУслугиПереченьНоменклатуры.Ссылка = &Ссылка
|СГРУППИРОВАТЬ ПО
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры";
Новым здесь является только строка (листинг 14.21).
Листинг 14.21. Создание временной таблицы
|ПОМЕСТИТЬ НоменклатураДокумента
Это означает, что результат запроса будет сохранен во временной
таблице НоменклатураДокумента.
Теперь если мы для другого запроса укажем этот же самый менеджер
временных таблиц МенеджерВТ, то в этом другом запросе мы сможем
обратиться к данным этой временной таблицы.
Таким образом, мы выполнили первую часть нашего плана – создали
запрос, помещающий данные табличной части документа во
временную таблицу (рис. 14.17).
Теперь займемся конструированием второго запроса.
Установим курсор на следующую строку после оператора РезультатЗапроса = Запрос.Выполнить(); (именно здесь выполняется
создание временной таблицы) и напишем заготовку будущего запроса
(листинг 14.22).
Листинг 14.22. Создание второго запроса
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос2.Текст = “”;
Мы создали новый объект Запрос и назначили ему тот же самый
менеджер временных таблиц, чтобы иметь возможность обращаться к
созданной нами ранее временной таблице.
Рис. 14.17. Создание первого запроса
Теперь установим курсор внутрь кавычек и выполним команду
контекстного меню Конструктор запроса. Согласимся на создание
нового запроса.
Поскольку мы собираемся выбирать данные из нашей временной
таблицы, создадим в запросе описание этой временной таблицы. Для
этого над списком Таблицы нажмем кнопку Создать описание
временной таблицы (рис. 14.18).
Рис. 14.18. Создание описания временной таблицы
В открывшемся окне введем имя нашей временной таблицы НоменклатураДокумента и добавим описание полей:
 Номенклатура, тип СправочникСсылка.Номенклатура;
 ВидНоменклатуры, тип ПеречислениеСсылка.ВидыНоменклатуры;
 КоличествоВДокументе, тип Число, 15, 3;
 СуммаВДокументе, тип Число, 15, 2.
Нажмем ОК.
В результате у нас получится следующее описание временной
таблицы (рис. 14.19).
Рис. 14.19. Создание описания временной таблицы
Нажмем ОК. Выберем из этой таблицы все поля (рис. 14.20) и нажмем
кнопку Запрос в левом нижнем углу окна конструктора запроса.
Рис. 14.20. Выбранные поля временной таблицы
Текст запроса будет иметь вид (листинг 14.23).
Листинг 14.23. Текст второго запроса
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура КАК Номенклатура,
НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе
ИЗ
НоменклатураДокумента КАК НоменклатураДокумента
Итак, мы создали первую часть второго запроса – выбрали информацию из временной таблицы (рис. 14.21).
Рис. 14.21. Создание второго запроса
Теперь будем соединять эту конструкцию левыми соединениями с
таблицами остатков.
Начнем со стоимости материалов.
Добавим в список таблиц запроса виртуальную таблицу РегистрНакопления.СтоимостьМатериалов.Остатки. Из нее выберем поле
СтоимостьОстаток. Перейдем на закладку Связи и зададим связь
между таблицами.
Из временной таблицы будем выбирать все записи, и поле Номенклатура временной таблицы должно быть равно полю Материал таблицы
остатков (рис. 14.22).
Рис. 14.22. Связи между таблицами
Нужно не забыть ограничить виртуальную таблицу только той номенклатурой, которая есть в нашей временной таблице.
Поэтому вернемся на закладку Таблицы и поля, выделим в списке
таблиц таблицу СтоимостьМатериаловОстатки и нажмем кнопку
Параметры виртуальной таблицы, расположенную над списком
таблиц.
Зададим параметр Условие следующим образом (листинг 14.24).
Листинг 14.24. Условие виртуальной таблицы
Материал В (ВЫБРАТЬ НоменклатураДокумента.Номенклатура ИЗ НоменклатураДокумента)
То есть материал должен быть среди номенклатуры, выбранной из
временной таблицы.
УЗНАЙ БОЛЬШЕ!
Следует внимательно подходить к использованию виртуальных
таблиц запросов. В частности, необходимо уделять особое внимание
максимально возможному использованию параметров этих таблиц.
Например, в нашем случае можно было бы и не использовать
параметр Условие, а ограничить выбранные поля уже в самом запросе,
указав в условии ПО равенство номенклатуры из документа и
материала из таблицы остатков. Формально мы получили бы тот же
самый результат, однако по производительности этот способ сильно
отли-чался бы от того, который мы используем.
В самом деле в нашем варианте виртуальная таблица предоставит
нам ровно столько записей, сколько различных элементов номенклатуры содержится в проводимом документе. Если же не указывать
условие, виртуальная таблица предоставит нам записи по абсо-
лютно всем элементам номенклатуры, информация о которых есть в
регистре накопления. И уже в самом нашем запросе мы будем
отбирать из этой огромной массы записей лишь несколько, которые
нам действительно нужны.
Очевидно, что второй вариант будет работать дольше, и время
выполнения такого запроса будет зависеть в основном не от количества данных, содержащихся в документе (т.е. реального
количества обрабатываемой информации), а от размера регистра
накопления.
Кроме того что подобный вариант снижает производительность
конфигурации, могут возникать ситуации, когда результаты, полученные одним и другим способом, будут различны. Такое, например,
вполне возможно при использовании виртуальной таблицы регистра
сведений СрезПоследних. Подробнее можно прочитать об этом на
диске ИТС (информационно-технологического сопровождения) в
статье «Использование отборов в запросах с виртуальными
таблицами».
Нажмем кнопку Запрос и посмотрим, какой текст запроса сформировал конструктор (листинг 14.25).
Листинг 14.25. Текст запроса
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура КАК Номенклатура,
НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе,
СтоимостьМатериаловОстатки.СтоимостьОстаток КАК СтоимостьОстаток
ИЗ
НоменклатураДокумента КАК НоменклатураДокумента
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СтоимостьМатериалов.Остатки(
, Материал В
(ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)) КАК СтоимостьМатериаловОстатки
ПО НоменклатураДокумента.Номенклатура =
СтоимостьМатериаловОстатки.Материал
Тем самым мы добавили к выбранным ранее полям стоимость номенклатуры (рис. 14.23).
Теперь добавим виртуальную таблицу остатков регистра ОстаткиМатериалов.Остатки, из которой выберем поле КоличествоОстаток.
Перейдем на закладку Связи и зададим связь между таблицами.
Рис. 14.23. Создание второго запроса
Из временной таблицы будем выбирать все записи, и поле Номенклатура временной таблицы должно быть равно полю Материал таблицы
остатков (рис. 14.24).
Рис. 14.24. Связи между таблицами
Также зададим параметры виртуальной таблицы ОстаткиМатериаловОстатки.
В параметр Условие внесем следующий текст (листинг 14.26).
Листинг 14.26. Условие виртуальной таблицы
Материал В (ВЫБРАТЬ НоменклатураДокумента.Номенклатура ИЗ НоменклатураДокумента)
В результате мы получим следующий текст запроса (листинг 14.27).
Листинг 14.27. Текст запроса
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура КАК Номенклатура,
НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе,
СтоимостьМатериаловОстатки.СтоимостьОстаток КАК СтоимостьОстаток,
ОстаткиМатериаловОстатки.КоличествоОстаток КАК КоличествоОстаток
ИЗ
НоменклатураДокумента КАК НоменклатураДокумента
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СтоимостьМатериалов.Остатки( ,
Материал В (
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)) КАК СтоимостьМатериаловОстатки
ПО НоменклатураДокумента.Номенклатура =
СтоимостьМатериаловОстатки.Материал
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки(
, Материал В (
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)) КАК ОстаткиМатериаловОстатки
ПО НоменклатураДокумента.Номенклатура = ОстаткиМатериаловОстатки.Материал
Тем самым мы добавили к выбранным ранее полям остатки номенклатуры на всех складах (рис. 14.25).
Рис. 14.25. Создание второго запроса
В заключение перейдем на закладку Объединения/Псевдонимы и
зададим следующие псевдонимы полей (рис. 14.26):
 СтоимостьОстаток – Стоимость;
 КоличествоОстаток – Количество.
Рис. 14.26. Псевдонимы полей
В результате мы получим следующий текст запроса (листинг 14.28).
Листинг 14.28. Текст запроса
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура КАК Номенклатура,
НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе,
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе,
СтоимостьМатериаловОстатки.СтоимостьОстаток КАК Стоимость,
ОстаткиМатериаловОстатки.КоличествоОстаток КАК Количество
ИЗ
НоменклатураДокумента КАК НоменклатураДокумента
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СтоимостьМатериалов.Остатки( ,
Материал В (
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)) КАК СтоимостьМатериаловОстатки
ПО НоменклатураДокумента.Номенклатура =
СтоимостьМатериаловОстатки.Материал
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки(
, Материал В (
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)) КАК ОстаткиМатериаловОстатки
ПО НоменклатураДокумента.Номенклатура = ОстаткиМатериаловОстатки.Материал
В качестве последнего штриха нашей работы с запросом нужно
предусмотреть тот случай, когда номенклатура в справочнике есть, но
у нее нет ни остатков, ни стоимости. Это может быть, например, в том
случае, когда номенклатуру создали в справочнике, но она еще не
поступала в нашу фирму.
В такой ситуации левые соединения с виртуальными таблицами не
вернут ничего. На языке запросов это значит, что в полях Стоимость и
Количество будут значения NULL.
Чтобы в дальнейшем нам было удобно работать с результатом нашего
запроса, сразу же в самом запросе избавимся от этих значений.
Для этого мы применим функцию ЕСТЬNULL() к полям Стоимость и
Количество. Если значение этого поля будет NULL, функция вернет 0.
В остальных случаях функция вернет само значение этого поля.
Перейдем на закладку Таблицы и поля, выделим поле
СтоимостьМатериаловОстатки.СтоимостьОстаток и нажмем кнопку
Изменить текущий элемент (рис. 14.27).
Рис. 14.27. Изменение значения поля в запросе
В открывшемся окне отредактируем значение поля следующим
образом (листинг 14.29), рис. 14.28.
Листинг 14.29. Выражение для расчета поля в запросе
ЕСТЬNULL(СтоимостьМатериаловОстатки.СтоимостьОстаток, 0)
Рис. 14.28. Изменение значения поля в запросе
Аналогично поступим и с другим полем: ОстаткиМатериаловОстатки.
КоличествоОстаток.
Нажмем ОК – текст запроса будет вставлен в модуль (листинг 14.30).
Листинг 14.30. Текст запроса
Запрос2.Текст = "ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура КАК Номенклатура,
| НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
| НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе, |
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе,
| ЕСТЬNULL(СтоимостьМатериаловОстатки.СтоимостьОстаток, 0) КАК Стоимость, |
ЕСТЬNULL(ОстаткиМатериаловОстатки.КоличествоОстаток, 0) КАК Количество
|ИЗ
| НоменклатураДокумента КАК НоменклатураДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СтоимостьМатериалов.Остатки( |
, Материал В (
| ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)) КАК СтоимостьМатериаловОстатки |
ПО НоменклатураДокумента.Номенклатура =
| СтоимостьМатериаловОстатки.Материал
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки( |
, Материал В (
| ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)) КАК ОстаткиМатериаловОстатки |
ПО НоменклатураДокумента.Номенклатура =
| ОстаткиМатериаловОстатки.Материал";
Нам останется всего лишь дописать после него оператор выполнения
запроса (листинг 14.31).
Листинг 14.31. Фрагмент процедуры «ОбработкаПроведения()»
…
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос2.Текст = "ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура,
…
| ПО НоменклатураДокумента.Номенклатура =
| СтоимостьМатериаловОстатки.Материал";
РезультатЗапроса = Запрос2.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать(); …
Теперь разберемся с записью движений.
Все операторы, которые были написаны нами ранее, будут работать
без изменений.
Единственное, что потребуется изменить, – это способ получения
стоимости. Раньше мы просто брали ее из документа, теперь же нам
нужно ее рассчитать на основании тех данных, которые мы получили
запросом.
Стоимость материала равна частному от деления всей стоимости,
полученной запросом (Стоимость), на общее количество материала на
всех складах (Количество).
Но, как мы уже сказали, возможна ситуация, когда поле Количество у
нас будет равно 0, а на 0 делить нельзя. Поэтому сразу после начала
цикла обхода результата запроса рассчитаем стоимость для текущей
номенклатуры (листинг 14.32).
Листинг 14.32. Фрагмент процедуры «ОбработкаПроведения()»
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
СтоимостьМатериала = 0;
Иначе
СтоимостьМатериала = ВыборкаДетальныеЗаписи.Стоимость /
ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
…
Теперь заменим расчет стоимости в движениях регистров СтоимостьМатериалов и Продажи (листинг 14.33).
Листинг 14.33. Фрагмент процедуры «ОбработкаПроведения()»
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
СтоимостьМатериала = 0;
Иначе
СтоимостьМатериала = ВыборкаДетальныеЗаписи.Стоимость /
ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.Количество;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе
* СтоимостьМатериала;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
Движение.Выручка = ВыборкаДетальныеЗаписи.СуммаВДокументе;
Движение.Стоимость = СтоимостьМатериала * ВыборкаДетальныеЗаписи
.КоличествоВДокументе;
КонецЦикла;
Теперь все вроде бы хорошо, но остался один важный момент.
Если проводить этот документ в первый раз, то результат получится
правильный.
Однако если документ был проведен ранее и мы заново решим
провести его, мы получим неправильный результат.
Дело в том, что когда мы находимся в обработчике проведения
документа и этот документ был уже проведен ранее, то в базе данных
существуют движения этого документа.
Таким образом, читая из базы данных стоимость и остатки материалов, мы прочитаем их с учетом тех движений, которые документ
выполнил ранее. А это неправильно.
Чтобы в обработчике проведения документа прочитать данные базы
данных без учета предыдущих движений, которые мог выполнять
документ, нужно перед чтением записать пустые наборы записей в те
регистры, из которых мы собираемся читать.
В нашем случае такими регистрами являются регистры накопления
СтоимостьМатериалов и ОстаткиМатериалов.
Поэтому перед выполнением второго запроса добавим следующие две
строки:
Листинг 14.33а. Фрагмент процедуры «ОбработкаПроведения()»
…
| ПО НоменклатураДокумента.Номенклатура = СтоимостьМатериаловОстатки.Материал”;
// Запишем пустые наборы записей, чтобы читать остатки без учета данных в документе
Движения.СтоимостьМатериалов.Записать();
Движения.ОстаткиМатериалов.Записать();
РезультатЗапроса = Запрос2.Выполнить();
В режиме «1С:Предприятие»
Теперь нужно запустить «1С:Предприятие» в режиме отладки, перепровести все документы Оказание услуги и проверить, что данные
правильно заносятся в регистры.
Теория
Как быстро посмотреть результат запроса
В процессе изменения процедуры проведения документа ОказаниеУслуги мы с вами подошли уже к написанию довольно сложных
запросов.
Зачастую возникает необходимость быстро убедиться в правильности
тех данных, которые читаются из базы данных с помощью запроса,
просмотреть их в виде таблицы.
Есть простой способ сделать это в конфигураторе. Рассмотрим его на
примере запроса из нашей обработки проведения.
Предположим, после выполнения запроса (Запрос2) нужно выгрузить
результат запроса в таблицу значений (ТЗ). Это можно сделать
следующим образом (листинг 14.34).
Листинг 14.34. Фрагмент процедуры «ОбработкаПроведения()»
…
РезультатЗапроса = Запрос2.Выполнить();
ТЗ = РезультатЗапроса.Выгрузить();
ВыборкаДетальныеЗаписи = Результат.Выбрать(); …
После этого нужно установить точку останова на следующем
операторе (ВыборкаДетальныеЗаписи = Результат.Выбрать()). Затем
запустить «1С:Предприятие» в режиме отладки и перепровести один
из документов Оказание услуги. Например, документ № 2.
После того как исполнение кода будет остановлено, нужно двойным
щелчком выделить слово ТЗ и нажать кнопку Вычислить выражение
(Shift + F9) на панели инструментов Отладка конфигурации.
Откроется окно просмотра выражений, в котором будет находиться
наша таблица значений ТЗ.
Таблица значений является коллекцией, поэтому, чтобы просмотреть
ее содержимое, выделим строку ТЗ в окне Результат и нажмем кнопку
Показать значения в отдельном окне (или F2) над окном результата
(рис. 14.29).
Рис. 14.29. Просмотр таблицы значений, содержащей результат запроса
Мы увидим всю таблицу значений, которая будет содержать
результат выполнения нашего запроса.
С помощью кнопки Вывести список можно вывести эту таблицу
значений, например, в табличный документ, если требуется какой-то
анализ этих данных или если эти данные нужно сохранить для
сравнения.
После просмотра таким образом результата запроса нужно не забыть
снять точку останова в процедуре проведения документа и закомментировать (или удалить) добавленную нами строку, выгружающую
результат запроса в таблицу значений, поскольку для нормальной
работы документа она не нужна.
Оперативное и неоперативное
проведение документов
Теперь мы поговорим о некоторых особенностях, связанных с тем, что
в обработке проведения мы будем контролировать наличие достаточного количества материалов на складе при проведении документа.
Делать это мы будем не всегда. А только в том случае, когда
документ проводится оперативно. Если документ проводится
неоперативно,
то мы это делать не будем. Так для чего же предназначено оперативное и неоперативное проведение документа? Как устроен в системе
механизм оперативного проведения документов?
При разработке конфигураций на платформе «1С:Предприятие»
используется концепция оперативного и неоперативного прове-дения
документов. Эта концепция используется при решении задач
оперативного учета (например, при складском учете товаров) и
позволяет организовать корректную работу с документами в условиях
реальной действительности. Однако можно отключить этот механизм
(запретить оперативное проведение документов) и реализовывать
собственные алгоритмы, проверять актуальность документов на оси
событий и т.д.
Эта концепция подразумевает, что работа пользователей может
проис-ходить в двух принципиально разных по своей сути режимах.
Оперативное проведение документов пользователями выполня-ется в
режиме «реального времени», то есть отображает изменения, факты,
свершающиеся в настоящее время. Оперативное проведение особенно
актуально при многопользовательской работе. Поэтому при этом
способе проведения документов следует осуществлять максимум
проверок, способных исключить ошибки при вводе данных
пользователями.
Например, при оперативном проведении следует выполнять контроль
остатков на складе списываемой номенклатуры с тем, чтобы
исключить одновременную продажу одного товара несколькими
продавцами.
При оперативном проведении документа система, прежде всего,
проверит положение даты документа относительно текущей даты
сеанса. Текущая дата сеанса равна системной дате компьютера, приведенной к часовому поясу сеанса. Если дата проводимого документа
совпадает с текущей датой сеанса, то система будет проводить такой
документ в оперативном режиме, и в обработке проведения об этом
можно узнать, чтобы выстроить определенный алгоритм проведения
документа.
Если дата проводимого документа меньше текущей даты сеанса, то
такой документ система будет проводить в неоперативном режиме.
Неоперативное проведение документов подразумевает отражение в
базе данных фактов, которые свершились в прошлом или которые
точно будут совершены в будущем. Поэтому задача неоперативного
проведения документов – просто отразить в информационной базе
данные о совершенных операциях.
При неоперативном проведении документов не имеет смысла производить целый ряд проверок, в частности контроль остатков. Подразумевается, что если в процессе неоперативного проведения документов
были допущены ошибки (например, списано такое количество номенклатуры, которого не было на складе на дату проведения документа),
то анализ полученного состояния базы данных является отдельной
задачей, не относящейся к неоперативному проведению и выполняющейся не в момент проведения документа, а тогда, когда в базе
имеются достаточные данные для анализа, например, когда введены
более ранние документы, приходующие товары.
Таким образом, оперативное проведение служит для того, чтобы в
реальном режиме многопользовательской работы определить
возможность или невозможность выполнения той или иной операции
(и выполнить ее, если возможно). Неоперативное проведение предназначено для безусловного отражения в базе операций, которые уже
были совершены (или точно будут совершены).
Зачастую возникает желание провести документ будущей датой,
чтобы отразить какие-то события, которые точно наступят в будущем.
В этом случае если документу разрешено использовать оперативное
проведение, то система не даст провести такой документ будущей
датой. Она просто сообщит, что не может оперативно провести такой
документ, и не даст никаких вариантов выбора. Пользова-телю
останется только поменять дату документа или на текущую дату (тогда
документ будет проведен в оперативном режиме), или прошедшую
(тогда документ будет проведен в неоперативном режиме). Поэтому
если логика учета подразумевает, что какой-то документ должен
проводиться будущей датой, для такого документа механизм
оперативного проведения должен быть отключен в мета-данных (на
закладке Движения окна редактирования объекта конфи-гурации).
С оперативным проведением документов связано понятие оперативной отметки времени и понятие момента времени. Прямо сейчас
нам эта информация не понадобится, но раз уж зашла речь об оперативном проведении, есть подходящий случай рассказать об этом.
Понятие момента времени
Для определения положения документа на оси времени использу-ется
реквизит документа Дата. Дата содержит время с точностью до
секунды. Это позволяет контролировать последовательность записи
документов. Однако при большом объеме создаваемых документов
вероятна ситуация, когда несколько документов будут иметь
одинаковое значение даты (т.е. будут созданы в течение одной
секунды). Как в этом случае определить последовательность
созданных документов?
Для обработки подобных ситуаций было введено понятие момент
времени. Момент времени представляет собой совокупность даты,
времени и ссылки на объект базы данных. Он позволяет однозначно
идентифицировать любой объект ссылочного типа базы данных на оси
событий, но имеет смысл в основном только для документов. Кроме
того, момент времени позволяет идентифицировать и необъектные
данные, например, записи регистров, подчиненных регистратору.
Понятие момента времени реализовано во встроенном языке при
помощи универсального объекта МоментВремени. Этот объект имеет
свойства Дата и Ссылка, которые позволяют получить «составляющие»
момента времени, и один метод – Сравнить(), при помощи которого
возможно сравнение двух моментов времени между собой. Кроме
этого, объект МоментВремени имеет конструктор и может быть создан
в явном виде для любого объекта базы данных ссылочного типа.
Для нескольких документов, имеющих одинаковую дату и время,
последовательность их на оси событий определяется системой исходя
из ссылок на эти документы. Она может не совпадать с последовательностью создания документов, и она недоступна для изменения
пользователем, то есть нельзя каким-либо образом повлиять на последовательность документов внутри одной секунды или вычислить, что
один документ создан раньше, а другой – позже.
Оперативная отметка времени создается системой каждый раз при
оперативном проведении документа. Ее значение формируется исходя
из текущей даты сеанса и последней созданной оперативной отметки.
Если последняя оперативная отметка меньше текущей даты сеанса, в
качестве новой оперативной отметки принимается текущая дата
сеанса.
Если последняя оперативная отметка равна или больше текущей даты
сеанса, в качестве новой оперативной отметки принимается значение
на одну секунду большее, чем старая оперативная отметка времени.
Таким образом, если у объекта конфигурации Документ установлено
свойство оперативного проведения (рис. 14.30), последовательность
действий системы будет следующей:
 при создании нового документа система будет устанавливать ему
текущую дату сеанса и «нулевое» время;
 при проведении такого документа (с датой, день которой соответствует дню текущей даты сеанса) система установит в качестве
даты документа оперативную отметку времени;
 если отменить проведение документа и затем провести его снова
(не изменяя даты), система установит документу новую оперативную отметку времени;
 если попытаться перепровести документ, то система также автоматически установит документу новую оперативную отметку
времени и проведет его;
 при попытке проведения (или перепроведения) оперативно проводимого документа с датой, день которой меньше дня текущей
даты сеанса, документ будет проведен неоперативно;
 если попытаться провести (или перепровести) оперативно проводимый документ с датой, день которой больше дня текущей даты
сеанса, то система не даст выполнить такое действие.
Рис. 14.30. Разрешение оперативного проведения документа
Контроль остатков
Общая методика контроля остатков при проведении документа
заключается в следующем: сначала, не глядя ни на что, нужно записать
движения документа, а затем, когда движения уже записаны,
прочитать из базы данных остатки.
Если появились отрицательные остатки, значит, такой документ
проводить нельзя. Нужно сообщить пользователю, каких материалов
не хватает, и отменить проведение документа.
Если же отрицательных остатков не появилось, тогда можно смело
проводить документ.
Полдела у нас уже сделано: мы формируем движения документа и
записываем их. Единственное, что нам осталось, – в случае оперативного проведения проконтролировать, что получилось, и, если
появились отрицательные остатки, отменить проведение документа.
В режиме «Конфигуратор»
Сделаем заготовку. После цикла обхода результата запроса и перед
концом процедуры напишем следующие строки (листинг 14.35).
Листинг 14.35. Фрагмент процедуры «ОбработкаПроведения()»
…
КонецЦикла;
Движения.Записать();
Если Режим = РежимПроведенияДокумента.Оперативный Тогда
// Проверить отрицательные остатки
КонецЕсли;
КонецПроцедуры
Сначала мы записываем движения в регистры.
Затем определяем режим проведения документа. При выполнении
процедуры ОбработкаПроведения() вторым параметром (Режим) в нее
передается режим проведения документа, и значение этой переменной
сравнивается со значением системного перечисления
РежимПроведенияДокумента. В случае оперативного проведения мы
будем выполнять контроль остатков.
Теперь сделаем заготовку запроса для проверки отрицательных
остатков.
Так как нам придется снова получать остатки только для той номенклатуры, которая в документе, укажем, что этот запрос будет использовать тот же самый менеджер временных таблиц МенеджерВТ
(листинг 14.36).
Листинг 14.36. Фрагмент процедуры «ОбработкаПроведения()»
Если Режим = РежимПроведенияДокумента.Оперативный Тогда //
Проверить отрицательные остатки
Запрос3 = Новый Запрос;
Запрос3.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос3.Текст = "";
КонецЕсли;
Установим курсор внутрь кавычек и вызовем конструктор запроса.
Подтвердим создание нового запроса.
Выберем таблицу ОстаткиМатериалов.Остатки и из нее два поля:
Материал и КоличествоОстаток.
Зададим параметры этой таблицы. В параметре Условие напишем:
Листинг 14.37. Условие виртуальной таблицы
Материал В (
ВЫБРАТЬ
НоменклатураДокумента.Номенклатура
ИЗ
НоменклатураДокумента)
И Склад = &Склад
То есть мы получаем итоги только для той номенклатуры, которая
содержится в нашей временной таблице, и только по складу, который
указан в документе.
Затем на закладке Условия перенесем в список условий поле КоличествоОстаток, установим флажок Произвольное и укажем, что нас
интересуют только отрицательные остатки (листинг 14.38).
Листинг 14.38. Условие запроса
ОстаткиМатериаловОстатки.КоличествоОстаток < 0
Нажмем ОК. Текст запроса будет выглядеть следующим образом
(листинг 14.39).
Листинг 14.39. Текст запроса
// Проверить отрицательные остатки
Запрос3 = Новый Запрос;
Запрос3.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос3.Текст = "
|ВЫБРАТЬ
| ОстаткиМатериаловОстатки.Материал КАК Материал,
| ОстаткиМатериаловОстатки.КоличествоОстаток КАК КоличествоОстаток |ИЗ
| РегистрНакопления.ОстаткиМатериалов.Остатки( , Материал В ( |
ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)
| И Склад = &Склад) КАК ОстаткиМатериаловОстатки
|ГДЕ
| ОстаткиМатериаловОстатки.КоличествоОстаток < 0";
Теперь осталось только установить параметр запроса, обойти
результат запроса и вывести сообщения об отрицательных остатках
(листинг 14.40).
Листинг 14.40. Фрагмент процедуры «ОбработкаПроведения()»
// Проверить отрицательные остатки
Запрос3 = Новый Запрос;
Запрос3.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос3.Текст = "
|ВЫБРАТЬ
| ОстаткиМатериаловОстатки.Материал,
…
|ГДЕ
| ОстаткиМатериаловОстатки.КоличествоОстаток < 0";
Запрос3.УстановитьПараметр("Склад", Склад);
РезультатЗапроса = Запрос3.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Сообщение = Новый СообщениеПользователю();
Сообщение.Текст = "Не хватает " + Строка(- ВыборкаДетальныеЗаписи
.КоличествоОстаток) + " единиц материала """
+ ВыборкаДетальныеЗаписи.Материал + """";
Сообщение.Сообщить();
Отказ = Истина;
КонецЦикла;
Кратко поясним добавленный текст.
При выполнении проверки в запрос в параметре Склад передается
склад, указанный в документе.
Затем выполняется запрос для получения отрицательных остатков
номенклатуры, содержащейся во временной таблице и на складе,
указанном в параметре Склад.
После этого выборка записей запроса обходится в цикле, и, если есть
такие записи, они выводятся в сообщениях пользователю.
При этом параметру Отказ процедуры проведения документа
присваивается значение Истина, то есть документ не проводится,
начатая транзакция отменяется, и состояние данных, измененных в
процессе проведения, возвращается в исходное, до начала прове-дения
документа.
Блокировка данных, которые читаются и
изменяются при проведении
Казалось бы, это все? Но нет, есть очень важный момент, о котором
мы не позаботились.
Сейчас схема нашей процедуры такова:
1. Выполняем первый запрос с именем Запрос. В результате мы
формируем временную таблицу из перечня номенклатуры
документа.
2. Выполняем второй запрос с именем Запрос2. В результате мы
читаем стоимость и остатки для номенклатуры, содержащейся в
табличной части документа.
3. Записываем движения регистров (Движения.Записать()).
4. Выполняем третий запрос с именем Запрос3. Тем самым мы
проверяем наличие отрицательных остатков.
Обратите внимание, что, начиная с выполнения второго запроса и до
конца процедуры, нам необходимо обеспечить неизмен-ность
стоимости и остатков номенклатуры, с которой мы работаем, и
запретить другим транзакциям даже читать эти данные. Сама система,
естественно, заблокирует изменение этих данных, но лишь начиная с
того момента, когда мы запишем движения.
Однако может возникнуть следующая ситуация. Выполняя второй
запрос, мы прочитали, что есть 2 шт. некоторого материала. И другая
транзакция (другой пользователь), которая собирается списывать
материалы, тоже прочитала, что есть 2 шт. этого материала. После
этого мы записали движения, и система заблокировала эти данные.
Другая транзакция ждет, когда мы освободим данные. Мы провели
документ, списали 2 шт. материала и освободили данные. Другая
транзакция пытается тоже списать 2 шт. материала, но его уже нет!
Аналогичная ситуация может возникнуть и между п. 3 и п. 4, в
резуль-тате чего контроль остатков будет работать неверно.
Поэтому, чтобы не происходило таких коллизий, нам необходимо
заблокировать остатки от чтения другими транзакциями еще до
выполнения второго запроса. То есть прежде чем читать что-то, что
мы собираемся изменять, нужно запретить чтение этих данных
другими транзакциями до тех пор, пока мы не закончим свои
изменения (или пока не откажемся от проведения документа).
В режиме «Конфигуратор»
Как это сделать? Хороший вопрос. Давайте посмотрим на свойство
Режим управления блокировкой данных нашей конфигурации. Оно
установлено в значение Управляемый (рис. 14.31).
Рис. 14.31. Режим управления блокировкой данных
в свойствах конфигурации
Это значит, что нам нужно использовать управляемые блокировки,
которые устанавливаются средствами встроенного языка.
Необходимо заблокировать те данные, которые мы собираемся читать
и впоследствии изменять. Для этого у наборов записей регистров есть
свойство БлокироватьДляИзменения, которым мы и воспользуемся.
Вставим этот код перед записью пустых наборов записей
(листинг 14.41).
Листинг 14.41. Фрагмент процедуры «ОбработкаПроведения()»
…
| ПО НоменклатураДокумента.Номенклатура = ОстаткиМатериаловОстатки.Материал”;
// Установим необходимость блокировки данных в регистрах СтоимостьМатериалов
// и ОстаткиМатериалов
Движения.СтоимостьМатериалов.БлокироватьДляИзменения = Истина;
Движения.ОстаткиМатериалов.БлокироватьДляИзменения = Истина;
// Запишем пустые наборы записей, чтобы читать остатки без учета данных в документе
Движения.СтоимостьМатериалов.Записать();
Движения.ОстаткиМатериалов.Записать();
РезультатЗапроса = Запрос2.Выполнить();
Управляемая блокировка будет установлена в момент записи этих
наборов записей, то есть как раз перед выполнением второго запроса.
Что нам и требовалось.
Выделение произвольных областей модуля
Итак, мы закончили редактирование процедуры ОбработкаПроведения() в модуле документа ОказаниеУслуги. Хотя модуль содержит
всего одну процедуру, эта процедура довольно объемная (особенно
для неопытного разработчика), и ее нельзя «охватить одним
взглядом». Это затрудняет процесс восприятия и понимания текста
процедуры, поиск в ней ошибок и т.д.
Для удобства разработчика в редакторе модуля существует возможность выделять произвольные области текста, группировать и сворачивать их подобно тому, как сворачиваются инструкции циклов,
условий, процедур и функций. Каждой области текста разработчик
может дать собственное имя и таким образом выделить часть модуля,
имеющую определенное назначение.
Например, в нашей процедуре можно выделить три логические части:
в первой части формируется временная таблица, содержащая перечень
номенклатуры документа; во второй части рассчитывается стоимость
номенклатуры и формируются движения в регистрах нако-пления; и в
третьей части производится контроль остатков номенкла-туры при
оперативном проведении документа.
Выделим эти три области в тексте процедуры, используя инструкции
препроцессору #Область <имя области> и #КонецОбласти
(листинг 14.42).
Листинг 14.42. Процедура «ОбработкаПроведения()»
Процедура ОбработкаПроведения(Отказ, Режим)
Движения.ОстаткиМатериалов.Записывать = Истина;
Движения.СтоимостьМатериалов.Записывать = Истина;
Движения.Продажи.Записывать = Истина;
// Создать менеджер временных таблиц МенеджерВТ =
Новый МенеджерВременныхТаблиц;
#Область НоменклатураДокумента
Запрос = Новый Запрос;
// Укажем, какой менеджер временных таблиц использует этот запрос
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
"ВЫБРАТЬ
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура КАК Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры КАК |
ВидНоменклатуры,
| СУММА(ОказаниеУслугиПереченьНоменклатуры.Количество) КАК
| КоличествоВДокументе, |
СУММА(ОказаниеУслугиПереченьНоменклатуры.Сумма) КАК
| СуммаВДокументе
|ПОМЕСТИТЬ НоменклатураДокумента
|ИЗ
| Документ.ОказаниеУслуги.ПереченьНоменклатуры КАК
| ОказаниеУслугиПереченьНоменклатуры
|ГДЕ
| ОказаниеУслугиПереченьНоменклатуры.Ссылка = &Ссылка |
|СГРУППИРОВАТЬ ПО
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура,
| ОказаниеУслугиПереченьНоменклатуры.Номенклатура.ВидНоменклатуры";
Запрос.УстановитьПараметр("Ссылка", Ссылка);
РезультатЗапроса = Запрос.Выполнить();
#КонецОбласти
#Область ДвиженияДокумента
Запрос2 = Новый Запрос;
Запрос2.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос2.Текст = "ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура КАК Номенклатура,
| НоменклатураДокумента.ВидНоменклатуры КАК ВидНоменклатуры,
| НоменклатураДокумента.КоличествоВДокументе КАК КоличествоВДокументе, |
НоменклатураДокумента.СуммаВДокументе КАК СуммаВДокументе,
| ЕСТЬNULL(СтоимостьМатериаловОстатки.СтоимостьОстаток, 0) КАК |
Стоимость,
| ЕСТЬNULL(ОстаткиМатериаловОстатки.КоличествоОстаток, 0) КАК |
Количество
|ИЗ
| НоменклатураДокумента КАК НоменклатураДокумента
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СтоимостьМатериалов |
.Остатки(
| ,
| Материал В
| (ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)) КАК
| СтоимостьМатериаловОстатки |
ПО НоменклатураДокумента.Номенклатура =
| СтоимостьМатериаловОстатки.Материал
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ОстаткиМатериалов.Остатки( |
,
| Материал В
| (ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)) КАК
| ОстаткиМатериаловОстатки |
ПО НоменклатураДокумента.Номенклатура =
| ОстаткиМатериаловОстатки.Материал";
// Установим необходимость блокировки данных в регистрах //
СтоимостьМатериалов и ОстаткиМатериалов
Движения.СтоимостьМатериалов.БлокироватьДляИзменения = Истина;
Движения.ОстаткиМатериалов.БлокироватьДляИзменения = Истина;
// Запишем пустые наборы записей, чтобы читать остатки без учета данных в документе
Движения.СтоимостьМатериалов.Записать();
Движения.ОстаткиМатериалов.Записать();
РезультатЗапроса = Запрос2.Выполнить();
// ТЗ = РезультатЗапроса.Выгрузить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если ВыборкаДетальныеЗаписи.Количество = 0 Тогда
СтоимостьМатериала = 0;
Иначе
СтоимостьМатериала = ВыборкаДетальныеЗаписи.Стоимость /
ВыборкаДетальныеЗаписи.Количество;
КонецЕсли;
Если ВыборкаДетальныеЗаписи.ВидНоменклатуры =
Перечисления.ВидыНоменклатуры.Материал Тогда
// Регистр ОстаткиМатериалов Расход
Движение = Движения.ОстаткиМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Склад = Склад;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
// Регистр СтоимостьМатериалов Расход
Движение = Движения.СтоимостьМатериалов.Добавить();
Движение.ВидДвижения = ВидДвиженияНакопления.Расход;
Движение.Период = Дата;
Движение.Материал = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Стоимость = ВыборкаДетальныеЗаписи.КоличествоВДокументе *
СтоимостьМатериала;
КонецЕсли;
// Регистр Продажи
Движение = Движения.Продажи.Добавить();
Движение.Период = Дата;
Движение.Номенклатура = ВыборкаДетальныеЗаписи.Номенклатура;
Движение.Клиент = Клиент;
Движение.Мастер = Мастер;
Движение.Количество = ВыборкаДетальныеЗаписи.КоличествоВДокументе;
Движение.Выручка = ВыборкаДетальныеЗаписи.СуммаВДокументе;
Движение.Стоимость = СтоимостьМатериала
* ВыборкаДетальныеЗаписи.КоличествоВДокументе;
КонецЦикла;
Движения.Записать();
#КонецОбласти
#Область КонтрольОстатков
Если Режим = РежимПроведенияДокумента.Оперативный Тогда //
Проверить отрицательные остатки
Запрос3 = Новый Запрос;
Запрос3.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос3.Текст = "ВЫБРАТЬ
| ОстаткиМатериаловОстатки.Материал КАК Материал,
| ОстаткиМатериаловОстатки.КоличествоОстаток КАК КоличествоОстаток |ИЗ
| РегистрНакопления.ОстаткиМатериалов.Остатки( |
,
| Материал В
| (ВЫБРАТЬ
| НоменклатураДокумента.Номенклатура |
ИЗ
| НоменклатураДокумента)
| И Склад = &Склад) КАК ОстаткиМатериаловОстатки
|ГДЕ
| ОстаткиМатериаловОстатки.КоличествоОстаток < 0";
Запрос3.УстановитьПараметр("Склад", Склад);
РезультатЗапроса = Запрос3.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Сообщение = Новый СообщениеПользователю();
Сообщение.Текст = "Не хватает " + Строка(
- ВыборкаДетальныеЗаписи.КоличествоОстаток) +
" единиц материала """ + ВыборкаДетальныеЗаписи.Материал + """";
Сообщение.Сообщить();
Отказ = Истина;
КонецЦикла;
КонецЕсли;
#КонецОбласти
КонецПроцедуры
В результате мы можем свернуть выделенные программные области в
тексте процедуры (рис. 14.32).
Рис. 14.32. Выделение произвольных областей в модуле
Затем можно развернуть только нужную область модуля и работать с
ней. Текст процедуры станет более компактным и читаемым.
В нашем примере мы разбили одну процедуру модуля на три логические части. Но чаще наоборот: когда модуль содержит много
различных процедур, сходные по назначению процедуры можно объединить в группы. Например, в модуле формы можно выделить такие
области, как ПрограммныйИнтерфейс, ОбработчикиСобытий, СлужебныеПроцедурыИФункции. Название этих областей задается самим
разработчиком и говорит само за себя.
Области могут быть вложены друг в друга или в другие группируемые конструкции языка. При расстановке областей в модуле нужно
следить за тем, чтобы области не пересекались между собой и с
другими группируемыми конструкциями. Потому что группировка по
таким областям работать не будет.
В режиме «1С:Предприятие»
Запустим «1С:Предприятие» в режиме отладки и проверим работу
нового обработчика события ОбработкаПроведения, перепроведя все
документы Оказание услуги.
В результате все работает точно так же, с точки зрения пользователя,
но проведение документов организовано методически правильно и
более эффективно с точки зрения доступа к данным.
Теория: устройство кеша
В разделе «Особенности использования ссылочных данных» мы
упомянули о том, что в платформе есть некий кеш, который хранит в
себе данные объектов, читаемых из базы данных. Теперь расскажем о
работе этого механизма подробнее.
Система «1С:Предприятие» использует механизм кеширования
данных объектов, считанных из базы данных при использовании
объектной техники.
Таким образом, для получения реквизитов какого-либо объекта через
ссылку выполняется обращение к кешу объектов, расположенному в
оперативной памяти.
Кеш объектов состоит из двух частей: транзакционного кеша и
обычного кеша. В зависимости от того, происходит ли обращение в
рамках транзакции или нет, в действие вступает тот или иной кеш (рис.
14.33).
Рис. 14.33. Кеш объектов
Все данные, находящиеся в кеше, предназначены только для чтения
(ReadOnly). Таким образом, чтение любых данных, получаемых через
ссылку, выполняется только через кеш объектов, а запись – механизмами самих программных объектов.
Обычный кеш
Если при обращении к обычному кешу требуемых данных в нем нет,
то выполняется чтение данных объекта из базы данных и сохранение
их в кеше. Уникальным идентификатором для кеша в данном случае
будет являться ссылка на объект базы данных. Поэтому данные
каждого считанного объекта могут существовать в кеше в одном из
двух видов: либо все данные объекта, либо представление объекта.
Таким образом, если мы обратимся к кешу для получения представления объекта и в кеше есть информация для нашей ссылки, данные
будут взяты из кеша (если в кеше весь объект, нужное представление
будет получено из данных объекта).
Если в кеше нет информации для нашей ссылки, из базы данных в кеш
будут считаны только поля, необходимые для формирования
представления объекта.
Если мы обратимся к кешу для получения реквизита объекта и в кеше
есть информация для нашей ссылки, дальнейшие действия будут
зависеть от того, что находится в кеше.
Если в кеше весь объект, значение реквизита будет получено из кеша.
Если в кеше представление объекта, оно будет удалено из кеша, и в
кеш будут считаны все данные объекта. Если же при получении
реквизита объекта в кеше нет информации для нашей ссылки, из базы
данных будут считаны все поля объекта.
Считанные данные будут находиться в кеше до тех пор, пока не
наступит одно из следующих событий:
 считанные данные будут вытеснены из кеша другими
считанными данными других объектов (переполнение кеша);
 при очередном обращении к кешу окажется, что считанные данные были изменены в базе данных;
 закончится интервал времени в 20 минут.
Все считанные данные помещаются в последовательную очередь, и,
поскольку объем кеша ограничен, наиболее старые данные будут
вытесняться из кеша последними считанными.
При повторном обращении к кешу за данными уже считанного объекта
будет анализироваться интервал времени, прошедший с момента
появления данных в кеше.
Если обращение происходит в пределах 20 секунд после посту-пления
данных в кеш, данные считаются верными (валидными). Если
интервал превысил 20 секунд, будет выполняться проверка на соответствие версии данных, хранящихся в кеше, версии данных, находящихся в базе данных.
Если окажется, что версии данных не совпадают (т.е. произошло
изменение данных в базе данных), данные, находящиеся в кеше, будут
удалены из него, и будет выполнено повторное считывание данных из
базы данных. Начиная с этого момента, идет отсчет следующего 20-
секундного интервала валидности этих данных.
Кроме всех вышеперечисленных событий считанные данные будут
удалены из кеша по истечении 20 минут после их последнего считывания из базы данных.
Таким образом, при последовательном выполнении двух операторов
(листинг 14.43), где Номенклатура – это ссылка на объект справочника, на выполнение второго оператора будет тратиться гораздо
меньше времени, поскольку в первом случае будет выполняться
обращение к базе данных, а во втором – чтение из оперативной памяти
(кеша объектов).
Листинг 14.43. Последовательность операторов
А = Номенклатура.Наименование;
В = Номенклатура.ВидНоменклатуры;
Транзакционный кеш
Если обращение к данным происходит в рамках транзакции, то оно
переадресуется транзакционному кешу. В рамках транзакции в
«1С:Предприятии» выполняются все операции, приводящие к
изменению данных в базе данных. Например, в рамках транзакции
выполняется обработка проведения документа.
Транзакция – это неделимая последовательность манипулирования
данными, переводящая базу данных из одного целостного состояния в
другое. Если по каким-то причинам одно из действий транзакции
невыполнимо, база данных возвращается в то состояние, которое было
до начала транзакции (происходит откат транзакции – Rollback).
Транзакционный кеш по сути представляет собой ту же последовательную очередь, что и обычный кеш. Разница заключается в том, что
все данные, находящиеся в транзакционном кеше, являются
валидными (гарантированно актуальными).
При считывании данных в транзакционный кеш устанавливается
блокировка на данные в базе данных, поэтому они гарантированно не
могут быть изменены до окончания транзакции.
Транзакционный кеш хранит считанные данные до тех пор, пока они
не будут вытеснены более поздними или пока не закончится транзакция. По окончании транзакции кеш очищается, однако действия,
выполняемые при этом, зависят от состояния завершения транзакции.
Если транзакция завершена успешно (Commit), данные всех объектов,
содержащиеся в транзакционном кеше, переносятся в обычный кеш, а
транзакционный кеш очищается (рис. 14.34).
Рис. 14.34. Транзакция завершена успешно
Если был выполнен отказ от изменений (Rollback), то просто
очищается транзакционный кеш (рис. 14.35).
Рис. 14.35. Очистка транзакционного кеша при отказе от изменений