From 5ec289bfa2cd2ae6feed51086cf3e5df84a3f6b3 Mon Sep 17 00:00:00 2001 From: Urs Joss Date: Sat, 4 May 2024 15:41:33 +0200 Subject: [PATCH] feat: [#194] Sponsored reductions (#197) --- README.md | 10 +- src/export/export_accounting.rs | 89 +++++++++------- src/export/export_miti.rs | 6 ++ src/export/export_summary.rs | 175 +++++++++++++++++++++++++++----- src/test_fixtures.rs | 68 ++++++++----- 5 files changed, 255 insertions(+), 93 deletions(-) diff --git a/README.md b/README.md index e17275b..b45f8b2 100644 --- a/README.md +++ b/README.md @@ -215,10 +215,13 @@ The columns of the resulting summary file are defined as follows: - `Net MiTi (LoLa)`: Net total income Mittagstisch with LoLa items w/o commission [`Gross MiTi (LoLa)` - `LoLa_Commission_MiTi`] - `Contribution MiTi`: Share MiTi from selling LoLa items [20% * `Net MiTi (LoLa)`] - `Net MiTi (LoLA) - Share LoLa`: 80% of Net total income Mittagstisch with LoLa items w/o commission [`Net MiTi (LoLa)` * 0.8] - - `Debt to MiTi`: Net amount LoLa needs to pay out to Mittagstisch [`Net Payment SumUp MiTi` - `Net MiTi (LoLA) - Share LoLa`] + - `Sponsored Reductions`: Reductions sponsored by LoLa [2 * (`MealCount_Reduced` + `MealCount_Regular`)] + - `Debt to MiTi`: Net amount LoLa needs to pay out to Mittagstisch [`Net Payment SumUp MiTi` - `Net MiTi (LoLA) - Share LoLa` + `Sponsored Reductions`] - `Income LoLa MiTi`: Income LoLa from MiTi selling LoLa [`Gross MiTi (LoLa)` - `Contribution MiTi`] - Statistics relevant for Mittagstisch: - `MealCount_Regular`: Number of regular meals per day + - `MealCount_Reduced`: Number of meals with reductions sponsored by LoLa per day + - `MealCount_Regular`: Number of meals for stagaire sponsored by LoLA per day - `MealCount_Children`: Number of children meals per day ### Mittagstisch Report @@ -231,6 +234,7 @@ The columns of the resulting file are defined as follows: - `Datum`: [`Date`] - Count of Menus - `Hauptgang`: Number of regular meals per day [`MealCount_Regular`] + - `Reduziert`: Number of regular meals per day [`MealCount_Reduced` + `MealCount_Praktikum`] - `Kind`: Number of children meals per day [`MealCount_Children`] - Income by ownership (MiTi or LoLa): - `Küche`: Gross income from menus [`Gross MiTi (MiTi)`] @@ -252,6 +256,7 @@ The columns of the resulting file are defined as follows: - Netting with LoLa - `Net Total Karte`: Net card payments concerning Mittagstisch (Sales from meals, tips, sales of LoLa goods paid via card) [`Net Paymnet SumUp MiTi`] - `Verkauf LoLa (80%)` 80% of net sales lola goods [-`Net MiTi (LoLA) - Share LoLa`] + - `Gesponsort` LoLa sponsored reductions (2.00 per Meal) [`Sponsored Reductions`] - `Überweisung`: Net Payment LoLa to Mittagstisch [`Debt to MiTi`] ### Accounting Report @@ -282,6 +287,7 @@ The columns of the resulting accounting.csv file are defined as follows: - `10920/20051`: Net Card income + tips (card) Mittagstisch [`Net Card MiTi` + `MiTi_Tips_Card`] - `10920/10910`: Tips LoLa paid via Card [`Tips_Card` - `MiTi_Tips_Card`] - `68450/10920`: Commission for Café, Vermietung, summer party, Deposit, Rental, Cultural Payments, and `PaidOut`, i.e. w/o Mittagstisch [`Commission LoLa`] +- `59991/20051`: LoLa sponsored reductions (`Sponsored Reductions`) - `20051/10930`: Amount LoLa owes to Mittagstisch (`Debt to MiTi`) - `20051/30500`: Income LoLa from MiTi selling LoLa [`Gross MiTi (LoLa)` - `Contribution MiTi` = `Income LoLa MiTi`] @@ -295,4 +301,4 @@ They serve for consolidation purposes: Where the absolute net sum for the transitory accounts must not be > 0.05, i.e.: - for `10920`: abs(`10920/30200` + `10920/30700` + `10920/30800` + `10920/23050` + `10920/46000` + `10920/31000` + `10920/32000` +`10920/20051` + `10920/10000` + `10920/10910` - `Payment SumUp` - `68450/10920`) < 0.05 -- for `20051`: abs(`10920/20051` - `20051/10930` - `20051/30200`) < 0.05 +- for `20051`: abs(`10920/20051` - `20051/10930` - `20051/30200` + `59991/20051`) < 0.05 diff --git a/src/export/export_accounting.rs b/src/export/export_accounting.rs index 7b097ce..a08a16e 100644 --- a/src/export/export_accounting.rs +++ b/src/export/export_accounting.rs @@ -57,6 +57,7 @@ pub fn gather_df_accounting(df: &DataFrame) -> PolarsResult { col("Net Card Total MiTi").alias("10920/20051"), col("Tips Card LoLa").alias("10920/10910"), col("LoLa_Commission").alias("68450/10920"), + col("Sponsored Reductions").alias("59991/20051"), col("Debt to MiTi").alias("20051/10930"), col("Income LoLa MiTi").alias("20051/30500"), ]) @@ -87,7 +88,8 @@ fn validate_acc_constraint_10920(df_acc: &DataFrame) -> Result<(), Box Result<(), Box> { - let net_expr = col("10920/20051") - col("20051/10930") - col("20051/30500"); + let net_expr = + col("10920/20051") - col("20051/10930") - col("20051/30500") + col("59991/20051"); validate_constraint(df_acc, net_expr, "20051")? } @@ -134,80 +136,80 @@ mod tests { // fully valid case #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] // still valid, as we accept a deviation of 0.05/-0.05 #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 19.98, 12.0, 20.0, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.02, 12.0, 20.0, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.01, 20.0, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 11.99, 20.0, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.01, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 19.99, 10.0, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.01, - 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 9.99, 100.0, - 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, - 100.01, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 100.01, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 99.99, - 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.01, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.01, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 499.99, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 499.99, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.01, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.01, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 399.99, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 399.99, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.0, 100.01, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.0, 100.01, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.0, 99.99, 189.25, 1.0, 17.99, 146.76, 42.49, None, "" + 500.0, 400.0, 99.99, 189.25, 1.0, 17.99, 2.00, 148.76, 42.49, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.48, None, "" + 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.48, None, "" )] #[case( 1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, - 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.50, None, "" + 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 2.00, 148.76, 42.50, None, "" )] // violations of account 10920 #[case( @@ -232,7 +234,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -259,7 +262,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -286,7 +290,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -313,7 +318,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -340,7 +346,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -367,7 +374,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -394,7 +402,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" @@ -421,13 +430,16 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(0.1), "10920" )] - #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 188.15, 1.0, 17.99, 146.76, 42.49, Some(-1.1), "10920")] - #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 0.5, 17.99, 146.76, 42.49, Some(-0.5), "10920")] + #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 188.15, 1.0, 17.99, + 2.0, 148.76, 42.49, Some(-1.1), "10920")] + #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 0.5, 17.99, + 2.0, 148.76, 42.49, Some(-0.5), "10920")] #[case( 1234.06, 552.0, @@ -450,12 +462,14 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.49, Some(100.2), "10920" )] - #[case(1343.36, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.49, Some(-9.1), "10920")] + #[case(1343.36, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, + 2.0, 148.76, 42.49, Some(-9.1), "10920")] // violations of account 20051 #[case( 1334.26, @@ -479,7 +493,8 @@ mod tests { 189.25, 1.0, 17.99, - 146.70, + 2.0, + 148.70, 42.49, Some(0.06), "20051" @@ -506,12 +521,14 @@ mod tests { 189.25, 1.0, 17.99, - 146.76, + 2.0, + 148.76, 42.43, Some(0.06), "20051" )] - #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, 146.76, 42.55, Some(-0.06), "20051")] + #[case(1334.26, 552.0, 1162.0, 0.0, 0.0, 10.0, 20.0, 0.0, 0.0, 0.0, 20.0, 12.0, 20.0, 10.0, 100.0, 500.0, 400.0, 100.0, 189.25, 1.0, 17.99, + 2.0, 148.76, 42.55, Some(-0.06), "20051")] fn test_violations( #[case] payment_sumup: f64, #[case] total_cash_debit: f64, @@ -534,6 +551,7 @@ mod tests { #[case] net_card_income_plus_tips_miti_card: f64, #[case] tips_lola_paid_via_card: f64, #[case] commission_lola: f64, + #[case] sponsored_reductions: f64, #[case] debt_to_miti: f64, #[case] income_lola_miti: f64, #[case] delta: Option, @@ -563,6 +581,7 @@ mod tests { "10920/20051" => &[net_card_income_plus_tips_miti_card], "10920/10910" => &[tips_lola_paid_via_card], "68450/10920" => &[Some(commission_lola)], + "59991/20051" => &[sponsored_reductions], "20051/10930" => &[debt_to_miti], "20051/30500" => &[income_lola_miti], )?; diff --git a/src/export/export_miti.rs b/src/export/export_miti.rs index 1a6bb2d..c09372f 100644 --- a/src/export/export_miti.rs +++ b/src/export/export_miti.rs @@ -4,6 +4,10 @@ use polars::prelude::*; pub fn gather_df_miti(df: &DataFrame) -> PolarsResult { df.clone() .lazy() + .with_column( + (col("MealCount_Reduced").fill_null(0) + col("MealCount_Praktikum").fill_null(0)) + .alias("MealCount_ReducedPraktikum"), + ) .with_column( (col("MiTi_Cash").fill_null(0.0) + col("MiTi_Tips_Cash").fill_null(0.0)) .round(2) @@ -47,6 +51,7 @@ pub fn gather_df_miti(df: &DataFrame) -> PolarsResult { .select([ col("Date").alias("Datum"), col("MealCount_Regular").alias("Hauptgang"), + col("MealCount_ReducedPraktikum").alias("Reduziert"), col("MealCount_Children").alias("Kind"), col("Gross MiTi (MiTi)").alias("Küche"), col("Gross MiTi (LoLa)").alias("Total Bar"), @@ -64,6 +69,7 @@ pub fn gather_df_miti(df: &DataFrame) -> PolarsResult { col("Net MiTi (MiTi) Card").alias("Netto Karte MiTi"), col("Net Payment SumUp MiTi").alias("Net Total Karte"), col("Verkauf LoLa (80%)"), + col("Sponsored Reductions").alias("Gesponsort"), col("Debt to MiTi").alias("Überweisung"), ]) .collect() diff --git a/src/export/export_summary.rs b/src/export/export_summary.rs index 1a54802..41f9bb3 100644 --- a/src/export/export_summary.rs +++ b/src/export/export_summary.rs @@ -1,6 +1,6 @@ use polars::prelude::*; -use crate::export::export_summary::MitiMealType::{Children, Regular}; +use crate::export::export_summary::MitiMealType::{Children, Praktikum, Reduced, Regular}; use crate::prepare::{Owner, PaymentMethod, Purpose, Topic}; /// Produces the Summary dataframe from the `raw_df` read from the file @@ -134,6 +134,8 @@ pub fn collect_data(raw_df: DataFrame) -> PolarsResult { ); let meal_count_regular = meal_count(for_meals_of_type(&Regular), ldf.clone()); + let meal_count_reduced = meal_count(for_meals_of_type(&Reduced), ldf.clone()); + let meal_count_praktikum = meal_count(for_meals_of_type(&Praktikum), ldf.clone()); let meal_count_children = meal_count(for_meals_of_type(&Children), ldf); let with_cafe_cash = all_dates.join( @@ -364,7 +366,19 @@ pub fn collect_data(raw_df: DataFrame) -> PolarsResult { [col("Date")], JoinType::Left.into(), ); - let with_meal_count_children = with_meal_count_regular.join( + let with_meal_count_reduced = with_meal_count_regular.join( + meal_count_reduced, + [col("Date")], + [col("Date")], + JoinType::Left.into(), + ); + let with_meal_count_praktikum = with_meal_count_reduced.join( + meal_count_praktikum, + [col("Date")], + [col("Date")], + JoinType::Left.into(), + ); + let with_meal_count_children = with_meal_count_praktikum.join( meal_count_children, [col("Date")], [col("Date")], @@ -531,9 +545,17 @@ pub fn collect_data(raw_df: DataFrame) -> PolarsResult { .alias("Net MiTi (LoLA) - Share LoLa"), ) .with_column( - (col("Net Payment SumUp MiTi") - col("Net MiTi (LoLA) - Share LoLa")) - .round(2) - .alias("Debt to MiTi"), + (lit(2.0) + * (col("MealCount_Reduced").fill_null(0) + + col("MealCount_Praktikum").fill_null(0))) + .round(2) + .alias("Sponsored Reductions"), + ) + .with_column( + (col("Net Payment SumUp MiTi") - col("Net MiTi (LoLA) - Share LoLa") + + col("Sponsored Reductions")) + .round(2) + .alias("Debt to MiTi"), ) .select([ col("Date"), @@ -596,9 +618,12 @@ pub fn collect_data(raw_df: DataFrame) -> PolarsResult { col("Net MiTi (LoLa)"), col("Contribution MiTi"), col("Net MiTi (LoLA) - Share LoLa"), + col("Sponsored Reductions"), col("Debt to MiTi"), col("Income LoLa MiTi"), col("MealCount_Regular"), + col("MealCount_Reduced"), + col("MealCount_Praktikum"), col("MealCount_Children"), ]) .collect() @@ -754,7 +779,11 @@ fn count_by_date_for(predicate_and_alias: (Expr, String), ldf: LazyFrame) -> Laz fn for_meals_of_type(meal_type: &MitiMealType) -> (Expr, String) { let expr = (col("Topic").eq(lit(Topic::MiTi.to_string()))) .and(col("Owner").eq(lit(Owner::MiTi.to_string()))) - .and(meal_type.expr()); + .and( + col("Purpose") + .neq(lit(Purpose::Tip.to_string())) + .and(meal_type.expr()), + ); let alias = format!("MealCount_{}", meal_type.label()); (expr, alias) } @@ -769,6 +798,8 @@ trait FilterExpressionProvider { enum MitiMealType { /// Regular Menus (Adults) Regular, + Reduced, + Praktikum, /// Kids menus Children, } @@ -776,9 +807,25 @@ enum MitiMealType { impl FilterExpressionProvider for MitiMealType { fn expr(&self) -> Expr { match self { + Children => col("Description").str().contains(lit("Kinder"), true), + Reduced => col("Description").str().ends_with(lit("Reduziert")), + Praktikum => col("Description").str().ends_with(lit("Praktikum")), Regular => col("Description") .str() .contains(lit("Hauptgang"), true) + .and(col("Description").str().contains(lit("Kinder"), true).not()) + .and( + col("Description") + .str() + .contains(lit("Reduziert"), true) + .not(), + ) + .and( + col("Description") + .str() + .contains(lit("Praktikum"), true) + .not(), + ) .or(col("Description") .str() .contains(lit("Seniorenmittagstisch"), true)) @@ -789,12 +836,13 @@ impl FilterExpressionProvider for MitiMealType { .or(col("Description").str().contains(lit("Hauptsp"), true)) .or(col("Description").str().starts_with(lit("Menü"))) .or(col("Description").str().starts_with(lit("Praktika"))), - Children => col("Description").str().starts_with(lit("Kinder")), } } fn label(&self) -> &str { match self { Regular => "Regular", + Reduced => "Reduced", + Praktikum => "Praktikum", Children => "Children", } } @@ -913,32 +961,55 @@ mod tests { } #[rstest] - #[case("Menü", Some(Regular))] - #[case("Menü ganz", Some(Regular))] - #[case("Hauptgang", Some(Regular))] - #[case("Praktika", Some(Regular))] - #[case("Vorsp.+Hauptspeise", Some(Regular))] - #[case("Vorsp. + Hauptsp. red.", Some(Regular))] - #[case("Hauptspeise spezial", Some(Regular))] - #[case("Hauptsp. + Dessert", Some(Regular))] - #[case("Nur Hauptgang", Some(Regular))] - #[case("Menu (nur Hauptgang)", Some(Regular))] - #[case("2-Gang-Menu (mit Vorspeise oder Dessert)", Some(Regular))] - #[case("3 Gang-Menu (mit Vorspeise + Dessert)", Some(Regular))] - #[case("Seniorenmittagstisch", Some(Regular))] - #[case("Senioren-Mittagstisch", Some(Regular))] - #[case("Kindermenü", Some(Children))] - #[case("Kinderpasta", Some(Children))] - #[case("Kinder-Teigwaren", Some(Children))] - #[case("Kindermenu (kleiner Hauptgang, bis 12 Jahre)", Some(Children))] + #[case("Menü", "Consumption", Some(Regular))] + #[case("Menü", "Tip", None)] + #[case("Menü ganz", "Consumption", Some(Regular))] + #[case("Hauptgang", "Consumption", Some(Regular))] + #[case("Praktika", "Consumption", Some(Regular))] + #[case("Vorsp.+Hauptspeise", "Consumption", Some(Regular))] + #[case("Vorsp. + Hauptsp. red.", "Consumption", Some(Regular))] + #[case("Hauptspeise spezial", "Consumption", Some(Regular))] + #[case("Hauptsp. + Dessert", "Consumption", Some(Regular))] + #[case("Nur Hauptgang", "Consumption", Some(Regular))] + #[case("Menu (nur Hauptgang)", "Consumption", Some(Regular))] + #[case( + "2-Gang-Menu (mit Vorspeise oder Dessert)", + "Consumption", + Some(Regular) + )] + #[case("3 Gang-Menu (mit Vorspeise + Dessert)", "Consumption", Some(Regular))] + #[case("Seniorenmittagstisch", "Consumption", Some(Regular))] + #[case("Senioren-Mittagstisch", "Consumption", Some(Regular))] + #[case("Kindermenü", "Consumption", Some(Children))] + #[case("Kinderpasta", "Consumption", Some(Children))] + #[case("Kinder-Teigwaren", "Consumption", Some(Children))] + #[case( + "Kindermenu (kleiner Hauptgang, bis 12 Jahre)", + "Consumption", + Some(Children) + )] + #[case("Hauptgang Vegi Standard", "Consumption", Some(Regular))] + #[case("Hauptgang Vegi Reduziert", "Consumption", Some(Reduced))] + #[case("Hauptgang Vegi Praktikum", "Consumption", Some(Praktikum))] + #[case("Hauptgang Fleisch Standard", "Consumption", Some(Regular))] + #[case("Hauptgang Fleisch Reduziert", "Consumption", Some(Reduced))] + #[case("Hauptgang Fleisch Praktikum", "Consumption", Some(Praktikum))] + #[case( + "Hauptgang Vegi Kindermenu (bis 12 Jahre)", + "Consumption", + Some(Children) + )] + #[case("Kinderpasta Standard", "Consumption", Some(Children))] fn test_meal_count( #[case] description: &str, + #[case] purpose: &str, #[case] meal_type: Option, ) -> PolarsResult<()> { let df_in = df!( "Date" => &["16.03.2023"], "Topic" => &["MiTi"], "Owner" => &["MiTi"], + "Purpose" => &[purpose], "Description" => &[description], "Quantity" => &[1], )?; @@ -948,19 +1019,67 @@ mod tests { "Date" => & ["16.03.2023"], "MealCount_Regular" => & [1_i64], )?; - let out = meal_count(for_meals_of_type(&Regular), df_in.lazy()).collect()?; + let out = + meal_count(for_meals_of_type(&Regular), df_in.clone().lazy()).collect()?; assert_dataframe(&out, &exp); + assert_dataframe_not(&Reduced, df_in.clone().lazy()); + assert_dataframe_not(&Praktikum, df_in.clone().lazy()); + assert_dataframe_not(&Children, df_in.lazy()); + } + Some(Reduced) => { + let exp = df!( + "Date" => & ["16.03.2023"], + "MealCount_Reduced" => & [1_i64], + )?; + let out = + meal_count(for_meals_of_type(&Reduced), df_in.clone().lazy()).collect()?; + assert_dataframe(&out, &exp); + assert_dataframe_not(&Regular, df_in.clone().lazy()); + assert_dataframe_not(&Praktikum, df_in.clone().lazy()); + assert_dataframe_not(&Children, df_in.lazy()); + } + Some(Praktikum) => { + let exp = df!( + "Date" => & ["16.03.2023"], + "MealCount_Praktikum" => & [1_i64], + )?; + let out = + meal_count(for_meals_of_type(&Praktikum), df_in.clone().lazy()).collect()?; + assert_dataframe(&out, &exp); + assert_dataframe_not(&Regular, df_in.clone().lazy()); + assert_dataframe_not(&Reduced, df_in.clone().lazy()); + assert_dataframe_not(&Children, df_in.lazy()); } Some(Children) => { let exp = df!( "Date" => & ["16.03.2023"], "MealCount_Children" => & [1_i64], )?; - let out = meal_count(for_meals_of_type(&Children), df_in.lazy()).collect()?; + let out = + meal_count(for_meals_of_type(&Children), df_in.clone().lazy()).collect()?; assert_dataframe(&out, &exp); + assert_dataframe_not(&Regular, df_in.clone().lazy()); + assert_dataframe_not(&Reduced, df_in.clone().lazy()); + assert_dataframe_not(&Praktikum, df_in.lazy()); + } + None => { + assert_dataframe_not(&Regular, df_in.clone().lazy()); + assert_dataframe_not(&Reduced, df_in.clone().lazy()); + assert_dataframe_not(&Praktikum, df_in.clone().lazy()); + assert_dataframe_not(&Children, df_in.lazy()); } - None => {} } Ok(()) } + + fn assert_dataframe_not(meal_type: &MitiMealType, ldf: LazyFrame) { + let actual = meal_count(for_meals_of_type(meal_type), ldf) + .collect() + .unwrap(); + assert_eq!( + actual.shape(), + (0, 2), + "shape of the dataframes is not matching!" + ); + } } diff --git a/src/test_fixtures.rs b/src/test_fixtures.rs index 9b69bd7..4fc2910 100644 --- a/src/test_fixtures.rs +++ b/src/test_fixtures.rs @@ -115,26 +115,26 @@ pub fn intermediate_df_01(sample_date: NaiveDate, sample_time: NaiveTime) -> Dat pub fn intermediate_df_02(sample_date: NaiveDate) -> DataFrame { let date = sample_date.format("%d.%m.%Y").to_string(); df!( - "Account" => &["a@b.ch", "a@b.ch", "a@b.ch", "a@b.ch", "a@b.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch"], - "Date" => &[date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date], - "Time" => &["12:32:00", "12:33:00", "12:34:00", "12:35:00", "12:36:00", "12:37:00", "12:40:00", "13:50:00", "13:51:00", "13:52:00", "13:53:00"], - "Type" => &["Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales"], - "Transaction ID" => &["TEGUCXAGDE", "TEGUCXAGDF", "TEGUCXAGDG", "TEGUCXAGDH", "TEGUCXAGDI", "TEGUCXAGDJ", "EGUCXAGDK", "EGUCXAGDI", "EGUCXAGDJ", "EGUCXAGDK", "EGUCXAGDL"], - "Receipt Number" => &["S20230000303", "S20230000304", "S20230000305", "S20230000306", "S20230000307", "S20230000308", "S20230000309", "S20230000310", "S20230000311", "S20230000312", "S20230000313"], - "Payment Method" => &["Card", "Cash", "Card", "Card", "Card", "Card", "Card", "Cash", "Card", "Card", "Cash"], - "Quantity" => &[1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1], - "Description" => &["Hauptgang, normal", "Kaffee", "Cappuccino", "Schlüsseldepot", "Kulturevent", "Rental fee", "Sold by renter, paid out in cash", "SoFe 1", "SoFe 2", "Recircle Tupper Deopt", "Recircle Tupper Deopt"], - "Currency" => &["CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF"], - "Price (Gross)" => &[16.0, 3.50, 20.0, 100.0, 400.0, 500.0, 100.0, 10.0, 20.0, 10.0, 20.0], - "Price (Net)" => &[16.0, 3.50, 20.0, 100.0, 400.0, 500.0, 100.0, 10.0, 20.0, 10.0, 20.0], - "Tax" => &["0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%"], - "Tax rate" => &["", "", "", "", "", "", "", "", "", "", ""], - "Transaction refunded" => &["", "", "", "", "", "", "", "", "", "", ""], - "Commission" =>[0.24123, 0.0, 0.3, 1.5, 6.0, 7.5, 1.5, 0.0, 0.3, 0.15, 0.0], - "Topic" => &["MiTi", "MiTi", "MiTi", "Deposit", "Culture", "Rental", "PaidOut", "SoFe", "SoFe", "Packaging", "Packaging"], - "Owner" => &["MiTi", "LoLa", "LoLa", "", "", "", "", "", "", "", ""], - "Purpose" => &["Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption"], - "Comment" => &[None::, None::, None::, None::, None::, None::, None::, None::, None::, None::, None::], + "Account" => &["a@b.ch", "a@b.ch", "a@b.ch", "a@b.ch", "a@b.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch", "a@B.ch"], + "Date" => &[date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date.clone(), date], + "Time" => &["12:32:00", "12:33:00", "12:34:00", "12:35:00", "12:36:00", "12:37:00", "12:40:00", "13:50:00", "13:51:00", "13:52:00", "13:53:00", "13:54:00"], + "Type" => &["Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales", "Sales"], + "Transaction ID" => &["TEGUCXAGDE", "TEGUCXAGDF", "TEGUCXAGDG", "TEGUCXAGDH", "TEGUCXAGDI", "TEGUCXAGDJ", "EGUCXAGDK", "EGUCXAGDI", "EGUCXAGDJ", "EGUCXAGDK", "EGUCXAGDL", "EGUCXAGDM"], + "Receipt Number" => &["S20230000303", "S20230000304", "S20230000305", "S20230000306", "S20230000307", "S20230000308", "S20230000309", "S20230000310", "S20230000311", "S20230000312", "S20230000313", "S20230000314"], + "Payment Method" => &["Card", "Cash", "Card", "Card", "Card", "Card", "Card", "Cash", "Card", "Card", "Cash", "Cash"], + "Quantity" => &[1, 1, 4, 1, 1, 1, 1, 1, 1, 1, 1, 1], + "Description" => &["Hauptgang, normal", "Kaffee", "Cappuccino", "Schlüsseldepot", "Kulturevent", "Rental fee", "Sold by renter, paid out in cash", "SoFe 1", "SoFe 2", "Recircle Tupper Depot", "Recircle Tupper Depot", "Hauptgang Fleisch Reduziert"], + "Currency" => &["CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF", "CHF"], + "Price (Gross)" => &[16.0, 3.50, 20.0, 100.0, 400.0, 500.0, 100.0, 10.0, 20.0, 10.0, 20.0, 13.0], + "Price (Net)" => &[16.0, 3.50, 20.0, 100.0, 400.0, 500.0, 100.0, 10.0, 20.0, 10.0, 20.0, 13.0], + "Tax" => &["0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%", "0.0%"], + "Tax rate" => &["", "", "", "", "", "", "", "", "", "", "", ""], + "Transaction refunded" => &["", "", "", "", "", "", "", "", "", "", "", ""], + "Commission" =>[0.24123, 0.0, 0.3, 1.5, 6.0, 7.5, 1.5, 0.0, 0.3, 0.15, 0.0, 0.0], + "Topic" => &["MiTi", "MiTi", "MiTi", "Deposit", "Culture", "Rental", "PaidOut", "SoFe", "SoFe", "Packaging", "Packaging", "MiTi"], + "Owner" => &["MiTi", "LoLa", "LoLa", "", "", "", "", "", "", "", "", "MiTi"], + "Purpose" => &["Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption", "Consumption"], + "Comment" => &[None::, None::, None::, None::, None::, None::, None::, None::, None::, None::, None::, None::], ) .expect("valid intermediate dataframe 03") } @@ -145,9 +145,9 @@ pub fn intermediate_df_02(sample_date: NaiveDate) -> DataFrame { pub fn summary_df_02(sample_date: NaiveDate) -> DataFrame { df!( "Date" => &[sample_date], - "MiTi_Cash" => &[Some(3.5)], + "MiTi_Cash" => &[Some(16.5)], "MiTi_Card" => &[Some(36.0)], - "MiTi Total" => &[39.5], + "MiTi Total" => &[52.5], "Cafe_Cash" => &[None::], "Cafe_Card" => &[None::], "Cafe Total" => &[0.0], @@ -172,15 +172,15 @@ pub fn summary_df_02(sample_date: NaiveDate) -> DataFrame { "PaidOut_Cash" => &[None::], "PaidOut_Card" => &[100.0], "PaidOut Total" => &[100.0], - "Gross Cash" => &[Some(33.5)], + "Gross Cash" => &[Some(46.5)], "Tips_Cash" => &[None::], - "SumUp Cash" => &[Some(33.5)], + "SumUp Cash" => &[Some(46.5)], "Gross Card" => &[Some(1166.0)], "Tips_Card" => &[None::], "SumUp Card" => &[Some(1166.0)], - "Gross Total" => &[Some(1199.5)], + "Gross Total" => &[Some(1212.5)], "Tips Total" => &[0.0], - "SumUp Total" => &[Some(1199.5)], + "SumUp Total" => &[Some(1212.5)], "Gross Card MiTi" => &[36.0], "MiTi_Commission" => &[Some(0.24)], "Net Card MiTi" => &[35.76], @@ -197,16 +197,19 @@ pub fn summary_df_02(sample_date: NaiveDate) -> DataFrame { "MiTi_Tips" => &[None::], "Cafe_Tips" => &[None::], "Verm_Tips" => &[None::], - "Gross MiTi (MiTi)" => &[Some(16.0)], + "Gross MiTi (MiTi)" => &[Some(29.0)], "Gross MiTi (LoLa)" => &[Some(23.5)], "Gross MiTi (MiTi) Card" => &[Some(16.0)], "Net MiTi (MiTi) Card" => &[15.76], "Net MiTi (LoLa)" => &[23.2], "Contribution MiTi" => &[4.64], "Net MiTi (LoLA) - Share LoLa" => &[18.56], - "Debt to MiTi" => &[16.9], + "Sponsored Reductions" => &[2.0], + "Debt to MiTi" => &[18.9], "Income LoLa MiTi" => &[18.86], "MealCount_Regular" => &[1_i64], + "MealCount_Reduced" => &[1_i64], + "MealCount_Praktikum" => &[None::], "MealCount_Children" => &[None::], ) .expect("valid summary dataframe 02") @@ -282,9 +285,12 @@ pub fn summary_df_03(sample_date: NaiveDate) -> DataFrame { "Net MiTi (LoLa)" => &[52.56], "Contribution MiTi" => &[10.51], "Net MiTi (LoLA) - Share LoLa" => &[42.05], + "Sponsored Reductions" => &[0.0], "Debt to MiTi" => &[145.76], "Income LoLa MiTi" => &[42.49], "MealCount_Regular" => &[14], + "MealCount_Reduced" => &[0], + "MealCount_Praktikum" => &[0], "MealCount_Children" => &[1], ) .expect("valid summary dataframe 02") @@ -316,6 +322,7 @@ pub fn accounting_df_03(sample_date: NaiveDate) -> DataFrame { "10920/20051" => &[189.25], "10920/10910" => &[0.0], "68450/10920" => &[17.99], + "59991/20051" => &[0.0], "20051/10930" => &[145.76], "20051/30500" => &[42.49], ) @@ -328,6 +335,7 @@ pub fn miti_df_03(sample_date: NaiveDate) -> DataFrame { df!( "Datum" => &[sample_date], "Hauptgang" => &[14], + "Reduziert" => &[0], "Kind" => &[1], "Küche" => &[Some(250.0)], "Total Bar" => &[Some(53.0)], @@ -345,6 +353,7 @@ pub fn miti_df_03(sample_date: NaiveDate) -> DataFrame { "Netto Karte MiTi" => &[164.25], "Net Total Karte" => &[187.81], "Verkauf LoLa (80%)" => &[-42.05], + "Gesponsort" => &[0.0], "Überweisung" => &[145.76], ) .expect("Valid miti df 03") @@ -451,9 +460,12 @@ pub fn summary_df_04(sample_date: NaiveDate, sample_date2: NaiveDate) -> DataFra "Net MiTi (LoLa)" => &[Some(0.0), Some(0.0), Some(0.0)], "Contribution MiTi" => &[Some(0.0), Some(0.0), Some(0.0)], "Net MiTi (LoLA) - Share LoLa" => &[Some(0.0), Some(0.0), Some(0.0)], + "Sponsored Reductions" =>[Some(0.0), Some(0.0), Some(0.0)], "Debt to MiTi" => &[Some(0.0), Some(19.7), Some(19.7)], "Income LoLa MiTi" => &[Some(0.0), Some(0.0), Some(0.0)], "MealCount_Regular" => &[None::, None::, Some(0)], + "MealCount_Reduced" => &[None::, None::, Some(0)], + "MealCount_Praktikum" => &[None::, None::, Some(0)], "MealCount_Children" => &[None::, Some(3), Some(3)], ) .expect("valid summary dataframe 04")