1mod icms_variant;
4mod prod;
5
6#[cfg(test)]
7mod tests;
8
9use crate::FiscalError;
10use crate::format_utils::{format_cents, format_decimal, format_rate4};
11use crate::newtypes::{Cents, Rate4};
12use crate::tax_ibs_cbs;
13use crate::tax_icms::{self, IcmsTotals};
14use crate::tax_is;
15use crate::tax_issqn;
16use crate::tax_pis_cofins_ipi::{self, CofinsData, IiData, IpiData, PisData};
17use crate::types::{InvoiceBuildData, InvoiceItemData, InvoiceModel, SefazEnvironment, TaxRegime};
18use crate::xml_utils::{TagContent, tag};
19
20use icms_variant::build_icms_variant;
21#[cfg(test)]
22use prod::build_comb_xml;
23use prod::{build_det_export_xml, build_det_extras, build_di_xml, build_prod_options};
24
25const HOMOLOGATION_XPROD: &str =
27 "NOTA FISCAL EMITIDA EM AMBIENTE DE HOMOLOGACAO - SEM VALOR FISCAL";
28
29#[derive(Debug, Clone)]
31pub struct DetResult {
32 pub xml: String,
34 pub icms_totals: IcmsTotals,
36 pub v_ipi: i64,
38 pub v_pis: i64,
40 pub v_cofins: i64,
42 pub v_ii: i64,
44 pub v_frete: i64,
46 pub v_seg: i64,
48 pub v_desc: i64,
50 pub v_outro: i64,
52 pub ind_tot: u8,
54 pub v_tot_trib: i64,
56 pub v_ipi_devol: i64,
58 pub v_pis_st: i64,
60 pub v_cofins_st: i64,
62 pub ind_deduz_deson: bool,
64 pub has_issqn: bool,
66}
67
68pub(crate) fn build_det(
70 item: &InvoiceItemData,
71 data: &InvoiceBuildData,
72) -> Result<DetResult, FiscalError> {
73 if item.nve.len() > 8 {
75 return Err(FiscalError::InvalidTaxData(format!(
76 "Item {}: NVE limited to 8 entries, got {}",
77 item.item_number,
78 item.nve.len()
79 )));
80 }
81
82 let is_simples = matches!(
83 data.issuer.tax_regime,
84 TaxRegime::SimplesNacional | TaxRegime::SimplesExcess
85 );
86
87 let has_issqn = item.issqn.is_some();
88
89 let mut icms_totals = IcmsTotals::default();
91 let icms_xml = if has_issqn {
92 String::new()
93 } else {
94 let icms_variant = build_icms_variant(item, is_simples)?;
95 tax_icms::build_icms_xml(&icms_variant, &mut icms_totals)?
96 };
97
98 let issqn_xml = if let Some(ref issqn_data) = item.issqn {
100 tax_issqn::build_issqn_xml(issqn_data)
101 } else {
102 String::new()
103 };
104
105 let pis_xml = tax_pis_cofins_ipi::build_pis_xml(&PisData {
107 cst: item.pis_cst.clone(),
108 v_bc: item.pis_v_bc.or(Some(Cents(0))),
109 p_pis: item.pis_p_pis.or(Some(Rate4(0))),
110 v_pis: item.pis_v_pis.or(Some(Cents(0))),
111 q_bc_prod: item.pis_q_bc_prod,
112 v_aliq_prod: item.pis_v_aliq_prod,
113 });
114
115 let cofins_xml = tax_pis_cofins_ipi::build_cofins_xml(&CofinsData {
117 cst: item.cofins_cst.clone(),
118 v_bc: item.cofins_v_bc.or(Some(Cents(0))),
119 p_cofins: item.cofins_p_cofins.or(Some(Rate4(0))),
120 v_cofins: item.cofins_v_cofins.or(Some(Cents(0))),
121 q_bc_prod: item.cofins_q_bc_prod,
122 v_aliq_prod: item.cofins_v_aliq_prod,
123 });
124
125 let mut ipi_xml = String::new();
127 let mut v_ipi = 0i64;
128 if let Some(ref ipi_cst) = item.ipi_cst {
129 ipi_xml = tax_pis_cofins_ipi::build_ipi_xml(&IpiData {
130 cst: ipi_cst.clone(),
131 c_enq: item.ipi_c_enq.clone().unwrap_or_else(|| "999".to_string()),
132 v_bc: item.ipi_v_bc,
133 p_ipi: item.ipi_p_ipi,
134 v_ipi: item.ipi_v_ipi,
135 q_unid: item.ipi_q_unid,
136 v_unid: item.ipi_v_unid,
137 ..IpiData::default()
138 });
139 v_ipi = item.ipi_v_ipi.map(|c| c.0).unwrap_or(0);
140 }
141
142 let mut ii_xml = String::new();
144 let mut v_ii = 0i64;
145 if let Some(ii_vbc) = item.ii_v_bc {
146 ii_xml = tax_pis_cofins_ipi::build_ii_xml(&IiData {
147 v_bc: ii_vbc,
148 v_desp_adu: item.ii_v_desp_adu.unwrap_or(Cents(0)),
149 v_ii: item.ii_v_ii.unwrap_or(Cents(0)),
150 v_iof: item.ii_v_iof.unwrap_or(Cents(0)),
151 });
152 v_ii = item.ii_v_ii.map(|c| c.0).unwrap_or(0);
153 }
154
155 let prod_options = build_prod_options(item);
157
158 let mut v_pis_st = 0i64;
160 if let Some(ref pis_st_data) = item.pis_st {
161 if pis_st_data.ind_soma_pis_st == Some(1) {
163 v_pis_st = pis_st_data.v_pis.0;
164 }
165 }
166
167 let mut v_cofins_st = 0i64;
169 if let Some(ref cofins_st_data) = item.cofins_st {
170 if cofins_st_data.ind_soma_cofins_st == Some(1) {
172 v_cofins_st = cofins_st_data.v_cofins.0;
173 }
174 }
175
176 let item_ind_deduz_deson = item
178 .icms_ind_deduz_deson
179 .as_deref()
180 .map(|v| v == "1")
181 .unwrap_or(false);
182
183 let mut imposto_children: Vec<String> = Vec::new();
188 if let Some(ref v) = item.v_tot_trib {
190 if v.0 > 0 {
191 imposto_children.push(tag(
192 "vTotTrib",
193 &[],
194 TagContent::Text(&format_cents(v.0, 2)),
195 ));
196 }
197 }
198 if !icms_xml.is_empty() {
199 imposto_children.push(icms_xml);
200 }
201 if !ipi_xml.is_empty() {
202 imposto_children.push(ipi_xml);
203 }
204 if let Some(ref pis_st) = item.pis_st {
206 imposto_children.push(tax_pis_cofins_ipi::build_pis_st_xml(pis_st));
207 } else {
208 imposto_children.push(pis_xml);
209 }
210 if let Some(ref cofins_st) = item.cofins_st {
212 imposto_children.push(tax_pis_cofins_ipi::build_cofins_st_xml(cofins_st));
213 } else {
214 imposto_children.push(cofins_xml);
215 }
216 if !ii_xml.is_empty() {
217 imposto_children.push(ii_xml);
218 }
219 if !issqn_xml.is_empty() {
220 imposto_children.push(issqn_xml);
221 }
222
223 if data.schema_version.is_pl010() {
226 if let Some(ref is_data) = item.is_data {
227 imposto_children.push(tax_is::build_is_xml(is_data));
228 }
229
230 if let Some(ref ibs_cbs_data) = item.ibs_cbs {
232 imposto_children.push(tax_ibs_cbs::build_ibs_cbs_xml(ibs_cbs_data));
233 }
234 }
235
236 let fc2 = |c: i64| format_cents(c, 2);
238 let fc10 = |c: i64| format_cents(c, 10);
239 let fd4 = |v: f64| format_decimal(v, 4);
240
241 let mut prod_children = vec![
242 tag("cProd", &[], TagContent::Text(&item.product_code)),
243 tag(
244 "cEAN",
245 &[],
246 TagContent::Text(item.c_ean.as_deref().unwrap_or("SEM GTIN")),
247 ),
248 ];
249 if let Some(ref cb) = item.c_barra {
250 prod_children.push(tag("cBarra", &[], TagContent::Text(cb)));
251 }
252 prod_children.push(tag(
253 "xProd",
254 &[],
255 TagContent::Text(
256 if item.item_number == 1
258 && data.environment == SefazEnvironment::Homologation
259 && data.model == InvoiceModel::Nfce
260 {
261 HOMOLOGATION_XPROD
262 } else {
263 &item.description
264 },
265 ),
266 ));
267 prod_children.push(tag("NCM", &[], TagContent::Text(&item.ncm)));
268 for nve_code in &item.nve {
269 prod_children.push(tag("NVE", &[], TagContent::Text(nve_code)));
270 }
271 if let Some(ref cest) = item.cest {
272 prod_children.push(tag("CEST", &[], TagContent::Text(cest)));
273 if let Some(ref ind) = item.cest_ind_escala {
274 prod_children.push(tag("indEscala", &[], TagContent::Text(ind)));
275 }
276 if let Some(ref fab) = item.cest_cnpj_fab {
277 prod_children.push(tag("CNPJFab", &[], TagContent::Text(fab)));
278 }
279 }
280 if let Some(ref cb) = item.c_benef {
281 prod_children.push(tag("cBenef", &[], TagContent::Text(cb)));
282 }
283 if data.schema_version.is_pl010() {
285 if let Some(ref tp) = item.tp_cred_pres_ibs_zfm {
286 prod_children.push(tag("tpCredPresIBSZFM", &[], TagContent::Text(tp)));
287 }
288 }
289 for gc in item.g_cred.iter().take(4) {
291 let p_str = format_rate4(gc.p_cred_presumido.0);
292 let mut gc_children = vec![
293 tag(
294 "cCredPresumido",
295 &[],
296 TagContent::Text(&gc.c_cred_presumido),
297 ),
298 tag("pCredPresumido", &[], TagContent::Text(&p_str)),
299 ];
300 if let Some(v) = gc.v_cred_presumido {
301 let v_str = format_cents(v.0, 2);
302 gc_children.push(tag("vCredPresumido", &[], TagContent::Text(&v_str)));
303 }
304 prod_children.push(tag("gCred", &[], TagContent::Children(gc_children)));
305 }
306 if let Some(ref ex) = item.extipi {
307 prod_children.push(tag("EXTIPI", &[], TagContent::Text(ex)));
308 }
309 prod_children.extend([
310 tag("CFOP", &[], TagContent::Text(&item.cfop)),
311 tag("uCom", &[], TagContent::Text(&item.unit_of_measure)),
312 tag("qCom", &[], TagContent::Text(&fd4(item.quantity))),
313 tag("vUnCom", &[], TagContent::Text(&fc10(item.unit_price.0))),
314 tag("vProd", &[], TagContent::Text(&fc2(item.total_price.0))),
315 tag(
316 "cEANTrib",
317 &[],
318 TagContent::Text(item.c_ean_trib.as_deref().unwrap_or("SEM GTIN")),
319 ),
320 ]);
321 if let Some(ref cbt) = item.c_barra_trib {
322 prod_children.push(tag("cBarraTrib", &[], TagContent::Text(cbt)));
323 }
324 let u_trib = item
325 .taxable_unit
326 .as_deref()
327 .unwrap_or(&item.unit_of_measure);
328 let q_trib = item.taxable_quantity.unwrap_or(item.quantity);
329 let v_un_trib = item
330 .taxable_unit_price
331 .map(|c| c.0)
332 .unwrap_or(item.unit_price.0);
333 prod_children.extend([
334 tag("uTrib", &[], TagContent::Text(u_trib)),
335 tag("qTrib", &[], TagContent::Text(&fd4(q_trib))),
336 tag("vUnTrib", &[], TagContent::Text(&fc10(v_un_trib))),
337 ]);
338 if let Some(v) = item.v_frete {
339 prod_children.push(tag("vFrete", &[], TagContent::Text(&fc2(v.0))));
340 }
341 if let Some(v) = item.v_seg {
342 prod_children.push(tag("vSeg", &[], TagContent::Text(&fc2(v.0))));
343 }
344 if let Some(v) = item.v_desc {
345 prod_children.push(tag("vDesc", &[], TagContent::Text(&fc2(v.0))));
346 }
347 if let Some(v) = item.v_outro {
348 prod_children.push(tag("vOutro", &[], TagContent::Text(&fc2(v.0))));
349 }
350 let ind_tot_str = match item.ind_tot {
351 Some(v) => v.to_string(),
352 None => "1".to_string(),
353 };
354 prod_children.push(tag("indTot", &[], TagContent::Text(&ind_tot_str)));
355 if item.ind_bem_movel_usado == Some(true) {
356 prod_children.push(tag("indBemMovelUsado", &[], TagContent::Text("1")));
357 }
358 if let Some(ref dis) = item.di {
360 for di in dis.iter().take(100) {
361 prod_children.push(build_di_xml(di));
362 }
363 }
364 if let Some(ref exports) = item.det_export {
366 for dex in exports.iter().take(500) {
367 prod_children.push(build_det_export_xml(dex));
368 }
369 }
370 if let Some(ref xped) = item.x_ped {
371 prod_children.push(tag("xPed", &[], TagContent::Text(xped)));
372 }
373 if let Some(ref nip) = item.n_item_ped {
374 prod_children.push(tag("nItemPed", &[], TagContent::Text(nip)));
375 }
376 if let Some(ref nfci) = item.n_fci {
377 prod_children.push(tag("nFCI", &[], TagContent::Text(nfci)));
378 }
379 prod_children.extend(prod_options);
380
381 let mut v_ipi_devol = 0i64;
383 let imposto_devol_xml = if let Some(ref devol) = item.imposto_devol {
384 v_ipi_devol = devol.v_ipi_devol.0;
385 let p_devol_str = format_decimal(devol.p_devol.0 as f64 / 100.0, 2);
386 let v_ipi_devol_str = format_cents(devol.v_ipi_devol.0, 2);
387 tag(
388 "impostoDevol",
389 &[],
390 TagContent::Children(vec![
391 tag("pDevol", &[], TagContent::Text(&p_devol_str)),
392 tag(
393 "IPI",
394 &[],
395 TagContent::Children(vec![tag(
396 "vIPIDevol",
397 &[],
398 TagContent::Text(&v_ipi_devol_str),
399 )]),
400 ),
401 ]),
402 )
403 } else {
404 String::new()
405 };
406
407 let v_item_xml =
410 if data.schema_version.is_pl010() && data.items.iter().any(|i| i.ibs_cbs.is_some()) {
411 let v_item_cents = if let Some(ref explicit) = item.v_item {
412 explicit.0
414 } else {
415 let v_prod = item.total_price.0;
417 let v_desc = item.v_desc.map(|c| c.0).unwrap_or(0);
418 let v_icms_deson = if item_ind_deduz_deson {
419 item.icms_v_icms_deson.map(|c| c.0).unwrap_or(0)
420 } else {
421 0
422 };
423 let v_icms_st = icms_totals.v_st.0;
424 let v_icms_mono_reten = icms_totals.v_icms_mono_reten.0;
425 let v_fcp_st = icms_totals.v_fcp_st.0;
426 let v_frete = item.v_frete.map(|c| c.0).unwrap_or(0);
427 let v_seg = item.v_seg.map(|c| c.0).unwrap_or(0);
428 let v_outro = item.v_outro.map(|c| c.0).unwrap_or(0);
429
430 v_prod - v_desc - v_icms_deson
431 + v_icms_st
432 + v_icms_mono_reten
433 + v_fcp_st
434 + v_frete
435 + v_seg
436 + v_outro
437 + v_ii
438 + v_ipi
439 + v_ipi_devol
440 + v_pis_st
441 + v_cofins_st
442 };
443 let v_item_str = format_cents(v_item_cents, 2);
444 tag("vItem", &[], TagContent::Text(&v_item_str))
445 } else {
446 String::new()
447 };
448
449 let det_extras = build_det_extras(item, &v_item_xml);
451
452 let nitem = item.item_number.to_string();
454 let mut det_children = vec![
455 tag("prod", &[], TagContent::Children(prod_children)),
456 tag("imposto", &[], TagContent::Children(imposto_children)),
457 ];
458 if !imposto_devol_xml.is_empty() {
459 det_children.push(imposto_devol_xml);
460 }
461 det_children.extend(det_extras);
462
463 let xml = tag(
464 "det",
465 &[("nItem", &nitem)],
466 TagContent::Children(det_children),
467 );
468
469 Ok(DetResult {
470 xml,
471 icms_totals,
472 v_ipi,
473 v_pis: item.pis_v_pis.map(|c| c.0).unwrap_or(0),
474 v_cofins: item.cofins_v_cofins.map(|c| c.0).unwrap_or(0),
475 v_ii,
476 v_frete: item.v_frete.map(|c| c.0).unwrap_or(0),
477 v_seg: item.v_seg.map(|c| c.0).unwrap_or(0),
478 v_desc: item.v_desc.map(|c| c.0).unwrap_or(0),
479 v_outro: item.v_outro.map(|c| c.0).unwrap_or(0),
480 ind_tot: item.ind_tot.unwrap_or(1),
481 v_tot_trib: item.v_tot_trib.map(|c| c.0).unwrap_or(0),
482 v_ipi_devol,
483 v_pis_st,
484 v_cofins_st,
485 ind_deduz_deson: item_ind_deduz_deson,
486 has_issqn,
487 })
488}