1use std::io::{self, Write};
2
3use crate::models::Comprobante;
4
5#[derive(Debug, Clone, Copy)]
6pub enum OutputFormat {
7 Json,
8 Csv,
9 Table,
10}
11
12impl std::str::FromStr for OutputFormat {
13 type Err = String;
14
15 fn from_str(s: &str) -> Result<Self, Self::Err> {
16 match s.to_lowercase().as_str() {
17 "json" => Ok(OutputFormat::Json),
18 "csv" => Ok(OutputFormat::Csv),
19 "table" => Ok(OutputFormat::Table),
20 other => Err(format!("Unknown format: {other}. Use json, csv, or table")),
21 }
22 }
23}
24
25pub fn format_json(cfdi: &Comprobante, writer: &mut dyn Write) -> io::Result<()> {
26 let json = serde_json::to_string_pretty(cfdi)
27 .map_err(io::Error::other)?;
28 writeln!(writer, "{json}")
29}
30
31pub fn format_csv_header(writer: &mut csv::Writer<impl Write>) -> Result<(), csv::Error> {
32 writer.write_record([
33 "UUID",
34 "Fecha",
35 "Tipo",
36 "Serie",
37 "Folio",
38 "EmisorRFC",
39 "EmisorNombre",
40 "RegimenFiscal",
41 "ReceptorRFC",
42 "ReceptorNombre",
43 "UsoCFDI",
44 "SubTotal",
45 "Descuento",
46 "Total",
47 "Moneda",
48 "TipoCambio",
49 "FormaPago",
50 "MetodoPago",
51 "IVATrasladado",
52 "IVARetenido",
53 "ISRRetenido",
54 "LugarExpedicion",
55 ])
56}
57
58pub fn format_csv_row(
59 cfdi: &Comprobante,
60 writer: &mut csv::Writer<impl Write>,
61) -> Result<(), csv::Error> {
62 let uuid = cfdi.uuid().unwrap_or("").to_string();
63 let iva_trasladado = get_impuesto_trasladado(cfdi, "002");
64 let iva_retenido = get_impuesto_retenido(cfdi, "002");
65 let isr_retenido = get_impuesto_retenido(cfdi, "001");
66
67 writer.write_record([
68 &uuid,
69 &cfdi.fecha,
70 &cfdi.tipo_de_comprobante,
71 cfdi.serie.as_deref().unwrap_or(""),
72 cfdi.folio.as_deref().unwrap_or(""),
73 &cfdi.emisor.rfc,
74 cfdi.emisor.nombre.as_deref().unwrap_or(""),
75 &cfdi.emisor.regimen_fiscal,
76 &cfdi.receptor.rfc,
77 cfdi.receptor.nombre.as_deref().unwrap_or(""),
78 &cfdi.receptor.uso_cfdi,
79 &cfdi.sub_total,
80 cfdi.descuento.as_deref().unwrap_or("0"),
81 &cfdi.total,
82 &cfdi.moneda,
83 cfdi.tipo_cambio.as_deref().unwrap_or(""),
84 cfdi.forma_pago.as_deref().unwrap_or(""),
85 cfdi.metodo_pago.as_deref().unwrap_or(""),
86 &iva_trasladado,
87 &iva_retenido,
88 &isr_retenido,
89 &cfdi.lugar_expedicion,
90 ])
91}
92
93pub fn format_table(cfdi: &Comprobante, writer: &mut dyn Write) -> io::Result<()> {
94 use colored::Colorize;
95
96 let uuid = cfdi.uuid().unwrap_or("N/A");
97 let tipo = cfdi.tipo_comprobante_str();
98
99 writeln!(writer, "{}", "=".repeat(60).dimmed())?;
100 writeln!(writer, " {} {}", "UUID:".bold(), uuid.cyan())?;
101 writeln!(
102 writer,
103 " {} {} {} {} {} {}",
104 "Fecha:".bold(),
105 cfdi.fecha,
106 "Tipo:".bold(),
107 tipo,
108 "Moneda:".bold(),
109 cfdi.moneda
110 )?;
111 if let Some(ref serie) = cfdi.serie {
112 write!(writer, " {} {}", "Serie:".bold(), serie)?;
113 if let Some(ref folio) = cfdi.folio {
114 write!(writer, " {} {}", "Folio:".bold(), folio)?;
115 }
116 writeln!(writer)?;
117 }
118 writeln!(writer)?;
119
120 writeln!(writer, " {}", "EMISOR".bold().green())?;
121 writeln!(writer, " RFC: {}", cfdi.emisor.rfc)?;
122 if let Some(ref nombre) = cfdi.emisor.nombre {
123 writeln!(writer, " Nombre: {nombre}")?;
124 }
125 writeln!(writer, " Regimen: {}", cfdi.emisor.regimen_fiscal)?;
126 writeln!(writer)?;
127
128 writeln!(writer, " {}", "RECEPTOR".bold().blue())?;
129 writeln!(writer, " RFC: {}", cfdi.receptor.rfc)?;
130 if let Some(ref nombre) = cfdi.receptor.nombre {
131 writeln!(writer, " Nombre: {nombre}")?;
132 }
133 writeln!(writer, " UsoCFDI: {}", cfdi.receptor.uso_cfdi)?;
134 writeln!(writer)?;
135
136 writeln!(writer, " {}", "CONCEPTOS".bold().yellow())?;
137 for (i, c) in cfdi.conceptos.iter().enumerate() {
138 writeln!(
139 writer,
140 " {}. {} (x{}) ${} = ${}",
141 i + 1,
142 c.descripcion,
143 c.cantidad,
144 c.valor_unitario,
145 c.importe
146 )?;
147 }
148 writeln!(writer)?;
149
150 writeln!(writer, " {}", "TOTALES".bold().magenta())?;
151 writeln!(writer, " SubTotal: ${}", cfdi.sub_total)?;
152 if let Some(ref desc) = cfdi.descuento {
153 writeln!(writer, " Descuento: ${desc}")?;
154 }
155 if let Some(ref imp) = cfdi.impuestos {
156 if let Some(ref t) = imp.total_impuestos_trasladados {
157 writeln!(writer, " IVA Trasladado: ${t}")?;
158 }
159 if let Some(ref t) = imp.total_impuestos_retenidos {
160 writeln!(writer, " Impuestos Retenidos: ${t}")?;
161 }
162 }
163 writeln!(writer, " {} ${}", "Total:".bold(), cfdi.total.bold())?;
164 writeln!(writer, "{}", "=".repeat(60).dimmed())?;
165
166 Ok(())
167}
168
169fn get_impuesto_trasladado(cfdi: &Comprobante, codigo: &str) -> String {
170 cfdi.impuestos
171 .as_ref()
172 .map(|imp| {
173 imp.traslados
174 .iter()
175 .filter(|t| t.impuesto == codigo)
176 .filter_map(|t| t.importe.as_deref())
177 .next()
178 .unwrap_or("0")
179 .to_string()
180 })
181 .unwrap_or_else(|| "0".to_string())
182}
183
184fn get_impuesto_retenido(cfdi: &Comprobante, codigo: &str) -> String {
185 cfdi.impuestos
186 .as_ref()
187 .map(|imp| {
188 imp.retenciones
189 .iter()
190 .filter(|r| r.impuesto == codigo)
191 .map(|r| r.importe.as_str())
192 .next()
193 .unwrap_or("0")
194 .to_string()
195 })
196 .unwrap_or_else(|| "0".to_string())
197}
198
199#[cfg(test)]
200mod tests {
201 use super::*;
202 use crate::parser::parse_str;
203
204 fn sample_cfdi_xml() -> &'static str {
205 r#"<?xml version="1.0" encoding="utf-8"?>
206<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/4" xmlns:tfd="http://www.sat.gob.mx/TimbreFiscalDigital" Version="4.0" Fecha="2024-01-15T10:30:00" Sello="abc" NoCertificado="001" Certificado="CERT" SubTotal="1000.00" Total="1160.00" TipoDeComprobante="I" Moneda="MXN" LugarExpedicion="44100">
207 <cfdi:Emisor Rfc="AAA010101AAA" Nombre="Test" RegimenFiscal="601"/>
208 <cfdi:Receptor Rfc="BBB020202BBB" Nombre="Cliente" UsoCFDI="G03"/>
209 <cfdi:Conceptos>
210 <cfdi:Concepto ClaveProdServ="84111506" Cantidad="1" ClaveUnidad="E48" Descripcion="Servicio" ValorUnitario="1000.00" Importe="1000.00" ObjetoImp="02"/>
211 </cfdi:Conceptos>
212</cfdi:Comprobante>"#
213 }
214
215 #[test]
216 fn test_format_json() {
217 let cfdi = parse_str(sample_cfdi_xml()).unwrap();
218 let mut buf = Vec::new();
219 format_json(&cfdi, &mut buf).unwrap();
220 let output = String::from_utf8(buf).unwrap();
221 assert!(output.contains("AAA010101AAA"));
222 assert!(output.contains("1160.00"));
223 }
224
225 #[test]
226 fn test_format_csv() {
227 let cfdi = parse_str(sample_cfdi_xml()).unwrap();
228 let mut wtr = csv::Writer::from_writer(Vec::new());
229 format_csv_header(&mut wtr).unwrap();
230 format_csv_row(&cfdi, &mut wtr).unwrap();
231 let data = String::from_utf8(wtr.into_inner().unwrap()).unwrap();
232 assert!(data.contains("AAA010101AAA"));
233 assert!(data.contains("1160.00"));
234 }
235}