use std::io::{self, Write}; use std::fs::File; use std::path::Path; use std::rc::Rc; use std::cmp; use std::collections::HashMap; enum TableCell { TableHeader { content: String, rowspan: Option, colspan: Option }, TableData { content: String, rowspan: Option, colspan: Option }, EmptyCell { rowspan: Option, colspan: Option } } impl TableCell { fn to_html(&self) -> String { match *self { TableCell::TableHeader { ref content, rowspan: Some(rowspan), colspan: Some(colspan) } => { format!(r#"{}"#, rowspan, colspan, content) } TableCell::TableHeader { ref content, rowspan: Some(rowspan), .. } => { format!(r#"{}"#, rowspan, content) } TableCell::TableHeader { ref content, colspan: Some(colspan), .. } => { format!(r#"{}"#, colspan, content) } TableCell::TableHeader { ref content, .. } => { format!(r#"{}"#, content) } TableCell::TableData { ref content, rowspan: Some(rowspan), colspan: Some(colspan) } => { format!(r#"{}"#, rowspan, colspan, content) } TableCell::TableData { ref content, rowspan: Some(rowspan), .. } => { format!(r#"{}"#, rowspan, content) } TableCell::TableData { ref content, colspan: Some(colspan), .. } => { format!(r#"{}"#, colspan, content) } TableCell::TableData { ref content, .. } => { format!(r#"{}"#, content) } TableCell::EmptyCell { rowspan: Some(rowspan), colspan: Some(colspan) } => { format!(r#""#, rowspan, colspan) } TableCell::EmptyCell { rowspan: Some(rowspan), .. } => { format!(r#""#, rowspan) } TableCell::EmptyCell { colspan: Some(colspan), .. } => { format!(r#""#, colspan) } TableCell::EmptyCell {..} => { format!(r#""#) } } } } type Row = Vec; struct Table(Vec); impl Table { fn to_html(&self) -> String { let table_body = self.0.iter().map(|row| { format!("{}", row.iter().map(|cell| cell.to_html()).collect::>().concat()) }).collect::>().concat(); format!("{}
", table_body) } } struct Html(Vec); impl Html { fn to_html(&self) -> String { format!(r#" {}"#, self.0.iter() .map(|tb| tb.to_html()) .collect::>() .concat()) } fn save_to>(&self, path: P) -> io::Result<()> { let mut file = try!(File::create(path)); try!(file.write_all(self.to_html().as_bytes())); Ok(()) } } #[derive(Clone)] struct Costo { cantidad: u32, producto: String, precio_unitario: f64 } impl Costo { fn get_total(&self) -> f64 { self.cantidad as f64 * self.precio_unitario } } struct Tarea { nombre: String, dur: u32, materiales: Vec, mano_obra: Vec, costos_indirectos: Vec } impl Tarea { fn new(nombre: String, dur: u32) -> Tarea { Tarea { nombre: nombre, dur: dur, materiales: Vec::new(), mano_obra: Vec::new(), costos_indirectos: Vec::new() } } fn add_materiales(&mut self, material: Costo) { self.materiales.push(material); } fn add_mano_obra(&mut self, mano_obra: Costo) { self.mano_obra.push(mano_obra); } fn add_costo_indirecto(&mut self, costo_indirecto: Costo) { self.costos_indirectos.push(costo_indirecto); } fn add_mano_obra_from_builders(&mut self, builders: &[Rc Costo>]) { for builder in builders { let costo = (builder)(self.dur, self.get_total_materiales(), self.get_total_mano_obra(), self.get_total_costo_indirectos()); self.add_mano_obra(costo); } } fn add_costo_indirecto_from_builders(&mut self, builders: &[Rc Costo>]) { for builder in builders { let costo = (builder)(self.dur, self.get_total_materiales(), self.get_total_mano_obra(), self.get_total_costo_indirectos()); self.add_costo_indirecto(costo); } } fn get_total_materiales(&self) -> f64 { self.materiales.iter().map(|c| c.get_total()).sum() } fn get_total_mano_obra(&self) -> f64 { self.mano_obra.iter().map(|c| c.get_total()).sum() } fn get_total_costo_indirectos(&self) -> f64 { self.costos_indirectos.iter().map(|c| c.get_total()).sum() } fn get_max_row(&self) -> usize { cmp::max(self.materiales.len(), cmp::max(self.mano_obra.len(), self.costos_indirectos.len())) } fn get_presupuesto(&self) -> Presupuesto { let mut presupuesto = Presupuesto::new(); for material in &self.materiales { presupuesto.add_costo(Presupuesto::material(), material); } for mano_obra in &self.mano_obra { presupuesto.add_costo(Presupuesto::mano_obra(), mano_obra); } for costo_indirecto in &self.costos_indirectos { presupuesto.add_costo(Presupuesto::costo_indirecto(), costo_indirecto); } presupuesto } } struct Actividad { nombre: String, tareas: Vec } impl Actividad { fn add_mano_obra_from_builders(&mut self, builders: &[Rc Costo>]) { for task in &mut self.tareas { task.add_mano_obra_from_builders(builders); } } fn add_costo_indirecto_from_builders(&mut self, builders: &[Rc Costo>]) { for task in &mut self.tareas { task.add_costo_indirecto_from_builders(builders); } } fn get_dur_total(&self) -> u32 { self.tareas.iter().map(|task| task.dur).sum() } fn get_total_materiales(&self) -> f64 { self.tareas.iter().map(|task| task.get_total_materiales()).sum() } fn get_total_mano_obra(&self) -> f64 { self.tareas.iter().map(|task| task.get_total_mano_obra()).sum() } fn get_total_costo_indirectos(&self) -> f64 { self.tareas.iter().map(|task| task.get_total_costo_indirectos()).sum() } fn get_presupuesto(&self) -> Option { self.tareas.iter().map(|task| task.get_presupuesto()).fold(None, |p1, p2| { match p1 { Some(p1) => Some(p1.merge(p2)), None => Some(p2) } }) } fn get_rows(&self) -> Vec { let total_rows = self.tareas.iter().map(|task| task.get_max_row() + 1).sum(); self.tareas.iter().enumerate().flat_map(|(i1, task)| { let mut materiales_iter = task.materiales.iter(); let mut mano_obra_iter = task.mano_obra.iter(); let mut costos_indirectos_iter = task.costos_indirectos.iter(); let costo_total_tarea = task.get_total_materiales() + task.get_total_mano_obra() + task.get_total_costo_indirectos(); let task_rows = task.get_max_row() + 1; let mut rows = Vec::new(); for i2 in 0..task_rows { let mut row = Vec::new(); if i2 == 0 { if i1 == 0{ row.push(TableCell::TableData { content: self.nombre.clone(), rowspan: Some(total_rows), colspan: None }); } row.push(TableCell::TableData { content: task.nombre.clone(), rowspan: Some(task_rows), colspan: None }); row.push(TableCell::TableData { content: task.dur.to_string(), rowspan: Some(task_rows), colspan: None }); } macro_rules! add_section { ($iter:expr, $total:expr) => {{ if let Some(costo) = $iter.next() { row.push(TableCell::TableData { content: costo.cantidad.to_string(), rowspan: None, colspan: None }); row.push(TableCell::TableData { content: costo.producto.clone(), rowspan: None, colspan: None }); row.push(TableCell::TableData { content: format!("{:.2}", costo.precio_unitario), rowspan: None, colspan: None }); row.push(TableCell::TableData { content: format!("{:.2}", costo.get_total()), rowspan: None, colspan: None }); } else if i2 == task.get_max_row() { row.push(TableCell::EmptyCell {rowspan: None, colspan: Some(3)}); row.push(TableCell::TableData { content: format!("{:.2}", $total), rowspan: None, colspan: None }); } else { for _ in 0..4 { row.push(TableCell::EmptyCell {rowspan: None, colspan: None}); } } }} } add_section!(materiales_iter, task.get_total_materiales()); add_section!(mano_obra_iter, task.get_total_mano_obra()); add_section!(costos_indirectos_iter, task.get_total_costo_indirectos()); if i2 == 0 { row.push(TableCell::TableData { content: format!("{:.2}", costo_total_tarea), rowspan: Some(task_rows), colspan: None }); } rows.push(row); } rows.into_iter() }).collect::>() } } struct CostoActividad(Vec); impl CostoActividad { fn add_costo_indirecto_from_builders(&mut self, builders: &[Rc Costo>]) { for actividad in &mut self.0 { actividad.add_costo_indirecto_from_builders(builders); } } fn get_presupuesto(&self) -> Option { self.0.iter().map(|act| act.get_presupuesto()).fold(None, |p1, p2| { match (p1, p2) { (Some(p1), Some(p2)) => Some(p1.merge(p2)), (Some(p1), None) => Some(p1), (None, Some(p2)) => Some(p2), _ => None } }) } fn as_html_table(&self) -> Table { let mut dur_total = 0; let mut total_materiales = 0.0; let mut total_mano_obra = 0.0; let mut total_costos_indirectos = 0.0; let mut rows = vec![ vec![ TableCell::EmptyCell {rowspan: Some(2), colspan: Some(2)}, TableCell::TableHeader {content: "Duración (días)".to_string(), rowspan: Some(2), colspan: None}, TableCell::TableHeader {content: "Materiales directos".to_string(), rowspan: None, colspan: Some(4)}, TableCell::TableHeader {content: "Mano de obra directa".to_string(), rowspan: None, colspan: Some(4)}, TableCell::TableHeader {content: "Costos indirectos".to_string(), rowspan: None, colspan: Some(4)}, TableCell::TableHeader {content: "Costo total por actividad".to_string(), rowspan: Some(2), colspan: None} ], vec![ TableCell::TableHeader {content: "Cantidad".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Producto".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Costo Unitario".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Total".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Cantidad".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Producto".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Costo Unitario".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Total".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Cantidad".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Producto".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Costo Unitario".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Total".to_string(), rowspan: None, colspan: None}, ] ]; for actividad in &self.0 { dur_total += actividad.get_dur_total(); total_materiales += actividad.get_total_materiales(); total_mano_obra += actividad.get_total_mano_obra(); total_costos_indirectos += actividad.get_total_costo_indirectos(); rows = rows.into_iter().chain(actividad.get_rows().into_iter()).collect::>(); } let total_neto = total_materiales + total_mano_obra + total_costos_indirectos; let table_body = rows.into_iter().chain(vec![vec![ TableCell::EmptyCell {rowspan: None, colspan: Some(2)}, TableCell::TableData {content: dur_total.to_string(), rowspan: None, colspan: None}, TableCell::TableData {content: "Total de Materiales".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_materiales), rowspan: None, colspan: None}, TableCell::TableData {content: "Total de Mano de Obra directa".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_mano_obra), rowspan: None, colspan: None}, TableCell::TableData {content: "Total de Costos Indirectos".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_costos_indirectos), rowspan: None, colspan: None}, TableCell::TableData {content: format!("{:.2}", total_neto), rowspan: None, colspan: None} ]].into_iter()).collect::>(); Table(table_body) } } struct Presupuesto(HashMap>); impl Presupuesto { fn material() -> u8 { 1 } fn mano_obra() -> u8 { 2 } fn costo_indirecto() -> u8 { 3 } fn new() -> Presupuesto { Presupuesto(HashMap::new()) } fn add_costo(&mut self, section: u8, costo: &Costo) { if self.0.contains_key(§ion) { let inner_map = self.0.get_mut(§ion).unwrap(); if inner_map.contains_key(&costo.producto) { let &mut (ref mut cantidad, _, ref mut total) = inner_map.get_mut(&costo.producto).unwrap(); *cantidad = cmp::max(*cantidad, costo.cantidad); *total += costo.get_total(); } else { inner_map.insert(costo.producto.clone(), (costo.cantidad, costo.precio_unitario, costo.get_total())); } } else { let mut new_map = HashMap::new(); new_map.insert(costo.producto.clone(), (costo.cantidad, costo.precio_unitario, costo.get_total())); self.0.insert(section, new_map); } } fn merge(mut self, other: Presupuesto) -> Presupuesto { for (key, other_inner_map) in other.0.iter() { if self.0.contains_key(key) { let self_inner_map = self.0.get_mut(key).unwrap(); for (product, &(c1, precio_unitario, t1)) in other_inner_map.iter() { if self_inner_map.contains_key(product) { let &mut (ref mut c2, _, ref mut t2) = self_inner_map.get_mut(product).unwrap(); *c2 = cmp::max(*c2, c1); *t2 += t1; } else { self_inner_map.insert(product.clone(), (c1, precio_unitario, t1)); } } } else { self.0.insert(*key, other_inner_map.clone()); } } self } fn as_html_table(&self) -> Table { let mut total_materiales = 0.0; let mut total_mano_obra = 0.0; let mut total_costos_indirectos = 0.0; let mut rows = vec![vec![ TableCell::TableHeader {content: "Cantidad".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Productos".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Costo Unitario".to_string(), rowspan: None, colspan: None}, TableCell::TableHeader {content: "Total".to_string(), rowspan: None, colspan: None}, ]]; macro_rules! add_table_section_rows { ($key:expr, $counter:ident) => {{ if let Some(map) = self.0.get(&$key) { $counter += map .iter() .map(|(_, &(_, _, total))| total) .sum(); rows = rows.into_iter().chain(map .iter() .map(|(producto, &(cantidad, precio_unitario, total))| { vec![ TableCell::TableData {content: cantidad.to_string(), rowspan: None, colspan: None}, TableCell::TableData {content: producto.clone(), rowspan: None, colspan: None}, TableCell::TableData {content: format!("{:.2}", precio_unitario), rowspan: None, colspan: None}, TableCell::TableData {content: format!("{:.2}", total), rowspan: None, colspan: None} ] } )).collect::>(); } }} } add_table_section_rows!(Presupuesto::material(), total_materiales); rows.push(vec![ TableCell::TableData {content: "Total de Materiales".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_materiales), rowspan: None, colspan: None}, ]); add_table_section_rows!(Presupuesto::mano_obra(), total_mano_obra); rows.push(vec![ TableCell::TableData {content: "Total de Mano de Obra".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_mano_obra), rowspan: None, colspan: None}, ]); add_table_section_rows!(Presupuesto::costo_indirecto(), total_costos_indirectos); rows.push(vec![ TableCell::TableData {content: "Total de Costos Indirectos".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_costos_indirectos), rowspan: None, colspan: None}, ]); let total_costos = total_materiales + total_mano_obra + total_costos_indirectos; rows.push(vec![ TableCell::TableData {content: "Total de Costos".to_string(), rowspan: None, colspan: Some(3)}, TableCell::TableData {content: format!("{:.2}", total_costos), rowspan: None, colspan: None}, ]); Table(rows) } } fn create_builder_mano_obra(puesto: &'static str, cantidad: u32, salario_base: f64) -> Rc Costo> { Rc::new(move |dur, _, _, _| Costo { cantidad: cantidad, producto: puesto.to_string(), precio_unitario: salario_base / 30.0 * dur as f64 }) } fn main() { const TASA_CAMBIO_DOLAR: f64 = 28.66; //Materiales let m1 = Costo { producto: "LibreOffice".to_string(), cantidad: 4, precio_unitario: 0.0 }; let m2 = Costo { producto: "Arch Linux".to_string(), cantidad: 4, precio_unitario: 0.0 }; let m3 = Costo { producto: "StarUML 2".to_string(), cantidad: 4, precio_unitario: 0.0 }; let m4 = Costo { producto: "Android Studio".to_string(), cantidad: 4, precio_unitario: 0.0 }; let m5 = Costo { producto: "Visual Studio Code".to_string(), cantidad: 4, precio_unitario: 0.0 }; let m6 = Costo { producto: "Phoenix Framework".to_string(), cantidad: 8, precio_unitario: 0.0 }; let m7 = Costo { producto: "Licencia de Google Play Store".to_string(), cantidad: 1, precio_unitario: 25.0 * TASA_CAMBIO_DOLAR }; let m8 = Costo { producto: "Licencia de la App Store".to_string(), cantidad: 1, precio_unitario: 99.0 * TASA_CAMBIO_DOLAR }; let m9 = Costo { producto: "Paquete de instancias de AWS EC2 Linux t2.medium + Disco Duros".to_string(), cantidad: 1, precio_unitario: 153.13 * TASA_CAMBIO_DOLAR }; let m10 = Costo { producto: "Paquete de instancias de AWS RDS t2.medium".to_string(), cantidad: 1, precio_unitario: 100.72 * TASA_CAMBIO_DOLAR }; //Mano de obra let analista = create_builder_mano_obra("Analista de Sistemas", 1, 12500.0); let programador_1 = create_builder_mano_obra("Programador", 1, 10000.0); let programador_2 = create_builder_mano_obra("Programador", 2, 10000.0); let dba = create_builder_mano_obra("Administrador de Base de Datos", 1, 10000.0); //Costos indirectos let costo_papel = Costo { producto: "Papelería".to_string(), cantidad: 1, precio_unitario: 50.0 }; let costo_transporte = Rc::new(move |dur, _, _, _| Costo { cantidad: dur, producto: "Transporte de mano de obra".to_string(), precio_unitario: 150.0 }); let costo_electricidad = Rc::new(move |dur, _, _, _| Costo { cantidad: 1, producto: "Luz eléctrica".to_string(), precio_unitario: 2.4 * 8.0 * dur as f64 }); let costo_internet_builder = |cantidad| Rc::new(move |_, _, _, _| Costo { cantidad: cantidad, producto: "Internet".to_string(), precio_unitario: 550.0 }); let costo_dep_pc = |cantidad| Rc::new(move |dur, _, _, _| Costo { cantidad: cantidad, producto: "Depreciación de computadoras".to_string(), precio_unitario: [22.91, 58.33, 31.25, 14.5] .iter() .map(|pd| pd * TASA_CAMBIO_DOLAR) .sum::() / 30.0 * dur as f64 / 4.0 /*4 pcs*/ }); let costo_impuestos_prof = Rc::new(move |_, _, mano_obra, _| Costo { cantidad: 1, producto: "Impuestos por servicios profesionales".to_string(), precio_unitario: mano_obra * 0.1 }); //Actividad 1 let mut t1_1 = Tarea::new("Inpección de la infraestructuara del MTI".to_string(), 3); let mut t1_2 = Tarea::new("Recolección de requerimientos del negocio".to_string(), 10); let mut t1_3 = Tarea::new("Elaboración de presupuesto y contrato".to_string(), 10); let mut t1_4 = Tarea::new("Elaboración del concepto del sistema".to_string(), 5); let mut t1_5 = Tarea::new("Elaboración del plan de gestión de riesgos".to_string(), 2); t1_1.add_costo_indirecto(costo_papel.clone()); t1_1.add_materiales(m1); t1_1.add_materiales(m2); t1_1.add_costo_indirecto_from_builders(&[costo_transporte.clone()]); t1_2.add_costo_indirecto(costo_papel.clone()); t1_2.add_costo_indirecto_from_builders(&[costo_transporte.clone()]); t1_3.add_costo_indirecto(costo_papel.clone()); t1_3.add_costo_indirecto_from_builders(&[costo_electricidad.clone()]); t1_4.add_costo_indirecto_from_builders(&[costo_electricidad.clone()]); t1_5.add_costo_indirecto_from_builders(&[costo_electricidad.clone()]); let mut t1 = Actividad { nombre: "Actividad 1: Concepción".to_string(), tareas: vec![t1_1, t1_2, t1_3, t1_4, t1_5] }; t1.add_costo_indirecto_from_builders(&[costo_dep_pc(1), costo_internet_builder(1)]); t1.add_mano_obra_from_builders(&[analista.clone()]); //Actividad 2 let mut t2_1 = Tarea::new("Análisis de requerimientos del sistema".to_string(), 5); let t2_2 = Tarea::new("Elaboración de casos de uso".to_string(), 3); let t2_3 = Tarea::new("Diseño de componentes a nivel arquitectónico".to_string(), 7); let t2_4 = Tarea::new("Elaboración del plan de pruebas".to_string(), 2); let t2_5 = Tarea::new("Especificación del sistema".to_string(), 13); let t2_6 = Tarea::new("Iteración II".to_string(), 15); t2_1.add_materiales(m3); let mut t2 = Actividad { nombre: "Actividad 2: Elaboración".to_string(), tareas: vec![t2_1, t2_2, t2_3, t2_4, t2_5, t2_6] }; t2.add_costo_indirecto_from_builders(&[ costo_electricidad.clone(), costo_dep_pc(2), costo_internet_builder(2) ]); t2.add_mano_obra_from_builders(&[analista.clone(), programador_1.clone()]); //Actividad 3 let mut t3_1 = Tarea::new("Construcción de componentes individuales".to_string(), 30); let mut t3_2 = Tarea::new("Integración de componentes".to_string(), 15); let mut t3_3 = Tarea::new("Ejecución del plan de pruebas".to_string(), 45); let mut t3_4 = Tarea::new("Iteración II".to_string(), 25); let mut t3_5 = Tarea::new("Iteración III".to_string(), 20); t3_1.add_materiales(m4); t3_1.add_materiales(m5); t3_1.add_materiales(m6); t3_1.add_materiales(m7); t3_1.add_materiales(m8); t3_1.add_materiales(m9); t3_1.add_materiales(m10); t3_1.add_mano_obra_from_builders(&[programador_2.clone()]); t3_1.add_costo_indirecto_from_builders(&[costo_internet_builder(2), costo_dep_pc(2)]); t3_2.add_mano_obra_from_builders(&[programador_2.clone()]); t3_2.add_costo_indirecto_from_builders(&[costo_internet_builder(2), costo_dep_pc(2)]); t3_3.add_mano_obra_from_builders(&[analista.clone(), dba.clone()]); t3_3.add_costo_indirecto_from_builders(&[costo_internet_builder(2), costo_dep_pc(2)]); t3_4.add_mano_obra_from_builders(&[ programador_2.clone(), analista.clone(), dba.clone() ]); t3_4.add_costo_indirecto_from_builders(&[costo_internet_builder(4), costo_dep_pc(4)]); t3_5.add_mano_obra_from_builders(&[ programador_2.clone(), analista.clone(), dba.clone() ]); t3_5.add_costo_indirecto_from_builders(&[costo_internet_builder(4), costo_dep_pc(4)]); let mut t3 = Actividad { nombre: "Actividad 3: Construcción".to_string(), tareas: vec![t3_1, t3_2, t3_3, t3_4, t3_5] }; t3.add_costo_indirecto_from_builders(&[costo_electricidad.clone()]); //Actividad 4 let t4_1 = Tarea::new("Migración de componentes al entorno de producción".to_string(), 7); let t4_2 = Tarea::new("Verificación de funcionalidad en producción".to_string(), 15); let t4_3 = Tarea::new("Evaluar retroalimentación del usuario".to_string(), 8); let t4_4 = Tarea::new("Iteración II".to_string(), 20); let mut t4 = Actividad { nombre: "Actividad 4: Ejecución".to_string(), tareas: vec![t4_1, t4_2, t4_3, t4_4] }; t4.add_costo_indirecto_from_builders(&[ costo_electricidad.clone(), costo_dep_pc(2), costo_internet_builder(2) ]); t4.add_mano_obra_from_builders(&[analista, dba]); let mut costo_actividad = CostoActividad(vec![t1, t2, t3, t4]); costo_actividad.add_costo_indirecto_from_builders(&[costo_impuestos_prof]); let html = Html(vec![ costo_actividad.as_html_table(), costo_actividad.get_presupuesto().unwrap().as_html_table() ]); html.save_to("output.html").unwrap(); }