Разработка обработок для загрузки данных из внешних источников (например, Microsoft Excel) — одна из наиболее частых задач программиста 1С. В среде «1С:Предприятие 7.7» (которую ласково называют «клюшками») эта задача имеет свои архитектурные особенности. Особенно часто вопросы возникают при работе со справочниками, имеющими сложную структуру: подчинение и периодические реквизиты. Типичным примером является справочник Цены, который подчинен справочнику Номенклатура.
В этой статье мы подробно разберем, почему может не работать обновление уже существующих цен, как правильно позиционироваться на подчиненных элементах и как корректно записывать периодические значения, избегая критических ошибок выполнения системы.
Рассмотрим классическую ошибку, с которой часто сталкиваются начинающие разработчики при работе с выборками в 1С 7.7. Посмотрим на исходный фрагмент кода, который выполняет поиск существующей цены:
ЕстьВыборка=СпрЦен.ВыбратьЭлементы();
Если ЕстьВыборка=0 тогда // создаем новый
СпрЦен.Новый();
// Заполнение реквизитов...
СпрЦен.Записать();
ИначеЕсли ЕстьВыборка=1 тогда // меняем просто значение цены на текущую дату
СпрЦен.Цена=ЦенаХЛ;
СпрЦен.Записать();
КонецЕсли;
Выясним причину, почему ветка изменения цены срабатывает некорректно. Метод ВыбратьЭлементы() возвращает единицу (1), если в справочнике по заданному фильтру (в данном случае — по владельцу) есть хотя бы один элемент, и ноль (0), если элементов нет.
Однако вызов метода ВыбратьЭлементы() лишь открывает выборку, но не позиционирует объект базы данных на конкретном элементе. Переменная СпрЦен в этот момент указывает на «пустой» элемент справочника. Пытаясь выполнить присвоение СпрЦен.Цена = ЦенаХЛ без предварительного считывания элемента, мы совершаем ошибку: система не понимает, для какого именно элемента нужно изменить реквизит.
Для корректного позиционирования внутри открытой выборки обязательно нужно использовать метод ПолучитьЭлемент(). Разберем правильную структуру обхода выборки по шагам:
СпрЦен.ИспользоватьВладельца(ТМЦ);
Если СпрЦен.ВыбратьЭлементы() = 0 Тогда
// Если выборка пуста, гарантированно создаем новый элемент
СпрЦен.Новый();
СпрЦен.Валюта = Константа.БазоваяВалюта;
СпрЦен.Единица = ТМЦ.БазоваяЕдиница;
СпрЦен.КатегорияЦены = Спис;
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
Иначе
// Если элементы найдены, обходим их в цикле
Пока СпрЦен.ПолучитьЭлемент() = 1 Цикл
// Проверяем соответствие категории цены (если нужно обновить конкретную)
Если СпрЦен.КатегорияЦены = Спис Тогда
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
КонецЕсли;
КонецЦикла;
КонецЕсли;
Обратите внимание, что в 1С 7.7 отсутствует метод ПолучитьОбъект(), характерный для платформы 1С 8.x. Работа с данными здесь происходит непосредственно через позиционированный объект справочника.
Хотя обход выборки через цикл Пока ... ПолучитьЭлемент() = 1 является рабочим решением, его нельзя назвать оптимальным с точки зрения производительности, особенно если у одного товара может быть много типов цен или история изменений слишком велика.
Рассмотрим более элегантный способ позиционирования на нужном элементе подчиненного справочника — метод НайтиПоРеквизиту() с флагом локального поиска. Если предварительно задать владельца, этот метод выполнит поиск целевого элемента строго внутри подчинения конкретной номенклатуры:
СпрЦен.ИспользоватьВладельца(ТМЦ);
// Третий параметр "0" указывает искать только среди элементов данного владельца
Если СпрЦен.НайтиПоРеквизиту("КатегорияЦены", Спис, 0) = 1 Тогда
// Элемент успешно найден и объект СпрЦен позиционирован на нем.
// Переходим к обновлению цены.
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
Иначе
// Элемент не найден, создаем новую категорию цены для товара
СпрЦен.Новый();
СпрЦен.Валюта = Константа.БазоваяВалюта;
СпрЦен.Единица = ТМЦ.БазоваяЕдиница; // Важно использовать подчиненный справочник единиц
СпрЦен.КатегорияЦены = Спис;
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
КонецЕсли;
Важное требование: Для того чтобы метод НайтиПоРеквизиту() работал, у реквизита КатегорияЦены в конфигураторе обязательно должен быть установлен флаг «Сортировка». Без этого индекса поиск вызовет ошибку времени выполнения.
В типовых конфигурациях 1С 7.7 (например, «Торговля и Склад») реквизит Цена в справочнике цен является периодическим. Это означает, что его значение хранится в привязке к датам истории. Запись таких реквизитов имеет свои правила.
Проанализируем два альтернативных способа работы с периодическими реквизитами:
Когда мы вызываем метод СпрЦен.ИспользоватьДату(НужнаяДата), мы временно переводим объект справочника в режим работы, при котором все периодические реквизиты ведут себя как обычные реквизиты. Присвоение СпрЦен.Цена = ЦенаХЛ запишет значение именно на дату, переданную в функцию ИспользоватьДату().
Важное предостережение: Если вы применили к объекту справочника метод ИспользоватьДату(), использовать метод прямого изменения периодических значений Установить() для этого же объекта категорически запрещено. Код вида СпрЦен.Цена.Установить(Дата, Значение) вызовет фатальную ошибку: «Значение не представляет агрегатный объект».
Если метод ИспользоватьДату() для объекта справочника не вызывался, правильный синтаксис установки периодического значения выглядит следующим образом:
СпрЦен.Цена.Установить(ТекущаяДата(), ЦенаХЛ);
Особенность метода Установить() заключается в том, что он сразу физически записывает значение периодического реквизита в базу данных на указанную дату. При этом вызывать метод всего объекта Записать() не требуется (если только вы параллельно не меняли непериодические реквизиты, такие как Единица или Валюта).
Для сложных манипуляций с историей значений и во избежание блокировок транзакций опытные разработчики часто используют специализированный системный объект Периодический. Он работает напрямую со служебной таблицей истории значений. Рассмотрим пример перезаписи цены с помощью этого объекта:
ПериодЦена = СоздатьОбъект("Периодический");
// Связываем периодический объект с конкретным реквизитом конкретного элемента справочника
ПериодЦена.ИспользоватьОбъект("Цена", СпрЦен.ТекущийЭлемент());
ПериодЦена.ДатаЗнач = ТекущаяДата();
ПериодЦена.Значение = ЦенаХЛ;
ПериодЦена.Записать();
Такой подход обеспечивает максимальную надежность при обработке больших массивов данных или в распределенных базах.
В типовой конфигурации «Торговля и Склад 7.7» цена товара жестко привязана к единице измерения. Сами единицы измерения (штуки, коробки) хранятся в отдельном справочнике Единицы, который также подчинен справочнику Номенклатура.
Запись в поле Единица простого текста или неверной ссылки (например, ссылки на общий классификатор ОКЕИ напрямую вместо элемента подчиненного справочника) приведет к серьезным ошибкам. Программа не сможет рассчитать коэффициенты пересчета при оформлении документов (например, накладных), что приведет к нулевым суммам или ошибкам проведения.
Убедитесь, что вы связываете цену с корректным подчиненным элементом единицы. Рекомендуется использовать базовую или основную единицу товара:
СпрЦен.Единица = ТМЦ.БазоваяЕдиница; // или ТМЦ.ОсновнаяЕдиница (в зависимости от метаданных вашей базы)
Сведем все лучшие практики, рассмотренные нами, в единый готовый программный модуль для загрузки цен из Excel:
Товар = СоздатьОбъект("Справочник.Номенклатура");
СпрЦен = СоздатьОбъект("Справочник.Цены");
Т1 = Прав((Строка(Excel.Cells(ТекСтрока, СтлбКод).Value)), 5);
Т2 = "0000" + Т1;
ХХ = Прав(Т2, 5);
Если Товар.НайтиПоКоду(ХХ, 0) = 1 Тогда
ТМЦ = Товар.ТекущийЭлемент();
ЦенаХЛ = Окр(Число(СокрЛП(Строка(Excel.Cells(ТекСтрока, СтлбЦена_Розн).Value))), 2, 1);
Если ЦенаХЛ > 0 Тогда
// Задаем владельца для ограничения области поиска
СпрЦен.ИспользоватьВладельца(ТМЦ);
// Переходим в режим работы с периодическими реквизитами на текущую дату
СпрЦен.ИспользоватьДату(ТекущаяДата());
// Быстро ищем существующую категорию цены у этого товара
Если СпрЦен.НайтиПоРеквизиту("КатегорияЦены", Спис, 0) = 1 Тогда
// Элемент найден и позиционирован. Обновляем периодическую цену
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
Сообщить("Обновлена цена для товара: " + ТМЦ.Наименование);
Иначе
// Элемента нет, создаем новую карточку цены
СпрЦен.Новый();
СпрЦен.Валюта = Константа.БазоваяВалюта;
// Заполняем правильную подчиненную единицу измерения товара
СпрЦен.Единица = ТМЦ.БазоваяЕдиница;
СпрЦен.КатегорияЦены = Спис;
СпрЦен.Цена = ЦенаХЛ;
СпрЦен.Записать();
Сообщить("Создана новая цена для товара: " + ТМЦ.Наименование);
КонецЕсли;
КонецЕсли;
Иначе
Сообщить("Товар с кодом " + ХХ + " не найден в базе данных!", "!");
КонецЕсли;
Используя этот оптимизированный алгоритм, мы исключаем лишние полные сканирования базы данных, защищаем код от непредвиденных ошибок позиционирования выборки и корректно формируем историю периодических реквизитов.