br_danfe 0.11.2 → 0.12.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +3 -1
  3. data/Gemfile.lock +31 -31
  4. data/br_danfe.gemspec +6 -5
  5. data/config/locales/pt-BR.yml +8 -0
  6. data/lib/br_danfe/danfe.rb +2 -2
  7. data/lib/br_danfe/danfe_lib/det_body.rb +1 -1
  8. data/lib/br_danfe/danfe_lib/document.rb +1 -1
  9. data/lib/br_danfe/danfe_lib/emit_header.rb +2 -2
  10. data/lib/br_danfe/danfe_lib/helper.rb +1 -27
  11. data/lib/br_danfe/danfe_lib/infadic.rb +1 -1
  12. data/lib/br_danfe/danfe_lib/xprod.rb +15 -15
  13. data/lib/br_danfe/danfe_nfce.rb +43 -0
  14. data/lib/br_danfe/danfe_nfce_lib/document.rb +35 -0
  15. data/lib/br_danfe/danfe_nfce_lib/footer.rb +15 -0
  16. data/lib/br_danfe/danfe_nfce_lib/header.rb +67 -0
  17. data/lib/br_danfe/danfe_nfce_lib/helper.rb +9 -0
  18. data/lib/br_danfe/danfe_nfce_lib/key.rb +18 -0
  19. data/lib/br_danfe/danfe_nfce_lib/nfce_identification.rb +20 -0
  20. data/lib/br_danfe/danfe_nfce_lib/product_list.rb +100 -0
  21. data/lib/br_danfe/danfe_nfce_lib/qr_code.rb +23 -0
  22. data/lib/br_danfe/danfe_nfce_lib/recipient.rb +72 -0
  23. data/lib/br_danfe/danfe_nfce_lib/total_list.rb +78 -0
  24. data/lib/br_danfe/helper.rb +29 -0
  25. data/lib/br_danfe/{danfe_lib/options.rb → logo_config.rb} +2 -2
  26. data/lib/br_danfe/{danfe_lib/logo_options.rb → logo_options.rb} +2 -2
  27. data/lib/br_danfe/version.rb +1 -1
  28. data/lib/br_danfe/xml.rb +36 -0
  29. data/spec/features/danfe_nfce_spec.rb +31 -0
  30. data/spec/fixtures/nfce/lib/document#render.pdf +99 -0
  31. data/spec/fixtures/nfce/lib/footer#render.pdf +74 -0
  32. data/spec/fixtures/nfce/lib/header#render-homologation.pdf +0 -0
  33. data/spec/fixtures/nfce/lib/header#render-long_name_with_logo.pdf +0 -0
  34. data/spec/fixtures/nfce/lib/header#render-long_name_without_logo.pdf +118 -0
  35. data/spec/fixtures/nfce/lib/header#render-short_name_with_logo.pdf +0 -0
  36. data/spec/fixtures/nfce/lib/header#render-short_name_without_logo.pdf +118 -0
  37. data/spec/fixtures/nfce/lib/key#render.pdf +104 -0
  38. data/spec/fixtures/nfce/lib/nfce_identification#render.pdf +118 -0
  39. data/spec/fixtures/nfce/lib/product_list#render-with_many_products.pdf +307 -0
  40. data/spec/fixtures/nfce/lib/product_list#render-with_many_products_and_long_names.pdf +335 -0
  41. data/spec/fixtures/nfce/lib/product_list#render-with_single_product.pdf +181 -0
  42. data/spec/fixtures/nfce/lib/product_list#render-with_single_product_and_long_name.pdf +195 -0
  43. data/spec/fixtures/nfce/lib/product_list_with_many_products.xml +367 -0
  44. data/spec/fixtures/nfce/lib/product_list_with_many_products_and_long_names.xml +367 -0
  45. data/spec/fixtures/nfce/lib/product_list_with_single_product.xml +238 -0
  46. data/spec/fixtures/nfce/lib/product_list_with_single_product_and_long_name.xml +238 -0
  47. data/spec/fixtures/nfce/lib/qr_code#render.pdf +0 -0
  48. data/spec/fixtures/nfce/lib/recipient#render-company.pdf +95 -0
  49. data/spec/fixtures/nfce/lib/recipient#render-consumer_without_document.pdf +88 -0
  50. data/spec/fixtures/nfce/lib/recipient#render-foreign.pdf +95 -0
  51. data/spec/fixtures/nfce/lib/recipient#render-individual.pdf +95 -0
  52. data/spec/fixtures/nfce/lib/recipient#render-unidentified_consumer.pdf +81 -0
  53. data/spec/fixtures/nfce/lib/total_list#does_not_render-payment_methods.pdf +139 -0
  54. data/spec/fixtures/nfce/lib/total_list#render-grouped_payment_methods.pdf +188 -0
  55. data/spec/fixtures/nfce/lib/total_list#render-totals.pdf +139 -0
  56. data/spec/fixtures/nfce/lib/total_list#render-without_payment.pdf +202 -0
  57. data/spec/fixtures/nfce/lib/total_list#render.pdf +202 -0
  58. data/spec/fixtures/nfce/v4.00/nfce.xml +367 -0
  59. data/spec/fixtures/nfce/v4.00/rendered_nfce.fixture.pdf +0 -0
  60. data/spec/fixtures/nfce/v4.00/saved_nfce.fixture.pdf +0 -0
  61. data/spec/lib/danfe_lib/dest_spec.rb +1 -1
  62. data/spec/lib/danfe_lib/det_body_spec.rb +1 -1
  63. data/spec/lib/danfe_lib/document_spec.rb +1 -1
  64. data/spec/lib/danfe_lib/dup_spec.rb +1 -1
  65. data/spec/lib/danfe_lib/emit_header_spec.rb +1 -1
  66. data/spec/lib/danfe_lib/helper_spec.rb +0 -54
  67. data/spec/lib/danfe_lib/icmstot_spec.rb +1 -1
  68. data/spec/lib/danfe_lib/infadic_spec.rb +1 -1
  69. data/spec/lib/danfe_lib/infadic_vol_spec.rb +1 -1
  70. data/spec/lib/danfe_lib/issqn_spec.rb +1 -1
  71. data/spec/lib/danfe_lib/ticket_spec.rb +1 -1
  72. data/spec/lib/danfe_lib/transp_spec.rb +1 -1
  73. data/spec/lib/danfe_lib/vol_spec.rb +1 -1
  74. data/spec/lib/danfe_nfce_lib/document_spec.rb +19 -0
  75. data/spec/lib/danfe_nfce_lib/footer_spec.rb +42 -0
  76. data/spec/lib/danfe_nfce_lib/header_spec.rb +117 -0
  77. data/spec/lib/danfe_nfce_lib/helper_spec.rb +28 -0
  78. data/spec/lib/danfe_nfce_lib/key_spec.rb +45 -0
  79. data/spec/lib/danfe_nfce_lib/nfce_identification_spec.rb +48 -0
  80. data/spec/lib/danfe_nfce_lib/product_list_spec.rb +66 -0
  81. data/spec/lib/danfe_nfce_lib/qr_code_spec.rb +35 -0
  82. data/spec/lib/danfe_nfce_lib/recipient_spec.rb +163 -0
  83. data/spec/lib/danfe_nfce_lib/total_list_spec.rb +188 -0
  84. data/spec/lib/helper_spec.rb +102 -0
  85. data/spec/lib/logo_config_spec.rb +21 -0
  86. data/spec/lib/{danfe_lib/logo_options_spec.rb → logo_options_spec.rb} +8 -22
  87. data/spec/lib/{danfe_lib/xml_spec.rb → xml_spec.rb} +2 -2
  88. metadata +87 -18
  89. data/lib/br_danfe/danfe_lib/xml.rb +0 -38
  90. data/spec/lib/danfe_lib/options_spec.rb +0 -17
@@ -0,0 +1,9 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class Helper
4
+ def self.address(ender_tag)
5
+ "#{ender_tag.css('xLgr').text}, #{ender_tag.css('nro').text}, #{ender_tag.css('xBairro').text}, #{ender_tag.css('xMun').text} - #{ender_tag.css('UF').text}"
6
+ end
7
+ end
8
+ end
9
+ end
@@ -0,0 +1,18 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class Key
4
+ def initialize(pdf, xml)
5
+ @pdf = pdf
6
+ @xml = xml
7
+ end
8
+
9
+ def render
10
+ @pdf.render_blank_line
11
+
12
+ @pdf.text 'Consulte pela Chave de Acesso em', size: 7, align: :center, style: :bold
13
+ @pdf.text @xml['urlChave'], size: 7, align: :center
14
+ @pdf.text @xml['chNFe'].gsub(/(\d)(?=(\d\d\d\d)+(?!\d))/, '\\1 '), size: 6, align: :center
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,20 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class NfceIdentification
4
+ def initialize(pdf, xml)
5
+ @pdf = pdf
6
+ @xml = xml
7
+ end
8
+
9
+ def render
10
+ emitted_at = BrDanfe::Helper.format_datetime(@xml['ide/dhEmi'])
11
+ identification = "NFC-e nº #{@xml['ide/nNF']} Série #{@xml['ide/serie']} #{emitted_at}"
12
+
13
+ @pdf.render_blank_line
14
+ @pdf.text identification, size: 7, align: :center, style: :bold
15
+ @pdf.text "<b>Protocolo de autorização:</b> #{@xml['infProt/nProt']}", size: 7, align: :center, inline_format: true
16
+ @pdf.text "<b>Data de autorização: </b> #{BrDanfe::Helper.format_datetime(@xml['infProt/dhRecbto'])}", size: 7, align: :center, inline_format: true
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,100 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class ProductList
4
+ def initialize(pdf, xml)
5
+ @pdf = pdf
6
+ @xml = xml
7
+ end
8
+
9
+ def render
10
+ render_headers(headers)
11
+ render_products(products)
12
+ end
13
+
14
+ private
15
+
16
+ def headers
17
+ [
18
+ header_column('Código', :left),
19
+ header_column('Descrição', :left),
20
+ header_column('Qtde', :right),
21
+ header_column('UN', :right),
22
+ header_column('Vl Unit', :right),
23
+ header_column('Vl Total', :right)
24
+ ]
25
+ end
26
+
27
+ def header_column(title, align)
28
+ { content: title, options: { align: align, size: 6, style: :bold } }
29
+ end
30
+
31
+ def products
32
+ @xml.collect('xmlns', 'det') { |det| product(det) }
33
+ end
34
+
35
+ def product(det)
36
+ [
37
+ cell_text(det.css('prod/cProd').text),
38
+ cell_text(det.css('prod/xProd').text),
39
+ cell_number(BrDanfe::Helper.numerify(det.css('prod/qCom').text)),
40
+ cell_text(det.css('prod/uCom').text, { align: :right }),
41
+ cell_number(BrDanfe::Helper.numerify(det.css('prod/vUnCom').text)),
42
+ cell_number(BrDanfe::Helper.numerify(det.css('prod/vProd').text))
43
+ ]
44
+ end
45
+
46
+ def cell_text(text, options = {})
47
+ cell = { content: text, options: { border_width: 0, size: 6 } }
48
+ cell[:options].merge!(options)
49
+ cell
50
+ end
51
+
52
+ def cell_number(text)
53
+ cell_text(text, { align: :right })
54
+ end
55
+
56
+ def columns
57
+ [
58
+ { width: 0.9.cm, position: 0 },
59
+ { width: 2.6.cm, position: 0.9.cm },
60
+ { width: 1.1.cm, position: 3.5.cm },
61
+ { width: 0.4.cm, position: 4.6.cm },
62
+ { width: 1.2.cm, position: 5.cm },
63
+ { width: 1.2.cm, position: 6.2.cm }
64
+ ]
65
+ end
66
+
67
+ def render_headers(headers)
68
+ 2.times { @pdf.render_blank_line }
69
+ cursor = @pdf.cursor
70
+ headers.each_with_index do |header, index|
71
+ @pdf.bounding_box [columns[index][:position], cursor], width: columns[index][:width], height: 0.2.cm do
72
+ @pdf.text header[:content], header[:options]
73
+ end
74
+ end
75
+ end
76
+
77
+ def render_products(products)
78
+ @pdf.render_blank_line
79
+ index_of_product_name = 1
80
+ products.each do |product|
81
+ box_height = box_height(product[index_of_product_name][:content])
82
+ cursor = @pdf.cursor
83
+
84
+ product.each_with_index do |product, index|
85
+ @pdf.bounding_box [columns[index][:position], cursor], width: columns[index][:width], height: box_height do
86
+ @pdf.text product[:content], product[:options]
87
+ end
88
+ end
89
+ end
90
+ end
91
+
92
+ def box_height(content)
93
+ line_height_base = 0.23.cm
94
+
95
+ lines = content.scan(/[\s\S]{1,20}( |$)/).length
96
+ line_height_base * lines
97
+ end
98
+ end
99
+ end
100
+ end
@@ -0,0 +1,23 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class QrCode
4
+ require 'rqrcode'
5
+ require 'chunky_png'
6
+ require 'tempfile'
7
+
8
+ def initialize(pdf, xml)
9
+ @pdf = pdf
10
+ @xml = xml
11
+ end
12
+
13
+ def render
14
+ qrcode = RQRCode::QRCode.new(@xml['qrCode'])
15
+ image = Tempfile.create('qrcode.png')
16
+ image.write(qrcode.as_png(module_px_size: 12).to_s)
17
+
18
+ box_size = 3.cm
19
+ @pdf.image image, { width: box_size, height: box_size, position: :center }
20
+ end
21
+ end
22
+ end
23
+ end
@@ -0,0 +1,72 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class Recipient
4
+ def initialize(pdf, xml)
5
+ @pdf = pdf
6
+ @xml = xml
7
+ end
8
+
9
+ def render
10
+ @pdf.render_blank_line
11
+
12
+ if identified_recipient?
13
+ render_document
14
+
15
+ @pdf.text @xml['dest/xNome'], options
16
+ @pdf.text BrDanfe::DanfeNfceLib::Helper.address(@xml.css('enderDest')), options
17
+ else
18
+ @pdf.text 'CONSUMIDOR NÃO IDENTIFICADO', options
19
+ end
20
+ end
21
+
22
+ private
23
+
24
+ def options
25
+ { size: 7, align: :center }
26
+ end
27
+
28
+ def identified_recipient?
29
+ @xml['dest/xNome'].present?
30
+ end
31
+
32
+ def render_document
33
+ document_text = document
34
+
35
+ @pdf.text document_text, options if document_text.present?
36
+ end
37
+
38
+ def document
39
+ return company if company?
40
+ return individual if individual?
41
+ return foreign if foreign?
42
+ return ''
43
+ end
44
+
45
+ def company?
46
+ @xml['dest/CNPJ'].present?
47
+ end
48
+
49
+ def company
50
+ cnpj = BrDocuments::CnpjCpf::Cnpj.new @xml['dest/CNPJ']
51
+ "CONSUMIDOR CNPJ: #{cnpj.formatted}"
52
+ end
53
+
54
+ def individual?
55
+ @xml['dest/CPF'].present?
56
+ end
57
+
58
+ def individual
59
+ cpf = BrDocuments::CnpjCpf::Cpf.new(@xml['dest/CPF'])
60
+ "CONSUMIDOR CPF: #{cpf.formatted}"
61
+ end
62
+
63
+ def foreign?
64
+ @xml['dest/idEstrangeiro'].present?
65
+ end
66
+
67
+ def foreign
68
+ "CONSUMIDOR Id. Estrangeiro: #{@xml['dest/idEstrangeiro']}"
69
+ end
70
+ end
71
+ end
72
+ end
@@ -0,0 +1,78 @@
1
+ module BrDanfe
2
+ module DanfeNfceLib
3
+ class TotalList
4
+ require 'bigdecimal'
5
+
6
+ def initialize(pdf, xml)
7
+ @pdf = pdf
8
+ @xml = xml
9
+ end
10
+
11
+ def render
12
+ subtotal
13
+ totals
14
+ payment_methods
15
+ end
16
+
17
+ private
18
+
19
+ def subtotal
20
+ cursor = @pdf.cursor
21
+ @pdf.bounding_box [4.8.cm, cursor], width: 1.4.cm, height: 0.2.cm do
22
+ @pdf.text 'Subtotal R$', { size: 6, align: :left }
23
+ end
24
+ @pdf.bounding_box [6.2.cm, cursor], width: 1.2.cm, height: 0.2.cm do
25
+ @pdf.text BrDanfe::Helper.numerify(@xml['ICMSTot > vProd'].to_f), { size: 6, align: :right }
26
+ end
27
+ end
28
+
29
+ def totals
30
+ @pdf.render_blank_line
31
+
32
+ cursor = @pdf.cursor
33
+ print_text('Qtde. total de itens', cursor, { size: 7, align: :left })
34
+ print_text(@xml.css('det').count.to_s, cursor, { size: 7, align: :right })
35
+
36
+ cursor = @pdf.cursor
37
+ print_text('Desconto R$', cursor, { size: 7, align: :left})
38
+ print_text(BrDanfe::Helper.numerify(@xml['ICMSTot > vDesc'].to_f), cursor, { size: 7, align: :right })
39
+
40
+ cursor = @pdf.cursor
41
+ print_text('Valor Total R$', cursor, { size: 7, align: :left, style: :bold })
42
+ print_text(BrDanfe::Helper.numerify(@xml['ICMSTot > vNF'].to_f), cursor, { size: 7, align: :right, style: :bold })
43
+ end
44
+
45
+ def payment_methods
46
+ payments = {}
47
+ without_payment = '90'
48
+
49
+ @xml.css('detPag').each do |detPag|
50
+ next if detPag.css('tPag').text == without_payment
51
+ payments[detPag.css('tPag').text] ||= BigDecimal('0')
52
+ actual_payment_value = BigDecimal(detPag.css('vPag').text)
53
+ payments[detPag.css('tPag').text] += actual_payment_value
54
+ end
55
+
56
+ if payments.present?
57
+ @pdf.render_blank_line
58
+
59
+ cursor = @pdf.cursor
60
+ print_text('Forma de pagamento', cursor, { size: 7, align: :left, style: :bold })
61
+ print_text('Valor pago R$', cursor, { size: 7, align: :right, style: :bold })
62
+
63
+ payments.each do |key, value|
64
+ cursor = @pdf.cursor
65
+ print_text(I18n.t("nfce.payment_methods.#{key}"), cursor, { size: 7, align: :left })
66
+ print_text(BrDanfe::Helper.numerify(value.to_f), cursor, { size: 7, align: :right })
67
+ end
68
+ end
69
+ end
70
+
71
+ def print_text(text, cursor, options)
72
+ @pdf.bounding_box [0, cursor], width: 7.4.cm, height: 0.25.cm do
73
+ @pdf.text text, options
74
+ end
75
+ end
76
+ end
77
+ end
78
+ end
@@ -0,0 +1,29 @@
1
+ module BrDanfe
2
+ class Helper
3
+ def self.homologation?(xml)
4
+ xml.css('nfeProc/NFe/infNFe/ide/tpAmb').text == '2'
5
+ end
6
+
7
+ def self.numerify(number)
8
+ return '' if !number || number == ''
9
+
10
+ separated_number = number.to_s.split('.')
11
+ integer_part = separated_number[0].reverse.gsub(/\d{3}(?=\d)/, '\&.').reverse
12
+ decimal_part = separated_number[1] || '00'
13
+ decimal_part += '0' if decimal_part.size < 2
14
+
15
+ integer_part + ',' + decimal_part
16
+ end
17
+
18
+ def self.format_datetime(xml_datetime)
19
+ formated = ''
20
+
21
+ unless xml_datetime.empty?
22
+ date = DateTime.strptime(xml_datetime, '%Y-%m-%dT%H:%M:%S')
23
+ formated = date.strftime('%d/%m/%Y %H:%M:%S')
24
+ end
25
+
26
+ formated
27
+ end
28
+ end
29
+ end
@@ -1,6 +1,6 @@
1
1
  module BrDanfe
2
- module DanfeLib
3
- class Options < OpenStruct
2
+ module Logo
3
+ class Config < OpenStruct
4
4
  DEFAULTOPTIONS = { logo: '', logo_dimensions: {} }.freeze
5
5
 
6
6
  def initialize(new_options = {})
@@ -1,6 +1,6 @@
1
1
  module BrDanfe
2
- module DanfeLib
3
- class LogoOptions
2
+ module Logo
3
+ class Options
4
4
  def initialize(bounding_box_size, logo_dimensions)
5
5
  @bounding_box_size = bounding_box_size
6
6
  @logo_width = logo_dimensions[:width]
@@ -1,3 +1,3 @@
1
1
  module BrDanfe
2
- VERSION = '0.11.2'.freeze
2
+ VERSION = '0.12.0'.freeze
3
3
  end
@@ -0,0 +1,36 @@
1
+ module BrDanfe
2
+ class XML
3
+ def css(xpath)
4
+ @xml.css(xpath)
5
+ end
6
+
7
+ def initialize(xml)
8
+ @xml = Nokogiri::XML(xml)
9
+ end
10
+
11
+ def [](xpath)
12
+ node = @xml.css(xpath)
13
+ node ? node.text : ''
14
+ end
15
+
16
+ def collect(ns, tag)
17
+ result = []
18
+ # With namespace
19
+ begin
20
+ @xml.xpath("//#{ns}:#{tag}").each do |det|
21
+ result << yield(det)
22
+ end
23
+ rescue StandardError
24
+ # Without namespace
25
+ @xml.xpath("//#{tag}").each do |det|
26
+ result << yield(det)
27
+ end
28
+ end
29
+ result
30
+ end
31
+
32
+ def version_is_310_or_newer?
33
+ @xml.css('infNFe').attr('versao').to_s.to_f >= 3.10
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,31 @@
1
+ require 'spec_helper'
2
+
3
+ describe BrDanfe::DanfeNfce do
4
+ let(:output_pdf) { "#{base_dir}output.pdf" }
5
+ let(:base_dir) { './spec/fixtures/nfce/v4.00/' }
6
+ let(:danfe) { BrDanfe::DanfeNfce.new(File.read("#{base_dir}nfce.xml")) }
7
+
8
+ before do
9
+ danfe.options.logo = 'spec/fixtures/logo.png'
10
+ danfe.options.logo_dimensions = { width: 100, height: 100 }
11
+ end
12
+
13
+ describe '#render_pdf' do
14
+ it 'renders the NFC-e pdf' do
15
+ expected = IO.binread("#{base_dir}rendered_nfce.fixture.pdf")
16
+ expect(danfe.render_pdf).to eq expected
17
+ end
18
+ end
19
+
20
+ describe '#save_pdf' do
21
+ before { File.delete(output_pdf) if File.exist?(output_pdf) }
22
+ after { File.delete(output_pdf) if File.exist?(output_pdf) }
23
+
24
+ it 'saves the NFC-e as pdf' do
25
+ expect(File.exist?(output_pdf)).to be_falsey
26
+ danfe.save_pdf output_pdf
27
+
28
+ expect("#{base_dir}saved_nfce.fixture.pdf").to have_same_content_of file: output_pdf
29
+ end
30
+ end
31
+ end