Интеграция ФИАС (Федеральная информационная адресная система) с DirectumRX

16 4

В "чистой" системе Driectum RX в полях "Почтовый адрес" можно указывать произвольный текст, который не всегда заполняется корректно и может отсутствовать в федеральной информационной адресной системе. Интеграция с ФИАС позволяет заполнять поля почтовых адресов корректными данными, в правильном формате.

Задача

Поле "Почтовый адрес" используется во многих сущностях системы. Это и справочник "Компании", и справочник "Персоны, и "Наши организации". Кроме того, в некоторых готовых решениях также присутствуют поля с подобным названием, например, решение "Модуль обращений граждан", в котором в карточке обращения на вкладке "Заявитель" есть поле "Почтовый адрес" заявителя.

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

Для более удобного заполнения полей "Почтовый адрес" корректно сформированными адресами, хранящимися в базе ФИАС, можно использовать любой API-сервер ФИАС, например https://kladr-api.ru/docs .

Ниже будет представлен вариант реализации.

Решение

Что было сделано на нашем проекте:

  1. Созданы константы, хранящие адрес API-сервера ФИАС (В нашем случае на стороне заказчика развернут свой API-сервер с актуальной базой ФИАС`а) и максимальное количество результатов для отображения пользователю;

  2. Реализовано диалоговое окно для пользователя;

  3. Добавлена дополнительная кнопка, открывающая диалоговое окно подбора адресов;

  4. Описана структура данных, возвращаемых API-сервером ФИАС`а.

При разработке, мы столкнулись с тем, что нельзя из серверного метода передавать в клиентский код и обратно список IList структур. Для преодоления этого ограничения было добавлено 2 связанные структуры. Первая структура хранит текстовое представление адреса в ФИАС и его GUID. Вторая структура хранит список ILits первых структур и текст ошибки, если API вернуло ошибку.

Ниже представлены структуры:

  /// <summary>
  /// Структура связывает адрес в ФИАС и его GUID.
  /// Параметры с маленькой буквы т.к. заполняются десериализацией json данных, в которых параметры приходят тоже с маленькой буквы.
  /// </summary>
  [Public]
  partial class AddressFIAS
  {
    public string address { get; set; }
    public string ao_guid { get; set; }
  }
 
  /// <summary>
  /// Структура хранит список связей адресов и их GUIDов.
  /// </summary>
  [Public]
  partial class AddressesFIAS
  {
    public System.Collections.Generic.IList<GD.MainSolution.Module.CitizenRequests.Structures.Module.AddressFIAS> AddressFIAS { get; set; }
 public string Error { get; set; }
  }

  /// <summary>
  /// Структура для заполнения полей в карточке персоны или обращения.
  /// </summary>
  [Public]
  partial class FIASResult
  {
    public string Address { get; set; }
    public string FIASguidSC { get; set; }
    public bool FilledFromFIASSC { get; set; }
  }

 

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

    /// <summary>
    /// Открыть диалоговое окно для заполнения почтового адреса с ФИАС
    /// </summary>
    public void OpenDialogFIAS()
    {
      var result = GD.PTOEDMS.PublicFunctions.Module.OpenDialogFIAS(_obj.PostalAddress);
      if (!string.IsNullOrEmpty(result.Address))
      {
        _obj.PostalAddress = result.Address;
        _obj.FIASguidSC = !string.IsNullOrEmpty(result.FIASguidSC) ? result.FIASguidSC : null;
        _obj.FilledFromFIASSC = result.FilledFromFIASSC;
      }
    }

Диалоговое окно

Реализация работы диалогового окна представляет собой следующую последовательность действий:

  1. Получение пользовательских настроек или настроек заданных в константах, если не заполнены пользовательские;

  2. Объявляются используемые переменные и структуры;

  3. Если поле «Почтовый адрес» не было пустым, то выполняется первичный запрос к API с данными из этого поля для предзаполнения полей диалогового окна;

  4. Создается диалоговое окно;

  5. Добавляются обработчики изменения значения поля «Адрес в ФИАС» и чек-бокса « Отсутствует адрес в ФИАС»;

  6. При нажатии «Ок» заполняются поля результирующей структуры и метод возвращает эту структуру.

Метод работы диалогового окна реализованный в клиентском коде с модификатором доступа Public:

    /// <summary>
    /// Диалоговое окно работы с адресами ФИАС
    /// </summary>
    [Public]
    public GD.MainSolution.Module.CitizenRequests.Structures.Module.IFIASResult OpenDialogFIAS(string postalAddress)
    {
      var settings = GD.MainSolution.Module.CitizenRequests.PublicFunctions.Module.Remote.GetUrlFIAS();
      var url = settings[0];
      var count = int.Parse(settings[1]);
      var token = settings[2];
      var result = GD.MainSolution.Module.CitizenRequests.Structures.Module.FIASResult.Create(string.Empty, string.Empty, false);
      var addressStruct = GD.MainSolution.Module.CitizenRequests.Structures.Module.AddressesFIAS.Create();
      var addressesString = new string[count];
      if (!string.IsNullOrEmpty(postalAddress))
      {
        addressStruct = GD.MainSolution.Module.CitizenRequests.PublicFunctions.Module.Remote.RefreshAdress(addressStruct, postalAddress, url, count.ToString(), token);
        if (!string.IsNullOrEmpty(addressStruct.Error))
        {
          Logger.DebugFormat("!!! FIAS Error - {0}", addressStruct.Error);
          addressStruct.Error = string.Empty;
        }
        else
        {
          addressesString = addressStruct.AddressFIAS.Take(count).Select(a => a.address).ToArray();
        }
      }
      // Создать диалог ввода.
      var dialog = Dialogs.CreateInputDialog(GD.MainSolution.People.Resources.SearchAddressFIAS);
      if (addressesString.Length >= count)
      {
        dialog.Text = GD.MainSolution.People.Resources.AddressesMoreWhenSettingFormat(count);
      }
      else
      {
        dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat(addressesString.Length.ToString());
      }
      // Добавить поле для выбора адреса
      var addressString = dialog.AddString(GD.MainSolution.People.Resources.PostalAddress, true, postalAddress);
      var addressField = dialog.AddSelect(GD.MainSolution.People.Resources.AddressFIAS, true, 0).From(addressesString);
      var missingFIASAddress = dialog.AddBoolean(GD.MainSolution.People.Resources.MissingFIASAddress, false);
      dialog.Width = 1000;
      addressString.SetOnValueChanged((x) =>
                                      {
                                        if (x.NewValue != x.OldValue && !string.IsNullOrEmpty(x.NewValue) && !missingFIASAddress.Value.Value)
                                        {
                                          if (addressStruct.AddressFIAS != null)
                                          {
                                            addressStruct.AddressFIAS.Clear();
                                          }
                                          addressStruct = GD.MainSolution.Module.CitizenRequests.PublicFunctions.Module.Remote.RefreshAdress(addressStruct, x.NewValue,
                                                                                                                                             url, count.ToString(), token);
                                          if (!string.IsNullOrEmpty(addressStruct.Error))
                                          {
                                            Logger.DebugFormat("!!! FIAS Error - {0}", addressStruct.Error);
                                            dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat("0");
                                            addressField.From(string.Empty);
                                            addressField.Value = string.Empty;
                                            addressStruct.Error = string.Empty;
                                          }
                                          else
                                          {
                                            addressesString = addressStruct.AddressFIAS.Take(count).Select(a => a.address).ToArray();
                                            if (addressesString.Length > 0)
                                            {
                                              if (addressesString.Length >= count)
                                              {
                                                dialog.Text = GD.MainSolution.People.Resources.AddressesMoreWhenSettingFormat(count);
                                              }
                                              else
                                              {
                                                dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat(addressesString.Length.ToString());
                                              }
                                              addressField.From(addressesString);
                                              addressField.ValueIndex = 0;
                                            }
                                            else
                                            {
                                              dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat("0");
                                              addressField.From(string.Empty);
                                              addressField.Value = string.Empty;
                                            }
                                          }
                                        }
                                      });
      missingFIASAddress.SetOnValueChanged((x) =>
                                           {
                                             addressField.IsEnabled = !x.NewValue.Value;
                                             addressField.IsRequired = !x.NewValue.Value;
                                             if (x.NewValue.Value == false && !string.IsNullOrEmpty(addressString.Value))
                                             {
                                               if (addressStruct.AddressFIAS != null)
                                               {
                                                 addressStruct.AddressFIAS.Clear();
                                               }
                                               addressStruct = GD.MainSolution.Module.CitizenRequests.PublicFunctions.Module.Remote.RefreshAdress(addressStruct, addressString.Value,
                                                                                                                                                  url, count.ToString(), token);
                                               if (!string.IsNullOrEmpty(addressStruct.Error))
                                               {
                                                 Logger.DebugFormat("!!! FIAS Error - {0}", addressStruct.Error);
                                                 dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat("0");
                                                 addressField.From(string.Empty);
                                                 addressField.Value = string.Empty;
                                                 addressStruct.Error = string.Empty;
                                               }
                                               else
                                               {
                                                 addressesString = addressStruct.AddressFIAS.Take(count).Select(a => a.address).ToArray();
                                                 if (addressesString.Length > 0)
                                                 {
                                                   if (addressesString.Length >= count)
                                                   {
                                                     dialog.Text = GD.MainSolution.People.Resources.AddressesMoreWhenSettingFormat(count);
                                                   }
                                                   else
                                                   {
                                                     dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat(addressesString.Length.ToString());
                                                   }
                                                   addressField.From(addressesString);
                                                   addressField.ValueIndex = 0;
                                                 }
                                                 else
                                                 {
                                                   dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat("0");
                                                   addressField.From(string.Empty);
                                                   addressField.Value = string.Empty;
                                                 }
                                               }
                                             }
                                             else
                                             {
                                               dialog.Text = GD.MainSolution.People.Resources.AvailableAddressesFIASFormat("0");
                                               addressField.From(string.Empty);
                                               addressField.Value = string.Empty;
                                             }
                                           });

      if (dialog.Show() == DialogButtons.Ok)
      {
        // Сформировать строку из результатов ввода.
        if (missingFIASAddress.Value.Value)
        {
          result.Address = addressString.Value;
          return result;
        }
        else
        {
          result.Address = addressField.Value;
          var currentAddress = addressStruct.AddressFIAS.Where(a => a.address == addressField.Value).FirstOrDefault();
          result.FIASguidSC = currentAddress != null ? currentAddress.ao_guid : string.Empty;
          result.FilledFromFIASSC = currentAddress != null ? true : false;
          return result;
        }
      }
      return result;
    }

Взаимодействие с API реализовано GET-запросами по протоколу http:

    /// <summary>
    /// Обновить адреса в выпадающем списке
    /// </summary>
    [Public, Remote(IsPure = true)]
    public GD.MainSolution.Module.CitizenRequests.Structures.Module.IAddressesFIAS RefreshAdress(GD.MainSolution.Module.CitizenRequests.Structures.Module.IAddressesFIAS addressesFias,
string address,                                                                                                 string url,                                                                                                 string count,                                                                                                 string token)
    {
      using (var client = new HttpClient())
      {
        try
        {
          client.DefaultRequestHeaders.TryAddWithoutValidation("Authorization", token);
          var builder = new UriBuilder(url);
          builder.Query = string.Format("term={0}", address);
          url = builder.ToString();
          
          var response = client.GetAsync(url).Result;
          if (response.StatusCode == HttpStatusCode.OK)
          {
            var result = response.Content.ReadAsStringAsync().Result;
            addressesFias.AddressFIAS = JsonConvert.DeserializeObject<IList<Structures.Module.AddressFIAS>>(result);
            return addressesFias;
          }
          else
          {
            addressesFias.Error = response.Content.ReadAsStringAsync().Result;
            return addressesFias;
          }
        }
        catch (Exception ex)
        {
          addressesFias.Error = ex.Message;
          return addressesFias;
        }
      }
    }

Переменная token на нашем проекте используется для авторизации на сервере API заказчика. При использовании другого API могут потребоваться другие способы авторизации. Также авторизация может отсутствовать.

Пример использования в интерфейсе

Внешний вид вкладки "Заявитель" карточки обращения стал выглядеть так:

https://club.directum.ru/uploads/images/f7258a18-a3f2-487e-8dee-27449096a9a1.png

Поле "Почтовый адрес" сделали не редактируемым в самой карточке обращения, но ввести адрес вручную можно в диалоговом окне подбора адресов.

Для подбора адреса нужно нажать кнопку "Адрес в ФИАС". Откроется диалоговое окно с подбором адреса.

 

https://club.directum.ru/uploads/images/6970e121-20a9-4399-b071-fe9bcd41556b.png

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

Содержание диалогового окна:

  • Краткая информация о работе диалогового окна;
  • Информационное сообщение о количестве доступных для выбора адресов (максимальное возможное количество задано в константе, в нашем случае 10);
  • Поле для ввода почтового адреса в произвольном формате;
  • Выпадающий список с доступными адресами с ФИАС`а;
  • Чек-бокс, на случай если адрес в ФИАС не нашёлся и нужно указать его вручную.

Выбор доступных адресов выполняется следующим образом:

Как это работает внутри

  1. Пользователь вписывает адрес в произвольном формате в поле "Почтовый адрес";

  2. Пользователь снимает фокус с текстового поля, например, переходя к выпадающему списку (Обязательное условие срабатывания метода SetOnValueChanged());

  3. СЭД отправляет строку из поля "Почтовый адрес" на API-сервер ФИАС и затем, получив ответ, десериализует ответ в структуру;

  4. Из полученной структуры формируем список адресов доступных для выбора в выпадающем списке, а количество этих адресов выводим в информационном сообщении о количестве доступных адресов для выбора;

  5. Пользователь выбирает нужный адрес из списка предложенных, либо, если нужного адреса нет, дополняет текстовое поле "Почтовый адрес" уточнениями и повторяются пункты 2-5;

  6. Пользователь нажимает кнопку "Ок". В этот момент нужный нам адрес подставляется в поле "Почтовый адрес" карточки обращения, заполняется чек-бокс "Заполнено из ФИАС" и происходят дополнительные действия.

Результаты

Итак, что же даёт нам взаимодействие с ФИАС:

  • Позволяет заполнять почтовый адреса в корректном формате, тратя гораздо меньше времени на заполнение и актуализацию адресов;
  • Дополнительно можно заполнять не только поле «Почтовый адрес», но и данные по административно-территориальным единицам, такие как город, район и т.д.;
  • Кроме самих адресов можно хранить их GUID и прочую дополнительную информацию, часто необходимый для межведомственных взаимодействий СЭД;
  • Не нужно хранить базу адресов в информационном пространстве СЭД;
  • Доработку можно добавить в карточки любых сущностей с минимальной настройкой, обращаясь к методам, реализующим диалоговое окно.

 

Евгений Куликов

Спасибо за статью.

У меня следующий вопрос. А разве сейчас налоговая не перешла на ГАР, вместо ФИАС? Не лучше ли использовать именно ГАР ?

Андрей Кораблёв

Евгений, Добрый день! Все верно. И в данный момент ФИАС использует базу данных ГАР.
ФИАС является адресной системой, а ГАР - это конкретная база данных (Адресный реестр). 

 

На сайте ФИАС (https://fias.nalog.ru/Updates#) основная БД указана как раз ГАР.

Андрей Кораблёв: обновлено 20.10.2022 в 10:41
Алена Рунова

Помню когда-то делали такой же проект на Directum 5. Здорово что подобный кейс реализовали на DirectumRX ! Спасибо за статью!

Анатолий Придыбайло

Оставлю это тут:
Репозиторий с шаблоном разработки «Загрузка данных из xlsx-файлов с обложки» в котором реализована загрузка ФИАС в систему https://github.com/DirectumCompany/rx-template-upload-data-ui 

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