为方便公司开票人员准确快速的开票,通过两周的研究,码出此工具。
(试用下载地址https://download.csdn.net/download/super_farmers/12264989)
在此写下实现过程:
整个软件分为:
1.扫描二维码
手机扫描二维码使用DroidCam软件实现,DroidCam可以把手机做为二维码采集设备(由于机动车二维码的复杂性,手机摄像头像素越高越好);
首先在安装DroidCam软件的电脑端:
DroidCam可以使用wifi/usb两种方式连接手机,本文中使用wifi连接方式(方便);
接着安装手机端:
手机端安装好以后可以在电脑端输入手机端显示的IP地址,测试连接是否正常
在程序中使用下面代码读取摄像头列表会得到:DroidCam字串开头的四个设备,除编号为3的设备,其它均可使用。
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();foreach (const QCameraInfo &cameraInfo, cameras) {ui->Camera_List->addItem(cameraInfo.description());}
获取到设备以后,显示视频及截图代码:
QList<QCameraInfo> cameras = QCameraInfo::availableCameras();foreach (const QCameraInfo &cameraInfo, cameras) {if (cameraInfo.description() == ui->Camera_List->currentText()){// 摄像头实例m_Camera = new QCamera(cameraInfo);// 模式、视频采集器m_Camera->setCaptureMode(QCamera::CaptureStillImage);m_Camera->setViewfinder(m_ViewFinder);// 开始获取视频m_Camera->start();// 定时器定时截图 m_ImageCapture->capture(QDir::currentPath() + "/code");m_Timer->start(2000);// 截图m_ImageCapture = new QCameraImageCapture(m_Camera);m_ImageCapture->setCaptureDestination(QCameraImageCapture::CaptureToFile);// 由slot_imageSaved槽处理截取的图像connect(m_ImageCapture, SIGNAL(imageSaved(int,QString)), this, SLOT(slot_imageSaved(int,QString)));}}
至此扫码功能实现完成。
2.识别二维码
Qt识别二维码有QZxing库(),经测试,这个库对机动车二维码无能为力,最终选择一个商业二维码识别控件PsyQrDcd这个要注册授权的,不然识别出只有"AAAAA…"(http://www.psytec.co.jp/),这个识别率很高,速度也非常快。在这一步由于使用第三方库,没什么好说的,使用函数DecodePictureFile设置二维码文件路径,GetDecodeData函数读取结果。
3.数据解密
这一步是本软件最核心的一步,因防伪等各种原因,二维码读取到的内容为des加密后base64编码的一串文本。解码不了数据,一切白搭。这里可以调用大神做的接口,没有解密能力的可以参考(https://vehcode.scznnet.cn:449/QRcodeDoc.html#15614)。调用接口需要编译了openssl的Qt版本,因为接口是https协议的,我之前编译的Qt没有openssl,重新编译了下QT(好崩溃,需要静态编译的同学编译openssl一定不要加–debug选项,不然你得重编译);数据解密后得到64个字段,开具发票只用到其中10个字段(生产企业名称|车辆类型|车辆型号|产地|合格证号|发动机号码|车辆识别号VIN|吨位|限乘人数|车辆名称),其中车辆类型对应开票系统中产品分类名称,用此名称从产品分类名称中获取分类编码,这个分类编译在生成发票的时候要用。
4.生成发票
依照《增值税发票税控开票软件数据接口规范3.0》中描述的机动车发票导入接口生成XML文档,模板如下:
<?xml version="1.0" encoding="gbk"?>
<business><body><djh>djh</djh> //单据号(30字符)<bmb_bbh>bmb_bbh</bmb_bbh> //编码表版本号(20个字符)<fpdm>fpdm</fpdm> //发票代码(10个字符)<fphm>fphm</fphm> //发票号码(8个字符)<gfdwmc>gfdwmc</gfdwmc> //购方单位名称(72个字符)<sfzhm>sfzhm</sfzhm> //身份证号码/组织机构代码(22个字符)<gfdwsbh>gfdwsbh</gfdwsbh> //购方单位识别号(20个字符)<cllx>cllx</cllx> //车辆类型(40个字符)<cpxh>cpxh</cpxh> //厂牌型号(60个字符)<cd>cd</cd> //产地(32个字符)<hgzh>hgzs</hgzh> //合格证书(50个字符)<jkzmsh>jkzmsh</jkzmsh> //进口证明书号(36个字符)<sjdh>sjdh</sjdh> //商检单号(32个字符)<fdjhm>fdjhm</fdjhm> //发动机号码(60个字符)<clsbdh>clsbdh</clsbdh> //车辆识别代号(23个字符)<scqymc>scqymc</scqymc> //生产企业名称(80个字符)<jshj>jshj</jshj> //价税合计<dh>dh</dh> //电话(40个字符)<zh>zh</zh> //账号(40个字符)<dz>dz</dz> //地址(80个字符)<khyh>khyh</khyh> //开户银行(80个字符)<zzssl>zzssl</zzssl> //增值税税率(实际税率)<zzsse>zzsse</zzsse> //增值税税额<bhsj>bhsj</bhsj> //不含税价<dw>dw</dw> //吨位(8个字符)<xcrs>xcrs</xcrs> //限乘人数(12个字符)<spbm> spbm</spbm> //商品编码(19个字符)<zxbm>zxbm </zxbm> //自行编码(20个字符)<yhzcbs>yhzcbs</yhzcbs> //优惠政策标识(1个字符) 0:不使用,1:使用<lslbs>lslbs</lslbs> //税率标识空(1个字符):非零税率,0:出口退税,1:免税,2:不征收,3普通零税率<zzstsgl>zzstsgl</zzstsgl> //增值税特殊管理(50个字符)</body>
</business>
单据号可自定义;编码版本为税收分类编码的版本号,可以在最新税收分类编码文档中获取,发票代码,发票号码留空,增值税税率(13% 要填入 0.13)
这里有个小写金额转大写的功能,,自行实现耗时一下午(开始想的太简单了)只实现 了最大千亿的转换(应该够了吧),代码奉上:
QString Invoice::AmountsConverted(double amount)
{QStringList cnNumber = {"零","壹","贰","叁","肆","伍","陆","柒","捌","玖"};// 拆分整数与小数部分QString strAmount, strInteger, strDecimal; // 金额字符串, 整数部分, 小数部分strAmount.setNum(amount, 'f', 2);strInteger = strAmount.split('.').at(0);strDecimal = strAmount.split('.').at(1);QStringList strlInteger, strlDecimal;strlInteger = strInteger.split("");strlInteger.removeFirst(); strlInteger.removeLast(); //移除首尾空值strlDecimal = strDecimal.split("");strlDecimal.removeFirst(); strlDecimal.removeLast(); //移除首尾空值// 构建汉字列表QStringList strlCnInteger, strlCnDecimal;foreach (QString str, strlInteger) {strlCnInteger << cnNumber.at(str.toInt());}foreach (QString str, strlDecimal) {strlCnDecimal << cnNumber.at(str.toInt());}// 整数部分添加单位int nCount = strlInteger.size() - 1; // 总共多少位bool yflag = false, sflag = false, bflag = false, qflag = false, wflag = false;int nStep = 0;for(int i = nCount; i >= 0; i--){switch (nStep++){case 0: // 元if (strlInteger.at(i) == "0") strlCnInteger.removeAt(i);elseyflag = true;break;case 1: // 拾if (strlInteger.at(i) == "0"){if (!yflag)strlCnInteger.removeAt(i);}else if (strlInteger.at(i) != "0")strlCnInteger.insert(i + 1, "拾"), sflag = true;break;case 2: // 百if (strlInteger.at(i) == "0"){if (!yflag && !sflag)strlCnInteger.removeAt(i);if (yflag && !sflag)strlCnInteger.removeAt(i);}else if (strlInteger.at(i) != "0")strlCnInteger.insert(i + 1, "佰"), bflag = true;break;case 3: // 仟if (strlInteger.at(i) == "0"){if (!yflag && !sflag && !bflag)strlCnInteger.removeAt(i);if ((yflag || sflag) && !bflag)strlCnInteger.removeAt(i);}else if (strlInteger.at(i) != "0")strlCnInteger.insert(i + 1, "仟"), qflag = true;break;case 4: // 万if (!wflag){if (strlInteger.at(i) == "0"){if (!qflag)strlCnInteger.removeAt(i);strlCnInteger.insert(i, "万");yflag = false;wflag = true;}elsestrlCnInteger.insert(i + 1, "万"), yflag = true, wflag = true;}else // 亿{if (strlInteger.at(i) == "0"){strlCnInteger.removeAt(i);strlCnInteger.insert(i, "亿");yflag = false;}elsestrlCnInteger.insert(i + 1, "亿"), yflag = true;}nStep = 1; sflag = bflag = qflag = false;break;}}// 小数部分添加单位// 分if (strlDecimal.at(1) == "0"){strlCnDecimal.removeAt(1);}else{strlCnDecimal.insert(2, "分");}// 角if (strlDecimal.at(0) == "0"){if (strlDecimal.at(1) == "0")strlCnDecimal.removeAt(0);}else{strlCnDecimal.insert(1, "角");}QString result = strlCnInteger.join("") + "圆" + strlCnDecimal.join("") + "整";return result;
}
5.开票导入:
这里没什么说的,生成发票XML后,在开票界面导入就成。
X.openssl des加解密的学习:
在openssl中以evp模式使用des等加解密方便快捷,但一定要注意数据类型的转换,简要代码如下:
// 生成key及iv len为返回key的长度,
int len = EVP_BytesToKey(/*方式*/, /*hex*/, NULL, /*密钥*/, /*密钥长度*/, /*强度*/, key, iv);
// ctx上下文结构申请内存 (在构造函数中)
m_evp_ctx = EVP_CIPHER_CTX_new();// 加解密过程// 释放dtx上下文结构内存 (在析构函数中)
EVP_CIPHER_CTX_free(m_evp_ctx);
// 加密
QString Common::des_EnCrypto(QString Text)
{QByteArray bText = Text.toUtf8();unsigned char *inText = (unsigned char*)bText.data();int inTextLen = bText.length();unsigned char *outText = (unsigned char*)malloc(inTextLen + 8);int outLen, outFinalLent, outTotalLen;EVP_CIPHER_CTX_reset(m_evp_ctx);int ret = EVP_EncryptInit(m_evp_ctx, EVP_des_cbc(), m_des_key, m_des_iv);if (ret != 1){EVP_CIPHER_CTX_reset(m_evp_ctx);return QString();}ret = EVP_EncryptUpdate(m_evp_ctx, outText, &outLen, inText, inTextLen);if (ret != 1){EVP_CIPHER_CTX_reset(m_evp_ctx);return QString();}else{EVP_EncryptFinal(m_evp_ctx, outText + outLen, &outFinalLent);outTotalLen = outLen + outFinalLent;}EVP_CIPHER_CTX_reset(m_evp_ctx);return QString::fromUtf8(QByteArray((char*)outText, outTotalLen).toHex());
}
// 解密
QString Common::des_DeCrypto(QString Text)
{QByteArray bText = QByteArray::fromHex(Text.toUtf8());unsigned char *inText = (unsigned char*)bText.data();int inTextLen = bText.length();unsigned char *outText = (unsigned char*)malloc(inTextLen + 8);int outLen, outFinalLent, outTotalLen;EVP_CIPHER_CTX_reset(m_evp_ctx);int ret = EVP_DecryptInit(m_evp_ctx, EVP_des_cbc(), m_des_key, m_des_iv);if (ret != 1){EVP_CIPHER_CTX_reset(m_evp_ctx);return QString();}ret = EVP_DecryptUpdate(m_evp_ctx, outText, &outLen, inText, inTextLen);if (ret != 1){EVP_CIPHER_CTX_reset(m_evp_ctx);return QString();}else{EVP_DecryptFinal(m_evp_ctx, outText + outLen, &outFinalLent);outTotalLen = outLen + outFinalLent;}EVP_CIPHER_CTX_reset(m_evp_ctx);return QString::fromUtf8((char*)outText, outTotalLen);
}