1use crate::FiscalError;
4use crate::format_utils::{format_cents, format_decimal};
5use crate::newtypes::{Cents, Rate, Rate4};
6use crate::tax_ibs_cbs;
7use crate::tax_icms::{self, IcmsCsosn, IcmsCst, IcmsTotals, IcmsVariant};
8use crate::tax_is;
9use crate::tax_issqn;
10use crate::tax_pis_cofins_ipi::{self, CofinsData, IiData, IpiData, PisData};
11use crate::types::{
12 CombData, InvoiceBuildData, InvoiceItemData, InvoiceModel, SefazEnvironment, TaxRegime,
13};
14use crate::xml_utils::{TagContent, tag};
15
16const HOMOLOGATION_XPROD: &str =
18 "NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL";
19
20#[derive(Debug, Clone)]
22pub struct DetResult {
23 pub xml: String,
25 pub icms_totals: IcmsTotals,
27 pub v_ipi: i64,
29 pub v_pis: i64,
31 pub v_cofins: i64,
33 pub v_ii: i64,
35 pub v_frete: i64,
37 pub v_seg: i64,
39 pub v_desc: i64,
41 pub v_outro: i64,
43 pub ind_tot: u8,
45 pub v_tot_trib: i64,
47 pub v_ipi_devol: i64,
49 pub has_issqn: bool,
51}
52
53fn build_icms_variant(
55 item: &InvoiceItemData,
56 is_simples: bool,
57) -> Result<IcmsVariant, FiscalError> {
58 let orig = item.orig.clone().unwrap_or_else(|| "0".to_string());
59
60 if is_simples {
61 let csosn_code = if item.icms_cst.is_empty() {
62 "102"
63 } else {
64 item.icms_cst.as_str()
65 };
66
67 let csosn = match csosn_code {
68 "101" => IcmsCsosn::Csosn101 {
69 orig,
70 csosn: csosn_code.to_string(),
71 p_cred_sn: item.icms_p_cred_sn.ok_or_else(|| {
72 FiscalError::MissingRequiredField {
73 field: "pCredSN".to_string(),
74 }
75 })?,
76 v_cred_icms_sn: item.icms_v_cred_icms_sn.ok_or_else(|| {
77 FiscalError::MissingRequiredField {
78 field: "vCredICMSSN".to_string(),
79 }
80 })?,
81 },
82 "102" | "103" | "300" | "400" => IcmsCsosn::Csosn102 {
83 orig,
84 csosn: csosn_code.to_string(),
85 },
86 "201" => IcmsCsosn::Csosn201 {
87 orig,
88 csosn: csosn_code.to_string(),
89 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
90 FiscalError::MissingRequiredField {
91 field: "modBCST".to_string(),
92 }
93 })?,
94 p_mva_st: item.icms_p_mva_st,
95 p_red_bc_st: item.icms_red_bc_st,
96 v_bc_st: item
97 .icms_v_bc_st
98 .ok_or_else(|| FiscalError::MissingRequiredField {
99 field: "vBCST".to_string(),
100 })?,
101 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
102 FiscalError::MissingRequiredField {
103 field: "pICMSST".to_string(),
104 }
105 })?,
106 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
107 FiscalError::MissingRequiredField {
108 field: "vICMSST".to_string(),
109 }
110 })?,
111 v_bc_fcp_st: item.icms_v_bc_fcp_st,
112 p_fcp_st: item.icms_p_fcp_st,
113 v_fcp_st: item.icms_v_fcp_st,
114 p_cred_sn: item.icms_p_cred_sn,
115 v_cred_icms_sn: item.icms_v_cred_icms_sn,
116 },
117 "202" | "203" => IcmsCsosn::Csosn202 {
118 orig,
119 csosn: csosn_code.to_string(),
120 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
121 FiscalError::MissingRequiredField {
122 field: "modBCST".to_string(),
123 }
124 })?,
125 p_mva_st: item.icms_p_mva_st,
126 p_red_bc_st: item.icms_red_bc_st,
127 v_bc_st: item
128 .icms_v_bc_st
129 .ok_or_else(|| FiscalError::MissingRequiredField {
130 field: "vBCST".to_string(),
131 })?,
132 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
133 FiscalError::MissingRequiredField {
134 field: "pICMSST".to_string(),
135 }
136 })?,
137 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
138 FiscalError::MissingRequiredField {
139 field: "vICMSST".to_string(),
140 }
141 })?,
142 v_bc_fcp_st: item.icms_v_bc_fcp_st,
143 p_fcp_st: item.icms_p_fcp_st,
144 v_fcp_st: item.icms_v_fcp_st,
145 },
146 "500" => IcmsCsosn::Csosn500 {
147 orig,
148 csosn: csosn_code.to_string(),
149 v_bc_st_ret: None,
150 p_st: None,
151 v_icms_substituto: item.icms_v_icms_substituto,
152 v_icms_st_ret: None,
153 v_bc_fcp_st_ret: None,
154 p_fcp_st_ret: None,
155 v_fcp_st_ret: None,
156 p_red_bc_efet: None,
157 v_bc_efet: None,
158 p_icms_efet: None,
159 v_icms_efet: None,
160 },
161 "900" => IcmsCsosn::Csosn900 {
162 orig,
163 csosn: csosn_code.to_string(),
164 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
165 v_bc: Some(item.total_price),
166 p_red_bc: item.icms_red_bc,
167 p_icms: Some(item.icms_rate),
168 v_icms: Some(item.icms_amount),
169 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
170 p_mva_st: item.icms_p_mva_st,
171 p_red_bc_st: item.icms_red_bc_st,
172 v_bc_st: item.icms_v_bc_st,
173 p_icms_st: item.icms_p_icms_st,
174 v_icms_st: item.icms_v_icms_st,
175 v_bc_fcp_st: item.icms_v_bc_fcp_st,
176 p_fcp_st: item.icms_p_fcp_st,
177 v_fcp_st: item.icms_v_fcp_st,
178 p_cred_sn: item.icms_p_cred_sn,
179 v_cred_icms_sn: item.icms_v_cred_icms_sn,
180 },
181 other => return Err(FiscalError::UnsupportedIcmsCsosn(other.to_string())),
182 };
183 Ok(csosn.into())
184 } else {
185 let cst_code = item.icms_cst.as_str();
186 let cst = match cst_code {
187 "00" => IcmsCst::Cst00 {
188 orig,
189 mod_bc: item
190 .icms_mod_bc
191 .map(|v| v.to_string())
192 .unwrap_or_else(|| "3".to_string()),
193 v_bc: item.total_price,
194 p_icms: item.icms_rate,
195 v_icms: item.icms_amount,
196 p_fcp: item.icms_p_fcp,
197 v_fcp: item.icms_v_fcp,
198 },
199 "10" => IcmsCst::Cst10 {
200 orig,
201 mod_bc: item
202 .icms_mod_bc
203 .map(|v| v.to_string())
204 .unwrap_or_else(|| "3".to_string()),
205 v_bc: item.total_price,
206 p_icms: item.icms_rate,
207 v_icms: item.icms_amount,
208 v_bc_fcp: item.icms_v_bc_fcp,
209 p_fcp: item.icms_p_fcp,
210 v_fcp: item.icms_v_fcp,
211 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
212 FiscalError::MissingRequiredField {
213 field: "modBCST".to_string(),
214 }
215 })?,
216 p_mva_st: item.icms_p_mva_st,
217 p_red_bc_st: item.icms_red_bc_st,
218 v_bc_st: item
219 .icms_v_bc_st
220 .ok_or_else(|| FiscalError::MissingRequiredField {
221 field: "vBCST".to_string(),
222 })?,
223 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
224 FiscalError::MissingRequiredField {
225 field: "pICMSST".to_string(),
226 }
227 })?,
228 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
229 FiscalError::MissingRequiredField {
230 field: "vICMSST".to_string(),
231 }
232 })?,
233 v_bc_fcp_st: item.icms_v_bc_fcp_st,
234 p_fcp_st: item.icms_p_fcp_st,
235 v_fcp_st: item.icms_v_fcp_st,
236 v_icms_st_deson: None,
237 mot_des_icms_st: None,
238 },
239 "20" => IcmsCst::Cst20 {
240 orig,
241 mod_bc: item
242 .icms_mod_bc
243 .map(|v| v.to_string())
244 .unwrap_or_else(|| "3".to_string()),
245 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
246 v_bc: item.total_price,
247 p_icms: item.icms_rate,
248 v_icms: item.icms_amount,
249 v_bc_fcp: item.icms_v_bc_fcp,
250 p_fcp: item.icms_p_fcp,
251 v_fcp: item.icms_v_fcp,
252 v_icms_deson: item.icms_v_icms_deson,
253 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
254 ind_deduz_deson: None,
255 },
256 "30" => IcmsCst::Cst30 {
257 orig,
258 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
259 FiscalError::MissingRequiredField {
260 field: "modBCST".to_string(),
261 }
262 })?,
263 p_mva_st: item.icms_p_mva_st,
264 p_red_bc_st: item.icms_red_bc_st,
265 v_bc_st: item
266 .icms_v_bc_st
267 .ok_or_else(|| FiscalError::MissingRequiredField {
268 field: "vBCST".to_string(),
269 })?,
270 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
271 FiscalError::MissingRequiredField {
272 field: "pICMSST".to_string(),
273 }
274 })?,
275 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
276 FiscalError::MissingRequiredField {
277 field: "vICMSST".to_string(),
278 }
279 })?,
280 v_bc_fcp_st: item.icms_v_bc_fcp_st,
281 p_fcp_st: item.icms_p_fcp_st,
282 v_fcp_st: item.icms_v_fcp_st,
283 v_icms_deson: item.icms_v_icms_deson,
284 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
285 ind_deduz_deson: None,
286 },
287 "40" => IcmsCst::Cst40 {
288 orig,
289 v_icms_deson: item.icms_v_icms_deson,
290 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
291 ind_deduz_deson: None,
292 },
293 "41" => IcmsCst::Cst41 {
294 orig,
295 v_icms_deson: item.icms_v_icms_deson,
296 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
297 ind_deduz_deson: None,
298 },
299 "50" => IcmsCst::Cst50 {
300 orig,
301 v_icms_deson: item.icms_v_icms_deson,
302 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
303 ind_deduz_deson: None,
304 },
305 "51" => IcmsCst::Cst51 {
306 orig,
307 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
308 p_red_bc: item.icms_red_bc,
309 c_benef_rbc: None,
310 v_bc: Some(item.total_price),
311 p_icms: Some(item.icms_rate),
312 v_icms_op: None,
313 p_dif: None,
314 v_icms_dif: None,
315 v_icms: Some(item.icms_amount),
316 v_bc_fcp: item.icms_v_bc_fcp,
317 p_fcp: item.icms_p_fcp,
318 v_fcp: item.icms_v_fcp,
319 p_fcp_dif: None,
320 v_fcp_dif: None,
321 v_fcp_efet: None,
322 },
323 "60" => IcmsCst::Cst60 {
324 orig,
325 v_bc_st_ret: None,
326 p_st: None,
327 v_icms_substituto: item.icms_v_icms_substituto,
328 v_icms_st_ret: None,
329 v_bc_fcp_st_ret: None,
330 p_fcp_st_ret: None,
331 v_fcp_st_ret: None,
332 p_red_bc_efet: None,
333 v_bc_efet: None,
334 p_icms_efet: None,
335 v_icms_efet: None,
336 },
337 "70" => IcmsCst::Cst70 {
338 orig,
339 mod_bc: item
340 .icms_mod_bc
341 .map(|v| v.to_string())
342 .unwrap_or_else(|| "3".to_string()),
343 p_red_bc: item.icms_red_bc.unwrap_or(Rate(0)),
344 v_bc: item.total_price,
345 p_icms: item.icms_rate,
346 v_icms: item.icms_amount,
347 v_bc_fcp: item.icms_v_bc_fcp,
348 p_fcp: item.icms_p_fcp,
349 v_fcp: item.icms_v_fcp,
350 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()).ok_or_else(|| {
351 FiscalError::MissingRequiredField {
352 field: "modBCST".to_string(),
353 }
354 })?,
355 p_mva_st: item.icms_p_mva_st,
356 p_red_bc_st: item.icms_red_bc_st,
357 v_bc_st: item
358 .icms_v_bc_st
359 .ok_or_else(|| FiscalError::MissingRequiredField {
360 field: "vBCST".to_string(),
361 })?,
362 p_icms_st: item.icms_p_icms_st.ok_or_else(|| {
363 FiscalError::MissingRequiredField {
364 field: "pICMSST".to_string(),
365 }
366 })?,
367 v_icms_st: item.icms_v_icms_st.ok_or_else(|| {
368 FiscalError::MissingRequiredField {
369 field: "vICMSST".to_string(),
370 }
371 })?,
372 v_bc_fcp_st: item.icms_v_bc_fcp_st,
373 p_fcp_st: item.icms_p_fcp_st,
374 v_fcp_st: item.icms_v_fcp_st,
375 v_icms_deson: item.icms_v_icms_deson,
376 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
377 ind_deduz_deson: None,
378 v_icms_st_deson: None,
379 mot_des_icms_st: None,
380 },
381 "90" => IcmsCst::Cst90 {
382 orig,
383 mod_bc: item.icms_mod_bc.map(|v| v.to_string()),
384 v_bc: Some(item.total_price),
385 p_red_bc: item.icms_red_bc,
386 c_benef_rbc: None,
387 p_icms: Some(item.icms_rate),
388 v_icms_op: None,
389 p_dif: None,
390 v_icms_dif: None,
391 v_icms: Some(item.icms_amount),
392 v_bc_fcp: item.icms_v_bc_fcp,
393 p_fcp: item.icms_p_fcp,
394 v_fcp: item.icms_v_fcp,
395 p_fcp_dif: None,
396 v_fcp_dif: None,
397 v_fcp_efet: None,
398 mod_bc_st: item.icms_mod_bc_st.map(|v| v.to_string()),
399 p_mva_st: item.icms_p_mva_st,
400 p_red_bc_st: item.icms_red_bc_st,
401 v_bc_st: item.icms_v_bc_st,
402 p_icms_st: item.icms_p_icms_st,
403 v_icms_st: item.icms_v_icms_st,
404 v_bc_fcp_st: item.icms_v_bc_fcp_st,
405 p_fcp_st: item.icms_p_fcp_st,
406 v_fcp_st: item.icms_v_fcp_st,
407 v_icms_deson: item.icms_v_icms_deson,
408 mot_des_icms: item.icms_mot_des_icms.map(|v| v.to_string()),
409 ind_deduz_deson: None,
410 v_icms_st_deson: None,
411 mot_des_icms_st: None,
412 },
413 other => return Err(FiscalError::UnsupportedIcmsCst(other.to_string())),
414 };
415 Ok(cst.into())
416 }
417}
418
419pub(crate) fn build_det(
421 item: &InvoiceItemData,
422 data: &InvoiceBuildData,
423) -> Result<DetResult, FiscalError> {
424 let is_simples = matches!(
425 data.issuer.tax_regime,
426 TaxRegime::SimplesNacional | TaxRegime::SimplesExcess
427 );
428
429 let has_issqn = item.issqn.is_some();
430
431 let mut icms_totals = IcmsTotals::default();
433 let icms_xml = if has_issqn {
434 String::new()
435 } else {
436 let icms_variant = build_icms_variant(item, is_simples)?;
437 tax_icms::build_icms_xml(&icms_variant, &mut icms_totals)?
438 };
439
440 let issqn_xml = if let Some(ref issqn_data) = item.issqn {
442 tax_issqn::build_issqn_xml(issqn_data)
443 } else {
444 String::new()
445 };
446
447 let pis_xml = tax_pis_cofins_ipi::build_pis_xml(&PisData {
449 cst: item.pis_cst.clone(),
450 v_bc: item.pis_v_bc.or(Some(Cents(0))),
451 p_pis: item.pis_p_pis.or(Some(Rate4(0))),
452 v_pis: item.pis_v_pis.or(Some(Cents(0))),
453 q_bc_prod: item.pis_q_bc_prod,
454 v_aliq_prod: item.pis_v_aliq_prod,
455 });
456
457 let cofins_xml = tax_pis_cofins_ipi::build_cofins_xml(&CofinsData {
459 cst: item.cofins_cst.clone(),
460 v_bc: item.cofins_v_bc.or(Some(Cents(0))),
461 p_cofins: item.cofins_p_cofins.or(Some(Rate4(0))),
462 v_cofins: item.cofins_v_cofins.or(Some(Cents(0))),
463 q_bc_prod: item.cofins_q_bc_prod,
464 v_aliq_prod: item.cofins_v_aliq_prod,
465 });
466
467 let mut ipi_xml = String::new();
469 let mut v_ipi = 0i64;
470 if let Some(ref ipi_cst) = item.ipi_cst {
471 ipi_xml = tax_pis_cofins_ipi::build_ipi_xml(&IpiData {
472 cst: ipi_cst.clone(),
473 c_enq: item.ipi_c_enq.clone().unwrap_or_else(|| "999".to_string()),
474 v_bc: item.ipi_v_bc,
475 p_ipi: item.ipi_p_ipi,
476 v_ipi: item.ipi_v_ipi,
477 q_unid: item.ipi_q_unid,
478 v_unid: item.ipi_v_unid,
479 ..IpiData::default()
480 });
481 v_ipi = item.ipi_v_ipi.map(|c| c.0).unwrap_or(0);
482 }
483
484 let mut ii_xml = String::new();
486 let mut v_ii = 0i64;
487 if let Some(ii_vbc) = item.ii_v_bc {
488 ii_xml = tax_pis_cofins_ipi::build_ii_xml(&IiData {
489 v_bc: ii_vbc,
490 v_desp_adu: item.ii_v_desp_adu.unwrap_or(Cents(0)),
491 v_ii: item.ii_v_ii.unwrap_or(Cents(0)),
492 v_iof: item.ii_v_iof.unwrap_or(Cents(0)),
493 });
494 v_ii = item.ii_v_ii.map(|c| c.0).unwrap_or(0);
495 }
496
497 let prod_options = build_prod_options(item);
499
500 let det_extras = build_det_extras(item);
502
503 let mut imposto_children: Vec<String> = Vec::new();
505 if !icms_xml.is_empty() {
506 imposto_children.push(icms_xml);
507 }
508 if !ipi_xml.is_empty() {
509 imposto_children.push(ipi_xml);
510 }
511 imposto_children.push(pis_xml);
512 imposto_children.push(cofins_xml);
513 if !ii_xml.is_empty() {
514 imposto_children.push(ii_xml);
515 }
516 if !issqn_xml.is_empty() {
517 imposto_children.push(issqn_xml);
518 }
519
520 if let Some(ref is_data) = item.is_data {
522 imposto_children.push(tax_is::build_is_xml(is_data));
523 }
524
525 if let Some(ref ibs_cbs_data) = item.ibs_cbs {
527 imposto_children.push(tax_ibs_cbs::build_ibs_cbs_xml(ibs_cbs_data));
528 }
529
530 let fc2 = |c: i64| format_cents(c, 2);
532 let fc10 = |c: i64| format_cents(c, 10);
533 let fd4 = |v: f64| format_decimal(v, 4);
534
535 let mut prod_children = vec![
536 tag("cProd", &[], TagContent::Text(&item.product_code)),
537 tag(
538 "cEAN",
539 &[],
540 TagContent::Text(item.c_ean.as_deref().unwrap_or("SEM GTIN")),
541 ),
542 tag(
543 "xProd",
544 &[],
545 TagContent::Text(
546 if item.item_number == 1
548 && data.environment == SefazEnvironment::Homologation
549 && data.model == InvoiceModel::Nfce
550 {
551 HOMOLOGATION_XPROD
552 } else {
553 &item.description
554 },
555 ),
556 ),
557 tag("NCM", &[], TagContent::Text(&item.ncm)),
558 ];
559 if let Some(ref cest) = item.cest {
560 prod_children.push(tag("CEST", &[], TagContent::Text(cest)));
561 if let Some(ref ind) = item.cest_ind_escala {
562 prod_children.push(tag("indEscala", &[], TagContent::Text(ind)));
563 }
564 if let Some(ref fab) = item.cest_cnpj_fab {
565 prod_children.push(tag("CNPJFab", &[], TagContent::Text(fab)));
566 }
567 }
568 if let Some(ref cb) = item.c_benef {
569 prod_children.push(tag("cBenef", &[], TagContent::Text(cb)));
570 }
571 if let Some(ref ex) = item.extipi {
572 prod_children.push(tag("EXTIPI", &[], TagContent::Text(ex)));
573 }
574 prod_children.extend([
575 tag("CFOP", &[], TagContent::Text(&item.cfop)),
576 tag("uCom", &[], TagContent::Text(&item.unit_of_measure)),
577 tag("qCom", &[], TagContent::Text(&fd4(item.quantity))),
578 tag("vUnCom", &[], TagContent::Text(&fc10(item.unit_price.0))),
579 tag("vProd", &[], TagContent::Text(&fc2(item.total_price.0))),
580 tag(
581 "cEANTrib",
582 &[],
583 TagContent::Text(item.c_ean_trib.as_deref().unwrap_or("SEM GTIN")),
584 ),
585 tag("uTrib", &[], TagContent::Text(&item.unit_of_measure)),
586 tag("qTrib", &[], TagContent::Text(&fd4(item.quantity))),
587 tag("vUnTrib", &[], TagContent::Text(&fc10(item.unit_price.0))),
588 ]);
589 if let Some(v) = item.v_frete {
590 prod_children.push(tag("vFrete", &[], TagContent::Text(&fc2(v.0))));
591 }
592 if let Some(v) = item.v_seg {
593 prod_children.push(tag("vSeg", &[], TagContent::Text(&fc2(v.0))));
594 }
595 if let Some(v) = item.v_desc {
596 prod_children.push(tag("vDesc", &[], TagContent::Text(&fc2(v.0))));
597 }
598 if let Some(v) = item.v_outro {
599 prod_children.push(tag("vOutro", &[], TagContent::Text(&fc2(v.0))));
600 }
601 let ind_tot_str = match item.ind_tot {
602 Some(v) => v.to_string(),
603 None => "1".to_string(),
604 };
605 prod_children.push(tag("indTot", &[], TagContent::Text(&ind_tot_str)));
606 if let Some(ref dis) = item.di {
608 for di in dis.iter().take(100) {
609 prod_children.push(build_di_xml(di));
610 }
611 }
612 if let Some(ref exports) = item.det_export {
614 for dex in exports.iter().take(500) {
615 prod_children.push(build_det_export_xml(dex));
616 }
617 }
618 if let Some(ref xped) = item.x_ped {
619 prod_children.push(tag("xPed", &[], TagContent::Text(xped)));
620 }
621 if let Some(ref nip) = item.n_item_ped {
622 prod_children.push(tag("nItemPed", &[], TagContent::Text(nip)));
623 }
624 if let Some(ref nfci) = item.n_fci {
625 prod_children.push(tag("nFCI", &[], TagContent::Text(nfci)));
626 }
627 prod_children.extend(prod_options);
628
629 let mut v_ipi_devol = 0i64;
631 let imposto_devol_xml = if let Some(ref devol) = item.imposto_devol {
632 v_ipi_devol = devol.v_ipi_devol.0;
633 let p_devol_str = format_decimal(devol.p_devol.0 as f64 / 100.0, 2);
634 let v_ipi_devol_str = format_cents(devol.v_ipi_devol.0, 2);
635 tag(
636 "impostoDevol",
637 &[],
638 TagContent::Children(vec![
639 tag("pDevol", &[], TagContent::Text(&p_devol_str)),
640 tag(
641 "IPI",
642 &[],
643 TagContent::Children(vec![tag(
644 "vIPIDevol",
645 &[],
646 TagContent::Text(&v_ipi_devol_str),
647 )]),
648 ),
649 ]),
650 )
651 } else {
652 String::new()
653 };
654
655 let nitem = item.item_number.to_string();
657 let mut det_children = vec![
658 tag("prod", &[], TagContent::Children(prod_children)),
659 tag("imposto", &[], TagContent::Children(imposto_children)),
660 ];
661 if !imposto_devol_xml.is_empty() {
662 det_children.push(imposto_devol_xml);
663 }
664 det_children.extend(det_extras);
665
666 let xml = tag(
667 "det",
668 &[("nItem", &nitem)],
669 TagContent::Children(det_children),
670 );
671
672 Ok(DetResult {
673 xml,
674 icms_totals,
675 v_ipi,
676 v_pis: item.pis_v_pis.map(|c| c.0).unwrap_or(0),
677 v_cofins: item.cofins_v_cofins.map(|c| c.0).unwrap_or(0),
678 v_ii,
679 v_frete: item.v_frete.map(|c| c.0).unwrap_or(0),
680 v_seg: item.v_seg.map(|c| c.0).unwrap_or(0),
681 v_desc: item.v_desc.map(|c| c.0).unwrap_or(0),
682 v_outro: item.v_outro.map(|c| c.0).unwrap_or(0),
683 ind_tot: item.ind_tot.unwrap_or(1),
684 v_tot_trib: item.v_tot_trib.map(|c| c.0).unwrap_or(0),
685 v_ipi_devol,
686 has_issqn,
687 })
688}
689
690fn build_prod_options(item: &InvoiceItemData) -> Vec<String> {
691 let mut opts = Vec::new();
692
693 if let Some(ref rastros) = item.rastro {
695 for r in rastros.iter().take(500) {
696 let mut rastro_children = vec![
697 tag("nLote", &[], TagContent::Text(&r.n_lote)),
698 tag("qLote", &[], TagContent::Text(&format_decimal(r.q_lote, 3))),
699 tag("dFab", &[], TagContent::Text(&r.d_fab)),
700 tag("dVal", &[], TagContent::Text(&r.d_val)),
701 ];
702 if let Some(ref agreg) = r.c_agreg {
703 rastro_children.push(tag("cAgreg", &[], TagContent::Text(agreg)));
704 }
705 opts.push(tag("rastro", &[], TagContent::Children(rastro_children)));
706 }
707 }
708
709 if let Some(ref v) = item.veic_prod {
711 opts.push(tag(
712 "veicProd",
713 &[],
714 TagContent::Children(vec![
715 tag("tpOp", &[], TagContent::Text(&v.tp_op)),
716 tag("chassi", &[], TagContent::Text(&v.chassi)),
717 tag("cCor", &[], TagContent::Text(&v.c_cor)),
718 tag("xCor", &[], TagContent::Text(&v.x_cor)),
719 tag("pot", &[], TagContent::Text(&v.pot)),
720 tag("cilin", &[], TagContent::Text(&v.cilin)),
721 tag("pesoL", &[], TagContent::Text(&v.peso_l)),
722 tag("pesoB", &[], TagContent::Text(&v.peso_b)),
723 tag("nSerie", &[], TagContent::Text(&v.n_serie)),
724 tag("tpComb", &[], TagContent::Text(&v.tp_comb)),
725 tag("nMotor", &[], TagContent::Text(&v.n_motor)),
726 tag("CMT", &[], TagContent::Text(&v.cmt)),
727 tag("dist", &[], TagContent::Text(&v.dist)),
728 tag("anoMod", &[], TagContent::Text(&v.ano_mod)),
729 tag("anoFab", &[], TagContent::Text(&v.ano_fab)),
730 tag("tpPint", &[], TagContent::Text(&v.tp_pint)),
731 tag("tpVeic", &[], TagContent::Text(&v.tp_veic)),
732 tag("espVeic", &[], TagContent::Text(&v.esp_veic)),
733 tag("VIN", &[], TagContent::Text(&v.vin)),
734 tag("condVeic", &[], TagContent::Text(&v.cond_veic)),
735 tag("cMod", &[], TagContent::Text(&v.c_mod)),
736 tag("cCorDENATRAN", &[], TagContent::Text(&v.c_cor_denatran)),
737 tag("lota", &[], TagContent::Text(&v.lota)),
738 tag("tpRest", &[], TagContent::Text(&v.tp_rest)),
739 ]),
740 ));
741 } else if let Some(ref m) = item.med {
742 let mut med_children = Vec::new();
743 if let Some(ref code) = m.c_prod_anvisa {
744 med_children.push(tag("cProdANVISA", &[], TagContent::Text(code)));
745 }
746 if let Some(ref reason) = m.x_motivo_isencao {
747 med_children.push(tag("xMotivoIsencao", &[], TagContent::Text(reason)));
748 }
749 med_children.push(tag(
750 "vPMC",
751 &[],
752 TagContent::Text(&format_cents(m.v_pmc.0, 2)),
753 ));
754 opts.push(tag("med", &[], TagContent::Children(med_children)));
755 } else if let Some(ref arms) = item.arma {
756 for a in arms.iter().take(500) {
757 opts.push(tag(
758 "arma",
759 &[],
760 TagContent::Children(vec![
761 tag("tpArma", &[], TagContent::Text(&a.tp_arma)),
762 tag("nSerie", &[], TagContent::Text(&a.n_serie)),
763 tag("nCano", &[], TagContent::Text(&a.n_cano)),
764 tag("descr", &[], TagContent::Text(&a.descr)),
765 ]),
766 ));
767 }
768 } else if let Some(ref recopi) = item.n_recopi {
769 if !recopi.is_empty() {
770 opts.push(tag("nRECOPI", &[], TagContent::Text(recopi)));
771 }
772 }
773
774 if let Some(ref comb) = item.comb {
776 opts.push(build_comb_xml(comb));
777 }
778
779 opts
780}
781
782fn build_di_xml(di: &crate::types::DiData) -> String {
784 let mut children = vec![
785 tag("nDI", &[], TagContent::Text(&di.n_di)),
786 tag("dDI", &[], TagContent::Text(&di.d_di)),
787 tag("xLocDesemb", &[], TagContent::Text(&di.x_loc_desemb)),
788 tag("UFDesemb", &[], TagContent::Text(&di.uf_desemb)),
789 tag("dDesemb", &[], TagContent::Text(&di.d_desemb)),
790 tag("tpViaTransp", &[], TagContent::Text(&di.tp_via_transp)),
791 ];
792 if let Some(ref v) = di.v_afrmm {
793 children.push(tag("vAFRMM", &[], TagContent::Text(&format_cents(v.0, 2))));
794 }
795 children.push(tag(
796 "tpIntermedio",
797 &[],
798 TagContent::Text(&di.tp_intermedio),
799 ));
800 if let Some(ref cnpj) = di.cnpj {
801 children.push(tag("CNPJ", &[], TagContent::Text(cnpj)));
802 } else if let Some(ref cpf) = di.cpf {
803 children.push(tag("CPF", &[], TagContent::Text(cpf)));
804 }
805 if let Some(ref uf) = di.uf_terceiro {
806 children.push(tag("UFTerceiro", &[], TagContent::Text(uf)));
807 }
808 children.push(tag("cExportador", &[], TagContent::Text(&di.c_exportador)));
809 for adi in di.adi.iter().take(999) {
811 let mut adi_children = Vec::new();
812 if let Some(ref n) = adi.n_adicao {
813 adi_children.push(tag("nAdicao", &[], TagContent::Text(n)));
814 }
815 adi_children.push(tag("nSeqAdic", &[], TagContent::Text(&adi.n_seq_adic)));
816 adi_children.push(tag("cFabricante", &[], TagContent::Text(&adi.c_fabricante)));
817 if let Some(ref v) = adi.v_desc_di {
818 adi_children.push(tag("vDescDI", &[], TagContent::Text(&format_cents(v.0, 2))));
819 }
820 if let Some(ref n) = adi.n_draw {
821 adi_children.push(tag("nDraw", &[], TagContent::Text(n)));
822 }
823 children.push(tag("adi", &[], TagContent::Children(adi_children)));
824 }
825 tag("DI", &[], TagContent::Children(children))
826}
827
828fn build_det_export_xml(dex: &crate::types::DetExportData) -> String {
830 let mut children = Vec::new();
831 if let Some(ref n) = dex.n_draw {
832 children.push(tag("nDraw", &[], TagContent::Text(n)));
833 }
834 if dex.n_re.is_some() || dex.ch_nfe.is_some() || dex.q_export.is_some() {
835 let mut exp_ind_children = Vec::new();
836 if let Some(ref n) = dex.n_re {
837 exp_ind_children.push(tag("nRE", &[], TagContent::Text(n)));
838 }
839 if let Some(ref ch) = dex.ch_nfe {
840 exp_ind_children.push(tag("chNFe", &[], TagContent::Text(ch)));
841 }
842 if let Some(q) = dex.q_export {
843 exp_ind_children.push(tag("qExport", &[], TagContent::Text(&format_decimal(q, 4))));
844 }
845 children.push(tag(
846 "exportInd",
847 &[],
848 TagContent::Children(exp_ind_children),
849 ));
850 }
851 tag("detExport", &[], TagContent::Children(children))
852}
853
854fn build_comb_xml(comb: &CombData) -> String {
860 let mut children = vec![
861 tag("cProdANP", &[], TagContent::Text(&comb.c_prod_anp)),
862 tag("descANP", &[], TagContent::Text(&comb.desc_anp)),
863 ];
864
865 if let Some(ref v) = comb.p_glp {
866 children.push(tag("pGLP", &[], TagContent::Text(v)));
867 }
868 if let Some(ref v) = comb.p_gn_n {
869 children.push(tag("pGNn", &[], TagContent::Text(v)));
870 }
871 if let Some(ref v) = comb.p_gn_i {
872 children.push(tag("pGNi", &[], TagContent::Text(v)));
873 }
874 if let Some(ref v) = comb.v_part {
875 children.push(tag("vPart", &[], TagContent::Text(v)));
876 }
877 if let Some(ref v) = comb.codif {
878 children.push(tag("CODIF", &[], TagContent::Text(v)));
879 }
880 if let Some(ref v) = comb.q_temp {
881 children.push(tag("qTemp", &[], TagContent::Text(v)));
882 }
883
884 children.push(tag("UFCons", &[], TagContent::Text(&comb.uf_cons)));
885
886 if let Some(ref cide) = comb.cide {
888 let cide_children = vec![
889 tag("qBCProd", &[], TagContent::Text(&cide.q_bc_prod)),
890 tag("vAliqProd", &[], TagContent::Text(&cide.v_aliq_prod)),
891 tag("vCIDE", &[], TagContent::Text(&cide.v_cide)),
892 ];
893 children.push(tag("CIDE", &[], TagContent::Children(cide_children)));
894 }
895
896 if let Some(ref enc) = comb.encerrante {
898 let mut enc_children = vec![tag("nBico", &[], TagContent::Text(&enc.n_bico))];
899 if let Some(ref bomba) = enc.n_bomba {
900 enc_children.push(tag("nBomba", &[], TagContent::Text(bomba)));
901 }
902 enc_children.push(tag("nTanque", &[], TagContent::Text(&enc.n_tanque)));
903 enc_children.push(tag("vEncIni", &[], TagContent::Text(&enc.v_enc_ini)));
904 enc_children.push(tag("vEncFin", &[], TagContent::Text(&enc.v_enc_fin)));
905 children.push(tag("encerrante", &[], TagContent::Children(enc_children)));
906 }
907
908 if let Some(ref v) = comb.p_bio {
910 children.push(tag("pBio", &[], TagContent::Text(v)));
911 }
912
913 if let Some(ref origins) = comb.orig_comb {
915 for orig in origins {
916 let orig_children = vec![
917 tag("indImport", &[], TagContent::Text(&orig.ind_import)),
918 tag("cUFOrig", &[], TagContent::Text(&orig.c_uf_orig)),
919 tag("pOrig", &[], TagContent::Text(&orig.p_orig)),
920 ];
921 children.push(tag("origComb", &[], TagContent::Children(orig_children)));
922 }
923 }
924
925 tag("comb", &[], TagContent::Children(children))
926}
927
928fn build_det_extras(item: &InvoiceItemData) -> Vec<String> {
929 let mut extras = Vec::new();
930
931 if let Some(ref info) = item.inf_ad_prod {
932 extras.push(tag("infAdProd", &[], TagContent::Text(info)));
933 }
934
935 if let Some(ref obs) = item.obs_item {
936 let mut obs_children = Vec::new();
937 if let Some(ref cont) = obs.obs_cont {
938 obs_children.push(tag(
939 "obsCont",
940 &[("xCampo", &cont.x_campo)],
941 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&cont.x_texto))]),
942 ));
943 }
944 if let Some(ref fisco) = obs.obs_fisco {
945 obs_children.push(tag(
946 "obsFisco",
947 &[("xCampo", &fisco.x_campo)],
948 TagContent::Children(vec![tag("xTexto", &[], TagContent::Text(&fisco.x_texto))]),
949 ));
950 }
951 extras.push(tag("obsItem", &[], TagContent::Children(obs_children)));
952 }
953
954 if let Some(ref dfe) = item.dfe_referenciado {
955 let mut dfe_children = vec![tag("chaveAcesso", &[], TagContent::Text(&dfe.chave_acesso))];
956 if let Some(ref n) = dfe.n_item {
957 dfe_children.push(tag("nItem", &[], TagContent::Text(n)));
958 }
959 extras.push(tag(
960 "DFeReferenciado",
961 &[],
962 TagContent::Children(dfe_children),
963 ));
964 }
965
966 extras
967}
968
969#[cfg(test)]
970mod tests {
971 use super::*;
972 use crate::newtypes::{Cents, IbgeCode, Rate};
973 use crate::tax_issqn::IssqnData as TaxIssqnData;
974 use crate::types::{
975 CideData, CombData, EncerranteData, InvoiceItemData, InvoiceModel, IssuerData,
976 OrigCombData, SefazEnvironment, TaxRegime,
977 };
978
979 fn sample_build_data() -> InvoiceBuildData {
980 let issuer = IssuerData::new(
981 "12345678000199",
982 "123456789",
983 "Test Company",
984 TaxRegime::SimplesNacional,
985 "SP",
986 IbgeCode("3550308".to_string()),
987 "Sao Paulo",
988 "Av Paulista",
989 "1000",
990 "Bela Vista",
991 "01310100",
992 );
993
994 InvoiceBuildData {
995 model: InvoiceModel::Nfe,
996 series: 1,
997 number: 1,
998 emission_type: crate::types::EmissionType::Normal,
999 environment: SefazEnvironment::Homologation,
1000 issued_at: chrono::Utc::now()
1001 .with_timezone(&chrono::FixedOffset::west_opt(3 * 3600).expect("valid offset")),
1002 operation_nature: "VENDA".to_string(),
1003 issuer,
1004 recipient: None,
1005 items: Vec::new(),
1006 payments: Vec::new(),
1007 change_amount: None,
1008 payment_card_details: None,
1009 contingency: None,
1010 exit_at: None,
1011 operation_type: None,
1012 purpose_code: None,
1013 intermediary_indicator: None,
1014 emission_process: None,
1015 consumer_type: None,
1016 buyer_presence: None,
1017 print_format: None,
1018 references: None,
1019 transport: None,
1020 billing: None,
1021 withdrawal: None,
1022 delivery: None,
1023 authorized_xml: None,
1024 additional_info: None,
1025 intermediary: None,
1026 ret_trib: None,
1027 tech_responsible: None,
1028 purchase: None,
1029 export: None,
1030 issqn_tot: None,
1031 cana: None,
1032 agropecuario: None,
1033 compra_gov: None,
1034 pag_antecipado: None,
1035 is_tot: None,
1036 ibs_cbs_tot: None,
1037 destination_indicator: None,
1038 ver_proc: None,
1039 }
1040 }
1041
1042 fn sample_item() -> InvoiceItemData {
1043 InvoiceItemData::new(
1044 1,
1045 "001",
1046 "Gasolina Comum",
1047 "27101259",
1048 "5102",
1049 "LT",
1050 50.0,
1051 Cents(599),
1052 Cents(29950),
1053 "102",
1054 Rate(0),
1055 Cents(0),
1056 "99",
1057 "99",
1058 )
1059 }
1060
1061 #[test]
1064 fn comb_minimal_produces_correct_xml() {
1065 let comb = CombData::new("210203001", "GLP", "SP");
1066 let xml = build_comb_xml(&comb);
1067
1068 assert_eq!(
1069 xml,
1070 "<comb>\
1071 <cProdANP>210203001</cProdANP>\
1072 <descANP>GLP</descANP>\
1073 <UFCons>SP</UFCons>\
1074 </comb>"
1075 );
1076 }
1077
1078 #[test]
1079 fn comb_with_glp_percentages() {
1080 let comb = CombData::new("210203001", "GLP", "SP")
1081 .p_glp("60.0000")
1082 .p_gn_n("25.0000")
1083 .p_gn_i("15.0000")
1084 .v_part("3.50");
1085
1086 let xml = build_comb_xml(&comb);
1087
1088 assert_eq!(
1089 xml,
1090 "<comb>\
1091 <cProdANP>210203001</cProdANP>\
1092 <descANP>GLP</descANP>\
1093 <pGLP>60.0000</pGLP>\
1094 <pGNn>25.0000</pGNn>\
1095 <pGNi>15.0000</pGNi>\
1096 <vPart>3.50</vPart>\
1097 <UFCons>SP</UFCons>\
1098 </comb>"
1099 );
1100 }
1101
1102 #[test]
1103 fn comb_with_codif_and_qtemp() {
1104 let comb = CombData::new("320102001", "GASOLINA COMUM", "PR")
1105 .codif("123456789")
1106 .q_temp("1000.0000");
1107
1108 let xml = build_comb_xml(&comb);
1109
1110 assert_eq!(
1111 xml,
1112 "<comb>\
1113 <cProdANP>320102001</cProdANP>\
1114 <descANP>GASOLINA COMUM</descANP>\
1115 <CODIF>123456789</CODIF>\
1116 <qTemp>1000.0000</qTemp>\
1117 <UFCons>PR</UFCons>\
1118 </comb>"
1119 );
1120 }
1121
1122 #[test]
1123 fn comb_with_cide() {
1124 let cide = CideData::new("1000.0000", "0.0700", "70.00");
1125 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").cide(cide);
1126
1127 let xml = build_comb_xml(&comb);
1128
1129 assert_eq!(
1130 xml,
1131 "<comb>\
1132 <cProdANP>320102001</cProdANP>\
1133 <descANP>GASOLINA COMUM</descANP>\
1134 <UFCons>SP</UFCons>\
1135 <CIDE>\
1136 <qBCProd>1000.0000</qBCProd>\
1137 <vAliqProd>0.0700</vAliqProd>\
1138 <vCIDE>70.00</vCIDE>\
1139 </CIDE>\
1140 </comb>"
1141 );
1142 }
1143
1144 #[test]
1145 fn comb_with_encerrante() {
1146 let enc = EncerranteData::new("1", "1", "1234.567", "1284.567").n_bomba("2");
1147 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").encerrante(enc);
1148
1149 let xml = build_comb_xml(&comb);
1150
1151 assert_eq!(
1152 xml,
1153 "<comb>\
1154 <cProdANP>320102001</cProdANP>\
1155 <descANP>GASOLINA COMUM</descANP>\
1156 <UFCons>SP</UFCons>\
1157 <encerrante>\
1158 <nBico>1</nBico>\
1159 <nBomba>2</nBomba>\
1160 <nTanque>1</nTanque>\
1161 <vEncIni>1234.567</vEncIni>\
1162 <vEncFin>1284.567</vEncFin>\
1163 </encerrante>\
1164 </comb>"
1165 );
1166 }
1167
1168 #[test]
1169 fn comb_encerrante_without_bomba() {
1170 let enc = EncerranteData::new("3", "2", "5000.000", "5050.000");
1171 let comb = CombData::new("320102001", "GASOLINA COMUM", "RJ").encerrante(enc);
1172
1173 let xml = build_comb_xml(&comb);
1174
1175 assert_eq!(
1176 xml,
1177 "<comb>\
1178 <cProdANP>320102001</cProdANP>\
1179 <descANP>GASOLINA COMUM</descANP>\
1180 <UFCons>RJ</UFCons>\
1181 <encerrante>\
1182 <nBico>3</nBico>\
1183 <nTanque>2</nTanque>\
1184 <vEncIni>5000.000</vEncIni>\
1185 <vEncFin>5050.000</vEncFin>\
1186 </encerrante>\
1187 </comb>"
1188 );
1189 }
1190
1191 #[test]
1192 fn comb_with_pbio() {
1193 let comb = CombData::new("810102001", "OLEO DIESEL B S10", "SP").p_bio("15.0000");
1194
1195 let xml = build_comb_xml(&comb);
1196
1197 assert_eq!(
1198 xml,
1199 "<comb>\
1200 <cProdANP>810102001</cProdANP>\
1201 <descANP>OLEO DIESEL B S10</descANP>\
1202 <UFCons>SP</UFCons>\
1203 <pBio>15.0000</pBio>\
1204 </comb>"
1205 );
1206 }
1207
1208 #[test]
1209 fn comb_with_orig_comb_single() {
1210 let orig = OrigCombData::new("0", "35", "100.0000");
1211 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig]);
1212
1213 let xml = build_comb_xml(&comb);
1214
1215 assert_eq!(
1216 xml,
1217 "<comb>\
1218 <cProdANP>320102001</cProdANP>\
1219 <descANP>GASOLINA COMUM</descANP>\
1220 <UFCons>SP</UFCons>\
1221 <origComb>\
1222 <indImport>0</indImport>\
1223 <cUFOrig>35</cUFOrig>\
1224 <pOrig>100.0000</pOrig>\
1225 </origComb>\
1226 </comb>"
1227 );
1228 }
1229
1230 #[test]
1231 fn comb_with_orig_comb_multiple() {
1232 let orig1 = OrigCombData::new("0", "35", "70.0000");
1233 let orig2 = OrigCombData::new("1", "99", "30.0000");
1234 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP").orig_comb(vec![orig1, orig2]);
1235
1236 let xml = build_comb_xml(&comb);
1237
1238 assert_eq!(
1239 xml,
1240 "<comb>\
1241 <cProdANP>320102001</cProdANP>\
1242 <descANP>GASOLINA COMUM</descANP>\
1243 <UFCons>SP</UFCons>\
1244 <origComb>\
1245 <indImport>0</indImport>\
1246 <cUFOrig>35</cUFOrig>\
1247 <pOrig>70.0000</pOrig>\
1248 </origComb>\
1249 <origComb>\
1250 <indImport>1</indImport>\
1251 <cUFOrig>99</cUFOrig>\
1252 <pOrig>30.0000</pOrig>\
1253 </origComb>\
1254 </comb>"
1255 );
1256 }
1257
1258 #[test]
1259 fn comb_full_with_all_fields() {
1260 let cide = CideData::new("500.0000", "0.0700", "35.00");
1261 let enc = EncerranteData::new("1", "1", "10000.000", "10050.000").n_bomba("1");
1262 let orig = OrigCombData::new("0", "35", "100.0000");
1263
1264 let comb = CombData::new("210203001", "GLP", "SP")
1265 .p_glp("60.0000")
1266 .p_gn_n("25.0000")
1267 .p_gn_i("15.0000")
1268 .v_part("3.50")
1269 .codif("999888777")
1270 .q_temp("500.0000")
1271 .cide(cide)
1272 .encerrante(enc)
1273 .p_bio("12.0000")
1274 .orig_comb(vec![orig]);
1275
1276 let xml = build_comb_xml(&comb);
1277
1278 assert_eq!(
1279 xml,
1280 "<comb>\
1281 <cProdANP>210203001</cProdANP>\
1282 <descANP>GLP</descANP>\
1283 <pGLP>60.0000</pGLP>\
1284 <pGNn>25.0000</pGNn>\
1285 <pGNi>15.0000</pGNi>\
1286 <vPart>3.50</vPart>\
1287 <CODIF>999888777</CODIF>\
1288 <qTemp>500.0000</qTemp>\
1289 <UFCons>SP</UFCons>\
1290 <CIDE>\
1291 <qBCProd>500.0000</qBCProd>\
1292 <vAliqProd>0.0700</vAliqProd>\
1293 <vCIDE>35.00</vCIDE>\
1294 </CIDE>\
1295 <encerrante>\
1296 <nBico>1</nBico>\
1297 <nBomba>1</nBomba>\
1298 <nTanque>1</nTanque>\
1299 <vEncIni>10000.000</vEncIni>\
1300 <vEncFin>10050.000</vEncFin>\
1301 </encerrante>\
1302 <pBio>12.0000</pBio>\
1303 <origComb>\
1304 <indImport>0</indImport>\
1305 <cUFOrig>35</cUFOrig>\
1306 <pOrig>100.0000</pOrig>\
1307 </origComb>\
1308 </comb>"
1309 );
1310 }
1311
1312 #[test]
1313 fn comb_in_det_xml() {
1314 let comb = CombData::new("320102001", "GASOLINA COMUM", "SP");
1315 let item = sample_item().comb(comb);
1316 let data = sample_build_data();
1317 let result = build_det(&item, &data).expect("build_det should succeed");
1318
1319 let prod_start = result.xml.find("<prod>").expect("<prod> must exist");
1321 let prod_end = result.xml.find("</prod>").expect("</prod> must exist");
1322 let prod_section = &result.xml[prod_start..prod_end];
1323
1324 assert!(prod_section.contains("<comb>"));
1325 assert!(prod_section.contains("<cProdANP>320102001</cProdANP>"));
1326 assert!(prod_section.contains("<descANP>GASOLINA COMUM</descANP>"));
1327 assert!(prod_section.contains("<UFCons>SP</UFCons>"));
1328 assert!(prod_section.contains("</comb>"));
1329 }
1330
1331 #[test]
1334 fn issqn_item_produces_issqn_tag_not_icms() {
1335 let issqn_data = TaxIssqnData::new(10000, 500, 500, "3550308", "14.01")
1336 .ind_iss("1")
1337 .ind_incentivo("2");
1338 let item = sample_item().issqn(issqn_data);
1339 let data = sample_build_data();
1340 let result = build_det(&item, &data).expect("build_det should succeed");
1341
1342 assert!(result.xml.contains("<ISSQN>"));
1344 assert!(result.xml.contains("<vBC>100.00</vBC>"));
1345 assert!(result.xml.contains("<vAliq>5.0000</vAliq>"));
1346 assert!(result.xml.contains("<vISSQN>5.00</vISSQN>"));
1347 assert!(result.xml.contains("<cMunFG>3550308</cMunFG>"));
1348 assert!(result.xml.contains("<cListServ>14.01</cListServ>"));
1349 assert!(result.xml.contains("<indISS>1</indISS>"));
1350 assert!(result.xml.contains("<indIncentivo>2</indIncentivo>"));
1351 assert!(result.xml.contains("</ISSQN>"));
1352
1353 assert!(!result.xml.contains("<ICMS>"));
1355 assert!(!result.xml.contains("</ICMS>"));
1356 assert!(result.has_issqn);
1357 }
1358
1359 #[test]
1360 fn issqn_item_with_all_optional_fields() {
1361 let issqn_data = TaxIssqnData::new(20000, 300, 600, "3304557", "07.02")
1362 .v_deducao(1000)
1363 .v_outro(500)
1364 .v_desc_incond(200)
1365 .v_desc_cond(100)
1366 .v_iss_ret(300)
1367 .ind_iss("1")
1368 .c_servico("1234")
1369 .c_mun("3304557")
1370 .c_pais("1058")
1371 .n_processo("ABC123")
1372 .ind_incentivo("1");
1373
1374 let item = sample_item().issqn(issqn_data);
1375 let data = sample_build_data();
1376 let result = build_det(&item, &data).expect("build_det should succeed");
1377
1378 assert!(result.xml.contains("<vBC>200.00</vBC>"));
1379 assert!(result.xml.contains("<vAliq>3.0000</vAliq>"));
1380 assert!(result.xml.contains("<vISSQN>6.00</vISSQN>"));
1381 assert!(result.xml.contains("<vDeducao>10.00</vDeducao>"));
1382 assert!(result.xml.contains("<vOutro>5.00</vOutro>"));
1383 assert!(result.xml.contains("<vDescIncond>2.00</vDescIncond>"));
1384 assert!(result.xml.contains("<vDescCond>1.00</vDescCond>"));
1385 assert!(result.xml.contains("<vISSRet>3.00</vISSRet>"));
1386 assert!(result.xml.contains("<cServico>1234</cServico>"));
1387 assert!(result.xml.contains("<cMun>3304557</cMun>"));
1388 assert!(result.xml.contains("<cPais>1058</cPais>"));
1389 assert!(result.xml.contains("<nProcesso>ABC123</nProcesso>"));
1390 assert!(result.xml.contains("<indIncentivo>1</indIncentivo>"));
1391 assert!(result.has_issqn);
1392 }
1393
1394 #[test]
1395 fn non_issqn_item_has_icms_and_no_issqn() {
1396 let item = sample_item();
1397 let data = sample_build_data();
1398 let result = build_det(&item, &data).expect("build_det should succeed");
1399
1400 assert!(result.xml.contains("<ICMS"));
1401 assert!(!result.xml.contains("<ISSQN>"));
1402 assert!(!result.has_issqn);
1403 }
1404
1405 #[test]
1408 fn di_minimal_with_one_adi() {
1409 use crate::types::{AdiData, DiData};
1410
1411 let adi = AdiData::new("1", "FABRICANTE_X").n_adicao("001");
1412 let di = DiData::new(
1413 "1234567890",
1414 "2025-01-15",
1415 "Santos",
1416 "SP",
1417 "2025-01-20",
1418 "1",
1419 "1",
1420 "EXP001",
1421 vec![adi],
1422 );
1423 let xml = build_di_xml(&di);
1424
1425 assert_eq!(
1426 xml,
1427 "<DI>\
1428 <nDI>1234567890</nDI>\
1429 <dDI>2025-01-15</dDI>\
1430 <xLocDesemb>Santos</xLocDesemb>\
1431 <UFDesemb>SP</UFDesemb>\
1432 <dDesemb>2025-01-20</dDesemb>\
1433 <tpViaTransp>1</tpViaTransp>\
1434 <tpIntermedio>1</tpIntermedio>\
1435 <cExportador>EXP001</cExportador>\
1436 <adi>\
1437 <nAdicao>001</nAdicao>\
1438 <nSeqAdic>1</nSeqAdic>\
1439 <cFabricante>FABRICANTE_X</cFabricante>\
1440 </adi>\
1441 </DI>"
1442 );
1443 }
1444
1445 #[test]
1446 fn di_with_all_optional_fields() {
1447 use crate::types::{AdiData, DiData};
1448
1449 let adi = AdiData::new("1", "FAB_Y")
1450 .n_adicao("002")
1451 .v_desc_di(Cents(15000))
1452 .n_draw("20259999999");
1453 let di = DiData::new(
1454 "DI-2025-001",
1455 "2025-03-01",
1456 "Paranagua",
1457 "PR",
1458 "2025-03-05",
1459 "1",
1460 "2",
1461 "EXP002",
1462 vec![adi],
1463 )
1464 .v_afrmm(Cents(5000))
1465 .cnpj("12345678000199")
1466 .uf_terceiro("RJ");
1467
1468 let xml = build_di_xml(&di);
1469
1470 assert_eq!(
1471 xml,
1472 "<DI>\
1473 <nDI>DI-2025-001</nDI>\
1474 <dDI>2025-03-01</dDI>\
1475 <xLocDesemb>Paranagua</xLocDesemb>\
1476 <UFDesemb>PR</UFDesemb>\
1477 <dDesemb>2025-03-05</dDesemb>\
1478 <tpViaTransp>1</tpViaTransp>\
1479 <vAFRMM>50.00</vAFRMM>\
1480 <tpIntermedio>2</tpIntermedio>\
1481 <CNPJ>12345678000199</CNPJ>\
1482 <UFTerceiro>RJ</UFTerceiro>\
1483 <cExportador>EXP002</cExportador>\
1484 <adi>\
1485 <nAdicao>002</nAdicao>\
1486 <nSeqAdic>1</nSeqAdic>\
1487 <cFabricante>FAB_Y</cFabricante>\
1488 <vDescDI>150.00</vDescDI>\
1489 <nDraw>20259999999</nDraw>\
1490 </adi>\
1491 </DI>"
1492 );
1493 }
1494
1495 #[test]
1496 fn di_with_cpf_instead_of_cnpj() {
1497 use crate::types::{AdiData, DiData};
1498
1499 let adi = AdiData::new("1", "FAB_Z");
1500 let di = DiData::new(
1501 "DI-CPF",
1502 "2025-06-01",
1503 "Recife",
1504 "PE",
1505 "2025-06-03",
1506 "7",
1507 "3",
1508 "EXP003",
1509 vec![adi],
1510 )
1511 .cpf("12345678901");
1512
1513 let xml = build_di_xml(&di);
1514 assert!(xml.contains("<CPF>12345678901</CPF>"));
1515 assert!(!xml.contains("<CNPJ>"));
1516 }
1517
1518 #[test]
1519 fn di_with_multiple_adi() {
1520 use crate::types::{AdiData, DiData};
1521
1522 let adi1 = AdiData::new("1", "FAB_A").n_adicao("001");
1523 let adi2 = AdiData::new("2", "FAB_B").n_adicao("001");
1524 let di = DiData::new(
1525 "DI-MULTI",
1526 "2025-01-01",
1527 "Santos",
1528 "SP",
1529 "2025-01-05",
1530 "1",
1531 "1",
1532 "EXP-M",
1533 vec![adi1, adi2],
1534 );
1535 let xml = build_di_xml(&di);
1536
1537 let count = xml.matches("<adi>").count();
1539 assert_eq!(count, 2, "expected 2 <adi> elements, got {count}");
1540 assert!(xml.contains("<nSeqAdic>1</nSeqAdic>"));
1541 assert!(xml.contains("<nSeqAdic>2</nSeqAdic>"));
1542 assert!(xml.contains("<cFabricante>FAB_A</cFabricante>"));
1543 assert!(xml.contains("<cFabricante>FAB_B</cFabricante>"));
1544 }
1545
1546 #[test]
1547 fn di_in_det_xml_between_ind_tot_and_xped() {
1548 use crate::types::{AdiData, DiData};
1549
1550 let adi = AdiData::new("1", "FAB").n_adicao("001");
1551 let di = DiData::new(
1552 "DI-001",
1553 "2025-01-15",
1554 "Santos",
1555 "SP",
1556 "2025-01-20",
1557 "1",
1558 "1",
1559 "EXP",
1560 vec![adi],
1561 );
1562 let item = sample_item().di(vec![di]).x_ped("PO-123");
1563 let data = sample_build_data();
1564 let result = build_det(&item, &data).expect("build_det should succeed");
1565
1566 let xml = &result.xml;
1567 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1568 let di_pos = xml.find("<DI>").expect("<DI> must exist");
1569 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1570
1571 assert!(di_pos > ind_tot_pos, "DI must come after indTot");
1572 assert!(xped_pos > di_pos, "xPed must come after DI");
1573 }
1574
1575 #[test]
1578 fn det_export_with_n_draw_only() {
1579 use crate::types::DetExportData;
1580
1581 let dex = DetExportData::new().n_draw("20250000001");
1582 let xml = build_det_export_xml(&dex);
1583
1584 assert_eq!(
1585 xml,
1586 "<detExport>\
1587 <nDraw>20250000001</nDraw>\
1588 </detExport>"
1589 );
1590 }
1591
1592 #[test]
1593 fn det_export_with_export_ind() {
1594 use crate::types::DetExportData;
1595
1596 let dex = DetExportData::new()
1597 .n_draw("20250000002")
1598 .n_re("123456789012")
1599 .ch_nfe("12345678901234567890123456789012345678901234")
1600 .q_export(100.5);
1601 let xml = build_det_export_xml(&dex);
1602
1603 assert_eq!(
1604 xml,
1605 "<detExport>\
1606 <nDraw>20250000002</nDraw>\
1607 <exportInd>\
1608 <nRE>123456789012</nRE>\
1609 <chNFe>12345678901234567890123456789012345678901234</chNFe>\
1610 <qExport>100.5000</qExport>\
1611 </exportInd>\
1612 </detExport>"
1613 );
1614 }
1615
1616 #[test]
1617 fn det_export_empty() {
1618 use crate::types::DetExportData;
1619
1620 let dex = DetExportData::new();
1621 let xml = build_det_export_xml(&dex);
1622
1623 assert_eq!(xml, "<detExport></detExport>");
1624 }
1625
1626 #[test]
1627 fn det_export_in_det_xml_between_ind_tot_and_xped() {
1628 use crate::types::DetExportData;
1629
1630 let dex = DetExportData::new().n_draw("20250000001");
1631 let item = sample_item().det_export(vec![dex]).x_ped("PO-456");
1632 let data = sample_build_data();
1633 let result = build_det(&item, &data).expect("build_det should succeed");
1634
1635 let xml = &result.xml;
1636 let ind_tot_pos = xml.find("</indTot>").expect("</indTot> must exist");
1637 let det_exp_pos = xml.find("<detExport>").expect("<detExport> must exist");
1638 let xped_pos = xml.find("<xPed>").expect("<xPed> must exist");
1639
1640 assert!(
1641 det_exp_pos > ind_tot_pos,
1642 "detExport must come after indTot"
1643 );
1644 assert!(xped_pos > det_exp_pos, "xPed must come after detExport");
1645 }
1646
1647 #[test]
1650 fn imposto_devol_produces_correct_xml() {
1651 use crate::types::ImpostoDevolData;
1652
1653 let devol = ImpostoDevolData::new(Rate(10000), Cents(5000));
1654 let item = sample_item().imposto_devol(devol);
1655 let data = sample_build_data();
1656 let result = build_det(&item, &data).expect("build_det should succeed");
1657
1658 assert!(result.xml.contains(
1659 "<impostoDevol>\
1660 <pDevol>100.00</pDevol>\
1661 <IPI>\
1662 <vIPIDevol>50.00</vIPIDevol>\
1663 </IPI>\
1664 </impostoDevol>"
1665 ));
1666 assert_eq!(result.v_ipi_devol, 5000);
1667 }
1668
1669 #[test]
1670 fn imposto_devol_50_percent() {
1671 use crate::types::ImpostoDevolData;
1672
1673 let devol = ImpostoDevolData::new(Rate(5000), Cents(2500));
1674 let item = sample_item().imposto_devol(devol);
1675 let data = sample_build_data();
1676 let result = build_det(&item, &data).expect("build_det should succeed");
1677
1678 assert!(result.xml.contains("<pDevol>50.00</pDevol>"));
1679 assert!(result.xml.contains("<vIPIDevol>25.00</vIPIDevol>"));
1680 assert_eq!(result.v_ipi_devol, 2500);
1681 }
1682
1683 #[test]
1684 fn imposto_devol_after_imposto_before_inf_ad_prod() {
1685 use crate::types::ImpostoDevolData;
1686
1687 let devol = ImpostoDevolData::new(Rate(10000), Cents(1000));
1688 let item = sample_item().imposto_devol(devol).inf_ad_prod("test info");
1689 let data = sample_build_data();
1690 let result = build_det(&item, &data).expect("build_det should succeed");
1691
1692 let imposto_end = result
1693 .xml
1694 .find("</imposto>")
1695 .expect("</imposto> must exist");
1696 let devol_pos = result
1697 .xml
1698 .find("<impostoDevol>")
1699 .expect("<impostoDevol> must exist");
1700 let inf_ad_pos = result
1701 .xml
1702 .find("<infAdProd>")
1703 .expect("<infAdProd> must exist");
1704
1705 assert!(
1706 devol_pos > imposto_end,
1707 "impostoDevol must come after </imposto>"
1708 );
1709 assert!(
1710 inf_ad_pos > devol_pos,
1711 "infAdProd must come after impostoDevol"
1712 );
1713 }
1714
1715 #[test]
1716 fn no_imposto_devol_when_none() {
1717 let item = sample_item();
1718 let data = sample_build_data();
1719 let result = build_det(&item, &data).expect("build_det should succeed");
1720
1721 assert!(!result.xml.contains("<impostoDevol>"));
1722 assert_eq!(result.v_ipi_devol, 0);
1723 }
1724}