Глава 5. СОЗДАЕМ ОКОННЫЙ ИНТЕРФЕЙС «ПОСТУПЛЕНИЯ НА СКЛАД»
Создаем SQL-запросы Insert, Update и Delete для «позиций» документа
Вызовем DataSet Editor через контекстное меню компонента qryDetails.
Нажмем кнопку Get Table Fields для того, чтобы выделить
все поля. Затем нажмем кнопку Select Primary Keys для
того, чтобы выделить ключевое поле N. Теперь нажмем
кнопку Generate SQL. Нажмем кнопку OK.
Скопируем текст запроса из свойства SelectSQL в RefreshSQL, удалив
из него секцию order by и условие SI.ID
=:ID и добавив в условие SI.N =
:N.
Текст запроса RefreshSQL должен выглядеть так:
select
SI.ID,
SI.N,
SI.GOODS,
O.SHORT_NAME ITEM_NAME,
SI.QUANTITY,
SI.PRICE_L,
SI.PRICE_L_WO_VAT,
SI.PRICE_R,
SI.PRICE_R_WO_VAT,
SI.AMOUNT_L,
SI.AMOUNT_R,
SI.AMOUNT_S
from
STOCK_IN_ITEM SI,
OBJECT_NAMES O
where
SI.GOODS = O.OBJECT_ID and
SI.N = :N
Установим у компонента qryDetails также значение свойства GeneratorField. В
качестве генератора будем использовать генератор STOCK_IN_ITEM
_N_GEN.
Затем, откроем в Инспекторе объектов закладку «События»
и назначим компоненту qryDetail на события AfterPost имеющийся уже обработчик
qryMasterAfterPost, выбрав его из выпадающего списка. Это мы
сделали для того, чтобы после любого изменения в позициях
вызывался один и тот же обработчик, устанавливающий нашу переменную
Modified в значение True.
Сохраним проект. Запустим его, предварительно установив в переменных
контекста наш первый документ, у которого имелась позиция.
Пока не будем добавлять новые позиции. Изменим количество или
цену у той, что есть. С помощью клавиши
«стрелка вниз» уйдем с имеющейся строки, чтобы
подтвердить редактирование. Мы видим, что теперь позиции редактируются
!
Нам осталось разобраться с добавлением новых позиций и расчетом всех
цен и сумм.
Но прежде доработаем немного окно StockInForm, добавив в него
еще несколько управляющих элементов.
Увеличим высоту верхней панели до 112 пикселей. Для этого
выберем компонент TopPanel и в Инспекторе объектов установим свойство Height
= 112.
Разместим на ней компонент DBNavigator (палитра DataControls) и
назначим ему свойства:
DataSource = dsrDetail
Flat = True
Left = 8
Top = 80
ShowHint = True
Навигатор поможет управлять набором данных в подчиненной таблице.
Заодно добавим компонент ToolBar (палитра Win32) и назначим
ему свойства:
Align = alNone
ButtonHeight = 25
ButtonWidth = 25
EdgeBorders = [ebLeft, ebBottom, ebRight, ebTop
]
Images = ImageList1
Flat = True
Left = 256
Top = 78
Width = 80
ShowHint = True
Этот компонент представляет собой панель инструментов. Создадим на ней
три кнопки. Для этого нужно использовать пункт Новая кнопка
контекстного меню компонента ToolBar1:
В инспекторе объектов назначим каждой кнопке свою команду из имеющегося
у нас списка:
actHeader
actSave
actReport
Переместим все надписи на панели влево, запустим проект (
F9):
Оформление верхней панели теперь не должно вызывать нареканий. Пользователь
имеет все необходимые для работы кнопки.
Попробуем добавить запись в набор, нажав кнопку с плюсиком
или клавишу Insert. Отменим добавление клавишей Escape. Мы
видим, что новая запись вставляется перед уже имеющейся.
Так работает метод Insert компонента типа TIBDataSet. Это явно
собьет с толку пользователя, привыкшего набирать приходные документы сверху
вниз, позицию за позицией. Для того чтобы преодолеть
это неприятное явление в документе «Поступление на склад»
мы отменим добавление Insert и вызовем метод Append, который
всегда добавляет запись в конец набора, в отличие от
метода Insert.
Для этого создадим для компонента qryDetail обработчик AfterInsert и впишем
в него такой текст:
var
lock_insert: boolean;
procedure TStockInForm.qryDetailAfterInsert(DataSet: TDataSet);
begin
if not lock_insert then
begin
lock_insert := True;
qryDetail.Cancel; //отменяем режим вставки
qryDetail.Append; //вставляем в конец набора
end;
lock_insert := False;
end;
Логическая переменная lock_insert используется нами для того,
чтобы заблокировать рекурсивное повторное исполнение кода, так как после
вызова метода Append снова произойдет событие AfterInsert , которое вновь
вызвало бы Append и так до бесконечности…
Запустим проект (F9). Попробуем добавить запись. Теперь
запись добавляется в конец набора.
Хорошо бы еще иметь нумерацию строк набора, так как
пользователь наверняка проверяет правильность ввода по количеству введенных строк…
Для того чтобы это реализовать, используем вычисляемое поле.
Дважды щелкнем на компоненте qryDetail. Появится редактор полей.
Нажмем кнопку с плюсиком, чтобы добавить новое поле.
Дадим ему имя NUM и выберем тип Integer. Нажмем
OK.
Теперь в инспекторе объектов придадим ему свойства:
FieldKind = fkCalculated
Alignment = taCenter
Index = 2
DisplayLabel = №
DisplayWidth = 5
Мы создали вычисляемое поле. Для того чтобы вычисляемые поля
получали значения, необходимо присваивать им их в событии OnCalcFieldst
. Создадим обработчик этого события у компонента qryDetail и впишем
в него такой код:
procedure TStockInForm.qryDetailCalcFields(DataSet: TDataSet);
begin
with DataSet do
FieldByName('NUM').AsInteger := RecNo;
end;
Для того чтобы после удаления не возникало «дырки»
в нумерации, переоткроем запрос после удаления позиции. Для
этого создадим у компонента qryDetail обработчик события AfterDelete и впишем
в него такой код:
procedure TStockInForm.qryDetailAfterDelete(DataSet: TDataSet);
begin
Modified := True;
DataSet.Close;
DataSet.Open;
end;
Теперь создадим «постоянные» (Persistent) колонки у
сетки dbgDetail. Если не созданы «постоянные» колонки
у сетки, то она автоматически создает «временные»,
как только в источнике данных оказывается открытый набор, что
пока у нас и происходит. Существует ряд плюсов в
том, чтобы создать «постоянные» колонки. Например
, тогда легко можно настроить их некоторые свойства в Инспекторе
объектов.
Для того чтобы создать «постоянные» колонки у сетки
dbgDetail, нужно выбрать ее и в Инспекторе объектов дважы
щелкнуть на свойстве Columns. Появится редактор колонок, в
котором следует добавить колонки и привязать их к полям в
источнике данных. Проще всего воспользоваться пунктом контекстного меню редактора
Добавить все колонки, а затем удалить ненужные:
Удалим все колонки кроме NUM, ITEM_NAME,
QUANTITY, PRICE_L, PRICE_L_
WO_VAT, AMOUNT_L, AMOUNT_
S:
Выберем в списке колонку NUM и в Инспекторе объектов дважды
щелкнем на его свойства Color. Установим цвет для этой
колонки. Теперь выберем в списке колонку ITEM_NAME
и установим свойство :
ButtonStyle = cbsEllipsis
Закроем редактор колонок. Раскроем в Инспекторе объектов подпункты свойства
Options сетки. Установим птичку dgAlwaysShowEditor (для того,
чтобы по ячейкам сетки можно было перемещаться клавишами со стрелками
и при этом сразу оказываться в режиме редактирования ячейки).
Снимем птичку с dgCancelOnExit, чтобы при потере сеткой фокуса
ввода набор данных оставался в режиме редактирования.
Теперь запустим проект (F9). Заметим, что первая
колонка окрашена, а во второй колонке, при попадании
экранного курсора в нее, высвечивается кнопка с многоточием.
Это и есть стиль кнопки cbsEllipsis. Для того чтобы
по нажатию кнопки что-то происходило, нужно для
сетки создать обработчик события OnEditButtonClick. Вернемся в режим дизайна
и добавим на форму компонент диалога справочника RefDialog с палитры
Allegro и установим его свойства:
Transaction = traCurrent
ClassTableName = GOODS
Title = Выберите товар
Options = [doValueRequired]
Выберем компонент сетки dbgDetail , создадим обработчик события OnEditButtonClick,
и впишем в него такой код:
procedure TStockInForm.dbgDetailEditButtonClick(Sender: TObject);
begin
with RefDialog1 do
if Execute then //вызываем диалог на экран и проверяем,
//был ли выбран товар
begin
qryDetail.Edit; //переводим набор данных в режим редактирования
{присваиваем значение ID товара полю GOODS набора}
qryDetail.FieldByName('GOODS').AsInteger := Object_ID;
{присваиваем значение краткое наименование товара полю ITEM_NAME набора}
qryDetail.FieldByName('ITEM_NAME').AsString:=
NameOfRefObject(Object_ID, 'SHORT_NAME');
{перемещаем фокус ввода в сетке в поле "Количество"}
dbgDetail.SelectedField := qryDetail.FieldByName('QUANTITY');
end;
end;
У компонента RefDialog1 метод Execute вызывет диалог на экран.
Если пользователь нажал в диалоге кнопку Выбрать, то метод
Exceute возвращает True и в свойстве Object_ID компонента
оказывается значение, равное ID объекта справочника. Функция NameOfRefObject
по ID запрашивает наименование объекта определенного типа, в данном
случае, краткое наименование.
Ели пользователь закрыл диалог справочника кнопкой Отмена, метод Execute
возвращает False.
Запустим проект еще раз. Попробуем нажать на кнопку с
многоточием или дважды щелкнуть мышью в колонке «Наименование».
Перед нами появится диалог справочника, из которого можно выбрать
товар. Мы имеем возможность, если нужно добавить в
справочник новый товар, упорядочить справочник по любой колонке,
щелкнув на ее заголовке, включить фильтрацию, одним словом
, нам доступны все функции справочной системы.
Выберем какой-нибудь товар в сетке справочника и нажмем
кнопку Выбрать.
Мы видим, что наименование выбранного нами товара появилось в
текущей строке документа.
Вернемся в режим дизайна.
Нам осталось реализовать вычисление полей сумм и еще некоторые мелочи
.
Выберем компонент запроса qryDetail и дважды щелкнем на нем.
В редакторе полей выберем поле ID и в Инспекторе объектов
уберем птичку со свойства Required, чтобы оно не требовало
ввода значений. У полей ITEM_NAME, QUANTITY
, PRICE, PRICE_L_WO_VAT
, наоборот, установим птичку к Required. Эти поля
будут требовать ввода значений. Закроем редактор полей.
Выберем компонент dsrDetail, связывающий компонент запроса и сетку.
Создадим для него обработчик события OnDataChange и впишем в него
такой код:
procedure TStockInForm.dsrDetailDataChange(Sender: TObject; Field: TField);
begin
if (Field <> nil) and
((Field.FieldName = 'PRICE_L') or
(Field.FieldName = 'PRICE_L_WO_VAT') or
(Field.FieldName = 'QUANTITY'))then
with qryDetail do
begin
{Если режим расчета сумм на основе цен с НДС
и изменено поле "Цена с НДС" или "Количество"}
if ((Field.FieldName = 'PRICE_L') or (Field.FieldName = 'QUANTITY'))
and (qryMaster.FieldByName('CALC_MODE').AsInteger = 1) then
begin
FieldByName('AMOUNT_L').AsCurrency :=
FieldByName('PRICE_L').AsCurrency *
FieldByName('QUANTITY').AsInteger;
FieldByName('PRICE_L_WO_VAT').AsCurrency :=
round(FieldByName('PRICE_L').AsCurrency * 1000 * 100/
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
FieldByName('PRICE_R').AsCurrency :=
FieldByName('PRICE_L').AsCurrency *
qryMaster.FieldByName('EXCH_RATE_L').AsCurrency;
FieldByName('PRICE_R_WO_VAT').AsCurrency :=
round(FieldByName('PRICE_R').AsCurrency * 1000 * 100/
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
FieldByName('AMOUNT_R').AsCurrency :=
round(FieldByName('PRICE_R').AsCurrency *
FieldByName('QUANTITY').AsInteger);
end
else
{Если режим расчета сумм на основе цен без НДС и
изменено поле "Цена без НДС" или "Количество"}
if ((Field.FieldName = 'PRICE_L_WO_VAT') or (Field.FieldName = 'QUANTITY'))
and (qryMaster.FieldByName('CALC_MODE').AsInteger = 0) then
begin
FieldByName('AMOUNT_L').AsCurrency :=
round(FieldByName('PRICE_L_WO_VAT').AsCurrency * 10 *
FieldByName('QUANTITY').AsInteger *
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
FieldByName('PRICE_L').AsCurrency :=
round(FieldByName('PRICE_L_WO_VAT').AsCurrency * 10 *
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
FieldByName('PRICE_R_WO_VAT').AsCurrency :=
FieldByName('PRICE_L_WO_VAT').AsCurrency *
qryMaster.FieldByName('EXCH_RATE_L').AsCurrency;;
FieldByName('PRICE_R').AsCurrency :=
round(FieldByName('PRICE_R_WO_VAT').AsCurrency * 10 *
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
FieldByName('AMOUNT_R').AsCurrency :=
round(FieldByName('PRICE_R_WO_VAT').AsCurrency * 10 *
FieldByName('QUANTITY').AsInteger *
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/1000;
end;
{Расчет суммы в долларах}
FieldByName('AMOUNT_S').AsCurrency :=
FieldByName('AMOUNT_L').AsCurrency *
qryMaster.FieldByName('EXCH_RATE_L').AsCurrency/
qryMaster.FieldByName('EXCH_RATE_S').Ascurrency;
end;
end;
Событие OnDataChange происходит после редактирования любого поля. При этом
само поле передается в качестве параметра Field в обработчик события
и мы можем проанализировать, какое именно поле было изменено
. В зависимости от того, какой режим расчетов установлен
в шапке документа (от цен с НДС или от
цен без НДС) и того, какое поле было
изменено («цена с НДС» или «Цена без
НДС») происходят разные расчеты с присвоением новых значений остальным
полям. С целью округления до 3 десятичных знаков мы
умножаем некоторые величины на 1000, округляем их до целого
функцией round и затем делим на 1000.
Для того чтобы поле ID не было пустым, создадим
еще у компонента qryDetail обработчик BeforePost:
procedure TStockInForm.qryDetailBeforePost(DataSet: TDataSet);
begin
qryDetail.FieldByName('ID').AsInteger := qryMaster.FieldByName('ID').AsInteger;
end;
Событие BeforePost происходит непосредственно перед тем, как данные будут
посланы на сервер SQL-командой INSERT или UPDATE.
Поле ID является связующим между Главной таблицей документа и подчиненной
, то есть в пределах одного документа значение ID одинаково
и в «шапке» и в «позициях».
Запустим проект. Установим в шапке значение «Расчет сумм
от цены c НДС» и попробуем поменять «Цену
» или «Количество». Мы видим, что вторая
цена и все суммы рассчитываются автоматически. Мы можем также
добавлять новые позиции в документ. Добавим несколько позиций.
Для каждой из них необходимо, как минимум, выбрать
товар из справочника и указать цену и количество.
Теперь сделаем так, чтобы, в зависимости от выбранного
режима (расчет от цен с НДС или цен без
НДС), вторая цена была недоступна для ручного редактирования.
Лучше вообще сделать ее невидимой. Заодно запретим ручное редактирование
полей сумм и отметим их иным цветом.
Для того чтобы запретить редактирование полей сумм в сетке,
вызовем редактор колонок сетки (свойство Columns в Инспекторе объектов
), выберем колонки AMOUNT_L и AMOUNT_R
, окрасим их в другой цвет (свойство Color)
и установим свойство ReadOnly = True.
Выберем компонент dsrMaster и создадим для него обработчик события OnDataChange
:
procedure TStockInForm.dsrMasterDataChange(Sender: TObject; Field: TField);
begin
{устанавливаем видимость колонок цены, в зависимости от режима расчета сумм}
dbgDetail.Columns[3].Visible := qryMaster.FieldByName('CALC_MODE').AsInteger=1;
dbgDetail.Columns[4].Visible := not dbgDetail.Columns[3].Visible;
end;
Теперь, в зависимости от режима расчета сумм, в
сетке будет отображаться только нужная цена.
Запустим проект и добавим несколько позиций товара. Проверим,
как все работает. Например, попробуем сохранить строку,
в которой какое-то из обязательных полей оставлено пустым
. Программа должна сообщить нам о том, что требуется
значение в этом поле.
Собственно, интерфейс ввода данных для документа «Поступление на
склад» можно было бы считать готовым. Но мы
хотим еще показать, как реализовывается «поиск по нажатию
клавиш», значительно ускоряющий поиск товаров и, как результат
- ввод данных в документы.
|