Files
boc/aamos-ledger-rust/crates/aamos-reports/src/income_statement.rs
T
Bernt bae705aa97 ARCHITECTURE: NFC roadmap, edge AI, audit logging
- 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
2026-06-29 16:24:48 +00:00

190 lines
5.9 KiB
Rust

use crate::{AccountBalance, IncomeStatement};
use aamos_core::{LedgerResult, Money, Period, TenantId};
use aamos_ledger::{Account, AccountType, BalanceSide};
/// Genererar en resultatrapport (income statement) för en given period.
///
/// Resultatrapporten visar intäkter och kostnader för perioden,
/// samt nettoresultatet (intäkter minus kostnader).
pub fn generate_income_statement(
tenant_id: TenantId,
period: Period,
accounts: &[Account],
balances: &[(String, Money)], // (account_number, period_balance)
) -> LedgerResult<IncomeStatement> {
let mut revenues = Vec::new();
let mut expenses = Vec::new();
let balance_map: std::collections::HashMap<String, Money> =
balances.iter().map(|(n, b)| (n.clone(), *b)).collect();
for account in accounts {
if !account.is_active {
continue;
}
let balance = balance_map
.get(&account.account_number)
.copied()
.unwrap_or(Money::zero());
// Resultaträkning: intäkter har kreditsaldo, kostnader har debetsaldo
let signed_balance = match (account.account_type, account.normal_balance) {
(AccountType::Revenue, BalanceSide::Credit) => balance,
(AccountType::Revenue, BalanceSide::Debit) => -balance,
(AccountType::Expense, BalanceSide::Debit) => balance,
(AccountType::Expense, BalanceSide::Credit) => -balance,
// Tillgångar, skulder, eget kapital ingår inte i resultatrapporten
_ => continue,
};
let item = AccountBalance {
account_number: account.account_number.clone(),
account_name: account.name.clone(),
balance: signed_balance,
};
match account.account_type {
AccountType::Revenue => revenues.push(item),
AccountType::Expense => expenses.push(item),
_ => {}
}
}
// Sortera efter kontonummer
revenues.sort_by(|a, b| a.account_number.cmp(&b.account_number));
expenses.sort_by(|a, b| a.account_number.cmp(&b.account_number));
let total_revenue = revenues
.iter()
.map(|a| a.balance)
.fold(Money::zero(), |a, b| a + b);
let total_expense = expenses
.iter()
.map(|a| a.balance)
.fold(Money::zero(), |a, b| a + b);
// Nettoresultat = intäkter - kostnader
let net_income = total_revenue - total_expense;
Ok(IncomeStatement {
tenant_id,
period,
revenues,
expenses,
total_revenue,
total_expense,
net_income,
})
}
#[cfg(test)]
mod tests {
use super::*;
use aamos_ledger::{CoaStandard};
use chrono::Utc;
use serde_json;
use uuid::Uuid;
fn test_tenant() -> TenantId {
TenantId::new("test-tenant")
}
fn make_account(number: &str, name: &str, account_type: AccountType, normal_balance: BalanceSide) -> Account {
Account {
id: Uuid::new_v4(),
tenant_id: test_tenant(),
account_number: number.to_string(),
name: name.to_string(),
account_type,
normal_balance,
coa_standard: CoaStandard::BAS,
parent_account: None,
vat_code: None,
is_active: true,
metadata: serde_json::Value::Object(serde_json::Map::new()),
created_at: Utc::now(),
updated_at: Utc::now(),
}
}
#[test]
fn test_income_statement_profit() {
let accounts = vec![
make_account("3001", "Konsultintäkter", AccountType::Revenue, BalanceSide::Credit),
make_account("7010", "Lönekostnader", AccountType::Expense, BalanceSide::Debit),
make_account("6991", "Kontorshyra", AccountType::Expense, BalanceSide::Debit),
];
let balances = vec![
("3001".to_string(), Money(100000)),
("7010".to_string(), Money(50000)),
("6991".to_string(), Money(20000)),
];
let stmt = generate_income_statement(
test_tenant(),
Period::new(2026, 6).unwrap(),
&accounts,
&balances,
)
.unwrap();
assert_eq!(stmt.total_revenue.0, 100000);
assert_eq!(stmt.total_expense.0, 70000);
assert_eq!(stmt.net_income.0, 30000); // vinst
}
#[test]
fn test_income_statement_loss() {
let accounts = vec![
make_account("3001", "Konsultintäkter", AccountType::Revenue, BalanceSide::Credit),
make_account("7010", "Lönekostnader", AccountType::Expense, BalanceSide::Debit),
];
let balances = vec![
("3001".to_string(), Money(30000)),
("7010".to_string(), Money(50000)),
];
let stmt = generate_income_statement(
test_tenant(),
Period::new(2026, 6).unwrap(),
&accounts,
&balances,
)
.unwrap();
assert_eq!(stmt.total_revenue.0, 30000);
assert_eq!(stmt.total_expense.0, 50000);
assert_eq!(stmt.net_income.0, -20000); // förlust
}
#[test]
fn test_income_statement_skips_balance_accounts() {
let accounts = vec![
make_account("3001", "Konsultintäkter", AccountType::Revenue, BalanceSide::Credit),
make_account("1910", "Kassa", AccountType::Asset, BalanceSide::Debit),
make_account("2010", "Eget kapital", AccountType::Equity, BalanceSide::Credit),
];
let balances = vec![
("3001".to_string(), Money(100000)),
("1910".to_string(), Money(50000)),
("2010".to_string(), Money(30000)),
];
let stmt = generate_income_statement(
test_tenant(),
Period::new(2026, 6).unwrap(),
&accounts,
&balances,
)
.unwrap();
assert_eq!(stmt.revenues.len(), 1);
assert_eq!(stmt.expenses.len(), 0);
assert_eq!(stmt.total_revenue.0, 100000);
}
}