f7b645895d
This might want to use the same YNAB CSV format as `convert`.
212 lines
7.3 KiB
Rust
212 lines
7.3 KiB
Rust
use clap::{Parser, Subcommand};
|
|
use std::{io::stdout, path::PathBuf};
|
|
|
|
use ynabmunger::csv::{output_csv, read_transactions_from};
|
|
use ynabmunger::ynab::Client;
|
|
|
|
#[derive(Subcommand)]
|
|
enum Command {
|
|
/// List available plans in YNAB
|
|
Plans {
|
|
/// Your YNAB token, available from developer settings in YNAB
|
|
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
|
|
token: String,
|
|
},
|
|
/// List available accounts in a given plan in YNAB
|
|
Accounts {
|
|
/// Your YNAB token, available from developer settings in YNAB
|
|
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
|
|
token: String,
|
|
|
|
/// The name of the YNAB plan to list
|
|
#[arg(short, long, group = "planref")]
|
|
plan: Option<String>,
|
|
|
|
/// Alternatively, give the YNAB plan ID directly
|
|
#[arg(short = 'P', long, group = "planref")]
|
|
plan_id: Option<String>,
|
|
},
|
|
/// List transactions
|
|
///
|
|
/// You have to give a plan to list transactions from. You can optionally
|
|
/// also give an account to show only transactions from that account. You can specify the
|
|
/// number of days, the default is 30.
|
|
Transactions {
|
|
/// Your YNAB token, available from developer settings in YNAB
|
|
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
|
|
token: String,
|
|
|
|
/// The name of the YNAB plan to list transactions from
|
|
#[arg(short, long, group = "planref")]
|
|
plan: Option<String>,
|
|
|
|
/// Alternatively, give the YNAB plan ID directly
|
|
#[arg(short = 'P', long, group = "planref")]
|
|
plan_id: Option<String>,
|
|
|
|
/// The name of the YNAB account to import to
|
|
#[arg(short, long, group = "accountref")]
|
|
account: Option<String>,
|
|
|
|
/// Alternatively, give the YNAB account ID directly
|
|
#[arg(short = 'A', long, group = "accountref")]
|
|
account_id: Option<String>,
|
|
|
|
/// The number of days to look back for transactions
|
|
#[arg(short, long, default_value_t = 30)]
|
|
days: i64,
|
|
|
|
/// Filter transactions by a search term in the payee field. This will match any transaction
|
|
/// whose payee contains the search term, case-insensitive.
|
|
#[arg(short, long)]
|
|
filter: Option<String>,
|
|
},
|
|
/// Convert from a bank export to a CSV you can import manually to YNAB
|
|
Convert {
|
|
#[arg(short, long, help = "Bank export format", default_value_t = String::from("bulder"))]
|
|
format: String,
|
|
|
|
inputs: Vec<PathBuf>,
|
|
},
|
|
/// Read a bank export and import it directly to an account in ynab
|
|
Import {
|
|
/// Your YNAB token, available from developer settings in YNAB
|
|
#[arg(short, long, env = "YNAB_API_TOKEN", hide_env_values = true)]
|
|
token: String,
|
|
|
|
/// The name of the YNAB plan to import to
|
|
#[arg(short, long, group = "planref")]
|
|
plan: Option<String>,
|
|
|
|
/// Alternatively, give the YNAB plan ID directly
|
|
#[arg(short = 'P', long, group = "planref")]
|
|
plan_id: Option<String>,
|
|
|
|
/// The name of the YNAB account to import to
|
|
#[arg(short, long, group = "accountref")]
|
|
account: Option<String>,
|
|
|
|
/// Alternatively, give the YNAB account ID directly
|
|
#[arg(short = 'A', long, group = "accountref")]
|
|
account_id: Option<String>,
|
|
|
|
/// The format of the bank export you are importing
|
|
///
|
|
/// This is different for every bank, and sometimes even the same
|
|
/// bank can have different formats for different account types.
|
|
#[arg(short, long, default_value_t = String::from("bulder"))]
|
|
format: String,
|
|
|
|
inputs: Vec<PathBuf>,
|
|
},
|
|
}
|
|
|
|
#[derive(Parser)]
|
|
struct Cli {
|
|
#[command(subcommand)]
|
|
command: Command,
|
|
}
|
|
|
|
#[tokio::main]
|
|
async fn main() -> anyhow::Result<()> {
|
|
let cli = Cli::parse();
|
|
|
|
match cli.command {
|
|
Command::Plans { token } => {
|
|
let plans = Client::new(&token).list_plans().await?;
|
|
println!("Available plans:");
|
|
for plan in plans {
|
|
println!(" - {}", plan);
|
|
}
|
|
Ok(())
|
|
}
|
|
Command::Accounts {
|
|
token,
|
|
plan,
|
|
plan_id,
|
|
} => {
|
|
let client = Client::new(&token);
|
|
let plan = match (plan, plan_id) {
|
|
(_, Some(id)) => Ok(client.plan_from_id(&id)),
|
|
(Some(name), _) => Ok(client.plan_from_name(&name).await?),
|
|
_ => Err(ynabmunger::Error::Text("no plan given".to_string())),
|
|
}?;
|
|
|
|
let accounts = plan.list_accounts().await?;
|
|
println!("Available accounts in plan:");
|
|
for account in accounts {
|
|
println!(" - {}", account);
|
|
}
|
|
Ok(())
|
|
}
|
|
Command::Transactions {
|
|
token,
|
|
plan,
|
|
plan_id,
|
|
account,
|
|
account_id,
|
|
days,
|
|
filter,
|
|
} => {
|
|
let client = Client::new(&token);
|
|
let plan = match (plan, plan_id) {
|
|
(_, Some(id)) => Ok(client.plan_from_id(&id)),
|
|
(Some(name), _) => Ok(client.plan_from_name(&name).await?),
|
|
_ => Err(ynabmunger::Error::Text("no plan given".to_string())),
|
|
}?;
|
|
|
|
// Account is optional here
|
|
let (account, accountstr) = match (account, account_id) {
|
|
(_, Some(id)) => (Some(plan.account_from_id(&id)), " for account"),
|
|
(Some(name), _) => (Some(plan.account_from_name(&name).await?), " for account"),
|
|
_ => (None, ""),
|
|
};
|
|
|
|
let transactions = match account {
|
|
Some(account) => account.list_transactions(days, filter.as_deref()).await?,
|
|
None => {
|
|
plan.list_transactions(None, days, filter.as_deref())
|
|
.await?
|
|
}
|
|
};
|
|
println!("Transactions{} in the last 30 days:", accountstr);
|
|
for t in transactions {
|
|
println!("{},{},{}", t.date, t.payee, t.format_amount());
|
|
}
|
|
|
|
Ok(())
|
|
}
|
|
Command::Import {
|
|
token,
|
|
plan,
|
|
plan_id,
|
|
account_id,
|
|
account,
|
|
format,
|
|
inputs,
|
|
} => {
|
|
let client = Client::new(&token);
|
|
let plan = match (plan, plan_id) {
|
|
(_, Some(id)) => Ok(client.plan_from_id(&id)),
|
|
(Some(name), _) => Ok(client.plan_from_name(&name).await?),
|
|
_ => Err(ynabmunger::Error::Text("no plan given".to_string())),
|
|
}?;
|
|
let account = match (account, account_id) {
|
|
(_, Some(id)) => Ok(plan.account_from_id(&id)),
|
|
(Some(name), _) => Ok(plan.account_from_name(&name).await?),
|
|
_ => Err(ynabmunger::Error::Text("no account given".to_string())),
|
|
}?;
|
|
|
|
let transactions = read_transactions_from(&inputs, &format)?;
|
|
|
|
account.upload(&transactions).await?;
|
|
Ok(())
|
|
}
|
|
Command::Convert { format, inputs } => {
|
|
let transactions = read_transactions_from(&inputs, &format)?;
|
|
output_csv(stdout(), &transactions)?;
|
|
Ok(())
|
|
}
|
|
}
|
|
}
|