v0.1.0: CRM/ERP 系统内测版本 - 安全加固完成

- Docker bridge 网络隔离(8000 端口封死)
- Gunicorn 4 Worker 多进程
- Alembic 数据库迁移基线
- 日志轮转 20m×3
- JWT 密钥 + DB 密码 + CORS 收紧
- 3-2-1 备份链路(NAS + R740-B 冷备)
- 连接池 pool_pre_ping + pool_recycle=3600
This commit is contained in:
hankin
2026-03-16 07:31:37 +00:00
commit 423baff73b
2578 changed files with 824643 additions and 0 deletions
@@ -0,0 +1,220 @@
"""
销项发票 Service 层 — CRUD + 多条件查询 + 导出
"""
from __future__ import annotations
import io
import uuid
from datetime import date, datetime
from sqlalchemy import and_, func, select, update
from sqlalchemy.ext.asyncio import AsyncSession
from app.core.exceptions import BizException, NotFoundException
from app.models.finance import FinSalesInvoice
from app.schemas.auth import CurrentUserPayload
from app.schemas.sales_invoice import (
SalesInvoiceCreate,
SalesInvoiceListResponse,
SalesInvoiceResponse,
SalesInvoiceUpdate,
)
def _to_response(inv: FinSalesInvoice) -> SalesInvoiceResponse:
return SalesInvoiceResponse(
id=inv.id,
issuer=inv.issuer,
receiver_customer_id=inv.receiver_customer_id,
customer_name=inv.receiver_customer.name if inv.receiver_customer else None,
invoice_number=inv.invoice_number,
amount=float(inv.amount or 0),
billing_date=inv.billing_date,
payment_status=inv.payment_status,
payment_date=inv.payment_date,
payment_amount=float(inv.payment_amount or 0),
remark=inv.remark,
created_by=inv.created_by,
creator_name=inv.creator.real_name if inv.creator else None,
created_at=inv.created_at,
updated_at=inv.updated_at,
)
async def create_invoice(
db: AsyncSession,
user: CurrentUserPayload,
body: SalesInvoiceCreate,
) -> SalesInvoiceResponse:
# 检查发票号唯一性
existing = (await db.execute(
select(func.count()).select_from(FinSalesInvoice).where(
FinSalesInvoice.invoice_number == body.invoice_number,
FinSalesInvoice.is_deleted.is_(False),
)
)).scalar()
if existing:
raise BizException(message=f"发票号 {body.invoice_number} 已存在")
inv = FinSalesInvoice(
issuer=body.issuer,
receiver_customer_id=body.receiver_customer_id,
invoice_number=body.invoice_number,
amount=body.amount,
billing_date=body.billing_date,
remark=body.remark,
created_by=user.user_id,
)
db.add(inv)
await db.commit()
await db.refresh(inv)
return _to_response(inv)
async def list_invoices(
db: AsyncSession,
page: int = 1,
size: int = 20,
customer_name: str | None = None,
invoice_number: str | None = None,
payment_status: str | None = None,
start_date: date | None = None,
end_date: date | None = None,
) -> SalesInvoiceListResponse:
conditions = [FinSalesInvoice.is_deleted.is_(False)]
if invoice_number:
conditions.append(FinSalesInvoice.invoice_number.ilike(f"%{invoice_number}%"))
if payment_status:
conditions.append(FinSalesInvoice.payment_status == payment_status)
if start_date:
conditions.append(FinSalesInvoice.billing_date >= start_date)
if end_date:
conditions.append(FinSalesInvoice.billing_date <= end_date)
where = and_(*conditions) if conditions else True
total = (await db.execute(
select(func.count()).select_from(FinSalesInvoice).where(where)
)).scalar() or 0
stmt = (
select(FinSalesInvoice)
.where(where)
.order_by(FinSalesInvoice.billing_date.desc())
.offset((page - 1) * size)
.limit(size)
)
invoices = (await db.execute(stmt)).scalars().all()
items = [_to_response(inv) for inv in invoices]
# 如果有客户名称筛选,在 Python 层过滤(因为是 join 字段)
if customer_name:
items = [i for i in items if customer_name.lower() in (i.customer_name or "").lower()]
total = len(items)
return SalesInvoiceListResponse(
total=total,
items=items,
page=page,
size=size,
)
async def get_invoice(
db: AsyncSession,
invoice_id: uuid.UUID,
) -> SalesInvoiceResponse:
stmt = select(FinSalesInvoice).where(
FinSalesInvoice.id == invoice_id,
FinSalesInvoice.is_deleted.is_(False),
)
inv = (await db.execute(stmt)).scalar_one_or_none()
if inv is None:
raise NotFoundException("发票不存在或已被删除")
return _to_response(inv)
async def update_invoice(
db: AsyncSession,
invoice_id: uuid.UUID,
body: SalesInvoiceUpdate,
) -> SalesInvoiceResponse:
stmt = select(FinSalesInvoice).where(
FinSalesInvoice.id == invoice_id,
FinSalesInvoice.is_deleted.is_(False),
)
inv = (await db.execute(stmt)).scalar_one_or_none()
if inv is None:
raise NotFoundException("发票不存在或已被删除")
update_data = body.model_dump(exclude_unset=True)
if not update_data:
raise BizException(message="未提供任何需要更新的字段")
update_data["updated_at"] = datetime.utcnow()
await db.execute(
update(FinSalesInvoice)
.where(FinSalesInvoice.id == invoice_id)
.values(**update_data)
)
await db.commit()
updated = (await db.execute(
select(FinSalesInvoice).where(FinSalesInvoice.id == invoice_id)
)).scalar_one()
return _to_response(updated)
async def export_invoices(
db: AsyncSession,
start_date: date | None = None,
end_date: date | None = None,
) -> io.BytesIO:
"""导出指定时间段的发票汇总及回款追踪表"""
from openpyxl import Workbook
conditions = [FinSalesInvoice.is_deleted.is_(False)]
if start_date:
conditions.append(FinSalesInvoice.billing_date >= start_date)
if end_date:
conditions.append(FinSalesInvoice.billing_date <= end_date)
stmt = (
select(FinSalesInvoice)
.where(and_(*conditions))
.order_by(FinSalesInvoice.billing_date.desc())
)
invoices = (await db.execute(stmt)).scalars().all()
wb = Workbook()
ws = wb.active
ws.title = "发票汇总及回款追踪"
ws.append([
"发票号", "开票方", "受票客户", "票面金额",
"开票日期", "回款状态", "已回款金额", "回款日期", "备注"
])
for inv in invoices:
ws.append([
inv.invoice_number,
inv.issuer,
inv.receiver_customer.name if inv.receiver_customer else "",
float(inv.amount or 0),
inv.billing_date.isoformat() if inv.billing_date else "",
inv.payment_status,
float(inv.payment_amount or 0),
inv.payment_date.isoformat() if inv.payment_date else "",
inv.remark or "",
])
# 列宽
col_widths = [20, 25, 25, 15, 15, 12, 15, 15, 30]
for i, w in enumerate(col_widths, 1):
ws.column_dimensions[chr(64 + i)].width = w
buffer = io.BytesIO()
wb.save(buffer)
buffer.seek(0)
return buffer