Multiply price and amount by 0.8 before generating HTML. Original CSV and TXT files remain unchanged.
313 lines
8.9 KiB
Rust
313 lines
8.9 KiB
Rust
use askama::Template;
|
|
use chrono::{NaiveDateTime, Utc};
|
|
use csv::ReaderBuilder;
|
|
use std::collections::HashMap;
|
|
use std::env;
|
|
use std::fs;
|
|
use std::path::Path;
|
|
|
|
mod invoice_generator;
|
|
|
|
use invoice_generator::{group_by_customer, read_csv_file, Customer};
|
|
|
|
fn fmt(v: f64) -> String {
|
|
format!("{:.2}", v)
|
|
}
|
|
|
|
fn clean_csv_file(
|
|
input_path: &Path,
|
|
output_path: &Path,
|
|
) -> Result<String, Box<dyn std::error::Error>> {
|
|
let file = fs::File::open(input_path)?;
|
|
let mut rdr = ReaderBuilder::new()
|
|
.delimiter(b'\t')
|
|
.has_headers(true)
|
|
.flexible(true)
|
|
.from_reader(file);
|
|
|
|
let output = fs::File::create(output_path)?;
|
|
let mut writer = csv::WriterBuilder::new()
|
|
.delimiter(b'\t')
|
|
.from_writer(output);
|
|
|
|
let mut batch_number = String::new();
|
|
|
|
for result in rdr.records() {
|
|
let record = result?;
|
|
|
|
let date_str = record.get(0).unwrap_or("");
|
|
let batch = record.get(1).unwrap_or("").to_string();
|
|
|
|
if batch_number.is_empty() {
|
|
batch_number = batch.clone();
|
|
}
|
|
|
|
let date =
|
|
NaiveDateTime::parse_from_str(date_str, "%m/%d/%Y %I:%M:%S %p").unwrap_or_else(|_| {
|
|
NaiveDateTime::parse_from_str(date_str, "%Y-%m-%d %H:%M:%S").unwrap_or_default()
|
|
});
|
|
|
|
let row = vec![
|
|
date.format("%Y-%m-%d %H:%M:%S").to_string(),
|
|
batch,
|
|
record.get(2).unwrap_or("").to_string(),
|
|
record.get(3).unwrap_or("").to_string(),
|
|
record.get(4).unwrap_or("").to_string(),
|
|
record.get(5).unwrap_or("").to_string(),
|
|
record.get(6).unwrap_or("").to_string(),
|
|
record.get(7).unwrap_or("").to_string(),
|
|
record.get(8).unwrap_or("").to_string(),
|
|
record.get(9).unwrap_or("").to_string(),
|
|
record.get(10).unwrap_or("").to_string(),
|
|
record.get(11).unwrap_or("").to_string(),
|
|
record.get(12).unwrap_or("").to_string(),
|
|
record.get(13).unwrap_or("").to_string(),
|
|
record.get(14).unwrap_or("").to_string(),
|
|
record.get(15).unwrap_or("").to_string(),
|
|
];
|
|
|
|
writer.write_record(&row)?;
|
|
}
|
|
|
|
writer.flush()?;
|
|
Ok(batch_number)
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct ProductSummary {
|
|
name: String,
|
|
volume: String,
|
|
amount: String,
|
|
avg_price: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct Summary {
|
|
total_volume: String,
|
|
grand_total: String,
|
|
products: Vec<ProductSummary>,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct CardData {
|
|
card_number: String,
|
|
transactions: Vec<FormattedTransaction>,
|
|
total_amount: String,
|
|
total_volume: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct FormattedTransaction {
|
|
date: String,
|
|
quality_name: String,
|
|
price: String,
|
|
volume: String,
|
|
amount: String,
|
|
receipt: String,
|
|
}
|
|
|
|
#[derive(Clone)]
|
|
struct PreparedCustomer {
|
|
customer_number: String,
|
|
cards: Vec<CardData>,
|
|
summary: Summary,
|
|
}
|
|
|
|
impl PreparedCustomer {
|
|
fn from_customer(customer: Customer) -> Self {
|
|
let cards: Vec<CardData> = customer
|
|
.cards
|
|
.into_iter()
|
|
.map(|(card_number, transactions)| {
|
|
let formatted_txs: Vec<FormattedTransaction> = transactions
|
|
.into_iter()
|
|
.map(|t| FormattedTransaction {
|
|
date: t.date.format("%Y-%m-%d %H:%M").to_string(),
|
|
quality_name: t.quality_name,
|
|
price: fmt(t.price * 0.8),
|
|
volume: fmt(t.volume),
|
|
amount: fmt(t.amount * 0.8),
|
|
receipt: t.receipt,
|
|
})
|
|
.collect();
|
|
|
|
let total_amount: f64 = formatted_txs
|
|
.iter()
|
|
.map(|t| t.amount.parse::<f64>().unwrap())
|
|
.sum();
|
|
let total_volume: f64 = formatted_txs
|
|
.iter()
|
|
.map(|t| t.volume.parse::<f64>().unwrap())
|
|
.sum();
|
|
|
|
CardData {
|
|
card_number,
|
|
transactions: formatted_txs,
|
|
total_amount: fmt(total_amount),
|
|
total_volume: fmt(total_volume),
|
|
}
|
|
})
|
|
.collect();
|
|
|
|
let grand_total: f64 = cards
|
|
.iter()
|
|
.map(|c| c.total_amount.parse::<f64>().unwrap())
|
|
.sum();
|
|
|
|
let mut product_totals: HashMap<String, (f64, f64)> = HashMap::new();
|
|
for card in &cards {
|
|
for tx in &card.transactions {
|
|
let volume: f64 = tx.volume.parse().unwrap();
|
|
let amount: f64 = tx.amount.parse::<f64>().unwrap();
|
|
let entry = product_totals
|
|
.entry(tx.quality_name.clone())
|
|
.or_insert((0.0, 0.0));
|
|
entry.0 += volume;
|
|
entry.1 += amount;
|
|
}
|
|
}
|
|
|
|
let mut products: Vec<ProductSummary> = product_totals
|
|
.into_iter()
|
|
.map(|(name, (volume, amount))| {
|
|
let avg_price = if volume > 0.0 { amount / volume } else { 0.0 };
|
|
ProductSummary {
|
|
name,
|
|
volume: fmt(volume),
|
|
amount: fmt(amount),
|
|
avg_price: fmt(avg_price),
|
|
}
|
|
})
|
|
.collect();
|
|
products.sort_by(|a, b| a.name.cmp(&b.name));
|
|
|
|
let total_volume: f64 = products
|
|
.iter()
|
|
.map(|p| p.volume.parse::<f64>().unwrap())
|
|
.sum();
|
|
|
|
let summary = Summary {
|
|
total_volume: fmt(total_volume),
|
|
grand_total: fmt(grand_total),
|
|
products,
|
|
};
|
|
|
|
PreparedCustomer {
|
|
customer_number: customer.customer_number,
|
|
cards,
|
|
summary,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "index.html")]
|
|
struct IndexTemplate {
|
|
customers: Vec<(String, usize)>,
|
|
period: String,
|
|
}
|
|
|
|
#[derive(Template)]
|
|
#[template(path = "customer.html")]
|
|
struct CustomerTemplate {
|
|
customer: PreparedCustomer,
|
|
period: String,
|
|
generated_date: String,
|
|
}
|
|
|
|
fn main() -> Result<(), Box<dyn std::error::Error>> {
|
|
let args: Vec<String> = env::args().collect();
|
|
|
|
if args.len() != 3 {
|
|
eprintln!("Användning: {} <csv-fil> <utdatakatalog>", args[0]);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let input_path = Path::new(&args[1]);
|
|
let base_output_dir = Path::new(&args[2]);
|
|
|
|
if !input_path.exists() {
|
|
eprintln!("Fel: Filen hittades inte: {:?}", input_path);
|
|
std::process::exit(1);
|
|
}
|
|
|
|
let filename = input_path
|
|
.file_name()
|
|
.and_then(|n| n.to_str())
|
|
.unwrap_or("unknown")
|
|
.to_string();
|
|
|
|
println!("Konverterar {} till rensat format...", filename);
|
|
|
|
let temp_cleaned_path =
|
|
base_output_dir.join(format!("{}.temp.csv", filename.trim_end_matches(".txt")));
|
|
let batch_number = clean_csv_file(input_path, &temp_cleaned_path)?;
|
|
|
|
let output_dir = base_output_dir.join(&batch_number);
|
|
fs::create_dir_all(&output_dir)?;
|
|
|
|
fs::copy(input_path, output_dir.join(format!("{}.txt", batch_number)))?;
|
|
fs::rename(
|
|
&temp_cleaned_path,
|
|
output_dir.join(format!("{}.csv", batch_number)),
|
|
)?;
|
|
|
|
println!(
|
|
"Konverterade {} transaktioner",
|
|
fs::read_to_string(output_dir.join(format!("{}.csv", batch_number)))?
|
|
.lines()
|
|
.count()
|
|
- 1
|
|
);
|
|
|
|
let batch = read_csv_file(&output_dir.join(format!("{}.csv", batch_number)))?;
|
|
println!("Laddade {} transaktioner", batch.transactions.len());
|
|
|
|
let first_date = batch.transactions.first().map(|t| t.date).unwrap();
|
|
let last_date = batch.transactions.last().map(|t| t.date).unwrap();
|
|
let period = format!(
|
|
"{} - {}",
|
|
first_date.format("%Y-%m-%d"),
|
|
last_date.format("%Y-%m-%d")
|
|
);
|
|
|
|
let customers = group_by_customer(&[batch]);
|
|
|
|
let index_customers: Vec<(String, usize)> = customers
|
|
.iter()
|
|
.map(|(num, c)| (num.clone(), c.cards.len()))
|
|
.collect();
|
|
|
|
let html = IndexTemplate {
|
|
customers: index_customers.clone(),
|
|
period: period.clone(),
|
|
}
|
|
.render()
|
|
.unwrap();
|
|
fs::write(output_dir.join("index.html"), html)?;
|
|
|
|
let generated_date = Utc::now().format("%Y-%m-%d %H:%M").to_string();
|
|
|
|
let customer_count = customers.len();
|
|
for (customer_num, customer) in customers {
|
|
let prepared = PreparedCustomer::from_customer(customer);
|
|
let customer_html = CustomerTemplate {
|
|
customer: prepared,
|
|
period: period.clone(),
|
|
generated_date: generated_date.clone(),
|
|
}
|
|
.render()
|
|
.unwrap();
|
|
let filename = format!("customer_{}.html", customer_num);
|
|
fs::write(output_dir.join(&filename), customer_html)?;
|
|
println!("Genererade {}", filename);
|
|
}
|
|
|
|
println!(
|
|
"\nGenererade {} kundfakturor i {:?}",
|
|
customer_count, output_dir
|
|
);
|
|
|
|
Ok(())
|
|
}
|