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 { let mut revenues = Vec::new(); let mut expenses = Vec::new(); let balance_map: std::collections::HashMap = 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); } }