Новые элементы номера в справочнике "Журнал регистрации"

17 8

Кейс из заголовка встречается, если не на каждом крупном проекте, то у половины клиентов точно. Тем не менее, его реализация вызывает кучу вопросов у новых разработчиков из-за "интуитивно" понятной разработки механизма регистрации и "простенького" диалога.

Давайте попробуем немножко разобраться в данной тематике.


 

Постановка задачи

В наличии следующие компоненты:

  • Разработанный ТД "Заявки" на основе OfficialDocument;
  • Разработанный справочник "Виды заявок".

Дополнительная информация: 

  • В ТД "Заявки" добавлено свойство-ссылка "Вид заявки" (ссылка на справочник "Виды заявок");
  • В справочнике "Виды заявок" добавлено текстовое свойство "Код вида заявки";
  • Вид документа "Заявка" нумеруемый и имеет признак "Автоматическая нумерация".

Необходимо добавить в "Журнал регистрации" для ТД "Заявка" новый элемент номера "Код вида заявки", который должен подставляться в регистрационный номер документа из свойства "Код вида заявки", расположенного на форме карточки справочника "Виды заявок".

Общий анализ механизма регистрации


Для лучшего восприятия, код выполняемый в клиентском слое выделен синим цветом, серверный/разделяемый слой зеленым.

Всю логику механизма регистрации можно условно разделить на две отдельные ветки:
1) Регистрация через пользовательский диалог регистрации/нумерации/резервирования (он один для всех механизмов).
2) Автонумерация документа при его сохранении, если ВЭД документа нумеруемый и в нем установлен признак "Автоматическая нумерация".

С одной стороны, эти две ветки используют одни и те же механизмы из справочника DocumentRegister (Журнал регистрации), с другой стороны не совсем те же ...


Код выполняемый в клиентском слое выделен синим цветом, разделяемый слой серый, серверный слой зеленый.
Черный и красные стрелки - вызов, голубые - ответ.
Ветка начинающаяся с 1-1 - это работа диалога регистрации.
Ветка начинающаяся с 2-1 - это работа механизма автонумерации.

Тут я опустил часть функций, чтобы уж совсем не перегружать схему, но если присмотреться, то базово при работе диалога регистрации, на разных этапах, вызывается функция GetNextNumber(), которая вызывает функции разделяемого слоя и далее по схеме, а вот автонумерация на сохранении документа сразу вызывает GenerateRegistrationNumberPrefixAndPostfix(). Причем _obj текущего объекта (документ) доступен только в GetNextNumber(), а дальше передаются уже примитивы, по которым крайне сложно идентифицировать и получить документ для обработки.

Из этого потока сознания следует логичный вывод: для полноценной реализации нужно делать изменения не только в GetNextNumber(), который будет работать условно для всех ТД в системе, но и в ТД "Заявки", т.к. механизм автонумерации обходит ту часть механизма, где фактически можно подставить свой элемент номера.

На текущем этапе поняли, что ничего не поняли!
Пойдемте разрабатывать.

Реализация 

Весь код в перекрытых функциях я буду выделать в директивой #region CUSTOM, чтобы легче было отличить, где базовая логика, а где доработка.

1) Перекрываем справочник DocumentRegister, если ранее этого не сделали.

2) В справочнике находим коллекцию NumberFormatItems, в ней свойство Element и добавляем свое перечисление RequestKindCodestarkov.

3) Добавляем событие "Фильтрация выбора из выпадающего списка" и пишем код фильтрации

public override IEnumerable<Enumeration> NumberFormatItemsElementFiltering(IEnumerable<Enumeration> query)
    {
      query = base.NumberFormatItemsElementFiltering(query);
      
      #region CUSTOM
      var excludeRequestKindCodes = true;
      var documentRegisters = Sungero.Docflow.PublicFunctions.RegistrationSetting.GetByDocumentRegister(Sungero.Docflow.DocumentRegisters.As(_obj.DocumentRegister));      
      var documentKinds = documentRegisters.SelectMany(r => r.DocumentKinds.Select(d => d.DocumentKind));
      
      //Скидываем флаг, если в настройках регистрации есть ВЭД, который относится к ТД "Заявка"
      if (documentKinds.Any(dk => dk.DocumentType.DocumentTypeGuid == starkov.Common.PublicConstants.Module.EntitysGuid.DocumentTypeGuids.ApplicationOnEquipmentAndAccessGuid))
        excludeRequestKindCodes = false;
      
      //Исключить элемент "Код вида заявки" для всех ТД, кроме ТД "Заявка"
      if (excludeRequestKindCodes)
        query = query.Where(e => e != starkov.CommonOverlap.DocumentRegisterNumberFormatItems.Element.RequestKindCode);
      #endregion

      return query;
    }

Тут нужно немного пояснить за код. 

Т.к. в самом журнале регистрации нет привязки ни к ТД, ни к ВЭД, то приходится идти от обратного, а именно от настроек регистрации, где явно указан журнал регистрации, а чтобы не плодить кучу функций я взял готовую GetByDocumentRegister().

4) Перекрываем в разделяемом слое функцию GenerateRegistrationNumberPrefixAndPostfix()

    /// <summary>
    /// Генерировать префикс и постфикс регистрационного номера документа.
    /// </summary>
    /// <param name="date">Дата.</param>
    /// <param name="leadingDocumentNumber">Ведущий документ.</param>
    /// <param name="departmentCode">Код подразделения.</param>
    /// <param name="businessUnitCode">Код нашей организации.</param>
    /// <param name="caseFileIndex">Индекс дела.</param>
    /// <param name="docKindCode">Код вида документа.</param>
    /// <param name="counterpartyCode">Код контрагента.</param>
    /// <param name="counterpartyCodeIsMetasymbol">Признак того, что код контрагента нужен в виде метасимвола.</param>
    /// <returns>Сгенерированный регистрационный номер.</returns>
    public override Sungero.Docflow.Structures.DocumentRegister.RegistrationNumberParts GenerateRegistrationNumberPrefixAndPostfix(DateTime date,                                                                                                                                                              
                                           string leadingDocumentNumber,                                                                                                                                                                                                                        
                                           string departmentCode,                                                                                                                                  
                                           string businessUnitCode,                                                                                                                                  
                                           string caseFileIndex,                                                                                                                                 
                                           string docKindCode,                                                                                                                                  
                                           string counterpartyCode,                                                                                                                                  
                                           bool counterpartyCodeIsMetasymbol)
    {
      var prefix = string.Empty;
      var postfix = string.Empty;
      var numberElement = string.Empty;
      var orderedNumberFormatItems = _obj.NumberFormatItems.OrderBy(f => f.Number);
      foreach (var element in orderedNumberFormatItems)
      {
        if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Number)
        {
          prefix = numberElement;
          numberElement = string.Empty;
        }
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Log)
          numberElement += _obj.Index;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.RegistrPlace && _obj.RegistrationGroup != null)
          numberElement += _obj.RegistrationGroup.Index;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Year2Place)
          numberElement += date.ToString("yy");
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Year4Place)
          numberElement += date.ToString("yyyy");
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Month)
          numberElement += date.ToString("MM");
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.Quarter)
          numberElement += ToQuarterString(date);
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.LeadingNumber)
          numberElement += leadingDocumentNumber;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.DepartmentCode)
          numberElement += departmentCode;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.BUCode)
          numberElement += businessUnitCode;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.CaseFile)
          numberElement += caseFileIndex;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.DocKindCode)
          numberElement += docKindCode;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.CPartyCode && !counterpartyCodeIsMetasymbol)
          numberElement += counterpartyCode;
        else if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.CPartyCode && counterpartyCodeIsMetasymbol)
          numberElement += DocumentRegisters.Resources.NumberFormatCounterpartyCode;
        else
        
        #region CUSTOM
        if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.RequestKindCode)
          numberElement += DocumentRegisters.Resources.RequestKindCode;
        #endregion

        // Не добавлять разделитель, для пустого кода контрагента.
        if (string.IsNullOrEmpty(counterpartyCode) || counterpartyCodeIsMetasymbol)
        {
          // Разделитель после пустого кода контрагента.
          if (element.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.CPartyCode)
            continue;
          
          // Разделитель до кода контрагента, если код контрагента последний в номере.
          var nextElement = orderedNumberFormatItems.Where(f => f.Number > element.Number).FirstOrDefault();
          var lastElement = orderedNumberFormatItems.LastOrDefault();
          if (nextElement != null && nextElement.Element == CommonOverlap.DocumentRegisterNumberFormatItems.Element.CPartyCode &&
              lastElement != null && lastElement.Number == nextElement.Number)
            continue;
        }
        
        // Добавить разделитель.
        numberElement += element.Separator;
      }
      
      postfix = numberElement;
      return Sungero.Docflow.Structures.DocumentRegister.RegistrationNumberParts.Create(prefix, postfix);
    }

Т.к. новый элемент может находиться в любом месте номера, а сборка происходит в цикле foreach, то приходится перетаскивать весь код функции, ради пары строк кода.

Так же вместо элемента я вставляю текст ресурса, далее в коде будем его менять на значение.

5) Можно двигаться в серверный слой и перекрывать функцию GetNextNumber()

        /// <summary>
		/// Получить следующий регистрационный номер.
		/// </summary>
		/// <param name="date">Дата регистрации.</param>
		/// <param name="leadDocumentId">ID ведущего документа.</param>
		/// <param name="document">Документ.</param>
		/// <param name="leadingDocumentNumber">Номер ведущего документа.</param>
		/// <param name="departmentId">ИД подразделения.</param>
		/// <param name="businessUnitId">ID НОР.</param>
		/// <param name="caseFileIndex">Индекс дела.</param>
		/// <param name="docKindCode">Код вида документа.</param>
		/// <param name="indexLeadingSymbol">Ведущий символ индекса.</param>
		/// <returns>Регистрационный номер.</returns>
		[Remote(IsPure = true)]
		public override string GetNextNumber(DateTime date,
		                                     int leadDocumentId,
		                                     Sungero.Docflow.IOfficialDocument document,
		                                     string leadingDocumentNumber,
		                                     int departmentId,
		                                     int businessUnitId,
		                                     string caseFileIndex,
		                                     string docKindCode,
		                                     string indexLeadingSymbol)
		{
			var number = base.GetNextNumber(date, leadDocumentId, document, leadingDocumentNumber, departmentId, businessUnitId, caseFileIndex, docKindCode, indexLeadingSymbol);
			
            #region CUSTOM
			if (document != null)
				number = ChangeCustomElementNumber(document, number);
            #endregion
			
			return number;
		}

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

6) Собственно пишем публичную серверную функцию ChangeCustomElementNumber(), которую мы вызвали в предыдущем пункте

        /// <summary>
		/// Заменить кастомные элементы номера.
		/// </summary>
		/// <param name="document">Id документа.</param>
		/// <param name="number">Регистрационный номер.</param>
		/// <returns>Регистрационный номер.</returns>
		[Public]
		public static string ChangeCustomElementNumber(object document, string number)
		{
			if (number.IndexOf(DocumentRegisters.Resources.RequestKindCode) > -1)
			{
				var type = document.GetType();
				var typeProperty = type.GetProperty("RequestTypeCode");
				if (typeProperty != null)
				{
					var requestKindCode = (string)typeProperty.GetValue(document);
					number = number.Replace(DocumentRegisters.Resources.RequestKindCode, requestKindCode);
				}
			}
			
			return number;
		}

И вот на этом месте нужно пояснить поподробнее.

Если в решении, в котором вы сделали перекрытие справочника DocumentRegister можно добавить зависимость от другого решения, где реализован новый или перекрытый ТД, или ТД реализован в этом же решении, то для Вас никаких заморочек не будет. Приводите  документ (Sungero.Docflow.IOfficialDocument document) к нужному Вам типу через .As() и берете нужный реквизит прямо из него.

//Пример без рефлексии
public static string ChangeCustomElementNumber(Sungero.Docflow.IOfficialDocument document, string number)
{
   var application = Issuance.ApplicationOnEquipmentAndAccesses.As(document);
   if (application != null)
	 number = number.Replace(DocumentRegisters.Resources.RequestKindCode, application.RequestTypeCode);			
   return number;
}

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

7) Осталось добавить проверку номера на валидность, поэтому идем в разделяемый слой и перекрываем функцию CheckRegistrationNumberFormat()

    /// <summary>
    /// Проверить регистрационный номер на валидность.
    /// </summary>
    /// <param name="registrationDate">Дата регистрации.</param>
    /// <param name="registrationNumber">Номер регистрации.</param>
    /// <param name="departmentCode">Код подразделения.</param>
    /// <param name="businessUnitCode">Код нашей организации.</param>
    /// <param name="caseFileIndex">Индекс дела.</param>
    /// <param name="docKindCode">Код вида документа.</param>
    /// <param name="counterpartyCode">Код контрагента.</param>
    /// <param name="leadDocNumber">Номер ведущего документа.</param>
    /// <param name="searchCorrectingPostfix">Искать корректировочный постфикс.</param>
    /// <returns>Сообщение об ошибке. Пустая строка, если номер соответствует журналу.</returns>
    /// <remarks>Пример: 5/1-П/2020, где 5 - порядковый номер, П - индекс журнала, 2020 - год, /1 - корректировочный постфикс.</remarks>
    public override string CheckRegistrationNumberFormat(DateTime? registrationDate,
                                                         string registrationNumber,
                                                         string departmentCode,
                                                         string businessUnitCode,
                                                         string caseFileIndex,
                                                         string docKindCode,
                                                         string counterpartyCode,
                                                         string leadDocNumber,
                                                         bool searchCorrectingPostfix)
    {
      if (string.IsNullOrWhiteSpace(registrationNumber))
        return DocumentRegisters.Resources.EnterRegistrationNumber;
      
      // Регулярное выражение для рег. индекса.
      // "([0-9]+)" определяет, где искать индекс в номере.
      // "([\.\/-][0-9]+)?" определяет, где искать корректировочный постфикс в номере.
      // Пустые скобки в выражении @"([0-9]+)()" означают корректировочный постфикс,
      // чтобы количество групп в результате регулярного выражения было всегда одинаковым, независимо от того, нужно искать корректировочный постфикс или нет.
      var indexTemplate = searchCorrectingPostfix ? @"([0-9]+)([\.\/-][0-9]+)?" : @"([0-9]+)()";
      
      // Перед проверкой правильности формата дополнительно проверить наличие непечатных символов в строке ("\s").
      if (Regex.IsMatch(registrationNumber, @"\s"))
        return DocumentRegisters.Resources.NoSpaces;
      
      if (!GetRegexMatchFromRegistrationNumber(_obj, registrationDate ?? Calendar.UserToday, registrationNumber, indexTemplate,
                                               departmentCode, businessUnitCode, caseFileIndex, docKindCode, counterpartyCode, leadDocNumber,
                                               string.Empty, string.Empty)
          .Success)
      {
        // Шаблон номера, состоящий из символов "*".
        var numberTemplate = string.Concat(Enumerable.Repeat("*", _obj.NumberOfDigitsInNumber.Value));
        var example = base.GenerateRegistrationNumber(registrationDate.Value, numberTemplate, departmentCode, businessUnitCode, caseFileIndex, docKindCode, counterpartyCode, "0");
        return Sungero.Docflow.Resources.RegistrationNumberNotMatchFormatFormat(example);
      }
      
      return string.Empty;
    }

Это целиком базовая разработка, за исключением своих namespace и делается она для того, чтобы доработать статическую функцию GetRegexMatchFromRegistrationNumber()

7) Копируем и дорабатываем функцию GetRegexMatchFromRegistrationNumber()

/// <summary>
    /// Получить сравнение рег.номера с шаблоном.
    /// </summary>
    /// <param name="documentRegister">Журнал.</param>
    /// <param name="date">Дата.</param>
    /// <param name="registrationNumber">Рег. номер.</param>
    /// <param name="indexTemplate">Шаблон номера.</param>
    /// <param name="departmentCode">Код подразделения.</param>
    /// <param name="businessUnitCode">Код нашей организации.</param>
    /// <param name="caseFileIndex">Индекс дела.</param>
    /// <param name="docKindCode">Код вида документа.</param>
    /// <param name="counterpartyCode">Код контрагента.</param>
    /// <param name="leadDocNumber">Номер ведущего документа.</param>
    /// <param name="numberPostfix">Постфикс номера.</param>
    /// <param name="additionalPrefix">Дополнительный префикс номера.</param>
    /// <returns>Индекс.</returns>
    internal static Match GetRegexMatchFromRegistrationNumber(IDocumentRegister documentRegister, DateTime date, string registrationNumber,
                                                              string indexTemplate, string departmentCode, string businessUnitCode,
                                                              string caseFileIndex, string docKindCode, string counterpartyCode, string leadDocNumber,
                                                              string numberPostfix, string additionalPrefix)
    {
      var prefixAndPostfix = Functions.DocumentRegister.GenerateRegistrationNumberPrefixAndPostfix(documentRegister, date, leadDocNumber, departmentCode,
                                                                                                   businessUnitCode, caseFileIndex, docKindCode, counterpartyCode, true);
      
      var template = string.Format("{0}{1}{2}{3}", Regex.Escape(prefixAndPostfix.Prefix),
                                   indexTemplate,
                                   Regex.Escape(prefixAndPostfix.Postfix),
                                   numberPostfix);

      // Заменить метасимвол для кода контрагента на соответствующее регулярное выражение.
      var metaCounterpartyCode = Regex.Escape(DocumentRegisters.Resources.NumberFormatCounterpartyCode);
      template = template.Replace(metaCounterpartyCode, Sungero.Docflow.Constants.DocumentRegister.CounterpartyCodeRegex);
      
      #region CUSTOM
      // Заменить метасимвол для вида заявки на соответствующее регулярное выражение.
      var metaRequestKindCode = Regex.Escape(DocumentRegisters.Resources.RequestKindCode);
      template = template.Replace(metaRequestKindCode, Sungero.Docflow.Constants.DocumentRegister.CounterpartyCodeRegex);
      #endregion
      
      // Совпадение в начале строки.
      var numberTemplate = string.Format("^{0}", template);
      var match = Regex.Match(registrationNumber, numberTemplate);
      if (match.Success)
        return match;
      
      // Совпадение в конце строки.
      numberTemplate = string.Format("{0}{1}$", additionalPrefix, template);
      return Regex.Match(registrationNumber, numberTemplate);
    }

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

8) Осталось чуть чуть - автонумерация.
Открываем событие "До сохранения" ТД "Заявки" (или Вашего ТД) и добавляем код.

    public override void BeforeSave(Sungero.Domain.BeforeSaveEventArgs e)
    {
      base.BeforeSave(e);
      
      var prefixName = Sungero.Docflow.Constants.OfficialDocument.RegistrationNumberPrefix;
      if (e.Params.Contains(prefixName))
      {
        var prefix = string.Empty;
        e.Params.TryGetValue(prefixName, out prefix);
        
        prefix = CommonOverlap.PublicFunctions.DocumentRegister.ChangeCustomElementNumber(_obj, prefix);
        e.Params.AddOrUpdate(prefixName, prefix);
      }
      
      var postfixName = Sungero.Docflow.Constants.OfficialDocument.RegistrationNumberPostfix;
      if (e.Params.Contains(postfixName))
      {
        var postfix = string.Empty;
        e.Params.TryGetValue(postfixName, out postfix);
        
        postfix = CommonOverlap.PublicFunctions.DocumentRegister.ChangeCustomElementNumber(_obj, postfix);
        e.Params.AddOrUpdate(postfixName, postfix);
      }
    }

Если посмотреть на ветку начинающуюся с 2-1 в схеме прохождения функций, то видно, что при автонумерации в OfficialDocument сразу вызывается функция GenerateRegistrationNumberPrefixAndPostfix() в обход GetNextNumber(), в которой мы подставляли нужное значение элемента в номер.

Да, знаю, я это уже говорил.

При этом, если проанализировать код события "До сохранения", то можно увидеть, что полученные элементы номера записываются в параметры объекта с наименованиями registrationNumberPrefix и registrationNumberPostfix. В данном случае наша задача их получить и заменить в них наш [Код заявки] на нужное значение.

Если хотите убрать дублирование, то можно вынести код в отдельную функцию. Мне честно стало лень )))

Итог

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

Если я где-то накосячил, или у вас есть варианты реализации попроще, прошу в комментарии. 

Всем добра!

Прикреплен файл: Схема работы механизма регистрации.vsdx
 

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

Сергей Беляков

Екатерина, Нет, там в функции  GenerateRegistrationNumberPrefixAndPostfix() идет перебор элементов и каждый ставиться на свое место, поэтому новый элемент можно ставить куда угодно, будет работать так же, как и базовая логика.

Дмитрий Панкрашов

Сделать проще конечно можно - для этого нужно на событии "До сохранения" просто подставлять\менять номер безо всяких там журналов регистрации.

Но у вас, как я понял, новый элемент не является разрезом нумерации?

Сергей Беляков

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

Елена Попова

Интересное решение с подменой ресурса RequestKindCode . Я правильно понимаю, что за счет использования этого ресурса в GenerateRegistrationNumberPrefixAndPostfix, решается и вопрос корректного формированиям примера номера в функции GetValueExample?

Сергей Беляков

Елена, Да, верно. GetValueExample() вызывает GenerateRegistrationNumber(), которая в свою очередь вызывает GenerateRegistrationNumberPrefixAndPostfix(), который мы перекрыли, поэтому в шаблон подставится [Код заявки], так же, как это происходит с [Код контрагента].


 

Елена Попова

Сергей, если столкнусь на проекте еще раз, попробую ваше решение, спасибо что поделились наработкой! 

Сергей Беляков

Елена, Да не за что )))

Будет время опубликую обновление решения для разработчиков. 

Там полезного куда больше, на мой взгляд.

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