模块说明
invoice 发票校验
- 通过华为API接口校验发票的真伪
- 通过发票代码,发票号码校验是否重复
后端代码
数据模型:models.py
from django.db import models
from dvadmin.utils.models import CoreModel
table_prefix = 'apps_'
class InvoiceModel(CoreModel):
input_date = models.DateField(verbose_name="录入日期")
type = models.CharField(max_length=50, null=True, blank=True, verbose_name="发票类型")
code = models.CharField(max_length=50, verbose_name="发票代码")
number = models.CharField(max_length=50, verbose_name="发票号码")
issue_date = models.DateField(verbose_name="开票日期")
machine_number = models.CharField(max_length=50, null=True, blank=True, verbose_name="机器编号")
check_code = models.CharField(max_length=50, verbose_name="校验码")
status = models.CharField(max_length=10, null=True, blank=True, verbose_name="发票状态")
subtotal_amount = models.FloatField(default=0, verbose_name="合计金额(不含税)")
subtotal_tax = models.FloatField(default=0, verbose_name="合计税额")
total = models.FloatField(default=0, verbose_name="价税合计(小写)")
total_in_words = models.CharField(max_length=100, null=True, blank=True, verbose_name="价税合计(大写)")
remarks = models.TextField(null=True, blank=True, verbose_name="备注")
receiver = models.CharField(max_length=10, null=True, blank=True, verbose_name="收款人")
issuer = models.CharField(max_length=10, null=True, blank=True, verbose_name="开票人")
reviewer = models.CharField(max_length=10, null=True, blank=True, verbose_name="复核人")
buyer_name = models.CharField(max_length=50, null=True, blank=True, verbose_name="购买方名称")
buyer_id = models.CharField(max_length=50, null=True, blank=True, verbose_name="购买方纳税人识别号")
buyer_address = models.CharField(max_length=100, null=True, blank=True, verbose_name="购买方地址、电话")
buyer_bank = models.CharField(max_length=50, null=True, blank=True, verbose_name="购买方开户行及账号")
seller_name = models.CharField(max_length=50, null=True, blank=True, verbose_name="销售方名称")
seller_id = models.CharField(max_length=50, null=True, blank=True, verbose_name="销售方纳税人识别号")
seller_address = models.CharField(max_length=100, null=True, blank=True, verbose_name="销售方地址、电话")
seller_bank = models.CharField(max_length=50, null=True, blank=True, verbose_name="销售方开户行及账号")
deductible_toll = models.CharField(max_length=10, null=True, blank=True, verbose_name="通行费发票返回信息")
class Meta:
db_table = table_prefix + "invoice"
verbose_name = "发票表"
verbose_name_plural = verbose_name
ordering = ("id",)
unique_together = ("code", "number") # 联合唯一索引
序列化器:serializers.py
from dvadmin.utils.serializers import CustomModelSerializer
from .models import InvoiceModel
class InvoiceSerializer(CustomModelSerializer):
"""
发票查询序列化
"""
class Meta:
model = InvoiceModel
fields = "__all__"
class InvoiceCreateUpdateSerializer(CustomModelSerializer):
"""
发票新增,修改序列化
"""
class Meta:
model = InvoiceModel
fields = "__all__"
class InvoiceImportSerializer(CustomModelSerializer):
"""
发票导入,导出序列化
"""
class Meta:
model = InvoiceModel
fields = "__all__"
华为云:SDK
需要安装华为云SDK pip install huaweicloudsdkcore~=3.0.93 huaweicloudsdkocr~=3.0.94
核心函数:通过调用华为云的发票发票验真接口,在线调试地址:API Eexplorer
'''
@author: MuziLi
@contact: 1537080775@qq.com
@file: huaweicloudsdk.py
@time: 2023-05-16 22:36
@SoftWare: PyCharm
'''
# pip install huaweicloudsdkcore~=3.0.93 huaweicloudsdkocr~=3.0.94
from huaweicloudsdkcore.auth.credentials import BasicCredentials
from huaweicloudsdkcore.exceptions import exceptions
from huaweicloudsdkocr.v1 import *
from huaweicloudsdkocr.v1.region.ocr_region import OcrRegion
def RecognizeInvoiceVerification(check_code, issue_date, number, code):
ak = "<YOUR AK>"
sk = "<YOUR SK>"
credentials = BasicCredentials(ak, sk)
client = OcrClient.new_builder().with_credentials(credentials).with_region(OcrRegion.value_of("cn-north-4")).build()
try:
request = RecognizeInvoiceVerificationRequest()
request.body = InvoiceVerificationRequestBody(
check_code=check_code,
issue_date=issue_date,
number=number,
code=code
)
response = client.recognize_invoice_verification(request)
return response.result
except exceptions.ClientRequestException as e:
data = {
"result_code": e.status_code,
"request_id": e.request_id,
"error_msg": e.error_msg,
}
return data
视图:views.py
# Create your views here.
from rest_framework.decorators import action
from dvadmin.utils.json_response import DetailResponse
from dvadmin.utils.viewset import CustomModelViewSet
from .huaweicloudsdk import RecognizeInvoiceVerification
from .models import InvoiceModel
from .serializers import InvoiceSerializer, InvoiceCreateUpdateSerializer
class InvoiceViewSet(CustomModelViewSet):
"""
list:查询
create:新增
update:修改
retrieve:单例
destroy:删除
"""
queryset = InvoiceModel.objects.all()
serializer_class = InvoiceSerializer
create_serializer_class = InvoiceCreateUpdateSerializer
update_serializer_class = InvoiceCreateUpdateSerializer
# # 导入
# import_serializer_class = InvoiceImportSerializer
# import_field_dict = {
# 'code': "表编号",
# 'name': '表描述',
# 'where': '默认条件'
# }
# # 导出
# export_serializer_class = InvoiceImportSerializer
# export_field_label = {
# 'code': "表编号",
# 'name': '表描述',
# 'where': '默认条件'
# }
@action(methods=["POST"], detail=False)
def inspection(self, request, *args, **kwargs):
"""
校验发票
"""
code = request.data.get("code", None)
number = request.data.get("number", None)
issue_date = request.data.get("issue_date", None)
check_code = request.data.get("check_code", None)
queryset = self.queryset.filter(code=code, number=number)
if queryset.count() > 0:
huawei = {
"result_code": 4000,
"error_msg": "发票号码 + 发票号码 已存在,请勿重复提交",
}
else:
huawei = RecognizeInvoiceVerification(check_code, issue_date, number, code)
return DetailResponse(data=huawei, msg="请求成功")
路由:urls.py
from rest_framework.routers import SimpleRouter
from .views import InvoiceViewSet
router = SimpleRouter()
router.register("invoice", InvoiceViewSet)
urlpatterns = [
]
urlpatterns += router.urls
注册
APP注册
在
application/settings.py文件中注册创建的app应用
# application/settings.py
INSTALLED_APPS = [
"django.contrib.auth",
"django.contrib.contenttypes",
"django.contrib.sessions",
"django.contrib.messages",
"django.contrib.staticfiles",
"django_comment_migrate",
...,
'apps.invoice'
]
路由注册
在
application/urls.py注册路由
# application/urls.py
urlpatterns = (
[
...,
path("api/system/", include("dvadmin.system.urls")),
...,
path("api/invoice/", include("apps.invoice.urls")), # 发票检验
]
...
)
迁移数据库
- 生成迁移文件:
python3 manage.py makemigrations - 迁移数据库:
python3 manage.py migrate - 启动后端:
python3 manage.py runserver 0.0.0.0:8000 - 后端API地址:
http://127.0.0.1:8000/
前端项目
在views新建APPS文件夹,在APPS文件夹下新建 invocie 文件夹
接口文件:api.js
/*
* @文件介绍: 发票校验接口
*/
import { request, downloadFile } from '@/api/service';
export const urlPrefix = '/api/invoice/invoice/';
/**
* 列表查询
* @param {*} query
* @returns
*/
export function GetList(query) {
return request({
url: urlPrefix,
method: 'get',
params: { ...query },
});
}
/**
* 单例查询
* @param {*} id
* @returns
*/
export function GetObj(id) {
return request({
url: urlPrefix + id + '/',
method: 'get',
});
}
/**
* 新增
* @param {*} obj
* @returns
*/
export function AddObj(obj) {
return request({
url: urlPrefix,
method: 'post',
data: obj,
});
}
/**
* 修改
* @param {*} obj
* @returns
*/
export function UpdateObj(obj) {
return request({
url: urlPrefix + obj.id + '/',
method: 'put',
data: obj,
});
}
/**
* 删除
* @param {*} id
* @returns
*/
export function DelObj(id) {
return request({
url: urlPrefix + id + '/',
method: 'delete',
data: { id },
});
}
/**
* 批量删除
* @param {*} keys
* @returns
*/
export function BatchDel(keys) {
return request({
url: urlPrefix + 'multiple_delete/',
method: 'delete',
data: { keys },
});
}
/**
* 导出
* @param params
*/
export function ExportData(params) {
return downloadFile({
url: urlPrefix + 'export_data/',
params: params,
method: 'get',
});
}
配置文件:crud.js
/*
* @文件介绍: 发票校验配置文件
*/
import { request } from '@/api/service';
import XEUtils from 'xe-utils';
export const crudOptions = (vm) => {
return {
pageOptions: {
compact: true,
},
options: {
height: '100%',
rowKey: 'id',
},
formOptions: {
defaultSpan: 12, // 默认的表单 span
fullscreen: true,
size: 'mini',
labelWidth: '150px',
maxHeight: true, // 对话框内部显示滚动条
},
pageOptions: {
export: { local: false }, // 导出
},
viewOptions: {
componentType: 'row',
},
selectionRow: {
align: 'center',
width: 40,
},
columns: [
{
title: '校验真伪',
key: 'inspection',
type: 'button',
form: {
valueChange(key, value, form, others) {
const refForm = vm.getD2Crud().$refs.form;
refForm.validate((valid) => {
if (valid) {
request({
url: '/api/invoice/invoice/inspection/',
method: 'post',
data: form,
}).then((res) => {
if (res.data.result_code === '1000') {
const data = res.data;
data.issue_date = `${data.issue_date.slice(0, 4)}-${data.issue_date.slice(4, 6)}-${data.issue_date.slice(6, 8)}`;
XEUtils.merge(form, res.data);
} else {
vm.$notify.error({ title: '提示', message: res.data });
XEUtils.objectEach(form, (value, key) => {
if (!['code', 'number', 'issue_date', 'check_code'].includes) {
form[key] = undefined;
}
});
}
});
} else {
return false;
}
});
},
},
},
{
title: '录入日期',
key: 'input_date',
type: 'date',
form: {
value: XEUtils.toDateString(XEUtils.now(), 'yyyy-MM-dd'),
component: {
clearable: true,
props: {
valueFormat: 'yyyy-MM-dd',
},
},
},
},
{
title: '发票类型',
key: 'type',
type: 'select',
dict: {
// 数据字典配置, 供select等组件通过value匹配label
data: [
// 本地数据字典,若data为null,则通过http请求获取远程数据字典
{ value: 'vat_special', label: '增值税专用发票' },
{ value: 'vat_normal', label: '增值税普通发票' },
{ value: 'vat_normal_roll', label: '增值税普通发票(卷式)' },
{ value: 'vat_special_electronic', label: '增值税电子专用发票' },
{ value: 'vat_normal_electronic', label: '增值税电子普通发票' },
{ value: 'vat_normal_electronic_toll', label: '增值税电子普通发票(通行费)' },
{ value: 'blockchain_electronic', label: '区块链电子发票' },
{ value: 'fully_digitalized_special_electronic', label: '全电专用发票' },
{ value: 'fully_digitalized_normal_electronic', label: '全电普通发票' },
],
},
form: {
component: {
clearable: true,
},
},
},
{
title: '发票代码',
key: 'code',
search: {
disabled: false,
},
form: {
helper: {
render(h) {
return <el-alert type="warning">发票验真时必填</el-alert>;
},
},
rules: [{ required: true, message: '请输入发票代码', trigger: 'blur' }],
component: {
clearable: true,
},
},
},
{
title: '发票号码',
key: 'number',
search: {
disabled: false,
},
form: {
helper: {
render(h) {
return <el-alert type="warning">发票验真时必填</el-alert>;
},
},
rules: [{ required: true, message: '请输入发票号码', trigger: 'blur' }],
component: {
clearable: true,
},
},
},
{
title: '开票日期',
key: 'issue_date',
type: 'date',
form: {
helper: {
render(h) {
return <el-alert type="warning">发票验真时必填</el-alert>;
},
},
rules: [{ required: true, message: '请输入开票日期', trigger: 'blur' }],
component: {
clearable: true,
props: {
format: 'yyyy-MM-dd',
valueFormat: 'yyyy-MM-dd',
},
},
},
},
{
title: '校验码',
key: 'check_code',
form: {
helper: {
render(h) {
return <el-alert type="warning">发票验真时必填,需要校验码后六位</el-alert>;
},
},
rules: [{ required: true, message: '请输入校验码', trigger: 'blur' }],
component: {
clearable: true,
},
},
},
{
title: '机器编号',
key: 'machine_number',
form: {
component: {
clearable: true,
},
},
},
{
title: '发票状态',
key: 'status',
type: 'select',
dict: {
// 数据字典配置, 供select等组件通过value匹配label
data: [
// 本地数据字典,若data为null,则通过http请求获取远程数据字典
{ value: 'valid', label: '正常' },
{ value: 'invalidated', label: '已作废' },
{ value: 'reversed', label: '已红冲' },
],
},
form: {
component: {
clearable: true,
},
},
},
{
title: '合计金额(不含税)',
key: 'subtotal_amount',
type: 'number',
},
{
title: '合计税额',
key: 'subtotal_tax',
type: 'number',
},
{
title: '价税合计(小写)',
key: 'total',
type: 'number',
},
{
title: '价税合计(小写)',
key: 'total',
type: 'number',
},
{
title: '价税合计(大写)',
key: 'total_in_words',
form: {
component: {
clearable: true,
},
},
},
{
title: '备注',
key: 'remarks',
form: {
component: {
clearable: true,
},
},
},
{
title: '收款人',
key: 'receiver',
form: {
component: {
clearable: true,
},
},
},
{
title: '开票人',
key: 'issuer',
form: {
component: {
clearable: true,
},
},
},
{
title: '复核人',
key: 'reviewer',
form: {
component: {
clearable: true,
},
},
},
{
title: '购买方名称',
key: 'buyer_name',
form: {
component: {
clearable: true,
},
},
},
{
title: '购买方纳税人识别号',
key: 'buyer_id',
form: {
component: {
clearable: true,
},
},
},
{
title: '购买方地址、电话',
key: 'buyer_address',
form: {
component: {
clearable: true,
},
},
},
{
title: '购买方开户行及账号',
key: 'buyer_bank',
form: {
component: {
clearable: true,
},
},
},
{
title: '销售方名称',
key: 'seller_name',
form: {
component: {
clearable: true,
},
},
},
{
title: '销售方纳税人识别号',
key: 'seller_id',
form: {
component: {
clearable: true,
},
},
},
{
title: '销售方地址、电话',
key: 'seller_address',
form: {
component: {
clearable: true,
},
},
},
{
title: '销售方开户行及账号',
key: 'seller_bank',
form: {
component: {
clearable: true,
},
},
},
{
title: '通行费发票返回信息',
key: 'deductible_toll',
type: 'select',
dict: {
// 数据字典配置, 供select等组件通过value匹配label
data: [
// 本地数据字典,若data为null,则通过http请求获取远程数据字典
{ value: 'Y', label: '可抵扣通行费' },
{ value: 'N', label: '不可抵扣通行费' },
],
},
form: {
component: {
clearable: true,
},
},
},
].concat(
vm.commonEndColumns({
description: { showTable: false, showForm: false },
create_datetime: { showTable: false },
update_datetime: { showTable: false },
})
),
formGroup: {
//表单分组
type: 'collapse', // tab暂未实现
accordion: false,
groups: {
//分组
base: {
title: '基础信息', //分组标题
disabled: true, //禁止展开或收起
columns: ['input_date', 'type', 'code', 'number', 'issue_date', 'check_code', 'status', 'inspection'], //该组内包含的字段列表
},
amout: {
title: '金额信息',
columns: ['subtotal_amount', 'subtotal_tax', 'total', 'total_in_words'],
},
buyer: {
title: '购买方信息',
columns: ['buyer_name', 'buyer_id', 'buyer_address', 'buyer_bank'],
},
seller: {
title: '销售方信息',
columns: ['seller_name', 'seller_id', 'seller_address', 'seller_bank'],
},
other: {
title: '其他信息',
columns: ['machine_number', 'receiver', 'issuer', 'reviewer', 'deductible_toll', 'remarks'],
},
},
},
};
};
页面文件:index.vue
<!--
* @文件介绍:发票校验管理
-->
<template>
<d2-container :class="{ 'page-compact': crud.pageOptions.compact }">
<template slot="header">班级管理</template>
<d2-crud-x ref="d2Crud" v-bind="_crudProps" v-on="_crudListeners">
<div slot="header">
<crud-search ref="search" :options="crud.searchOptions" @submit="handleSearch" />
<el-button-group>
<el-button size="small" type="primary" @click="addRow"> <i class="el-icon-plus" /> 新增 </el-button>
</el-button-group>
<crud-toolbar v-bind="_crudToolbarProps" v-on="_crudToolbarListeners" />
</div>
<span slot="PaginationPrefixSlot" class="prefix">
<el-button
class="square"
size="mini"
title="批量删除"
@click="batchDelete"
icon="el-icon-delete"
:disabled="!multipleSelection || multipleSelection.length == 0"
/>
</span>
</d2-crud-x>
</d2-container>
</template>
<script>
import * as api from './api';
import { crudOptions } from './crud';
import { d2CrudPlus } from 'd2-crud-plus';
export default {
name: 'invoice',
mixins: [d2CrudPlus.crud],
data() {
return {
api: api.urlPrefix,
};
},
methods: {
getCrudOptions() {
return crudOptions(this);
},
pageRequest(query) {
return api.GetList(query);
},
addRequest(row) {
return api.AddObj(row);
},
updateRequest(row) {
return api.UpdateObj(row);
},
delRequest(row) {
return api.DelObj(row.id);
},
batchDelRequest(ids) {
return api.BatchDel(ids);
},
doServerExport(context) {
return api.ExportData({ ...context.search });
},
handleRowAdd(row, done) {
this.crud.formOptions.saveLoading = true;
row = this._unFlatData(row);
this.doValueResolve(row);
this.addBefore(row);
const promise = this.addRequest(row);
if (promise == null || !(promise instanceof Promise)) {
this.crud.formOptions.saveLoading = false;
return;
}
return promise
.then(() => {
this.showAddMessage({ row });
return this.addAfter(row);
})
.catch((error) => {
console.error('error', error);
})
.finally(() => {
this.crud.formOptions.saveLoading = false;
});
},
},
};
</script>
<style>
.el-date-editor.el-input {
width: 100%;
}
.el-input-number--mini {
width: 100%;
}
</style>