Три года назад с целью автоматизации управления организационно-распорядительной документацией была приобретена система elma. Но после года эксплуатации стало понятно, что от нее придется отказаться.
Рассказ о том, как я ушел от elma к easla.com.
Прежде, чем приобрести elma был проведен тщательный анализ системы на адекватность поставленным задачам. Понравилось в системе следующее:
- Современный веб-интерфейс
- Возможность настройки бизнес-процессов с помощью блок-схем
- Возможность написания кода на C#.
Жаль, что не смог увидеть подводных камней сразу. Не буду сильно вдаваться в подробности, но месяца через три после начала эксплуатации появились проблемы разного плана. Прежде всего, меня конечно беспокоили технические проблемы, например:
- Крайне сложно именовать файлы объекта программно (помнится, техподдержка предложила для этого написать больше сотни строк кода в разных событиях), правда месяца через два разработчики исправили эту проблему.
- Проблемы при создании процессов с ветвлениями. Был создан процесс рассмотрения входящего письма, который ветвился в соответствии с количеством ответственных исполнителей. Кода в процессе практически не было. Однако регулярно появлялись задачи-фантомы — я их так назвал. Назначено 5 задач, а получается 10. Решить проблему с помощью техподдержки не удалось и мне пришлось писать «костыли», которые уничтожали фантомов программно. Но все равно в непредсказуемых ситуациях они проявлялись.
- Разработка новых процессов была возможна только на отдельном, тестовом сервере, для которого регулярно приходилось выпрашивать лицензию, т.к. она не включена в поставку. Готовый процесс приходилось вручную переносить с тестового сервера на боевой. При этом боевой сервер приходилось перезапускать. В рабочее время такое невозможно, что приводило к регулярному переносу рабочего времени на ночь.
- Каждое обновление приносило свои исправления и свои баги. Однажды, обновление принесло баг, который был исправлен 2-3 обновления назад. Причем порой ошибки были смешные — детские. Например, два обновления подряд не могли главное меню развернуть на всю высоту браузера.
Были и другие проблемы. Но то, технические проблемы, которые конечных пользователей не касаются.
Однако и пользователи тоже регулярно жаловались на неудобства, например:
- Поиск объектов осуществляется только на отдельной странице, что вроде бы логично, и даже критерии поиска можно вводить весьма сложные, но для конечного пользователя этот функционал оказался неудобным. Все давно привыкли, что искать удобно указывая критерии в шапке таблицы, а переход на отдельную страницу и многоходовой ввод критериев вызывал недовольство и отторжение.
- Elma, работая в локальной сети, начала удивлять своей задумчивостью. Мы изучили систему изнутри. Оказалось, что для каждого объекта система создает отдельную таблицу и еще кучку смежных таблиц для хранения данных, т.е. торможения быть не должно. Однако, когда количество записей каждого объекта превысило тысяч пять, тормоза стали заметны на интуитивном уровне. Странички со списком открывались несколько секунд.
После года эксплуатации elma стало понятно, что система приведет организацию в тупик. Мы не сможем реализовать на ней другие процессы без деградации производительности. И нет гарантии, что реализовав процессы, они будут работать стабильно, учитывая постоянно «вылезающие баги». Все преимущества системы сошли на нет.
Поиск
Был осуществлен поиск системы взамен elma. Одним из критериев поиска была ее бесплатность или дешевизна, т.к. руководство организации не хотело тратиться на покупку новой системы, а мне, как руководителю отдела ИТ принявшему в свое время решение о приобретении elma и обосновав ее приобретение было стыдно в очередной раз просить деньги на тоже самое. Также, в списке критериев были:
- Привычный пользователю веб-интерфейс
- Разработка и изменение процессов в реальном времени
- Гибкость хранения файлов
- Возможность импорт данных из elma в новую систему.
Как раз easla.com подошла по всем пунктам.
Разработка
После регистрации в системе начал разработку соответствующих бизнес-процессов. Причем атрибуты объектов пришлось реализовывать так, чтобы они могли принимать импортируемые данные из elma.
Потребовалось реализовать четыре процесса:
- Заказчики
- Договоры
- Переписка
- Задачи.
Процесс "Заказчики" сложно назвать процессом. У объектов нет даже статусов. Фактически – это каталог контрагентов и контактов связанных между собой. Но контрагент отличается от всех других объектов числом атрибутов. Он позволяет хранить не только наименование, но и юридический, и почтовый адреса, а также банковские реквизиты. Всего 40 атрибутов! Контакт – это физическое лицо являющееся сотрудником контрагента.
Процесс «Договоры» в тот момент тоже представлял собой простой каталог договоров. Он был нужен исключительно для классификации писем.
Процесс "Переписка" – очень важный для организации процесс. Он управляет официальной входящей и исходящей корреспонденцией, которая влияет на принятие технических решений и участвует в разрешении конфликтных ситуации в суде. Входящие письма могут принести убытки из-за несвоевременного рассмотрения или ответа. Исходящие письма могут приносить шести-, а иногда и семизначную прибыль при их своевременной отправке. Важными отличиями объекта «Исходящий документ» являются:
- Способ хранения файла письма
- Способ хранения прилагаемых документов
- Возможность разработки письма онлайн
- Возможность отправки письма одним из десятка способов.
Процесс "Задачи" важен точно также, как «Переписка», т.к. является его логическим продолжением. С помощью задач осуществляется контроль исполнения поручений по каждому входящему или исходящему документу. Кроме этого, в каждой задаче осуществляется регистрация трудозатрат в минутах и связка с входящим и исходящим письмом.
Разработку процессов закончил за месяц и встал вопрос собственно импорта данных. Но прежде – экспорта из elma.
Экспорт-импорт
Импорт в easla.com обладает некоторыми особенностями, которые надо учитывать:
- Первая строка файла с данными может содержать обозначения атрибутов
- Можно импортировать только 1000 записей за один раз
- Нескольких значений для одного атрибута надо разделить символом "|"
- Импортируемые файлы должны иметь разные имена.
Штатных средств выгрузки данных из elma не нашел, поэтому пришлось лезть в базу данных. В базе elma море разных таблиц, необходимые таблицы были найдены интуитивно. Все-таки программисты мыслят одинаково.
Начал экспорт с простых объектов. Сперва контрагенты. Написал вот такой SQL запрос:
SELECT DISTINCT [ELMA].[dbo].[Contractor].[Name] AS [crm_management_contragent_name] ,[LF].[LongName] AS [crm_management_contragent_opf] ,[ELMA].[dbo].[ContractorType].[Name] AS [crm_management_contragent_type] ,[Site] AS [crm_management_contragent_url] ,[crm_management_contragent_email] = STUFF(( SELECT '|' + [ELMA].[dbo].[Email].[EmailString] FROM [ELMA].[dbo].[Contractor_Email] LEFT OUTER JOIN [ELMA].[dbo].[Email] ON [ELMA].[dbo].[Email].[Id] = [ELMA].[dbo].[Contractor_Email].[Email] WHERE [ELMA].[dbo].[Contractor_Email].[Contractor] = [ELMA].[dbo].[Contractor].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,[Fax] AS [crm_management_contragent_fax] ,[crm_management_contragent_phone] = STUFF(( SELECT '|' + [ELMA].[dbo].[Phone].[PhoneString] FROM [ELMA].[dbo].[Contractor_Phone] LEFT OUTER JOIN [ELMA].[dbo].[Phone] ON [ELMA].[dbo].[Phone].[Id] = [ELMA].[dbo].[Contractor_Phone].[Phone] WHERE [ELMA].[dbo].[Contractor_Phone].[Contractor] = [ELMA].[dbo].[Contractor].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,[LAC].[Name] AS [crm_management_contragent_legal_country] ,[LA].[Region] AS [crm_management_contragent_legal_region] ,[LA].[District] AS [crm_management_contragent_legal_district] ,[LA].[City] AS [crm_management_contragent_legal_city] ,[LA].[Locality] AS [crm_management_contragent_legal_locality] ,[LA].[Street] AS [crm_management_contragent_legal_street] ,[LA].[Building] AS [crm_management_contragent_legal_house] ,[LA].[Stroenie] AS [crm_management_contragent_legal_building] ,[LA].[Korpus] AS [crm_management_contragent_legal_corp] ,[LA].[Appartment] AS [crm_management_contragent_legal_apartment] ,[LA].[Zip] AS [crm_management_contragent_legal_postcode] ,[PAC].[Name] AS [crm_management_contragent_post_country] ,[PA].[Region] AS [crm_management_contragent_post_region] ,[PA].[District] AS [crm_management_contragent_post_district] ,[PA].[City] AS [crm_management_contragent_post_city] ,[PA].[Locality] AS [crm_management_contragent_post_locality] ,[PA].[Street] AS [crm_management_contragent_post_street] ,[PA].[Building] AS [crm_management_contragent_post_house] ,[PA].[Stroenie] AS [crm_management_contragent_post_building] ,[PA].[Korpus] AS [crm_management_contragent_post_corp] ,[PA].[Appartment] AS [crm_management_contragent_post_apartment] ,[PA].[Zip] AS [crm_management_contragent_post_postcode] ,[INN] AS [crm_management_contragent_inn] ,[ELMA].[dbo].[ContractorLegal].[KPP] AS [crm_management_contragent_kpp] ,[ELMA].[dbo].[ContractorLegal].[OGRN] AS [crm_management_contragent_ogrn] ,[ELMA].[dbo].[ContractorLegal].[BIK] AS [crm_management_contragent_bik] ,[ELMA].[dbo].[ContractorLegal].[BANK] AS [crm_management_contragent_bank] ,[ELMA].[dbo].[ContractorLegal].[KS] AS [crm_management_contragent_ks] ,[ELMA].[dbo].[ContractorLegal].[RS] AS [crm_management_contragent_rs] FROM [ELMA].[dbo].[Contractor] LEFT OUTER JOIN [ELMA].[dbo].[ContractorType] ON [ELMA].[dbo].[ContractorType].[Id] = [ELMA].[dbo].[Contractor].[Type] LEFT OUTER JOIN [ELMA].[dbo].[ContractorLegal] ON [ELMA].[dbo].[ContractorLegal].[Id] = [ELMA].[dbo].[Contractor].[Id] LEFT OUTER JOIN [ELMA].[dbo].[Address] AS [LA] ON [LA].[Id] = [ELMA].[dbo].[Contractor].[LegalAddress] LEFT OUTER JOIN [ELMA].[dbo].[Country] AS [LAC] ON [LAC].[Id] = [LA].[Country] LEFT OUTER JOIN [ELMA].[dbo].[Address] AS [PA] ON [PA].[Id] = [ELMA].[dbo].[Contractor].[PostalAddress] LEFT OUTER JOIN [ELMA].[dbo].[Country] AS [PAC] ON [PAC].[Id] = [PA].[Country] LEFT OUTER JOIN [ELMA].[dbo].[LegalForm] AS [LF] ON [LF].[Id] = [ELMA].[dbo].[ContractorLegal].[LegalForm] WHERE [ELMA].[dbo].[Contractor].[IsDeleted] = 0 AND [ELMA].[dbo].[Contractor].[Name] IS NOT NULL AND [ELMA].[dbo].[Contractor].[id] > 500
Несколько значений для одного атрибута объединил в одно с помощью STUFF(…). Результат запроса сохранил в формате csv с разделителем ";". Первой строкой в файле добавил обозначения атрибутов в easla.com. Загрузил файл в разделе «Импорт объектов». Потом указал кодировку файла и пользователя, от имени которого выполнить импорт. Запустил импорт. Процедура импорта не очень быстрая, на создание каждого объекта может уходить порядка одной секунды. Количество импортированных записей было примерно 500. Выглядело это примерно так:
Теперь контакты. Написал вот такой SQL запрос:
SELECT [Surname] AS [crm_management_contact_lastname] ,[Firstname] AS [crm_management_contact_firstname] ,[Middlename] AS [crm_management_contact_middlename] ,[ELMA].[dbo].[Contractor].[Name] AS [crm_management_contact_contragent] ,[Department] AS [cnt_management_contact_department] ,[Position] AS [cnt_management_contact_position] ,[crm_management_contact_email] = STUFF(( SELECT '|' + [ELMA].[dbo].[Email].[EmailString] FROM [ELMA].[dbo].[Contact_Email] LEFT OUTER JOIN [ELMA].[dbo].[Email] ON [ELMA].[dbo].[Email].[Id] = [ELMA].[dbo].[Contact_Email].[Email] WHERE [ELMA].[dbo].[Contact_Email].[Contact] = [ELMA].[dbo].[Contact].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,[crm_management_contact_phone] = STUFF(( SELECT '|' + [ELMA].[dbo].[Phone].[PhoneString] FROM [ELMA].[dbo].[Contact_Phone] LEFT OUTER JOIN [ELMA].[dbo].[Phone] ON [ELMA].[dbo].[Phone].[Id] = [ELMA].[dbo].[Contact_Phone].[Phone] WHERE [ELMA].[dbo].[Contact_Phone].[Contact] = [ELMA].[dbo].[Contact].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,CASE WHEN [Year] IS NULL THEN NULL ELSE CONVERT(varchar, [Birthday],104) END AS [crm_management_contact_birthday] ,[RAC].[Name] AS [crm_management_contact_country] ,[RA].[Region] AS [crm_management_contact_region] ,[RA].[District] AS [crm_management_contact_district] ,[RA].[City] AS [crm_management_contact_city] ,[RA].[Locality] AS [crm_management_contact_locality] ,[RA].[Street] AS [crm_management_contact_street] ,[RA].[Building] AS [crm_management_contact_house] ,[RA].[Stroenie] AS [crm_management_contact_building] ,[RA].[Korpus] AS [crm_management_contact_corp] ,[RA].[Appartment] AS [crm_management_contact_apartment] ,[RA].[Zip] AS [crm_management_contact_postcode] FROM [ELMA].[dbo].[Contact] LEFT OUTER JOIN [ELMA].[dbo].[Contractor] ON [ELMA].[dbo].[Contractor].[Id] = [ELMA].[dbo].[Contact].[Contractor] LEFT OUTER JOIN [ELMA].[dbo].[Address] AS [RA] ON [RA].[Id] = [ELMA].[dbo].[Contact].[RegistrationAddress] LEFT OUTER JOIN [ELMA].[dbo].[Country] AS [RAC] ON [RAC].[Id] = [RA].[Country] WHERE [ELMA].[dbo].[Contact].[Id] > 1919
Все действия аналогичны предыдущему импорту. Особенность импорта контактов после контрагентов заключалась в том, что контакт обладает атрибутом «Контрагент», который ссылается на контрагента. Поэтому, в первую очередь были импортированы контрагенты, а за ними контакты. При импорте контактов в поле «Контрагент» easla.com находила и подставляла ссылки на контрагентов! Количество импортированных записей было примерно 1800.
После импорта контрагентов и контактов получилась структурированная база данных, в которой все контакты принадлежат определенным контрагентам. На импорт данных ушло примерно 2 часа. Причем половину времени потратил на разработку SQL запросов. В результате получил две готовые и взаимосвязанные таблицы.
Реестр контрагентов:
Реестр контактов:
На очереди стоял импорт договоров. Выгрузил их с помощью вот такого SQL запроса:
SELECT [RegYear] AS [agr_management_contract_year] ,[RegNum] AS [agr_management_contract_num] ,[RegAppendixFront] AS [agr_management_contract_appendix_front] ,[RegAppendixBack] AS [agr_management_contract_appendix_back] ,[RegCode] ,[ELMA].[dbo].[Contractor].[Name] AS [agr_management_contract_contragent] ,'"'+REPLACE(REPLACE(REPLACE([Title],CHAR(10),''),CHAR(13),''),';',',')+'"' AS [agr_management_contract_title] ,CONVERT(varchar, [RegDate],104) AS [agr_management_contract_reg_date] ,CONVERT(varchar, [SignDate],104) AS [agr_management_contract_sign_date] ,[PM1].[EMail] AS [agr_management_contract_project_manager] ,[PM2].[EMail] AS [agr_management_contract_project_manager_helper] ,agr_management_contract_fields = STUFF(( SELECT '|' + [F1].[Title] FROM [ELMA].[dbo].[Project_Fields] LEFT OUTER JOIN [ELMA].[dbo].[Field] AS [F1] ON [F1].[Id] = [ELMA].[dbo].[Project_Fields].[Field] WHERE [ELMA].[dbo].[Project_Fields].[Parent] = [ELMA].[dbo].[Project].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') FROM [ELMA].[dbo].[Project] LEFT JOIN [ELMA].[dbo].[Contractor] ON [ELMA].[dbo].[Contractor].[Id] = [ELMA].[dbo].[Project].[Contragent] LEFT JOIN [ELMA].[dbo].[User] AS [PM1] ON [PM1].[Id] = [ELMA].[dbo].[Project].[ProjectManager] LEFT JOIN [ELMA].[dbo].[User] AS [PM2] ON [PM2].[Id] = [ELMA].[dbo].[Project].[ProjectManagerHelper]
Все действия также аналогичны предыдущему импорту. И точно также, как с контактами, все договоры обладая атрибутом «Контрагент» при импорте связались с соответствующими контрагентами. Количество импортированных записей было немногим больше 200.
Вот теперь, когда все контрагенты, контакты и договоры, на которые ссылаются входящие и исходящие документы находились в easla.com, можно было приступить к импорту переписки.
Количество входящих и исходящих писем было большим — примерно 5400 и 5300 соответственно.
Интересно то, что входящие и исходящие письма ссылаются друг на друга, т.е. исходящее может быть отправлено в ответ на входящее и информации об этом сохраняется в соответствующем атрибуте исходящего документа. Входящий документ может быть получен в ответ на исходящий и информация об этом хранится в соответствующем атрибуте входящего документа. Таким образом, при импорте одного из типов объектов обеспечить формирование всех связей невозможно. Первыми были импортированы входящие документы, но перед этим, тип атрибута хранящего ссылку на исходящий документ был изменен с «Объект» на «Строка». Это позволило сохранить в атрибуте описание исходящего документа, которого пока нет в базе данных и использовать его позже, после импорта исходящих документов. В конечном счете, для выгрузки входящих документов получился вот такой SQL запрос:
SELECT CASE [DocMethod] WHEN 1 THEN 'Эл. письмо' WHEN 2 THEN 'Бумажное письмо' WHEN 3 THEN 'Факсимиле' WHEN 4 THEN 'Передан лично' END AS [crs_management_incoming_method] ,[ELMA].[dbo].[Contractor].[Name] AS [crs_management_incoming_contragent] ,[ELMA].[dbo].[Contact].[Surname] + ' ' + SUBSTRING([ELMA].[dbo].[Contact].[Firstname],1,1) + '.' + SUBSTRING([ELMA].[dbo].[Contact].[Middlename],1,1) + '.' AS [crs_management_incoming_contact] ,[crs_management_incoming_performers] = STUFF(( SELECT '|' + [C1].[Surname] + ' ' + SUBSTRING([C1].[Firstname],1,1) + '.' + SUBSTRING([C1].[Middlename],1,1) + '.' FROM [ELMA].[dbo].[IncomingDoc_Performers] LEFT OUTER JOIN [ELMA].[dbo].[Contact] AS [C1] ON [C1].[Id] = [ELMA].[dbo].[IncomingDoc_Performers].[Performer] WHERE [ELMA].[dbo].[IncomingDoc_Performers].[Parent] = [ELMA].[dbo].[IncomingDoc].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,CONVERT(varchar, [ELMA].[dbo].[IncomingDoc].[ContragentDate], 104) AS [crs_management_incoming_contragent_date] ,[ELMA].[dbo].[IncomingDoc].[ContragentRegNum] AS [crs_management_incoming_contragent_regnum] ,CONVERT(VARCHAR,[ELMA].[dbo].[IncomingDoc].[ReceiveDate],20) AS [crs_management_incoming_receive_date] ,[ELMA].[dbo].[IncomingDoc].[ReceiveOriginalDate] AS [crs_management_incoming_receive_original_date] ,[ReceiveOriginalDateFlag] AS [crs_management_incoming_receive_original_flag] ,[ELMA].[dbo].[IncomingDoc].[ReceiveRegNum] AS [crs_management_incoming_receive_regnum] ,REPLACE(REPLACE([ELMA].[dbo].[IncomingDoc].[Subj],CHAR(13),''),CHAR(10),'') AS [crs_management_incoming_subj] ,CASE [ELMA].[dbo].[IncomingDoc].[DocType] WHEN 0 THEN 'Задания' WHEN 1 THEN 'Замечания' WHEN 2 THEN 'Ответы на замечания' WHEN 3 THEN 'Запрос исходных данных' WHEN 4 THEN 'Выдача исходных данных' WHEN 5 THEN 'Запрос согласования' WHEN 6 THEN 'Согласование ранее направленных материалов' WHEN 7 THEN 'Запрос информации' WHEN 8 THEN 'Пожелания контрагента' WHEN 9 THEN 'Претензия' WHEN 13 THEN 'Постановление' WHEN 14 THEN 'Отзыв' WHEN 17 THEN 'Ходатайство' WHEN 15 THEN 'Определение' WHEN 10 THEN 'Приглашение' WHEN 18 THEN 'О подтверждении технической возможности' WHEN 11 THEN 'Запрос коммерческого предложения' WHEN 12 THEN 'Договорная документация' WHEN 16 THEN 'Коммерческое предложение' WHEN 99 THEN 'Прочее' END AS [crs_management_incoming_content] ,[U1].[UserName]+'@sngp.ru' AS [crs_management_incoming_to] ,[U2].[UserName]+'@sngp.ru' AS [crs_management_incoming_forwardto] ,[ELMA].[dbo].[OutgoingDoc].[RegNum] AS [crs_management_incoming_outgoing] ,[ELMA].[dbo].[Project].[RegCode] AS [crs_management_incoming_contract] ,[crs_management_incoming_attachments] = STUFF(( SELECT '|' + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(LTRIM(STR([F0].[Id]) + ' ' + [F0].[Name]),'«',''),'»',''),'–','-'),'.zip.zip','.zip'),'.docx','.docx'),' ',' ') FROM [ELMA].[dbo].[IncomingDoc_DocAttachments] LEFT OUTER JOIN [ELMA].[dbo].[FS_FILES] AS [F0] ON [F0].[Uid] = [ELMA].[dbo].[IncomingDoc_DocAttachments].[DocAttachment] WHERE [ELMA].[dbo].[IncomingDoc_DocAttachments].[Parent] = [ELMA].[dbo].[IncomingDoc].[Id] FOR XML PATH(''), TYPE).value('.', 'VARCHAR(MAX)'), 1, 1, '') ,LTRIM(STR(ISNULL([F1].[Id],[F2].[Id]))) + REVERSE(SUBSTRING(REVERSE(ISNULL([F1].[Name],[F2].[Name])), 0, CHARINDEX('.',REVERSE(ISNULL([F1].[Name],[F2].[Name]))) + 1)) AS [crs_management_incoming_document] ,[S].[Name] AS [status] FROM [ELMA].[dbo].[IncomingDoc] LEFT OUTER JOIN [ELMA].[dbo].[Contractor] ON [ELMA].[dbo].[Contractor].[Id] = [ELMA].[dbo].[IncomingDoc].[Contragent] LEFT OUTER JOIN [ELMA].[dbo].[Contact] ON [ELMA].[dbo].[Contact].[Id] = [ELMA].[dbo].[IncomingDoc].[Contact] LEFT JOIN [ELMA].[dbo].[User] AS [U1] ON [U1].[Id] = [ELMA].[dbo].[IncomingDoc].[To] LEFT JOIN [ELMA].[dbo].[User] AS [U2] ON [U2].[Id] = [ELMA].[dbo].[IncomingDoc].[ForwardTo] LEFT JOIN [ELMA].[dbo].[Project] ON [ELMA].[dbo].[Project].[Id] = [ELMA].[dbo].[IncomingDoc].[Project] LEFT JOIN [ELMA].[dbo].[OutgoingDoc] ON [ELMA].[dbo].[OutgoingDoc].[Id] = [ELMA].[dbo].[IncomingDoc].[OutgoingDoc] LEFT JOIN [ELMA].[dbo].[DocumentVersion] AS [DV1] ON [DV1].[DocumentId] = [ELMA].[dbo].[IncomingDoc].[Id] AND [DV1].[Status] = 2 AND ISNUMERIC([DV1].[File]) = 1 LEFT JOIN [ELMA].[dbo].[DocumentVersion] AS [DV2] ON [DV2].[DocumentId] = [ELMA].[dbo].[IncomingDoc].[Id] AND [DV2].[Status] = 2 AND ISNUMERIC([DV1].[File]) = 0 LEFT JOIN [ELMA].[dbo].[FS_FILES] AS [F1] ON [F1].[Id] = [DV1].[File] LEFT JOIN [ELMA].[dbo].[FS_FILES] AS [F2] ON [F2].[Uid] = [DV2].[File] LEFT JOIN [ELMA].[dbo].[Document] ON [ELMA].[dbo].[Document].[Id] = [ELMA].[dbo].[IncomingDoc].[Id] LEFT JOIN [ELMA].[dbo].[LifeCycleStatus] AS [S] ON [S].[Id] = [ELMA].[dbo].[Document].[Status] WHERE --[ELMA].[dbo].[IncomingDoc].[Subj] NOT LIKE 'тест%' AND [ELMA].[dbo].[IncomingDoc].[Subj] NOT LIKE 'test%' [ELMA].[dbo].[IncomingDoc].[Id] > 12193 ORDER BY [ELMA].[dbo].[IncomingDoc].[ReceiveDate]
Кстати, осложнялась ситуация еще и тем, что вместе с данными нужно было выгрузить и импортировать все файлы.
Совершенной неожиданностью стало то, что разработчики elma в какой-то момент изменили способ хранения файлов, поэтому пришлось создавать два разных запроса и объединять их вместе для выгрузки файлов. При первой попытке выгрузки возникла проблема. Имена некоторых файлов повторялись и перезаписывали друг друга. Пришлось изменить в запросе имя конечного файла, добавив к нему id. Кроме, собственно, файла и его имени нужно было перенести в easla.com и дополнительные данные, хранящиеся вместе с файлом. Их тоже пришлось вписать в имя файла. Таким образом, конечное имя файла формировалось по принципу: id data filename.ext. Так как elma хранит файлы в отдельном каталоге, результат запроса планировалось преобразовать в bat файл для выгрузки файлов в нужное место. В конечном счете, для выгрузки файлов входящих документов получился вот такой запрос:
SELECT 'copy "\\ssv11\c$\ELMA3-Standart.cfg\UserConfig\Files\' + [FNAME] + '" "c:\temp\elma\incoming\' + REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(REPLACE(ISNULL([ENDNAME],[FNAME]),'«',''),'»',''),'–','-'),'.zip.zip','.zip'),'.docx','.docx'),' ',' ') + '"' FROM ( SELECT [ELMA].[dbo].[IncomingDoc].[Id] AS [Id], [ELMA].[dbo].[IncomingDoc].[Subj] AS [Subj], [ELMA].[dbo].[IncomingDoc].[ReceiveDate] AS [RD], LTRIM(STR([F0].[Id])) + REVERSE(SUBSTRING(REVERSE([F0].[Name]), 0, CHARINDEX('.',REVERSE([F0].[Name])) + 1)) AS [FNAME], LTRIM(STR([F0].[Id]) + ' ' + [Name]) AS [ENDNAME] FROM [ELMA].[dbo].[IncomingDoc] LEFT JOIN [ELMA].[dbo].[IncomingDoc_DocAttachments] ON [ELMA].[dbo].[IncomingDoc_DocAttachments].[Parent] = [ELMA].[dbo].[IncomingDoc].[Id] LEFT OUTER JOIN [ELMA].[dbo].[FS_FILES] AS [F0] ON [F0].[Uid] = [ELMA].[dbo].[IncomingDoc_DocAttachments].[DocAttachment] UNION SELECT [ELMA].[dbo].[IncomingDoc].[Id] AS [Id], [ELMA].[dbo].[IncomingDoc].[Subj] AS [Subj], [ELMA].[dbo].[IncomingDoc].[ReceiveDate] AS [RD], LTRIM(STR(ISNULL([F1].[Id],[F2].[Id]))) + REVERSE(SUBSTRING(REVERSE(ISNULL([F1].[Name],[F2].[Name])), 0, CHARINDEX('.',REVERSE(ISNULL([F1].[Name],[F2].[Name]))) + 1)) AS [FNAME], NULL AS [ENDNAME] FROM [ELMA].[dbo].[IncomingDoc] LEFT JOIN [ELMA].[dbo].[DocumentVersion] AS [DV1] ON [DV1].[DocumentId] = [ELMA].[dbo].[IncomingDoc].[Id] AND [DV1].[Status] = 2 AND ISNUMERIC([DV1].[File]) = 1 LEFT JOIN [ELMA].[dbo].[DocumentVersion] AS [DV2] ON [DV2].[DocumentId] = [ELMA].[dbo].[IncomingDoc].[Id] AND [DV2].[Status] = 2 AND ISNUMERIC([DV1].[File]) = 0 LEFT JOIN [ELMA].[dbo].[FS_FILES] AS [F1] ON [F1].[Id] = [DV1].[File] LEFT JOIN [ELMA].[dbo].[FS_FILES] AS [F2] ON [F2].[Uid] = [DV2].[File] ) AS [F] WHERE [FNAME] IS NOT NULL ORDER BY [RD]
Загрузил csv файл, загрузил все файлы входящих. Процедура оказалась долгой, т.к. общий объем файлов был несколько гигабайт. Хорошо, что импорт данных по 1000 записей, т.к. без накладок не обошлось. Обязательно в каждой тысяче всплывало по 10-15 ошибок. Выявить ошибки удалось перед процедурой импорта, с помощью предварительного просмотра. Очень удобно. В общем, каждая тысяча требовала предварительной подготовки.
Таким же образом импортировал все исходящие документы.
После этого, в объектах была написана процедура преобразования имен файлов. Она удаляла id, а data, переносила в revdata файла в easla.com. Таким образом, файлы приложений получили исходные имена filename.ext.
function updateAttachmentsFileName() { $files = cobjectref()->attributeref('crs_management_incoming_attachments')->availableFiles(); foreach ($files as $f) { $parts = explode(' ', $f->nowname, 2); if (count($parts) == 2) { $f->nowname = $parts[1]; $f->save(); } } }
Специально для входящих писем была написана процедура, которая заменяла описания ссылок на исходящие документы на их идентификаторы.
function updateOutgoing() { if (empty(cobjectref()->attributeref('crs_management_incoming_outgoing')->value)) return; $outgoing_doc = selectAll( 'crs_management', 'crs_management_outgoing', array(), array('crs_management_outgoing_regnum'=>cobjectref()->attributeref('crs_management_incoming_outgoing')->value) ); if (empty($outgoing_doc)) return; cobjectref()->attributeref('crs_management_incoming_outgoing')->value = $outgoing_doc['id']; }
Проверив работу этих процедур запустил пакетное обновление объектов. Медленно, но верно easla.com обновила все объекты! После этого, изменил тип атрибута ссылающегося на исходящий документ во входящем документе со «Строка» на «Объект», чтобы идентификаторы объекта стали объектами.
Таким образом, easla.com переименовала файлы и организовала перекрестные ссылки между входящими и исходящими документами!
В целом, процедура экспорта и импорта заняла примерно 3 рабочих дня. Загрузку файлов на easla.com осуществлял по вечерам, когда канал Интернет был свободен. Импорт данных по 1000 был удобен еще и потому, что старые письма можно было импортировать сразу, не беспокоясь о целостности данных. И только перед «боевым» запуском загрузил и импортировал остатки – последние пару сотен писем.
Миграцию осуществил в конце 2014 года, а запустил в эксплуатацию все описанные бизнес-процессы 12 января 2015 года.
Прошло уже больше года. Общий объем писем превысил 20 тысяч. Количество задач перевалило за 10 тысяч. Вот свежие данные из главного меню:
Итоги
Могу с уверенностью сказать, что год назад все сделал правильно. Вот основные выгоды:
- Время на регистрацию писем сократилось до секунд или минут
- Время на отправку исходящего письма сократилось с 30-40 минут до секунд
- Появилась возможность размещать файлы исходящих писем на файлообменнике easla.com
- Разработка письма стала проще, т.к. теперь файл в формате docx хранится в объекте easla.com, а не где попало у пользователя
- Связь задач и писем позволяет формировать и выгружать сложные отчеты, которые приносят организации прибыль
- Получил развитие процесс управления договорами, если раньше он был простым каталогом, то теперь он настоящий процесс со статусами, контролем выполнения, подписанием актов и поступлением средств.
В настоящий момент развитие получили и другие процессы: управление задачам, согласованиями, субподрядными договорами. Все то, что опасались ранее делать в elma, теперь сделано в easla.com!
P.S. Прошу не расценивать статью как антирекламу elma. Система не подошла нам, но вполне может подойти другим, если они не будут «выжимать из нее соки», как мы.
ссылка на оригинал статьи https://habrahabr.ru/post/282662/
Добавить комментарий