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
190 lines
5.9 KiB
Rust
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);
|
|
}
|
|
}
|