from flask import Flask, request, jsonify, send_file
from flask_sqlalchemy import SQLAlchemy
from datetime import datetime
from io import BytesIO
from reportlab.lib.pagesizes import A4
from reportlab.pdfgen import canvas
from reportlab.lib.units import mm
app = Flask(name)
app.config['SQLALCHEMY_DATABASE_URI'] = 'sqlite:///tickets.db'
app.config['SQLALCHEMY_TRACK_MODIFICATIONS'] = False
db = SQLAlchemy(app)
数据模型
class Ticket(db.Model):
id = db.Column(db.Integer, primary_key=True)
event = db.Column(db.String(200), nullable=False)
buyer_name = db.Column(db.String(200), nullable=False)
buyer_tax_id = db.Column(db.String(100), nullable=True) # 纳税人识别号
quantity = db.Column(db.Integer, default=1)
unit_price = db.Column(db.Float, nullable=False)
total = db.Column(db.Float, nullable=False)
created_at = db.Column(db.DateTime, default=datetime.utcnow)
invoice_no = db.Column(db.String(100), nullable=True) # 发票号码(模拟)
db.create_all()
下单/购买票
@app.route('/buy', methods=['POST'])
def buy():
data = request.json
required = ['event', 'buyer_name', 'unit_price', 'quantity']
for r in required:
if r not in data:
return jsonify({'error': f'missing {r}'}), 400
quantity = int(data.get('quantity', 1))
unit_price = float(data.get('unit_price'))
total = round(quantity * unit_price, 2)
ticket = Ticket(
event=data['event'],
buyer_name=data['buyer_name'],
buyer_tax_id=data.get('buyer_tax_id'),
quantity=quantity,
unit_price=unit_price,
total=total
)
db.session.add(ticket)
db.session.commit()
return jsonify({'message': 'order created', 'ticket_id': ticket.id}), 201
查询订单
@app.route('/ticket/<int:ticket_id>', methods=['GET'])
def get_ticket(ticket_id):
t = Ticket.query.get_or_404(ticket_id)
return jsonify({
'id': t.id,
'event': t.event,
'buyer_name': t.buyer_name,
'buyer_tax_id': t.buyer_tax_id,
'quantity': t.quantity,
'unit_price': t.unit_price,
'total': t.total,
'created_at': t.created_at.isoformat(),
'invoice_no': t.invoice_no
})
生成发票(PDF)并返回
@app.route('/invoice/<int:ticket_id>', methods=['GET'])
def generate_invoice(ticket_id):
t = Ticket.query.get_or_404(ticket_id)
模拟分配发票号码(真实场景通过税控/第三方服务获取)
if not t.invoice_no:
t.invoice_no = f'INV-{datetime.utcnow().strftime("%Y%m%d%H%M%S")}-{t.id}'
db.session.commit()
buffer = BytesIO()
c = canvas.Canvas(buffer, pagesize=A4)
width, height = A4
绘制发票标题
c.setFont("Helvetica-Bold", 16)
c.drawCentredString(width / 2, height - 30 * mm, "增值税普通发票(示例)")
发票头部信息
c.setFont("Helvetica", 10)
left = 20 * mm
top = height - 40 * mm
line_h = 6 * mm
c.drawString(left, top, f"发票代码: 示例代码")
c.drawString(width - 90 * mm, top, f"发票号码: {t.invoice_no}")
top -= line_h
c.drawString(left, top, f"开票日期: {t.created_at.strftime('%Y-%m-%d %H:%M:%S')}")
c.drawString(width - 90 * mm, top, f"销售方: 示例售票方")
top -= line_h
c.drawString(left, top, f"购买方(抬头): {t.buyer_name}")
c.drawString(width - 90 * mm, top, f"纳税人识别号: {t.buyer_tax_id or '---'}")
表格表头
top -= 12 * mm
c.line(left, top + 4 * mm, width - left, top + 4 * mm)
c.setFont("Helvetica-Bold", 9)
c.drawString(left + 2 * mm, top, "货物或应税劳务、服务名称")
c.drawString(left + 95 * mm, top, "数量")
c.drawString(left + 115 * mm, top, "单价")
c.drawString(left + 145 * mm, top, "金额")
c.line(left, top - 3 * mm, width - left, top - 3 * mm)
表格内容(一个项目示例)
top -= 10 * mm
c.setFont("Helvetica", 9)
c.drawString(left + 2 * mm, top, t.event)
c.drawRightString(left + 120 * mm, top, str(t.quantity))
c.drawRightString(left + 150 * mm, top, f"{t.unit_price:.2f}")
c.drawRightString(width - left - 2 * mm, top, f"{t.total:.2f}")
合计
top -= 18 * mm
c.setFont("Helvetica-Bold", 10)
c.drawString(left, top, f"合计金额(小写): {t.total:.2f} 元")
top -= 6 * mm
c.drawString(left, top, f"合计金额(大写): {num_to_chinese_upper(t.total)}")
备注与签章位置(示例)
top -= 18 * mm
c.setFont("Helvetica", 9)
c.drawString(left, top, "备注: 本发票为系统示例发票,不作为税务正式凭证。")
c.drawString(width - 60 * mm, top, "开票人: 系统")
c.drawString(width - 30 * mm, top, "收款: ------")
c.showPage()
c.save()
buffer.seek(0)
return send_file(buffer, as_attachment=True, download_name=f"invoice_{t.id}.pdf", mimetype='application/pdf')
简单的数字转大写人民币(仅示例,覆盖到元,不含分)
CN_NUM = ["零","壹","贰","叁","肆","伍","陆","柒","捌","玖"]
CN_UNIT = ["","拾","佰","仟"]
CN_SECTION = ["","万","亿","兆"]
def num_to_chinese_upper(amount):
仅处理非负且小于兆级别的数,保留到元
amount = int(round(amount))
if amount == 0:
return "零元"
parts = []
unit_pos = 0
while amount > 0:
section = amount % 10000
if section != 0:
section_str = section_to_chinese(section) + CN_SECTION[unit_pos]
parts.insert(0, section_str)
amount //= 10000
unit_pos += 1
return "".join(parts) + "元"
def section_to_chinese(section):
str_ins = ""
unit_pos = 0
zero = True
while section > 0:
v = section % 10
if v == 0:
if not zero:
zero = True
str_ins = CN_NUM[0] + str_ins
else:
zero = False
str_ins = CN_NUM[v] + CN_UNIT[unit_pos] + str_ins
unit_pos += 1
section //= 10
return str_ins
if name == 'main':
app.run(debug=True)
使用说明(简短):
- 启动:python app.py
- 创建订单(POST /buy,JSON body 示例): {"event":"演唱会门票","buyer_name":"张三","buyer_tax_id":"123456789012345","quantity":2,"unit_price":150.0}
- 查询订单:GET /ticket/
- 下载发票:GET /invoice/(返回 PDF)
注意与扩展建议:
- 真实"正规发票"必须通过税务局/税控设备接口(增值税发票服务)开具并取得法律效力,本示例仅生成演示用 PDF 模板。
- 可扩展:增加用户认证、支付接口、发票项目多行、税额与税率计算、打印样式、发票电子数据格式(如 e-invoice XML)、日志与审计等。