Skip to main content

nfe_web/pdf/
danfe.rs

1//! Gerador de DANFE profissional
2//!
3//! Layout moderno e elegante para NF-e usando printpdf
4
5use printpdf::*;
6use serde::{Deserialize, Serialize};
7use std::io::BufWriter;
8
9/// Dados para geração do DANFE
10#[derive(Debug, Clone, Serialize, Deserialize)]
11pub struct DanfeInput {
12    pub chave_acesso: String,
13    pub numero: u32,
14    pub serie: u16,
15    pub data_emissao: String,
16    pub natureza_operacao: String,
17    pub protocolo: Option<String>,
18    pub data_autorizacao: Option<String>,
19    pub emitente: DanfeEmitente,
20    pub destinatario: Option<DanfeDestinatario>,
21    pub itens: Vec<DanfeItem>,
22    pub totais: DanfeTotais,
23    pub transporte: Option<DanfeTransporte>,
24    pub informacoes_complementares: Option<String>,
25}
26
27#[derive(Debug, Clone, Serialize, Deserialize)]
28pub struct DanfeEmitente {
29    pub cnpj: String,
30    pub razao_social: String,
31    pub nome_fantasia: Option<String>,
32    pub inscricao_estadual: Option<String>,
33    pub endereco: String,
34    pub municipio: String,
35    pub uf: String,
36    pub cep: String,
37    pub telefone: Option<String>,
38}
39
40#[derive(Debug, Clone, Serialize, Deserialize)]
41pub struct DanfeDestinatario {
42    pub cnpj_cpf: String,
43    pub razao_social: String,
44    pub inscricao_estadual: Option<String>,
45    pub endereco: String,
46    pub municipio: String,
47    pub uf: String,
48    pub cep: String,
49}
50
51#[derive(Debug, Clone, Serialize, Deserialize)]
52pub struct DanfeItem {
53    pub numero: u32,
54    pub codigo: String,
55    pub descricao: String,
56    pub ncm: String,
57    pub cfop: String,
58    pub unidade: String,
59    pub quantidade: f64,
60    pub valor_unitario: f64,
61    pub valor_total: f64,
62}
63
64#[derive(Debug, Clone, Serialize, Deserialize)]
65pub struct DanfeTotais {
66    pub base_calculo_icms: f64,
67    pub valor_icms: f64,
68    pub base_calculo_st: f64,
69    pub valor_st: f64,
70    pub valor_produtos: f64,
71    pub valor_frete: f64,
72    pub valor_seguro: f64,
73    pub valor_desconto: f64,
74    pub valor_ipi: f64,
75    pub valor_total: f64,
76}
77
78#[derive(Debug, Clone, Serialize, Deserialize)]
79pub struct DanfeTransporte {
80    pub modalidade: String,
81    pub transportadora: Option<String>,
82    pub placa: Option<String>,
83    pub uf: Option<String>,
84}
85
86/// Gera DANFE em PDF
87pub fn gerar_danfe(input: &DanfeInput) -> Result<Vec<u8>, String> {
88    // Criar documento A4 (210 x 297 mm)
89    let (doc, page1, layer1) = PdfDocument::new(
90        "DANFE - Documento Auxiliar da Nota Fiscal Eletrônica",
91        Mm(210.0),
92        Mm(297.0),
93        "Página 1",
94    );
95
96    let current_layer = doc.get_page(page1).get_layer(layer1);
97
98    // Usar fonte built-in
99    let font = doc.add_builtin_font(BuiltinFont::Helvetica)
100        .map_err(|e| format!("Erro ao carregar fonte: {:?}", e))?;
101    let font_bold = doc.add_builtin_font(BuiltinFont::HelveticaBold)
102        .map_err(|e| format!("Erro ao carregar fonte bold: {:?}", e))?;
103
104    let mut y: f32 = 280.0; // Começar do topo
105
106    // === CABEÇALHO ===
107    y = draw_header(&current_layer, &font, &font_bold, input, y)?;
108
109    // === EMITENTE ===
110    y = draw_emitente(&current_layer, &font, &font_bold, &input.emitente, y)?;
111
112    // === DESTINATÁRIO ===
113    if let Some(ref dest) = input.destinatario {
114        y = draw_destinatario(&current_layer, &font, &font_bold, dest, y)?;
115    }
116
117    // === ITENS ===
118    y = draw_itens(&current_layer, &font, &font_bold, &input.itens, y)?;
119
120    // === TOTAIS ===
121    y = draw_totais(&current_layer, &font, &font_bold, &input.totais, y)?;
122
123    // === INFORMAÇÕES COMPLEMENTARES ===
124    if let Some(ref info) = input.informacoes_complementares {
125        let _ = draw_info_complementares(&current_layer, &font, &font_bold, info, y)?;
126    }
127
128    // === RODAPÉ ===
129    draw_footer(&current_layer, &font, input)?;
130
131    // Salvar PDF em buffer
132    let mut buffer = Vec::new();
133    {
134        let mut writer = BufWriter::new(&mut buffer);
135        doc.save(&mut writer)
136            .map_err(|e| format!("Erro ao salvar PDF: {:?}", e))?;
137    }
138
139    Ok(buffer)
140}
141
142fn draw_header(
143    layer: &PdfLayerReference,
144    font: &IndirectFontRef,
145    font_bold: &IndirectFontRef,
146    input: &DanfeInput,
147    mut y: f32,
148) -> Result<f32, String> {
149    // Título
150    layer.use_text("DANFE", 18.0, Mm(90.0), Mm(y), font_bold);
151    y -= 6.0;
152
153    layer.use_text(
154        "Documento Auxiliar da Nota Fiscal Eletrônica",
155        8.0,
156        Mm(65.0),
157        Mm(y),
158        font,
159    );
160    y -= 8.0;
161
162    // Box com informações da NF-e
163    draw_box(layer, 10.0, y - 30.0, 190.0, 35.0);
164
165    // NF-e número, série
166    let nfe_info = format!("NF-e Nº {:09} | Série {:03}", input.numero, input.serie);
167    layer.use_text(&nfe_info, 12.0, Mm(15.0), Mm(y - 5.0), font_bold);
168    y -= 10.0;
169
170    // Data emissão
171    layer.use_text(
172        &format!("Emissão: {}", input.data_emissao),
173        10.0,
174        Mm(15.0),
175        Mm(y - 5.0),
176        font,
177    );
178
179    // Natureza da operação
180    layer.use_text(
181        &format!("Natureza: {}", input.natureza_operacao),
182        10.0,
183        Mm(100.0),
184        Mm(y - 5.0),
185        font,
186    );
187    y -= 10.0;
188
189    // Chave de acesso
190    layer.use_text("CHAVE DE ACESSO", 8.0, Mm(15.0), Mm(y - 5.0), font_bold);
191    y -= 5.0;
192    layer.use_text(&format_chave(&input.chave_acesso), 9.0, Mm(15.0), Mm(y - 5.0), font);
193    y -= 10.0;
194
195    // Protocolo
196    if let Some(ref prot) = input.protocolo {
197        layer.use_text(
198            &format!(
199                "Protocolo: {} - {}",
200                prot,
201                input.data_autorizacao.as_deref().unwrap_or("")
202            ),
203            8.0,
204            Mm(15.0),
205            Mm(y - 5.0),
206            font,
207        );
208    }
209    y -= 10.0;
210
211    Ok(y)
212}
213
214fn draw_emitente(
215    layer: &PdfLayerReference,
216    font: &IndirectFontRef,
217    font_bold: &IndirectFontRef,
218    emit: &DanfeEmitente,
219    mut y: f32,
220) -> Result<f32, String> {
221    // Título da seção
222    draw_section_title(layer, font_bold, "EMITENTE", y);
223    y -= 5.0;
224
225    // Box
226    draw_box(layer, 10.0, y - 30.0, 190.0, 35.0);
227
228    // Razão Social
229    layer.use_text(&emit.razao_social, 10.0, Mm(15.0), Mm(y - 5.0), font_bold);
230
231    // CNPJ
232    layer.use_text(
233        &format!("CNPJ: {}", format_cnpj(&emit.cnpj)),
234        9.0,
235        Mm(140.0),
236        Mm(y - 5.0),
237        font,
238    );
239    y -= 8.0;
240
241    // Endereço
242    layer.use_text(&emit.endereco, 9.0, Mm(15.0), Mm(y - 5.0), font);
243    y -= 6.0;
244
245    // Cidade/UF/CEP
246    layer.use_text(
247        &format!("{} - {} - CEP: {}", emit.municipio, emit.uf, emit.cep),
248        9.0,
249        Mm(15.0),
250        Mm(y - 5.0),
251        font,
252    );
253
254    // IE
255    layer.use_text(
256        &format!("IE: {}", emit.inscricao_estadual.as_deref().unwrap_or("-")),
257        9.0,
258        Mm(140.0),
259        Mm(y - 5.0),
260        font,
261    );
262    y -= 6.0;
263
264    // Telefone
265    if let Some(ref tel) = emit.telefone {
266        layer.use_text(&format!("Fone: {}", tel), 9.0, Mm(15.0), Mm(y - 5.0), font);
267    }
268    y -= 10.0;
269
270    Ok(y)
271}
272
273fn draw_destinatario(
274    layer: &PdfLayerReference,
275    font: &IndirectFontRef,
276    font_bold: &IndirectFontRef,
277    dest: &DanfeDestinatario,
278    mut y: f32,
279) -> Result<f32, String> {
280    draw_section_title(layer, font_bold, "DESTINATÁRIO / REMETENTE", y);
281    y -= 5.0;
282
283    draw_box(layer, 10.0, y - 25.0, 190.0, 30.0);
284
285    // Razão Social
286    layer.use_text(&dest.razao_social, 10.0, Mm(15.0), Mm(y - 5.0), font_bold);
287
288    // CNPJ/CPF
289    layer.use_text(
290        &format!("CNPJ/CPF: {}", format_cnpj_cpf(&dest.cnpj_cpf)),
291        9.0,
292        Mm(140.0),
293        Mm(y - 5.0),
294        font,
295    );
296    y -= 8.0;
297
298    // Endereço
299    layer.use_text(&dest.endereco, 9.0, Mm(15.0), Mm(y - 5.0), font);
300    y -= 6.0;
301
302    // Cidade/UF/CEP
303    layer.use_text(
304        &format!("{} - {} - CEP: {}", dest.municipio, dest.uf, dest.cep),
305        9.0,
306        Mm(15.0),
307        Mm(y - 5.0),
308        font,
309    );
310
311    // IE
312    layer.use_text(
313        &format!("IE: {}", dest.inscricao_estadual.as_deref().unwrap_or("-")),
314        9.0,
315        Mm(140.0),
316        Mm(y - 5.0),
317        font,
318    );
319    y -= 15.0;
320
321    Ok(y)
322}
323
324fn draw_itens(
325    layer: &PdfLayerReference,
326    font: &IndirectFontRef,
327    font_bold: &IndirectFontRef,
328    itens: &[DanfeItem],
329    mut y: f32,
330) -> Result<f32, String> {
331    draw_section_title(layer, font_bold, "PRODUTOS / SERVIÇOS", y);
332    y -= 5.0;
333
334    // Cabeçalho da tabela
335    let headers = ["Cód.", "Descrição", "NCM", "CFOP", "Un", "Qtd", "V.Unit", "V.Total"];
336    let x_positions: [f32; 8] = [15.0, 35.0, 100.0, 125.0, 145.0, 160.0, 175.0, 190.0];
337
338    for (i, header) in headers.iter().enumerate() {
339        layer.use_text(*header, 7.0, Mm(x_positions[i]), Mm(y - 3.0), font_bold);
340    }
341    y -= 5.0;
342
343    // Linha separadora
344    draw_line(layer, 10.0, y, 200.0, y);
345    y -= 3.0;
346
347    // Itens (limitar a 20 por página)
348    for item in itens.iter().take(20) {
349        layer.use_text(&item.codigo, 7.0, Mm(15.0), Mm(y), font);
350
351        // Descrição truncada
352        let desc = if item.descricao.len() > 40 {
353            format!("{}...", &item.descricao[..37])
354        } else {
355            item.descricao.clone()
356        };
357        layer.use_text(&desc, 7.0, Mm(35.0), Mm(y), font);
358
359        layer.use_text(&item.ncm, 7.0, Mm(100.0), Mm(y), font);
360        layer.use_text(&item.cfop, 7.0, Mm(125.0), Mm(y), font);
361        layer.use_text(&item.unidade, 7.0, Mm(145.0), Mm(y), font);
362        layer.use_text(&format!("{:.2}", item.quantidade), 7.0, Mm(160.0), Mm(y), font);
363        layer.use_text(&format!("{:.2}", item.valor_unitario), 7.0, Mm(175.0), Mm(y), font);
364        layer.use_text(&format!("{:.2}", item.valor_total), 7.0, Mm(190.0), Mm(y), font);
365
366        y -= 5.0;
367    }
368
369    y -= 5.0;
370    Ok(y)
371}
372
373fn draw_totais(
374    layer: &PdfLayerReference,
375    font: &IndirectFontRef,
376    font_bold: &IndirectFontRef,
377    totais: &DanfeTotais,
378    mut y: f32,
379) -> Result<f32, String> {
380    draw_section_title(layer, font_bold, "CÁLCULO DO IMPOSTO", y);
381    y -= 5.0;
382
383    draw_box(layer, 10.0, y - 25.0, 190.0, 30.0);
384
385    // Linha 1
386    layer.use_text(
387        &format!("Base ICMS: R$ {:.2}", totais.base_calculo_icms),
388        8.0,
389        Mm(15.0),
390        Mm(y - 5.0),
391        font,
392    );
393    layer.use_text(
394        &format!("Valor ICMS: R$ {:.2}", totais.valor_icms),
395        8.0,
396        Mm(60.0),
397        Mm(y - 5.0),
398        font,
399    );
400    layer.use_text(
401        &format!("Base ST: R$ {:.2}", totais.base_calculo_st),
402        8.0,
403        Mm(110.0),
404        Mm(y - 5.0),
405        font,
406    );
407    layer.use_text(
408        &format!("Valor ST: R$ {:.2}", totais.valor_st),
409        8.0,
410        Mm(155.0),
411        Mm(y - 5.0),
412        font,
413    );
414    y -= 8.0;
415
416    // Linha 2
417    layer.use_text(
418        &format!("V. Produtos: R$ {:.2}", totais.valor_produtos),
419        8.0,
420        Mm(15.0),
421        Mm(y - 5.0),
422        font,
423    );
424    layer.use_text(
425        &format!("V. Frete: R$ {:.2}", totais.valor_frete),
426        8.0,
427        Mm(60.0),
428        Mm(y - 5.0),
429        font,
430    );
431    layer.use_text(
432        &format!("V. Desconto: R$ {:.2}", totais.valor_desconto),
433        8.0,
434        Mm(110.0),
435        Mm(y - 5.0),
436        font,
437    );
438    layer.use_text(
439        &format!("V. IPI: R$ {:.2}", totais.valor_ipi),
440        8.0,
441        Mm(155.0),
442        Mm(y - 5.0),
443        font,
444    );
445    y -= 10.0;
446
447    // Total em destaque
448    layer.use_text(
449        &format!("VALOR TOTAL DA NOTA: R$ {:.2}", totais.valor_total),
450        12.0,
451        Mm(120.0),
452        Mm(y - 5.0),
453        font_bold,
454    );
455    y -= 15.0;
456
457    Ok(y)
458}
459
460fn draw_info_complementares(
461    layer: &PdfLayerReference,
462    font: &IndirectFontRef,
463    font_bold: &IndirectFontRef,
464    info: &str,
465    mut y: f32,
466) -> Result<f32, String> {
467    draw_section_title(layer, font_bold, "INFORMAÇÕES COMPLEMENTARES", y);
468    y -= 5.0;
469
470    draw_box(layer, 10.0, y - 20.0, 190.0, 25.0);
471
472    // Quebrar texto em linhas
473    let max_chars = 100;
474    let mut current_y = y - 5.0;
475    for chunk in info.chars().collect::<Vec<_>>().chunks(max_chars) {
476        let line: String = chunk.iter().collect();
477        layer.use_text(&line, 7.0, Mm(15.0), Mm(current_y), font);
478        current_y -= 4.0;
479    }
480
481    y -= 25.0;
482    Ok(y)
483}
484
485fn draw_footer(
486    layer: &PdfLayerReference,
487    font: &IndirectFontRef,
488    input: &DanfeInput,
489) -> Result<(), String> {
490    let y: f32 = 15.0;
491
492    layer.use_text(
493        "Consulte a autenticidade em: www.nfe.fazenda.gov.br/portal",
494        8.0,
495        Mm(50.0),
496        Mm(y),
497        font,
498    );
499
500    layer.use_text(
501        &format!("Chave: {}", format_chave(&input.chave_acesso)),
502        7.0,
503        Mm(40.0),
504        Mm(y - 5.0),
505        font,
506    );
507
508    Ok(())
509}
510
511// === Funções auxiliares de desenho ===
512
513fn draw_box(layer: &PdfLayerReference, x: f32, y: f32, width: f32, height: f32) {
514    let points = vec![
515        (Point::new(Mm(x), Mm(y)), false),
516        (Point::new(Mm(x + width), Mm(y)), false),
517        (Point::new(Mm(x + width), Mm(y + height)), false),
518        (Point::new(Mm(x), Mm(y + height)), false),
519    ];
520
521    let line = Line {
522        points,
523        is_closed: true,
524    };
525
526    layer.set_outline_color(Color::Rgb(Rgb::new(0.7, 0.7, 0.7, None)));
527    layer.set_outline_thickness(0.5);
528    layer.add_line(line);
529}
530
531fn draw_line(layer: &PdfLayerReference, x1: f32, y1: f32, x2: f32, y2: f32) {
532    let points = vec![
533        (Point::new(Mm(x1), Mm(y1)), false),
534        (Point::new(Mm(x2), Mm(y2)), false),
535    ];
536
537    let line = Line {
538        points,
539        is_closed: false,
540    };
541
542    layer.set_outline_color(Color::Rgb(Rgb::new(0.8, 0.8, 0.8, None)));
543    layer.set_outline_thickness(0.3);
544    layer.add_line(line);
545}
546
547fn draw_section_title(layer: &PdfLayerReference, font: &IndirectFontRef, title: &str, y: f32) {
548    layer.set_fill_color(Color::Rgb(Rgb::new(0.2, 0.2, 0.2, None)));
549    layer.use_text(title, 9.0, Mm(10.0), Mm(y), font);
550}
551
552// === Funções de formatação ===
553
554fn format_chave(chave: &str) -> String {
555    let digits: String = chave.chars().filter(|c| c.is_ascii_digit()).collect();
556    digits
557        .chars()
558        .collect::<Vec<_>>()
559        .chunks(4)
560        .map(|c| c.iter().collect::<String>())
561        .collect::<Vec<_>>()
562        .join(" ")
563}
564
565fn format_cnpj(cnpj: &str) -> String {
566    let digits: String = cnpj.chars().filter(|c| c.is_ascii_digit()).collect();
567    if digits.len() == 14 {
568        format!(
569            "{}.{}.{}/{}-{}",
570            &digits[0..2],
571            &digits[2..5],
572            &digits[5..8],
573            &digits[8..12],
574            &digits[12..14]
575        )
576    } else {
577        cnpj.to_string()
578    }
579}
580
581fn format_cnpj_cpf(doc: &str) -> String {
582    let digits: String = doc.chars().filter(|c| c.is_ascii_digit()).collect();
583    if digits.len() == 14 {
584        format_cnpj(&digits)
585    } else if digits.len() == 11 {
586        format!(
587            "{}.{}.{}-{}",
588            &digits[0..3],
589            &digits[3..6],
590            &digits[6..9],
591            &digits[9..11]
592        )
593    } else {
594        doc.to_string()
595    }
596}