""" IOM to Ledger Mapping Integration between Infrastructure Object Model and aamos-ledger """ from typing import Dict, List, Optional from dataclasses import dataclass from datetime import date from enum import Enum class BASAccount(str, Enum): """BAS-konton för IOM-objekt""" # Anläggningstillgångar BUILDINGS = "1220" MACHINERY = "1230" IMPROVEMENTS = "1240" # Underhåll MAINTENANCE = "6110" CONTRACTOR = "6130" # Avskrivningar DEPRECIATION_BUILDINGS = "7830" DEPRECIATION_MACHINERY = "7831" # Försäkring INSURANCE = "6310" # Förlust IMPAIRMENT = "7970" # Leverantörsskulder ACCOUNTS_PAYABLE = "2440" # Moms VAT_IN = "2640" # Ackumulerade avskrivningar ACCUM_DEPRECIATION = "1229" @dataclass class AssetMapping: """Mapping between IOM object and ledger asset""" goid: str account: BASAccount acquisition_value: float acquisition_date: date useful_life_years: int depreciation_method: str = "linear" # IOM metadata object_type: str = "" material: str = "" dimensions: str = "" # Ledger state accumulated_depreciation: float = 0.0 book_value: float = 0.0 def calculate_depreciation(self, year: int) -> float: """Calculate annual depreciation""" if self.depreciation_method == "linear": return self.acquisition_value / self.useful_life_years return 0.0 def update_book_value(self, year: int): """Update book value after depreciation""" annual_dep = self.calculate_depreciation(year) self.accumulated_depreciation += annual_dep self.book_value = self.acquisition_value - self.accumulated_depreciation class ObjectTypeAccountMapping: """Maps IOM object types to BAS accounts""" MAPPINGS = { # Buildings and structures "abutment": BASAccount.BUILDINGS, "pier": BASAccount.BUILDINGS, "deck": BASAccount.BUILDINGS, "facade": BASAccount.BUILDINGS, "roof": BASAccount.BUILDINGS, "window": BASAccount.IMPROVEMENTS, "entrance": BASAccount.IMPROVEMENTS, "balcony": BASAccount.IMPROVEMENTS, # Machinery and equipment "transformer": BASAccount.MACHINERY, "charging_station": BASAccount.MACHINERY, "street_light": BASAccount.MACHINERY, "traffic_signal": BASAccount.MACHINERY, # Infrastructure "road_surface": BASAccount.BUILDINGS, "guardrail": BASAccount.BUILDINGS, "sign": BASAccount.BUILDINGS, "parking_surface": BASAccount.BUILDINGS, "bollard": BASAccount.BUILDINGS, } @classmethod def get_account(cls, object_type: str) -> BASAccount: """Get BAS account for object type""" return cls.MAPPINGS.get(object_type.lower(), BASAccount.BUILDINGS) @classmethod def get_useful_life(cls, object_type: str) -> int: """Get useful life in years for object type""" lifespans = { "abutment": 50, "pier": 50, "deck": 40, "facade": 30, "roof": 25, "window": 20, "entrance": 25, "balcony": 30, "transformer": 30, "charging_station": 15, "street_light": 20, "traffic_signal": 15, "road_surface": 20, "guardrail": 25, "sign": 10, "parking_surface": 20, "bollard": 20, } return lifespans.get(object_type.lower(), 25) class LedgerTransaction: """Represents a ledger transaction""" def __init__( self, date: date, verno: str, account: BASAccount, amount: float, description: str, goid: str, metadata: Optional[Dict] = None ): self.date = date self.verno = verno self.account = account self.amount = amount self.description = description self.goid = goid self.metadata = metadata or {} def to_dict(self) -> Dict: return { "date": self.date.isoformat(), "verno": self.verno, "account": self.account.value, "account_name": self.account.name, "amount": self.amount, "description": self.description, "goid": self.goid, "metadata": self.metadata } class IOMLedgerMapper: """Maps IOM events to ledger transactions""" def __init__(self): self.transaction_counter = 0 def _generate_verno(self, year: int) -> str: """Generate verifikationsnummer""" self.transaction_counter += 1 return f"{year}-{self.transaction_counter:07d}" def map_installation( self, goid: str, object_type: str, cost: float, installation_date: date, contractor: str = "", project: str = "" ) -> List[LedgerTransaction]: """ Map object installation to ledger transactions Returns: List of ledger transactions """ account = ObjectTypeAccountMapping.get_account(object_type) verno = self._generate_verno(installation_date.year) transactions = [] # Debit asset account transactions.append(LedgerTransaction( date=installation_date, verno=verno, account=account, amount=cost, description=f"Installation: {goid}", goid=goid, metadata={ "event_type": "installation", "contractor": contractor, "project": project } )) # Credit accounts payable transactions.append(LedgerTransaction( date=installation_date, verno=verno, account=BASAccount.ACCOUNTS_PAYABLE, amount=-cost, description=f"Leverantörsskuld: {goid}", goid=goid, metadata={ "event_type": "installation", "contractor": contractor } )) return transactions def map_maintenance( self, goid: str, cost: float, maintenance_date: date, observation_id: str = "", defect_code: str = "", contractor: str = "", description: str = "" ) -> List[LedgerTransaction]: """ Map maintenance to ledger transactions Returns: List of ledger transactions """ verno = self._generate_verno(maintenance_date.year) transactions = [] # Debit maintenance account transactions.append(LedgerTransaction( date=maintenance_date, verno=verno, account=BASAccount.MAINTENANCE, amount=cost, description=description or f"Underhåll: {goid}", goid=goid, metadata={ "event_type": "maintenance", "observation_id": observation_id, "defect_code": defect_code, "contractor": contractor } )) # Credit accounts payable transactions.append(LedgerTransaction( date=maintenance_date, verno=verno, account=BASAccount.ACCOUNTS_PAYABLE, amount=-cost, description=f"Leverantörsskuld: {goid}", goid=goid, metadata={ "event_type": "maintenance", "contractor": contractor } )) return transactions def map_depreciation( self, goid: str, object_type: str, acquisition_value: float, useful_life: int, depreciation_date: date, accumulated_depreciation: float = 0.0 ) -> List[LedgerTransaction]: """ Map annual depreciation to ledger transactions Returns: List of ledger transactions """ annual_depreciation = acquisition_value / useful_life verno = self._generate_verno(depreciation_date.year) account = ObjectTypeAccountMapping.get_account(object_type) dep_account = ( BASAccount.DEPRECIATION_BUILDINGS if account == BASAccount.BUILDINGS else BASAccount.DEPRECIATION_MACHINERY ) transactions = [] # Debit depreciation expense transactions.append(LedgerTransaction( date=depreciation_date, verno=verno, account=dep_account, amount=annual_depreciation, description=f"Avskrivning: {goid}", goid=goid, metadata={ "event_type": "depreciation", "annual_amount": annual_depreciation, "useful_life": useful_life, "accumulated": accumulated_depreciation + annual_depreciation } )) # Credit accumulated depreciation transactions.append(LedgerTransaction( date=depreciation_date, verno=verno, account=BASAccount.ACCUM_DEPRECIATION, amount=-annual_depreciation, description=f"Ackumulerad avskrivning: {goid}", goid=goid, metadata={ "event_type": "depreciation", "annual_amount": annual_depreciation } )) return transactions def map_component_replacement( self, goid: str, parent_goid: str, cost: float, replacement_date: date, old_component_goid: str = "", new_component_goid: str = "", contractor: str = "" ) -> List[LedgerTransaction]: """ Map component replacement to ledger transactions Returns: List of ledger transactions """ verno = self._generate_verno(replacement_date.year) transactions = [] # Debit maintenance (or capitalize if major) transactions.append(LedgerTransaction( date=replacement_date, verno=verno, account=BASAccount.MAINTENANCE, amount=cost, description=f"Komponentbyte: {goid}", goid=goid, metadata={ "event_type": "component_replacement", "parent_goid": parent_goid, "old_component": old_component_goid, "new_component": new_component_goid, "contractor": contractor } )) # Credit accounts payable transactions.append(LedgerTransaction( date=replacement_date, verno=verno, account=BASAccount.ACCOUNTS_PAYABLE, amount=-cost, description=f"Leverantörsskuld: {goid}", goid=goid, metadata={ "event_type": "component_replacement", "contractor": contractor } )) return transactions def map_impairment( self, goid: str, book_value: float, recoverable_amount: float, impairment_date: date, reason: str = "", observation_id: str = "" ) -> List[LedgerTransaction]: """ Map impairment to ledger transactions Returns: List of ledger transactions """ impairment_loss = book_value - recoverable_amount if impairment_loss <= 0: return [] verno = self._generate_verno(impairment_date.year) transactions = [] # Debit impairment loss transactions.append(LedgerTransaction( date=impairment_date, verno=verno, account=BASAccount.IMPAIRMENT, amount=impairment_loss, description=f"Nedskrivning: {goid}", goid=goid, metadata={ "event_type": "impairment", "book_value": book_value, "recoverable_amount": recoverable_amount, "reason": reason, "observation_id": observation_id } )) # Credit asset account transactions.append(LedgerTransaction( date=impairment_date, verno=verno, account=BASAccount.BUILDINGS, # Simplified - should look up actual account amount=-impairment_loss, description=f"Nedskrivning tillgång: {goid}", goid=goid, metadata={ "event_type": "impairment", "impairment_loss": impairment_loss } )) return transactions if __name__ == '__main__': # Example usage mapper = IOMLedgerMapper() from datetime import date # Example 1: Installation print("=== Installation ===") transactions = mapper.map_installation( goid="TRN-BRG-ABT-CON-001", object_type="abutment", cost=4500000, installation_date=date(2019, 3, 15), contractor="Skanska AB", project="Bromöllan bro" ) for t in transactions: print(f"{t.account.value}: {t.amount:>12.2f} - {t.description}") # Example 2: Maintenance print("\n=== Maintenance ===") transactions = mapper.map_maintenance( goid="TRN-BRG-ABT-CON-001", cost=350000, maintenance_date=date(2028, 5, 20), observation_id="OBS-2028-006633", defect_code="1300", contractor="Cementa AB", description="Sprickreparation" ) for t in transactions: print(f"{t.account.value}: {t.amount:>12.2f} - {t.description}") # Example 3: Depreciation print("\n=== Depreciation ===") transactions = mapper.map_depreciation( goid="TRN-BRG-ABT-CON-001", object_type="abutment", acquisition_value=4500000, useful_life=50, depreciation_date=date(2026, 12, 31), accumulated_depreciation=720000 ) for t in transactions: print(f"{t.account.value}: {t.amount:>12.2f} - {t.description}") # Example 4: Impairment print("\n=== Impairment ===") transactions = mapper.map_impairment( goid="TRN-BRG-ABT-CON-001", book_value=3600000, recoverable_amount=2100000, impairment_date=date(2031, 6, 15), reason="Structural degradation", observation_id="OBS-2031-001847" ) for t in transactions: print(f"{t.account.value}: {t.amount:>12.2f} - {t.description}")