815cbf9d8c
- 更新 .gitignore:全面覆盖环境变量、数据库、日志、缓存、上传文件 - 移除误跟踪的 server/venv/、crm_data.db、.env 文件 - 新增 server/.env.example 模板 - 新增合同管理、利润核算、AI教练等功能模块 - 新增 Playwright e2e 测试套件 - 前后端多项功能升级和 bug 修复
230 lines
7.0 KiB
Python
230 lines
7.0 KiB
Python
"""
|
|
销项发票 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.models.sys import SysUser
|
|
from app.models.crm import CrmCustomer
|
|
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,
|
|
company_id: uuid.UUID | None = None,
|
|
) -> 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} 已存在")
|
|
|
|
kwargs: dict = dict(
|
|
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,
|
|
)
|
|
if company_id is not None:
|
|
kwargs["company_id"] = company_id
|
|
inv = FinSalesInvoice(**kwargs)
|
|
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,
|
|
company_id: uuid.UUID | None = None,
|
|
) -> SalesInvoiceListResponse:
|
|
conditions = [FinSalesInvoice.is_deleted.is_(False)]
|
|
if company_id:
|
|
conditions.append(FinSalesInvoice.company_id == company_id)
|
|
|
|
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
|