Qt

Печатаем из Qt на онлайн фискальнике ККТ PAYONLINE-01-ФА

В связи с очередным злоебучим (по моему мнению) законом 54-ФЗ пришлось клиентам резко обновлять кассы и выкидывать из своего кармана 40 штук, что бы поменять шило на полноценный анальный зонд.

Я бы да же этому был бы рад из-за возможности заработать на обновлении, но вот что-то радоваться чужому горю вообще не фонтан.

В общем, подключаем приблуду по микро USB (ком-порта у меня в ноуте нет), заходим на оф. сайт, качаем и драйвер «USB VCOM«.

В дальнейшем оттуда нас будет так же интересовать руководство программиста, почитайте.

С драйвером мы будем работать через ActiveX (старовато, ну а херли делать. Радует немного, что Qt поддерживает эту технологию).

Создаём новый проект (у меня консольное приложение), создаём в проекте папку activex, в неё кидаем саму либо. В неё же в дальнейшем сгенерируем заголовочник и исходник.

C:\Projects\KKT-PAYONLINE-01-FA\KKT-PAYONLINE-01-FA\activex
C:\Qt\5.9.2\mingw53_32\bin\dumpcpp DrvFR.dll

Пути замените на свои. Кстати, в путях не используйте кириллицу. Если есть пробелы, то возьмите в кавычки. 

В папке должно появится два файла: «drvfrlib.h» и «drvfrlib.cpp» — они то нам и нужны, добавляем их в проект.

В настройках проекта («*.pro» файле) подключаем пару модулей 

QT += axcontainer widgets

Они нам нужны для работы с ActiveX.

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

#include <QCoreApplication>
#include <QApplication>
#include <QDebug>

#include "activex/drvfrlib.h" //-- Подключаем заголовочник либы
using namespace DrvFRLib; //-- Сразу задаём в каком пространстве имён будем работать

int main(int argc, char *argv[])
{
    QApplication a(argc, argv);
    qDebug()<<"Started!";

    DrvFR * fr = new DrvFR(0, 0); //-- Создаём класс драйвера

    //-- Подключаемся к поебени
    fr->SetConnectionType(0); //-- Метод её подключения к нам, 0-локально
    fr->SetProtocolType(0); //-- Вервия протокола, 0-стандартная
    fr->SetComputerName("HCSTerminal"); //-- Задаём имя, под которым она нас будет знать
    fr->SetTimeout(0); //-- Сколько будет ждать ответа (рекомендую 0, иначе будут сюрпризы)
    fr->SetComNumber(3); //-- Устанавливаем на каком com-порту висит (COM3 - в моём случае)
    fr->SetBaudRate(115200); //-- Устанавливаем скорость подключения (115200 - дефолтная)
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ по дефолту)
    qDebug()<<"Connect"<<fr->Connect();


    //-- Проверим состояние
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    qDebug()<<"EcrStatus"<< fr->GetECRStatus();
    qDebug()<<"EcrMode"<< fr->ECRMode(); //-- Подробнее на стр. 64. Сейчас нас интересует: 4-смена закрыта, 3-смена открыта, но кончилась (нужно напечатать Z-отчёт с гашением)
    qDebug()<<fr->ECRModeDescription();



    //-- Печатаем z-отчёт с гашением
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    fr->PrintReportWithCleaning();
  
      //-- Откроем новую смену
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    fr->OpenSession(); //-- открывает смену
    

    //-- Начинаем формирование нового чека
    fr->SetCheckType(0); //-- Устанавливаем тип нового чека, 0-продажа
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    qDebug()<<"OpenCheck"<<fr->OpenCheck();
    qDebug()<<"Result"<<fr->ResultCode()<<fr->ResultCodeDescription();
    
    //-- Добавляем позиции продажи
    fr->SetQuantity(1.0); //-- Устанавливаем количество
    fr->SetPrice(3); //-- Устанавливаем цену
    fr->SetStringForPrinting("Проверка печати"); //-- Описание товара
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    fr->SetDepartment(1); //-- Задаём отдел продажи (если хуёвин несколько, что бы отличать)
    qDebug()<<"Sale result"<<fr->Sale();


    //-- Завершаем формирование чека, после закрытия выйдет на печать
    fr->SetSumm1(3); //-- Сумма оплаты наличными, если несколько типов оплат, то соответственно fr->setSumm2(); Если сумма по всем позициям будет не совпадать, то на чеке автоматически будет выведена сдача.
    fr->SetStringForPrinting("==="); //-- В чеке будет двойная линия
    fr->SetPassword(30); //-- Задаём пароль оператора (30 - админ)
    qDebug()<<"Check status"<<fr->CloseCheck();
    
    //-- Отключаемся
    fr->Disconnect();

    delete fr;

    qDebug()<<"Ended!";
    return a.exec();
}

Вот как-то так выглядит алгоритм печати чека (продажи). Если какой-то метод вернёт что-то отличное от 0, значит произошла ошибка.

Разумеется каждый раз печатать z-отчёт и открывать смену не нужно, как и писал выше, нужно проверять сначала, но для примера не стал (лень). 

[tip]Если будет ругаться на «enum TBarcodePrintType«, то просто закомментите в двух местах.[/tip]

А вот теперь попробуем  продать товар с ценой, где есть копейки:

....
fr->SetPrice(3.23);
....
fr->SetSumm1(3.23);
....

Облом, да? Дробная часть у цена откинулась наглухо. Дело в том, что в документации у цены стоит тип «Cyrrency» (Денежный).
Так как в C++ явно такого типа нет, то Qt преобразовал его в qlonglong.

Currency широко используется в Delphi для хранения денежных единиц. Представляет он из себя, по сути, тип с фиксированной точкой, а храниться как 8 байтное целое, где 4 младших разряда — неявная дробная часть. Подробнее.

Перевод из double (c++) в Currency (Delphi) прост:

double d = 12.3456;
qlonglong currency = (round(d*10000)/10000)*10000;

Пояснение:
(round(d*10000)/10000) так как у Currency значащими являются только 4 знака после запятой, то так мы округляем до них.
*10000 — тем самым приводим к виду Currency.

Кстати, здесь не используется банковское округление (когда 0.5 считается в меньшую сторону).

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

#ifndef TCURRENCY_H
#define TCURRENCY_H

#include <cmath>
#include <QVariant>

class TCurrency
{
public:
    TCurrency() { _val = 0; }
    TCurrency(const TCurrency &c) { _val = c._val; }
    TCurrency(double d) { _val = fromDouble(d); }
    TCurrency(qlonglong ll) { _val = ll; }
    ~TCurrency() {}

    static inline qlonglong fromDouble(double d) { return (round(d*10000)/10000)*10000; }

    TCurrency& operator =(double d) { _val = fromDouble(d); return *this; }

    const TCurrency operator +(double d) { return TCurrency(_val+fromDouble(d)); }
    const TCurrency operator +(const TCurrency &tc) { return TCurrency(_val+tc._val); }
    TCurrency& operator +=(double d) { _val += fromDouble(d); return *this; }
    TCurrency& operator +=(const TCurrency &tc) { _val += tc._val; return *this; }

    const TCurrency operator -(double d) { return TCurrency(_val-fromDouble(d)); }
    const TCurrency operator -(const TCurrency &tc) { return TCurrency(_val-tc._val); }
    TCurrency& operator -=(double d) { _val -= fromDouble(d); return *this; }
    TCurrency& operator -=(const TCurrency &tc) { _val -= tc._val; return *this; }

    explicit operator qlonglong() const { return toLongLong(); }
    explicit operator QString() const { return toString(); }
    explicit operator double() const { return toDouble(); }
    explicit operator QVariant() const { return QVariant::fromValue(TCurrency(*this)); }

    static void registTypes() { QMetaType::registerConverter<TCurrency, qlonglong>(&TCurrency::toLongLong); QMetaType::registerConverter<TCurrency, QString>(&TCurrency::toString); }

    qlonglong toLongLong() const { return _val; }
    QString toString() const { return QString::number((double)_val/10000, 'f', 4); }
    double toDouble() const { return (double)_val/10000; }

private:
    qlonglong _val;

};


Q_DECLARE_METATYPE(TCurrency)

#endif // TCURRENCY_H

В итоге:

fr->SetPrice(TCurrency(3.23));
или 
TCurrency cr;
cr = 2.23;
cr += 0.1;
fr->SetPrice(cr);

Упс…

Тут нас ждёт облом и вылезет чек на 22400 !

Почему? Дело в том, что в Qt это то-ли бага, то ли фича.
Подробнее: https://bugreports.qt.io/browse/QTBUG-26683

В общем, нужно немного подправить модуль QtActiveQt.
У меня Qt 5.14.1, думаю для других версий аналогично.
Надеюсь, при установке Qt Вы отметили установку исходников (иначе в MaintenanceTool.exe доустановите. ВАЖНО такой же версии, как и Qt).
Дальше в QtCreator открываем проект
«<QtDir>\<version>\Src\qtactiveqt\qtactiveqt.pro«

Проблема в том, что в файле «<QtDir>\<version>\Src\qtactiveqt\src\activeqt\shared\qaxtypes.cpp»
В блоке: «case QVariant::LongLong:» не срабатывает условие «if (out && arg.vt == (VT_CY|VT_BYREF)) {«, так как arg.vt равно VT_ERROR.
Ноги этой проблемы, по-моему, в «<QtDir>\<version>\Src\qtactiveqt\src\activeqt\container\qaxbase.cpp» в блоке «case QMetaObject::WriteProperty:».

Для решения этой проблемы предлагаю:

1. Что бы dumpcpp сразу для типа Currency выставлял наш класс «TCurrency», для этого правим файл «<QtDir>\<version>\Src\qtactiveqt\src\activeqt\container\qaxbase.cpp»

case VT_CY:
 str = "TCurrency"; //-- "qlonglong"
 break;

2. Что бы теперь QVariant сконвертировать в Виндовый VARIANT правим файл «<QtDir>\<version>\Src\qtactiveqt\src\activeqt\shared\qaxtypes.cpp»

case QVariant::UserType:
.....
if ( !qstrcmp(qvar.typeName(), "TCurrency") ) {
    arg.vt = VT_CY;
    arg.cyVal.int64 = qvar.toLongLong();
} else
....

Нус, запускаем проект QtActiveQt на компиляция в режиме релиза, курим пару минут.
Теперь нужно скопировать либы из папки:
«<QtDir>\<version>\Src\build-qtactiveqt-Desktop_Qt_5_14_1_MinGW_32_bit-Release\lib\» в папку «<QtDir>\<version>\mingw73_32\lib\«.
А также «<QtDir>\<version>\Src\build-qtactiveqt-Desktop_Qt_5_14_1_MinGW_32_bit-Release\bin\» в папку «<QtDir>\<version>\mingw73_32\bin\«.

Теперь заново запускаем dumpcpp, что бы сгенерировались новые исходник и заголовочник.

В заголовочник только потом не забудьте подключить tcurrency.h
Так же не забудьте где-нить зарегистрировать каст типов:
TCurrency::registTypes();

Компилим и запускаем наш тестовый проект, радуемся!

Вот как-то так.

Related posts

QML Сделать задержку перед началом анимации

QtCreator не открывает диалог выбора файлов, проектов, не открывает проект

Сборка Qt 6.8 (Dev) из исходников в Docker контейнере (Linux)

2 комментария

Alexey 14 января 2019 - 13:38
Обратите внимание на вот этот проект https://github.com/shtrih-m/fr_drv_ng&#13; Он кроссплатформенный, есть обёртка для Qt (свойства с уведомлениями) и нет проблем с currency итд.
Pavelk 25 января 2019 - 3:20
Спасибо. Покопаюсь, как проект будет.
Add Comment