学生成绩管理项目-番外篇(发票管理)

383 阅读6分钟

17-番外篇-发票管理_哔哩哔哩_bilibili

模块说明

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>