业务逻辑
这几年电子税务局进步了很多,提供未抵扣增值税和全量增值税发票下载,这些数据相当于发票数据库,有了这些数据库,就可以来弄查重和查真的功能了。
由于电子税务局无法提供全部发票的下载,所以仅限于增值税发票的查重和查真。
查真
这个实现的逻辑非常简单,只要在发票数据库中搜索发票号码(数电票20位数字,其他增值税发票8位数字),如果找到就认为是真,找不到就认为是假即可。
查重
由于要检查重复,必须额外记录检查过的发票才能谈得上查重。因此还必须要增加一个查重的发票记录(查重台账)。
有了查重台账之后,查重先直接在查重台账中寻找,如果找到则说明是重复报销的发票,如果找不到,再去进行查真和后续操作,这样程序的速度会比较快。
程序设计
实际上程序业务的部分并不复杂,由于查重查真不是大量处理数据,涉及到交互的部分比较多。在检查发票的时候,因为都是从其他信息或者报销单上看来的数据,用户需要一个一个录入并实时获取反馈,因此需要设计一个GUI用来获取发票号码和实时获取反馈。
此外,需要设计一个开关用来检测是否需要查重。需要查重的话,先查查重台账,如果找不到再查真。不需要查重则仅仅只查真。
程序流程图
完整的程序逻辑如下:
GUI界面
GUI还是用tkinter来绘制,界面的元素如下:
- 标题栏
- 若干个文字提示,显示简短的操作提示
- 输入发票号码的控件
- 勾选是否查重的CheckBox
- 启动程序的按钮
- 程序运行结果展示区域
用户界面如下:
具体实现
来看一下具体实现,分为如下几个步骤:
- 基础配置
- 编写用户界面
- 编写业务程序
- 将业务程序和用户界面的交互绑定上
基础配置
这里主要是确定好发票数据库和查重台账的放置位置,以及设计好查重台账。
发票数据库
用来存放所有从电子税务局导出的发票信息。我将其设置在程序运行目录下的“发票数据库”目录内,因此要先写一个路径配置变量:
DATABASE_PATH = os.path.join(os.getcwd(), "发票数据库")
未来就遍历这个目录下所有的Excel文件,作为数据库。如此做的好处是可以直接把增量数据丢进去,对于使用的人比较方便。
查重台账
查重台账设置在程序运行目录下,需要预先创建一个"查重台账.xlsx"文件,和程序放在同一个目录下,并写好对应的变量:
REPEAT_DATABASE_PATH = os.path.join(os.getcwd(), '查重台账.xlsx')
查重台账的内容只有一行五列,是一个标题行,内容分别是:
- 发票号码
- 抬头
- 金额
- 发票开具时间
- 最初查验时间
手工输入即可。程序将来也不会对标题行进行操作。
编写用户界面
在这个部分,就用tkinter来将用户界面一点点画出来。用户界面的绘制都写在if __name__ == '__main__':
里边,程序启动的时候绘制用户界面。
依次创建各个组件
先把所有的组件创建出来:
# 主窗口
MAIN_FRAME = tk.Tk()
# 最上方的提示文字
MESSAGE_LABEL = tk.Label(master=MAIN_FRAME, text='请输入发票号码。需要同时查重的发票请勾选查重',
font=("微软雅黑", 14), pady=14)
# 提示输入发票的文字
CHECK_INVOICE_LABEL = tk.Label(master=MAIN_FRAME, text="请输入发票号码以检查真伪:", font=("微软雅黑", 14),
border=10)
# 发票输入框
INVOICE_NUMBER_ENTRY = tk.Entry(master=MAIN_FRAME, font=("微软雅黑", 14), width=25)
# 是否查重的复选框
REPEAT_CHECKBOX = tk.Checkbutton(master=MAIN_FRAME, text="是否同时查重", font=("微软雅黑", 14))
REPEAT_CHECKBOX.select()
# 启动按钮
CHECK_INVOICE_BUTTON = tk.Button(master=MAIN_FRAME, text="检查发票", font=("微软雅黑", 14))
# 结果展示区域
RESULT_LABEL = tk.Label(master=MAIN_FRAME, font=("微软雅黑", 12), anchor='w', pady=12, padx=12, justify='left')
这些组件有的在创建时候就可以指定一些属性,有的还必须在创建之后再进行一些配置和绑定业务程序。
绑定需要获取的值
根据之前的设计思路,需要从用户处获取的两个关键信息是发票号码和是否勾选查重,这需要设置两个变量并绑定到界面控件上用于获取对应信息。
此外就是整个界面的初始化等杂项工作,这里选择把窗口固定大小,采用pack来居中往下排列:
# 初始化用户主界面
MAIN_FRAME.resizable(width=False, height=False)
MAIN_FRAME.geometry('1440x900')
MAIN_FRAME.title('发票查验和查重')
MESSAGE_LABEL.pack(side=tk.TOP)
之后是其他相关的配置:
# 初始化显示发票查验提示框
CHECK_INVOICE_LABEL.pack(side=tk.TOP, fill=tk.X)
# 初始化发票查验输入框和对应绑定的值
INVOICE_NUMBER = tk.StringVar()
INVOICE_NUMBER_ENTRY.pack(side=tk.TOP)
INVOICE_NUMBER_ENTRY.config(textvariable=INVOICE_NUMBER)
# 初始化查重复选框并绑定对应的值
IS_CHECK_REPEAT = tk.IntVar()
REPEAT_CHECKBOX.config(variable=IS_CHECK_REPEAT)
REPEAT_CHECKBOX.pack(side=tk.TOP)
# 初始化按钮 程序要到最后再绑定处理程序
CHECK_INVOICE_BUTTON.pack(side=tk.TOP)
RESULT_LABEL.pack(side=tk.TOP, fill=tk.X,)
这里有两个关键点:
一是获取发票号码,通过设置了INVOICE_NUMBER
这个变量为tk.StringVar()
这个类型的值,并将其通过INVOICE_NUMBER_ENTRY.config(textvariable=INVOICE_NUMBER)
设置在输入框上,这样获取INVOICE_NUMBER
变量的值就获取到了实际输入的值。这个值是一个字符串,因为电子税务局导出数据的发票号码都是字符串,方便匹配。
二是获取查重是否勾选,这里创建一个Int
类型的IS_CHECK_REPEAT
变量,经过查看tkinter的文档,如果选中就是1,未选中就是0,因此给这个变量起了一个类似布尔类型变量的名称。
编写业务程序
通过GUI的排布可以知道,用户在输入了发票号码,勾选是否查重之后,需要点击按钮来启动程序,因此必须编写一个函数,绑定到按钮上,每次用户单击按钮的时候,启动实际的业务逻辑,最后把结果写入到结果展示区域中。
核心业务逻辑
我们创建一个start_check
函数作为核心逻辑函数,并在将来将其绑定到CHECK_INVOICE_BUTTON
这个按钮上。
函数逻辑如下:
- 对发票号码进行校验,仅接受8或20位的纯数字字符串
- 按之前设计图中逻辑进行操作
- 将结果返回至结果展示区域
start_check
函数如下:
def start_check():
# 一旦按下按钮就设置按钮为DISABLED,防止反复点击
CHECK_INVOICE_BUTTON.config(state=tk.DISABLED)
MAIN_FRAME.update()
RESULT_LABEL.config(text='')
# 设置最终的显示字符串
result = ''
# 获取发票字符串并检查是否有效
invoice_number_string = INVOICE_NUMBER_ENTRY.get().strip()
if not Functions.validate_invoice_number(invoice_number_string):
result += "发票号码: {} 无效,请输入8位或20位发票号码。".format(invoice_number_string)
RESULT_LABEL.config(text=result)
CHECK_INVOICE_BUTTON.config(state=tk.NORMAL)
INVOICE_NUMBER.set('')
return
# 如果勾选查重 先到查重表里查找相应的号码,如果没有,则进行查真,查真之后,将发票信息写入查重表
# 如果勾选查重,找到了查重表里的号码,则提示该发票已经查重过
# 勾选了查重的情况下
if IS_CHECK_REPEAT.get():
# 先到查重表里寻找编码看是否能够找到该号码,如果找到直接组装字符串返回
repeat_result = Functions.find_repeated_invoice(invoice_number_string)
if len(repeat_result) > 0:
result += '发票号码:{} 已经存在于查重台账中,属于重复发票。'.format(invoice_number_string)
RESULT_LABEL.config(text=result)
CHECK_INVOICE_BUTTON.config(state=tk.NORMAL)
INVOICE_NUMBER.set('')
return
# 未找到则要先去查真,然后将查真数据写入查重台账,之后返回结果
else:
result = '发票号码:{} 不存在于查重台账中,将进行验真。\n'.format(invoice_number_string)
# 进行查真, 返回查找的结果
found_string = Functions.find_invoice(invoice_number_string)
# 查真结果为真, 先写入字符串
if len(found_string) > 0:
result += Functions.assemble_find_invoice_result(found_string, invoice_number_string)
# 将结果写入到查重台账中
Functions.write_found_invoice_to_repeat_database(found_string)
result += "已将上述查找结果写入查重台账"
RESULT_LABEL.config(text=result)
CHECK_INVOICE_BUTTON.config(state=tk.NORMAL)
INVOICE_NUMBER.set('')
return
else:
# 查真结果为假和未勾选的结果是一样的,直接交给组装函数
RESULT_LABEL.config(
text=Functions.assemble_find_invoice_result(found_string, invoice_number_string))
CHECK_INVOICE_BUTTON.config(state=tk.NORMAL)
INVOICE_NUMBER.set('')
return
else:
# 未勾选查重,仅仅查真
RESULT_LABEL.config(
text=Functions.assemble_find_invoice_result(Functions.find_invoice(invoice_number_string),
invoice_number_string))
CHECK_INVOICE_BUTTON.config(state=tk.NORMAL)
INVOICE_NUMBER.set('')
return
start_check
函数的主要功能,就是根据业务逻辑最后调用实际的功能函数来完成各个功能,并且展示结果。这其中调用了在Functions
模块中编写的函数:
Functions.validate_invoice_number(invoice_number_string)
是判断发票号码长度是否符合要求的函数Functions.find_invoice(invoice_number_string)
是查真的函数Functions.assemble_find_invoice_result(invoice_list: list, invoice_number: str)
是组装查询结果的函数Functions.find_repeated_invoice(invoice_number: str)
是在查重台账中查找发票号码的函数Functions.write_found_invoice_to_repeat_database(invoice_list: list)
是将查真结果写入查重台账的函数
交互绑定
其实这里就干了一件事情,就是将start_check
函数绑定到启动按钮上即可:
CHECK_INVOICE_BUTTON.config(command=start_check)
最后设置tk来启动主界面即可:
MAIN_FRAME.mainloop()
后记
发票查重查真的问题,在不上系统和花费额外金钱的情况下,针对增值税发票这一块就这么实现了,也依赖于现在税务局的信息化程度提高,很容易获取获取完整的增值税发票信息。信息化只要贴合实际,永远是值得追求的目标。
Github地址
Functions
中的代码,以及一些辅助的代码就不再一一放出了,都是对Excel文件的常规操作。
有兴趣的朋友可以到我的Github项目地址:https://github.com/minkolee/InvoiceCheckSystem 来阅读这些代码。其中有两个分支,master
分支对应发票数据库是全量发票,onlyUseInvoiceNotChecked
分支对应发票数据库是未勾选增值税发票。为了区分不同情况,没有将两种类型混在一起。
采用数据库存放发票信息
在将程序交给同事实际操作的过程中,反馈的问题是查询全量发票比较慢,这是因为全量发票导出的文件大小太大,在加载的时候需要全部读入内存再进行加载,对于业务量大的公司,按这个循环读取一遍文件要耗时很久。对于未勾选发票,一般不会超过几千张,速度还是可以保证的。
如果大家想要解决此问题,可以考虑用正式的数据库而不是excel文件来充当发票数据库。
需要额外编写一个程序用于将发票信息写入到数据库,可以集成在这个程序中,也可以另外再写,就看打算如何维护数据库信息。实现起来很简单,因为税务局导出的发票信息,除了字符串就是字符串形式的时间信息,而且未必所有字段都需要导入,只需要关键信息即可。用了数据库之后,查个几十万上百万条信息正常情况下都是秒出结果。