from datetime import date from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.db.session import get_db from app.models.operations import AccountingPeriod, OverheadEntry, StatementSnapshot from app.schemas.operations import ( AccountingPeriodCreate, AccountingPeriodRead, CostAllocationGenerateResult, CostAllocationRead, OverheadEntryCreate, OverheadEntryRead, ProductMaterialCostBoardRead, StatementSnapshotCreate, StatementSnapshotRead, ) from app.services.auth import AuthContext, require_authenticated_user from app.services.operations import ( build_product_material_cost_board, build_statement_summary, generate_cost_allocations, get_cost_allocation_rows, get_overheads_query, get_periods_query, get_statement_snapshots_query, to_decimal, ) from app.services.sales_planning import code_token, next_doc_no router = APIRouter(dependencies=[Depends(require_authenticated_user)]) def build_statement_snapshot_no(db: Session, period: AccountingPeriod, statement_type: str, report_date: date) -> str: date_part = report_date.strftime("%Y%m%d") period_part = code_token(period.period_code, f"PER{period.id:04d}", 8) type_part = code_token(statement_type, "STM", 8) return next_doc_no(db, StatementSnapshot, "snapshot_no", f"FIN-NB-{date_part}-{period_part}-{type_part}") @router.get("/product-material-cost-board", response_model=ProductMaterialCostBoardRead) def get_product_material_cost_board( date_from: date | None = Query(default=None), date_to: date | None = Query(default=None), db: Session = Depends(get_db), ) -> ProductMaterialCostBoardRead: end_date = date_to or date.today() start_date = date_from or end_date.replace(day=1) return build_product_material_cost_board(db, date_from=start_date, date_to=end_date) @router.get("/periods", response_model=list[AccountingPeriodRead]) def list_accounting_periods( limit: int = Query(default=50, ge=1, le=200), db: Session = Depends(get_db), ) -> list[AccountingPeriodRead]: rows = db.execute(get_periods_query(limit=limit)).mappings().all() return [AccountingPeriodRead.model_validate(dict(row)) for row in rows] @router.post("/periods", response_model=AccountingPeriodRead) def create_accounting_period(payload: AccountingPeriodCreate, db: Session = Depends(get_db)) -> AccountingPeriodRead: period_code = f"{payload.year_no}{payload.month_no:02d}" period = AccountingPeriod( period_code=period_code, year_no=payload.year_no, month_no=payload.month_no, start_date=payload.start_date, end_date=payload.end_date, close_status=payload.close_status, closed_at=None, ) db.add(period) db.commit() row = db.execute(get_periods_query(limit=1).where(AccountingPeriod.id == period.id)).mappings().first() if not row: raise HTTPException(status_code=500, detail="财务期间创建后读取失败") return AccountingPeriodRead.model_validate(dict(row)) @router.get("/overheads", response_model=list[OverheadEntryRead]) def list_overhead_entries( limit: int = Query(default=100, ge=1, le=300), db: Session = Depends(get_db), ) -> list[OverheadEntryRead]: rows = db.execute(get_overheads_query(limit=limit)).mappings().all() return [OverheadEntryRead.model_validate(dict(row)) for row in rows] @router.post("/overheads", response_model=OverheadEntryRead) def create_overhead_entry( payload: OverheadEntryCreate, context: AuthContext = Depends(require_authenticated_user), db: Session = Depends(get_db), ) -> OverheadEntryRead: period = db.get(AccountingPeriod, payload.period_id) if not period: raise HTTPException(status_code=404, detail="财务期间不存在") entry = OverheadEntry( period_id=payload.period_id, overhead_type=payload.overhead_type, dept_id=payload.dept_id, amount=to_decimal(payload.amount, "0.01"), allocation_basis=payload.allocation_basis, description=payload.description, created_by=context.user.id, ) db.add(entry) db.commit() row = db.execute(get_overheads_query(limit=1).where(OverheadEntry.id == entry.id)).mappings().first() if not row: raise HTTPException(status_code=500, detail="制造费用录入后读取失败") return OverheadEntryRead.model_validate(dict(row)) @router.get("/cost-allocations", response_model=list[CostAllocationRead]) def list_cost_allocations( period_id: int | None = Query(default=None), limit: int = Query(default=100, ge=1, le=500), db: Session = Depends(get_db), ) -> list[CostAllocationRead]: return get_cost_allocation_rows(db, period_id=period_id, limit=limit) @router.post("/generate-cost-allocations/{period_id}", response_model=CostAllocationGenerateResult) def generate_period_cost_allocations(period_id: int, db: Session = Depends(get_db)) -> CostAllocationGenerateResult: period = db.get(AccountingPeriod, period_id) if not period: raise HTTPException(status_code=404, detail="财务期间不存在") rows = generate_cost_allocations(db, period_id) db.commit() return CostAllocationGenerateResult( period_id=period.id, period_code=period.period_code, generated_count=len(rows), rows=rows, ) @router.get("/statements", response_model=list[StatementSnapshotRead]) def list_statement_snapshots( limit: int = Query(default=100, ge=1, le=300), db: Session = Depends(get_db), ) -> list[StatementSnapshotRead]: rows = db.execute(get_statement_snapshots_query(limit=limit)).mappings().all() return [StatementSnapshotRead.model_validate(dict(row)) for row in rows] @router.post("/statements", response_model=StatementSnapshotRead) def create_statement_snapshot( payload: StatementSnapshotCreate, context: AuthContext = Depends(require_authenticated_user), db: Session = Depends(get_db), ) -> StatementSnapshotRead: period = db.get(AccountingPeriod, payload.period_id) if not period: raise HTTPException(status_code=404, detail="财务期间不存在") summary_json = build_statement_summary(db, payload.period_id, payload.report_date) snapshot = StatementSnapshot( snapshot_no=build_statement_snapshot_no(db, period, payload.statement_type, payload.report_date), period_id=payload.period_id, statement_type=payload.statement_type, report_date=payload.report_date, summary_json=summary_json, created_by=context.user.id, ) db.add(snapshot) db.commit() row = db.execute(get_statement_snapshots_query(limit=1).where(StatementSnapshot.id == snapshot.id)).mappings().first() if not row: raise HTTPException(status_code=500, detail="财务报表快照创建后读取失败") return StatementSnapshotRead.model_validate(dict(row))