为方便公司开票人员准确快速的开票,通过两周的研究,码出此工具。
(试用下载地址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);
else
yflag = 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;
}
else
strlCnInteger.insert(i + 1, "万"), yflag = true, wflag = true;
}
else // 亿
{
if (strlInteger.at(i) == "0")
{
strlCnInteger.removeAt(i);
strlCnInteger.insert(i, "亿");
yflag = false;
}
else
strlCnInteger.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);
}