Нумератор документов

Материал из Бронетанковой Энциклопедии — armor.kiev.ua/wiki
Перейти к: навигация, поиск


Автор(ы): Чобиток Василий, 02.02.2010

Во многих системах документоборота, реестрах и т. п. используются различные схемы нумерации документов. Часто схема нумерации зависит от субъективного понимания удобства присвоения и использования номера конкретными исполнителями организации, которые организовывают её документооборот.

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

Не раз наблюдал попытки сделать «универсальный нумератор», который у разработчиков получался сложным с точки зрения реализации и часто очень трудным в использовании и применении.

Как-то раз вместо техзадания на такой «нумератор», на которое понадобилось бы порядка недели, я за пару дней набросал в Delphi 7 работающий прототип такого нумератора.

Недавно поднял исходники прототипа нумератора и за несколько вечеров в Delphi 2009 довёл их до готовности к использованию в любом проекте, в котором необходимо присваивать каким-либо сущностям номера отличные от обычных чисел. Как оказалось, сделать «универсальный» нумератор не так уж и сложно.

Анализ проблемы

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

Например, номер «03-123/2010» может означать 123-й по счету документ номенклатуры с кодом 03 в 2010 году. Этот же номер с сокращенной записью года может иметь вид: «03-123/10».

Встречаются случаи, когда счетчик работает в рамках других периодов, например в течении дня. Такой номер «3-2010/01/31» может означать третий документ за 31 января 2010 года.

Отсюда можно сделать выводы:

  • заранее (при разработке программы) формат номера документа знать невозможно, должна быть возможность его настроить;
  • в номере могут использоваться различные составные части:
    • текстовые значения (константы, задаваемые при настройке);
    • даты и их составные части;
    • различные счетчики (триггеры), которых в основной программе может быть несколько;
    • значения атрибутов текущего документа.

Решение (проектирование)

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

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

Нумератор отвечает за:

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

Внешняя система отвечает за:

  • хранение настроенных форматов номеров в их привязке к тем сущностям (видам документов), для которых он настраивался;
  • передачу нумератору списков используемых в системе счетчиков и используемых для текущего вида документа полей;
  • передачу нумератору следующего за текущим значения поля или счетчика, который у системы запрошен нумератором.

Поскольку нумератор (класс TCounter, см. диаграмму ниже) заранее не знает о внешней системе, к которой он будет подключен, то вместе с ним в одном модуле определён интерфейс ICounter, определяющий методы взаимодействия с внешней системой. Кроме того, в этом же модуле определен абстрактный класс TCounterPart, инкапсулирующий от TCounter поведение различных типов составных частей номера, и наследники TCounterPart, реализующие поведение конкретных типов частей номера.

При подключении нумератора к внешней системе ExternalSystem задача разработчика сводится к тому, чтобы реализовать методы интерфейса ICounter, подключить в интерфейсе программы вызов диалога TdlgOptions настройки нумератора и организовать вызов нумератора в момент присвоения номера документу или другому объекту.

Подробнее см. на диаграмме классов нумератора.

Диаграмма классов нумератора

Для хранения и восстановление настроек нумератора принят следующий формат строки-шаблона для каждой части номера:

{<ТипКлассаЧастиНомера>:<Значение>}

Здесь <ТипКлассаЧастиНомера> может принимать значения имен классов наследников класса TCounterPart, а <Значение> — текущее значение настройки части номера. Например, в номере «Вх-12/09» первая часть номера «Вх-» является текстовой, поэтому её шаблон будет иметь вид:

{TCounterPartText:Вх-}

Вторая часть этого примера является порядковым номером, т.е. получается вызовом системного счетчика. Например, этот счетчик в системе носит имя «tgrInputNo», тогда шаблон настройки будет следующим:

{TCounterPartTrigger:tgrInputNo}

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

Таким образом, полная строка настройки для примера «Вх-12/09» должна иметь примерно такой вид («09» — текущий год в формате «ГГ»):

{TCounterPartText:Вх-}{TCounterPartTrigger:tgrInputNo}{TCounterPartText:/}{TCounterPartDate:ГГ}

Более того, значение «Вх-» определяет, что при классификации документа по направлению, он является входящим. Соответственно, для исходящих при такой настройке следовало бы осуществить отдельную настройку нумератора со значением «Исх-». Так как внешней системе классификация регистрируемого документа по направлению известна, то вместо двух настроек можно сделать одну путём доработки предыдущего примера, в котором часть {TCounterPartText:Вх-} будет заменена такой настройкой:

{TCounterPartField:directType}{TCounterPartText:-}

Где, directType — имя атрибута (свойства) внешней системы, отвечающего за классификацию текущего документа по направлению и возвращающего «Вх» для входящих документов и «Исх» для исходящих.

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

Реализация

Компонентная структура нумератора представлена на диаграмме.

Диаграмма компонентов (модулей) нумератора

Модуль Counter

Все классы нумератора, кроме формы его настройки, и интерфейс ICounters определены в модуле counter. Кроме стандартных модулей дополнительно используется библиотека RegExpr для работы с регулярными выражениями.

unit counter;

interface

uses
  Classes, StdCtrls, ExtCtrls, Controls, SysUtils, RegExpr, Dialogs;

...


Интерфейс ICounters объявлен следующим образом (назначение методов см. в комментариях):

ICounters = interface(IUnknown)
  ['{75533674-8A39-4501-97E7-0A8F9BB5625F}']
  // Возвращает список имеющихся во внешней системе счётчиков triggerName=Caption
  // triggerName - код (имя) счётчика в системе
  // Caption - описание счётчика
  procedure GetTriggers(aList: TStrings);

  // Возвращает следующее значение счётчика
  function TriggerNext(TriggerName: string): string;

  // Возвращает список системных полей SystemCode=Caption
  // SystemCode - код (имя) поля в системе
  // Caption - описание поля
  procedure GetSystemCodes(aList: TStrings);

  // Возвращает текущее значение поля в системе
  function SystemCode(CodeName: string): string;
end;

Класс TCounter интересен тем, что с одним объектом этого класса могут быть ассоциированы несколько объектов класса TCounterPart, при этом последние должны идти в строго определённом пользователем порядке. Опять же, в любой момент пользователь может добавлять, удалять, менять местами объекты TCounterPart.

Есть ли смысл реализовывать подобный функционал самостоятельно? Существует стандартный класс TStringList, в котором кроме списка строк могут задаваться соответствующие им объекты базового класса TObject. Воспользовавшись этими возможностями, определяем TCounter наследником класса TStringList.

TCounter кроме прочих задач решает задачу создания строки (шаблона) настройки нумератора (метод GetTemplate) для её хранения внешней системой в привязке к типу сущностей, для которых используется нумератор, а так же возможность восстановления объектной структуры нумератора при передаче ему строки настройки (метод ParceTemplate).

Класс TCounter объявлен следующим образом.

TCounter = class(TStringList)
private
  FCounterName: string;
  FICounters: ICounters;
public
  constructor Create(aName: string; I: ICounters; template: string = '');
  procedure AddPart(Part: TCounterPart);
  procedure Clear; override;
  function GetTemplate: string;         
  procedure ParceTemplate(template: string);
  function GetSample: string;
  function Generate: string;
  property CounterName: string read FCounterName write FCounterName;
  property CounterInterface: ICounters read FICounters;
end;

Опишем некоторые методы:

  • AddPart — добавление нового объекта класса TCounterPart;
  • CounterName — название нумератора для пользователя;
  • GetTemplate — возвращает строку-шаблон текущих настроек нумератора;
  • ParceTemplate — по заданной строке-шаблону восстанавливает объектную структуру и настройки нумератора;
  • Generate — генерирует и возвращает текущее значение номера;
  • GetSample — возвращает пример внешнего вида номера, который может получиться при использовании текущих настроек нумератора. Требуется для взаимодействия с формой настройки, в которой необходимо продемонстрировать пользователю пример внешнего вида номера, но это нельзя делать вызовом метода Generate, так как генерация номера может вызвать нежелательное при настройке срабатывание счётчиков.

Абстрактный класс TCounterPart объявляем наследником от TPersistent. Такое наследование необходимо для того, чтобы была возможность зарегистрировать эти классы с целью их последующего поиска по текстовому значению имени класса при создании объектов по текстовому шаблону.

TCounterPart = class abstract (TPersistent)
private
  FValue: string;
  FCounter: TCounter;
  function GetValue: string;
  procedure SetValue(const aValue: string);
public
  function GetControlType: TControlClass; virtual;
  function GetName: string; virtual;
  function GetTemplate: string;
  function GetSample: string; virtual;
  function Generate: string; virtual;
  procedure InitControlData(aControl: TControl); virtual;
  property Value: string read GetValue write SetValue;
end;

Методы и свойства имеют следующее назначение:

  • GetControlType — фабричный метод, возвращает тип элемента управления («контрола»), отвечающего за настройку объекта текущего класса. Например, для текстовой части номера класса TCounterPartText элементом управления будет объект ввода класса TEdit.
  • GetName — возвращает название (описание) элемента номера, отображаемое в интерфейсе программы пользователю, например, в форме настройки.
  • GetTemplate — возвращает строку-шаблон настройки текущего объекта части номера;
  • GetSample — возвращает пример, как может выглядеть текущая часть номера при генерации номера;
  • Generate — вызывает генерацию и возвращает значение части номера;
  • InitControlData — подготавливает к использованию в форме настройки (заполняет исходными данными) элемент управления, тип которого возвращает метод GetControlType;
  • Value — возвращает текущее строковое значение настройки части номера.

Следует акцентировать внимание на отличии метода Generate от свойства Value. Так, например, для текстовой части номера (класс TCounterPartText) с настройкой «Вх-», хранимой в переменной FValue, они оба будут возвращать значение «Вх-». Для класса TCounterPartDate свойство Value будет возвращать настройку формата выдачи текущей даты, например «ГГГГ», хранимое в переменной FValue, а метод Generate — значение даты, соответствующее этому формату, в данном случае — «2010».

Далее приведены объявления остальных классов этого модуля.

TCounterPartText = class(TCounterPart)
public
  function GetName: string; override;
  function GetControlType: TControlClass; override;
end;

TCounterPartDate = class(TCounterPart)
public
  function GetName: string; override;
  function GetControlType: TControlClass; override;
  function Generate: string; override;
  procedure InitControlData(aControl: TControl); override;
end;

// Переопределяем TComboBox для обеспечения его взаимодействия
// с классами TCounterPartTrigger и TCounterPartField. 
// Строки в списке полей и счетчиков имеют вид «Name=Caption», 
// а пользователю следует показывать только «Caption»
TFieldsComboBox = class(TComboBox)
private
  FFieldItems: TStrings;
  procedure SetFieldItems(const Value: TStrings);
  function GetFieldName: string;
public
  constructor Create(AOwner: TComponent); override;
  destructor Destroy; override;
  property FieldItems: TStrings read FFieldItems write SetFieldItems;
  property FieldName: string read GetFieldName;
end;

TCounterPartTrigger = class(TCounterPart)
public
  function GetName: string; override;
  function GetControlType: TControlClass; override;
  function Generate: string; override;
  procedure InitControlData(aControl: TControl); override;
  function GetSample: string; override;
end;

TCounterPartField = class(TCounterPart)
public
  function GetName: string; override;
  function GetControlType: TControlClass; override;
  function Generate: string; override;
  procedure InitControlData(aControl: TControl); override;
end;


const

  PartsClassArray : array [0..3] of TCounterPartClass = (
    TCounterPartText,
    TCounterPartDate,
    TCounterPartTrigger,
    TCounterPartField
  );

implementation

{**
 * Реализация условно не показана :-)
 *}

var
  PC: TCounterPartClass;

initialization

for PC in PartsClassArray do
  RegisterClass(PC);

end.

Особенности реализации класса TCounter

Приведем пример реализации некоторых методов класса TCounter.

Метод AddPart позволяет одним вызовом добавить объект части номера с присвоением элементу списка его названия и сообщить объекту Part о себе.

procedure TCounter.AddPart(Part: TCounterPart);
begin
  AddObject(Part.GetName, Part);
  Part.FCounter := Self;
end;

Каждый объект класса TCounterPart отвечает за генерацию собственной части номера. Поэтому генерировать номер целиком элементарно — вызываем последовательную генерацию каждой части номера и конкатенируем результат.

function TCounter.Generate: string;
var
  i: integer;
begin
  result := '';
  for i := 0 to Count - 1 do
    result := result + TCounterPart(Objects[i]).Generate;
end;

Методы GetSample и GetTemplate аналогичны.

Метод ParceTemplate реализован самым простым (как для меня) способом — строка шаблона разбирается с использованием регулярного выражения.

procedure TCounter.ParceTemplate(template: string);
var
  re: TRegExpr;
  part: TPersistent;
begin
  if trim(template) = '' then exit;

  re := TRegExpr.Create;
  try
    Clear;
    re.Expression := '\{([^:]+):([^}]*)\}';
    if re.Exec(template) then
    repeat
      part := FindClass(re.Match[1]).Create;
      TCounterPart(part).Value := re.Match[2];
      AddPart(TCounterPart(part));
    until (not re.ExecNext);
  finally
    re.Free;
  end;
end;

Модуль Options (форма настройки)

Задача формы настройки (класс TdlgOptions) состоит в том, чтобы предоставить пользователю экранный интерфейс настройки номера.

При создании формы её конструктору передаётся объект счётчика: constructor Create(AOwner: TComponent; ACounter: TCounter), ссылка на который хранится в переменной FExtCounter класса TdlgOptions.

В процессе настройки используется внутреннее свойство этого класса property Counter: TCounter. При подтверждении пользователем сделанных настроек, состояние объекта Counter копируется в объект FExtCounter, в противном случае FExtCounter остаётся неизменным.

Черновой вариант формы настройки в рабочем состоянии имеет вид, представленный на скриншотах:

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

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

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

CounterScreen4.png

В модуле main класс TForm1 этой формы объявлен следующим образом:

type
  TForm1 = class(TForm, ICounters)
    {**
     * Перечень элементов управления 
     * и методов обработки событий
     * условно не показан
     *}
  public
    // Ниже следуют методы, объявленные в ICounters
    procedure GetTriggers(aList: TStrings);
    function TriggerNext(TriggerName: string): string;
    procedure GetSystemCodes(aList: TStrings);
    function SystemCode(CodeName: string): string;
  end;

Важным здесь является то, что в этом классе реализуются методы интерфейса ICounters, объявленного в модуле Counter.

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

{**
 * ВНИМАНИЕ! Этот говнокод имитирует работу внешней системы.
 *
 * В реальной жизни не повторять!
 *}

procedure TForm1.GetTriggers(aList: TStrings);
begin
  aList.Add('trigger1=Счетчик 1');
  aList.Add('trigger2=Счетчик 2');
  aList.Add('trigger3=Счетчик 3');
end;

function TForm1.TriggerNext(TriggerName: string): string;

  function NextFromEdit(aEdit: TEdit): string;
  begin
    aEdit.Text := IntToStr(StrToInt(aEdit.Text) + 1);
    result := aEdit.Text;
  end;

begin
  if TriggerName = 'trigger1' then
    result := NextFromEdit(CounterEdit1);
  if TriggerName = 'trigger2' then
    result := NextFromEdit(CounterEdit2);
  if TriggerName = 'trigger3' then
    result := NextFromEdit(CounterEdit3);
end;

Скачать пример

  • counters.zip — скачать пример программы, использующей нумератор, в архиве.
Примечание: в демонстрационном примере хранение настроек нумераторов после выхода из программы не реализовано. Прежде чем вызывать генерацию номера командой Генерировать номер следует в списке «Генераторы» выбрать любой нумератор и настроить его командой Настройка.