FIX: changed calculation of personnel billing hours; FEA: Added Logging feature
This commit is contained in:
parent
8db5a6f954
commit
187ccde762
|
@ -580,6 +580,7 @@ dependencies = [
|
|||
"iban_validate",
|
||||
"lettre",
|
||||
"log",
|
||||
"num-bigint",
|
||||
"rand",
|
||||
"rocket",
|
||||
"rocket_dyn_templates",
|
||||
|
|
|
@ -21,6 +21,7 @@ rand = "0.8.5"
|
|||
iban_validate = "4.0.1"
|
||||
base64 = "0.13.0"
|
||||
bigdecimal = "0.1.2"
|
||||
num-bigint = "0.2.6"
|
||||
lettre = { version = "0.10.0-rc.4", features = ["tokio1", "tokio1-native-tls"] }
|
||||
|
||||
[dependencies.rocket]
|
||||
|
|
|
@ -0,0 +1,4 @@
|
|||
-- This file should undo anything in `up.sql`
|
||||
drop table if exists log_actions;
|
||||
|
||||
drop table if exists log;
|
|
@ -0,0 +1,33 @@
|
|||
-- Your SQL goes here
|
||||
create table log_actions
|
||||
(
|
||||
action text
|
||||
constraint log_actions_pk
|
||||
primary key,
|
||||
description text
|
||||
);
|
||||
|
||||
create table log
|
||||
(
|
||||
entry_id serial
|
||||
constraint log_pk
|
||||
primary key,
|
||||
timestamp timestamptz default now() not null,
|
||||
action text not null,
|
||||
affected_entity uuid
|
||||
constraint log_entities_entity_id_fk_2
|
||||
references entities
|
||||
on update cascade,
|
||||
causer uuid
|
||||
constraint log_entities_entity_id_fk
|
||||
references entities
|
||||
on update cascade,
|
||||
details jsonb
|
||||
);
|
||||
|
||||
CREATE INDEX ON log USING gin(details);
|
||||
|
||||
INSERT INTO log_actions (action, description)
|
||||
VALUES ('password_reset', null);
|
||||
INSERT INTO log_actions (action, description)
|
||||
VALUES ('billing_state_approved', null);
|
|
@ -1,16 +1,17 @@
|
|||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, sql_query};
|
||||
use diesel::{debug_query, ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, sql_query};
|
||||
use diesel::pg::types::sql_types::Uuid;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::logger::entries::LogEntry;
|
||||
use crate::Settings;
|
||||
|
||||
#[derive(Queryable, Clone, Deserialize, Serialize)]
|
||||
pub struct BillingState {
|
||||
entity_id: uuid::Uuid,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
pub(crate) final_approve: bool,
|
||||
pub entity_id: uuid::Uuid,
|
||||
pub name: String,
|
||||
pub description: Option<String>,
|
||||
pub final_approve: bool,
|
||||
}
|
||||
|
||||
pub fn get_billing_states(settings: &State<Settings>) -> Result<Vec<BillingState>, diesel::result::Error> {
|
||||
|
@ -64,7 +65,7 @@ struct TempQuery {
|
|||
pub fn get_min_billing_states_for_event(settings: &State<Settings>, event: uuid::Uuid) -> Result<Option<uuid::Uuid>, diesel::result::Error> {
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
let res: Result<Option<TempQuery>, diesel::result::Error> = sql_query("SELECT billing_state_id FROM eu_instances AS eui INNER JOIN billing_states bs on eui.billing_state_id = bs.entity_id WHERE event_id = $1 ORDER BY bs.order ASC LIMIT 1;").bind::<crate::diesel::sql_types::Uuid, _>(event).get_result(&connection);
|
||||
let res: Result<Option<TempQuery>, diesel::result::Error> = sql_query("SELECT billing_state_id FROM eu_instances AS eui INNER JOIN billing_states bs on eui.billing_state_id = bs.entity_id WHERE event_id = $1 ORDER BY bs.order ASC LIMIT 1;").bind::<crate::diesel::sql_types::Uuid, _>(event).get_result(&connection).optional();
|
||||
match res {
|
||||
Ok(res) => {
|
||||
match res {
|
||||
|
@ -77,4 +78,18 @@ pub fn get_min_billing_states_for_event(settings: &State<Settings>, event: uuid:
|
|||
Err(e)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn get_billing_approve_log_entry_with_state(settings: &State<Settings>, event: uuid::Uuid, state: uuid::Uuid) -> Result<Option<LogEntry>, diesel::result::Error>{
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
let res: Result<Option<LogEntry>, diesel::result::Error> = sql_query("SELECT * FROM log WHERE affected_entity = $1 AND details->>'state' = $2 ORDER BY timestamp DESC LIMIT 1;").bind::<crate::diesel::sql_types::Uuid, _>(event).bind::<crate::diesel::sql_types::Text, _>(state.to_string()).get_result(&connection).optional();
|
||||
match res{
|
||||
Err(e) => {
|
||||
error!("Couldn't get billing approve log entry: {}", e);
|
||||
Err(e)
|
||||
},
|
||||
Ok(res) => Ok(res)
|
||||
}
|
||||
|
||||
}
|
|
@ -1,16 +1,20 @@
|
|||
use crate::helper::settings::Settings;
|
||||
use rocket::State;
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::schema::password_resets::dsl::{password_resets};
|
||||
use rand::{thread_rng, Rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use diesel::{ExpressionMethods, RunQueryDsl};
|
||||
use std::iter;
|
||||
|
||||
use argon2::Config;
|
||||
use chrono::{NaiveDateTime, Utc};
|
||||
use diesel::{ExpressionMethods, RunQueryDsl};
|
||||
use diesel::query_dsl::filter_dsl::FilterDsl;
|
||||
use diesel::query_dsl::select_dsl::SelectDsl;
|
||||
use argon2::Config;
|
||||
use rand::{Rng, thread_rng};
|
||||
use rand::distributions::Alphanumeric;
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::logger::{add_entry, LogActions};
|
||||
use crate::logger::entries::{InsertableLogEntry, LogEntry};
|
||||
use crate::schema::password_resets::dsl::password_resets;
|
||||
use crate::schema::users::dsl::users;
|
||||
use std::iter;
|
||||
|
||||
/// Adds password reset token to database and returns it
|
||||
pub fn add_token(settings: &State<Settings>, user_id: uuid::Uuid) -> Result<String, diesel::result::Error>{
|
||||
|
@ -96,6 +100,8 @@ pub fn change_password_with_token(settings: &State<Settings>, token2: String, pa
|
|||
}
|
||||
};
|
||||
|
||||
add_entry(InsertableLogEntry::new(LogActions::PasswordReset, Some(user_id), None, None), settings);
|
||||
|
||||
match remove_token(settings, token2){
|
||||
Ok(_) => Ok(()),
|
||||
Err(_) => Err(())
|
||||
|
|
|
@ -0,0 +1,209 @@
|
|||
use std::borrow::BorrowMut;
|
||||
|
||||
use bigdecimal::BigDecimal;
|
||||
use num_bigint::Sign;
|
||||
|
||||
pub fn convert_old(val: BigDecimal, decimal_places: u8, separator: char, thousands_separator: Option<char>) -> String{
|
||||
let mut res = String::new();
|
||||
|
||||
let (base, exponent) = val.as_bigint_and_exponent();
|
||||
let mut base = base.to_str_radix(10);
|
||||
|
||||
let mut base= base.chars();
|
||||
let mut exp = exponent;
|
||||
let mut res_base : Vec<char> = Vec::new();
|
||||
let mut res_decimal_places: Vec<char> = Vec::new();
|
||||
let mut thousands_counter = 0;
|
||||
|
||||
if exponent > 0{
|
||||
|
||||
let mut iter = base.rev().peekable();
|
||||
//-10^exponent
|
||||
loop{
|
||||
match iter.next(){
|
||||
Some(num) => {
|
||||
println!("num: {}", num);
|
||||
if exp > 0{
|
||||
//There are decimal places left
|
||||
exp = exp-1;
|
||||
res_decimal_places.push(num);
|
||||
}else{
|
||||
if let Some(thsep) = thousands_separator{
|
||||
if thousands_counter == 3 && iter.peek().is_some(){ //Add thousands separator if there are chars left
|
||||
res_base.push(thsep);
|
||||
thousands_counter = 0;
|
||||
}
|
||||
thousands_counter = thousands_counter +1;
|
||||
}
|
||||
res_base.push(num);
|
||||
}
|
||||
},
|
||||
None => {
|
||||
if exp > 0{
|
||||
exp = exp-1;
|
||||
res_decimal_places.push('0');
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
//First add all non decimal places:
|
||||
res.push_str(&res_base.iter().rev().collect::<String>());
|
||||
|
||||
if decimal_places > 0{
|
||||
//There are decimal places, add saperator and add decimal places
|
||||
res.push(separator);
|
||||
|
||||
let mut decimal_places_used = 0;
|
||||
|
||||
let res_decimal_places = res_decimal_places.iter().rev();
|
||||
|
||||
for place in res_decimal_places{
|
||||
if decimal_places_used < decimal_places {
|
||||
res.push(*place)
|
||||
}else{
|
||||
break;
|
||||
}
|
||||
}
|
||||
}else{
|
||||
if res.is_empty(){
|
||||
res = "0".to_string();
|
||||
}
|
||||
}
|
||||
}else if exponent < 0{
|
||||
//10^exponent
|
||||
res = String::from("test123");
|
||||
}else{
|
||||
let mut iter = base.rev().peekable();
|
||||
while let Some(num) = iter.next(){
|
||||
if let Some(thsep) = thousands_separator{
|
||||
if thousands_counter == 3 && iter.peek().is_some(){ //Add thousands separator if there are chars left
|
||||
res_base.push(thsep);
|
||||
thousands_counter = 0;
|
||||
}
|
||||
thousands_counter = thousands_counter +1;
|
||||
}
|
||||
res_base.push(num);
|
||||
}
|
||||
res.push_str(&res_base.into_iter().rev().collect::<String>());
|
||||
}
|
||||
res
|
||||
}
|
||||
|
||||
|
||||
pub fn convert(val: BigDecimal, decimal_places: u8, seperator: char, thousands_seperator: Option<char>) -> String{
|
||||
let (base, mut exponent) = val.as_bigint_and_exponent();
|
||||
let mut base = base.to_str_radix(10);
|
||||
|
||||
// Negative power of 10, need to move comma to the left
|
||||
|
||||
let mut res_decimal_places:Vec<char> = Vec::new();
|
||||
let mut rev_base = base.chars().rev();
|
||||
let mut exp = exponent.clone();
|
||||
|
||||
//Add decimal places until exponent = 0
|
||||
while exp > 0{
|
||||
match rev_base.next(){
|
||||
Some(next_base_char) => res_decimal_places.push(next_base_char),
|
||||
None => res_decimal_places.push('0')
|
||||
}
|
||||
exp = exp-1;
|
||||
}
|
||||
|
||||
//Reverse decimal places back into right order
|
||||
res_decimal_places = res_decimal_places.into_iter().rev().collect::<Vec<char>>();
|
||||
|
||||
let mut new_base : Vec<char> = rev_base.rev().collect();
|
||||
|
||||
if let Some(ts) = thousands_seperator{
|
||||
new_base = add_thousands_seperator(new_base, ts)
|
||||
}
|
||||
|
||||
if new_base.is_empty(){
|
||||
new_base = vec!['0'];
|
||||
}
|
||||
|
||||
if res_decimal_places.len() > 0{
|
||||
format!("{}{}{}", new_base.iter().collect::<String>(), seperator, res_decimal_places.iter().collect::<String>())
|
||||
}else{
|
||||
new_base.iter().collect::<String>()
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
pub fn add_thousands_seperator(input: Vec<char>, thousands_seperator: char) -> Vec<char>{
|
||||
let mut input = input.into_iter().rev().peekable();
|
||||
let mut output : Vec<char> = Vec::new();
|
||||
let mut counter = 0;
|
||||
|
||||
while let Some(val) = input.next(){
|
||||
counter = counter+1;
|
||||
output.push(val);
|
||||
|
||||
if counter == 3 && input.peek().is_some(){
|
||||
output.push(thousands_seperator);
|
||||
counter = 0;
|
||||
}
|
||||
}
|
||||
|
||||
output.into_iter().rev().collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use std::str::FromStr;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_convert_positive() {
|
||||
let bd = BigDecimal::from_str("54000.16712").unwrap();
|
||||
println!("Exponent: {}", bd.as_bigint_and_exponent().1.to_string());
|
||||
assert_eq!(convert(bd, 2, ',', Some('.')), String::from("54.000,16712"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert_negative() {
|
||||
let bd = BigDecimal::from_str("-54000.16712").unwrap();
|
||||
assert_eq!(convert(bd, 2, ',', Some('.')), String::from("-54.000,16712"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert_no_decimal_places() {
|
||||
let bd = BigDecimal::from_str("-54000.16712").unwrap();
|
||||
assert_eq!(convert(bd, 0, ',', Some('.')), String::from("-54.000"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert_no_thousands_separator() {
|
||||
let bd = BigDecimal::from_str("-54000.16712").unwrap();
|
||||
assert_eq!(convert(bd, 0, ',', None), String::from("-54000"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert_positive_no_decimal_places() {
|
||||
let bd = BigDecimal::from_str("54000").unwrap();
|
||||
assert_eq!(convert(bd, 2, ',', Some('.')), String::from("54.000"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert_negative_no_decimal_places() {
|
||||
let bd = BigDecimal::from_str("-54000").unwrap();
|
||||
assert_eq!(convert(bd, 2, ',', Some('.')), String::from("-54.000"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert2() {
|
||||
let bd = BigDecimal::from_str("54000000000000").unwrap();
|
||||
assert_eq!(convert(bd, 2, ',', Some('.')), String::from("54.000.000.000.000"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert3() {
|
||||
let bd = BigDecimal::from_str("0.000000016712").unwrap();
|
||||
println!("Exponent: {}", bd.as_bigint_and_exponent().1.to_string());
|
||||
assert_eq!(convert(bd, 0, ',', None), String::from("0"));
|
||||
}
|
||||
#[test]
|
||||
pub fn test_convert4() {
|
||||
let bd = BigDecimal::from_str("0.000000016712").unwrap();
|
||||
println!("Exponent: {}", bd.as_bigint_and_exponent().1.to_string());
|
||||
assert_eq!(convert(bd, 2, ',', None), String::from("0,00"));
|
||||
}
|
||||
}
|
|
@ -16,4 +16,5 @@ pub mod mail_templates;
|
|||
pub mod permission;
|
||||
pub mod order_enum;
|
||||
pub mod time;
|
||||
pub mod serde_patch;
|
||||
pub mod serde_patch;
|
||||
pub mod bigdecimal_to_string;
|
|
@ -0,0 +1,66 @@
|
|||
use chrono::{DateTime, Utc};
|
||||
use diesel::sql_types::*;
|
||||
use serde_json::Value;
|
||||
|
||||
use crate::logger::LogActions;
|
||||
|
||||
/// LogEntry to create a new log entry with specified action, affected_entity (optional), causer (optional) and optional details
|
||||
/// Timestamp and log entry id are generated automatically
|
||||
pub struct InsertableLogEntry {
|
||||
pub action: String,
|
||||
pub affected_entity: Option<uuid::Uuid>,
|
||||
pub causer: Option<uuid::Uuid>,
|
||||
pub details: Option<Value>
|
||||
}
|
||||
|
||||
impl InsertableLogEntry{
|
||||
/// Create new LogEntry with given values
|
||||
pub fn new(action: LogActions, affected_entity: Option<uuid::Uuid>, causer: Option<uuid::Uuid>, details: Option<Value>) -> Self{
|
||||
InsertableLogEntry{
|
||||
action: action.value(),
|
||||
affected_entity,
|
||||
causer,
|
||||
details
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(QueryableByName)]
|
||||
pub struct LogEntry{
|
||||
#[sql_type = "Integer"]
|
||||
pub entry_id: i32,
|
||||
#[sql_type = "Timestamptz"]
|
||||
pub timestamp: DateTime<Utc>,
|
||||
#[sql_type = "Text"]
|
||||
pub action: String,
|
||||
#[sql_type = "Nullable<Uuid>"]
|
||||
pub affected_entity: Option<uuid::Uuid>,
|
||||
#[sql_type = "Nullable<Uuid>"]
|
||||
pub causer: Option<uuid::Uuid>,
|
||||
#[sql_type = "Nullable<Jsonb>"]
|
||||
pub details: Option<Value>
|
||||
}
|
||||
|
||||
#[derive(Serialize)]
|
||||
pub struct EventBillingStateApproveLogEntry{
|
||||
pub state: uuid::Uuid
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use serde_json::json;
|
||||
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
pub fn test_conversion() {
|
||||
let test = EventBillingStateApproveLogEntry{
|
||||
state: uuid::Uuid::parse_str("00000000-0000-0000-0000-000000001234").unwrap()
|
||||
};
|
||||
|
||||
let test3 : Value = serde_json::to_value(test).unwrap();
|
||||
|
||||
let comp = json!({"state": "00000000-0000-0000-0000-000000001234"});
|
||||
assert_eq!(test3, comp);
|
||||
}
|
||||
}
|
|
@ -0,0 +1,31 @@
|
|||
use diesel::{ExpressionMethods, RunQueryDsl};
|
||||
use rocket::State;
|
||||
|
||||
use crate::database::controller::connector::establish_connection;
|
||||
use crate::logger::entries::InsertableLogEntry;
|
||||
use crate::schema::log::dsl::*;
|
||||
use crate::Settings;
|
||||
|
||||
pub mod entries;
|
||||
|
||||
pub fn add_entry(entry: InsertableLogEntry, settings: &State<Settings>){
|
||||
let connection = establish_connection(settings);
|
||||
|
||||
if let Err(err) = diesel::insert_into(crate::schema::log::table).values((action.eq(entry.action), affected_entity.eq(entry.affected_entity), causer.eq(entry.causer), details.eq(entry.details))).execute(&connection){
|
||||
error!("Couldn't add new application log entry: {}", err);
|
||||
}
|
||||
}
|
||||
|
||||
pub enum LogActions{
|
||||
PasswordReset,
|
||||
BillingStateApproved,
|
||||
}
|
||||
|
||||
impl LogActions{
|
||||
fn value(&self) -> String {
|
||||
match *self{
|
||||
LogActions::PasswordReset => String::from("password_reset"),
|
||||
LogActions::BillingStateApproved => String::from("billing_state_approved")
|
||||
}
|
||||
}
|
||||
}
|
|
@ -3,6 +3,7 @@ extern crate base64;
|
|||
extern crate chrono;
|
||||
extern crate chrono_tz;
|
||||
extern crate config;
|
||||
extern crate core;
|
||||
#[macro_use]
|
||||
extern crate diesel;
|
||||
extern crate iban;
|
||||
|
@ -36,6 +37,7 @@ pub mod helper;
|
|||
pub mod modules;
|
||||
pub mod permissions;
|
||||
pub mod schema;
|
||||
pub mod logger;
|
||||
|
||||
#[launch]
|
||||
fn rocket() -> _ {
|
||||
|
|
|
@ -14,6 +14,8 @@ use crate::database::model::events::Event;
|
|||
use crate::helper::session_cookies::model::SessionCookie;
|
||||
use crate::helper::settings::Settings;
|
||||
use crate::helper::translate_diesel_error::translate_diesel;
|
||||
use crate::logger::{add_entry, LogActions};
|
||||
use crate::logger::entries::{EventBillingStateApproveLogEntry, InsertableLogEntry, LogEntry};
|
||||
use crate::MailQueue;
|
||||
use crate::modules::api::events::create::CreateEventData;
|
||||
use crate::modules::api::member_management::controller::parser::{parse_member_cookie, parse_option_uuid, parse_uuid_string};
|
||||
|
@ -246,6 +248,13 @@ pub async fn approve(
|
|||
|
||||
match crate::database::controller::events::approve_stage(settings, event_id, stage, caller.entity_id) {
|
||||
Ok(_) => {
|
||||
let details = EventBillingStateApproveLogEntry{
|
||||
state: stage
|
||||
};
|
||||
|
||||
//Log approval
|
||||
add_entry(InsertableLogEntry::new(LogActions::BillingStateApproved, Some(event_id), Some(caller.entity_id), Some(serde_json::to_value(details).unwrap())), settings);
|
||||
|
||||
if billing_state.final_approve {
|
||||
if let Err(e) = finish_billing(settings, event_id) {
|
||||
return Err(translate_diesel(e));
|
||||
|
|
|
@ -8,14 +8,14 @@ pub fn calculate_hours(real_start_time: chrono::DateTime<Utc>, real_end_time: ch
|
|||
return Some(0);
|
||||
}
|
||||
|
||||
let hours_since_start = (timestamp_start / 3600.0).floor();
|
||||
let hours_since_end = (timestamp_end / 3600.0).ceil();
|
||||
let hours_since_start = (timestamp_start / 3600.0);
|
||||
let hours_since_end = (timestamp_end / 3600.0);
|
||||
|
||||
if hours_since_start > hours_since_end {
|
||||
error!("real_start_time is after real_end_time");
|
||||
None
|
||||
} else {
|
||||
Some((hours_since_end - hours_since_start) as i32)
|
||||
Some((hours_since_end - hours_since_start).ceil() as i32)
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -6,11 +6,13 @@ 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::time::utc_to_local_user_time;
|
||||
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)]
|
||||
|
@ -108,7 +110,7 @@ pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) ->
|
|||
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")));
|
||||
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)?;
|
||||
|
@ -116,17 +118,17 @@ pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) ->
|
|||
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) => utc_to_local_user_time(settings, None, start),
|
||||
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) => utc_to_local_user_time(settings, None, end),
|
||||
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(personnel.money_from_lump_sum.to_string()), sanitize(personnel.money_for_time.to_string()), sanitize(personnel.total_money.to_string())));
|
||||
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(personnel.money_from_lump_sum.to_string()), 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));
|
||||
|
@ -135,6 +137,24 @@ pub fn generate_billing_csv(settings: &State<Settings>, event_id: uuid::Uuid) ->
|
|||
}
|
||||
}
|
||||
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)
|
||||
}
|
||||
|
|
118
src/schema.rs
118
src/schema.rs
|
@ -1,6 +1,6 @@
|
|||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
addresses (id) {
|
||||
id -> Uuid,
|
||||
|
@ -15,7 +15,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
addresses_entities (address_id, entitiy_id) {
|
||||
address_id -> Uuid,
|
||||
|
@ -25,7 +25,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
appointment_types (type_id) {
|
||||
type_id -> Uuid,
|
||||
|
@ -37,7 +37,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
appointments (id) {
|
||||
id -> Uuid,
|
||||
|
@ -49,7 +49,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
billing_states (entity_id) {
|
||||
name -> Text,
|
||||
|
@ -62,7 +62,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
buildings (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -73,7 +73,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
communication_targets (target_id) {
|
||||
target_id -> Uuid,
|
||||
|
@ -86,7 +86,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
communication_types (type_id) {
|
||||
type_id -> Uuid,
|
||||
|
@ -96,7 +96,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
cost_centres (short_id) {
|
||||
short_id -> Int4,
|
||||
|
@ -106,7 +106,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
cost_centres_members (member_entity_id, cost_centre_shortid) {
|
||||
member_entity_id -> Uuid,
|
||||
|
@ -116,7 +116,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
entities (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -125,7 +125,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_instances (instance_id) {
|
||||
instance_id -> Uuid,
|
||||
|
@ -144,7 +144,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_position_instances (position_instance_id) {
|
||||
instance_id -> Uuid,
|
||||
|
@ -159,7 +159,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_positions (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -171,7 +171,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_positions_templates (position_template_id) {
|
||||
position_entity_id -> Uuid,
|
||||
|
@ -184,7 +184,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_templates (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -195,7 +195,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
eu_vehicle_positions (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -208,7 +208,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
event_organisers (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -223,7 +223,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
event_requests (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -246,7 +246,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
event_types (type_id) {
|
||||
type_id -> Uuid,
|
||||
|
@ -258,7 +258,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
events (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -281,7 +281,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
group_entity_state (state_id) {
|
||||
state_id -> Uuid,
|
||||
|
@ -293,7 +293,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
groups (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -304,7 +304,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
groups_entities (group_id, entity_id) {
|
||||
group_id -> Uuid,
|
||||
|
@ -315,7 +315,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
license_categories (name) {
|
||||
name -> Text,
|
||||
|
@ -325,7 +325,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
licenses_members (member_id, license_name) {
|
||||
member_id -> Uuid,
|
||||
|
@ -336,7 +336,31 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
log (entry_id) {
|
||||
entry_id -> Int4,
|
||||
timestamp -> Timestamptz,
|
||||
action -> Text,
|
||||
affected_entity -> Nullable<Uuid>,
|
||||
causer -> Nullable<Uuid>,
|
||||
details -> Nullable<Jsonb>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
log_actions (action) {
|
||||
action -> Text,
|
||||
description -> Nullable<Text>,
|
||||
}
|
||||
}
|
||||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
login_attempts (id) {
|
||||
id -> Uuid,
|
||||
|
@ -347,7 +371,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
login_attempts_usernames (id) {
|
||||
id -> Uuid,
|
||||
|
@ -358,7 +382,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
members (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -382,7 +406,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
members_roles (member_id, role_id) {
|
||||
member_id -> Uuid,
|
||||
|
@ -392,7 +416,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
notification_types (name) {
|
||||
name -> Text,
|
||||
|
@ -402,7 +426,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
password_resets (token) {
|
||||
token -> Text,
|
||||
|
@ -413,7 +437,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
permissions (permission) {
|
||||
permission -> Text,
|
||||
|
@ -425,7 +449,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
personnel_billing (position_instance_id) {
|
||||
position_instance_id -> Uuid,
|
||||
|
@ -439,7 +463,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
personnel_billing_rates (billing_rate_id) {
|
||||
billing_rate_id -> Uuid,
|
||||
|
@ -453,7 +477,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualification_categories (id) {
|
||||
id -> Uuid,
|
||||
|
@ -464,7 +488,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualifications (id) {
|
||||
id -> Uuid,
|
||||
|
@ -476,7 +500,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
qualifications_members (member_id, qualification_id) {
|
||||
member_id -> Uuid,
|
||||
|
@ -486,7 +510,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles (id) {
|
||||
id -> Text,
|
||||
|
@ -496,7 +520,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles_permissions (role_permission_id) {
|
||||
role_id -> Text,
|
||||
|
@ -507,7 +531,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
roles_permissions_context (role_permission_id, entity) {
|
||||
role_permission_id -> Uuid,
|
||||
|
@ -517,7 +541,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
units (unit_id) {
|
||||
unit_id -> Uuid,
|
||||
|
@ -527,7 +551,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
units_members (unit_id, member_id) {
|
||||
unit_id -> Uuid,
|
||||
|
@ -538,7 +562,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
users (id) {
|
||||
id -> Uuid,
|
||||
|
@ -551,7 +575,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
vehicle_categories (id) {
|
||||
id -> Uuid,
|
||||
|
@ -562,7 +586,7 @@ table! {
|
|||
|
||||
table! {
|
||||
use diesel::sql_types::*;
|
||||
|
||||
use diesel_geometry::sql_types::*;
|
||||
|
||||
vehicles (entity_id) {
|
||||
entity_id -> Uuid,
|
||||
|
@ -652,6 +676,8 @@ allow_tables_to_appear_in_same_query!(
|
|||
groups_entities,
|
||||
license_categories,
|
||||
licenses_members,
|
||||
log,
|
||||
log_actions,
|
||||
login_attempts,
|
||||
login_attempts_usernames,
|
||||
members,
|
||||
|
|
Loading…
Reference in New Issue