Глава 9. СОЗДАЕМ ОКОННЫЙ ИНТЕРФЕЙС ДОКУМЕНТА «ПРОДАЖА»
Создаем оконный интерфейс документа «Продажа» путем копирования
Мы создадим проект оконного интерфейса, взяв за основу имеющийся
у нас проект «Поступления на склад».
Включим режим «Дизайнер».
Откроем проект stock_in_project.ipr и
сохраним проект под новым именем, используя пункт меню Файл
/Сохранить Проект как…
Назовем файл проекта sale_project.ipr
Теперь сохраним входящие в проект модули stock_in.
pas и stock_in_header.pas под
новыми именами: sale.pas и sale_header
.pas. Для этого выберем по очереди соответствующие закладки
в редакторе текста и вызовем пункт меню Файл/Сохранить
как…
Внимание! Переименовывать модули следует именно так, из под
среды дизайнера. При этом переименовывается не только файл,
но и имя модуля (unit) в его тексте
. Эти два имени должны совпадать. Поэтому не следует
пытаться переименовывать модули при помощи обычных средств работы с файлами
в операционной системе Windows.
Итак, теперь у нас имеется новый проект, полученный
путем «клонирования» старого. Но нам необходимо внести
еще ряд изменений, прежде чем пробовать его запустить.
Прежде всего следует заменить ссылки в секции uses раздела implementation
. В модуле sale в этой секции нужно заменить
uses stock_in_header;
на
uses sale_header;
Аналогично в модуле sale_header в этой секции нужно
заменить
uses stock_in;
на
uses sale;
Везде в модуле заменим упроминание главной таблицы STOCK_IN
на SALE.
Переименуем в Инспекторе объектов форму StockInForm в SaleForm и изменим
ее заголовок Caption на «Продажа», а форму StockInHeaderForm
в SaleHeaderForm. При помощи поиска Ctrl+R заменим
все упоминания формы StockInHeaderForm в тексте модуля sale на SaleHeaderForm
(таких упоминаний должно быть 2). Аналогично заменим все
упоминания формы StockInForm во втором модуле заменим на SaleForm.
На форме SaleForm удалим компонент DBText6 и Label6 с надписью
«Кредитуемый счет». В компоненте Label5 в свойстве Caption
заменим значение «Поставщик» на «Покупатель», а
в компоненте Label2 значение «Поступило» на «Отгружено
».
Сохраним все изменения. Теперь нам необходимо заменить тексты SQL
-запросов, чтобы компоненты qryMaster и qryDetail обращались не
к таблицам документа «Поступление на склад», а к
таблицам документа «Продажа». Для этого в свойстве SelectSQL
компонента qryMaster вместо старого текста введем новый:
select
S.ID,
S.DIR_ID,
S.DOC_DATE,
S.DOC_NO,
S.ENTRY_DATE,
S.HAS_ENTRY,
S.STOCK_ACC,
A1.NAME STOCK_ACC_NAME,
S.LAYER_ID,
L.SHORT_NAME LAYER_NAME,
S.VAT_RATE,
S.CONTRAGENT,
O.SHORT_NAME CONTRAGENT_NAME,
S.CALC_MODE,
S.EXCH_RATE_L,
S.EXCH_RATE_S,
S.TOTAL_L,
S.TOTAL_R,
S.TOTAL_S
S.PRICE_TYPE,
S.PRICE_DISCOUNT
from
SALE S,
OBJECT_NAMES O,
LAYER L,
ACC A1
where
S.CONTRAGENT = O.OBJECT_ID and
S.LAYER_ID = L.LAYER_ID and
S.STOCK_ACC = A1.ACC_ID and
S.ID = :ID
Удалим также тексты запросов из свойств InsertSQL, ModifySLQ,
RefreshSQL.
Дважды щелкнем на компоненте qryMaster и в редакторе полей удалим
поля DOC_KIND, ACC_ID и CREDIT
_ACC_NAME.
С помощью контекстного меню компонента qryMaster вызовем DataSet Editor.
Выберем все поля таблицы SALE с помощью кнопки Get Table
Fields. В списке Key Fields выберем ключевое поле ID
, а в списке Update Fields все имеющиеся поля и
нажмем кнопку Generate SQL.
Будут автоматически сформированы SQL-запросы InsertSQL, ModifySQL,
DeleteSQL и RefreshSQL. Сохраним изменения кнопкой OK.
Удалим текст запроса в свойстве DeleteSQL. А текст из
SelectSQL перепишем в RefreshSQL.
Теперь займемся компонентом qryDetail. Заменим в свойстве SelectSQL в
тексте запроса лишь имя таблицы STOK_IN_ITEM
на SALE_ITEM. В остальном запрос оставим тем
же:
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
SALE_ITEM SI,
OBJECT_NAMES O
where
SI.GOODS = O.OBJECT_ID and
SI.ID = :ID
order by SI.N
Ту же замену имени таблицы с STOK_IN_
ITEM на SALE_ITEM предпримем в текстах SQL-
команд в свойствах InsertSQL, ModifySQL, DeleteSQL, RefreshSQL
.
Заменим генератор в свойстве GeneratorField со значения STOCK_IN
_ITEM_N_GEN на SALE_ITEM
_N_GEN.
В тексте модуля sale удалим команды:
qryMaster.FieldByName('DOC_KIND').AsInteger := 0;
qryMaster.FieldByName('ACC_ID').AsInteger := 1012; //Поставщики
Вместо них вставим команды:
qryMaster.FieldByName('PRICE_TYPE').AsInteger := 0;
qryMaster.FieldByName('PRICE_DISCOUNT').AsCurrency := 0;
Дважды щелкнем на компоненте qryMaster и в редакторе полей удалим
все прежние поля и добавим все поля заново с помощью
пункта контекстного меню «Добавить поля» редактора полей:
Назначим каждому полю русское название, присваивая его свойству DisplayLabel
в Инспекторе объектов.
Теперь перейдем к форме «шапки». Компоненты на этой
форме достались нам от формы «шапки» «Поступления
на склад». Некоторые компоненты мы оставим, а некоторые
– удалим.
Удалим с формы SaleHeaderForm все компоненты, предназначенные для редактирования
полей «Вид документа» и «Кредитуемый счет»,
так как этих полей в документе «Продажа» нет
. Изменим надписи в компонентах Label:
«Поставщик» на «Покупатель»
«Дата поступления на склад» на «Дата отгрузки
со склада»
Изменим свойство Caption компонента-птички cbHasEntry:
«Товар поступил на склад» на «Товар отгружен
»
Добавим компонент Label с палитры Standard, назначив ему Caption
«Скидка в цене».
Добавим компонент DBEdit с палитры DataControls и назначим ему свойства
:
свойство |
Значение |
Name |
edPRICE_DISCOUNT |
DataSource |
SaleForm.dsrMaster |
DataField |
PRICE_DISCOUNT |
Добавим компонент DBRadioGroup с палитры Data Controls и назначим ему
свойства:
свойство |
Значение |
Name |
RgPRICE_TYPE |
DataSource |
SaleForm.dsrMaster |
DataField |
PRICE_TYPE |
Caption |
Использовать цены |
Items |
Оптовые
Розничные |
Values |
0
1 |
Расположим все оконные органы управления так, как показано на
рисунке:
Установим по вкусу порядок перехода фокуса в этом окне,
вызвав редактор свойства «Порядок перехода» (TabOrder)
компонентов с помощью контекстного меню компонента формы:
Сохраним все изменения. Установим контекст запуска проекта. Для
этого с помощью меню Запуск/Переменные контекста вызовем окно
«Переменные контекста»:
Выберем переменную RunContext.Documents[0].doc_
type_id (id типа документа). Нажав на
кнопку с многоточием , вызовем диалог выбора типа документа,
выберем в нем «Продажу» и нажмем кнопку OK
:
Выберем вторую переменную - RunContext.Documents[0].
doc_id (id документа). Нажав на кнопку
с многоточием , вызовем диалог выбора документа, выберем какой
-нибудь документ и нажмем кнопку OK:
Значения переменных контекста установлены. Нажмем кнопку OK, чтобы
сохранить их.
Запустим проект (F9).
Проверим, как работает редактирование шапки и позиций, как
происходит поиск по нажатию клавиш.
Для того чтобы завершить проект интерфейса «Продажа», нам
предстоит еще большая работа. Нам предстоит реализовать:
- Копирование цен из справочника товаров в документ
- Определение средней себестоимости товаров при сохранении документа, если товар
отгружен.
- Печать документов типа «Счета-фактуры» и «
Накладной»
Начнем с копирования цены при добавлении товаров из справочника.
Заказчик желает, чтобы скидка покупателю распространялась непосредственно на цену
товара. Следовательно, нам не только придется копировать цену
из справочника, но и изменять ее в соответствии со
скидкой.
У нас имеется два способа добавить товар в документ «
Продажа»:
- Найти товар «поиском по нажатию клавиш» и вставить
его из нижней сетки клавишей Enter.
- Нажать кнопку с многоточием в поле «Наименование» позиции
документа и в появившемся окне диалога «Выбрать из справочника
» найти нужный нам товар и нажать кнопку «Выбрать
»
Нам нужно сделать так, чтобы вне зависимости от способа
добавления товара цена, рассчитанная на основании цены в справочнике
, вставлялась вместе с добавляемым товаром в позицию документа.
Нам также представляется разумным, чтобы пользователь мог видеть справочные
цены товаров в нижней сетке, когда он ищет их
«поиском по нажатию клавиши». Поэтому мы несколько изменим
текст запроса, управляющего этим поиском, добавив в него
поля цен.
Итак, изменим текст запроса в свойстве SQL компонента qryGoods
на:
SELECT O1.OBJECT_ID, O1.SHORT_NAME, G.PRICE_W, G.PRICE_R
FROM GOODS G, OBJECT_NAMES O1
WHERE G.ID = O1.OBJECT_ID
Фактически мы просто добавили два поля в запрос.
Дважды щелкнем на компоненте qryGoods и в редакторе полей с
помощью контекстного меню вызовем пункт Добавить все поля…
Нажмем кнопку OK. Выбирая в редакторе полей поля,
назначим им свойства в Инспекторе объектов:
Поле |
DisplayLabel |
Alignment |
PRICE_W |
Опт.Цена |
taCenter |
PRICE_R |
Розн.Цена |
taCenter |
Закроем редактор полей. Выберем нижнюю сетку dbgGoods и вызовем
редактор колонок с помощью контекстного меню или дважды щелкнув в
Инспекторе объектов на свойстве Columns. Добавим все колонки в
редактор с помощью соответствующего пункта его контекстного меню и удалим
затем колонку OBJECT_ID.
Изменим также текст модуля в части формирования текста запроса «
по нажатию клавиш». Это происходит в обработчике события OnSetEditText
сетки dbgDetail:
{Конструирование запроса из строки признаков, разделенных пробелами}
s := 'SELECT O1.OBJECT_ID, O1.SHORT_NAME, G.PRICE_W, G.PRICE_R '#13+
'FROM GOODS G, OBJECT_NAMES O1'#13+
'WHERE G.ID = O1.OBJECT_ID AND O1.OBJECT_ID <> 0';
Запустим проект. Теперь при поиске «по нажатию клавиш
» в нижней сетке товары отображаются со своими ценами:
Осталось реализовать вставку нужной цены в документ при вставке товара
из нижней сетки в позицию документа. Мы напишем универсальную
процедуру, которая копирует цену в позицию документа. Эта
процедура должна рассчитать цену на основании справочной и учесть при
этом скидку, предоставляемую покупателю, а если требуется,
то и пересчитать ее по курсу в другую валюту.
Остановим проект. Добавим вызов процедуры, которая выделена жирным
шрифтом, в обработчик события OnDblClick нижней сетки:
{Копирование позиции из нижнего списка товаров двойным щелчком мыши}
procedure TSaleForm.dbgGoodsDblClick(Sender: TObject);
begin
qryDetail.Edit; {Присвоение значений полям позиции}
qryDetail.FieldByName('GOODS').AsInteger :=
qryGoods.FieldByName('OBJECT_ID').AsInteger;
qryDetail.FieldByName('ITEM_NAME').AsString :=
qryGoods.FieldByName('SHORT_NAME').AsString;
UpdatePrice(qryGoods);
LastValue := qryDetail.FieldByName('ITEM_NAME').AsString;
dbgDetail.SelectedField := qryDetail.FieldByName('QUANTITY');
dbgDetail.SetFocus; //Возвращение фокуса ввода в сетку позиций
end;
Добавим в секцию private класса формы TSaleForm объявление процедуры (
выделено жирным):
private
{ Private declarations }
procedure UpdatePrice(PriceQuery: TDataSet);
public
{ Public declarations }
end;
Добавим в раздел implementation модуля sale реализацию этой процедуры:
{Вставка цены}
procedure TSaleForm.UpdatePrice(PriceQuery: TDataSet);
var
price_s, price_l: Extended;
begin
{В зависимости от типа используемой цены}
if qryMaster.FieldByName('PRICE_TYPE').AsInteger = 1 then
price_s := PriceQuery.FieldByName('PRICE_R').AsCurrency
else
price_s := PriceQuery.FieldByName('PRICE_W').AsCurrency;
{Применение скидки и пересчет в валюту документа}
price_l := price_s *
(1 - qryMaster.FieldByName('PRICE_DISCOUNT').AsCurrency/100)*
qryMaster.FieldByName('EXCH_RATE_S').AsCurrency/
qryMaster.FieldByName('EXCH_RATE_L').Ascurrency;
{В зависимости от режима расчета (от цен с НДС или без НДС)
происходит окгругление}
if qryMaster.FieldByName('CALC_MODE').AsInteger = 1 then
qryDetail.FieldByName('PRICE_L').AsCurrency :=
round(price_l * 100)/100
else
qryDetail.FieldByName('PRICE_L_WO_VAT').AsCurrency :=
round(price_l * 10000/
(qryMaster.FieldByName('VAT_RATE').AsCurrency + 100))/100;
end;
Согласно договоренности с заказчиком, в справочнике товаров хранятся цены
с НДС. Поэтому способ расчета сумм CACL_MODE
(от цены с НДС или от цены без НДС
) в документе «Продажа» имеет несколько иное значение
, чем в документе «Поступление на склад». Если
в документе «Продажа» выбран режим CALC_MODE
= 0 (расчет от цен без НДС), то
берется за основу справочная цена с НДС, из нее
вычитается НДС, затем цена округляется до двух знаков после
запятой и вставляется в документ. После таких преобразований цена
с НДС в документе может уже не совпадать со справочной
из-за ошибок округления. Если же выбран режим
CALC_MODE = 1 (режим по умолчанию),
то в документ копируются цены с НДС из справочника,
а цены без НДС вычисляются с точностью до 3 знаков
после запятой. В этом режиме цены с НДС в
документе соответствуют справочным.
Запустим проект и убедимся, что расчет цен работает при
разных валютах документа и в разных режимах «использовать цену
».
Нам осталось реализовать вызов процедуры UpdatePrice при втором способе добавления
товаров в документ – из окна диалога «Выбрать из
справочника», которое вызывается нажатием кнопки с многоточием в сетке
позиций. Добавим в событие OnEditButtonClick сетки dbgDetail текст,
выделенный жирным шрифтом:
procedure TSaleForm.dbgDetailEditButtonClick(Sender: TObject);
var
Q: TIBQuery;
begin
with RefDialog1 do
if Execute then //вызываем диалог на экран и проверяем, был ли выбран товар
begin
qryDetail.Edit; //переводим набор данных в режим редактирования
{присваиваем значение ID товара полю GOODS набора}
qryDetail.FieldByName('GOODS').AsInteger := Object_ID;
{создаем run-time компонент запроса и запрашиваем цену товара}
Q := TIBQuery.Create(nil);
try
Q.Transaction := self.traCurrent;
Q.SQL.Text := 'SELECT PRICE_W, PRICE_R FROM GOODS WHERE ID = :ID';
Q.ParamByName('ID').AsInteger := Object_ID;
Q.Open;
UpdatePrice(Q);
finally
Q.Free;
end;
{присваиваем значение краткое наименование товара полю ITEM_NAME набора}
qryDetail.FieldByName('ITEM_NAME').AsString:=
NameOfRefObject(Object_ID, 'SHORT_NAME');
{перемещаем фокус ввода в сетке в поле "Количество"}
dbgDetail.SelectedField := qryDetail.FieldByName('QUANTITY');
end;
end;
Запустим проект (F9) и убедимся, что при
выборе товара из справочника в диалоге справочная цена копируется в
позицию документа.
Нам еще нужно добиться того, чтобы скидка, предоставляемая
покупателю, копировалась из справочника в документ сразу после выбора
покупателя из справочника контрагентов. Обычно это происходит на стадии
заполнения шапки документа, когда пользователь создает новую «продажу
».
Для этого выберем на форме SaleHeaderForm компонент refCONTRAGENT и создадим
обработчик его события OnExecuted. В обработчик впишем такой текст
:
procedure TSaleHeaderForm.refCONTRAGENTExecuted(Sender: TObject);
var
Q: TIBQuery;
begin
{создаем run-time компонент запроса и запрашиваем скидку}
Q := TIBQuery.Create(nil);
try
Q.Transaction := SaleForm.traCurrent;
Q.SQL.Text := 'SELECT DISCOUNT FROM FIRM WHERE ID = :ID';
Q.ParamByName('ID').AsInteger := refCONTRAGENT.Object_ID;
Q.Open;
{Копируем скидку в шапку документа}
SaleForm.qryMaster.FieldByName('PRICE_DISCOUNT').AsCurrency :=
Q.Fields[0].AsCurrency;
finally
Q.Free;
end;
end;
Выйдем из режима дизайнера и вызовем окно «Метаданные».
Выберем тип документа «Продажа» и в «Дополнительных
свойствах документа» на закладке «Интерфейс» назначим проект
оконного интерфейса sale_project.ipr. Установим птичку
«создавать документ из проводника» и выберем запуск «
Множество экземпляров». Нажмем кнопку «Сохранить». Вызовем «
Проводник по документам» и убедимся, что теперь документы
продаж вызываются двойным щелчком на экран. Попытаемся создать новый
документ «Продажа» с помощью соответствующего пункта контекстного меню
«Проводника по документам»:
Можно заметить, что присутствует еще ряд шероховатостей. Например
, так и не решен вопрос об автоматической нумерации документов
типа «Продажа». На главной форме SaleForm никак не
отображается информация о том, какие цены используются в документе
, оптовые или розничные. Попробуйте самостоятельно реализовать отображение информации
об использующихся ценах на форме SaleForm. Попробуйте также самостоятельно
реализовать автоматическую нумерацию документов. Разные компании используют разные способы
нумерации документов. Постарайтесь реализовать тот способ, который принят
в Вашей компании.
Хорошо бы еще при выборе валюты документа, вставлять в
него курсы валют, для того чтобы не набирать их
каждый раз вручную. Для этого можно было бы обратиться
к системной таблице LAYER_RATES или системной хранимой процедуре
LAST_EXCHANGE_RATES, запрашивающей ближайшие системные курсы
на какую-то дату. Здесь возможны несколько подходов
. Дата документа и дата отгрузки могут не совпадать между
собой, документ может выписываться заранее и курс использоваться договорной
. В любом случае, универсального решения здесь, видимо
нет. Попробуйте найти свое решение проблемы и реализовать его
.
|