tallyprime-sdk 0.1.2

Rust SDK for TallyPrime XML/HTTP integration with typed APIs for masters, vouchers, and financial reports.
Documentation
#![allow(dead_code)]

use chrono::{Datelike, Local};
use tallyprime_sdk::config::TallyConfig;
use tallyprime_sdk::{TallyClient, Voucher, VoucherEntry};

pub fn create_client_from_env() -> TallyClient {
    let cfg = TallyConfig {
        host: std::env::var("TALLY_HOST").unwrap_or_else(|_| "localhost".into()),
        port: std::env::var("TALLY_PORT")
            .ok()
            .and_then(|s| s.parse::<u16>().ok())
            .unwrap_or(9000),
        current_company: std::env::var("TALLY_COMPANY").ok(),
        ..Default::default()
    };

    let client = TallyClient::new(cfg).unwrap_or_else(|err| {
        eprintln!("Failed to initialize Tally client: {err}");
        std::process::exit(1);
    });

    if let Err(err) = client.test_connection() {
        eprintln!("Failed to connect to Tally: {err}");
        std::process::exit(1);
    }

    client
}

pub fn active_company_label(client: &TallyClient) -> String {
    client
        .active_company_name()
        .unwrap_or_else(|err| {
            eprintln!("Failed to determine active company: {err}");
            std::process::exit(1);
        })
        .unwrap_or_else(|| "(no active company)".into())
}

pub fn has_flag(flag: &str) -> bool {
    std::env::args().any(|arg| arg == flag)
}

pub fn arg_value(flag: &str) -> Option<String> {
    let args: Vec<String> = std::env::args().collect();
    args.windows(2)
        .find(|window| window[0] == flag)
        .map(|window| window[1].clone())
}

pub fn resolve_financial_period() -> (String, String) {
    if let Some(fy) = arg_value("--fy") {
        if let Some(period) = parse_financial_year(&fy) {
            return period;
        }
        eprintln!("Invalid --fy value `{fy}`. Expected YYYY-YYYY, for example 2025-2026.");
        std::process::exit(1);
    }

    match (arg_value("--from"), arg_value("--to")) {
        (Some(from), Some(to)) => (from, to),
        (None, None) => current_financial_year(),
        _ => {
            eprintln!("Use either both --from and --to, or provide --fy.");
            std::process::exit(1);
        }
    }
}

pub fn format_yyyymmdd(date: &str) -> String {
    if date.len() == 8 {
        format!("{}-{}-{}", &date[0..4], &date[4..6], &date[6..8])
    } else {
        date.to_string()
    }
}

pub fn format_amount(value: Option<f64>) -> String {
    value
        .map(|amount| format!("{amount:.2}"))
        .unwrap_or_else(|| "-".into())
}

pub fn summarize_voucher(voucher: &Voucher) -> String {
    let voucher_number = voucher.voucher_number.as_deref().unwrap_or("-");
    let party = voucher.party_ledger_name.as_deref().unwrap_or("-");
    let party_entry = voucher.entries.iter().find(|entry| entry.is_party_ledger);
    let amount = party_entry
        .map(|entry| entry.amount)
        .or(voucher.amount.map(|amount| amount.abs()))
        .unwrap_or(0.0);
    let forex = party_entry
        .and_then(|entry| entry.forex.as_ref())
        .or(voucher.amount_forex.as_ref());

    let mut summary = format!(
        "{} | {} | no. {} | party {} | amount {:.2}",
        format_yyyymmdd(&voucher.date_yyyymmdd),
        voucher.voucher_type,
        voucher_number,
        party,
        amount
    );
    if let Some(forex) = forex {
        summary.push_str(&format!(
            " | fx {:.4} {} @ {} {:.6}",
            forex.foreign_amount.abs(),
            forex.foreign_currency_label(),
            forex.base_currency_label(),
            forex.exchange_rate
        ));
    }
    summary
}

pub fn summarize_entry(entry: &VoucherEntry) -> String {
    let side = if entry.is_debit { "Dr" } else { "Cr" };
    let party = if entry.is_party_ledger {
        " [Party]"
    } else {
        ""
    };
    let mut summary = format!(
        "{} | {} {:.2}{}",
        entry.ledger_name, side, entry.amount, party
    );
    if let Some(forex) = &entry.forex {
        summary.push_str(&format!(
            " | fx {:.4} {} @ {} {:.6}",
            forex.foreign_amount,
            forex.foreign_currency_label(),
            forex.base_currency_label(),
            forex.exchange_rate
        ));
    }
    summary
}

fn current_financial_year() -> (String, String) {
    let today = Local::now().date_naive();
    let start_year = if today.month() >= 4 {
        today.year()
    } else {
        today.year() - 1
    };

    (
        format!("{start_year}0401"),
        format!("{}0331", start_year + 1),
    )
}

fn parse_financial_year(value: &str) -> Option<(String, String)> {
    let (start, end) = value.split_once('-')?;
    let start_year = start.parse::<i32>().ok()?;
    let end_year = end.parse::<i32>().ok()?;
    if end_year != start_year + 1 {
        return None;
    }
    Some((format!("{start_year}0401"), format!("{end_year}0331")))
}