diff --git a/.sqlx/query-1b92e8dfc8c9193953af7b2b5360d01d9decfd8b98558fa4997cb7bf03a2897b.json b/.sqlx/query-1b92e8dfc8c9193953af7b2b5360d01d9decfd8b98558fa4997cb7bf03a2897b.json deleted file mode 100644 index d79ae72..0000000 --- a/.sqlx/query-1b92e8dfc8c9193953af7b2b5360d01d9decfd8b98558fa4997cb7bf03a2897b.json +++ /dev/null @@ -1,38 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "SELECT cost_centre.name AS cost_centre_name,\n invoice_item.vat AS vat,\n ROUND(SUM(invoice_item.amount::numeric * invoice_item.net_price_single::numeric), 3)::double precision AS sum_net,\n ROUND(SUM(CASE WHEN invoice_item.vat_exempt THEN (invoice_item.amount::numeric * invoice_item.net_price_single::numeric) else 0 END), 3)::double precision as sum_vat_exempted\n FROM cost_centre\n JOIN invoice_item ON cost_centre.id=invoice_item.cost_centre_id\n GROUP BY cost_centre_name, vat\n ORDER BY cost_centre_name, vat;", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "cost_centre_name", - "type_info": "Varchar" - }, - { - "ordinal": 1, - "name": "vat", - "type_info": "Float8" - }, - { - "ordinal": 2, - "name": "sum_net", - "type_info": "Float8" - }, - { - "ordinal": 3, - "name": "sum_vat_exempted", - "type_info": "Float8" - } - ], - "parameters": { - "Left": [] - }, - "nullable": [ - false, - false, - null, - null - ] - }, - "hash": "1b92e8dfc8c9193953af7b2b5360d01d9decfd8b98558fa4997cb7bf03a2897b" -} diff --git a/.sqlx/query-3b2ca69b508b26c8254e4c097a4e9f95bb46381597958feb967daacaffbb1896.json b/.sqlx/query-3b2ca69b508b26c8254e4c097a4e9f95bb46381597958feb967daacaffbb1896.json new file mode 100644 index 0000000..791ef39 --- /dev/null +++ b/.sqlx/query-3b2ca69b508b26c8254e4c097a4e9f95bb46381597958feb967daacaffbb1896.json @@ -0,0 +1,38 @@ +{ + "db_name": "PostgreSQL", + "query": "SELECT\n cost_centre.name AS cost_centre_name,\n invoice_item.vat AS vat,\n ROUND(SUM(invoice_item.amount::numeric * invoice_item.net_price_single::numeric), 3)::double precision AS sum_net,\n ROUND(SUM(\n CASE\n WHEN invoice_item.vat_exempt\n THEN (invoice_item.amount::numeric * invoice_item.net_price_single::numeric) else 0\n END), 3)::double precision as sum_vat_exempted\n FROM cost_centre\n JOIN invoice_item ON cost_centre.id=invoice_item.cost_centre_id\n GROUP BY cost_centre_name, vat\n ORDER BY cost_centre_name, vat", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "cost_centre_name", + "type_info": "Varchar" + }, + { + "ordinal": 1, + "name": "vat", + "type_info": "Float8" + }, + { + "ordinal": 2, + "name": "sum_net", + "type_info": "Float8" + }, + { + "ordinal": 3, + "name": "sum_vat_exempted", + "type_info": "Float8" + } + ], + "parameters": { + "Left": [] + }, + "nullable": [ + false, + false, + null, + null + ] + }, + "hash": "3b2ca69b508b26c8254e4c097a4e9f95bb46381597958feb967daacaffbb1896" +} diff --git a/.sqlx/query-65dab896dc871b7553b02b3509a2c6e441be6f2fc1d450055af097d15c32eb2a.json b/.sqlx/query-65dab896dc871b7553b02b3509a2c6e441be6f2fc1d450055af097d15c32eb2a.json new file mode 100644 index 0000000..cd8d77d --- /dev/null +++ b/.sqlx/query-65dab896dc871b7553b02b3509a2c6e441be6f2fc1d450055af097d15c32eb2a.json @@ -0,0 +1,31 @@ +{ + "db_name": "PostgreSQL", + "query": "INSERT INTO \"invoice_item\" (\n position,\n invoice_id,\n typ,\n description,\n amount,\n net_price_single,\n vat,\n vat_exempt,\n cost_centre_id,\n project_id)\n VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id", + "describe": { + "columns": [ + { + "ordinal": 0, + "name": "id", + "type_info": "Int8" + } + ], + "parameters": { + "Left": [ + "Int8", + "Int8", + "Varchar", + "Varchar", + "Float8", + "Float8", + "Float8", + "Bool", + "Int8", + "Int8" + ] + }, + "nullable": [ + false + ] + }, + "hash": "65dab896dc871b7553b02b3509a2c6e441be6f2fc1d450055af097d15c32eb2a" +} diff --git a/.sqlx/query-7b1db7975ebf43035e3249faeeee9ad53d93f1ccd648a4baf89f41ca430b6bdd.json b/.sqlx/query-971cdc63e75c5ac1c08517c2f4859e3b11af0bb122b6ecc9e7a394d5ee43565e.json similarity index 50% rename from .sqlx/query-7b1db7975ebf43035e3249faeeee9ad53d93f1ccd648a4baf89f41ca430b6bdd.json rename to .sqlx/query-971cdc63e75c5ac1c08517c2f4859e3b11af0bb122b6ecc9e7a394d5ee43565e.json index 3abeb68..4ce084e 100644 --- a/.sqlx/query-7b1db7975ebf43035e3249faeeee9ad53d93f1ccd648a4baf89f41ca430b6bdd.json +++ b/.sqlx/query-971cdc63e75c5ac1c08517c2f4859e3b11af0bb122b6ecc9e7a394d5ee43565e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT SUM(invoice_item.amount * invoice_item.net_price_single * (1 + invoice_item.vat)) FROM invoice_item WHERE invoice_id=$1", + "query": "SELECT\n SUM(invoice_item.amount * invoice_item.net_price_single * (1 + invoice_item.vat))\n FROM invoice_item\n WHERE invoice_id=$1", "describe": { "columns": [ { @@ -18,5 +18,5 @@ null ] }, - "hash": "7b1db7975ebf43035e3249faeeee9ad53d93f1ccd648a4baf89f41ca430b6bdd" + "hash": "971cdc63e75c5ac1c08517c2f4859e3b11af0bb122b6ecc9e7a394d5ee43565e" } diff --git a/.sqlx/query-ff14a11b98616ca6c0363f7084275ed3d432c108c1aec2e6f15dae2d5e5620bd.json b/.sqlx/query-bdb568ae2ee49580d481c885625f4ca48a27d2e4fd8ad65c14833c239b860ff6.json similarity index 78% rename from .sqlx/query-ff14a11b98616ca6c0363f7084275ed3d432c108c1aec2e6f15dae2d5e5620bd.json rename to .sqlx/query-bdb568ae2ee49580d481c885625f4ca48a27d2e4fd8ad65c14833c239b860ff6.json index 0887696..a402bd5 100644 --- a/.sqlx/query-ff14a11b98616ca6c0363f7084275ed3d432c108c1aec2e6f15dae2d5e5620bd.json +++ b/.sqlx/query-bdb568ae2ee49580d481c885625f4ca48a27d2e4fd8ad65c14833c239b860ff6.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT invoice_item.*, cost_centre.name as \"cost_centre?\" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id WHERE invoice_item.id = $1 ORDER BY invoice_item.position,invoice_item.id", + "query": "SELECT\n invoice_item.*,\n cost_centre.name as \"cost_centre?\"\n FROM invoice_item\n LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id\n WHERE invoice_item.id = $1\n ORDER BY invoice_item.position,invoice_item.id", "describe": { "columns": [ { @@ -84,5 +84,5 @@ false ] }, - "hash": "ff14a11b98616ca6c0363f7084275ed3d432c108c1aec2e6f15dae2d5e5620bd" + "hash": "bdb568ae2ee49580d481c885625f4ca48a27d2e4fd8ad65c14833c239b860ff6" } diff --git a/.sqlx/query-de2c1db846176331f6e43248afd7cdc759a3f38260189505ebd66fedc0d1b2ed.json b/.sqlx/query-c8290f740fdc6c621b868925ba27f6531edfdd69a9ce0ca9419eaebfb63875a1.json similarity index 78% rename from .sqlx/query-de2c1db846176331f6e43248afd7cdc759a3f38260189505ebd66fedc0d1b2ed.json rename to .sqlx/query-c8290f740fdc6c621b868925ba27f6531edfdd69a9ce0ca9419eaebfb63875a1.json index 70f9803..9cadb98 100644 --- a/.sqlx/query-de2c1db846176331f6e43248afd7cdc759a3f38260189505ebd66fedc0d1b2ed.json +++ b/.sqlx/query-c8290f740fdc6c621b868925ba27f6531edfdd69a9ce0ca9419eaebfb63875a1.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT invoice_item.*, cost_centre.name as \"cost_centre?\" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id WHERE invoice_item.invoice_id = $1 ORDER BY invoice_item.position,invoice_item.id", + "query": "SELECT\n invoice_item.*,\n cost_centre.name as \"cost_centre?\"\n FROM invoice_item\n LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id\n WHERE invoice_item.invoice_id = $1\n ORDER BY invoice_item.position,invoice_item.id", "describe": { "columns": [ { @@ -84,5 +84,5 @@ false ] }, - "hash": "de2c1db846176331f6e43248afd7cdc759a3f38260189505ebd66fedc0d1b2ed" + "hash": "c8290f740fdc6c621b868925ba27f6531edfdd69a9ce0ca9419eaebfb63875a1" } diff --git a/.sqlx/query-2776591813552924d67dc607ffd533cb033b8d258dbe5f39ffe00679bd3f5539.json b/.sqlx/query-e3b86d7e19e12ae2a88dcee480af001191c9023fd87537586dc1d45e193a452e.json similarity index 73% rename from .sqlx/query-2776591813552924d67dc607ffd533cb033b8d258dbe5f39ffe00679bd3f5539.json rename to .sqlx/query-e3b86d7e19e12ae2a88dcee480af001191c9023fd87537586dc1d45e193a452e.json index e99b85a..093788f 100644 --- a/.sqlx/query-2776591813552924d67dc607ffd533cb033b8d258dbe5f39ffe00679bd3f5539.json +++ b/.sqlx/query-e3b86d7e19e12ae2a88dcee480af001191c9023fd87537586dc1d45e193a452e.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "SELECT invoice.vendor as invoice_vendor, invoice.invoice_number, invoice.date AS invoice_date, invoice_item.*, cost_centre.name as \"cost_centre?\" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id JOIN invoice ON invoice_item.invoice_id = invoice.id ORDER BY invoice.date,invoice.id,invoice_item.position,invoice_item.id", + "query": "SELECT\n invoice.vendor AS invoice_vendor,\n invoice.invoice_number,\n invoice.date AS invoice_date,\n invoice_item.*,\n cost_centre.name AS \"cost_centre?\"\n FROM invoice_item\n LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id\n JOIN invoice ON invoice_item.invoice_id = invoice.id\n ORDER BY\n invoice.date,\n invoice.id,\n invoice_item.position,\n invoice_item.id", "describe": { "columns": [ { @@ -100,5 +100,5 @@ false ] }, - "hash": "2776591813552924d67dc607ffd533cb033b8d258dbe5f39ffe00679bd3f5539" + "hash": "e3b86d7e19e12ae2a88dcee480af001191c9023fd87537586dc1d45e193a452e" } diff --git a/.sqlx/query-5b5050a11818f0fd5a2edd2d36bb4c0e3278d0217a15c991688f6d587a3ca493.json b/.sqlx/query-f9c6519a736d199e7e8c84b6add4357aabf5b9a3379e7c07de983196f6f14c3f.json similarity index 74% rename from .sqlx/query-5b5050a11818f0fd5a2edd2d36bb4c0e3278d0217a15c991688f6d587a3ca493.json rename to .sqlx/query-f9c6519a736d199e7e8c84b6add4357aabf5b9a3379e7c07de983196f6f14c3f.json index 926998f..f9375db 100644 --- a/.sqlx/query-5b5050a11818f0fd5a2edd2d36bb4c0e3278d0217a15c991688f6d587a3ca493.json +++ b/.sqlx/query-f9c6519a736d199e7e8c84b6add4357aabf5b9a3379e7c07de983196f6f14c3f.json @@ -1,6 +1,6 @@ { "db_name": "PostgreSQL", - "query": "INSERT INTO \"project\" (id, name, description, active, \"default\", \"start\", \"end\")\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id)\n DO UPDATE SET name = $2, description = $3, active = $4, \"default\" = $5, \"start\" = $6, \"end\" = $7\n RETURNING *", + "query": "INSERT INTO \"project\" (id, name, description, active, \"default\", \"start\", \"end\")\n VALUES ($1, $2, $3, $4, $5, $6, $7)\n ON CONFLICT(id)\n DO UPDATE SET name = $2, description = $3, active = $4, \"default\" = $5, \"start\" = $6, \"end\" = $7\n RETURNING *", "describe": { "columns": [ { @@ -60,5 +60,5 @@ false ] }, - "hash": "5b5050a11818f0fd5a2edd2d36bb4c0e3278d0217a15c991688f6d587a3ca493" + "hash": "f9c6519a736d199e7e8c84b6add4357aabf5b9a3379e7c07de983196f6f14c3f" } diff --git a/.sqlx/query-ff0028004594ad7c15160d3274d5dc27c22ae7d1e44c43d0dc635f5455abd45a.json b/.sqlx/query-ff0028004594ad7c15160d3274d5dc27c22ae7d1e44c43d0dc635f5455abd45a.json deleted file mode 100644 index efd9dc5..0000000 --- a/.sqlx/query-ff0028004594ad7c15160d3274d5dc27c22ae7d1e44c43d0dc635f5455abd45a.json +++ /dev/null @@ -1,31 +0,0 @@ -{ - "db_name": "PostgreSQL", - "query": "INSERT INTO \"invoice_item\" (position, invoice_id, typ, description, amount, net_price_single, vat, vat_exempt, cost_centre_id, project_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id", - "describe": { - "columns": [ - { - "ordinal": 0, - "name": "id", - "type_info": "Int8" - } - ], - "parameters": { - "Left": [ - "Int8", - "Int8", - "Varchar", - "Varchar", - "Float8", - "Float8", - "Float8", - "Bool", - "Int8", - "Int8" - ] - }, - "nullable": [ - false - ] - }, - "hash": "ff0028004594ad7c15160d3274d5dc27c22ae7d1e44c43d0dc635f5455abd45a" -} diff --git a/berechenbarkeit-lib/src/lib.rs b/berechenbarkeit-lib/src/lib.rs index ab22722..590998f 100644 --- a/berechenbarkeit-lib/src/lib.rs +++ b/berechenbarkeit-lib/src/lib.rs @@ -1,19 +1,17 @@ use std::{ - path::PathBuf, - num::{ParseFloatError, ParseIntError}, fmt, + num::{ParseFloatError, ParseIntError}, + path::PathBuf, }; use clap::Parser; -use serde::Serialize; -use thiserror::Error; use once_cell::sync::Lazy; -use time::{PrimitiveDateTime, error::ComponentRange}; +use serde::Serialize; use strum::IntoEnumIterator; use strum_macros::EnumIter; -use vendors::regex::{ - METRO, BAUHAUS, IKEA, MEDICALCORNER, MOLTONDISCOUNT, KOKKU, ROHALM -}; +use thiserror::Error; +use time::{error::ComponentRange, PrimitiveDateTime}; +use vendors::regex::{BAUHAUS, IKEA, KOKKU, MEDICALCORNER, METRO, MOLTONDISCOUNT, ROHALM}; pub mod vendors; @@ -47,7 +45,7 @@ pub enum InvoiceVendor { #[derive(Debug, Clone, Serialize)] pub enum InvoiceItemType { Expense, - Credit + Credit, } pub enum InvoiceParser { @@ -100,7 +98,7 @@ impl fmt::Display for InvoiceVendor { Self::MedicalCorner => write!(f, "MedicalCorner"), Self::MoltonDiscount => write!(f, "MoltonDiscount"), Self::Kokku => write!(f, "Kokku"), - Self::Rohalm => write!(f, "Rohalm") + Self::Rohalm => write!(f, "Rohalm"), } } } @@ -129,9 +127,7 @@ impl From for InvoiceParser { } pub fn get_vendors() -> Vec { - InvoiceVendor::iter() - .map(|vendor| vendor.to_string()) - .collect() + InvoiceVendor::iter().map(|vendor| vendor.to_string()).collect() } pub fn get_parser_for_vendor(vendor: Option) -> Option { diff --git a/berechenbarkeit-lib/src/vendors/regex.rs b/berechenbarkeit-lib/src/vendors/regex.rs index 5bfbb39..7fad0d0 100644 --- a/berechenbarkeit-lib/src/vendors/regex.rs +++ b/berechenbarkeit-lib/src/vendors/regex.rs @@ -1,14 +1,8 @@ -use time::{ - Date, Time, PrimitiveDateTime -}; -use regex::{Captures, Regex, RegexBuilder}; +use crate::{Invoice, InvoiceItem, InvoiceItemType, InvoiceMeta, InvoiceParseError, InvoiceVendor, Vendor}; use once_cell::sync::Lazy; +use regex::{Captures, Regex, RegexBuilder}; use std::collections::HashMap; -use crate::{ - Invoice, InvoiceMeta, InvoiceItem, InvoiceItemType, InvoiceVendor, - InvoiceParseError, - Vendor, -}; +use time::{Date, PrimitiveDateTime, Time}; pub struct RegexVendor { invoice_number_regex: Regex, @@ -19,14 +13,22 @@ pub struct RegexVendor { vat_classes: HashMap<&'static str, f64>, default_vat_class: Option, } -#[derive(Default,Debug)] +#[derive(Default, Debug)] pub struct ItemRegex { re: &'static str, multi_line: bool, dot_matches_newline: Option, } impl RegexVendor { - fn new(invoice_number: ItemRegex, invoice_date: ItemRegex, invoice_total: ItemRegex, invoice_item: ItemRegex, invoice_discount_item: Option, vat_entries: Vec<(&'static str, f64)>, default_vat_class: Option) -> RegexVendor { + fn new( + invoice_number: ItemRegex, + invoice_date: ItemRegex, + invoice_total: ItemRegex, + invoice_item: ItemRegex, + invoice_discount_item: Option, + vat_entries: Vec<(&'static str, f64)>, + default_vat_class: Option, + ) -> RegexVendor { let mut vat_map: HashMap<&str, f64> = HashMap::with_capacity(100); for (k, v) in vat_entries { vat_map.insert(k, v); @@ -35,10 +37,26 @@ impl RegexVendor { RegexBuilder::new(re).multi_line(multi_line).dot_matches_new_line(dot_matches_new_line).build().unwrap() }; RegexVendor { - invoice_number_regex: build_re(invoice_number.re, !invoice_number.multi_line, invoice_number.dot_matches_newline.unwrap_or(invoice_number.multi_line)), - invoice_total_regex: build_re(invoice_total.re, !invoice_total.multi_line, invoice_total.dot_matches_newline.unwrap_or(invoice_total.multi_line)), - invoice_date_regex: build_re(invoice_date.re, !invoice_date.multi_line, invoice_date.dot_matches_newline.unwrap_or(invoice_date.multi_line)), - invoice_item_regex: build_re(invoice_item.re, !invoice_item.multi_line, invoice_item.dot_matches_newline.unwrap_or(invoice_date.multi_line)), + invoice_number_regex: build_re( + invoice_number.re, + !invoice_number.multi_line, + invoice_number.dot_matches_newline.unwrap_or(invoice_number.multi_line), + ), + invoice_total_regex: build_re( + invoice_total.re, + !invoice_total.multi_line, + invoice_total.dot_matches_newline.unwrap_or(invoice_total.multi_line), + ), + invoice_date_regex: build_re( + invoice_date.re, + !invoice_date.multi_line, + invoice_date.dot_matches_newline.unwrap_or(invoice_date.multi_line), + ), + invoice_item_regex: build_re( + invoice_item.re, + !invoice_item.multi_line, + invoice_item.dot_matches_newline.unwrap_or(invoice_date.multi_line), + ), invoice_discount_regex: invoice_discount_item.map(|item| Regex::new(item.re).unwrap()), vat_classes: vat_map, default_vat_class, @@ -46,7 +64,9 @@ impl RegexVendor { } pub fn get_meta(&self, invoice_text: &str) -> Result { - let invoice_number: String = self.invoice_number_regex.captures(invoice_text) + let invoice_number: String = self + .invoice_number_regex + .captures(invoice_text) .and_then(|captures| captures.name("INVOICE_NUMBER")) .ok_or(InvoiceParseError::FieldMissingError("INVOICE_NUMBER".to_string()))? .as_str() @@ -62,50 +82,66 @@ impl RegexVendor { } fn get_gross_sum(&self, invoice_text: &str) -> Result { - return Ok(parse_as_float(self.invoice_total_regex.captures(invoice_text) + return Ok(parse_as_float( + self.invoice_total_regex + .captures(invoice_text) .and_then(|c| c.name("SUM")) .ok_or(InvoiceParseError::FieldMissingError("SUM".to_string()))? - .as_str())); + .as_str(), + )); } fn get_date(&self, invoice_text: &str) -> Result { - let date_matches = self.invoice_date_regex.captures(invoice_text) + let date_matches = self + .invoice_date_regex + .captures(invoice_text) .ok_or(InvoiceParseError::FieldMissingError("INVOICE_DATE".to_string()))?; let (y, m, d): (i32, u8, u8) = ( - date_matches.name("year").ok_or(InvoiceParseError::FieldMissingError("date.year".to_string()))?.as_str().parse()?, - date_matches.name("month").ok_or(InvoiceParseError::FieldMissingError("date.month".to_string()))?.as_str().parse()?, - date_matches.name("day").ok_or(InvoiceParseError::FieldMissingError("date.day".to_string()))?.as_str().parse()?, + date_matches + .name("year") + .ok_or(InvoiceParseError::FieldMissingError("date.year".to_string()))? + .as_str() + .parse()?, + date_matches + .name("month") + .ok_or(InvoiceParseError::FieldMissingError("date.month".to_string()))? + .as_str() + .parse()?, + date_matches + .name("day") + .ok_or(InvoiceParseError::FieldMissingError("date.day".to_string()))? + .as_str() + .parse()?, ); let (h, i, s): (u8, u8, u8) = ( match date_matches.name("hour") { Some(re_match) => re_match.as_str().parse()?, - None => 0 + None => 0, }, match date_matches.name("min") { Some(re_match) => re_match.as_str().parse()?, - None => 0 + None => 0, }, match date_matches.name("sec") { Some(re_match) => re_match.as_str().parse()?, - None => 0 + None => 0, }, ); - Ok(PrimitiveDateTime::new( - Date::from_calendar_date(y, m.try_into()?, d)?, - Time::from_hms(h, i, s)?, - )) + Ok(PrimitiveDateTime::new(Date::from_calendar_date(y, m.try_into()?, d)?, Time::from_hms(h, i, s)?)) } pub fn get_items(&self, invoice_text: &str) -> Result, InvoiceParseError> { let mut position_counter: u32 = 1; let discount_items: Vec = match &self.invoice_discount_regex { - Some(re) => invoice_text.lines() + Some(re) => invoice_text + .lines() .filter(|line| re.is_match(line)) .map(|line| self.extract_discount_item_from_capture_groups(re.captures(line).unwrap())) - .collect::,InvoiceParseError>>()?, + .collect::, InvoiceParseError>>()?, None => vec![], }; - let mut items: Vec = self.invoice_item_regex + let mut items: Vec = self + .invoice_item_regex .captures_iter(&invoice_text) .map(|captures| self.extract_item_from_capture_groups(captures, &mut position_counter)) .collect::, InvoiceParseError>>()?; @@ -128,14 +164,13 @@ impl RegexVendor { vat, amount: 1f64, net_total_price: discount, - }) } fn extract_item_from_capture_groups(&self, groups: Captures, pos_counter: &mut u32) -> Result { let pos: u32 = match groups.name("POS") { Some(p) => p.as_str().parse::()?, - None => pos_counter.clone() + None => pos_counter.clone(), }; *pos_counter = *pos_counter + 1u32; let vat: f64 = match groups.name("VAT") { @@ -147,7 +182,7 @@ impl RegexVendor { None => 1f64, }; let amount: f64 = parse_as_float(groups.name("AMOUNT").unwrap().as_str().trim()); - let net_price_single: f64 = match groups.name("NET_PRICE_SINGLE") { + let net_price_single: f64 = match groups.name("NET_PRICE_SINGLE") { Some(net_price_single) => parse_as_float(net_price_single.as_str().trim()), None => (parse_as_float(groups.name("GROSS_PRICE_SINGLE").unwrap().as_str().trim()) * (1f64 - (vat / (1f64 + vat))) * 1000f64).round() / 1000f64, }; @@ -157,7 +192,11 @@ impl RegexVendor { }; Ok(InvoiceItem { - typ: if (net_price_single * amount) >= 0.0f64 { InvoiceItemType::Expense } else { InvoiceItemType::Credit }, + typ: if (net_price_single * amount) >= 0.0f64 { + InvoiceItemType::Expense + } else { + InvoiceItemType::Credit + }, pos, article_number: match groups.name("ARTNR") { Some(m) => m.as_str().to_string(), @@ -172,13 +211,14 @@ impl RegexVendor { } fn extract_description(&self, groups: Captures) -> Result { - Ok(groups.name("DESC") - .ok_or(InvoiceParseError::FieldMissingError("DESC".to_string()))? - .as_str().to_string()) + Ok(groups.name("DESC").ok_or(InvoiceParseError::FieldMissingError("DESC".to_string()))?.as_str().to_string()) } fn get_vat_rate_from_class(&self, class: &str) -> Result { - self.vat_classes.get(class).ok_or_else(|| InvoiceParseError::UnrecognizedVatClass(class.to_string())).copied() + self.vat_classes + .get(class) + .ok_or_else(|| InvoiceParseError::UnrecognizedVatClass(class.to_string())) + .copied() } } @@ -188,7 +228,7 @@ fn parse_as_float(float: &str) -> f64 { let is_negative = raw.contains("-"); let absolute = raw.replace("-", "").replace(",", "."); if is_negative { - return -1f64 * (absolute.parse::().unwrap()) + return -1f64 * (absolute.parse::().unwrap()); } return absolute.parse::().unwrap(); } @@ -204,72 +244,205 @@ impl Vendor for RegexVendor { } } -pub static METRO: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"RECHNUNGS?-? ?NR\.?\:?\s+(?P[\.\d\/]+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"RECHNUNGSDATUM:\s+(?P\d\d)\.(?P\d\d)\.(?P\d{4}) (?P\d\d):(?P\d\d)", multi_line: false, ..ItemRegex::default() }, - ItemRegex {re: r"SUMME EUR\s+(?P[\d\.,\-]+)([\s\-]+(?P[a-zA-Z0-9:\-\., ]+) +[\d\.,\-]+)?", multi_line: false, ..ItemRegex::default() }, - ItemRegex {re: r"^(?P.) (?P\d{6}\.\d) (?P[\d ]{14}) (?P.{31}) (?P.{2}) (?P.{11}) (?P.{10}) (?P.{10}) (?P.{6}) (?P.{11}) (?P.) (?P.{10})[  ](?P.) (?P.+)?$", multi_line: false, ..ItemRegex::default() }, - Some(ItemRegex {re: r"^ {26}(?P.{50}) *(?P.{11}) (?P.)?[ 0-9]{12}$", multi_line: false, ..ItemRegex::default() }), - vec![("A", 0.19f64), ("B", 0.07f64)], - None, -)); +pub static METRO: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"RECHNUNGS?-? ?NR\.?\:?\s+(?P[\.\d\/]+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"RECHNUNGSDATUM:\s+(?P\d\d)\.(?P\d\d)\.(?P\d{4}) (?P\d\d):(?P\d\d)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"SUMME EUR\s+(?P[\d\.,\-]+)([\s\-]+(?P[a-zA-Z0-9:\-\., ]+) +[\d\.,\-]+)?", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"^(?P.) (?P\d{6}\.\d) (?P[\d ]{14}) (?P.{31}) (?P.{2}) (?P.{11}) (?P.{10}) (?P.{10}) (?P.{6}) (?P.{11}) (?P.) (?P.{10})[  ](?P.) (?P.+)?$", // editorconfig-checker-disable-lin + multi_line: false, + ..ItemRegex::default() + }, + Some(ItemRegex { + re: r"^ {26}(?P.{50}) *(?P.{11}) (?P.)?[ 0-9]{12}$", + multi_line: false, + ..ItemRegex::default() + }), + vec![("A", 0.19f64), ("B", 0.07f64)], + None, + ) +}); -pub static BAUHAUS: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"Einzelrechnung\s+Nr\.\s+(?P[\.\d\/]+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Rechnungsdatum\s+(?P\d\d)\.(?P\d\d)\.(?P\d{4})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Zu zahlender Betrag\s+(?P[\d\.,\-]+)\ EUR", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"^(?P\d+)\s+(?P\d{8})\s+(?P.{1,100})\s+(?P\d{1,6}) (ST|KAR)\s+(?P.{1,7})\s+(?P.{1,7})\s+(?P\w)$", multi_line: false, ..ItemRegex::default()}, - None, - vec![("C", 0.19f64)], - None, -)); +pub static BAUHAUS: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"Einzelrechnung\s+Nr\.\s+(?P[\.\d\/]+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Rechnungsdatum\s+(?P\d\d)\.(?P\d\d)\.(?P\d{4})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Zu zahlender Betrag\s+(?P[\d\.,\-]+)\ EUR", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"^(?P\d+)\s+(?P\d{8})\s+(?P.{1,100})\s+(?P\d{1,6}) (ST|KAR)\s+(?P.{1,7})\s+(?P.{1,7})\s+(?P\w)$", // editorconfig-checker-disable-lin + multi_line: false, + ..ItemRegex::default() + }, + None, + vec![("C", 0.19f64)], + None, + ) +}); -pub static IKEA: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"Rechnungsnummer: (?P\w+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Rechnungsdatum:\s+(?P\d{1,2})\.(?P\d{1,2})\.(?P\d{2,4})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Rechnungssumme:\s+€\s+(?P[0-9,\.]+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"^(?P\d{2,3}\.\d{2,3}\.\d{2,3})\s+(?P.+)\s+(?P\d+)\s+(?P\d{1,5},\d{0,2})\s+(?P\d{1,2}) %\s+€ (?P[0-9,\.]+)$", multi_line: false, ..ItemRegex::default()}, - None, - vec![("19", 0.19f64), ("7", 0.07f64)], - None, -)); +pub static IKEA: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"Rechnungsnummer: (?P\w+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Rechnungsdatum:\s+(?P\d{1,2})\.(?P\d{1,2})\.(?P\d{2,4})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Rechnungssumme:\s+€\s+(?P[0-9,\.]+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"^(?P\d{2,3}\.\d{2,3}\.\d{2,3})\s+(?P.+)\s+(?P\d+)\s+(?P\d{1,5},\d{0,2})\s+(?P\d{1,2}) %\s+€ (?P[0-9,\.]+)$", // editorconfig-checker-disable-lin + multi_line: false, + ..ItemRegex::default() + }, + None, + vec![("19", 0.19f64), ("7", 0.07f64)], + None, + ) +}); -pub static MEDICALCORNER: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"Rechnung\s+(?P\w+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Rechnungsdatum:\s+\nKundennummer:\s+\nLieferschein:\s+\nLieferdatum:\s+\nBearbeiter:\s+\n.+\n(?\d{1,2})\.(?\d{2})\.(?\d{4})", multi_line: true, ..ItemRegex::default()}, - ItemRegex {re: r"Gesamt (?\d+,\d{2}) EUR", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"\n\n(?P\d+) (?P[A-Z0-9-_]+([\w&&[^A-Z]]{4})?) (?P.+?) (?P\d+) (?P\d+)% (?P\d+,\d{2}) (?P\d+,\d{2})", multi_line: true, dot_matches_newline: Some(true), ..ItemRegex::default()}, - None, - vec![("0", 0.0f64), ("19", 0.19f64), ("7", 0.07f64)], - None, -)); +pub static MEDICALCORNER: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"Rechnung\s+(?P\w+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Rechnungsdatum:\s+\nKundennummer:\s+\nLieferschein:\s+\nLieferdatum:\s+\nBearbeiter:\s+\n.+\n(?\d{1,2})\.(?\d{2})\.(?\d{4})", + multi_line: true, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Gesamt (?\d+,\d{2}) EUR", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"\n\n(?P\d+) (?P[A-Z0-9-_]+([\w&&[^A-Z]]{4})?) (?P.+?) (?P\d+) (?P\d+)% (?P\d+,\d{2}) (?P\d+,\d{2})", + multi_line: true, + dot_matches_newline: Some(true), + ..ItemRegex::default() + }, + None, + vec![("0", 0.0f64), ("19", 0.19f64), ("7", 0.07f64)], + None, + ) +}); -pub static MOLTONDISCOUNT: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"Rechnungs-Nr\.\s+(?P[\w-]+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Datum\s+(?P\d{1,2})\.(?P\d{2})\.(?P\d{4})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Gesamtsumme:\s+(?P\d+,\d{2})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"\n(?P\d+)\s+(?P.+?)\s+(?P\d+)\s+(?P\d+,\d{2})\s+¬\s+(?P\d+,\d{2})", multi_line: true, dot_matches_newline: Some(true), ..ItemRegex::default()}, - None, - vec![("19", 0.19f64)], - Some(0.19f64), -)); +pub static MOLTONDISCOUNT: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"Rechnungs-Nr\.\s+(?P[\w-]+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Datum\s+(?P\d{1,2})\.(?P\d{2})\.(?P\d{4})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Gesamtsumme:\s+(?P\d+,\d{2})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"\n(?P\d+)\s+(?P.+?)\s+(?P\d+)\s+(?P\d+,\d{2})\s+¬\s+(?P\d+,\d{2})", + multi_line: true, + dot_matches_newline: Some(true), + ..ItemRegex::default() + }, + None, + vec![("19", 0.19f64)], + Some(0.19f64), + ) +}); -pub static KOKKU: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"Rechnungsnummer\s+(?P\w+)", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Rechnungsdatum\s+(?P\d{2})\.(?P\d{2})\.(?P\d{4})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Gesamtsumme\s+inkl\.\s+MwSt\.:\s+(?P\d{1,},\d{2})\s+€", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"\n\n(?P[\w\d -]+?)\s+(?P\d{1,2}\.\d{1,2})%(?P\d+)\s+(?P\d{1,},\d{1,2})\s+€\s+(?P\d{1,},\d{1,2})\s+€", multi_line: true, ..ItemRegex::default()}, - None, - vec![("7.0", 0.07f64)], - None, -)); +pub static KOKKU: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"Rechnungsnummer\s+(?P\w+)", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Rechnungsdatum\s+(?P\d{2})\.(?P\d{2})\.(?P\d{4})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Gesamtsumme\s+inkl\.\s+MwSt\.:\s+(?P\d{1,},\d{2})\s+€", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"\n\n(?P[\w\d -]+?)\s+(?P\d{1,2}\.\d{1,2})%(?P\d+)\s+(?P\d{1,},\d{1,2})\s+€\s+(?P\d{1,},\d{1,2})\s+€", + multi_line: true, + ..ItemRegex::default() + }, + None, + vec![("7.0", 0.07f64)], + None, + ) +}); -pub static ROHALM: Lazy = Lazy::new(|| RegexVendor::new( - ItemRegex {re: r"(?P[\w\d]+)\s+(?P\d+)\s+\d{2}\.\d{2}\.\d{4}\s+\d{2}\.\d{2}\.\d{4}", multi_line: true, ..ItemRegex::default()}, - ItemRegex {re: r"(?P[\w\w]+)\s+(?P\d+)\s+(?P\d{2})\.(?P\d{2})\.(?P\d{4})\s+\d{2}\.\d{2}\.\d{4}", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"Gesamtbetrag\*\s+(?P\d{1,},\d{1,2})", multi_line: false, ..ItemRegex::default()}, - ItemRegex {re: r"\n\n(?P\d+)\s+(?P.+?)\s+(?P\d+)\s+(?P\d{1,},\d{2})\s+(?P\d{1,},\d{2})", multi_line: true, dot_matches_newline: Some(true), ..ItemRegex::default()}, - None, - vec![("19.0", 0.19f64)], - Some(0.19f64), -)); +pub static ROHALM: Lazy = Lazy::new(|| { + RegexVendor::new( + ItemRegex { + re: r"(?P[\w\d]+)\s+(?P\d+)\s+\d{2}\.\d{2}\.\d{4}\s+\d{2}\.\d{2}\.\d{4}", + multi_line: true, + ..ItemRegex::default() + }, + ItemRegex { + re: r"(?P[\w\w]+)\s+(?P\d+)\s+(?P\d{2})\.(?P\d{2})\.(?P\d{4})\s+\d{2}\.\d{2}\.\d{4}", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"Gesamtbetrag\*\s+(?P\d{1,},\d{1,2})", + multi_line: false, + ..ItemRegex::default() + }, + ItemRegex { + re: r"\n\n(?P\d+)\s+(?P.+?)\s+(?P\d+)\s+(?P\d{1,},\d{2})\s+(?P\d{1,},\d{2})", + multi_line: true, + dot_matches_newline: Some(true), + ..ItemRegex::default() + }, + None, + vec![("19.0", 0.19f64)], + Some(0.19f64), + ) +}); diff --git a/src/config.rs b/src/config.rs index ab9286d..bdf963c 100644 --- a/src/config.rs +++ b/src/config.rs @@ -4,4 +4,3 @@ pub struct Config { #[clap(long, env)] pub database_url: String, } - diff --git a/src/db/cost_centres.rs b/src/db/cost_centres.rs index 79907ed..fb40f4b 100644 --- a/src/db/cost_centres.rs +++ b/src/db/cost_centres.rs @@ -1,10 +1,6 @@ -use serde::{ - Serialize, -}; -use sqlx::{ - PgConnection, -}; use crate::db::util::DBResult; +use serde::Serialize; +use sqlx::PgConnection; #[derive(Debug, Clone, Serialize)] pub(crate) struct DBCostCentre { @@ -12,7 +8,6 @@ pub(crate) struct DBCostCentre { pub name: String, } - #[derive(Debug, Clone, Serialize)] pub(crate) struct CostCentreWithSum { pub cost_centre_name: String, @@ -23,23 +18,22 @@ pub(crate) struct CostCentreWithSum { impl DBCostCentre { pub(crate) async fn get_all(connection: &mut PgConnection) -> DBResult> { - sqlx::query_as!(DBCostCentre, r#"SELECT id, name FROM "cost_centre" ORDER BY id ASC"#).fetch_all(connection).await + sqlx::query_as!(DBCostCentre, r#"SELECT id, name FROM "cost_centre" ORDER BY id ASC"#) + .fetch_all(connection) + .await } pub(crate) async fn insert(name: &str, connection: &mut PgConnection) -> DBResult { - Ok(sqlx::query!( - r#"INSERT INTO "cost_centre" (name) VALUES ($1) RETURNING id"#, - name, - ).fetch_one(connection).await?.id) + Ok(sqlx::query!(r#"INSERT INTO "cost_centre" (name) VALUES ($1) RETURNING id"#, name,) + .fetch_one(connection) + .await? + .id) } pub(crate) async fn update(id: i64, name: &str, connection: &mut PgConnection) -> DBResult { - Ok(sqlx::query_as!( - DBCostCentre, - r#"UPDATE "cost_centre" SET "name" = $2 WHERE id = $1 RETURNING *"#, - id, - name, - ).fetch_one(connection).await?) + sqlx::query_as!(DBCostCentre, r#"UPDATE "cost_centre" SET "name" = $2 WHERE id = $1 RETURNING *"#, id, name,) + .fetch_one(connection) + .await } pub(crate) async fn delete(id: i64, connection: &mut PgConnection) -> DBResult<()> { @@ -48,19 +42,30 @@ impl DBCostCentre { } pub(crate) async fn get_summary(connection: &mut PgConnection) -> DBResult> { - Ok(sqlx::query!(r#"SELECT cost_centre.name AS cost_centre_name, - invoice_item.vat AS vat, - ROUND(SUM(invoice_item.amount::numeric * invoice_item.net_price_single::numeric), 3)::double precision AS sum_net, - ROUND(SUM(CASE WHEN invoice_item.vat_exempt THEN (invoice_item.amount::numeric * invoice_item.net_price_single::numeric) else 0 END), 3)::double precision as sum_vat_exempted + Ok(sqlx::query!( + r#"SELECT + cost_centre.name AS cost_centre_name, + invoice_item.vat AS vat, + ROUND(SUM(invoice_item.amount::numeric * invoice_item.net_price_single::numeric), 3)::double precision AS sum_net, + ROUND(SUM( + CASE + WHEN invoice_item.vat_exempt + THEN (invoice_item.amount::numeric * invoice_item.net_price_single::numeric) else 0 + END), 3)::double precision as sum_vat_exempted FROM cost_centre JOIN invoice_item ON cost_centre.id=invoice_item.cost_centre_id GROUP BY cost_centre_name, vat - ORDER BY cost_centre_name, vat;"#) - .fetch_all(connection).await?.into_iter().map(|x| CostCentreWithSum { - cost_centre_name: x.cost_centre_name, - vat: x.vat, - sum_net: x.sum_net.unwrap_or(0f64), - sum_vat_exempted: x.sum_vat_exempted.unwrap_or(0f64) - }).collect()) + ORDER BY cost_centre_name, vat"# + ) + .fetch_all(connection) + .await? + .into_iter() + .map(|x| CostCentreWithSum { + cost_centre_name: x.cost_centre_name, + vat: x.vat, + sum_net: x.sum_net.unwrap_or(0f64), + sum_vat_exempted: x.sum_vat_exempted.unwrap_or(0f64), + }) + .collect()) } } diff --git a/src/db/invoices.rs b/src/db/invoices.rs index 06c2fde..7afb918 100644 --- a/src/db/invoices.rs +++ b/src/db/invoices.rs @@ -1,7 +1,5 @@ -use serde::{ - Deserialize, Serialize -}; -use sqlx::{PgConnection, QueryBuilder, Postgres}; +use serde::{Deserialize, Serialize}; +use sqlx::{PgConnection, Postgres, QueryBuilder}; use time::PrimitiveDateTime; use crate::db::util::DBResult; @@ -35,14 +33,14 @@ impl DBInvoice { object.sum_gross, object.date, object.payment_type, - ).fetch_one(connection).await?.id) + ) + .fetch_one(connection) + .await? + .id) } pub(crate) async fn delete(id: i64, connection: &mut PgConnection) -> DBResult<()> { - let result = sqlx::query!( - r#"DELETE FROM invoice WHERE id=$1"#, - id - ).execute(connection).await?; + let result = sqlx::query!(r#"DELETE FROM invoice WHERE id=$1"#, id).execute(connection).await?; Ok(()) } } @@ -98,12 +96,10 @@ pub(crate) struct InvoiceItemExtended { impl DBInvoiceItem { pub(crate) async fn bulk_insert(connection: &mut PgConnection, objects: Vec) -> DBResult<()> { - let mut qb: QueryBuilder = QueryBuilder::new( - "INSERT INTO invoice_item (position, invoice_id, typ, description, amount, net_price_single, vat, vat_exempt, cost_centre_id, project_id)", - ); + let mut qb: QueryBuilder = + QueryBuilder::new("INSERT INTO invoice_item (position, invoice_id, typ, description, amount, net_price_single, vat, vat_exempt, cost_centre_id, project_id)"); qb.push_values(objects.iter(), |mut b, rec| { - b - .push_bind(rec.position) + b.push_bind(rec.position) .push_bind(rec.invoice_id) .push_bind(&rec.typ) .push_bind(&rec.description) @@ -120,20 +116,71 @@ impl DBInvoiceItem { } pub(crate) async fn get_all(connection: &mut PgConnection) -> DBResult> { - sqlx::query_as!(InvoiceItemExtended, r#"SELECT invoice.vendor as invoice_vendor, invoice.invoice_number, invoice.date AS invoice_date, invoice_item.*, cost_centre.name as "cost_centre?" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id JOIN invoice ON invoice_item.invoice_id = invoice.id ORDER BY invoice.date,invoice.id,invoice_item.position,invoice_item.id"#).fetch_all(connection).await + sqlx::query_as!( + InvoiceItemExtended, + r#"SELECT + invoice.vendor AS invoice_vendor, + invoice.invoice_number, + invoice.date AS invoice_date, + invoice_item.*, + cost_centre.name AS "cost_centre?" + FROM invoice_item + LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id + JOIN invoice ON invoice_item.invoice_id = invoice.id + ORDER BY + invoice.date, + invoice.id, + invoice_item.position, + invoice_item.id"# + ) + .fetch_all(connection) + .await } pub(crate) async fn get_by_id(invoiceitem_id: i64, connection: &mut PgConnection) -> DBResult { - sqlx::query_as!(DBInvoiceItem, r#"SELECT invoice_item.*, cost_centre.name as "cost_centre?" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id WHERE invoice_item.id = $1 ORDER BY invoice_item.position,invoice_item.id"#, invoiceitem_id).fetch_one(connection).await + sqlx::query_as!( + DBInvoiceItem, + r#"SELECT + invoice_item.*, + cost_centre.name as "cost_centre?" + FROM invoice_item + LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id + WHERE invoice_item.id = $1 + ORDER BY invoice_item.position,invoice_item.id"#, + invoiceitem_id + ) + .fetch_one(connection) + .await } pub(crate) async fn get_by_invoice_id(invoice_id: i64, connection: &mut PgConnection) -> DBResult> { - sqlx::query_as!(DBInvoiceItem, r#"SELECT invoice_item.*, cost_centre.name as "cost_centre?" FROM invoice_item LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id WHERE invoice_item.invoice_id = $1 ORDER BY invoice_item.position,invoice_item.id"#, invoice_id).fetch_all(connection).await + sqlx::query_as!( + DBInvoiceItem, + r#"SELECT + invoice_item.*, + cost_centre.name as "cost_centre?" + FROM invoice_item + LEFT OUTER JOIN cost_centre ON invoice_item.cost_centre_id = cost_centre.id + WHERE invoice_item.invoice_id = $1 + ORDER BY invoice_item.position,invoice_item.id"#, + invoice_id + ) + .fetch_all(connection) + .await } pub(crate) async fn calculate_sum_gross_by_invoice_id(invoice_id: i64, connection: &mut PgConnection) -> DBResult { - Ok(sqlx::query!(r#"SELECT SUM(invoice_item.amount * invoice_item.net_price_single * (1 + invoice_item.vat)) FROM invoice_item WHERE invoice_id=$1"#, invoice_id).fetch_one(connection).await?.sum.unwrap_or(0f64)) - + Ok(sqlx::query!( + r#"SELECT + SUM(invoice_item.amount * invoice_item.net_price_single * (1 + invoice_item.vat)) + FROM invoice_item + WHERE invoice_id=$1"#, + invoice_id + ) + .fetch_one(connection) + .await? + .sum + .unwrap_or(0f64)) } pub(crate) async fn update_amount(id: i64, amount: f64, connection: &mut PgConnection) -> DBResult<()> { @@ -142,27 +189,40 @@ impl DBInvoiceItem { } pub(crate) async fn update_cost_centre(id: i64, cost_centre_id: Option, connection: &mut PgConnection) -> DBResult<()> { - sqlx::query!(r#"UPDATE "invoice_item" SET cost_centre_id=$1 WHERE id=$2"#, cost_centre_id, id).execute(connection).await?; + sqlx::query!(r#"UPDATE "invoice_item" SET cost_centre_id=$1 WHERE id=$2"#, cost_centre_id, id) + .execute(connection) + .await?; Ok(()) } pub(crate) async fn update_project(id: i64, project: i64, connection: &mut PgConnection) -> DBResult<()> { - sqlx::query!( - r#"UPDATE "invoice_item" SET project_id = $2 WHERE ID = $1"#, - id, - project, - ).execute(connection).await?; + sqlx::query!(r#"UPDATE "invoice_item" SET project_id = $2 WHERE ID = $1"#, id, project,) + .execute(connection) + .await?; Ok(()) } pub(crate) async fn update_vat_exemption(id: i64, vat_exempt: bool, connection: &mut PgConnection) -> DBResult<()> { - sqlx::query!(r#"UPDATE "invoice_item" SET vat_exempt=$1 WHERE id=$2"#, vat_exempt, id).execute(connection).await?; + sqlx::query!(r#"UPDATE "invoice_item" SET vat_exempt=$1 WHERE id=$2"#, vat_exempt, id) + .execute(connection) + .await?; Ok(()) } pub(crate) async fn insert(object: DBInvoiceItem, connection: &mut PgConnection) -> DBResult { Ok(sqlx::query!( - r#"INSERT INTO "invoice_item" (position, invoice_id, typ, description, amount, net_price_single, vat, vat_exempt, cost_centre_id, project_id) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id"#, + r#"INSERT INTO "invoice_item" ( + position, + invoice_id, + typ, + description, + amount, + net_price_single, + vat, + vat_exempt, + cost_centre_id, + project_id) + VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10) RETURNING id"#, object.position, object.invoice_id, object.typ, @@ -173,6 +233,9 @@ impl DBInvoiceItem { object.vat_exempt, object.cost_centre_id, object.project_id, - ).fetch_one(connection).await?.id) + ) + .fetch_one(connection) + .await? + .id) } } diff --git a/src/db/mod.rs b/src/db/mod.rs index e4175b1..5aff46c 100644 --- a/src/db/mod.rs +++ b/src/db/mod.rs @@ -1,4 +1,4 @@ -pub mod util; -pub mod invoices; pub mod cost_centres; +pub mod invoices; pub mod projects; +pub mod util; diff --git a/src/db/projects.rs b/src/db/projects.rs index b100542..2c87d1b 100644 --- a/src/db/projects.rs +++ b/src/db/projects.rs @@ -1,14 +1,7 @@ -use serde::{ - Deserialize, Serialize, -}; -use sqlx::{ - PgConnection, -}; +use serde::{Deserialize, Serialize}; +use sqlx::PgConnection; -use crate::db::util::{ - DbDate, - DBResult, -}; +use crate::db::util::{DBResult, DbDate}; #[derive(Debug, Clone, Serialize, Deserialize)] pub(crate) struct DBProject { @@ -24,77 +17,68 @@ pub(crate) struct DBProject { impl DBProject { pub(crate) async fn get_ordered_by_id(conn: &mut PgConnection) -> DBResult> { - Ok(sqlx::query_as!( - DBProject, - r#"SELECT * FROM "project" ORDER BY id ASC;"# - ).fetch_all(conn).await?) + sqlx::query_as!(DBProject, r#"SELECT * FROM "project" ORDER BY id ASC;"#).fetch_all(conn).await } pub(crate) async fn get(conn: &mut PgConnection) -> DBResult> { - Ok(sqlx::query_as!( - DBProject, - r#"SELECT * FROM "project" ORDER BY "default" DESC, active DESC, id DESC;"# - ).fetch_all(conn).await?) + sqlx::query_as!(DBProject, r#"SELECT * FROM "project" ORDER BY "default" DESC, active DESC, id DESC;"#) + .fetch_all(conn) + .await } pub(crate) async fn get_by_id(project_id: i64, conn: &mut PgConnection) -> DBResult { - Ok(sqlx::query_as!( - DBProject, - r#"SELECT * FROM "project" WHERE id = $1 ORDER BY id ASC;"#, - project_id, - ).fetch_one(conn).await?) + sqlx::query_as!(DBProject, r#"SELECT * FROM "project" WHERE id = $1 ORDER BY id ASC;"#, project_id,) + .fetch_one(conn) + .await } pub(crate) async fn add(project: DBProject, conn: &mut PgConnection) -> DBResult { - Ok(sqlx::query_as!( - DBProject, - r#"INSERT INTO "project" (name, description, active, "default", "start", "end") VALUES ($1, $2, $3, $4, $5, $6) RETURNING *"#, - project.name, - project.description, - project.active, - project.default, - project.start.datetime, - project.end.datetime, - ).fetch_one(conn).await?) + sqlx::query_as!( + DBProject, + r#"INSERT INTO "project" (name, description, active, "default", "start", "end") VALUES ($1, $2, $3, $4, $5, $6) RETURNING *"#, + project.name, + project.description, + project.active, + project.default, + project.start.datetime, + project.end.datetime, + ) + .fetch_one(conn) + .await } pub(crate) async fn update(project: DBProject, conn: &mut PgConnection) -> DBResult { - Ok(sqlx::query_as!( - DBProject, - r#"INSERT INTO "project" (id, name, description, active, "default", "start", "end") - VALUES ($1, $2, $3, $4, $5, $6, $7) - ON CONFLICT(id) - DO UPDATE SET name = $2, description = $3, active = $4, "default" = $5, "start" = $6, "end" = $7 - RETURNING *"#, - project.id.unwrap(), - project.name, - project.description, - project.active, - project.default, - project.start.datetime, - project.end.datetime, - ).fetch_one(conn).await?) + sqlx::query_as!( + DBProject, + r#"INSERT INTO "project" (id, name, description, active, "default", "start", "end") + VALUES ($1, $2, $3, $4, $5, $6, $7) + ON CONFLICT(id) + DO UPDATE SET name = $2, description = $3, active = $4, "default" = $5, "start" = $6, "end" = $7 + RETURNING *"#, + project.id.unwrap(), + project.name, + project.description, + project.active, + project.default, + project.start.datetime, + project.end.datetime, + ) + .fetch_one(conn) + .await } pub(crate) async fn delete(project_id: i64, conn: &mut PgConnection) -> DBResult<()> { - sqlx::query!( - r#"DELETE FROM "project" WHERE id=$1"#, - project_id, - ).execute(conn).await?; + sqlx::query!(r#"DELETE FROM "project" WHERE id=$1"#, project_id).execute(conn).await?; Ok(()) } pub(crate) async fn set_default(project_id: i64, conn: &mut PgConnection) -> DBResult<()> { Self::clear_default(conn).await?; - sqlx::query!( - r#"UPDATE "project" SET "default" = true WHERE id = $1;"#, - project_id, - ).execute(conn).await?; + sqlx::query!(r#"UPDATE "project" SET "default" = true WHERE id = $1;"#, project_id,).execute(conn).await?; Ok(()) } pub(crate) async fn clear_default(conn: &mut PgConnection) -> DBResult<()> { - sqlx::query!(r#"UPDATE "project" SET "default" = false WHERE "default";"#) - .execute(conn).await?; + sqlx::query!(r#"UPDATE "project" SET "default" = false WHERE "default";"#).execute(conn).await?; Ok(()) } } diff --git a/src/db/util.rs b/src/db/util.rs index d571295..0d2c318 100644 --- a/src/db/util.rs +++ b/src/db/util.rs @@ -1,28 +1,12 @@ -use std::fmt; use axum::{ async_trait, - extract::{ - FromRef, - FromRequestParts - }, - http::{ - StatusCode, - request::Parts, - }, -}; -use serde::{ - Deserialize, - Serialize, -}; -use sqlx::{ - Postgres, - pool::PoolConnection, - postgres::PgPool -}; -use time::{ - macros::format_description, - PrimitiveDateTime, + extract::{FromRef, FromRequestParts}, + http::{request::Parts, StatusCode}, }; +use serde::{Deserialize, Serialize}; +use sqlx::{pool::PoolConnection, postgres::PgPool, Postgres}; +use std::fmt; +use time::{macros::format_description, PrimitiveDateTime}; pub(crate) type DBResult = std::result::Result; @@ -42,15 +26,17 @@ impl fmt::Display for DbDate { } impl From> for DbDate { - fn from(e: Option) -> Self{ - DbDate {datetime: e} + fn from(e: Option) -> Self { + DbDate { datetime: e } } } #[async_trait] impl FromRequestParts for DatabaseConnection - where PgPool: FromRef, - S: Send + Sync, { +where + PgPool: FromRef, + S: Send + Sync, +{ type Rejection = (StatusCode, String); async fn from_request_parts(_parts: &mut Parts, state: &S) -> Result { diff --git a/src/handlers/cost_centre.rs b/src/handlers/cost_centre.rs index b24f4e2..e29f50a 100644 --- a/src/handlers/cost_centre.rs +++ b/src/handlers/cost_centre.rs @@ -1,20 +1,9 @@ +use crate::db::{cost_centres::DBCostCentre, util::DatabaseConnection}; +use crate::{utils::make_htmx_redirect, AppError, HtmlTemplate}; use askama::Template; -use axum::{ - Form, - extract::Path, - http::HeaderMap, -}; +use axum::{extract::Path, http::HeaderMap, Form}; use axum_core::response::IntoResponse; use serde::Deserialize; -use crate::{ - AppError, - HtmlTemplate, - utils::make_htmx_redirect, -}; -use crate::db::{ - util::DatabaseConnection, - cost_centres::DBCostCentre -}; #[derive(Template)] #[template(path = "cost_centre/list.html")] @@ -25,7 +14,6 @@ struct CostCentreListTemplate { pub(crate) async fn cost_centre_list(DatabaseConnection(mut conn): DatabaseConnection) -> Result { let cost_centres = DBCostCentre::get_all(&mut conn).await?; Ok(HtmlTemplate(CostCentreListTemplate { cost_centres })) - } #[derive(Deserialize, Debug)] @@ -55,7 +43,7 @@ pub(crate) async fn update( pub(crate) async fn cost_centre_delete( request_headers: HeaderMap, DatabaseConnection(mut conn): DatabaseConnection, - Path(cost_centre_id): Path + Path(cost_centre_id): Path, ) -> Result { DBCostCentre::delete(cost_centre_id, &mut conn).await?; make_htmx_redirect(request_headers, "cost_centres") diff --git a/src/handlers/home.rs b/src/handlers/home.rs index 1917b61..72000e0 100644 --- a/src/handlers/home.rs +++ b/src/handlers/home.rs @@ -1,6 +1,6 @@ +use crate::HtmlTemplate; use askama::Template; use axum::response::IntoResponse; -use crate::HtmlTemplate; #[derive(Template)] #[template(path = "home.html")] diff --git a/src/handlers/invoice.rs b/src/handlers/invoice.rs index 0add41e..a4a2cfb 100644 --- a/src/handlers/invoice.rs +++ b/src/handlers/invoice.rs @@ -1,35 +1,23 @@ -use std::collections::HashSet; -use std::fs::File; -use std::io::{ - Read, - Write, -}; -use std::str::FromStr; -use askama::Template; -use axum::body::Bytes; -use axum::extract::{Path, RawForm}; -use axum::http::{ - StatusCode, - HeaderMap, -}; -use axum::Json; -use axum::response::{IntoResponse, Redirect}; -use serde::Serialize; -use berechenbarkeit_lib::{ - InvoiceVendor, InvoiceItemType, - Vendor, - InvoiceParser, - get_parser_for_vendor, -}; -use crate::{AppError, HtmlTemplate}; use crate::db::{ - util::DatabaseConnection, cost_centres::DBCostCentre, invoices::{DBInvoice, DBInvoiceItem}, projects::DBProject, + util::DatabaseConnection, }; +use crate::{AppError, HtmlTemplate}; +use askama::Template; +use axum::body::Bytes; +use axum::extract::{Path, RawForm}; +use axum::http::{HeaderMap, StatusCode}; +use axum::response::{IntoResponse, Redirect}; +use axum::Json; use axum_typed_multipart::{TryFromMultipart, TypedMultipart}; - +use berechenbarkeit_lib::{get_parser_for_vendor, InvoiceItemType, InvoiceParser, InvoiceVendor, Vendor}; +use serde::Serialize; +use std::collections::HashSet; +use std::fs::File; +use std::io::{Read, Write}; +use std::str::FromStr; #[derive(TryFromMultipart, Debug)] pub(crate) struct InvoiceUploadRequest { @@ -44,27 +32,34 @@ pub(crate) async fn invoice_add_upload(DatabaseConnection(mut conn): DatabaseCon let file = data.file; let parsed_invoice = match parser.unwrap() { - InvoiceParser::Regex(p) => p.extract_invoice_data(&file, vendor.unwrap())? + InvoiceParser::Regex(p) => p.extract_invoice_data(&file, vendor.unwrap())?, }; let invoice_id = DBInvoice::insert(parsed_invoice.clone().into(), &mut conn).await?; - DBInvoiceItem::bulk_insert(&mut conn, (parsed_invoice.items).into_iter().map(|i| DBInvoiceItem { - id: None, - invoice_id, - position: i.pos as i64, - typ: match i.typ { - InvoiceItemType::Credit => "Credit".to_string(), - InvoiceItemType::Expense => "Expense".to_string() - }, - description: i.description.clone(), - amount: i.amount, - net_price_single: i.net_price_single, - vat: i.vat, - vat_exempt: false, - cost_centre_id: None, - cost_centre: None, - project_id: None, - }).collect()).await?; + DBInvoiceItem::bulk_insert( + &mut conn, + (parsed_invoice.items) + .into_iter() + .map(|i| DBInvoiceItem { + id: None, + invoice_id, + position: i.pos as i64, + typ: match i.typ { + InvoiceItemType::Credit => "Credit".to_string(), + InvoiceItemType::Expense => "Expense".to_string(), + }, + description: i.description.clone(), + amount: i.amount, + net_price_single: i.net_price_single, + vat: i.vat, + vat_exempt: false, + cost_centre_id: None, + cost_centre: None, + project_id: None, + }) + .collect(), + ) + .await?; let file_storage_base_path = std::env::var("BERECHENBARKEIT_STORAGE_BASE_PATH"); if file_storage_base_path.is_ok() { @@ -76,7 +71,6 @@ pub(crate) async fn invoice_add_upload(DatabaseConnection(mut conn): DatabaseCon Ok(Redirect::to(&format!("/invoice/{}/edit", invoice_id))) } - #[derive(Template)] #[template(path = "invoice/edit.html")] struct InvoiceEditTemplate { @@ -84,7 +78,7 @@ struct InvoiceEditTemplate { invoice_items: Vec, cost_centres: Vec, projects: Vec, - diff_invoice_item_sum: f64 + diff_invoice_item_sum: f64, } pub(crate) async fn download(Path(invoice_id): Path) -> Result<(StatusCode, HeaderMap, Vec), impl IntoResponse> { @@ -93,7 +87,7 @@ pub(crate) async fn download(Path(invoice_id): Path) -> Result<(StatusCode, if file_storage_base_path.is_ok() { let filepath = format!("{}/invoice-{}.pdf", file_storage_base_path.unwrap(), invoice_id); if File::open(filepath).and_then(|mut f| f.read_to_end(&mut pdf_content)).is_err() { - return Err((StatusCode::NOT_FOUND, "No invoice could be found")) + return Err((StatusCode::NOT_FOUND, "No invoice could be found")); } } let mut response_headers = HeaderMap::new(); @@ -109,22 +103,17 @@ pub(crate) async fn invoice_edit(DatabaseConnection(mut conn): DatabaseConnectio let cost_centres = DBCostCentre::get_all(&mut conn).await?; let projects = DBProject::get(&mut conn).await?; let diff_invoice_item_sum = f64::round((invoice.sum_gross - DBInvoiceItem::calculate_sum_gross_by_invoice_id(invoice_id, &mut conn).await?) * 1000f64) / 1000f64; - let used_project_ids: Vec<_> = invoice_items - .clone() - .into_iter() - .map(|invoice_item| invoice_item.project_id) - .collect(); + let used_project_ids: Vec<_> = invoice_items.clone().into_iter().map(|invoice_item| invoice_item.project_id).collect(); Ok(HtmlTemplate(InvoiceEditTemplate { invoice, invoice_items, cost_centres, projects: projects.into_iter().filter(|p| p.active || used_project_ids.contains(&p.id)).collect(), - diff_invoice_item_sum + diff_invoice_item_sum, })) } - #[derive(Template)] #[template(path = "invoice/delete_confirm.html")] struct InvoiceDeleteConfirmTemplate { @@ -133,9 +122,7 @@ struct InvoiceDeleteConfirmTemplate { pub(crate) async fn invoice_delete_confirm(DatabaseConnection(mut conn): DatabaseConnection, Path(invoice_id): Path) -> Result { let invoice = DBInvoice::get_by_id(invoice_id, &mut conn).await?; - Ok(HtmlTemplate(InvoiceDeleteConfirmTemplate { - invoice, - })) + Ok(HtmlTemplate(InvoiceDeleteConfirmTemplate { invoice })) } pub(crate) async fn invoice_delete(DatabaseConnection(mut conn): DatabaseConnection, Path(invoice_id): Path) -> Result { @@ -144,35 +131,37 @@ pub(crate) async fn invoice_delete(DatabaseConnection(mut conn): DatabaseConnect Ok(Redirect::to("/invoices")) } - - #[derive(Serialize)] struct InvoiceItemSplitResponse { new_id: i64, } -pub(crate) async fn invoice_item_split(DatabaseConnection(mut conn): DatabaseConnection, Path((_invoice_id, invoiceitem_id)): Path<(i64, i64)>) -> Result { +pub(crate) async fn invoice_item_split( + DatabaseConnection(mut conn): DatabaseConnection, + Path((_invoice_id, invoiceitem_id)): Path<(i64, i64)>, +) -> Result { let invoice_item = DBInvoiceItem::get_by_id(invoiceitem_id, &mut conn).await?; - let new_id = DBInvoiceItem::insert(DBInvoiceItem { - id: None, - position: invoice_item.position, - invoice_id: invoice_item.invoice_id, - typ: invoice_item.typ, - description: invoice_item.description, - amount: 0f64, - net_price_single: invoice_item.net_price_single, - vat: invoice_item.vat, - vat_exempt: false, - cost_centre_id: None, - cost_centre: None, - project_id: None, - }, &mut conn).await?; - Ok(Json(InvoiceItemSplitResponse { - new_id - })) + let new_id = DBInvoiceItem::insert( + DBInvoiceItem { + id: None, + position: invoice_item.position, + invoice_id: invoice_item.invoice_id, + typ: invoice_item.typ, + description: invoice_item.description, + amount: 0f64, + net_price_single: invoice_item.net_price_single, + vat: invoice_item.vat, + vat_exempt: false, + cost_centre_id: None, + cost_centre: None, + project_id: None, + }, + &mut conn, + ) + .await?; + Ok(Json(InvoiceItemSplitResponse { new_id })) } - pub(crate) async fn invoice_edit_submit(DatabaseConnection(mut conn): DatabaseConnection, Path(invoice_id): Path, RawForm(form): RawForm) -> Result { let form_data = serde_html_form::from_bytes::>(&form)?; @@ -183,7 +172,7 @@ pub(crate) async fn invoice_edit_submit(DatabaseConnection(mut conn): DatabaseCo let (invoiceitem_id, data_type) = form_field.0.split_once('-').unwrap(); let invoiceitem_id = invoiceitem_id.parse()?; if data_type == "amount" { - DBInvoiceItem::update_amount(invoiceitem_id, f64::from_str(&form_field. 1)?, &mut conn).await?; + DBInvoiceItem::update_amount(invoiceitem_id, f64::from_str(&form_field.1)?, &mut conn).await?; } else if data_type == "costcentre" { let mut cost_centre_id = None; if !form_field.1.is_empty() { @@ -192,7 +181,7 @@ pub(crate) async fn invoice_edit_submit(DatabaseConnection(mut conn): DatabaseCo DBInvoiceItem::update_cost_centre(invoiceitem_id, cost_centre_id, &mut conn).await?; } else if data_type == "vatexempt" { if form_field.1 != "on" { - continue + continue; } vat_exempt_items_changed.insert(invoiceitem_id); DBInvoiceItem::update_vat_exemption(invoiceitem_id, true, &mut conn).await?; @@ -202,7 +191,7 @@ pub(crate) async fn invoice_edit_submit(DatabaseConnection(mut conn): DatabaseCo DBInvoiceItem::update_project(invoiceitem_id, project_id, &mut conn).await?; } } - }; + } // As html only send the value if they're checked, we need to set all other values to null. for ii in DBInvoiceItem::get_by_invoice_id(invoice_id, &mut conn).await? { @@ -214,7 +203,6 @@ pub(crate) async fn invoice_edit_submit(DatabaseConnection(mut conn): DatabaseCo Ok(Redirect::to(&format!("/invoice/{}/edit", invoice_id))) } - #[derive(Template)] #[template(path = "invoice/list.html")] struct InvoiceListTemplate { @@ -224,5 +212,4 @@ struct InvoiceListTemplate { pub(crate) async fn invoice_list(DatabaseConnection(mut conn): DatabaseConnection) -> Result { let invoices = DBInvoice::get_all(&mut conn).await?; Ok(HtmlTemplate(InvoiceListTemplate { invoices })) - } diff --git a/src/handlers/mod.rs b/src/handlers/mod.rs index db4afd3..0f188d9 100644 --- a/src/handlers/mod.rs +++ b/src/handlers/mod.rs @@ -1,5 +1,5 @@ -pub mod invoice; pub mod cost_centre; pub mod home; -pub mod summary; +pub mod invoice; pub mod projects; +pub mod summary; diff --git a/src/handlers/projects.rs b/src/handlers/projects.rs index b63fe49..e885574 100644 --- a/src/handlers/projects.rs +++ b/src/handlers/projects.rs @@ -1,24 +1,18 @@ use askama::Template; -use axum::extract::{Path}; -use axum::Form; -use axum::response::Redirect; +use axum::extract::Path; use axum::http::HeaderMap; +use axum::response::Redirect; +use axum::Form; use axum_core::response::IntoResponse; use serde::Deserialize; -use time::{ - macros::format_description, - PrimitiveDateTime, -}; +use time::{macros::format_description, PrimitiveDateTime}; -use crate::{AppError, HtmlTemplate}; use crate::db::{ - util::{ - DbDate, - DatabaseConnection - }, projects::DBProject, + util::{DatabaseConnection, DbDate}, }; use crate::utils::make_htmx_redirect; +use crate::{AppError, HtmlTemplate}; #[derive(Deserialize, Debug)] pub(crate) struct ProjectForm { @@ -44,7 +38,7 @@ struct ProjectListTemplate { #[derive(Template)] #[template(path = "projects/edit.html")] struct ProjectEditTemplate { - project: DBProject + project: DBProject, } #[derive(Template)] @@ -62,8 +56,12 @@ impl From for DBProject { description: e.description, active: html_checkbox_to_boolean(e.active), default: html_checkbox_to_boolean(e.default), - start: DbDate {datetime: parse_date_into_option(e.start)}, - end: DbDate {datetime: parse_date_into_option(e.end)}, + start: DbDate { + datetime: parse_date_into_option(e.start), + }, + end: DbDate { + datetime: parse_date_into_option(e.end), + }, } } } @@ -77,37 +75,34 @@ pub(crate) async fn new_project_page() -> Result { Ok(HtmlTemplate(ProjectNewTemplate {})) } -pub(crate) async fn edit_project_page( - DatabaseConnection(mut conn): DatabaseConnection, - Path(project_id): Path, - ) -> Result { +pub(crate) async fn edit_project_page(DatabaseConnection(mut conn): DatabaseConnection, Path(project_id): Path) -> Result { let project = DBProject::get_by_id(project_id, &mut conn).await?; Ok(HtmlTemplate(ProjectEditTemplate { project })) } -pub(crate) async fn add( - DatabaseConnection(mut conn): DatabaseConnection, - Form(project_form): Form, - ) -> Result { +pub(crate) async fn add(DatabaseConnection(mut conn): DatabaseConnection, Form(project_form): Form) -> Result { DBProject::add(DBProject::from(project_form), &mut conn).await?; Ok(Redirect::to("/projects")) } pub(crate) async fn update( - req_headers: HeaderMap, - DatabaseConnection(mut conn): DatabaseConnection, - Path(project_id): Path, - Form(project_form): Form, - ) -> Result { - DBProject::update(DBProject {id: Some(project_id), ..DBProject::from(project_form)}, &mut conn).await?; + req_headers: HeaderMap, + DatabaseConnection(mut conn): DatabaseConnection, + Path(project_id): Path, + Form(project_form): Form, +) -> Result { + DBProject::update( + DBProject { + id: Some(project_id), + ..DBProject::from(project_form) + }, + &mut conn, + ) + .await?; make_htmx_redirect(req_headers, "/projects") } -pub(crate) async fn delete( - req_headers: HeaderMap, - DatabaseConnection(mut conn): DatabaseConnection, - Path(project_id): Path - ) -> Result { +pub(crate) async fn delete(req_headers: HeaderMap, DatabaseConnection(mut conn): DatabaseConnection, Path(project_id): Path) -> Result { DBProject::delete(project_id, &mut conn).await?; make_htmx_redirect(req_headers, "/projects") } @@ -121,10 +116,7 @@ pub(crate) async fn set_default( make_htmx_redirect(req_headers, "/projects") } -pub(crate) async fn clear_default( - req_headers: HeaderMap, - DatabaseConnection(mut conn): DatabaseConnection, -) -> Result { +pub(crate) async fn clear_default(req_headers: HeaderMap, DatabaseConnection(mut conn): DatabaseConnection) -> Result { DBProject::clear_default(&mut conn).await?; make_htmx_redirect(req_headers, "/projects") } diff --git a/src/handlers/summary.rs b/src/handlers/summary.rs index 65b4bdb..0460979 100644 --- a/src/handlers/summary.rs +++ b/src/handlers/summary.rs @@ -1,11 +1,11 @@ -use askama::Template; -use axum_core::response::IntoResponse; -use crate::{AppError, HtmlTemplate}; use crate::db::{ cost_centres::{CostCentreWithSum, DBCostCentre}, + invoices::DBInvoiceItem, util::DatabaseConnection, - invoices::DBInvoiceItem }; +use crate::{AppError, HtmlTemplate}; +use askama::Template; +use axum_core::response::IntoResponse; use http::header; #[derive(Template)] @@ -19,52 +19,73 @@ pub(crate) async fn summary_overview(DatabaseConnection(mut conn): DatabaseConne Ok(HtmlTemplate(SummaryOverview { sums })) } -pub(crate) async fn summary_csv_aggregated( - DatabaseConnection(mut conn): DatabaseConnection, -) -> Result { +pub(crate) async fn summary_csv_aggregated(DatabaseConnection(mut conn): DatabaseConnection) -> Result { let sums = DBCostCentre::get_summary(&mut conn).await?; let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); wtr.write_record(["kostenstelle", "mwst_satz", "summe_netto", "summe_mwst_befreit"])?; for record in sums { - wtr.write_record([record.cost_centre_name, record.vat.to_string(), record.sum_net.to_string(), record.sum_vat_exempted.to_string()])?; + wtr.write_record([ + record.cost_centre_name, + record.vat.to_string(), + record.sum_net.to_string(), + record.sum_vat_exempted.to_string(), + ])?; } let csv_string = String::from_utf8(wtr.into_inner()?)?; Ok(( [ (header::CONTENT_TYPE, "text/csv; charset=utf-8"), - ( - header::CONTENT_DISPOSITION, - "attachment; filename=\"berechenbarkeit-aggregated.csv\"", - ), + (header::CONTENT_DISPOSITION, "attachment; filename=\"berechenbarkeit-aggregated.csv\""), ], csv_string, )) } - -pub(crate) async fn summary_csv_raw( - DatabaseConnection(mut conn): DatabaseConnection, -) -> Result { +pub(crate) async fn summary_csv_raw(DatabaseConnection(mut conn): DatabaseConnection) -> Result { let items = DBInvoiceItem::get_all(&mut conn).await?; let mut wtr = csv::WriterBuilder::new().from_writer(vec![]); - wtr.write_record(["haendler", "rechnungsdatum", "rechnungsnummer", "typ", "beschreibung", "menge", "einzelpreis_netto", "gesamtpreis_netto", "mwst_satz", "mwst_befreit", "kostenstelle"])?; + wtr.write_record([ + "haendler", + "rechnungsdatum", + "rechnungsnummer", + "typ", + "beschreibung", + "menge", + "einzelpreis_netto", + "gesamtpreis_netto", + "mwst_satz", + "mwst_befreit", + "kostenstelle", + ])?; for record in items { - wtr.write_record([record.invoice_vendor, record.invoice_date.to_string(), record.invoice_number, record.typ, record.description, record.amount.to_string(), record.net_price_single.to_string(), (record.net_price_single * record.amount).to_string(), record.vat.to_string(), match record.vat_exempt { true => "true".to_string(), false => "false".to_string()}, record.cost_centre.unwrap_or_else(|| "".to_string())])?; + wtr.write_record([ + record.invoice_vendor, + record.invoice_date.to_string(), + record.invoice_number, + record.typ, + record.description, + record.amount.to_string(), + record.net_price_single.to_string(), + (record.net_price_single * record.amount).to_string(), + record.vat.to_string(), + match record.vat_exempt { + true => "true".to_string(), + false => "false".to_string(), + }, + record.cost_centre.unwrap_or_else(|| "".to_string()), + ])?; } let csv_string = String::from_utf8(wtr.into_inner()?)?; Ok(( [ (header::CONTENT_TYPE, "text/csv; charset=utf-8"), - ( - header::CONTENT_DISPOSITION, - "attachment; filename=\"berechenbarkeit-raw.csv\"", - ), + (header::CONTENT_DISPOSITION, "attachment; filename=\"berechenbarkeit-raw.csv\""), ], csv_string, )) diff --git a/src/main.rs b/src/main.rs index 7ba36b0..fa4ffe4 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,12 +1,10 @@ +use crate::config::Config; use askama::Template; -use axum::{ - http::StatusCode, - response::IntoResponse, Router, -}; use axum::extract::{DefaultBodyLimit, MatchedPath}; use axum::http::Request; use axum::response::{Html, Response}; -use axum::routing::{get, post, delete, put}; +use axum::routing::{delete, get, post, put}; +use axum::{http::StatusCode, response::IntoResponse, Router}; use clap::Parser; use sqlx::postgres::PgPoolOptions; use tower_http::services::ServeDir; @@ -14,29 +12,22 @@ use tower_http::trace::TraceLayer; use tracing::info_span; use tracing_subscriber::layer::SubscriberExt; use tracing_subscriber::util::SubscriberInitExt; -use crate::config::Config; -pub mod handlers; -mod db; mod config; +mod db; +pub mod handlers; mod utils; #[tokio::main] async fn main() { tracing_subscriber::registry() - .with( - tracing_subscriber::EnvFilter::try_from_default_env() - .unwrap_or_else(|_| "berechenbarkeit=debug,tower_http=debug".into()), - ) + .with(tracing_subscriber::EnvFilter::try_from_default_env().unwrap_or_else(|_| "berechenbarkeit=debug,tower_http=debug".into())) .with(tracing_subscriber::fmt::layer()) .init(); let config = Config::parse(); - let db_pool = PgPoolOptions::new() - .connect(&config.database_url) - .await - .expect("sqlx: could not connect to database_url"); + let db_pool = PgPoolOptions::new().connect(&config.database_url).await.expect("sqlx: could not connect to database_url"); // This embeds database migrations in the application binary so we can ensure the database // is migrated correctly on startup @@ -49,13 +40,25 @@ async fn main() { let app = Router::new() .route("/invoices", get(handlers::invoice::invoice_list)) - .route("/invoice/upload", post(handlers::invoice::invoice_add_upload)).layer(DefaultBodyLimit::max(10 * 1024 * 1024)) + .route("/invoice/upload", post(handlers::invoice::invoice_add_upload)) + .layer(DefaultBodyLimit::max(10 * 1024 * 1024)) .route("/invoice/:invoice_id/pdf", get(handlers::invoice::download)) .route("/invoice/:invoice_id/invoiceitem/:invoiceitem_id/split", post(handlers::invoice::invoice_item_split)) - .route("/invoice/:invoice_id/edit", get(handlers::invoice::invoice_edit).post(handlers::invoice::invoice_edit_submit)) - .route("/invoice/:invoice_id/delete", get(handlers::invoice::invoice_delete_confirm).post(handlers::invoice::invoice_delete)) + .route( + "/invoice/:invoice_id/edit", + get(handlers::invoice::invoice_edit).post(handlers::invoice::invoice_edit_submit), + ) + .route( + "/invoice/:invoice_id/delete", + get(handlers::invoice::invoice_delete_confirm).post(handlers::invoice::invoice_delete), + ) .route("/projects", get(handlers::projects::list).post(handlers::projects::add)) - .route("/projects/default", put(handlers::projects::set_default).post(handlers::projects::set_default).delete(handlers::projects::clear_default)) + .route( + "/projects/default", + put(handlers::projects::set_default) + .post(handlers::projects::set_default) + .delete(handlers::projects::clear_default), + ) .route("/projects/new", get(handlers::projects::new_project_page)) .route("/projects/:id", delete(handlers::projects::delete).put(handlers::projects::update)) .route("/projects/:id/edit", get(handlers::projects::edit_project_page)) @@ -67,52 +70,39 @@ async fn main() { .route("/summary/raw_csv", get(handlers::summary::summary_csv_raw)) .route("/", get(handlers::home::home)) .with_state(db_pool) - .layer( - TraceLayer::new_for_http() - .make_span_with(|request: &Request<_>| { - // Log the matched route's path (with placeholders not filled in). - // Use request.uri() or OriginalUri if you want the real path. - let matched_path = request - .extensions() - .get::() - .map(MatchedPath::as_str); - - info_span!( - "http_request", - method = ?request.method(), - matched_path, - ) - }) - ) + .layer(TraceLayer::new_for_http().make_span_with(|request: &Request<_>| { + // Log the matched route's path (with placeholders not filled in). + // Use request.uri() or OriginalUri if you want the real path. + let matched_path = request.extensions().get::().map(MatchedPath::as_str); + + info_span!( + "http_request", + method = ?request.method(), + matched_path, + ) + })) .nest_service("/assets", ServeDir::new(assets_base_path)); // run our app with hyper - let listener = tokio::net::TcpListener::bind("127.0.0.1:3000") - .await - .unwrap(); + let listener = tokio::net::TcpListener::bind("127.0.0.1:3000").await.unwrap(); tracing::info!("listening on {}", listener.local_addr().unwrap()); axum::serve(listener, app).await.unwrap(); } - struct AppError(anyhow::Error); impl IntoResponse for AppError { fn into_response(self) -> Response { tracing::error!("500 error: {}", self.0); tracing::debug!("500 error stacktrace: {}", self.0.backtrace()); - ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Something went wrong: {}", self.0), - ) - .into_response() + (StatusCode::INTERNAL_SERVER_ERROR, format!("Something went wrong: {}", self.0)).into_response() } } impl From for AppError - where - E: Into, +where + E: Into, { fn from(err: E) -> Self { Self(err.into()) @@ -122,17 +112,13 @@ impl From for AppError struct HtmlTemplate(T); impl IntoResponse for HtmlTemplate - where - T: Template, +where + T: Template, { fn into_response(self) -> Response { match self.0.render() { Ok(html) => Html(html).into_response(), - Err(err) => ( - StatusCode::INTERNAL_SERVER_ERROR, - format!("Failed to render template. Error: {err}"), - ) - .into_response(), + Err(err) => (StatusCode::INTERNAL_SERVER_ERROR, format!("Failed to render template. Error: {err}")).into_response(), } } } diff --git a/src/templates/base.html b/src/templates/base.html index b7837f9..ea69e0b 100644 --- a/src/templates/base.html +++ b/src/templates/base.html @@ -39,9 +39,9 @@
@@ -66,4 +66,3 @@ - diff --git a/src/templates/cost_centre/list.html b/src/templates/cost_centre/list.html index 41628b5..d0a32bb 100644 --- a/src/templates/cost_centre/list.html +++ b/src/templates/cost_centre/list.html @@ -14,16 +14,16 @@

Kostenstellen

{% for i in cost_centres %} - {{ i.id }} - - -
{{ i.name }}
- - - Speichern - Bearbeiten - Löschen - + {{ i.id }} + + +
{{ i.name }}
+ + + Speichern + Bearbeiten + Löschen + {% endfor %} diff --git a/src/templates/invoice/edit.html b/src/templates/invoice/edit.html index be9b10e..db1ecde 100644 --- a/src/templates/invoice/edit.html +++ b/src/templates/invoice/edit.html @@ -30,7 +30,7 @@

{{ invoice.vendor }} – {{ invoice.invoice_number }}

@@ -63,7 +63,7 @@

{{ invoice.vendor }} – {{ invoice.invoice_number }}

@@ -92,26 +92,26 @@

{{ invoice.vendor }} – {{ invoice.invoice_number }}

} input.addEventListener('change', event => { - if (input.value === "") { - input.classList.remove("is-valid") - input.classList.add("is-invalid") - } else { - input.classList.remove("is-invalid") - input.classList.add("is-valid") - } + if (input.value === "") { + input.classList.remove("is-valid") + input.classList.add("is-invalid") + } else { + input.classList.remove("is-invalid") + input.classList.add("is-valid") + } }, false) }); const buttons = document.querySelectorAll('.invoice-item-split-button'); Array.from(buttons).forEach(button => { - button.addEventListener("click", event => { - event.preventDefault(); - fetch(`/invoice/{{invoice.id.unwrap()}}/invoiceitem/${button.dataset.id}/split`, { - method: "POST" - }).then(res => { - form.submit() + button.addEventListener("click", event => { + event.preventDefault(); + fetch(`/invoice/{{invoice.id.unwrap()}}/invoiceitem/${button.dataset.id}/split`, { + method: "POST" + }).then(res => { + form.submit() + }) }) - }) }); document.querySelector('#invoice-edit-change-global-cost-centre').addEventListener('change', e => { diff --git a/src/templates/projects/edit.html b/src/templates/projects/edit.html index 1614f09..d839412 100644 --- a/src/templates/projects/edit.html +++ b/src/templates/projects/edit.html @@ -26,11 +26,11 @@

Projekt bearbeiten

- +
- +
diff --git a/src/templates/projects/list.html b/src/templates/projects/list.html index 3f2cf6c..9ad91f4 100644 --- a/src/templates/projects/list.html +++ b/src/templates/projects/list.html @@ -17,19 +17,19 @@

Projekte

- {% for i in projects %} + {% for i in projects %} {{ i.id.unwrap() }} {{ i.name }} {{ i.description }}
- +
- +
@@ -39,7 +39,7 @@

Projekte

Löschen - {% endfor %} + {% endfor %}
diff --git a/src/templates/projects/new.html b/src/templates/projects/new.html index 3caf667..da393a1 100644 --- a/src/templates/projects/new.html +++ b/src/templates/projects/new.html @@ -15,7 +15,8 @@

Projekt hinzufügen

- + +
diff --git a/src/utils.rs b/src/utils.rs index 45c1d9d..bee4a6e 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -1,8 +1,4 @@ -use axum::{ - http::{ - HeaderMap, - }, -}; +use axum::http::HeaderMap; use axum_core::response::IntoResponse; @@ -10,10 +6,7 @@ use crate::AppError; pub fn make_htmx_redirect(request_headers: HeaderMap, target: &str) -> Result { let mut headers = HeaderMap::new(); - let header_name: &str = request_headers - .get("HX-Request") - .filter(|v| *v == "true") - .map_or_else(|| "Location", |_| "HX-Location"); + let header_name: &str = request_headers.get("HX-Request").filter(|v| *v == "true").map_or_else(|| "Location", |_| "HX-Location"); headers.insert(header_name, target.parse().unwrap()); Ok(headers) }