Skip to main content

yuki_cli/cli/
accounts.rs

1use crate::cli::setup_domain;
2use crate::client::accounting::AccountingClient;
3use crate::client::accounting_info::AccountingInfoClient;
4use crate::config::Config;
5use crate::error::YukiError;
6use crate::output::{
7    ListOptions, OutputFormat, apply_pagination, format_json, format_table, is_tty, select_fields,
8};
9use crate::period::parse_period;
10
11pub async fn balance(
12    config: &Config,
13    admin: Option<&str>,
14    account: Option<&str>,
15    period: Option<&str>,
16    format: Option<&str>,
17) -> Result<(), YukiError> {
18    let (start, _end) = resolve_period(period)?;
19    let (client, entry) = setup_domain(config, admin).await?;
20    let mut balances = client.gl_account_balances(&entry.admin_id, &start).await?;
21    // The operation returns every account; narrow to one when --account is given.
22    if let Some(code) = account {
23        balances.retain(|b| b.code == code);
24    }
25
26    let headers = vec!["Account".into(), "Description".into(), "Balance".into()];
27    let rows: Vec<Vec<String>> = balances
28        .iter()
29        .map(|b| vec![b.code.clone(), b.description.clone(), b.amount.clone()])
30        .collect();
31
32    let fmt = OutputFormat::from_flag(format, is_tty());
33    match fmt {
34        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
35        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
36    }
37    Ok(())
38}
39
40pub async fn transactions(
41    config: &Config,
42    admin: Option<&str>,
43    account: Option<&str>,
44    period: Option<&str>,
45    format: Option<&str>,
46    opts: ListOptions<'_>,
47) -> Result<(), YukiError> {
48    let (start, end) = resolve_period(period)?;
49    let gl_code = account.unwrap_or("");
50    let (client, entry) = setup_domain(config, admin).await?;
51    let xml = client
52        .gl_account_transactions(&entry.admin_id, gl_code, &start, &end)
53        .await?;
54    let transactions = AccountingClient::parse_gl_transactions(&xml)?;
55
56    let mut headers = vec![
57        "ID".into(),
58        "Date".into(),
59        "Amount".into(),
60        "Description".into(),
61    ];
62    let mut rows: Vec<Vec<String>> = transactions
63        .iter()
64        .map(|t| {
65            vec![
66                t.id.clone(),
67                t.date.clone(),
68                t.amount.clone(),
69                t.description.clone(),
70            ]
71        })
72        .collect();
73    apply_pagination(&mut rows, &opts);
74    select_fields(&mut headers, &mut rows, &opts)?;
75
76    let fmt = OutputFormat::from_flag(format, is_tty());
77    match fmt {
78        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
79        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
80    }
81    Ok(())
82}
83
84pub async fn scheme(
85    config: &Config,
86    admin: Option<&str>,
87    format: Option<&str>,
88) -> Result<(), YukiError> {
89    let entry = config.resolve_admin(admin)?;
90    let mut client = AccountingInfoClient::new();
91    client.authenticate(&config.api_key).await?;
92    let accounts = client.get_gl_account_scheme(&entry.admin_id).await?;
93
94    let headers = vec!["Code".into(), "Description".into(), "Type".into()];
95    let rows: Vec<Vec<String>> = accounts
96        .into_iter()
97        .map(|a| vec![a.code, a.description, a.account_type])
98        .collect();
99
100    let fmt = OutputFormat::from_flag(format, is_tty());
101    match fmt {
102        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
103        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
104    }
105    Ok(())
106}
107
108pub async fn start_balance(
109    config: &Config,
110    admin: Option<&str>,
111    year: Option<&str>,
112    format: Option<&str>,
113) -> Result<(), YukiError> {
114    let year_str;
115    let bookyear = match year {
116        Some(y) => y,
117        None => {
118            year_str = current_year().to_string();
119            &year_str
120        }
121    };
122    let entry = config.resolve_admin(admin)?;
123    let mut client = AccountingInfoClient::new();
124    client.authenticate(&config.api_key).await?;
125    let balances = client
126        .get_start_balance_by_gl_account(&entry.admin_id, bookyear)
127        .await?;
128
129    let headers = vec!["GL Account".into(), "Description".into(), "Balance".into()];
130    let rows: Vec<Vec<String>> = balances
131        .into_iter()
132        .map(|b| vec![b.gl_account_code, b.description, b.balance])
133        .collect();
134
135    let fmt = OutputFormat::from_flag(format, is_tty());
136    match fmt {
137        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
138        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
139    }
140    Ok(())
141}
142
143pub async fn revenue(
144    config: &Config,
145    admin: Option<&str>,
146    period: Option<&str>,
147    format: Option<&str>,
148) -> Result<(), YukiError> {
149    let (start, end) = resolve_period(period)?;
150    let (client, entry) = setup_domain(config, admin).await?;
151    let amount = client.net_revenue(&entry.admin_id, &start, &end).await?;
152
153    let headers = vec!["Period".into(), "Net Revenue".into()];
154    let rows = vec![vec![format!("{start} to {end}"), amount]];
155
156    let fmt = OutputFormat::from_flag(format, is_tty());
157    match fmt {
158        OutputFormat::Table => println!("{}", format_table(&headers, &rows)),
159        OutputFormat::Json => println!("{}", format_json(&headers, &rows)),
160    }
161    Ok(())
162}
163
164/// Resolve an optional period string to (start_date, end_date).
165///
166/// When no period is given, defaults to the current calendar year.
167fn resolve_period(period: Option<&str>) -> Result<(String, String), YukiError> {
168    match period {
169        Some(p) => parse_period(p),
170        None => {
171            let year = current_year();
172            Ok((format!("{year}-01-01"), format!("{year}-12-31")))
173        }
174    }
175}
176
177fn current_year() -> u32 {
178    use std::time::{SystemTime, UNIX_EPOCH};
179    let secs = SystemTime::now()
180        .duration_since(UNIX_EPOCH)
181        .unwrap_or_default()
182        .as_secs();
183    // Approximate: seconds since epoch divided by seconds per year
184    1970 + (secs / 31_557_600) as u32
185}