增值税发票查重查真 - 带GUI界面的简易实现

增值税发票查重查真 - 带GUI界面的简易实现

简单却实用的增值税发票查真查重程序

业务逻辑

这几年电子税务局进步了很多,提供未抵扣增值税和全量增值税发票下载,这些数据相当于发票数据库,有了这些数据库,就可以来弄查重和查真的功能了。

由于电子税务局无法提供全部发票的下载,所以仅限于增值税发票的查重和查真。

查真

这个实现的逻辑非常简单,只要在发票数据库中搜索发票号码(数电票20位数字,其他增值税发票8位数字),如果找到就认为是真,找不到就认为是假即可。

查重

由于要检查重复,必须额外记录检查过的发票才能谈得上查重。因此还必须要增加一个查重的发票记录(查重台账)。

有了查重台账之后,查重先直接在查重台账中寻找,如果找到则说明是重复报销的发票,如果找不到,再去进行查真和后续操作,这样程序的速度会比较快。

程序设计

实际上程序业务的部分并不复杂,由于查重查真不是大量处理数据,涉及到交互的部分比较多。在检查发票的时候,因为都是从其他信息或者报销单上看来的数据,用户需要一个一个录入并实时获取反馈,因此需要设计一个GUI用来获取发票号码和实时获取反馈。

此外,需要设计一个开关用来检测是否需要查重。需要查重的话,先查查重台账,如果找不到再查真。不需要查重则仅仅只查真。

程序流程图

完整的程序逻辑如下:
InvoiceCheckDesign.webp

GUI界面

GUI还是用tkinter来绘制,界面的元素如下:

  1. 标题栏
  2. 若干个文字提示,显示简短的操作提示
  3. 输入发票号码的控件
  4. 勾选是否查重的CheckBox
  5. 启动程序的按钮
  6. 程序运行结果展示区域

用户界面如下:
GuiSample.webp

具体实现

来看一下具体实现,分为如下几个步骤:

  1. 基础配置
  2. 编写用户界面
  3. 编写业务程序
  4. 将业务程序和用户界面的交互绑定上

基础配置

这里主要是确定好发票数据库和查重台账的放置位置,以及设计好查重台账。

发票数据库

用来存放所有从电子税务局导出的发票信息。我将其设置在程序运行目录下的“发票数据库”目录内,因此要先写一个路径配置变量:

DATABASE_PATH = os.path.join(os.getcwd(), "发票数据库")

未来就遍历这个目录下所有的Excel文件,作为数据库。如此做的好处是可以直接把增量数据丢进去,对于使用的人比较方便。

查重台账

查重台账设置在程序运行目录下,需要预先创建一个"查重台账.xlsx"文件,和程序放在同一个目录下,并写好对应的变量:

REPEAT_DATABASE_PATH = os.path.join(os.getcwd(), '查重台账.xlsx')

查重台账的内容只有一行五列,是一个标题行,内容分别是:

  1. 发票号码
  2. 抬头
  3. 金额
  4. 发票开具时间
  5. 最初查验时间

手工输入即可。程序将来也不会对标题行进行操作。

编写用户界面

在这个部分,就用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这个按钮上。

函数逻辑如下:

  1. 对发票号码进行校验,仅接受8或20位的纯数字字符串
  2. 按之前设计图中逻辑进行操作
  3. 将结果返回至结果展示区域

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模块中编写的函数:

  1. Functions.validate_invoice_number(invoice_number_string)是判断发票号码长度是否符合要求的函数
  2. Functions.find_invoice(invoice_number_string)是查真的函数
  3. Functions.assemble_find_invoice_result(invoice_list: list, invoice_number: str)是组装查询结果的函数
  4. Functions.find_repeated_invoice(invoice_number: str)是在查重台账中查找发票号码的函数
  5. 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文件来充当发票数据库。

需要额外编写一个程序用于将发票信息写入到数据库,可以集成在这个程序中,也可以另外再写,就看打算如何维护数据库信息。实现起来很简单,因为税务局导出的发票信息,除了字符串就是字符串形式的时间信息,而且未必所有字段都需要导入,只需要关键信息即可。用了数据库之后,查个几十万上百万条信息正常情况下都是秒出结果。

LICENSED UNDER CC BY-NC-SA 4.0
Comment