192 lines
8.3 KiB
Rust
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"), settings.billing.lump_sum_name, settings.billing.money_for_time_name, 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)\""))
|
|
}
|
|
} |