Автоформирование документов

10 6

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

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

Для чего это может быть нужно:

  • Требуется сформировать документ для некоторой программы, чтобы пользователи работали с документом в привычном режиме. В нашей компании примером для этого случая будет редактор бюджетных заявок. Пользователи заполняют карточку документа и потом, двойным щелчком по документу открывая редактор, вносят данные о планируемых затратах. При этом технически сам документ в DIRECTUM - это лишь XML-файл, в котором лежат основные данные о документе (информация с карточки документа, номер версии, ИД и др.), а информация, которую вносят пользователи через редактор, хранится в отдельной БД.
  • Нужна гибкая система прав. Чем хороша система прав на документы? Кому права дали, тот документ и видит, включая любые замещения. Для того, чтобы реализовать подобное в справочнике нужно дописывать условия в событии "Набор данных" - "Открытие" и еще не факт, что нужной степени гибкости удастся достичь. Чего точно не удастся достичь - это того, чтобы пользователь не видел записи справочника, которые ему видеть не положено, в карточке задания/задачи в области вложений. Таким образом, если стоит задача заполнения данных пользователем без создания документа и нужна хорошая система прав, то нужно делать выбор между разработкой условий на открытие справочника и разработкой отчетной формы документа (если у нас есть документ, то не будем же мы его оставлять пустым). И тут нам на помощь приходит следующий пункт нашего списка требований.
  • Нужно наглядно представить данные из полей карточки. Например, если согласуется некая заявка, то пользователям-согласующим явно будет удобнее работать не с карточкой, а с отчетной формой, где можно подобрать самое наглядное представление данных. Карточки документов/справочников ориентированы на ввод информации, а не на ее отображение, поэтому если мы дорожим временем пользователей, то нужно предоставить им альтернативные варианты просмотра, особенно если у нас в маршруте принимают участие высокие начальники. Кроме того, для отчетной формы из поддерживаемых форматов бонусом мы получаем работающий предпросмотр в карточке задания, что еще больше улучшает юзабилити.

Почему не реализовать карточку справочника, привязав к ней формирующийся документ? Можно систему прав тоже привязать к документу и использовать ее при открытии справочника (т.е. сделать фильтр в событии "Набор данных" - "Открытие": если имеешь права на привязанный документ, то видишь запись справочника). Все дело опять в юзер экспириенсе. Ввел данные на карточке документа, сохранил и получил тело этого же самого документа - это для пользователя естественно, все данные между собой связаны визуально. А ввод данных в справочнике и формирование документа где-то в другом месте - это уже явно выглядит хуже.

Недостатки технологии:

  • Автоформирование документов явно никогда не имелось в виду разработчиками DIRECTUM. Из-за этого возникают разные ошибки при попытках сделать что-то нетривиальное. Дальше в статье эти особенности будут упомянуты.
  • Работа в вебе с автоформированием документов, как я подозреваю, будет невозможна, либо затруднена. Не тестировалось.

Общими особенностями для всего процесса автоформирования документов будут:

1) При первом сохранении документа всегда в тело документа записывается выбранный шаблон. При этом запись происходит после отработки всех событий сохранения, поэтому повлиять на сохраняемый шаблон никак нельзя. Обращение к разработчикам от меня зарегистрировано, но в самой свежей на данный момент версии это поведение сохранилось, поэтому будем работать с тем, что есть. Это порождает нам некоторые сложности. Например, раз мы не можем использовать событие сохранения, то код формирования тела документа можно поместить в событие "Запись" - "Открытие". Тогда, если пользователь внес данные на карточку документа, сохранил его и тут же открыл, он увидит нормальную отчетную форму. Но если произошло подписание документа электронной подписью без срабатывания события открытия (у нас такое бывало), то тело документа у нас так и останется в пустом состоянии шаблона.

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

2) Автоформирование документа подразумевает, что пользователь во многих случаях не должен управлять созданием версий, за него это должна делать система. На практике выходит так, что помешать цивилизованными средствами пользователю создать новую версию документа мы можем, но это не очень стабильный механизм. То работает, то начинают вылазить некрасивые ошибки, типа Access Violation. На форуме есть небольшое обсуждение. Поэтому в этом вопросе полагаемся в том числе на сознательность пользователей. Многолетняя практика показала, что обычно этого хватает, и очень-очень редко кто-то по ошибке пытается создать версию там, где это не нужно.

3) В реквизит ISBVersionData второй таблицы списка реквизитов документа (это где хранится тело документа) данные можно записывать напрямую. Теоретически это может быть запрещено в будущих версиях и приведет к несовместимости, но на данный момент этим очень удобно пользоваться, если в версию нужно сохранить текстовые данные. Это наглядно в коде и быстро выполняется у пользователя, поэтому кое-где я такой способ применяю. Как говорится, трюк исполняется профессионалами в специально подготовленных условиях, не пытайтесь повторить это дома, лучше через IEDocument.Versions, тем более, что такой пример тоже будет.

4) Нужно некоторым образом определять, когда нужно формировать тело документа, а когда нет. Самый очевидный вариант: при каждом изменении любого реквизита устанавливать некий флаг и потом его анализировать. Недостаток этого метода заключается в том, что при добавлении нового реквизита в документ нужно не забыть добавить установку флага и на изменение этого нового реквизита. Да и вообще, множество однотипного кода заставляет задуматься о том, как бы фиксировать изменения скопом (особенно, если реквизитов штук сорок, включая табличные). Второй простой вариант - это установка флага в событии "Форма" - "Показ" (флаг можно устанавливать в IObject.Params) и последующий анализ флага в событии сохранения. Т.е. если была открыта карточка и после этого было сохранение, то предполагаем, что были изменены какие-то данные на карточке. Помимо красивого минимализма у этого метода есть глобальный недостаток - будут пропущены мимо все программные изменения объекта, например, из-под Workflow или в скрипте. Какой из методов применять и как их улучшить, решается в каждой конкретной ситуации.

Итак, рассмотрим два случая автоформирования документов:

1) Когда нужно подготовить файл для другой программы. Я приведу конкретный пример, который используется у нас. Нужно сформировать XML-файл, в который помещаются нужные данные. В данном случае при изменении данных на карточке документа пересоздавать тело документа не надо. Можно рассматривать тело документа, как табличную часть, хранящуюся вне DIRECTUM и обладающую версионностью.

Добавляем в список реквизитов новый строковый реквизит avkFlag. В событии "Запись" - "Открытие" размещаем код:

  if not Object.Inserted 
    if not Assigned(Object.avkFlag) //если документ только что создан
      sysCode = Application.Connection.SystemInfo.Code
      docID = Object.ID
      
      str = '' & CR &
            '' & CR
      
      str = str & '  ' & CR & '    ' & docID & '' & CR & "  " & CR
      str = str & '  1' & CR
      str = str & '  ' & CR
      str = str & '  ' & CR
      str = str & '  ' & CR
      str = str & '  '&sysCode&'' & CR & ""
      
      Object.DetailDataset(2).ISBVersionData = str
      Object.avkFlag = "1"
      Object.Save
    else
      if Object.avkFlag == "2" //если создана версия документа
        dds = Object.DetailDataset(2)
        dds.Last
        
        str = dds.ISBVersionData
        path = IncludeTrailingPathDelimiter(GetTempFolder()) & "temp.xml"
        WriteFile(path;;str)
        
        XMLDoc = CreateObject("MSXml.DomDocument")
        XMLDoc.Load(path)  
        RootElem = XMLDoc.DocumentElement
        
        //Version Node
        node = RootElem.selectSingleNode("//DocumentVersion")
        baseVers = node.text
        node.text = dds.НомСтр
        
        //Source Version Node
        node = RootElem.selectSingleNode("//SourceVersion")
        node.text = baseVers
        
        XMLDoc.Save(path)
        
        str = ReadFile(path)
        dds.ISBVersionData = str
                
        Object.avkFlag = "1"
        Object.Save
      else
        if Object.avkFlag == "3" //если создана копия документа
          docID = Object.ID
          
          dds = Object.DetailDataset(2)
          
          str = dds.ISBVersionData
          path = IncludeTrailingPathDelimiter(GetTempFolder()) & "temp.xml"
          WriteFile(path;;str)
          
          XMLDoc = CreateObject("MSXml.DomDocument")
          XMLDoc.Load(path)  
          RootElem = XMLDoc.DocumentElement
          
          //Version Node
          node = RootElem.selectSingleNode("//DocumentVersion")
          baseVers = node.text
          node.text = dds.НомСтр
          
          //Source Version Node
          node = RootElem.selectSingleNode("//SourceVersion")
          node.text = ''
          
          //DocumentId Node
          node = RootElem.selectSingleNode("//DocumentId/id")
          sourceDocID = node.text
          node.text = Object.ID
          
          //Source document id
          node = RootElem.selectSingleNode("//SourceDocumentID")
          node.text = sourceDocID
          
          //Source document version number
          node = RootElem.selectSingleNode("//SourceDocumentVersionNum")
          node.text = baseVers
          
          XMLDoc.Save(path)
          
          str = ReadFile(path)
          dds.ISBVersionData = str
                  
          Object.avkFlag = "1"
          Object.Save
        endif
      endif
    endif
  endif

Формат XML-файла был предоставлен нашим разработчиком, я оставил только наиболее интересные моменты. В файл записывается ИД документа, номер версии, ИД и номер версии скопированного документа (если есть), код текущей системы (чтобы различать документы тестовой и рабочей баз). Получать данные об исходном документе можно из EDocumentVersionSource, но вышеприведенный код был написан еще до появления этой предопределенной переменной. Оставил здесь для примера работы с MSXml.DomDocument.

Подобным образом можно формировать HTML-файл с редиректом на какой-нибудь отчет на MS Reporting Services.

В событие "Запись" - "Добавление после" устанавливаем флаг, если документ был скопирован:

  if Object.Copied
    Object.avkFlag = "3"
  endif

А в событии "Таблица 2" - "Добавление после" ставим признак добавления версии:

  if not Object.Inserted
    Object.avkFlag = "2"
  endif

Для наведения лоска запретим создавать документ, кроме как путем копирования и создания из шаблона. В событие "Запись" - "Добавление до" пишем код:

  if EDocumentVersionSource.SourceType <> edvstTemplate AND EDocumentVersionSource.SourceType <> edvstEDocumentVersionCopy
    Raise(CreateException('';'Документ может быть только создан из шаблона или скопирован из другого документа этого вида';ecInformation))
  endif

Тут бы еще надо проверять вид исходного документа, но это уже частности.

2) Второй вид автоформируемых документов - это отчетные формы для данных, представленных на карточке документа. Такие формы удобно формировать в Word или Excel из заранее заготовленных шаблонов. После заполнения документы конвертируются в pdf и импортируются в DIRECTUM в документ.

Вариантов программного формирования документов много, есть обсуждения на форуме (например, здесь), приведу в пример только один, с использованием закладок в Word. В шаблоне документа расставляем закладки (вкладка "Вставка" на риббон-панели, там кнопка "Закладка"), а при формировании документа вставляем текст в зависимости от имени закладки. В событие "Запись" - "Открытие" добавляем текст:

  if not Object.Inserted 
    if not Assigned(Object.avkFlag)
      tempFileName = IncludeTrailingPathDelimiter(GetTempFolder()) & 'directum tempfile.doc'
      pdfFileName = IncludeTrailingPathDelimiter(GetTempFolder()) & 'directum tempfile.pdf'
    
      versionNum = Object.Versions.Count
      Version = Object.Versions.Values(versionNum - 1)
      
      templateID = 243398
      template = EDocuments.GetObjectByID(templateID)
      versionsTemplate = template.Versions
      versTemplate = versionsTemplate.Values(0)
      versTemplate.Export(tempFileName)
      versTemplate = null
      versionsTemplate = null 
      template = null
      
      Word = CreateObject("Word.Application")
      Word.DisplayAlerts = FALSE
      doc = Word.Documents.Open(tempFileName)
      count = doc.Bookmarks.Count
      i = 1
      
      while i <= count
        if doc.Bookmarks.Item(i).Name == "t1"
          doc.Bookmarks.Item(i).Range.Text = Object.ISBEDocName
        else
          if doc.Bookmarks.Item(i).Name == "t2"
            doc.Bookmarks.Item(i).Range.Text = Object.Requisites("SIN_Rab").DisplayText
          endif
        endif
        i = i + 1
      endwhile
      doc.Save()
      doc.ExportAsFixedFormat(pdfFileName;17)
      doc.Close()
      Word.Quit()
      
      if version.Signed
        version.ChangeState(vsObsolete)
        Object.ImportFromFile(-1;'Версия ' & (versionNum + 1);pdfFileName)
      else
        Version.ImportFromFile(pdfFileName)
      endif
      Object.avkFlag = "1"
      Object.Save
    endif
  endif

С форматированием шаблона помогут отличные статьи Елизаветы Великановой: Word и Excel

Еще раз напомню, что скидывать значение avkFlag нужно при изменении значений реквизитов, в соответствии с тем способом, который был выбран. Например, в событие "Форма" - "Показ" пишем код:

  Object.Params.Add("CardShowed";'1')

А в событии "Запись" - "Сохранение до" размещаем следующий код:

  flagShowedCard = Object.Params.FindItem("CardShowed")
  if Assigned(flagShowedCard)
    Object.avkFlag = ''
  endif

Приведенный код работал в версии 4.8, точно работает (давно) в версии 4.9.1, проверено в версии 5.0.

Тестовая разработка приложена. В КД SIN_Test2 формируется XML-файл, в КД SIN_Test3 формируется pdf.

пакет 4.9.1.rar (19,02 Кб)

пакет 5.0.rar (18,79 Кб)

Для тестирования создаем вид документа Test2, снимаем галочку "Доступны все шаблоны", указываем шаблон "Общий шаблон документов формата TXT" (так сразу будет открываться в Блокноте, чтобы удобнее смотреть было), указываем карточку Test 2, указываем вид жизненного цикла, состояние версии по умолчанию и право создания.

Для второй карточки создаем или берем существующий документ Word (должен быть установлен Office 2010), создаем в этом документе две закладки t1 и t2 (место можно выбрать по желанию), помещаем его в DIRECTUM (вид не важен, можно указать "Шаблон электронного документа"), ИД нового документа указываем в событии "Запись" - "Открытие" в переменной templateID, создаем вид документа Test3, указываем тип карточки Test 3 и заполняем все остальные обязательные параметры.

10
Авторизуйтесь, чтобы оценить материал.
1
Алексей Пестерев

Очень интересно, только вот хочется увидеть результат, пощупать или что-то еще. 

Андрей Куров

Опубликовал пакеты для версии 4.9.1 и для 5.0. В принципе, в эти карточки просто помещен код из статьи - его вполне достаточно для работоспособности.

Руслан Гатаулин

Андрей, а нет желания несколько преобразовав содержимое статьи, поучаствовать с ней в Awards?

Алексей Пестерев

Поддержку Руслана, и со своей стороны также рекомендую вам оформить данный материал, как заявку на DIRECTUM Awards 2014

Андрей Куров

Пока нет времени переделывать статью под Awards.


 

Руслан Гатаулин

Ну ничего, дело не горящее. Так что ждемс.

Авторизуйтесь, чтобы написать комментарий