main.rs 4.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207
  1. use chrono::{Date, Utc};
  2. use pickledb::{PickleDb, PickleDbDumpPolicy, SerializationMethod};
  3. use qif_generator::{
  4. account::{Account, AccountType},
  5. split::Split,
  6. transaction::Transaction,
  7. };
  8. use radix_trie::Trie;
  9. use shellexpand::tilde;
  10. use std::fs;
  11. use std::path::{Path, PathBuf};
  12. use std::time::Duration;
  13. use structopt::StructOpt;
  14. mod categories;
  15. use categories::get_category;
  16. use categories::CatStats;
  17. mod import;
  18. mod receipt;
  19. #[cfg(feature = "telegram")]
  20. mod telegram;
  21. mod ui;
  22. fn read_file(f: &str) -> receipt::Purchase {
  23. let json = fs::read_to_string(f).expect("Can't read file");
  24. receipt::parse_purchase(&json)
  25. }
  26. #[cfg(test)]
  27. mod tests {
  28. use super::*;
  29. #[test]
  30. fn test_read_receipt() {
  31. let mut p = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
  32. p.push("tests/resources/test.json");
  33. let full_path = p.to_string_lossy();
  34. let result = read_file(&full_path).items;
  35. assert_eq!(result[0].name, "СИДР 0.5 MAGNERS APP");
  36. assert_eq!(result[0].sum, 17713);
  37. }
  38. }
  39. fn gen_splits(items: &[receipt::Item], cs: &mut CatStats) -> Vec<Split> {
  40. let mut result: Vec<Split> = Vec::new();
  41. for i in items.iter() {
  42. let t = Split::new()
  43. .memo(i.name.as_str())
  44. .amount(-i.sum)
  45. .category(&get_category(i.name.as_str(), cs))
  46. .build();
  47. result.push(t);
  48. }
  49. result
  50. }
  51. fn gen_trans<'a>(
  52. acc: &'a Account,
  53. date: Date<Utc>,
  54. sum: i64,
  55. splits: &'a [Split],
  56. ) -> Result<Transaction<'a>, String> {
  57. let t = Transaction::new(acc)
  58. .date(date)
  59. .memo("New")
  60. .splits(splits)
  61. .build();
  62. match t {
  63. Ok(t) => {
  64. if t.sum() == -sum {
  65. Ok(t)
  66. } else {
  67. Err(format!(
  68. "Total sum is wrong. Expected: {} Got: {}",
  69. sum,
  70. t.sum()
  71. ))
  72. }
  73. }
  74. Err(e) => Err(e),
  75. }
  76. }
  77. /// Configuration for single user
  78. struct User {
  79. /// Telegram user id
  80. uid: i128,
  81. /// Categories statistics for the user
  82. catmap: CatStats,
  83. /// Available accounts for the user
  84. accounts: Vec<String>,
  85. }
  86. const default_db_path: &str = "~/.config/receqif/";
  87. fn prep_user(uid: i128) -> User {
  88. let ten_sec = Duration::from_secs(10);
  89. let path = default_db_path.to_owned() + &uid.to_string() + ".db";
  90. let confpath: &str = &tilde(&path);
  91. let confpath = PathBuf::from(confpath);
  92. let db = PickleDb::load(
  93. &confpath,
  94. PickleDbDumpPolicy::PeriodicDump(ten_sec),
  95. SerializationMethod::Json,
  96. );
  97. let mut db = match db {
  98. Ok(db) => db,
  99. Err(_) => PickleDb::new(
  100. &confpath,
  101. PickleDbDumpPolicy::PeriodicDump(ten_sec),
  102. SerializationMethod::Json,
  103. ),
  104. };
  105. let mut catmap: CatStats = match db.get("catmap") {
  106. Some(v) => v,
  107. None => Trie::new(),
  108. };
  109. let accounts = match db.get("accounts") {
  110. Some(a) => a,
  111. None => vec![],
  112. };
  113. User {
  114. uid: uid,
  115. catmap: catmap,
  116. accounts: accounts,
  117. }
  118. }
  119. /// Search for a pattern in a file and display the lines that contain it.
  120. #[derive(StructOpt)]
  121. struct Cli {
  122. /// The path to the file to read
  123. filename: String,
  124. #[structopt(parse(from_os_str), long, help = "Accounts csv file")]
  125. accounts: Option<PathBuf>,
  126. #[structopt(short, long, default_value = &(default_db_path.to_owned() + "rc.db"))]
  127. database: String,
  128. }
  129. #[cfg(not(tarpaulin_include))]
  130. fn main() {
  131. let args = Cli::from_args();
  132. let confpath: &str = &tilde(&args.database);
  133. let confpath = PathBuf::from(confpath);
  134. let ten_sec = Duration::from_secs(10);
  135. let db = PickleDb::load(
  136. &confpath,
  137. PickleDbDumpPolicy::PeriodicDump(ten_sec),
  138. SerializationMethod::Json,
  139. );
  140. let mut db = match db {
  141. Ok(db) => db,
  142. Err(_) => PickleDb::new(
  143. &confpath,
  144. PickleDbDumpPolicy::PeriodicDump(ten_sec),
  145. SerializationMethod::Json,
  146. ),
  147. };
  148. let mut catmap: CatStats = match db.get("catmap") {
  149. Some(v) => v,
  150. None => Trie::new(),
  151. };
  152. if let Some(filename) = args.accounts {
  153. let accounts = import::read_accounts(Path::new(&filename)).unwrap();
  154. db.set("accounts", &accounts).unwrap();
  155. }
  156. #[cfg(feature = "telegram")]
  157. telegram::bot();
  158. let purchase = read_file(&args.filename);
  159. let splits = gen_splits(&purchase.items, &mut catmap);
  160. let acc = Account::new()
  161. .name("Wallet")
  162. .account_type(AccountType::Cash)
  163. .build();
  164. let t = gen_trans(&acc, purchase.date(), purchase.total_sum(), &splits).unwrap();
  165. print!("{}", acc.to_string());
  166. println!("{}", t.to_string());
  167. db.set("catmap", &catmap).unwrap();
  168. db.dump().unwrap();
  169. #[cfg(feature = "tv")]
  170. {
  171. ui::run_tv();
  172. }
  173. }