четверг, 10 мая 2012 г.

Урок 43. Группировка затрат

Очевидно, что быстро получить картинку для оценки, например, всех коммунальных платежей за определенный период не сложно. Но будет ли информативна детализированная картинка?

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

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



Напомню, что при создании базы данных я заготовил пару таблиц:
ExpenseGroup

и ExpenseAcc


С первой таблицей - все просто: это - классический кодификатор (список, перечень) названий будущих групп. Ее можно сразу же предварительно заполнить для наглядности (мне в голову пришли вот такие группы, но у Вас могут быть иные):


Вторая таблица называется корреляционной. Чтобы понять, в чем ее суть, нужно вернуться к схеме данных:


На этом фрагменте схемы данных изображены три связанные между собой таблицы. Две из них - кодификаторы (Accounts и ExpenseGroup), содержащие перечень счетов и перечень названий групп.

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

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


Раскрыв в списке групп строку с ID=6 (Дом), видно, что в связанной с ней корреляционной таблице существует несколько строк, т.е. в эту группу включены счета с их ID: 36, 44, 24, 38 и 28. По таблице счетов видно, что все эти счета так или иначе связаны с расходами по содержанию квартиры (дома).

Обратите внимание, что эти счета отмечены в поле Groups таблицы Accounts. Это - важно.

Надеюсь, что идея ясна. Пора приступать к реализации интерфейса.

Общий алгоритм добавления формы в проект уже пройден несколько раз.

Добавьте в проект форму GroupsFrm вот такого вида:


Несколько слов о компонентах этой формы.
Компонент TADOTable, 



с которым через TDataSource


связана верхняя сетка:


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

Следующие компоненты: запрос TADOQuery, источник данных и Grid слева внизу связаны между собой аналогичным образом:


Grid справа внизу связана посредством своего DataSource с таблицей, расположенной на главной форме:


ActionList имеет одну строку, о которой тоже говорилось ранее:




Остались две кнопки со стрелками, которые и будут определять основную логику работы формы: выбрал справа счет, нажал кнопочку "<<", счет включился в выбранную в верхней табличке группу. Кнопка ">>" будет исключать счет из группы.

Теперь займемся кодом.
В главной форме припишите к соответствующему пункту меню обработчик события:

procedure TMainFrm.N_ExpGrClick(Sender: TObject);
begin
  Application.CreateForm(TGroupsFrm, GroupsFrm);
  GroupsFrm.ShowMoDal;
  GroupsFrm.Free;
end;


Весь следующий текст будет касаться модуля Groups формы GroupsFRM.

Среди переменных общего пользования, нужно объявить одну - для хранения значения фильтра:

var
  GroupsFrm: TGroupsFrm;
  LastFilter: String;            // Понадобится запоминать значение фильтра



Чтобы можно было обращаться к главной форме, использовать ее общие компоненты и переменные и к форме Accounts:

implementation
Uses Main
    , Accounts;

Далее - в привычном порядке, сначала об открытии и закрытии формы, а потом - по сути алгоритма.

Процедура создания формы:

procedure TGroupsFrm.FormCreate(Sender: TObject);
Var MyStr: String;
begin

  // Подготовка таблицы - кодификатора групп
  ADOTableGroups.Connection:=MainFrm.ADOConnection1;
  ADOTableGroups.Filter:='ID>1';       // Первая строка, в которой обычно пишется
  ADOTableGroups.Filtered:=True;       // что-то типа "Значение не задано"
                                       // отфильтровывается
  ADOTableGroups.Active:=True;

  // Наполняется значениями запрос  ADOQueryMembers
  ADOQueryMembers.Connection:=MainFrm.ADOConnection1;
  ADOQueryMembers.Active:=False;
  ADOQueryMembers.SQL.Clear;
  MyStr:='SELECT Accounts.Name, ExpenseAcc.IDGroup, ExpenseAcc.IDAcc, ExpenseAcc.ID ';
  MyStr:=MyStr + 'FROM Accounts INNER JOIN ExpenseAcc ON Accounts.ID = ExpenseAcc.IDAcc ';
  ADOQueryMembers.SQL.Add(MyStr);
  ADOQueryMembers.Active:=True;

  ADOCommand1.Connection:=MainFrm.ADOConnection1;

  // Запоминается фильтр, наложенный на таблицу ранее (другим интерфейсом)
  LastFilter:=  MainFrm.ADOTableAcc.Filter;

  // С помощью нового фильтра выбираются счета с признаком Analiz, относящиеся к определенной валюте
  // и не включенные в группу
  MainFrm.ADOTableAcc.Filter:='(Analiz=True) AND (Val='+IntToStr(Main.MySelect.MySel_IDVal)+') AND Groups=0';
  MainFrm.ADOTableAcc.Filtered:=True;

end;


и процедура закрытия формы:

procedure TGroupsFrm.FormClose(Sender: TObject; var Action: TCloseAction);
begin

  // При закрытии формы - вернуть таблице счетов прежний фильтр
  MainFrm.ADOTableAcc.Filter:=LastFilter;
  MainFrm.ADOTableAcc.Filtered:=True;
  Action:= caFree;
end;


Код для обработки единственного Action:

procedure TGroupsFrm.BottonsClicksExecute(Sender: TObject);
begin

  // Если таблица ADOTableGroups находится в режиме редактирования
  if ADOTableGroups.Modified
  then
    Try
      ADOTableGroups.Post;              // то - запись в источник
    finally

    end;

  // Если пользователь не завершил редактирование запроса
  if ADOQueryMembers.Modified
  then
    Try
      ADOQueryMembers.Post;            // то - запись в источник
    finally

    end;

  case ModalResult of                 // Обработка нажатия кнопок OK и Cancel
  1:
  begin
    MyClose;
  end;
  2:
  Begin
    MyClose;
  end;
  end;

end;


где процедура MyClose имеет следующее содержание:


procedure TGroupsFrm.MyClose();
Begin


  // Отключение таблицы
  ADOTableGroups.Active:=False;


  // Закрытие формы
  Close;


end;


Несколько вспомогательных процедур.
Объявите приватную процедуру MyRefresh и напишите ее в следующем виде:


procedure TGroupsFrm.MyRefresh();
begin


  // Чтобы избежать сбоя на пустой таблице
  if ADOTableGroups.FieldByName('ID').Value>0
  then
  begin
    ADOQueryMembers.Filter:='IDGroup='+IntToStr(ADOTableGroups.FieldByName('ID').Value);
    ADOQueryMembers.Filtered:=True;
  end;
end;

Она обновляет набор содержащихся в группе счетов. Набор должен обновляться, когда пользователь осуществляет скролинг - переход по записям в таблице Групп или, когда он закончил ее редактирование:

procedure TGroupsFrm.ADOTableGroupsAfterScroll(DataSet: TDataSet);
begin
  MyRefresh();
end;

procedure TGroupsFrm.ADOTableGroupsAfterPost(DataSet: TDataSet);
begin
  MyRefresh();
end;

Добавьте еще две Private процедуры, одна из которых будет добавлять счет в группу, а вторая - удалять:


procedure TGroupsFrm.MyAdd();
begin

  // Добавление счета в группу
  With ADOQueryMembers Do  // Работаем с запросом
  Begin
      Insert;              // Добавление новой записи
      Try

        // Присвоение значений полям
        FieldByName('IDGroup').Value:=ADOTableGroups.FieldByName('ID').Value;
        FieldByName('IDAcc').Value:=MainFrm.ADOTableAcc.FieldByName('ID').Value;

        // Запись в таблицу-источник
        Post;

        // Фиксирование признака в кодификаторе счетов
        MainFrm.ADOTableAcc.Edit;
        MainFrm.ADOTableAcc.FieldByName('Groups').Value:=True;
        MainFrm.ADOTableAcc.Post;

        // Обновление данных
        Requery();
      finally
        MainFrm.ADOTableAcc.Requery();
      end;
  end;
end;

Procedure TGroupsFrm.MyDelete();
begin
  Try

    // Найти счет и пометить его, что он больше не используется в группах
    ADOCommand1.CommandText:='UPDATE Accounts SET Accounts.Groups = False WHERE (((Accounts.ID)='+IntToStr(ADOQueryMembers.FieldByName('IDAcc').Value)+'))';
    ADOCommand1.Execute;

    // Обновить кодификатор счетов
    MainFrm.ADOTableAcc.Requery();

    // Удалить строку из корреляционной таблицы
    ADOCommand1.CommandText:='DELETE ExpenseAcc.ID FROM ExpenseAcc WHERE (((ExpenseAcc.ID)='+IntToStr(ADOQueryMembers.FieldByName('ID').Value)+')) ';
    ADOCommand1.Execute;

  finally
    ADOQueryMembers.Requery();
  end;
end;

Теперь осталось назначить обработчики событий нажатия кнопок со стрелками и для удобства - двойных кликов по соответствующим сеткам:

procedure TGroupsFrm.Button1Click(Sender: TObject);
begin
  MyAdd();
end;

procedure TGroupsFrm.DBGridEh3DblClick(Sender: TObject);
begin
  MyAdd();
end;

procedure TGroupsFrm.Button2Click(Sender: TObject);
begin
  MyDelete;
end;

procedure TGroupsFrm.DBGridEh2DblClick(Sender: TObject);
begin
  MyDelete();
end;

Для любителей пользоваться клавиатурой можно запрограммировать под аналогичные действия нажатия кнопок:

procedure TGroupsFrm.FormKeyDown(Sender: TObject; var Key: Word;
  Shift: TShiftState);
begin

  case Key of            // Start Case

    VK_INSERT:
      MyAdd();

    VK_DELETE:
      MyDelete();


    else

  End;                   // End case

end;


С точки зрения программирования, я не рассказал ничего нового, но... в следующем уроке я планирую переделать интерфейс данной формы для использования технологии Drag & Drop при формировании списка счетов, включаемых в группу затрат.


Комментариев нет:

Отправить комментарий