""" 销项发票 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