use crate::error::FormsError;
use crate::map::{AmountCols, ScheduleDMap};
use crate::verify::{column_x_bands, in_band, no_unmapped_filled, Geo, Placement};
use crate::{fmt_money, pdf};
use btctax_core::ScheduleDTotals;
const SD_COL_D: usize = 0;
const SD_COL_E: usize = 1;
const SD_COL_H: usize = 3;
fn active(p: &btctax_core::ScheduleDPart) -> bool {
!p.proceeds.is_zero() || !p.cost_basis.is_zero() || !p.gain.is_zero()
}
fn push_amount_line(
line: &AmountCols,
proceeds: &str,
cost: &str,
gain: &str,
writes: &mut Vec<(String, pdf::FieldValue)>,
placements: &mut Vec<Placement>,
) {
for (fqn, value, col) in [
(&line.proceeds_d, proceeds, SD_COL_D),
(&line.cost_e, cost, SD_COL_E),
(&line.gain_h, gain, SD_COL_H),
] {
writes.push((fqn.clone(), pdf::FieldValue::Text(value.to_string())));
placements.push(Placement {
fqn: fqn.clone(),
geo: Geo::Data { row: 0, col },
});
}
}
pub fn fill_schedule_d_totals(
totals: &ScheduleDTotals,
map: &ScheduleDMap,
) -> Result<Vec<u8>, FormsError> {
let mut writes: Vec<(String, pdf::FieldValue)> = Vec::new();
let mut placements: Vec<Placement> = Vec::new();
let st_active = active(&totals.st);
let lt_active = active(&totals.lt);
if st_active {
push_amount_line(
&map.line3,
&fmt_money(totals.st.proceeds),
&fmt_money(totals.st.cost_basis),
&fmt_money(totals.st.gain),
&mut writes,
&mut placements,
);
push_h_line(
&map.line7_h,
&fmt_money(totals.st.gain),
&mut writes,
&mut placements,
);
}
if lt_active {
push_amount_line(
&map.line10,
&fmt_money(totals.lt.proceeds),
&fmt_money(totals.lt.cost_basis),
&fmt_money(totals.lt.gain),
&mut writes,
&mut placements,
);
push_h_line(
&map.line15_h,
&fmt_money(totals.lt.gain),
&mut writes,
&mut placements,
);
}
if st_active || lt_active {
let total = totals.st.gain + totals.lt.gain;
push_h_line(
&map.line16_h,
&fmt_money(total),
&mut writes,
&mut placements,
);
}
if let Some(qof_no) = &map.qof_no {
writes.push((
qof_no.field.clone(),
pdf::FieldValue::Check {
on: qof_no.on.clone(),
},
));
placements.push(Placement {
fqn: qof_no.field.clone(),
geo: Geo::Check,
});
}
let mut doc = pdf::load(pdf::schedule_d_pdf(map.year)?)?;
let index = pdf::index(&pdf::collect_fields(&doc)?);
pdf::drop_xfa_and_set_needappearances(&mut doc)?;
pdf::apply_writes(&mut doc, &index, &writes)?;
pdf::strip_nondeterminism(&mut doc);
let bytes = pdf::save(&mut doc)?;
let check = pdf::load(&bytes)?;
let fields = pdf::collect_fields(&check)?;
verify_schedule_d(&check, &fields, &placements, &map.table_token)?;
Ok(bytes)
}
fn push_h_line(
fqn: &str,
value: &str,
writes: &mut Vec<(String, pdf::FieldValue)>,
placements: &mut Vec<Placement>,
) {
writes.push((fqn.to_string(), pdf::FieldValue::Text(value.to_string())));
placements.push(Placement {
fqn: fqn.to_string(),
geo: Geo::Total { col: SD_COL_H }, });
}
pub fn verify_schedule_d(
doc: &lopdf::Document,
fields: &[pdf::Field],
placements: &[Placement],
table_token: &str,
) -> Result<(), FormsError> {
let bands = column_x_bands(fields, 0, table_token)?;
let index: std::collections::HashMap<&str, &pdf::Field> =
fields.iter().map(|f| (f.fqn.as_str(), f)).collect();
for p in placements {
let field = index
.get(p.fqn.as_str())
.ok_or_else(|| FormsError::MapFieldMissing(p.fqn.clone()))?;
let col = match &p.geo {
Geo::Data { col, .. } => *col,
Geo::Total { col } => *col,
Geo::Check => continue,
};
if p.fqn.contains("Page2") {
continue;
}
let cx = field
.cx()
.ok_or_else(|| FormsError::Geometry(format!("{}: no /Rect", p.fqn)))?;
let band = *bands
.get(col)
.ok_or_else(|| FormsError::Geometry(format!("column {col} out of range")))?;
if !in_band(cx, band) {
return Err(FormsError::Geometry(format!(
"{}: x-center {cx:.1} not in amount column {col} band {band:?} (mis-mapped column)",
p.fqn
)));
}
}
no_unmapped_filled(doc, fields, placements)
}