Периодические реквизиты: как есть, как может быть (Часть 2)
Начнем
В одной из своих прошлых статей я рассматривал как организованы периодические реквизиты в системе 1С:Предприятие. В ней было указано, что их хранение и обработка не являются оптимальными. Было предложено два метода моделирования периодических реквизитов:
С помощью подчиненного справочника на каждый реквизит
С помощью подчиненного справочника с несколькими реквизитами
В своей статье я не указал, что стандартными средствами 1С производить выборку из данного справочника будет совершенно неудобно. Например, в самом простом запросе для поиска реквизита на определенную дату придется выбирать все элементы по дате меньшей заданной. То есть примерно так:
Функция глВыбратьИсториюОС(ОС,Д) Экспорт
ТЗ = "Владелец = Справочник.ОС_ИсторияОС.Владелец;
|ТекущийЭлемент = Справочник.ОС_ИсторияОС.ТекущийЭлемент;
|ДатаИст = Справочник.ОС_ИсторияОС.ТекущийЭлемент.Дата;
|Группировка ТекущийЭлемент упорядочить по ТекущийЭлемент.Дата;
|Условие(Владелец = ОС);
|Условие(ДатаИст <= Д);
|";
Запрос = СоздатьОбъект("Запрос");
Если Запрос.Выполнить(ТЗ)=1 Тогда
Если Запрос.Группировка(1,-1)=1 Тогда
Возврат Запрос.ТекущийЭлемент;
КонецЕсли;
КонецЕсли;
Возврат 0;
КонецФункции
Согласитесь, что это не то чего хотелось бы.
Все течет, все изменяется. И это здорово! Не так давно в списке внешних компонент появился на свет еще один замечательный экземпляр — ВК 1С++. Про возможности данной компоненты вы можете прочесть здесь. Если сказать коротко о возможностях ВК, то это ООП на платформе 1С 7.7. Причем здесь ООП (объектно-ориентированное программирование) спросите вы? А притом, что ВК позволяет с легкостью создавать собственные объекты, наделенные нужной вам функциональностью и затем обращаться к ним как к обычным объектам 1С.
Итак, мы можем создавать собственные объекты! Что ж - прекрасно! Давайте создадим собственную реализацию периодических реквизитов.
Новый объект назовем "История". Дело в том, что основная функция объекта - хранение информации (ресурсы) в разрезе нужных измерений на определенную дату. Аналогичными возможностями обладает объект регистр сведений в новой версии системы 1С 8.0.
Конечно, при реализации данного объекта мы не обойдемся без ВК ToySQL. Ее мы будем использовать как движок для работы с источником данных объекта. Так… Вот мы и добрались до самого интересного - как мы будем хранить наш объект в базе. Таблицу ИБ 1С, в которой будет хранить данные наш объект, я буду называть источником данных. Чтобы реализовать ссылочную целостность желательно, чтобы это был объект из дерева конфигурации 1С. Первое, что приходит на ум — использовать в качестве источника данных справочник. После некоторых раздумий (а на самом деле мучительных дней тестирования и поиска альтернатив) находим, что в качестве источника данных можно использовать:
Обычный справочник
Подчиненный справочник
Регистр
Первые два варианта понятны. По сути, регистр сведений (так я буду называть объект "История", если подразумеваю связь с его прототипом в 8.0) является справочником, в котором выделены специальные типы реквизитов: измерения и ресурсы. Также только эти два варианта подходят, если запись движений может производиться вручную (и это тоже возможно). Эти варианты удобнее, если движения нужно отображать. Но. Есть несколько "но" при их использовании: наличие двух полей в таблице справочника, которые нужно чем-то заполнять. Это несложно — просто нужно вычислять следующее (новое) значение этих двух полей (ROW_ID, ID). Здесь есть одна тонкость — со временем новые значения (если использовать ХП 1С) могут закончиться. Происходит это по той причине, что новое значение вычисляется как последнее + 1. Можно несколько усовершенствовать эту ХП, чтобы можно было новое значение выбирать сначала исходя из минимального значения, но это все равно не выход. Таким образом, справочник не предназначен для частых изменений.
На выход нам приходит регистр. Не зря ведь регистр сведений назвали "регистром". Но в чистом виде использовать регистры 7.7 использовать нельзя - нужно избежать нежелательного пересчета итогов. И как оказывается это возможно! Просто перенесем все наши атрибуты в ветку реквизиты. В этом случае в регистре нет ни измерений, ни ресурсов и поэтому итоги не рассчитываются. На самом деле они рассчитываются, но довольно быстро, а при желании ХП, которые отвечают за этот пересчет можно удалить. К тому же запись в таблицу движений мы будем осуществлять напрямую, о чем читайте далее.
Реализуем
Источник данных у нас имеется. Это либо справочник, либо регистр. Но нам необходимо добавить свои параметры для данного объекта в конфигурацию. Здесь нам приходят на помощь печатные формы или таблицы. Удобнее было бы использовать таблицы из форм списков выбранных нами источников, но с другой стороны это неудобно тем, что они доступны только в модулях форм списков. Поэтому будем использовать общие таблицы, которые назовем также как и объекты источников данных.
Основные параметры, которые нам необходимо указать, это то какие атрибуты будут являться измерениями, а какие ресурсами. В случае, когда источник данных справочник, добавляются параметры контроля записи движений в историю. Параметры аналогичны параметрам периодического реквизита: запись документами и ручное изменение. Подробное описание параметров смотрите в документации. Также в параметрах можно указать дополнительные индексы.
Кроме того, что мы выбрали источник данных, для нормальной работы мы должны создать дополнительные индексы. С помощью менеджера индексов (внешняя обработка) в DDS файл добавляются системные индексы и индексы, описанные в параметрах. Системные индексы это:
· Индекс по измерениям. В источнике должен обязательно присутствовать реквизит "Дата", который также (автоматически) добавляется в список измерений. Порядок описаний измерений влияет на создание индекса. В каком порядке измерения описаны в таком порядке и создается индекс. · Индекс по документу (регистратору). Только в случае справочника, если в реквизитах определен реквизит "Регистратор"
Желательно (если используется метод ВыбратьЗаПериод) определять в параметрах индекс по дате.
Теперь опишем наш класс. Будем использовать два класса: для источника данных справочник и регистр
класс ИсторияСправочник = F:BASESHistoryhistref.ert {
// Инициализация // в модуле проведения или модуле формы документа void НазначитьРегистратор(Документ Регистратор); Число НазначитьВид(Строка Вид); Число ПриЗаписи(Справочник История); // вызывается из модуля формы при ручном изменении // вызывается для контроля редактирования при ручном изменении Число ПриРедактировании(Справочник История); // Выборка void СрезПоследних(ТаблицаЗначений Результат, Дата НаДату, СписокЗначений Группировка, СписокЗначений Условия); СписокЗначений Получить(Дата НаДату,...); // в списке - значения ресурсов void ВыбратьДвижения(ТаблицаЗначений Результат, Документ ВыбРегистратор); // для работы метода лучше добавить индекс по дате void ВыбратьЗаПериод(ТаблицаЗначений Результат, Дата НачалоПериода,Дата КонецПериода); // установить в форме списка отбор ч/з доп. реквизит void УстановитьОтбор(Неопределенный КонтекстФормы,Строка Отбор,...); // Запись из модуля проведения документа void НовоеДвижение(); // создать новое движение void УстановитьАтрибут(Строка Имя,Неопределенный Значение); void ОтменаДвижений(); // очистить буфер движений созданный с помощью НовоеДвижение Число ЗаписатьДвижения(Число ИспользоватьТранзакцию = 0); // при проведении (1 - успешно, 0 - нет) void УдалитьДвижения(); // при отмене проведения // Сервисные функции // очистить историю до даты не включая void ОчиститьДвижения(Дата ДоДаты, СписокЗначений Условия,Число ПоДокументам = 0);
}
класс ИсторияРегистр = F:BASESHistoryhistreg.ert {
// Инициализация // в модуле проведения или модуле формы документа void НазначитьРегистратор(Документ Регистратор); void ПривязыватьСтроку(Число НомерСтроки); Число НазначитьВид(Строка Вид); // Выборка void СрезПоследних(ТаблицаЗначений Результат, Дата НаДату, СписокЗначений Группировка, СписокЗначений Условия); СписокЗначений Получить(Дата НаДату,...); // в списке - значения ресурсов void ВыбратьДвижения(ТаблицаЗначений Результат, Документ ВыбРегистратор); void ВыбратьЗаПериод(ТаблицаЗначений Результат, Дата НачалоПериода,Дата КонецПериода); // Запись из модуля проведения документа void НовоеДвижение(); // создать новое движение void УстановитьАтрибут(Строка Имя,Неопределенный Значение); void ОтменаДвижений(); // очистить буфер движений созданный с помощью НовоеДвижение Число ЗаписатьДвижения(); // при проведении (1 - успешно, 0 - нет) void УдалитьДвижения(); // при отмене проведения // Сервисные функции void ОчиститьДвижения(Дата ДоДаты, СписокЗначений Условия);
}
Рассмотрим основные методы объекта.
Метод ЗаписатьДвижения используется для записи движений из модуля проведения документа. В случае использования справочника мы пользуемся методами создания и записи элементов стандартного объекта 1С:
ИсточникДанных.Новый();
Для НомерРеквизита = 1 по УстановитьРеквизитов Цикл
В случае использования регистра, во-первых, чтобы избежать пересчета итогов, а во-вторых, чтобы увеличить скорость записи, будем записывать движения напрямую в таблицу с помощью ToySQL:
Метод УдалитьДвижения примерно одинаково работает в обоих случаях — удаляем записи напрямую:
Процедура УдалитьДвижения() Экспорт
а_Запрос.Соединиться(0); // в транзакции 1С
ДокИД = а_Запрос.ЗначВSQL(а_Регистратор); а_Запрос.Выполнить("DELETE FROM RA"+а_НомерРегистра+" WHERE IDDOC = '"+ДокИД+"'"); а_Запрос.Выполнить("UPDATE _1SJOURN SET RF"+а_НомерРегистра+" = 0 WHERE IDDOC = '"+ДокИД+"'");
КонецПроцедуры //УдалитьДвижения
Для методов выборки главное шаблон запроса. Рассмотри два метода Получить и СрезПоследних. Первый метод получает на определенную дату все ресурсы по сочетанию всех измерений (в строго заданном в параметрах порядке) — аналог метода Получить периодического реквизита. Шаблон запроса:
ТекстЗапроса = "SELECT TOP 1 "+Ресурсы+" |FROM ["+а_ИсточникДанных+"] Рег |WHERE "+УсловияСтр+" and ["+а_ИсточникДанных+".Дата] = |(SELECT (MAX(["+а_ИсточникДанных+".Дата])) | FROM ["+а_ИсточникДанных+"] | WHERE "+УсловияСтр+" |) |ORDER BY [Рег.Документ] ASC";
Метод СрезПоследних выбирает на определенную дату значения ресурсов, в любом сочетании измерений, по заданным условиям. Код формирования шаблона метода:
н = Найти(ГруппировкаСтр, ","); Пока н > 0 Цикл
Измерение = Лев(ГруппировкаСтр, н - 1);
глДобавитьРеквизит(Поля,",","[RESULT."+Измерение+"]"); глДобавитьРеквизит(ПоляМакс,",","["+Измерение+"] "+Измерение); глДобавитьРеквизит(ГруппировкаМакс,",","["+Измерение+"]"); глДобавитьРеквизит(УсловияСоединения," and ","[RESULT."+Измерение+"] = [MAXDATE."+ Измерение+"]");
ГруппировкаСтр = Сред(ГруппировкаСтр, н + 1); н = Найти(ГруппировкаСтр, ",");
КонецЦикла;
глДобавитьРеквизит(Поля,",","[RESULT."+ГруппировкаСтр+"]"); глДобавитьРеквизит(ПоляМакс,",","["+ГруппировкаСтр+"] "+ГруппировкаСтр); глДобавитьРеквизит(ГруппировкаМакс,",","["+ГруппировкаСтр+"]"); глДобавитьРеквизит(УсловияСоединения," and ","[RESULT."+ГруппировкаСтр+"] = [MAXDATE."+ ГруппировкаСтр+"]");
глДобавитьРеквизит(УсловияСтр," and ","["+Измерение+"] "+ТипУсловия+ " [@Условия.ПолучитьЗначение("+НомерИзмерения+")]");
КонецЦикла;
глДобавитьРеквизит(УсловияСтр," and ","["+а_ИсточникДанных+".Дата] <= [@НаДату]");
глДобавитьРеквизит(УсловияСоединения," and ","(convert(char(8),[RESULT.Дата]))+ [RESULT.Документ] = MAXDATE.MAXFIELD");
ТекстЗапроса = " |SELECT "+Поля+" |FROM ["+а_ИсточникДанных+"] RESULT |INNER JOIN |(SELECT "+ПоляМакс+",(MAX(convert(char(8),["+а_ИсточникДанных+".Дата])+ ["+а_ИсточникДанных+".Документ])) MAXFIELD | FROM ["+а_ИсточникДанных+"] | WHERE "+УсловияСтр+" | GROUP BY "+ГруппировкаМакс+" |) MAXDATE |ON "+УсловияСоединения;
Для удобства создания объекта история определенного вида добавим в глобальный модуль следующую функцию.
Функция СоздатьОбъектEx(СтрОбъект,Регистратор = 0) Экспорт
н = Найти(СтрОбъект,"."); Объект = СоздатьОбъект(Лев(СтрОбъект, н - 1)); Объект.НазначитьВид(Сред(СтрОбъект, н + 1)); Объект.НазначитьРегистратор(Регистратор);
Возврат Объект;
КонецФункции //СоздатьОбъектРасш
Вот и все. Основные моменты реализации описаны. Хотел бы заметить, что в качестве движка для работы с ИБ можно использовать любые другие методы, как то: Rainbow, ADO, ODBCSQL & etc. Тут уже, как говориться, дело вкуса.
Используем
Напоследок пример реального использования (в поставке идет также демо-конфигурация). В одной из разрабатываемых нашей фирмой конфигураций необходимо фиксировать закупочные цены поставщиков (колхозы, поставщики молока) по сорту и виду цены (за сорт, за охлаждение). Для этого создается документ приказ по ценам. Приказ может быть издан как для всех поставщиков, для района, так и для конкретного хозяйства.
Приказ имеет срок действия, цена зависит также от объема поставки за день и текущего долга в днях. При вводе документа поступления (приемщик молока), должны проставляться текущие цены:
ТабНаДату = СоздатьОбъект("ТаблицаЗначенийEx"); Условия = СоздатьОбъект("СписокЗначений");
УсловиеКонтрагент = СоздатьОбъект("СписокЗначений"); УсловиеКонтрагент.ДобавитьЗначение(Контрагент); Район = Контрагент.Родитель; Если Район.Выбран()=1 Тогда УсловиеКонтрагент.ДобавитьЗначение(Район); ВсеХозяйства = Район.Родитель; Если ВсеХозяйства.Выбран()=1 Тогда УсловиеКонтрагент.ДобавитьЗначение(ВсеХозяйства); КонецЕсли; КонецЕсли;
Условия.Установить("Контрагент,in",УсловиеКонтрагент); // дата документа не первысила срок действия Условия.Установить("СрокДействия,>=",ДатаДок); // поиск ограничений по объему больше или равному текущему Условия.Установить("ОбъемДолг,>=",КлючПоиск);
ТабНаДату.ВыбратьСтроки(); Пока ТабНаДату.ПолучитьСтроку() = 1 Цикл ТабНаДату.Уровень = ТабНаДату.Контрагент.Уровень(); // уровень в справочнике КонецЦикла; // Сортировать по уровню контрагента (чем больше тем лучше) // и по объему минимальному из выбранных максимальных ТабНаДату.Сортировать("ТипЦены,Сорт,Уровень-,ОбъемДолг+"); // Выбрать только "максимально соответсвующие" строки ТабЦен = ТабНаДату.УдалитьДубли("ТипЦены,Сорт","Цена");
Иначе
Сообщить("Не найден приказ по ценам для "+Контрагент);
КонецЕсли;
Итоги
Без комментариев. Это ведь не курсовая работа. Скажем так раздел Итоги или Выводы будет для вас разделом Заметки, в котором каждый для себя сделает соответствующие выводы.