"""R10 适配器:收入与成本跨期匹配。 源明细:SrcRevenueRecognition / SrcCostAmortization 映射到:MetricEvent(收入确认/成本摊销时序) + Entity(CONTRACT) 关联补强 """ from __future__ import annotations import datetime as dt import uuid from sqlalchemy.orm import Session from app.datahub.graph_repo import upsert_entity from app.datahub.models import MetricEvent from app.datahub.ontology import EntityType from app.datahub.staging import SrcCostAmortization, SrcRevenueRecognition from app.ingest.base import BaseAdapter, IngestResult from app.ingest.registry import register_adapter @register_adapter class RevenueRecognitionAdapter(BaseAdapter): """SrcRevenueRecognition → MetricEvent(收入确认时序)。""" source_system = "FIN" staging_table = "src_revenue_recognition" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcRevenueRecognition) if data_version_id: query = query.filter(SrcRevenueRecognition.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: # 确保合同实体存在 if row.contract_no: upsert_entity( session, entity_type=EntityType.CONTRACT, business_key=row.contract_no, data_version_id=data_version_id, ) if row.recognition_date: event_time = dt.datetime.combine( row.recognition_date, dt.time.min, tzinfo=dt.timezone.utc ) event = MetricEvent( event_time=event_time, subject_type="contract", subject_key=row.contract_no or row.voucher_no, metric_name="revenue_recognition", metric_value=row.recognition_amount, attributes={ "voucher_no": row.voucher_no, "billing_mode": row.billing_mode, "period_start": str(row.period_start) if row.period_start else None, "period_end": str(row.period_end) if row.period_end else None, "prepaid_flag": row.prepaid_flag, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) result.row_count += 1 except Exception: result.error_count += 1 return result @register_adapter class CostAmortizationAdapter(BaseAdapter): """SrcCostAmortization → MetricEvent(成本摊销时序)。""" source_system = "FIN" staging_table = "src_cost_amortization" def ingest( self, session: Session, data_version_id: uuid.UUID | None = None, batch_size: int = 1000, ) -> IngestResult: result = IngestResult() query = session.query(SrcCostAmortization) if data_version_id: query = query.filter(SrcCostAmortization.data_version_id == data_version_id) rows = query.limit(batch_size).all() for row in rows: try: if row.contract_no: upsert_entity( session, entity_type=EntityType.CONTRACT, business_key=row.contract_no, data_version_id=data_version_id, ) if row.amortization_date: event_time = dt.datetime.combine( row.amortization_date, dt.time.min, tzinfo=dt.timezone.utc ) event = MetricEvent( event_time=event_time, subject_type="contract", subject_key=row.contract_no or row.voucher_no, metric_name="cost_amortization", metric_value=row.amortization_amount, attributes={ "voucher_no": row.voucher_no, "cost_type": row.cost_type, "total_periods": row.total_periods, "current_period": row.current_period, "delivery_date": str(row.delivery_date) if row.delivery_date else None, "acceptance_date": str(row.acceptance_date) if row.acceptance_date else None, }, data_version_id=data_version_id, ) session.add(event) result.metric_events.append(event) result.row_count += 1 except Exception: result.error_count += 1 return result