| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467 |
- """发票管理 API"""
- from typing import Optional, List
- from fastapi import APIRouter, Depends, HTTPException
- from fastapi.responses import FileResponse
- from sqlalchemy.orm import Session
- from pydantic import BaseModel, Field
- import os
- import tempfile
- from app.core.database import get_db
- from app.core.auth import get_current_user
- from app.core.response import success_response, error_response
- from app.core.utils import generate_id
- from app.models.models import BizInvoice
- from app.crud.base import CRUDBase
- router = APIRouter(prefix="/api/web/admin/invoice", tags=["发票管理"])
- class InvoiceCRUD(CRUDBase[BizInvoice]):
- """发票CRUD"""
- def __init__(self):
- super().__init__(BizInvoice)
- def create_invoice(
- self,
- db: Session,
- order_id: str,
- order_no: str,
- client_id: str,
- client_name: str,
- invoice_type: str,
- title: str,
- invoice_amount: float,
- tax_rate: float = 0,
- tax_amount: float = 0,
- tax_no: str = None,
- bank: str = None,
- bank_account: str = None,
- address: str = None,
- phone: str = None,
- remark: str = None,
- create_by: str = None,
- create_name: str = None
- ) -> BizInvoice:
- """创建发票"""
- # 生成发票号
- invoice_no = f"INV{datetime.now().strftime('%Y%m%d%H%M%S')}{random.randint(1000, 9999)}"
- data = {
- "id": generate_id(),
- "invoice_no": invoice_no,
- "order_id": order_id,
- "order_no": order_no,
- "client_id": client_id,
- "client_name": client_name,
- "invoice_type": invoice_type,
- "title": title,
- "invoice_amount": invoice_amount,
- "tax_rate": tax_rate,
- "tax_amount": tax_amount,
- "tax_no": tax_no,
- "bank": bank,
- "bank_account": bank_account,
- "address": address,
- "phone": phone,
- "remark": remark,
- "status": "pending",
- "create_by": create_by,
- "create_name": create_name
- }
- return self.create(db, data)
- def issue_invoice(
- self,
- db: Session,
- invoice_id: str,
- issued_by: str = None,
- issued_by_name: str = None,
- invoice_url: str = None
- ) -> BizInvoice:
- """开具发票"""
- invoice = self.get(db, invoice_id)
- if not invoice:
- return None
- invoice.status = "issued"
- invoice.issued_at = datetime.now()
- if issued_by:
- invoice.issued_by = issued_by
- if issued_by_name:
- invoice.issued_by_name = issued_by_name
- if invoice_url:
- invoice.invoice_url = invoice_url
- db.commit()
- db.refresh(invoice)
- return invoice
- def void_invoice(self, db: Session, invoice_id: str) -> BizInvoice:
- """作废发票"""
- invoice = self.get(db, invoice_id)
- if not invoice:
- return None
- invoice.status = "void"
- db.commit()
- db.refresh(invoice)
- return invoice
- def get_by_order(self, db: Session, order_id: str) -> List[BizInvoice]:
- """获取订单的所有发票"""
- return db.query(self.model).filter(
- self.model.order_id == order_id,
- self.model.is_del == 0
- ).all()
- from datetime import datetime
- import random
- invoice_crud = InvoiceCRUD()
- class InvoiceCreateRequest(BaseModel):
- """创建发票请求"""
- order_id: str = Field(..., description="订单ID")
- invoice_type: str = Field(..., description="发票类型: normal-普通发票, vat-增值税发票")
- title: str = Field(..., description="发票抬头")
- tax_rate: float = Field(0, description="税率")
- tax_no: Optional[str] = Field(None, description="税号")
- bank: Optional[str] = Field(None, description="开户银行")
- bank_account: Optional[str] = Field(None, description="银行账号")
- address: Optional[str] = Field(None, description="开票地址")
- phone: Optional[str] = Field(None, description="电话")
- remark: Optional[str] = Field(None, description="备注")
- @router.post("/create", response_model=dict)
- def create_invoice(
- request: InvoiceCreateRequest,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """创建发票"""
- try:
- # 获取订单信息
- from app.crud.order import order_crud
- order = order_crud.get(db, request.order_id)
- if not order:
- return error_response(404, "订单不存在")
- # 计算税额
- tax_amount = request.invoice_amount * request.tax_rate / 100 if request.tax_rate else 0
- invoice = invoice_crud.create_invoice(
- db=db,
- order_id=request.order_id,
- order_no=order.order_no,
- client_id=order.client_id,
- client_name=order.client_name,
- invoice_type=request.invoice_type,
- title=request.title,
- invoice_amount=request.invoice_amount,
- tax_rate=request.tax_rate,
- tax_amount=tax_amount,
- tax_no=request.tax_no,
- bank=request.bank,
- bank_account=request.bank_account,
- address=request.address,
- phone=request.phone,
- remark=request.remark,
- create_by=current_user.get("user_id"),
- create_name=current_user.get("real_name")
- )
- return success_response({
- "id": invoice.id,
- "invoice_no": invoice.invoice_no,
- "message": "发票创建成功"
- })
- except Exception as e:
- return error_response(500, f"创建发票失败: {str(e)}")
- @router.get("/list", response_model=dict)
- def get_invoice_list(
- page: int = 1,
- page_size: int = 20,
- invoice_no: str = None,
- client_name: str = None,
- invoice_type: str = None,
- status: str = None,
- start_date: str = None,
- end_date: str = None,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取发票列表"""
- try:
- query = db.query(invoice_crud.model).filter(invoice_crud.model.is_del == 0)
- if invoice_no:
- query = query.filter(invoice_crud.model.invoice_no.like(f"%{invoice_no}%"))
- if client_name:
- query = query.filter(invoice_crud.model.client_name.like(f"%{client_name}%"))
- if invoice_type:
- query = query.filter(invoice_crud.model.invoice_type == invoice_type)
- if status:
- query = query.filter(invoice_crud.model.status == status)
- if start_date:
- query = query.filter(invoice_crud.model.create_at >= start_date)
- if end_date:
- query = query.filter(invoice_crud.model.create_at <= end_date)
- total = query.count()
- items = query.order_by(invoice_crud.model.create_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return success_response({
- "list": [_to_response(item) for item in items],
- "total": total,
- "page": page,
- "page_size": page_size
- })
- except Exception as e:
- return error_response(500, f"获取发票列表失败: {str(e)}")
- @router.get("/detail/{invoice_id}", response_model=dict)
- def get_invoice_detail(
- invoice_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取发票详情"""
- try:
- invoice = invoice_crud.get(db, invoice_id)
- if not invoice:
- return error_response(404, "发票不存在")
- if invoice.is_del == 1:
- return error_response(404, "发票已删除")
- return success_response(_to_response(invoice))
- except Exception as e:
- return error_response(500, f"获取发票详情失败: {str(e)}")
- @router.post("/issue/{invoice_id}", response_model=dict)
- def issue_invoice(
- invoice_id: str,
- invoice_url: str = None,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """开具发票"""
- try:
- invoice = invoice_crud.issue_invoice(
- db=db,
- invoice_id=invoice_id,
- issued_by=current_user.get("user_id"),
- issued_by_name=current_user.get("real_name"),
- invoice_url=invoice_url
- )
- if not invoice:
- return error_response(404, "发票不存在")
- return success_response({"message": "发票开具成功"})
- except Exception as e:
- return error_response(500, f"开具发票失败: {str(e)}")
- @router.post("/void/{invoice_id}", response_model=dict)
- def void_invoice(
- invoice_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """作废发票"""
- try:
- invoice = invoice_crud.void_invoice(db, invoice_id)
- if not invoice:
- return error_response(404, "发票不存在")
- return success_response({"message": "发票作废成功"})
- except Exception as e:
- return error_response(500, f"作废发票失败: {str(e)}")
- @router.get("/download/{invoice_id}", response_model=dict)
- def download_invoice_pdf(
- invoice_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """下载发票PDF"""
- try:
- invoice = invoice_crud.get(db, invoice_id)
- if not invoice:
- return error_response(404, "发票不存在")
- if invoice.status != "issued":
- return error_response(400, "只有已开具的发票可以下载")
- # 如果有发票URL,直接返回
- if invoice.invoice_url:
- return success_response({
- "url": invoice.invoice_url,
- "filename": f"{invoice.invoice_no}.pdf"
- })
- # 生成PDF(模拟生成,实际需要调用PDF生成服务)
- pdf_path = _generate_invoice_pdf(invoice)
- if pdf_path:
- return success_response({
- "url": f"/files/{os.path.basename(pdf_path)}",
- "filename": f"{invoice.invoice_no}.pdf"
- })
- return error_response(500, "生成发票PDF失败")
- except Exception as e:
- return error_response(500, f"下载发票失败: {str(e)}")
- def _generate_invoice_pdf(invoice: BizInvoice) -> Optional[str]:
- """生成发票PDF(简化实现)"""
- try:
- from app.core.config import settings
- temp_dir = getattr(settings, 'TEMP_FILE_DIR', tempfile.gettempdir())
- os.makedirs(temp_dir, exist_ok=True)
- filename = f"{invoice.invoice_no}.pdf"
- filepath = os.path.join(temp_dir, filename)
- # 创建空文件作为占位符,实际应使用PDF库生成
- with open(filepath, 'w') as f:
- f.write(f"Invoice: {invoice.invoice_no}\n")
- f.write(f"Title: {invoice.title}\n")
- f.write(f"Amount: {invoice.invoice_amount}\n")
- return filepath
- except Exception:
- return None
- # =========================================
- # 小程序端发票接口
- # =========================================
- @router.get("/mini/client/invoices", response_model=dict)
- def get_client_invoices(
- page: int = 1,
- page_size: int = 20,
- status: str = None,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取客户发票列表"""
- try:
- if current_user.get("role") != "client":
- return error_response(403, "需要客户权限")
- client_id = current_user.get("user_id")
- query = db.query(invoice_crud.model).filter(
- invoice_crud.model.client_id == client_id,
- invoice_crud.model.is_del == 0
- )
- if status:
- query = query.filter(invoice_crud.model.status == status)
- total = query.count()
- items = query.order_by(invoice_crud.model.create_at.desc()).offset((page - 1) * page_size).limit(page_size).all()
- return success_response({
- "list": [_to_response(item) for item in items],
- "total": total,
- "page": page,
- "pageSize": page_size
- })
- except Exception as e:
- return error_response(500, f"获取发票列表失败: {str(e)}")
- @router.get("/mini/client/invoices/{invoice_id}", response_model=dict)
- def get_client_invoice_detail(
- invoice_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """获取客户发票详情"""
- try:
- if current_user.get("role") != "client":
- return error_response(403, "需要客户权限")
- invoice = invoice_crud.get(db, invoice_id)
- if not invoice:
- return error_response(404, "发票不存在")
- if invoice.client_id != current_user.get("user_id"):
- return error_response(403, "无权查看此发票")
- return success_response(_to_response(invoice))
- except Exception as e:
- return error_response(500, f"获取发票详情失败: {str(e)}")
- @router.get("/mini/client/invoices/{invoice_id}/download", response_model=dict)
- def download_client_invoice(
- invoice_id: str,
- db: Session = Depends(get_db),
- current_user: dict = Depends(get_current_user)
- ):
- """小程序端下载发票"""
- try:
- if current_user.get("role") != "client":
- return error_response(403, "需要客户权限")
- invoice = invoice_crud.get(db, invoice_id)
- if not invoice:
- return error_response(404, "发票不存在")
- if invoice.client_id != current_user.get("user_id"):
- return error_response(403, "无权下载此发票")
- if invoice.status != "issued":
- return error_response(400, "只有已开具的发票可以下载")
- if invoice.invoice_url:
- return success_response({
- "url": invoice.invoice_url,
- "filename": f"{invoice.invoice_no}.pdf"
- })
- return error_response(400, "发票文件不存在")
- except Exception as e:
- return error_response(500, f"下载发票失败: {str(e)}")
- def _to_response(invoice: BizInvoice) -> dict:
- """转换为响应格式"""
- return {
- "id": invoice.id,
- "invoice_no": invoice.invoice_no,
- "order_id": invoice.order_id,
- "order_no": invoice.order_no,
- "client_id": invoice.client_id,
- "client_name": invoice.client_name,
- "invoice_type": invoice.invoice_type,
- "title": invoice.title,
- "invoice_amount": float(invoice.invoice_amount) if invoice.invoice_amount else 0,
- "tax_rate": float(invoice.tax_rate) if invoice.tax_rate else 0,
- "tax_amount": float(invoice.tax_amount) if invoice.tax_amount else 0,
- "tax_no": invoice.tax_no,
- "bank": invoice.bank,
- "bank_account": invoice.bank_account,
- "address": invoice.address,
- "phone": invoice.phone,
- "status": invoice.status,
- "invoice_url": invoice.invoice_url,
- "issued_by": invoice.issued_by,
- "issued_by_name": invoice.issued_by_name,
- "issued_at": str(invoice.issued_at) if invoice.issued_at else None,
- "remark": invoice.remark,
- "create_at": str(invoice.create_at) if invoice.create_at else None
- }
|