1. 引言
一位相知相交多年的挚友,现在经营一家财务公司,业务重点是代理记账和税务筹划。去年的时候,我们偶然相聚,弹指一挥间,已十来个春秋未曾谋面。多年未见,再相聚,相互寒暄,谈笑风生,把酒言欢,相叙别后之境,正谓是“故人相遇情如故”,一切不再言表。
后来,慢慢谈到各自的工作上来。老友向我诉说,他现在有一件,不得不面对的忧心之事。有六七百家的客户,委托他的公司做账,每月都需要手工从开票软件中导出销项发票数据,然后再根据企业性质的不同,整理出相应的报表。这需要投入很多的人力去做,并且容易出错。
老友长叹一口气,然后望着我说道:“我们这个行业,说实话,现在就是价格战,算上投入的人力成本,盈利真的很难。你在IT行业也是资深人士,能否亲自开发一款工具出来,以解我的难言之隐。”
看到老友如此忧心烦闷,我便一口应允下来。身负老友的嘱托,我日夜兼程地奋斗两个多月的时间,开发出一款工具出来,这款工具切切实实地解决了老友的忧心烦恼之现状,大大超出其预期。老友感激之情不再言表,这款工具,至今依然在老友那里稳健地运行。
今日,笔者稍得闲暇,将这款工具的思路和核心代码分享给各位朋友,以期能为,遇到相似烦恼或者技术障碍的朋友,提供参考。同时,由于笔者认知和技术能力有限,文中难免会有不当或错误之处,欢迎批评指正。同时笔者也殷切的期盼,能和各位朋友做进一步的沟通交流,以期相互受益。
2. 采集工具需求分析及设计思路
笔者经过调研和分析,发现销项发票数据的采集,有几种实现途径,例如从开票软件系统中采集,从税盘中采集,从增值税发票综合服务平台采集等。但笔者认为,从开票软件系统采集是最便捷的途径。
2.1 开票软件现状分析
现在市场在用的开票软件分为金税盘(白盘)版,税控盘(黑盘)版,税务UKey版。从当前的市场占有率来说,以金税盘为代表的航信系开票软件遥遥领先,是佼佼者。从趋势来说,百旺系的税务UKey版开票软件独占鳌头,只有税务UKey版开票软件,才能开具电专发票,代表着新的趋势。金税盘版开票软件从大版本来说分为2.0版和3.0版,时至今日,2.0版已经没落,会逐渐升级到3.0版。税控盘版开票软件从大版本来说,分为旧版本和新版本,旧版本极少。
从开票软件数据库来说,金税盘版2.0分为cc3268和skfpdb.db两个版本,3.0又细分为3.0和3.1两个细分版本。税控盘版分为老版本,V2,V5三个细分版本。税务UKey版分为V3和UKey两个版本。
2.2 采集工具整体设计思路
笔者对采集工具制定的首要目标是:能满足老友的业务需求,解决其长期困扰他的,痛心疾首之状况。首先,工具要能支持所有的开票软件,无需人工干预的情况下,做到快速采集数据,并自动生成业务需要的报表。再者,报表模板配置尽最大可能灵活,做到用户体验良好。最后,整体程序设计要模块化,甚至微服务化,能做到最小粒度积木块设计,将来,能根据业务的变化,快速拼装出新形态的产品。按照这个思路,需要开发完全独立的采集组件,该组件要做到在业务上和上层业务系统零耦合。同时,组件也要做到技术形态的完全独立性,在技术上也要做到和上层业务系统的零耦合。
图-1 采集工具整体设计
3. 销项数据采集核心代码分享
3.1 采集组件接口声明代码分享
{*******************************************************************
函数:collectBySql
功能:组件接口,根据sql语句采集数据
参数:taxDeviceDir 输入参数,开票机路径
sql 输入参数,采集数据的sql语句
dataDir 输入参数,采集到的数据报文文件存储路径
outDataFileBuf 输入参数,采集到的数据报文文件名存储的
内存缓冲区指针
outDataFileBufLen 输入参数,outDataFileBuf缓冲区长度
errMsgBuf 输入参数,错误信息内存缓冲区指针
errMsgBufLen 输如参数,errMsgBuf缓冲区长度
返回值:true 成功
false 失败
date: 2020-04-20
author: 海之边 qq-3094353627
*******************************************************************}
function collectBySql(taxDeviceDir, sql, dataDir: PChar;
outDataFileBuf: PChar; outDataFileBufLen: DWord; errMsgBuf: PChar;
errMsgBufLen: DWord): boolean; stdcall;
external 'InvCollector.dll' name 'collectBySql';
{*******************************************************************
函数:decryptInvMw
功能:组件接口,解密发票密文区密文数据
参数:appKind 输入参数,开票软件类型
1- 金税盘2.0
2- 金税盘3.0
InvMwData 输入参数,密文区密文数据
outPlainBuf 输入参数,保存明文数据的内存缓冲区指针
outPlainBufLen 输入参数,outPlainBuf内存缓冲区长度
errMsgBuf 输入参数,错误信息内存缓冲区指针
errMsgBufLen 输如参数,errMsgBuf缓冲区长度
返回值:true 成功
false 失败
date: 2020-04-20
author: 海之边 qq-3094353627
*******************************************************************}
function decryptInvMw(appKind: integer; InvMwData: PChar;
outPlainBuf: PChar; outPlainBufLen: DWord;
errMsgBuf: PChar; errMsgBufLen: DWord): boolean; stdcall;
external 'InvCollector.dll' name 'decryptInvMw';
3.2 销项数据采集核心代码分享
{
函数:collectByKprq
功能:根据开票日期采集指定的开票机销项发票数据,并存储到本地数据库
参数:kpjlj 输入参数,开票机路径
ksrq 输入参数,起始开票日期
jsrq 输入参数,结束开票日期
errMsg 输出参数,错误信息
返回值:true 成功
false 失败
}
function TInvCollectTool.collectByKprq(kpjlj, ksrq, jsrq: string;
var errMsg: string): boolean;
var bRet: boolean;
mAppType: TAppType;
dwErrMsgBufLen, dwOutDataBufLen: dword;
pErrMsgBuf, pOutDataBuf: PChar;
sSql, sInvFile, sInvMxFile, sInvQdFile: string;
begin
result := true;
errMsg := '';
pErrMsgBuf := nil;
pOutDataBuf := nil;
try
//1. 初始化
dwErrMsgBufLen := 2048;
pErrMsgBuf := GetMemory(dwErrMsgBufLen);
FillChar(pErrMsgBuf^, dwErrMsgBufLen, #0);
dwOutDataBufLen := MAX_PATH;
pOutDataBuf := GetMemory(dwOutDataBufLen);
FillChar(pOutDataBuf^, dwOutDataBufLen, #0);
//2. 解析开票软件路径
mAppType := parseInvAppType(kpjlj);
if mAppType = atNone then
begin
result := false;
errMsg := '开票机路径错误';
Exit;
end;
try
//3. 采集发票
if mAppType in [mtAisino2, mtAisino3] then
begin
//金税盘2.0或金税盘3.0
sSql := 'select * from xxfp '#13#10
+ 'where kprq >= ''%s'' '#13#10
+ ' and kprq <= ''%s'' '#13#10;
end
else
begin
//税控盘或税务UKey
sSql := 'select * from zzs_fpkj '#13#10
+ 'where kprq >= ''%s'' '#13#10
+ ' and kprq <= ''%s'' '#13#10;
end;
sSql := format(sSql, [ksrq, jsrq]);
bRet := collectBySql(kpjlj, sSql, getOutTmpDir, pOutDataBuf,
dwOutDataBufLen, pErrMsgBuf, dwErrMsgBufLen);
if not bRet then
begin
result := false;
errMsg := format('采集销项数据失败:%s', [pErrMsgBuf]);
Exit;
end;
sInvFile := StrPas(pOutDataBuf);
//4. 费用明细
if mAppType in [mtAisino2, mtAisino3] then
begin
//金税盘2.0或金税盘3.0
sSql := 'select t2.* '#13#10
+ 'from xxfp '#13#10
+ 'join xxfp_mx t2 on t1.fpzl = t2.fpzl and t1.fpdm = t2.fpdm and t1.fphm = t2.fphm '#13#10
+ 'where t1.kprq >= ''%s'' '#13#10
+ ' and t1.kprq <= ''%s'' '#13#10;
end
else
begin
//税控盘或税务UKey
sSql := 'select * '#13#10
+ 'from zzs_fpkj '#13#10
+ 'join zzs_fpkj_mx t2 on t1.fpdm = t2.fpdm and t1.fphm = t2.fphm '#13#10
+ 'where kprq >= ''%s'' '#13#10
+ ' and kprq <= ''%s'' '#13#10;
end;
sSql := format(sSql, [ksrq, jsrq]);
bRet := collectBySql(kpjlj, sSql, getOutTmpDir, pOutDataBuf,
dwOutDataBufLen, pErrMsgBuf, dwErrMsgBufLen);
if not bRet then
begin
result := false;
errMsg := format('采集销项数据失败:%s', [pErrMsgBuf]);
Exit;
end;
sInvMxFileFile := StrPas(pOutDataBuf);
//5. 清单明细数据
if mAppType in [mtAisino2, mtAisino3] then
begin
//金税盘2.0或金税盘3.0
sSql := 'select t2.* '#13#10
+ 'from xxfp '#13#10
+ 'join xxfp_xhqd t2 on t1.fpzl = t2.fpzl and t1.fpdm = t2.fpdm and t1.fphm = t2.fphm '#13#10
+ 'where t1.kprq >= ''%s'' '#13#10
+ ' and t1.kprq <= ''%s'' '#13#10;
end
else
begin
//税控盘或税务UKey
sSql := 'select * '#13#10
+ 'from zzs_fpkj '#13#10
+ 'join zzs_fpkj_qd t2 on t1.fpdm = t2.fpdm and t1.fphm = t2.fphm '#13#10
+ 'where kprq >= ''%s'' '#13#10
+ ' and kprq <= ''%s'' '#13#10;
end;
sSql := format(sSql, [ksrq, jsrq]);
bRet := collectBySql(kpjlj, sSql, getOutTmpDir, pOutDataBuf,
dwOutDataBufLen, pErrMsgBuf, dwErrMsgBufLen);
if not bRet then
begin
result := false;
errMsg := format('采集销项数据失败:%s', [pErrMsgBuf]);
Exit;
end;
sInvQdFile := StrPas(pOutDataBuf);
//6. 保存采集到的销项数据
result := saveInvData(mAppType, sInvFile, sInvMxFile, sInvQdFile, errMsg);
except
on e: Exception do
begin
result := false;
errMsg := format('采集销项数据出错:%s', [e.Message]);
Exit;
end;
end;
finally
if FileExists(sInvFile) then
deleteFile(sInvFile);
if FileExists(sInvMxFileFile) then
deleteFile(sInvMxFileFile);
if FileExists(sInvQdFile) then
deleteFile(sInvQdFile);
if Assigned(pErrMsgBuf) then
FreeMemory(pErrMsgBuf);
if Assigned(pOutDataBuf) then
FreeMemory(pOutDataBuf);
end;
end;
4. 后记
该采集工具,已在老友公司稳健运行一年有余,至此,理应落幕。然而,笔者也一直在思考,能否对该工具,做进一步的优化,以发挥更大的作用。笔者诚恳地期望,能和更多的朋友进行更进一步的沟通交流,以期相互受益。