use crate::{
commands::{Cli, Config, Error, Result},
jobber::Jobber,
views::{BillView, ClientView, CompanyView, InvoiceView, WorkView},
};
use clap::Parser;
use color_print::cprintln;
use typst::foundations::Array;
use typst_as_lib::{TypstEngine, typst_kit_options::TypstKitFontOptions};
use typst_library::foundations::{Dict, Value};
#[derive(Parser, Debug)]
pub(crate) struct ExportInvoice {
no: usize,
#[clap(short, long, default_value = ":default")]
template: String,
}
impl ExportInvoice {
pub(crate) fn run(&self, cli: &Cli) -> Result<()> {
let config = Config::load()?;
let database = cli.open_database()?;
let template = if self.template.starts_with(':') {
config
.app_path()?
.join(&self.template[1..])
.join("invoice.typ")
} else {
std::path::PathBuf::from(&self.template)
};
export_pdf(self.no, &database, &template)?;
cprintln!("Successfully exported invoice <s>{}</>.", self.no);
Ok(())
}
}
fn insert(dict: &mut Dict, key: &str, s: &str) {
dict.insert(key.into(), Value::Str(s.into()));
}
impl std::convert::From<InvoiceView> for Value {
fn from(invoice: InvoiceView) -> Value {
let mut dict = Dict::new();
insert(&mut dict, "no", &invoice.no);
insert(&mut dict, "date", &invoice.date);
insert(&mut dict, "client", &invoice.client);
insert(&mut dict, "subject", &invoice.subject);
insert(&mut dict, "worked", &invoice.worked);
insert(&mut dict, "revenue", &invoice.revenue);
insert(&mut dict, "tax", &invoice.tax);
insert(&mut dict, "revenue_taxed", &invoice.revenue_taxed);
Value::Dict(dict)
}
}
impl std::convert::From<&BillView> for Value {
fn from(bill: &BillView) -> Value {
let mut dict = Dict::new();
insert(&mut dict, "pos", &bill.pos.to_string());
insert(&mut dict, "worker", &bill.worker);
insert(&mut dict, "subject", &bill.subject);
insert(&mut dict, "rate", &bill.rate);
insert(&mut dict, "interval", &bill.interval);
insert(&mut dict, "worked", &bill.worked);
insert(&mut dict, "revenue", &bill.revenue);
Value::Dict(dict)
}
}
impl std::convert::From<&CompanyView> for Value {
fn from(company: &CompanyView) -> Value {
let mut dict = Dict::new();
insert(&mut dict, "name", &company.name);
insert(&mut dict, "street", &company.street);
insert(&mut dict, "zip", &company.zip);
insert(&mut dict, "city", &company.city);
if let Some(vat) = &company.vat {
insert(&mut dict, "vat", vat);
}
if let Some(bank) = &company.bank {
insert(&mut dict, "bank", &bank.name);
insert(&mut dict, "iban", &bank.iban);
insert(&mut dict, "bic", &bank.bic);
}
insert(&mut dict, "register", &company.register);
Value::Dict(dict)
}
}
impl std::convert::From<&ClientView> for Value {
fn from(client: &ClientView) -> Value {
let mut dict = Dict::new();
insert(&mut dict, "name", &client.name);
insert(&mut dict, "street", &client.street);
insert(&mut dict, "zip", &client.zip);
insert(&mut dict, "city", &client.city);
insert(&mut dict, "country", &client.country);
insert(&mut dict, "vat", &client.vat);
insert(&mut dict, "rate", &client.rate);
insert(&mut dict, "interval", &client.interval);
insert(&mut dict, "billing", &client.billing);
insert(&mut dict, "work", &client.work);
Value::Dict(dict)
}
}
impl std::convert::From<&WorkView> for Value {
fn from(work: &WorkView) -> Value {
let mut dict = Dict::new();
insert(&mut dict, "no", &work.no);
insert(&mut dict, "start", &work.start);
insert(&mut dict, "end", &work.end);
insert(&mut dict, "duration", &work.duration);
insert(&mut dict, "subject", &work.subject);
insert(&mut dict, "tags", &work.tags);
insert(&mut dict, "order", &work.order.to_string());
Value::Dict(dict)
}
}
fn export_pdf(
no: usize,
database: &impl Jobber,
template: &impl AsRef<std::path::Path>,
) -> Result<()> {
let config = Config::load()?;
let template_folder = template.as_ref().parent();
let invoice = InvoiceView::new(no, database, false)?;
let mut doc = Dict::new();
doc.insert("invoice".into(), invoice.into());
let invoice = database.get_invoice(no)?;
let mut works = Vec::new();
let doc_bills = Array::from_iter(
invoice
.bills
.iter()
.enumerate()
.inspect(|(pos, bill)| {
let work: Vec<_> = bill
.work
.iter()
.enumerate()
.map(|(pos, work)| {
WorkView::new(Some(pos), work, database.get_tags(), &bill.scheme, false)
})
.collect();
works.push((
*pos,
database
.get_full_worker_name(&bill.worker)
.expect("unknown worker"),
work,
));
})
.map(|(pos, bill)| BillView::new(pos, bill, database, false))
.collect::<Result<Vec<_>>>()?
.iter()
.map(|bill| bill.into()),
);
doc.insert("bills".into(), Value::Array(doc_bills));
let doc_sheets = Array::from_iter(works.iter().map(|(pos, worker, works)| {
let doc_works = Array::from_iter(works.iter().map(|work| -> Value { work.into() }));
{
let mut dict = Dict::new();
dict.insert("worker".into(), Value::Str(worker.clone().into()));
dict.insert("works".into(), Value::Array(doc_works));
dict.insert("pos".into(), Value::Int(*pos as i64));
Value::Dict(dict)
}
}));
doc.insert("sheets".into(), Value::Array(doc_sheets));
let company_view = &CompanyView::new(database, false)?;
doc.insert("company".into(), company_view.into());
let client_view = &ClientView::new(&invoice.client, &config, database, false)?;
doc.insert("client".into(), client_view.into());
let template = std::fs::read_to_string(template)?;
let engine = TypstEngine::builder()
.main_file(template)
.search_fonts_with(TypstKitFontOptions::default().include_system_fonts(true))
.with_file_system_resolver(template_folder.unwrap_or(&std::path::PathBuf::from(".")))
.build();
let doc = engine.compile_with_input(doc).output?;
let pdf = typst_pdf::pdf(&doc, &Default::default()).map_err(|errors| {
let msg = errors
.iter()
.map(|e| e.message.to_string())
.collect::<Vec<_>>()
.join(", ");
Error::TypstPDF(msg)
})?;
std::fs::write(format!("invoice_{no}.pdf"), pdf)?;
Ok(())
}