EinsatzOnline/src/modules/event_billing/generate_billing_csv.rs

192 lines
8.3 KiB
Rust

use std::fmt;
use std::fs::File;
use std::io::Write;
use std::path::Path;
use rocket::State;
use crate::database::controller::billing::personnel_billing_rates::get_billing_rate;
use crate::database::controller::billing::states::{get_billing_approve_log_entry_with_state, get_billing_states};
use crate::database::controller::events::{get_event, get_position_instances};
use crate::database::controller::events::instances::instances::get_instances;
use crate::database::controller::groups::get_group;
use crate::database::controller::members::get_member_by_uuid;
use crate::helper::bigdecimal_to_string::convert;
use crate::helper::time::{get_timezone, utc_to_local_user_time};
use crate::Settings;
#[derive(Clone, Copy, Debug, Eq, Hash, Ord, PartialEq, PartialOrd)]
pub enum CSVGeneratorErrorKind {
MissingTimes,
MissingMember,
}
#[derive(Debug)]
pub enum CSVGeneratorError {
Database(diesel::result::Error),
Generator(CSVGeneratorErrorKind),
IoError(std::io::Error),
}
impl CSVGeneratorErrorKind {
pub fn as_str(&self) -> &str {
match *self {
CSVGeneratorErrorKind::MissingTimes => "Missing real start and/or real end times.",
CSVGeneratorErrorKind::MissingMember => "Missing member details. Maybe member got removed?",
}
}
}
impl fmt::Display for CSVGeneratorError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
match *self {
CSVGeneratorError::Generator(ref err) => write!(f, "CSV Generator error: {:?}", err),
CSVGeneratorError::Database(ref err) => std::fmt::Display::fmt(&err, f),
CSVGeneratorError::IoError(ref err) => std::fmt::Display::fmt(&err, f),
}
}
}
impl From<diesel::result::Error> for CSVGeneratorError {
fn from(err: diesel::result::Error) -> CSVGeneratorError {
CSVGeneratorError::Database(err)
}
}
impl From<std::io::Error> for CSVGeneratorError {
fn from(err: std::io::Error) -> CSVGeneratorError {
CSVGeneratorError::IoError(err)
}
}
pub fn save_billing_csv(settings: &State<Settings>, csv: String, event_name: String) -> Result<String, std::io::Error> {
let mut path_draft = format!("data/personnel_billing_csv/csv_{}", event_name);
let mut path = Path::new(&path_draft);
if path.exists() {
let mut i = 1;
loop {
path_draft = format!("data/personnel_billing_csv/csv_{}_{}", event_name, i);
if !Path::new(&path_draft).exists() {
break;
} else {
i = i + 1;
}
}
path = Path::new(&path_draft);
}
let mut file = File::create(&path)?;
file.write_all(csv.as_bytes())?;
Ok(path_draft)
}
pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) -> Result<String, CSVGeneratorError> {
let mut res = String::new();
let event_data = get_event(settings, event_id)?;
res.push_str(&format!("{},{}\n", sanitize(String::from("Einsatz: ")), sanitize(event_data.name.clone())));
if let Some(related_group) = event_data.related_group {
let group = get_group(settings, related_group)?;
res.push_str(&format!("{},{}\n", sanitize(String::from("Gruppe:")), sanitize(group.name)));
}
if let Some(member_responsible) = event_data.member_responsible {
if let Some(member) = get_member_by_uuid(member_responsible, settings) {
res.push_str(&format!("{},{}\n", sanitize(String::from("Verantwortliches Mitglied:")), sanitize(format!("{} {}", member.firstname, member.lastname))))
}
}
res.push('\n');
let instances = get_instances(settings, event_id)?;
for instance in instances {
res.push_str(&format!("Einheit:,{}\n", sanitize(instance.name)));
if let Some(billing_rate_id) = instance.billing_rate_id {
let billing_rate = get_billing_rate(settings, billing_rate_id)?;
res.push_str(&format!("Abrechnungssatz:,{},{},Pauschale:,{},€/Stunde:,{}\n", sanitize(billing_rate.name), sanitize(billing_rate.description.unwrap_or(String::from(""))), sanitize(format!("{}", billing_rate.lump_sum)), sanitize(format!("{}", billing_rate.payment_per_hour))));
}
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", String::from("Personalnummer"), String::from("Vorname"), String::from("Nachname"), String::from("Von"), String::from("Bis"), String::from("Stunden"), String::from("Pauschale"), String::from("Stundengeld"), String::from("Gesamt")));
let position_instances = get_position_instances(settings, instance.instance_id)?;
for position_instance in position_instances {
if let Some(personnel) = crate::database::controller::billing::personnel_billing::read(settings, position_instance.position_instance_id)? {
match get_member_by_uuid(personnel.member_id, settings) {
Some(member) => {
let tz = get_timezone(settings, None);
let begin = match position_instance.real_start_time {
Some(start) => start.with_timezone(&tz).format("%d.%m.%Y %H:%M").to_string(),
None => return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingTimes))
};
let end = match position_instance.real_end_time {
Some(end) => end.with_timezone(&tz).format("%d.%m.%Y %H:%M").to_string(),
None => return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingTimes))
};
res.push_str(&format!("{},{},{},{},{},{},{},{},{}\n", sanitize(member.personnel_number.unwrap_or(0).to_string()), sanitize(member.firstname), sanitize(member.lastname), sanitize(begin), sanitize(end), sanitize(personnel.fulfilled_time.to_string()), sanitize(convert(personnel.money_from_lump_sum.clone(), 2, ',', Some('.'))), sanitize(convert(personnel.money_for_time.clone(), 2, ',', Some('.'))), sanitize(convert(personnel.money_from_lump_sum+personnel.money_for_time, 2, ',', Some('.')))));
}
None => {
return Err(CSVGeneratorError::Generator(CSVGeneratorErrorKind::MissingMember));
}
};
}
}
res.push('\n');
let billing_states = get_billing_states(settings)?;
res.push_str("Freigaben:\n");
for state in billing_states{
if let Some(entry) = get_billing_approve_log_entry_with_state(settings, event_id, state.entity_id)?{
if let Some(causer) = entry.causer{
if let Some(member) = get_member_by_uuid(causer, settings){
res.push_str(&format!("{},{}\n", sanitize(format!("{} ({})", state.name, state.description.unwrap_or("".to_string()))), sanitize(format!("{} {}", member.firstname, member.lastname))))
}else{
res.push_str(&format!("{},{}\n", sanitize(state.name), "gelöschtes Mitglied"))
}
}else{
warn!("Billing State Approve Log Entry without causer!");
}
}else{
debug!("No entry with event_id {} and state {}", event_id, state.entity_id);
}
}
}
Ok(res)
}
fn sanitize(mut input: String) -> String {
input = input.replace("\"", "\"\"") //escape double quotes by another double quote
.replace("\r", "") //Remove carriage returns
.replace("\n", "") //Remove new lines
.replace("\t", ""); //Remove tabs
if input.starts_with("@") || input.starts_with("=") || input.starts_with("+") || input.starts_with("-") { //Add single quote if input starts with @ = + or -
input = format!("\'{}", input);
}
format!("\"{}\"", input)
}
#[cfg(test)]
mod tests {
use crate::modules::event_billing::generate_billing_csv::sanitize;
#[test]
fn csv_injection_test1() {
assert_eq!(sanitize(String::from("Tes\"t123")), String::from("\"Tes\"\"t123\""))
}
#[test]
fn csv_injection_test2() {
assert_eq!(sanitize(String::from("=1+2\";=1+2")), String::from("\"'=1+2\"\";=1+2\""))
}
#[test]
fn csv_injection_test3() {
assert_eq!(sanitize(String::from("=SUM(A1, A2)")), String::from("\"'=SUM(A1, A2)\""))
}
}