from __future__ import annotations from datetime import date, datetime, timezone from decimal import Decimal from sqlalchemy import Boolean, Date, DateTime, ForeignKey, Integer, Numeric, String, Text, UniqueConstraint from sqlalchemy.orm import Mapped, mapped_column, relationship from app.core.db.common import CommonBase class BillingSettings(CommonBase): __tablename__ = "billing_settings" __table_args__ = ( UniqueConstraint("tenant_id", "branch_id", name="uq_billing_settings_tenant_branch"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) invoice_prefix: Mapped[str] = mapped_column(String(40), nullable=False, default="INV") next_invoice_no: Mapped[int] = mapped_column(Integer, nullable=False, default=1) padding: Mapped[int] = mapped_column(Integer, nullable=False, default=4) default_gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00")) default_tax_type: Mapped[str] = mapped_column(String(20), nullable=False, default="CGST_SGST") legal_name: Mapped[str | None] = mapped_column(String(200), nullable=True) gstin: Mapped[str | None] = mapped_column(String(20), nullable=True) pan: Mapped[str | None] = mapped_column(String(10), nullable=True) state_code: Mapped[str | None] = mapped_column(String(2), nullable=True) billing_address: Mapped[str | None] = mapped_column(Text, nullable=True) contact_email: Mapped[str | None] = mapped_column(String(255), nullable=True) contact_mobile: Mapped[str | None] = mapped_column(String(50), nullable=True) website_url: Mapped[str | None] = mapped_column(String(255), nullable=True) invoice_title: Mapped[str | None] = mapped_column(String(80), nullable=True) invoice_number_format: Mapped[str | None] = mapped_column(String(120), nullable=True) default_due_days: Mapped[int] = mapped_column(Integer, nullable=False, default=15) default_sac_code: Mapped[str | None] = mapped_column(String(20), nullable=True) bank_name: Mapped[str | None] = mapped_column(String(200), nullable=True) bank_account_name: Mapped[str | None] = mapped_column(String(200), nullable=True) bank_account_number: Mapped[str | None] = mapped_column(String(50), nullable=True) bank_ifsc: Mapped[str | None] = mapped_column(String(20), nullable=True) upi_id: Mapped[str | None] = mapped_column(String(100), nullable=True) bank_details: Mapped[str | None] = mapped_column(Text, nullable=True) payumoney_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) payumoney_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="TEST") payumoney_merchant_key: Mapped[str | None] = mapped_column(String(120), nullable=True) payumoney_merchant_salt: Mapped[str | None] = mapped_column(String(200), nullable=True) payumoney_merchant_id: Mapped[str | None] = mapped_column(String(120), nullable=True) payumoney_product_info: Mapped[str | None] = mapped_column(String(200), nullable=True) cashfree_enabled: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) cashfree_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="TEST") cashfree_client_id: Mapped[str | None] = mapped_column(String(180), nullable=True) cashfree_client_secret: Mapped[str | None] = mapped_column(String(240), nullable=True) cashfree_api_version: Mapped[str] = mapped_column(String(20), nullable=False, default="2023-08-01") cashfree_order_note: Mapped[str | None] = mapped_column(String(250), nullable=True) authorised_signatory_name: Mapped[str | None] = mapped_column(String(200), nullable=True) declaration: Mapped[str | None] = mapped_column(Text, nullable=True) terms: Mapped[str | None] = mapped_column(Text, nullable=True) footer_note: Mapped[str | None] = mapped_column(Text, nullable=True) created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) class BillingInvoiceGenerationBatch(CommonBase): __tablename__ = "billing_invoice_generation_batches" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) billing_period_from: Mapped[date] = mapped_column(Date, nullable=False, index=True) billing_period_to: Mapped[date] = mapped_column(Date, nullable=False, index=True) financial_year: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) frequency: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT_CREATED", index=True) selected_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) created_invoice_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) skipped_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) error_count: Mapped[int] = mapped_column(Integer, nullable=False, default=0) remarks: Mapped[str | None] = mapped_column(Text, nullable=True) generated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) generated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) invoices = relationship("BillingInvoice", back_populates="generation_batch") class BillingInvoice(CommonBase): __tablename__ = "billing_invoices" __table_args__ = ( UniqueConstraint("tenant_id", "invoice_no", name="uq_billing_invoices_tenant_invoice_no"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="RESTRICT"), nullable=False, index=True) engagement_id: Mapped[int | None] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="SET NULL"), nullable=True, index=True) generation_batch_id: Mapped[int | None] = mapped_column(ForeignKey("billing_invoice_generation_batches.id", ondelete="SET NULL"), nullable=True, index=True) invoice_no: Mapped[str] = mapped_column(String(60), nullable=False, index=True) invoice_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today, index=True) due_date: Mapped[date | None] = mapped_column(Date, nullable=True, index=True) billing_period_from: Mapped[date | None] = mapped_column(Date, nullable=True) billing_period_to: Mapped[date | None] = mapped_column(Date, nullable=True) financial_year: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) invoice_title: Mapped[str | None] = mapped_column(String(80), nullable=True) place_of_supply: Mapped[str | None] = mapped_column(String(120), nullable=True) reverse_charge: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) client_legal_name: Mapped[str | None] = mapped_column(String(200), nullable=True) client_trade_name: Mapped[str | None] = mapped_column(String(200), nullable=True) client_gstin: Mapped[str | None] = mapped_column(String(20), nullable=True) client_pan: Mapped[str | None] = mapped_column(String(20), nullable=True) client_billing_address: Mapped[str | None] = mapped_column(Text, nullable=True) client_state: Mapped[str | None] = mapped_column(String(100), nullable=True) client_state_code: Mapped[str | None] = mapped_column(String(2), nullable=True) client_email: Mapped[str | None] = mapped_column(String(255), nullable=True) client_mobile: Mapped[str | None] = mapped_column(String(50), nullable=True) tax_type: Mapped[str] = mapped_column(String(20), nullable=False, default="CGST_SGST") subtotal: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) discount_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) taxable_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) cgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) sgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) igst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) round_off: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) total_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) amount_received: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) tds_deducted: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) bank_charges: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) balance_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) amount_in_words: Mapped[str | None] = mapped_column(String(500), nullable=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="DRAFT", index=True) notes: Mapped[str | None] = mapped_column(Text, nullable=True) terms: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) approved_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) posted_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) client = relationship("Client") engagement = relationship("ClientServiceSubscription", foreign_keys=[engagement_id]) generation_batch = relationship("BillingInvoiceGenerationBatch", back_populates="invoices") lines = relationship("BillingInvoiceLine", back_populates="invoice", cascade="all, delete-orphan", order_by="BillingInvoiceLine.sort_order.asc()") payments = relationship("BillingPayment", back_populates="invoice", cascade="all, delete-orphan", order_by="BillingPayment.payment_date.desc(), BillingPayment.id.desc()") online_transactions = relationship("BillingOnlinePaymentTransaction", back_populates="invoice", cascade="all, delete-orphan", order_by="BillingOnlinePaymentTransaction.created_at_utc.desc(), BillingOnlinePaymentTransaction.id.desc()") class BillingPayment(CommonBase): __tablename__ = "billing_payments" __table_args__ = ( UniqueConstraint("tenant_id", "receipt_no", name="uq_billing_payments_tenant_receipt_no"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) invoice_id: Mapped[int] = mapped_column(ForeignKey("billing_invoices.id", ondelete="CASCADE"), nullable=False, index=True) client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="RESTRICT"), nullable=False, index=True) receipt_no: Mapped[str] = mapped_column(String(60), nullable=False, index=True) receipt_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today, index=True) payment_date: Mapped[date] = mapped_column(Date, nullable=False, default=date.today, index=True) financial_year: Mapped[str | None] = mapped_column(String(20), nullable=True, index=True) amount_received: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) tds_deducted: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) bank_charges: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) mode: Mapped[str] = mapped_column(String(30), nullable=False, default="BANK") reference_no: Mapped[str | None] = mapped_column(String(120), nullable=True) payment_gateway: Mapped[str | None] = mapped_column(String(50), nullable=True) gateway_transaction_id: Mapped[str | None] = mapped_column(String(120), nullable=True) remarks: Mapped[str | None] = mapped_column(Text, nullable=True) status: Mapped[str] = mapped_column(String(20), nullable=False, default="RECEIVED", index=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) invoice = relationship("BillingInvoice", back_populates="payments") client = relationship("Client") class BillingOnlinePaymentTransaction(CommonBase): __tablename__ = "billing_online_payment_transactions" __table_args__ = ( UniqueConstraint("tenant_id", "txnid", name="uq_billing_online_payment_tenant_txnid"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) invoice_id: Mapped[int] = mapped_column(ForeignKey("billing_invoices.id", ondelete="CASCADE"), nullable=False, index=True) client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="RESTRICT"), nullable=False, index=True) provider: Mapped[str] = mapped_column(String(40), nullable=False, default="PAYUMONEY", index=True) mode: Mapped[str] = mapped_column(String(20), nullable=False, default="TEST") txnid: Mapped[str] = mapped_column(String(80), nullable=False, index=True) amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) productinfo: Mapped[str | None] = mapped_column(String(250), nullable=True) firstname: Mapped[str | None] = mapped_column(String(120), nullable=True) email: Mapped[str | None] = mapped_column(String(255), nullable=True) phone: Mapped[str | None] = mapped_column(String(50), nullable=True) payu_payment_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True) cashfree_order_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True) cashfree_cf_order_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True) cashfree_payment_session_id: Mapped[str | None] = mapped_column(String(500), nullable=True) cashfree_payment_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True) webhook_event_id: Mapped[str | None] = mapped_column(String(120), nullable=True, index=True) bank_ref_num: Mapped[str | None] = mapped_column(String(120), nullable=True) mihpayid: Mapped[str | None] = mapped_column(String(120), nullable=True) status: Mapped[str] = mapped_column(String(30), nullable=False, default="INITIATED", index=True) gateway_status: Mapped[str | None] = mapped_column(String(80), nullable=True) response_hash: Mapped[str | None] = mapped_column(String(200), nullable=True) raw_response: Mapped[str | None] = mapped_column(Text, nullable=True) receipt_payment_id: Mapped[int | None] = mapped_column(ForeignKey("billing_payments.id", ondelete="SET NULL"), nullable=True, index=True) created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) completed_at_utc: Mapped[datetime | None] = mapped_column(DateTime(timezone=True), nullable=True) invoice = relationship("BillingInvoice", back_populates="online_transactions") client = relationship("Client") receipt_payment = relationship("BillingPayment", foreign_keys=[receipt_payment_id]) class BillingInvoiceLine(CommonBase): __tablename__ = "billing_invoice_lines" id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) invoice_id: Mapped[int] = mapped_column(ForeignKey("billing_invoices.id", ondelete="CASCADE"), nullable=False, index=True) service_id: Mapped[int | None] = mapped_column(ForeignKey("service_catalogues.id", ondelete="SET NULL"), nullable=True, index=True) engagement_id: Mapped[int | None] = mapped_column(ForeignKey("client_service_subscriptions.id", ondelete="SET NULL"), nullable=True, index=True) fee_group_id: Mapped[int | None] = mapped_column(ForeignKey("billing_fee_groups.id", ondelete="SET NULL"), nullable=True, index=True) description: Mapped[str] = mapped_column(String(500), nullable=False) sac_code: Mapped[str | None] = mapped_column(String(20), nullable=True) billing_period_from: Mapped[date | None] = mapped_column(Date, nullable=True) billing_period_to: Mapped[date | None] = mapped_column(Date, nullable=True) quantity: Mapped[Decimal] = mapped_column(Numeric(12, 2), nullable=False, default=Decimal("1.00")) rate: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) discount_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) taxable_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00")) cgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) sgst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) igst_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) line_total: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=1) invoice = relationship("BillingInvoice", back_populates="lines") service = relationship("ServiceCatalogue") class BillingFeeGroup(CommonBase): __tablename__ = "billing_fee_groups" __table_args__ = ( UniqueConstraint("tenant_id", "group_code", name="uq_billing_fee_groups_tenant_code"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) tenant_id: Mapped[int] = mapped_column(ForeignKey("tenants.id", ondelete="CASCADE"), nullable=False, index=True) branch_id: Mapped[int | None] = mapped_column(ForeignKey("branches.id", ondelete="SET NULL"), nullable=True, index=True) client_id: Mapped[int] = mapped_column(ForeignKey("clients.id", ondelete="CASCADE"), nullable=False, index=True) partner_id: Mapped[int | None] = mapped_column(ForeignKey("users.id", ondelete="SET NULL"), nullable=True, index=True) group_code: Mapped[str] = mapped_column(String(80), nullable=False, index=True) group_name: Mapped[str] = mapped_column(String(200), nullable=False) billing_mode: Mapped[str] = mapped_column(String(20), nullable=False, default="PACKAGE") frequency: Mapped[str] = mapped_column(String(20), nullable=False, default="Monthly") fee_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) gst_rate: Mapped[Decimal] = mapped_column(Numeric(5, 2), nullable=False, default=Decimal("18.00")) tax_type: Mapped[str] = mapped_column(String(20), nullable=False, default="CGST_SGST") effective_from: Mapped[date | None] = mapped_column(Date, nullable=True) effective_to: Mapped[date | None] = mapped_column(Date, nullable=True) auto_generate: Mapped[bool] = mapped_column(Boolean, nullable=False, default=False) is_active: Mapped[bool] = mapped_column(Boolean, nullable=False, default=True, index=True) notes: Mapped[str | None] = mapped_column(Text, nullable=True) created_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) updated_by_user_id: Mapped[int | None] = mapped_column(ForeignKey("users.id"), nullable=True) created_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), nullable=False) updated_at_utc: Mapped[datetime] = mapped_column(DateTime(timezone=True), default=lambda: datetime.now(timezone.utc), onupdate=lambda: datetime.now(timezone.utc), nullable=False) client = relationship("Client") services = relationship("BillingFeeGroupService", back_populates="fee_group", cascade="all, delete-orphan", order_by="BillingFeeGroupService.sort_order.asc()") class BillingFeeGroupService(CommonBase): __tablename__ = "billing_fee_group_services" __table_args__ = ( UniqueConstraint("fee_group_id", "service_id", name="uq_billing_fee_group_services_group_service"), ) id: Mapped[int] = mapped_column(Integer, primary_key=True, autoincrement=True) fee_group_id: Mapped[int] = mapped_column(ForeignKey("billing_fee_groups.id", ondelete="CASCADE"), nullable=False, index=True) service_id: Mapped[int] = mapped_column(ForeignKey("service_catalogues.id", ondelete="RESTRICT"), nullable=False, index=True) line_description: Mapped[str | None] = mapped_column(String(500), nullable=True) allocation_type: Mapped[str] = mapped_column(String(20), nullable=False, default="Included") line_amount: Mapped[Decimal] = mapped_column(Numeric(14, 2), nullable=False, default=Decimal("0.00")) percentage: Mapped[Decimal | None] = mapped_column(Numeric(5, 2), nullable=True) sort_order: Mapped[int] = mapped_column(Integer, nullable=False, default=1) fee_group = relationship("BillingFeeGroup", back_populates="services") service = relationship("ServiceCatalogue")