""" SAMMS Enterprise - Fixed Asset Registry Schemas Fixed asset management with depreciation and automated journal entries """ from datetime import datetime, date from typing import Optional, List, Dict, Any, Union, Literal from decimal import Decimal from enum import Enum from pydantic import BaseModel, Field, ConfigDict, field_validator, model_validator, computed_field from .base import ( BaseResponseSchema, TimestampMixin, PaginationParams, ListResponseBase, AuditMixin, CompanyScopedMixin ) # ==================== ENUMS ==================== class AssetStatus(str, Enum): """Asset lifecycle status""" DRAFT = "draft" PENDING_APPROVAL = "pending_approval" ACTIVE = "active" IN_USE = "in_use" UNDER_CONSTRUCTION = "under_construction" IDLE = "idle" HELD_FOR_SALE = "held_for_sale" DISPOSED = "disposed" FULLY_DEPRECIATED = "fully_depreciated" WRITTEN_OFF = "written_off" REVALUED = "revalued" IMPAIRED = "impaired" class AssetCategory(str, Enum): """Asset category types""" LAND = "land" BUILDING = "building" MACHINERY = "machinery" EQUIPMENT = "equipment" VEHICLE = "vehicle" FURNITURE_FIXTURES = "furniture_fixtures" COMPUTER_HARDWARE = "computer_hardware" COMPUTER_SOFTWARE = "computer_software" LEASEHOLD_IMPROVEMENTS = "leasehold_improvements" INTANGIBLE_ASSETS = "intangible_assets" CONSTRUCTION_IN_PROGRESS = "construction_in_progress" GOODWILL = "goodwill" PATENTS = "patents" TRADEMARKS = "trademarks" COPYRIGHTS = "copyrights" LICENSES = "licenses" NATURAL_RESOURCES = "natural_resources" BIOLOGICAL_ASSETS = "biological_assets" INVESTMENT_PROPERTY = "investment_property" RIGHT_OF_USE_ASSET = "right_of_use_asset" OTHER = "other" class DepreciationMethod(str, Enum): """Depreciation calculation methods""" STRAIGHT_LINE = "straight_line" DECLINING_BALANCE = "declining_balance" DOUBLE_DECLINING = "double_declining" SUM_OF_YEARS_DIGITS = "sum_of_years_digits" UNITS_OF_PRODUCTION = "units_of_production" NONE = "none" CUSTOM = "custom" class AssetValuationMethod(str, Enum): """Asset valuation methods""" HISTORICAL_COST = "historical_cost" REVALUATION = "revaluation" FAIR_VALUE = "fair_value" NET_REALIZABLE_VALUE = "net_realizable_value" PRESENT_VALUE = "present_value" class DisposalMethod(str, Enum): """Asset disposal methods""" SALE = "sale" SCRAPPING = "scrapping" DONATION = "donation" EXCHANGE = "exchange" THEFT = "theft" DESTRUCTION = "destruction" TRANSFER = "transfer" OTHER = "other" class ImpairmentStatus(str, Enum): """Asset impairment status""" NOT_IMPAIRED = "not_impaired" POTENTIALLY_IMPAIRED = "potentially_impaired" IMPAIRED = "impaired" REVERSED = "reversed" class MaintenanceType(str, Enum): """Types of maintenance""" PREVENTIVE = "preventive" CORRECTIVE = "corrective" PREDICTIVE = "predictive" EMERGENCY = "emergency" ROUTINE = "routine" OVERHAUL = "overhaul" class MaintenanceStatus(str, Enum): """Maintenance record status""" SCHEDULED = "scheduled" IN_PROGRESS = "in_progress" COMPLETED = "completed" CANCELLED = "cancelled" OVERDUE = "overdue" class InsuranceStatus(str, Enum): """Insurance policy status""" ACTIVE = "active" EXPIRED = "expired" CANCELLED = "cancelled" PENDING_RENEWAL = "pending_renewal" # ==================== ASSET CLASS ==================== class AssetClassBase(BaseModel): """Base schema for asset classes""" name: str = Field(..., min_length=1, max_length=200, description="Asset class name") code: str = Field(..., min_length=1, max_length=20, pattern=r"^[A-Z][A-Z0-9._-]*$", description="Unique code") description: Optional[str] = Field(None, max_length=2000, description="Description") category: AssetCategory = Field(..., description="Asset category") parent_class_id: Optional[int] = Field(None, description="Parent asset class ID") depreciation_method: DepreciationMethod = Field(default=DepreciationMethod.STRAIGHT_LINE, description="Default depreciation method") useful_life_years: int = Field(..., ge=0, le=100, description="Default useful life in years") useful_life_months: int = Field(default=0, ge=0, le=1199, description="Additional months") salvage_value_percentage: Decimal = Field(default=Decimal('0'), ge=0, le=100, description="Default salvage value %") depreciation_rate: Optional[Decimal] = Field(None, ge=0, le=100, description="Depreciation rate for declining balance") asset_account_id: int = Field(..., description="GL account for asset cost") accumulated_depreciation_account_id: int = Field(..., description="GL account for accumulated depreciation") depreciation_expense_account_id: int = Field(..., description="GL account for depreciation expense") impairment_account_id: Optional[int] = Field(None, description="GL account for impairment losses") gain_loss_account_id: Optional[int] = Field(None, description="GL account for disposal gain/loss") is_active: bool = Field(default=True, description="Whether class is active") requires_tracking: bool = Field(default=True, description="Requires individual asset tracking") requires_maintenance: bool = Field(default=False, description="Requires maintenance tracking") requires_insurance: bool = Field(default=False, description="Requires insurance tracking") custom_fields_schema: Optional[Dict[str, Any]] = Field(None, description="Custom fields definition") class AssetClassCreate(AssetClassBase): """Schema for creating asset classes""" pass class AssetClassUpdate(BaseModel): """Schema for updating asset classes""" name: Optional[str] = Field(None, min_length=1, max_length=200) description: Optional[str] = Field(None, max_length=2000) depreciation_method: Optional[DepreciationMethod] = None useful_life_years: Optional[int] = Field(None, ge=0, le=100) useful_life_months: Optional[int] = Field(None, ge=0, le=1199) salvage_value_percentage: Optional[Decimal] = Field(None, ge=0, le=100) depreciation_rate: Optional[Decimal] = Field(None, ge=0, le=100) asset_account_id: Optional[int] = None accumulated_depreciation_account_id: Optional[int] = None depreciation_expense_account_id: Optional[int] = None impairment_account_id: Optional[int] = None gain_loss_account_id: Optional[int] = None is_active: Optional[bool] = None requires_tracking: Optional[bool] = None requires_maintenance: Optional[bool] = None requires_insurance: Optional[bool] = None custom_fields_schema: Optional[Dict[str, Any]] = None class AssetClassResponse(AssetClassBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset class responses""" full_code: Optional[str] = Field(None, description="Full hierarchical code") total_assets: int = Field(default=0, description="Total assets in class") total_cost: Decimal = Field(default=Decimal('0'), description="Total asset cost") total_accumulated_depreciation: Decimal = Field(default=Decimal('0'), description="Total accumulated depreciation") total_net_book_value: Decimal = Field(default=Decimal('0'), description="Total net book value") model_config = ConfigDict(from_attributes=True) # ==================== ASSET LOCATION ==================== class AssetLocationBase(BaseModel): """Base schema for asset locations""" name: str = Field(..., min_length=1, max_length=200, description="Location name") code: str = Field(..., min_length=1, max_length=20, description="Unique location code") description: Optional[str] = Field(None, max_length=2000, description="Description") parent_location_id: Optional[int] = Field(None, description="Parent location ID") location_type: str = Field(default="building", description="Location type: building, floor, room, warehouse, site, etc.") address_line1: Optional[str] = Field(None, max_length=200, description="Address line 1") address_line2: Optional[str] = Field(None, max_length=200, description="Address line 2") city: Optional[str] = Field(None, max_length=100, description="City") state_province: Optional[str] = Field(None, max_length=100, description="State/Province") postal_code: Optional[str] = Field(None, max_length=20, description="Postal code") country: Optional[str] = Field(None, max_length=100, description="Country") latitude: Optional[Decimal] = Field(None, ge=-90, le=90, description="GPS latitude") longitude: Optional[Decimal] = Field(None, ge=-180, le=180, description="GPS longitude") manager_id: Optional[int] = Field(None, description="Responsible manager user ID") department_id: Optional[int] = Field(None, description="Owning department ID") is_active: bool = Field(default=True, description="Whether location is active") class AssetLocationCreate(AssetLocationBase): """Schema for creating asset locations""" pass class AssetLocationUpdate(BaseModel): """Schema for updating asset locations""" name: Optional[str] = Field(None, min_length=1, max_length=200) description: Optional[str] = Field(None, max_length=2000) address_line1: Optional[str] = Field(None, max_length=200) address_line2: Optional[str] = Field(None, max_length=200) city: Optional[str] = Field(None, max_length=100) state_province: Optional[str] = Field(None, max_length=100) postal_code: Optional[str] = Field(None, max_length=20) country: Optional[str] = Field(None, max_length=100) latitude: Optional[Decimal] = Field(None, ge=-90, le=90) longitude: Optional[Decimal] = Field(None, ge=-180, le=180) manager_id: Optional[int] = None department_id: Optional[int] = None is_active: Optional[bool] = None class AssetLocationResponse(AssetLocationBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset location responses""" full_path: Optional[str] = Field(None, description="Full hierarchical path") total_assets: int = Field(default=0, description="Total assets at location") total_asset_value: Decimal = Field(default=Decimal('0'), description="Total asset value at location") model_config = ConfigDict(from_attributes=True) # ==================== FIXED ASSET ==================== class FixedAssetBase(BaseModel): """Base schema for fixed assets""" asset_number: str = Field(..., min_length=1, max_length=50, description="Unique asset number") name: str = Field(..., min_length=1, max_length=200, description="Asset name") description: Optional[str] = Field(None, max_length=2000, description="Asset description") asset_class_id: int = Field(..., description="Asset class ID") category: AssetCategory = Field(..., description="Asset category") status: AssetStatus = Field(default=AssetStatus.ACTIVE, description="Asset status") # Acquisition Information acquisition_date: date = Field(..., description="Date of acquisition") acquisition_cost: Decimal = Field(..., ge=0, description="Original acquisition cost") acquisition_type: str = Field(default="purchase", description="How asset was acquired: purchase, construction, donation, exchange, transfer") vendor_id: Optional[int] = Field(None, description="Vendor ID if purchased") purchase_order_id: Optional[int] = Field(None, description="Related purchase order ID") invoice_id: Optional[int] = Field(None, description="Related invoice ID") invoice_number: Optional[str] = Field(None, max_length=50, description="Invoice number") # Depreciation Settings depreciation_method: DepreciationMethod = Field(default=DepreciationMethod.STRAIGHT_LINE, description="Depreciation method") useful_life_years: int = Field(..., ge=0, le=100, description="Useful life in years") useful_life_months: int = Field(default=0, ge=0, le=1199, description="Additional months") salvage_value: Decimal = Field(default=Decimal('0'), ge=0, description="Expected salvage value") depreciation_start_date: Optional[date] = Field(None, description="Depreciation start date") depreciation_end_date: Optional[date] = Field(None, description="Depreciation end date") # Current Values accumulated_depreciation: Decimal = Field(default=Decimal('0'), description="Total accumulated depreciation") net_book_value: Optional[Decimal] = Field(None, description="Current net book value") revaluation_reserve: Decimal = Field(default=Decimal('0'), description="Revaluation reserve balance") impairment_loss: Decimal = Field(default=Decimal('0'), description="Accumulated impairment losses") # Location and Custody location_id: Optional[int] = Field(None, description="Physical location ID") custodian_id: Optional[int] = Field(None, description="Custodian employee ID") department_id: Optional[int] = Field(None, description="Owning department ID") cost_center_id: Optional[int] = Field(None, description="Cost center ID") # Physical Details serial_number: Optional[str] = Field(None, max_length=100, description="Serial number") model_number: Optional[str] = Field(None, max_length=100, description="Model number") manufacturer: Optional[str] = Field(None, max_length=200, description="Manufacturer") brand: Optional[str] = Field(None, max_length=100, description="Brand name") color: Optional[str] = Field(None, max_length=50, description="Color") size: Optional[str] = Field(None, max_length=50, description="Size") weight: Optional[Decimal] = Field(None, description="Weight") weight_unit: Optional[str] = Field(None, max_length=10, description="Weight unit") dimensions: Optional[str] = Field(None, max_length=100, description="Dimensions") # Identification barcode: Optional[str] = Field(None, max_length=100, description="Barcode") rfid_tag: Optional[str] = Field(None, max_length=100, description="RFID tag") plate_number: Optional[str] = Field(None, max_length=50, description="Asset plate number") # Additional Information warranty_expiry_date: Optional[date] = Field(None, description="Warranty expiry date") insurance_policy_id: Optional[int] = Field(None, description="Insurance policy ID") lease_id: Optional[int] = Field(None, description="Related lease ID") project_id: Optional[int] = Field(None, description="Related project ID") # GL Integration asset_account_id: int = Field(..., description="GL account for asset cost") accumulated_depreciation_account_id: int = Field(..., description="GL account for accumulated depreciation") depreciation_expense_account_id: int = Field(..., description="GL account for depreciation expense") # Custom Data custom_fields: Optional[Dict[str, Any]] = Field(None, description="Custom field values") tags: Optional[List[str]] = Field(None, description="Tags for categorization") # Attachments image_url: Optional[str] = Field(None, max_length=500, description="Asset image URL") document_ids: Optional[List[int]] = Field(None, description="Related document IDs") notes: Optional[str] = Field(None, max_length=5000, description="Additional notes") class FixedAssetCreate(FixedAssetBase): """Schema for creating fixed assets""" auto_generate_depreciation: bool = Field(default=True, description="Auto-generate depreciation schedule") @model_validator(mode='after') def calculate_nbv(self): if self.net_book_value is None: self.net_book_value = self.acquisition_cost return self class FixedAssetUpdate(BaseModel): """Schema for updating fixed assets""" name: Optional[str] = Field(None, min_length=1, max_length=200) description: Optional[str] = Field(None, max_length=2000) status: Optional[AssetStatus] = None location_id: Optional[int] = None custodian_id: Optional[int] = None department_id: Optional[int] = None cost_center_id: Optional[int] = None serial_number: Optional[str] = Field(None, max_length=100) model_number: Optional[str] = Field(None, max_length=100) barcode: Optional[str] = Field(None, max_length=100) rfid_tag: Optional[str] = Field(None, max_length=100) warranty_expiry_date: Optional[date] = None insurance_policy_id: Optional[int] = None custom_fields: Optional[Dict[str, Any]] = None tags: Optional[List[str]] = None image_url: Optional[str] = Field(None, max_length=500) notes: Optional[str] = Field(None, max_length=5000) class FixedAssetResponse(FixedAssetBase, BaseResponseSchema, CompanyScopedMixin): """Schema for fixed asset responses""" asset_class_name: Optional[str] = None asset_class_code: Optional[str] = None location_name: Optional[str] = None custodian_name: Optional[str] = None department_name: Optional[str] = None vendor_name: Optional[str] = None # Calculated fields total_useful_life_months: int = Field(default=0, description="Total useful life in months") age_in_months: int = Field(default=0, description="Asset age in months") remaining_life_months: int = Field(default=0, description="Remaining useful life in months") depreciation_percentage: Decimal = Field(default=Decimal('0'), description="Depreciation percentage") is_fully_depreciated: bool = Field(default=False, description="Whether fully depreciated") impairment_status: ImpairmentStatus = Field(default=ImpairmentStatus.NOT_IMPAIRED) # Statistics total_depreciation_expense: Decimal = Field(default=Decimal('0'), description="Total depreciation expense") total_revaluations: int = Field(default=0, description="Number of revaluations") total_transfers: int = Field(default=0, description="Number of transfers") created_by_id: int approved_by_id: Optional[int] = None approved_at: Optional[datetime] = None model_config = ConfigDict(from_attributes=True) # ==================== DEPRECIATION SCHEDULE ==================== class DepreciationScheduleLineBase(BaseModel): """Base schema for depreciation schedule lines""" fiscal_year: int = Field(..., ge=2000, le=2100, description="Fiscal year") fiscal_period: int = Field(..., ge=1, le=12, description="Fiscal period (month)") period_start_date: date = Field(..., description="Period start date") period_end_date: date = Field(..., description="Period end date") opening_net_book_value: Decimal = Field(..., description="Opening NBV") depreciation_amount: Decimal = Field(..., ge=0, description="Depreciation amount for period") closing_net_book_value: Decimal = Field(..., description="Closing NBV") is_posted: bool = Field(default=False, description="Whether journal entry is posted") journal_entry_id: Optional[int] = Field(None, description="Posted journal entry ID") posted_date: Optional[datetime] = Field(None, description="Date posted") class DepreciationScheduleLineResponse(DepreciationScheduleLineBase, BaseResponseSchema): """Schema for depreciation schedule line responses""" asset_id: int model_config = ConfigDict(from_attributes=True) class DepreciationScheduleBase(BaseModel): """Base schema for depreciation schedules""" asset_id: int = Field(..., description="Asset ID") schedule_type: str = Field(default="standard", description="Schedule type: standard, accelerated, custom") depreciation_method: DepreciationMethod = Field(..., description="Depreciation method") total_periods: int = Field(..., ge=1, description="Total depreciation periods") total_depreciation: Decimal = Field(..., description="Total depreciation amount") remaining_depreciation: Decimal = Field(default=Decimal('0'), description="Remaining depreciation") is_active: bool = Field(default=True, description="Whether schedule is active") class DepreciationScheduleResponse(DepreciationScheduleBase, BaseResponseSchema): """Schema for depreciation schedule responses""" lines: List[DepreciationScheduleLineResponse] = Field(default_factory=list) asset_number: Optional[str] = None asset_name: Optional[str] = None model_config = ConfigDict(from_attributes=True) # ==================== DEPRECIATION RUN ==================== class DepreciationRunLineBase(BaseModel): """Base schema for depreciation run lines""" asset_id: int = Field(..., description="Asset ID") asset_number: str = Field(..., description="Asset number") asset_name: str = Field(..., description="Asset name") opening_nbv: Decimal = Field(..., description="Opening net book value") depreciation_amount: Decimal = Field(..., description="Depreciation amount") closing_nbv: Decimal = Field(..., description="Closing net book value") depreciation_account_id: int = Field(..., description="Depreciation expense account") accumulated_account_id: int = Field(..., description="Accumulated depreciation account") is_selected: bool = Field(default=True, description="Whether to include in posting") journal_entry_line_id: Optional[int] = Field(None, description="Created journal entry line ID") class DepreciationRunLineResponse(DepreciationRunLineBase, BaseResponseSchema): """Schema for depreciation run line responses""" run_id: int model_config = ConfigDict(from_attributes=True) class DepreciationRunBase(BaseModel): """Base schema for depreciation runs""" name: str = Field(..., min_length=1, max_length=200, description="Run name") fiscal_year: int = Field(..., ge=2000, le=2100, description="Fiscal year") fiscal_period: int = Field(..., ge=1, le=12, description="Fiscal period") run_date: date = Field(..., description="Run date") description: Optional[str] = Field(None, max_length=2000, description="Description") status: str = Field(default="draft", description="Status: draft, posted, reversed") total_assets: int = Field(default=0, description="Total assets processed") total_depreciation: Decimal = Field(default=Decimal('0'), description="Total depreciation amount") journal_entry_id: Optional[int] = Field(None, description="Created journal entry ID") posted_at: Optional[datetime] = Field(None, description="Posted timestamp") posted_by_id: Optional[int] = Field(None, description="Posted by user ID") reversed_at: Optional[datetime] = Field(None, description="Reversed timestamp") reversed_by_id: Optional[int] = Field(None, description="Reversed by user ID") reversal_entry_id: Optional[int] = Field(None, description="Reversal journal entry ID") class DepreciationRunCreate(DepreciationRunBase): """Schema for creating depreciation runs""" asset_ids: Optional[List[int]] = Field(None, description="Specific assets to include") asset_class_ids: Optional[List[int]] = Field(None, description="Asset classes to include") auto_post: bool = Field(default=False, description="Automatically post journal entry") class DepreciationRunUpdate(BaseModel): """Schema for updating depreciation runs""" name: Optional[str] = Field(None, min_length=1, max_length=200) description: Optional[str] = Field(None, max_length=2000) class DepreciationRunResponse(DepreciationRunBase, BaseResponseSchema, CompanyScopedMixin): """Schema for depreciation run responses""" lines: List[DepreciationRunLineResponse] = Field(default_factory=list) created_by_id: int model_config = ConfigDict(from_attributes=True) # ==================== ASSET TRANSFER ==================== class AssetTransferBase(BaseModel): """Base schema for asset transfers""" transfer_number: str = Field(..., min_length=1, max_length=50, description="Transfer number") transfer_date: date = Field(..., description="Transfer date") transfer_type: str = Field(default="location", description="Transfer type: location, department, custodian, company") description: Optional[str] = Field(None, max_length=2000, description="Transfer description") # Source information from_location_id: Optional[int] = Field(None, description="Source location ID") from_department_id: Optional[int] = Field(None, description="Source department ID") from_custodian_id: Optional[int] = Field(None, description="Source custodian ID") from_company_id: Optional[int] = Field(None, description="Source company ID") # Destination information to_location_id: Optional[int] = Field(None, description="Destination location ID") to_department_id: Optional[int] = Field(None, description="Destination department ID") to_custodian_id: Optional[int] = Field(None, description="Destination custodian ID") to_company_id: Optional[int] = Field(None, description="Destination company ID") status: str = Field(default="pending", description="Status: pending, approved, completed, cancelled") approval_required: bool = Field(default=True, description="Whether approval is required") approved_by_id: Optional[int] = Field(None, description="Approved by user ID") approved_at: Optional[datetime] = Field(None, description="Approval timestamp") completed_at: Optional[datetime] = Field(None, description="Completion timestamp") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetTransferCreate(AssetTransferBase): """Schema for creating asset transfers""" asset_ids: List[int] = Field(..., min_length=1, description="Asset IDs to transfer") class AssetTransferUpdate(BaseModel): """Schema for updating asset transfers""" description: Optional[str] = Field(None, max_length=2000) notes: Optional[str] = Field(None, max_length=2000) class AssetTransferResponse(AssetTransferBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset transfer responses""" assets: List[FixedAssetResponse] = Field(default_factory=list) from_location_name: Optional[str] = None to_location_name: Optional[str] = None from_department_name: Optional[str] = None to_department_name: Optional[str] = None from_custodian_name: Optional[str] = None to_custodian_name: Optional[str] = None requested_by_id: int model_config = ConfigDict(from_attributes=True) # ==================== ASSET DISPOSAL ==================== class AssetDisposalLineBase(BaseModel): """Base schema for disposal lines""" asset_id: int = Field(..., description="Asset ID") asset_number: Optional[str] = None asset_name: Optional[str] = None original_cost: Decimal = Field(..., description="Original asset cost") accumulated_depreciation: Decimal = Field(..., description="Accumulated depreciation at disposal") net_book_value: Decimal = Field(..., description="Net book value at disposal") disposal_value: Decimal = Field(default=Decimal('0'), description="Proceeds from disposal") disposal_costs: Decimal = Field(default=Decimal('0'), description="Costs of disposal") gain_loss: Decimal = Field(default=Decimal('0'), description="Gain or loss on disposal") gain_loss_account_id: Optional[int] = Field(None, description="Gain/loss account ID") class AssetDisposalLineCreate(AssetDisposalLineBase): """Schema for creating disposal lines""" pass class AssetDisposalLineResponse(AssetDisposalLineBase, BaseResponseSchema): """Schema for disposal line responses""" disposal_id: int model_config = ConfigDict(from_attributes=True) class AssetDisposalBase(BaseModel): """Base schema for asset disposals""" disposal_number: str = Field(..., min_length=1, max_length=50, description="Disposal number") disposal_date: date = Field(..., description="Disposal date") disposal_method: DisposalMethod = Field(..., description="Method of disposal") description: Optional[str] = Field(None, max_length=2000, description="Disposal description") # Buyer information (for sales) buyer_id: Optional[int] = Field(None, description="Buyer contact ID") buyer_name: Optional[str] = Field(None, max_length=200, description="Buyer name") buyer_address: Optional[str] = Field(None, max_length=500, description="Buyer address") # Financial details total_proceeds: Decimal = Field(default=Decimal('0'), description="Total proceeds from disposal") total_disposal_costs: Decimal = Field(default=Decimal('0'), description="Total disposal costs") total_gain_loss: Decimal = Field(default=Decimal('0'), description="Total gain or loss") # Status and approval status: str = Field(default="pending", description="Status: pending, approved, completed, cancelled") approval_required: bool = Field(default=True, description="Whether approval is required") approved_by_id: Optional[int] = Field(None, description="Approved by user ID") approved_at: Optional[datetime] = Field(None, description="Approval timestamp") completed_at: Optional[datetime] = Field(None, description="Completion timestamp") # GL Integration journal_entry_id: Optional[int] = Field(None, description="Created journal entry ID") # Additional information reference: Optional[str] = Field(None, max_length=100, description="Reference number") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetDisposalCreate(AssetDisposalBase): """Schema for creating asset disposals""" lines: List[AssetDisposalLineCreate] = Field(..., min_length=1, description="Disposal lines") class AssetDisposalUpdate(BaseModel): """Schema for updating asset disposals""" description: Optional[str] = Field(None, max_length=2000) buyer_id: Optional[int] = None buyer_name: Optional[str] = Field(None, max_length=200) buyer_address: Optional[str] = Field(None, max_length=500) reference: Optional[str] = Field(None, max_length=100) notes: Optional[str] = Field(None, max_length=2000) class AssetDisposalResponse(AssetDisposalBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset disposal responses""" lines: List[AssetDisposalLineResponse] = Field(default_factory=list) total_original_cost: Decimal = Field(default=Decimal('0'), description="Total original cost") total_accumulated_depreciation: Decimal = Field(default=Decimal('0'), description="Total accumulated depreciation") total_net_book_value: Decimal = Field(default=Decimal('0'), description="Total net book value") requested_by_id: int model_config = ConfigDict(from_attributes=True) # ==================== ASSET REVALUATION ==================== class AssetRevaluationBase(BaseModel): """Base schema for asset revaluations""" revaluation_number: str = Field(..., min_length=1, max_length=50, description="Revaluation number") revaluation_date: date = Field(..., description="Revaluation date") description: Optional[str] = Field(None, max_length=2000, description="Description") revaluation_type: str = Field(default="upward", description="Revaluation type: upward, downward") # Values previous_cost: Decimal = Field(..., description="Previous asset cost") new_cost: Decimal = Field(..., description="New asset cost") cost_adjustment: Decimal = Field(..., description="Cost adjustment amount") previous_accumulated_depreciation: Decimal = Field(..., description="Previous accumulated depreciation") new_accumulated_depreciation: Decimal = Field(..., description="New accumulated depreciation") depreciation_adjustment: Decimal = Field(..., description="Depreciation adjustment") previous_net_book_value: Decimal = Field(..., description="Previous NBV") new_net_book_value: Decimal = Field(..., description="New NBV") revaluation_reserve: Decimal = Field(..., description="Revaluation reserve amount") # Independent valuation valuer_name: Optional[str] = Field(None, max_length=200, description="Independent valuer name") valuation_report_date: Optional[date] = Field(None, description="Valuation report date") valuation_report_ref: Optional[str] = Field(None, max_length=100, description="Valuation report reference") # Status status: str = Field(default="pending", description="Status: pending, approved, completed, cancelled") approved_by_id: Optional[int] = Field(None, description="Approved by user ID") approved_at: Optional[datetime] = Field(None, description="Approval timestamp") # GL Integration journal_entry_id: Optional[int] = Field(None, description="Created journal entry ID") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetRevaluationCreate(AssetRevaluationBase): """Schema for creating asset revaluations""" asset_id: int = Field(..., description="Asset ID") new_useful_life_years: Optional[int] = Field(None, ge=0, le=100, description="Revised useful life years") new_useful_life_months: Optional[int] = Field(None, ge=0, le=1199, description="Revised useful life months") class AssetRevaluationUpdate(BaseModel): """Schema for updating asset revaluations""" description: Optional[str] = Field(None, max_length=2000) valuer_name: Optional[str] = Field(None, max_length=200) valuation_report_date: Optional[date] = None valuation_report_ref: Optional[str] = Field(None, max_length=100) notes: Optional[str] = Field(None, max_length=2000) class AssetRevaluationResponse(AssetRevaluationBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset revaluation responses""" asset_id: int asset_number: Optional[str] = None asset_name: Optional[str] = None requested_by_id: int model_config = ConfigDict(from_attributes=True) # ==================== ASSET IMPAIRMENT ==================== class AssetImpairmentBase(BaseModel): """Base schema for asset impairments""" impairment_number: str = Field(..., min_length=1, max_length=50, description="Impairment number") impairment_date: date = Field(..., description="Impairment date") description: Optional[str] = Field(None, max_length=2000, description="Description") impairment_type: str = Field(default="recognition", description="Type: recognition, reversal") # Values carrying_amount: Decimal = Field(..., description="Carrying amount before impairment") recoverable_amount: Decimal = Field(..., description="Recoverable amount (fair value less costs or value in use)") impairment_loss: Decimal = Field(..., ge=0, description="Impairment loss amount") new_carrying_amount: Decimal = Field(..., description="New carrying amount after impairment") # Impairment indicators impairment_indicators: Optional[List[str]] = Field(None, description="Indicators of impairment") # Status status: str = Field(default="pending", description="Status: pending, approved, completed, cancelled") approved_by_id: Optional[int] = Field(None, description="Approved by user ID") approved_at: Optional[datetime] = Field(None, description="Approval timestamp") # GL Integration journal_entry_id: Optional[int] = Field(None, description="Created journal entry ID") impairment_account_id: int = Field(..., description="Impairment loss account ID") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetImpairmentCreate(AssetImpairmentBase): """Schema for creating asset impairments""" asset_id: int = Field(..., description="Asset ID") class AssetImpairmentUpdate(BaseModel): """Schema for updating asset impairments""" description: Optional[str] = Field(None, max_length=2000) impairment_indicators: Optional[List[str]] = None notes: Optional[str] = Field(None, max_length=2000) class AssetImpairmentResponse(AssetImpairmentBase, BaseResponseSchema, CompanyScopedMixin): """Schema for asset impairment responses""" asset_id: int asset_number: Optional[str] = None asset_name: Optional[str] = None requested_by_id: int model_config = ConfigDict(from_attributes=True) # ==================== MAINTENANCE RECORD ==================== class AssetMaintenanceBase(BaseModel): """Base schema for asset maintenance records""" maintenance_number: str = Field(..., min_length=1, max_length=50, description="Maintenance number") maintenance_type: MaintenanceType = Field(..., description="Type of maintenance") maintenance_date: date = Field(..., description="Maintenance date") description: str = Field(..., min_length=1, max_length=2000, description="Maintenance description") status: MaintenanceStatus = Field(default=MaintenanceStatus.SCHEDULED, description="Maintenance status") # Scheduling scheduled_date: Optional[date] = Field(None, description="Scheduled date") completed_date: Optional[date] = Field(None, description="Completion date") # Cost labor_cost: Decimal = Field(default=Decimal('0'), ge=0, description="Labor cost") parts_cost: Decimal = Field(default=Decimal('0'), ge=0, description="Parts/materials cost") other_cost: Decimal = Field(default=Decimal('0'), ge=0, description="Other costs") total_cost: Decimal = Field(default=Decimal('0'), ge=0, description="Total maintenance cost") # Vendor/Technician vendor_id: Optional[int] = Field(None, description="Maintenance vendor ID") technician_name: Optional[str] = Field(None, max_length=200, description="Technician name") # Additional information work_performed: Optional[str] = Field(None, max_length=5000, description="Work performed details") parts_replaced: Optional[List[str]] = Field(None, description="Parts replaced") next_maintenance_date: Optional[date] = Field(None, description="Next scheduled maintenance") meter_reading: Optional[Decimal] = Field(None, description="Meter reading at maintenance") # Capitalization capitalize_cost: bool = Field(default=False, description="Capitalize maintenance cost") capitalization_amount: Optional[Decimal] = Field(None, description="Amount to capitalize") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetMaintenanceCreate(AssetMaintenanceBase): """Schema for creating maintenance records""" asset_id: int = Field(..., description="Asset ID") class AssetMaintenanceUpdate(BaseModel): """Schema for updating maintenance records""" maintenance_type: Optional[MaintenanceType] = None maintenance_date: Optional[date] = None description: Optional[str] = Field(None, min_length=1, max_length=2000) status: Optional[MaintenanceStatus] = None scheduled_date: Optional[date] = None completed_date: Optional[date] = None labor_cost: Optional[Decimal] = Field(None, ge=0) parts_cost: Optional[Decimal] = Field(None, ge=0) other_cost: Optional[Decimal] = Field(None, ge=0) vendor_id: Optional[int] = None technician_name: Optional[str] = Field(None, max_length=200) work_performed: Optional[str] = Field(None, max_length=5000) parts_replaced: Optional[List[str]] = None next_maintenance_date: Optional[date] = None meter_reading: Optional[Decimal] = None capitalize_cost: Optional[bool] = None capitalization_amount: Optional[Decimal] = None notes: Optional[str] = Field(None, max_length=2000) class AssetMaintenanceResponse(AssetMaintenanceBase, BaseResponseSchema, CompanyScopedMixin): """Schema for maintenance record responses""" asset_id: int asset_number: Optional[str] = None asset_name: Optional[str] = None vendor_name: Optional[str] = None model_config = ConfigDict(from_attributes=True) # ==================== INSURANCE ==================== class AssetInsuranceBase(BaseModel): """Base schema for asset insurance policies""" policy_number: str = Field(..., min_length=1, max_length=50, description="Policy number") policy_name: str = Field(..., min_length=1, max_length=200, description="Policy name") insurance_company: str = Field(..., min_length=1, max_length=200, description="Insurance company") # Coverage period start_date: date = Field(..., description="Coverage start date") end_date: date = Field(..., description="Coverage end date") status: InsuranceStatus = Field(default=InsuranceStatus.ACTIVE, description="Policy status") # Coverage details coverage_type: str = Field(default="all_risk", description="Coverage type: all_risk, named_perils, fire_only, etc.") insured_value: Decimal = Field(..., ge=0, description="Insured value") deductible: Decimal = Field(default=Decimal('0'), ge=0, description="Deductible amount") # Premium premium_amount: Decimal = Field(..., ge=0, description="Premium amount") premium_frequency: str = Field(default="annual", description="Premium frequency: annual, semi_annual, quarterly, monthly") next_premium_date: Optional[date] = Field(None, description="Next premium due date") # Beneficiary beneficiary: Optional[str] = Field(None, max_length=200, description="Beneficiary") # Agent/Broker agent_name: Optional[str] = Field(None, max_length=200, description="Agent/Broker name") agent_contact: Optional[str] = Field(None, max_length=200, description="Agent contact info") notes: Optional[str] = Field(None, max_length=2000, description="Additional notes") class AssetInsuranceCreate(AssetInsuranceBase): """Schema for creating insurance policies""" asset_ids: Optional[List[int]] = Field(None, description="Covered asset IDs") class AssetInsuranceUpdate(BaseModel): """Schema for updating insurance policies""" policy_name: Optional[str] = Field(None, min_length=1, max_length=200) insurance_company: Optional[str] = Field(None, min_length=1, max_length=200) start_date: Optional[date] = None end_date: Optional[date] = None status: Optional[InsuranceStatus] = None coverage_type: Optional[str] = None insured_value: Optional[Decimal] = Field(None, ge=0) deductible: Optional[Decimal] = Field(None, ge=0) premium_amount: Optional[Decimal] = Field(None, ge=0) premium_frequency: Optional[str] = None next_premium_date: Optional[date] = None beneficiary: Optional[str] = Field(None, max_length=200) agent_name: Optional[str] = Field(None, max_length=200) agent_contact: Optional[str] = Field(None, max_length=200) notes: Optional[str] = Field(None, max_length=2000) class AssetInsuranceResponse(AssetInsuranceBase, BaseResponseSchema, CompanyScopedMixin): """Schema for insurance policy responses""" covered_assets: List[FixedAssetResponse] = Field(default_factory=list) total_insured_value: Decimal = Field(default=Decimal('0'), description="Total insured value of assets") days_to_expiry: int = Field(default=0, description="Days until policy expires") model_config = ConfigDict(from_attributes=True) # ==================== FILTER PARAMETERS ==================== class AssetClassFilterParams(PaginationParams): """Filter parameters for asset class queries""" category: Optional[AssetCategory] = None is_active: Optional[bool] = None parent_class_id: Optional[int] = None class AssetLocationFilterParams(PaginationParams): """Filter parameters for asset location queries""" location_type: Optional[str] = None is_active: Optional[bool] = None parent_location_id: Optional[int] = None class FixedAssetFilterParams(PaginationParams): """Filter parameters for fixed asset queries""" asset_class_id: Optional[int] = None category: Optional[AssetCategory] = None status: Optional[AssetStatus] = None location_id: Optional[int] = None department_id: Optional[int] = None custodian_id: Optional[int] = None vendor_id: Optional[int] = None acquisition_date_from: Optional[date] = None acquisition_date_to: Optional[date] = None cost_from: Optional[Decimal] = None cost_to: Optional[Decimal] = None search: Optional[str] = None is_fully_depreciated: Optional[bool] = None class DepreciationRunFilterParams(PaginationParams): """Filter parameters for depreciation run queries""" fiscal_year: Optional[int] = None fiscal_period: Optional[int] = None status: Optional[str] = None class AssetTransferFilterParams(PaginationParams): """Filter parameters for asset transfer queries""" transfer_type: Optional[str] = None status: Optional[str] = None transfer_date_from: Optional[date] = None transfer_date_to: Optional[date] = None class AssetDisposalFilterParams(PaginationParams): """Filter parameters for asset disposal queries""" disposal_method: Optional[DisposalMethod] = None status: Optional[str] = None disposal_date_from: Optional[date] = None disposal_date_to: Optional[date] = None class AssetMaintenanceFilterParams(PaginationParams): """Filter parameters for maintenance queries""" maintenance_type: Optional[MaintenanceType] = None status: Optional[MaintenanceStatus] = None maintenance_date_from: Optional[date] = None maintenance_date_to: Optional[date] = None class AssetInsuranceFilterParams(PaginationParams): """Filter parameters for insurance queries""" status: Optional[InsuranceStatus] = None insurance_company: Optional[str] = None expiring_within_days: Optional[int] = None # ==================== LIST RESPONSES ==================== class AssetClassListResponse(ListResponseBase): """List response for asset classes""" items: List[AssetClassResponse] class AssetLocationListResponse(ListResponseBase): """List response for asset locations""" items: List[AssetLocationResponse] class FixedAssetListResponse(ListResponseBase): """List response for fixed assets""" items: List[FixedAssetResponse] class DepreciationScheduleListResponse(ListResponseBase): """List response for depreciation schedules""" items: List[DepreciationScheduleResponse] class DepreciationRunListResponse(ListResponseBase): """List response for depreciation runs""" items: List[DepreciationRunResponse] class AssetTransferListResponse(ListResponseBase): """List response for asset transfers""" items: List[AssetTransferResponse] class AssetDisposalListResponse(ListResponseBase): """List response for asset disposals""" items: List[AssetDisposalResponse] class AssetRevaluationListResponse(ListResponseBase): """List response for asset revaluations""" items: List[AssetRevaluationResponse] class AssetImpairmentListResponse(ListResponseBase): """List response for asset impairments""" items: List[AssetImpairmentResponse] class AssetMaintenanceListResponse(ListResponseBase): """List response for maintenance records""" items: List[AssetMaintenanceResponse] class AssetInsuranceListResponse(ListResponseBase): """List response for insurance policies""" items: List[AssetInsuranceResponse] # ==================== ANALYTICS RESPONSES ==================== class AssetStatisticsResponse(BaseModel): """Asset module statistics""" total_assets: int = 0 active_assets: int = 0 total_original_cost: Decimal = Decimal('0') total_accumulated_depreciation: Decimal = Decimal('0') total_net_book_value: Decimal = Decimal('0') fully_depreciated_assets: int = 0 impaired_assets: int = 0 assets_under_construction: int = 0 held_for_sale_assets: int = 0 # By category assets_by_category: Dict[str, int] = Field(default_factory=dict) value_by_category: Dict[str, Decimal] = Field(default_factory=dict) # By status assets_by_status: Dict[str, int] = Field(default_factory=dict) # By location top_locations_by_value: List[Dict[str, Any]] = Field(default_factory=list) # Depreciation depreciation_this_period: Decimal = Decimal('0') depreciation_ytd: Decimal = Decimal('0') # Recent activity acquisitions_this_month: int = 0 disposals_this_month: int = 0 transfers_this_month: int = 0 # Insurance total_insured_value: Decimal = Decimal('0') expiring_insurance_policies: int = 0 # Maintenance pending_maintenance: int = 0 overdue_maintenance: int = 0 maintenance_cost_ytd: Decimal = Decimal('0') class DepreciationForecastResponse(BaseModel): """Depreciation forecast for planning""" periods: List[Dict[str, Any]] = Field(default_factory=list) total_forecast_depreciation: Decimal = Decimal('0') assets_included: int = 0 forecast_start_date: date forecast_end_date: date class AssetRollforwardResponse(BaseModel): """Asset rollforward report data""" period_start: date period_end: date category: Optional[str] = None beginning_balance: Decimal = Decimal('0') additions: Decimal = Decimal('0') disposals: Decimal = Decimal('0') transfers_in: Decimal = Decimal('0') transfers_out: Decimal = Decimal('0') revaluations: Decimal = Decimal('0') impairments: Decimal = Decimal('0') depreciation: Decimal = Decimal('0') ending_balance: Decimal = Decimal('0') details: List[Dict[str, Any]] = Field(default_factory=list)