bae705aa97
- Add NFC ePassport roadmap (ICAO 9303, eIDAS) - Add TensorFlow.js edge face detection (BlazeFace) - Add structured audit logger (GDPR-compliant) - Risk scoring support Part of KYC Apple Native UX v1.1.0
508 lines
15 KiB
Python
508 lines
15 KiB
Python
"""
|
|
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}")
|