Как я писал собственную ERP систему и что из этого получилось

в 20:39, , рубрики: Delphi, ERP-системы, rad studio, архитектура приложений

Некоторое время назад ко мне обратился знакомый, хозяин одного из филиалов крупной сети по доставке суши и пицц. Их нынешняя ERP его вообще не устраивала, так как не было контроля за кухней и доставщиками, а так как кухонь по городу у него 5, то это была серьёзная проблема. Он предложил мне написать под него новую систему. Единственное условие – так как он во франшизе, и особо с головниками портить отношения не хотел, необходимо интегрировать с существующей системой максимально скрытно.

Первый вопрос, который у меня возник – это средство разработки. Можно было взять что-то новомодное типа NodeJS для сервера, и отдавать web-интерфейс клиентам – самый простой, но самый тормозной вариант (аналогичные решения требуют i3). Можно прописывать отдельно серверную часть, отдельно каждое приложение (windows/android/ios). Или NodeJS сервер, и C# (xamarin если точнее) под все клиенты единый код. Или взять последнюю Rad studio и написать на единой кодовой базе всё. Так как я понимал, что разный код, описывающий одни и те же вещи – это потенциальная проблема, и что я буду работать над проектом один, я выбрал последний вариант.

Второй вопрос – это архитектура. Можно по-быстрому набросать на форму компоненты, ручками или через LiveBindings всё связать, и быстро получить результат. Но любое небольшое изменение в каком-то алгоритме требует пересборки и обновления всех программ, а это большие затраты по времени на тестирование, чтобы в другом месте ничего не сломалось. Поэтому я принял решение использовать xml для описания всех таблиц и интерфейсов, а всю логику убрать в скрипты.

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

image

На скриншоте видно, что элемент связан с таблицей Orders, но отображает не её содержимое, а ссылку на customer, колонку FirstName (имя заказчика). Так же при нажатии будет вызван обработчик atApplyChanges, который применит все изменения на форме, и закроет её. Без единой строчки кода! Естественно, когда в таблице Orders что-то будет меняться, то вызовется скрипт, который проверит корректность изменения. Что-то типа:

Немного скрипта

CurOrderState := GetNewValueF('Orders','OrderState');
LastOrderState := GetOldValueF('Orders','OrderState');
IsCashbackOnCurier := GetNewValue('Orders','CashbackOnDeliverer') = 'True';
PayType := GetNewValueF('Orders','PayType');
OldPayType := GetOldValueF('Orders','PayType');
OrderTurn := GetNewValueF('Orders','Turn');
CurTurn := GetConstantValue('CurrentTurn');
OrderNo := GetNewValue('Orders','OrderNo');
OrderPrice := GetNewValueF('Orders','Price');

PaySum := GetNewValueF('Orders','PaySum');
LastPaySum := GetOldValueF('Orders','PaySum');

CurTurn := GetConstantValue('CurrentTurn');
CurCashbox := GetConstantValue('CurrentCashbox');

if (OrderTurn > 0) and (OrderTurn <> CurTurn) then
begin
   ShowMessage('Нельзя производить действия с заказом другой смены. Заказ №'+IntToStr(OrderNo));
   exit;
end;

if (LastOrderState = 9) then
begin
   ShowMessage('Нельзя производить действия с завершенным заказом');
   exit;
end;

if (LastOrderState = 8) then
begin
   ShowMessage('Нельзя производить действия с отменённым заказом');
   exit;
end;

if (LastOrderState <> 8) and (CurOrderState = 8) then
begin
   TransactionCanAccept;
   exit;
end;

if (CurOrderState > 2) and (CurTurn  < 1) then
begin
   ShowMessage('Смена не открыта. Действие не может быть совершено');
   exit;
end;

if (CurOrderState > 4) and (CurCashbox  < 1) and (CurOrderState <> 8) then
begin
   ShowMessage('Касса не открыта. Действие не может быть совершено');
   exit;
end;

//Запрет изменения сумм, так как есть кассовые доки.
if IsCashbackOnCurier then
begin
   if PaySum <> LastPaySum then
   begin
      ShowMessage('Уже нельзя менять суммы оплаты');
      exit;
   end;
   Price := GetNewValueF('Orders','Price');
   LastPrice := GetOldValueF('Orders','Price');
   if Price <> LastPrice then
   begin
      ShowMessage('Уже нельзя менять суммы оплаты');
      exit;
   end;
end;

// Формируем сдачу
if (CurOrderState >= 6) and (CurOrderState < 8) and (PayType = 1) and (not IsCashbackOnCurier) then
begin
   if (PaySum - OrderPrice) > 0 then
   begin
      ss := 'Выдача сдачи для заказа №'+IntToStr(OrderNo);
      CreateCashboxDoc(ss,1,OrderPrice - PaySum);
      IsCashbackOnCurier := True;
      SetValue('Orders','CashbackOnDeliverer',True);
   end;
end;

//Отмена сдачи для курьера по статусу
if (CurOrderState < 6) and IsCashbackOnCurier then
begin
   if (PaySum - OrderPrice) > 0 then
   begin
      CreateCashboxDoc('Отмена выдачи сдачи для заказа №'+IntToStr(OrderNo),1,PaySum - OrderPrice);
      IsCashbackOnCurier := False;
      SetValue('Orders','CashbackOnDeliverer',False);
   end;
end;

// Формируем возврат сдачи
if (CurOrderState >= 8) and IsCashbackOnCurier then
begin
   if (PaySum - OrderPrice) > 0 then
   begin
      ss := 'Возврат сдачи для заказа №'+IntToStr(OrderNo);
      CreateCashboxDoc(ss,1,PaySum - OrderPrice);
      IsCashbackOnCurier := False;
      SetValue('Orders','CashbackOnDeliverer',False);
   end;
end;

// Формируем приходник на сумму заказа
if (CurOrderState = 9) and (LastOrderState <> 9) and (PayType = 1) then
begin
   ss := 'Розничная выручка с заказа №'+IntToStr(OrderNo);
   CreateCashboxDoc(ss,2,OrderPrice);
end;

// Назначаем смену и филиал при переходе из статуса новый дальше
if (LastOrderState < 3) and (CurOrderState >= 3) then
begin
   if CurTurn <= 0 then
   begin
      ShowMessage('Смена не открыта. Движение заказа не возможно.');
      exit;
   end;
   SetValue('Orders','Turn',CurTurn);
   CurFilial := GetConstantValue('FilialID');
   SetValue('Orders','Filial',CurFilial);
end;

if (CurOrderState <= 2) and (LastOrderState > 2) then
begin
   SetValue('Orders','Turn',0);
end;
TransactionCanAccept;

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

Следующий момент, который безумно напрягает во всех нынешних решениях – это латентность. Любое действие приводит к select из базы. Т.е. время пинга+передачи пакета+select+gzip — это минимальное время, через которое у оператора что-то произойдёт. А когда праздники и мало того, что повышенная нагрузка на оператора да ещё и сервер начинает тормозить, вот тут начинаются реальные денежные потери. Что бы этого не происходило, я сделал кэширование всех нужных данных в ОЗУ, и выборку оттуда через запросы. Сервер напрягается лишь транзакциями update/insert, а потом изменённые и новые данные рассылает всем подписчикам.

С отображением информации тоже пришлось сделать интересные оптимизации. Так как у нас нет select из 10 таблиц, то как, например, из таблицы деталей доставки выяснить, куда везти заказ (у меня точка хранится в таблице домов ФИАС)? Примерно вот так:

     fids := CComponentBaseLink.ClassGetPrintValue('Orders_Delivery_Details','Order|Orders->CustomerAddress|Customer_address->FiasHouse|Fias_Houses->ID',orid);
      if not StrToInteger(fids,fid) then
         Continue;
      las := g_Base.GetTblValueByID('Fias_Houses','PointLatitude',fid);
      los := g_Base.GetTblValueByID('Fias_Houses','PointLongitude',fid);

Т.е. прошли через 3 промежуточные таблицы и сопоставили заказ с точкой на карте, что бы её нарисовать. Всё очень быстро, и без единого обращения на сервер. И нарисовали в браузере ручками через соответствующее api. То есть так как у гугла большие проблемы с картами в России “в замкадье”, на андроиде использую яндекс, у которого эти проблемы выражены гораздо меньше. Через api 1.1 даже кода разработчика получать не надо, что бы отобразить пункты назначения и пробки например.

Естественно, всё это надо было как-то сопрягать с (пока) основной бизнес-системой. Основная бизнес-система (не буду её называть) печатает на принтере заказы. Небольшое использование сниффера показало, что печатается pdf. Через пол дня была готова моя программа, которая полностью копировала протокол оригинальной (до байта, спасибо, что исходники компонент дельфи доступны и там можно легко поправить заголовок не заморачиваясь с raw socket), но помимо печати на принтере парсила эти pdf и вычитывала оттуда заказ, загружая его в базу. Но этот метод носил некоторые ограничения. Во-первых, выгружалась не вся нужная информация, а пару полей приходилось перезаполнять оператору, а во-вторых, это была лишь синхронизация по заказам. Например, рейсы доставки приходилось формировать и там и там. Поэтому был написан прокси, который анализировал всю проходящую через него информацию и эта проблема решилась.

Курьерское приложение.

image

Оно отсылает в фоне на сервер трек. То есть оператор может в реальном времени наблюдать за перемещением курьеров. Так же для подтверждения доставки заказа курьер должен находиться в некотором заданном радиусе от точки доставки. Но при этом может отсутствовать интернет. И тогда программа передаст информацию о том, что заказ доставлен, как только интернет появится. Естественно, можно из программы запустить яндекс-навигатор, который автоматически проложит путь до адреса; можно посмотреть все адреса на карте, или позвонить клиенту, нажав 2 кнопки, и так далее.

В общем, система получилась очень мощной, с большим количеством возможностей реализованных, и с ещё большим количеством заложенных. И… оказалась не нужна. В последний момент заказчик стал изменять изначальные требования: начал в приказном тоне говорить, чтобы я делал так-то и так-то, потому что иначе это работать не будет (к слову, это требования полного переписывания всего на другом ЯП, просто «чтобы было»)… На резонный вопрос “а ведь работает”, “работает быстрее, чем текущая система” и т.д. меня будто не слышал. На этом мы расстались. В общем, сейчас у меня есть интересная, почти готовая, ERP система. Там не хватает каких-то отчётов, хорошо бы допилить (если это будет востребовано) изначально заложенный функционал автоматического разброса заказов по кухням, в зависимости от пробок и текущей загрузки точек и т.д. Доработать это, а так же реализовать дополнительный функционал – совершенно не сложно. А до тех пор моя ERP система ищет нового заказчика, а я новую работу. :)

P.S. Многим, я думаю, будет интересно узнать какого вообще разрабатывать кросс-платформенные приложения на RAD Studio. Выскажу своё субъективное и предвзятое мнение: на последней студии, со всеми апдейтами работать можно. Есть некоторые проблемные компоненты типа TWebBrowser, у которого сложности с удалением себя под всеми OS. Или проблемы у компонент типа TGrid на тач устройствах. Но это всё решаемо. Так же есть большая проблема, что установленный apk ~ это 100Mb. Да, жесть, согласен, но в остальном лишь преимущества. Единая кодовая база, куча компонент из коробки на все случаи жизни, причём под FireMonkey они стали гораздо гибче, чем vcl. Так что основная проблема нынешней Rad studio – это цена. Всё остальное в ней весьма достойно.

Автор: akadone

Источник

  1. Иван:

    Взвеселило: “опАздание” – чисто московский говор


https://ajax.googleapis.com/ajax/libs/jquery/3.4.1/jquery.min.js