Как реализовать прикрепление сканов к документам в 1С, если в конфигурации нет БСП?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При разработке и сопровождении старых или сильно кастомизированных конфигураций 1С (например, БГУ 1.0, УТ 10.3) часто возникает задача прикрепления внешних файлов и сканов к документам. В современных конфигурациях этот функционал «из коробки» закрывается внедрением Библиотеки стандартных подсистем (БСП). Но что делать, если БСП в вашей базе отсутствует, а пользователям жизненно необходимо прикреплять сканы счетов или накладных к документу ПриходТоваров? Для автоматизации этого процесса можно использовать решение для штрихкодирования и автоматической загрузки сканов документов.

В рамках этой статьи мы подробно разберем, как спроектировать и написать подсистему хранения файлов с нуля. Мы проанализируем два полярных подхода: хранение бинарных данных непосредственно в базе данных 1С и хранение файлов во внешнем томе на диске.

Архитектурный выбор: база данных или сетевой диск?

Перед тем как перейти к написанию кода, мы должны четко взвесить все преимущества и недостатки обоих вариантов хранения файлов. От этого выбора зависит производительность всей информационной системы в будущем.

Вариант 1. Хранение файлов внутри базы данных (СУБД)

При таком подходе файлы преобразуются в двоичные данные и записываются в специальное поле таблицы СУБД с типом ХранилищеЗначения.

Преимущества:

Недостатки:

Вариант 2. Хранение файлов во внешнем каталоге (на диске сервера)

В этом случае файлы физически копируются в выделенную сетевую папку на сервере, а в базе данных 1С фиксируется только текстовая ссылка (путь) на этот файл.

Преимущества:

Недостатки:

Проанализировав обе стороны, мы выводим золотое правило: если база файловая, пользователей мало (до 5-10 человек) и суммарный объем файлов не превысит пары гигабайт — допустимо хранение в базе. Во всех остальных случаях (клиент-серверный вариант, активный документооборот) настоятельно рекомендуется использовать внешнее хранилище.

---

Реализация Варианта 1: Хранение файлов внутри базы данных

Если мы приняли решение хранить сканы в СУБД, ни в коем случае нельзя добавлять реквизит с типом ХранилищеЗначения непосредственно в сам документ ПриходТоваров. Это приведет к тому, что при выборке документов или открытии списков система будет пытаться прочитать тяжелые данные файлов, что вызовет сильные зависания интерфейса. Мы должны спроектировать отдельный подчиненный справочник.

Создадим новый справочник в конфигураторе со следующими свойствами:

Разберем по шагам, как программно поместить файл в базу данных. Для этого нам понадобится использовать механизм сжатия данных, чтобы минимизировать нагрузку на СУБД. Напишем процедуру записи файла:


Процедура ЗаписатьФайлВБазу(ДокументСсылка, ПолныйПутьКФайлу) Экспорт
    
    ФайлНаДиске = Новый Файл(ПолныйПутьКФайлу);
    Если Не ФайлНаДиске.Существует() Тогда
        ВызватьИсключение "Указанный файл не найден на диске!";
    КонецЕсли;
    
    // Получаем двоичные данные файла
    ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ПолныйПутьКФайлу);
    
    // Помещаем данные в ХранилищеЗначения с максимальным сжатием (уровень 9)
    ХранилищеФайла = Новый ХранилищеЗначения(ДвоичныеДанныеФайла, Новый СжатиеДанных(9));
    
    // Создаем запись в нашем подчиненном справочнике
    НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
    НовыйФайл.Владелец    = ДокументСсылка;
    НовыйФайл.Наименование = ФайлНаДиске.ИмяБезРасширения;
    НовыйФайл.ИмяФайла     = ФайлНаДиске.Имя;
    НовыйФайл.Расширение   = Сред(ФайлНаДиске.Расширение, 2); // убираем точку из расширения
    НовыйФайл.ФайлДанные   = ХранилищеФайла;
    
    Попытка
        НовыйФайл.Записать();
        Сообщить("Файл успешно прикреплен к документу!");
    Исключение
        ЗаписьЖурналаРегистрации("УправлениеФайлами", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
        ВызватьИсключение "Не удалось сохранить файл в базу данных.";
    КонецПопытки;
    
КонецПроцедуры

Для обратного процесса — извлечения файла из базы данных и открытия его пользователю — нам необходимо выгрузить бинарные данные из СУБД во временный каталог на компьютере пользователя и запустить файл средствами операционной системы. Рассмотрим этот код:


Процедура ОткрытьФайлИзБазы(СсылкаНаФайл) Экспорт
    
    // Получаем двоичные данные из хранилища значения
    ДвоичныеДанныеФайла = СсылкаНаФайл.ФайлДанные.Получить();
    
    Если ДвоичныеДанныеФайла = Неопределено Тогда
        Предупреждение("Файл поврежден или не содержит данных!");
        Возврат;
    КонецЕсли;
    
    // Формируем уникальный путь во временном каталоге пользователя
    ИмяВременногоФайла = ПолучитьИмяВременногоФайла(СсылкаНаФайл.Расширение);
    
    Попытка
        // Записываем файл на локальный диск
        ДвоичныеДанныеФайла.Записать(ИмяВременногоФайла);
        
        // Запускаем файл ассоциированным в Windows приложением
        ЗапуститьПриложение(ИмяВременногоФайла);
    Исключение
        ВызватьИсключение "Не удалось открыть файл: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецПроцедуры

---

Реализация Варианта 2: Хранение файлов во внешнем каталоге на сервере

Если мы выбрали путь внешнего хранения, нам необходимо разработать надежную архитектуру папок, которая исключит конфликты имен и не снизит скорость работы операционной системы на сервере.

Разберем три важнейших правила организации дискового архива:

  1. Использование относительных путей. Никогда не пишите в базу абсолютные пути вроде D:\Shares\1C_Files\scan.pdf. Если завтра ваш сервер переименуют или вы решите перенести файлы на другой диск, все ссылки станут недействительными. Храните в константе базовый путь (например, \\Server1C\FilesShare\), а в справочнике файлов — только относительный путь внутри этого каталога.
  2. Иерархия каталогов на диске. Если вы положите 50 000 файлов в одну папку, файловая система NTFS начнет сильно тормозить при попытке обратиться к любому файлу. Мы рекомендуем программно распределять файлы по папкам дат (например, по структуре Год\Месяц\День\).
  3. Переименование файлов на диске. Пользователи часто называют сканированные документы одинаково (например, скан.pdf, 1.jpg). Чтобы файлы не перезаписывали друг друга на сервере, при сохранении переименовывайте их в уникальное имя, генерируемое на основе GUID объекта 1С, сохраняя оригинальное «человеческое» имя только в реквизитах справочника.

Спроектируем справочник ПриходТоваровПрисоединенныеФайлы под данный вариант работы. Нам понадобятся реквизиты:

Рассмотрим по шагам программный алгоритм сохранения файла на диск с распределением по датам и генерацией уникального имени:


Функция ПолучитьБазовыйКаталогФайлов()
    // В реальной системе это значение должно считываться из константы
    Возврат "\\ServerApp1C\StorageFiles1C\"; 
КонецФункции

Процедура СохранитьФайлНаДиске(ДокументСсылка, ИсходныйПутьКФайлу) Экспорт
    
    БазовыйПуть = ПолучитьБазовыйКаталогФайлов();
    ФайлИсточник = Новый Файл(ИсходныйПутьКФайлу);
    
    Если Не ФайлИсточник.Существует() Тогда
        ВызватьИсключение "Файл-источник не существует!";
    КонецЕсли;
    
    // Строим структуру папок по текущей дате
    ТекущаяДатаДок = ТекущаяДата();
    ОтносительныйКаталог = Формат(ТекущаяДатаДок, "ДФ=yyyy") + "\" 
                         + Формат(ТекущаяДатаДок, "ДФ=MM") + "\" 
                         + Формат(ТекущаяДатаДок, "ДФ=dd") + "\";
                         
    ПолныйКаталогНазначения = БазовыйПуть + ОтносительныйКаталог;
    
    // Создаем каталог на диске, если он еще не существует
    СоздатьКаталог(ПолныйКаталогНазначения);
    
    // Генерируем уникальное имя файла с сохранением расширения
    УникальноеИмяФайла = Строка(Новый УникальныйИдентификатор) + ФайлИсточник.Расширение;
    ПолныйПутьНазначения = ПолныйКаталогНазначения + УникальноеИмяФайла;
    ОтносительныйПутьКФайлу = ОтносительныйКаталог + УникальноеИмяФайла;
    
    Попытка
        // Копируем файл из исходного места в наше хранилище
        КопироватьФайл(ИсходныйПутьКФайлу, ПолныйПутьНазначения);
        
        // Фиксируем информацию в базе данных 1С
        НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
        НовыйФайл.Владелец          = ДокументСсылка;
        НовыйФайл.Наименование       = ФайлИсточник.ИмяБезРасширения;
        НовыйФайл.ИмяФайла           = ФайлИсточник.Имя;
        НовыйФайл.ОтносительныйПуть = ОтносительныйПутьКФайлу;
        НовыйФайл.Записать();
        
        Сообщить("Файл успешно перемещен в архив и привязан к документу!");
    Исключение
        ВызватьИсключение "Ошибка копирования файла на сервер: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецПроцедуры

Для открытия файла, сохраненного на диске, нам достаточно собрать полный путь из базового каталога и относительного пути, хранящегося в карточке нашего файла:


Процедура ОткрытьФайлСДиска(СсылкаНаФайл) Экспорт
    
    БазовыйПуть = ПолучитьБазовыйКаталогФайлов();
    ПолныйПутьКФайлу = БазовыйПуть + СсылкаНаФайл.ОтносительныйПуть;
    
    ФайлНаСервере = Новый Файл(ПолныйПутьКФайлу);
    Если Не ФайлНаСервере.Существует() Тогда
        Предупреждение("Файл не найден в сетевом архиве! Возможно, он был удален вручную.");
        Возврат;
    КонецЕсли;
    
    Попытка
        ЗапуститьПриложение(ПолныйПутьКФайлу);
    Исключение
        ВызватьИсключение "Не удалось запустить файл: " + ОписаниеОшибки();
    КонецПопытки;
    
КонецПроцедуры

---

Обеспечение безопасности и прав доступа (Феншуйный способ)

Если мы используем хранение файлов на диске, у нас возникает серьезная уязвимость: пользователи могут зайти по сети в общую папку и случайно стереть или модифицировать файлы сканов. Чтобы этого не произошло, мы должны настроить систему безопасности по всем правилам системного администрирования.

Настроим разграничение доступа следующим образом:

В такой конфигурации обычный пользователь физически не может повлиять на файлы на диске. Когда пользователь нажимает кнопку «Открыть файл» в интерфейсе 1С, клиентское приложение шлет запрос на сервер 1С. Серверная процедура, запущенная от имени привилегированного пользователя USR1CV8, забирает файл из закрытого хранилища, временно помещает его в веб-пространство или передает на клиентскую машину во временный каталог пользователя и только после этого открывает.

---

Особенности работы в управляемом интерфейсе и веб-клиенте

Если ваша конфигурация БГУ 1.0 или любая другая самописная база запускается через тонкий клиент или в браузере (веб-клиент), то описанные выше простые методы работы с файлами напрямую использовать не получится. Браузер из соображений безопасности блокирует прямой доступ к дискам сервера или локального компьютера пользователя. Все операции должны проходить асинхронно через временное хранилище 1С. Для разбора нюансов клиент-серверного взаимодействия подойдёт руководство по особенностям работы с файлами в управляемых формах 1С 8.3.

Разберем, как реализовать интерактивный выбор и отправку файла с компьютера пользователя на сервер 1С в управляемом приложении. Для этого мы будем использовать асинхронные методы:


&НаКлиенте
Процедура ПрикрепитьФайлУправляемаяФорма(Команда)
    
    // Описываем параметры диалога выбора файлов
    ПараметрыДиалога = Новый ПараметрыДиалогаВыбораФайла(РежимДиалогаВыбораФайла.Открытие);
    ПараметрыДиалога.Фильтр = "Все файлы (*.*)|*.*|Изображения (*.jpg;*.png)|*.jpg;*.png|Документы PDF (*.pdf)|*.pdf";
    ПараметрыДиалога.МножественныйВыбор = Ложь;
    ПараметрыДиалога.Заголовок = "Выберите скан-копию для загрузки";
    
    // Начинаем интерактивный выбор файла
    ОписаниеОповещения = Новый ОписаниеОповещения("ПослеВыбораФайла", ЭтотОбъект);
    НачатьВыборФайлов(ОписаниеОповещения, ПараметрыДиалога);
    
КонецПроцедуры

&НаКлиенте
Процедура ПослеВыбораФайла(ВыбранныеФайлы, ДополнительныеПараметры) Экспорт
    
    Если ВыбранныеФайлы = Неопределено Или ВыбранныеФайлы.Количество() = 0 Тогда
        Возврат; // Пользователь передумал выбирать файл
    КонецЕсли;
    
    ПутьКФайлу = ВыбранныеФайлы[0];
    Файл = Новый Файл(ПутьКФайлу);
    
    // Готовим описание загружаемого файла
    ОписаниеПередаваемогоФайла = Новый ОписаниеПередаваемогоФайла(ПутьКФайлу);
    МассивФайлов = Новый Массив;
    МассивФайлов.Добавить(ОписаниеПередаваемогоФайла);
    
    // Помещаем файл во временное хранилище
    ОписаниеОповещения = Новый ОписаниеОповещения("ПослеЗагрузкиФайлаНаСервер", ЭтотОбъект, Файл.Имя);
    НачатьПомещениеФайловНаСервер(ОписаниеОповещения,,, МассивФайлов, УникальныйИдентификатор);
    
КонецПроцедуры

&НаКлиенте
Процедура ПослеЗагрузкиФайлаНаСервер(РезультатЗагрузки, ДополнительныеПараметры) Экспорт
    
    ИмяФайла = ДополнительныеПараметры;
    
    Если РезультатЗагрузки = Неопределено Или РезультатЗагрузки.Количество() = 0 Тогда
        ПоказатьПредупреждение(,"Не удалось загрузить файл во временное хранилище!");
        Возврат;
    КонецЕсли;
    
    РезультатФайл = РезультатЗагрузки[0];
    Если РезультатФайл.ОшибкаПередачи <> Неопределено Тогда
        ПоказатьПредупреждение(,"Ошибка при передаче файла: " + РезультатФайл.ОшибкаПередачи);
        Возврат;
    КонецЕсли;
    
    // Передаем адрес временного хранилища на сервер для окончательной записи
    АдресВоВременномХранилище = РезультатФайл.Хранилище;
    СохранитьФайлИзХранилищаНаСервере(АдресВоВременномХранилище, ИмяФайла, Объект.Ссылка);
    
КонецПроцедуры

На стороне сервера мы получаем двоичные данные из временного хранилища по переданному адресу и сохраняем их либо в СУБД, либо во внешний сетевой каталог по нашему выбору:


&НаСервереБезКонтекста
Процедура СохранитьФайлИзХранилищаНаСервере(АдресХранилища, ИмяФайла, ДокументСсылка)
    
    // Извлекаем двоичные данные из временного адреса
    ДвоичныеДанные = ПолучитьИзВременногоХранилища(АдресХранилища);
    
    ФайлНаДиске = Новый Файл(ИмяФайла);
    
    // Пример записи в базу данных через справочник
    НовыйФайл = Справочники.ПриходТоваровПрисоединенныеФайлы.СоздатьЭлемент();
    НовыйФайл.Владелец    = ДокументСсылка;
    НовыйФайл.Наименование = ФайлНаДиске.ИмяБезРасширения;
    НовыйФайл.ИмяФайла     = ИмяФайла;
    НовыйФайл.Расширение   = Сред(ФайлНаДиске.Расширение, 2);
    
    // Сжимаем данные
    НовыйФайл.ФайлДанные   = Новый ХранилищеЗначения(ДвоичныеДанные, Новый СжатиеДанных(9));
    
    Попытка
        НовыйФайл.Записать();
        УдалитьИзВременногоХранилища(АдресХранилища);
    Исключение
        ЗаписьЖурналаРегистрации("ФайлыСлужба", УровеньЖурналаРегистрации.Ошибка,,, ОписаниеОшибки());
        ВызватьИсключение "Не удалось сохранить прикрепленный файл на сервере.";
    КонецПопытки;
    
КонецПроцедуры

---

Резюме: что выбрать для вашей системы?

Мы детально изучили методы реализации прикрепления файлов в 1С без использования БСП. Подводя итог, дадим финальные рекомендации по выбору архитектуры:

  1. Если база данных работает в файловом режиме, а общий объем документов невелик — смело внедряйте хранение внутри СУБД через подчиненный справочник с реквизитом ХранилищеЗначения. Это позволит вам легко делать резервные копии стандартными средствами и избавит от сложной настройки прав доступа к общим папкам. Обязательно используйте сжатие файлов методом СжатиеДанных(9) при записи.
  2. Если база развернута на MS SQL, PostgreSQL или IBM DB2, а сканов планируется много — делайте хранение во внешних томах. Настраивайте относительные пути и иерархию папок по датам (Год\Месяц\День). Ограничивайте права доступа к сетевой папке на сервере для обеспечения безопасности данных.
  3. Если ваша конфигурация работает под управлением управляемых форм и веб-клиента — всегда используйте асинхронную схему передачи через временное хранилище с помощью методов НачатьПомещениеФайловНаСервер.
  4. </ol
← На главную