nfcom 0.1.2
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.
- checksums.yaml +7 -0
- data/.rubocop.yml +115 -0
- data/.ruby-version +1 -0
- data/CHANGELOG.md +66 -0
- data/LICENSE +21 -0
- data/README.md +280 -0
- data/Rakefile +22 -0
- data/examples/.env.example +41 -0
- data/examples/emitir_nota.rb +91 -0
- data/examples/rails_initializer.rb +72 -0
- data/lib/nfcom/builder/danfe_com.rb +564 -0
- data/lib/nfcom/builder/qrcode.rb +68 -0
- data/lib/nfcom/builder/signature.rb +156 -0
- data/lib/nfcom/builder/xml_builder.rb +362 -0
- data/lib/nfcom/client.rb +106 -0
- data/lib/nfcom/configuration.rb +134 -0
- data/lib/nfcom/errors.rb +27 -0
- data/lib/nfcom/helpers/consulta.rb +28 -0
- data/lib/nfcom/models/assinante.rb +146 -0
- data/lib/nfcom/models/destinatario.rb +138 -0
- data/lib/nfcom/models/emitente.rb +105 -0
- data/lib/nfcom/models/endereco.rb +123 -0
- data/lib/nfcom/models/fatura/codigo_de_barras/formato_44.rb +52 -0
- data/lib/nfcom/models/fatura/codigo_de_barras.rb +57 -0
- data/lib/nfcom/models/fatura.rb +172 -0
- data/lib/nfcom/models/item.rb +353 -0
- data/lib/nfcom/models/nota.rb +398 -0
- data/lib/nfcom/models/total.rb +60 -0
- data/lib/nfcom/parsers/autorizacao.rb +28 -0
- data/lib/nfcom/parsers/base.rb +30 -0
- data/lib/nfcom/parsers/consulta.rb +34 -0
- data/lib/nfcom/parsers/inutilizacao.rb +23 -0
- data/lib/nfcom/parsers/status.rb +23 -0
- data/lib/nfcom/utils/certificate.rb +109 -0
- data/lib/nfcom/utils/compressor.rb +47 -0
- data/lib/nfcom/utils/helpers.rb +141 -0
- data/lib/nfcom/utils/response_decompressor.rb +47 -0
- data/lib/nfcom/utils/xml_authorized.rb +29 -0
- data/lib/nfcom/utils/xml_cleaner.rb +68 -0
- data/lib/nfcom/validators/business_rules.rb +45 -0
- data/lib/nfcom/validators/schema_validator.rb +316 -0
- data/lib/nfcom/validators/xml_validator.rb +29 -0
- data/lib/nfcom/version.rb +5 -0
- data/lib/nfcom/webservices/autorizacao.rb +36 -0
- data/lib/nfcom/webservices/base.rb +96 -0
- data/lib/nfcom/webservices/consulta.rb +59 -0
- data/lib/nfcom/webservices/inutilizacao.rb +71 -0
- data/lib/nfcom/webservices/status.rb +64 -0
- data/lib/nfcom.rb +98 -0
- data/nfcom.gemspec +42 -0
- metadata +242 -0
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
# config/initializers/nfcom.rb
|
|
4
|
+
# Configuração da gem Nfcom para emissão de NF-COM
|
|
5
|
+
|
|
6
|
+
require 'nfcom'
|
|
7
|
+
|
|
8
|
+
Nfcom.configure do |config|
|
|
9
|
+
# ============================================
|
|
10
|
+
# AMBIENTE
|
|
11
|
+
# ============================================
|
|
12
|
+
# Sempre começar em homologação!
|
|
13
|
+
# Mude para :producao apenas após testar tudo
|
|
14
|
+
config.ambiente = Rails.env.production? ? :producao : :homologacao
|
|
15
|
+
config.estado = 'PE'
|
|
16
|
+
|
|
17
|
+
# ============================================
|
|
18
|
+
# CERTIFICADO DIGITAL (OBRIGATÓRIO)
|
|
19
|
+
# ============================================
|
|
20
|
+
# Use variáveis de ambiente para não commitar senhas
|
|
21
|
+
config.certificado_path = ENV['NFCOM_CERTIFICADO_PATH'] ||
|
|
22
|
+
Rails.root.join('config', 'certificados', 'certificado.pfx').to_s
|
|
23
|
+
config.certificado_senha = ENV.fetch('NFCOM_CERTIFICADO_SENHA', nil)
|
|
24
|
+
|
|
25
|
+
# ============================================
|
|
26
|
+
# DADOS DO EMITENTE (OBRIGATÓRIO)
|
|
27
|
+
# ============================================
|
|
28
|
+
config.cnpj = ENV.fetch('NFCOM_CNPJ', nil)
|
|
29
|
+
config.razao_social = ENV.fetch('NFCOM_RAZAO_SOCIAL', nil)
|
|
30
|
+
config.inscricao_estadual = ENV.fetch('NFCOM_INSCRICAO_ESTADUAL', nil)
|
|
31
|
+
config.regime_tributario = ENV.fetch('NFCOM_REGIME_TRIBUTARIO', 1).to_i
|
|
32
|
+
# 1 = Simples Nacional
|
|
33
|
+
# 2 = Simples Nacional - Excesso de sublimite de receita bruta
|
|
34
|
+
# 3 = Regime Normal
|
|
35
|
+
|
|
36
|
+
# ============================================
|
|
37
|
+
# CONFIGURAÇÕES OPCIONAIS
|
|
38
|
+
# ============================================
|
|
39
|
+
config.serie_padrao = ENV.fetch('NFCOM_SERIE_PADRAO', 1).to_i
|
|
40
|
+
config.timeout = 30
|
|
41
|
+
config.max_tentativas = 3
|
|
42
|
+
|
|
43
|
+
# Logging
|
|
44
|
+
config.log_level = Rails.env.production? ? :info : :debug
|
|
45
|
+
config.logger = Rails.logger
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
# ============================================
|
|
49
|
+
# VALIDAÇÃO DA CONFIGURAÇÃO
|
|
50
|
+
# ============================================
|
|
51
|
+
# Valida se as configurações obrigatórias estão presentes
|
|
52
|
+
Rails.application.config.after_initialize do
|
|
53
|
+
config = Nfcom.configuration
|
|
54
|
+
|
|
55
|
+
erros = []
|
|
56
|
+
erros << 'NFCOM_CERTIFICADO_PATH não configurado' if config.certificado_path.nil?
|
|
57
|
+
erros << 'NFCOM_CERTIFICADO_SENHA não configurado' if config.certificado_senha.nil?
|
|
58
|
+
erros << 'NFCOM_CNPJ não configurado' if config.cnpj.nil?
|
|
59
|
+
erros << 'NFCOM_RAZAO_SOCIAL não configurado' if config.razao_social.nil?
|
|
60
|
+
erros << 'NFCOM_INSCRICAO_ESTADUAL não configurado' if config.inscricao_estadual.nil?
|
|
61
|
+
|
|
62
|
+
if erros.any? && Rails.env.production?
|
|
63
|
+
Rails.logger.error 'NFCOM: Configuração incompleta!'
|
|
64
|
+
erros.each { |erro| Rails.logger.error " - #{erro}" }
|
|
65
|
+
raise 'NFCOM não está configurado corretamente. Verifique as variáveis de ambiente.'
|
|
66
|
+
elsif erros.any?
|
|
67
|
+
Rails.logger.warn "NFCOM: Configuração incompleta (#{Rails.env}):"
|
|
68
|
+
erros.each { |erro| Rails.logger.warn " - #{erro}" }
|
|
69
|
+
else
|
|
70
|
+
Rails.logger.info "NFCOM: Configurado com sucesso! (#{config.ambiente} - #{config.estado})"
|
|
71
|
+
end
|
|
72
|
+
end
|
|
@@ -0,0 +1,564 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'date'
|
|
4
|
+
require 'prawn'
|
|
5
|
+
require 'prawn/measurement_extensions'
|
|
6
|
+
require 'prawn/table'
|
|
7
|
+
require 'prawn-svg'
|
|
8
|
+
|
|
9
|
+
module Nfcom
|
|
10
|
+
module Builder
|
|
11
|
+
class DanfeCom
|
|
12
|
+
include Utils::Helpers
|
|
13
|
+
|
|
14
|
+
Prawn::Fonts::AFM.hide_m17n_warning = true
|
|
15
|
+
|
|
16
|
+
attr_reader :xml_doc, :ns, :logo_path
|
|
17
|
+
|
|
18
|
+
def initialize(xml_autorizado, logo_path: nil)
|
|
19
|
+
raise Errors::XmlError, 'XML não pode ser vazio' if xml_autorizado.nil? || xml_autorizado.strip.empty?
|
|
20
|
+
|
|
21
|
+
@logo_path = logo_path
|
|
22
|
+
@xml_doc = Nokogiri::XML(xml_autorizado)
|
|
23
|
+
@ns = 'http://www.portalfiscal.inf.br/nfcom'
|
|
24
|
+
|
|
25
|
+
raise Errors::XmlError, 'XML inválido: não contém elemento NFCom' unless xml_doc.at_xpath('//xmlns:NFCom',
|
|
26
|
+
'xmlns' => ns)
|
|
27
|
+
rescue Nokogiri::XML::SyntaxError => e
|
|
28
|
+
raise Errors::XmlError, "Erro ao fazer parse do XML: #{e.message}"
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# ----------------------------
|
|
32
|
+
# MAIN PUBLIC METHODS
|
|
33
|
+
# ----------------------------
|
|
34
|
+
def gerar
|
|
35
|
+
Prawn::Document.new(page_size: 'A4', margin: [5.mm] * 4) do |pdf|
|
|
36
|
+
setup_fonts(pdf)
|
|
37
|
+
gerar_conteudo(pdf)
|
|
38
|
+
end.render
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def gerar_arquivo(filepath)
|
|
42
|
+
File.write(filepath, gerar)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
# ----------------------------
|
|
46
|
+
# PRIVATE METHODS
|
|
47
|
+
# ----------------------------
|
|
48
|
+
private
|
|
49
|
+
|
|
50
|
+
def setup_fonts(pdf)
|
|
51
|
+
pdf.font 'Helvetica'
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def gerar_conteudo(pdf)
|
|
55
|
+
ide = xml_doc.at_xpath('//xmlns:ide', 'xmlns' => ns)
|
|
56
|
+
emit = xml_doc.at_xpath('//xmlns:emit', 'xmlns' => ns)
|
|
57
|
+
dest = xml_doc.at_xpath('//xmlns:dest', 'xmlns' => ns)
|
|
58
|
+
total = xml_doc.at_xpath('//xmlns:total', 'xmlns' => ns)
|
|
59
|
+
itens = xml_doc.xpath('//xmlns:det', 'xmlns' => ns)
|
|
60
|
+
gfat = xml_doc.at_xpath('//xmlns:gFat', 'xmlns' => ns)
|
|
61
|
+
assinante = xml_doc.at_xpath('//xmlns:assinante', 'xmlns' => ns)
|
|
62
|
+
prot = xml_doc.at_xpath('//xmlns:protNFCom', 'xmlns' => ns)
|
|
63
|
+
inf_adic = xml_doc.at_xpath('//xmlns:infAdic', 'xmlns' => ns)
|
|
64
|
+
|
|
65
|
+
y_pos = pdf.cursor
|
|
66
|
+
y_pos = gerar_cabecalho(pdf, y_pos)
|
|
67
|
+
y_pos = gerar_info_emitente_documento(pdf, emit, ide, y_pos)
|
|
68
|
+
y_pos = gerar_chave_protocolo(pdf, prot, y_pos)
|
|
69
|
+
y_pos = gerar_destinatario(pdf, dest, y_pos)
|
|
70
|
+
y_pos = gerar_assinante(pdf, assinante, y_pos) if assinante
|
|
71
|
+
y_pos = gerar_faturamento(pdf, gfat, y_pos) if gfat
|
|
72
|
+
y_pos = gerar_itens(pdf, itens, y_pos)
|
|
73
|
+
gerar_totais(pdf, total, y_pos)
|
|
74
|
+
gerar_info_adicional(pdf, inf_adic) if inf_adic
|
|
75
|
+
gerar_rodape(pdf, ide, prot)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# ----------------------------
|
|
79
|
+
# HEADER / LOGO
|
|
80
|
+
# ----------------------------
|
|
81
|
+
def gerar_cabecalho(pdf, y_pos)
|
|
82
|
+
if logo_path && File.exist?(logo_path)
|
|
83
|
+
begin
|
|
84
|
+
if logo_path.to_s.downcase.end_with?('.svg')
|
|
85
|
+
renderizar_logo_svg(pdf, y_pos)
|
|
86
|
+
else
|
|
87
|
+
pdf.image logo_path.to_s, at: [0, y_pos - 2.mm], fit: [40.mm, 18.mm]
|
|
88
|
+
end
|
|
89
|
+
rescue StandardError => e
|
|
90
|
+
warn "Logo ignorado: #{e.message}"
|
|
91
|
+
end
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
title_x = logo_path && File.exist?(logo_path) ? 45.mm : 0
|
|
95
|
+
title_w = logo_path && File.exist?(logo_path) ? 155.mm : 200.mm
|
|
96
|
+
|
|
97
|
+
pdf.text_box 'DANFE-COM', at: [title_x, y_pos - 5.mm], size: 18, style: :bold, align: :center, width: title_w
|
|
98
|
+
pdf.text_box 'Documento Auxiliar da Nota Fiscal de Comunicação',
|
|
99
|
+
at: [title_x, y_pos - 12.mm], size: 10, align: :center, width: title_w
|
|
100
|
+
|
|
101
|
+
y_pos - 22.mm
|
|
102
|
+
end
|
|
103
|
+
|
|
104
|
+
def renderizar_logo_svg(pdf, y_pos)
|
|
105
|
+
raise 'prawn-svg não instalado' unless defined?(Prawn::Svg)
|
|
106
|
+
|
|
107
|
+
pdf.svg File.read(logo_path.to_s), at: [0, y_pos - 2.mm], width: 40.mm, height: 18.mm,
|
|
108
|
+
enable_web_requests: false
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
# ----------------------------
|
|
112
|
+
# EMITENTE / DESTINATARIO
|
|
113
|
+
# ----------------------------
|
|
114
|
+
def gerar_info_emitente_documento(pdf, emit, ide, y_pos)
|
|
115
|
+
pdf.stroke_rectangle [0, y_pos], 200.mm, 30.mm
|
|
116
|
+
emit_info = extrair_emitente(emit)
|
|
117
|
+
pdf.text_box 'EMITENTE', at: [2.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
118
|
+
pdf.text_box emit_info[:nome], at: [2.mm, y_pos - 6.mm], size: 10, style: :bold
|
|
119
|
+
pdf.text_box "CNPJ: #{formatar_cnpj(emit_info[:cnpj])}", at: [2.mm, y_pos - 11.mm], size: 10
|
|
120
|
+
pdf.text_box "IE: #{emit_info[:ie]}", at: [2.mm, y_pos - 16.mm], size: 10
|
|
121
|
+
pdf.text_box montar_endereco_linha(emit_info), at: [2.mm, y_pos - 21.mm], size: 10, width: 120.mm
|
|
122
|
+
pdf.text_box "#{emit_info[:municipio]}/#{emit_info[:uf]} - CEP: #{formatar_cep(emit_info[:cep])}",
|
|
123
|
+
at: [2.mm, y_pos - 26.mm], size: 10
|
|
124
|
+
|
|
125
|
+
pdf.stroke_vertical_line y_pos, y_pos - 30.mm, at: 130.mm
|
|
126
|
+
|
|
127
|
+
numero = ide.at_xpath('xmlns:nNF', 'xmlns' => ns)&.text
|
|
128
|
+
serie = ide.at_xpath('xmlns:serie', 'xmlns' => ns)&.text
|
|
129
|
+
dh_em = ide.at_xpath('xmlns:dhEmi', 'xmlns' => ns)&.text
|
|
130
|
+
fin_nf = ide.at_xpath('xmlns:finNFCom', 'xmlns' => ns)&.text
|
|
131
|
+
|
|
132
|
+
pdf.text_box 'NÚMERO', at: [132.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
133
|
+
pdf.text_box numero.to_s.rjust(6, '0'), at: [132.mm, y_pos - 7.mm], size: 14, style: :bold
|
|
134
|
+
pdf.text_box 'SÉRIE', at: [155.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
135
|
+
pdf.text_box serie, at: [155.mm, y_pos - 7.mm], size: 14, style: :bold
|
|
136
|
+
pdf.text_box 'DATA DE EMISSÃO', at: [132.mm, y_pos - 14.mm], size: 10, style: :bold
|
|
137
|
+
pdf.text_box formatar_data_hora_xml(dh_em), at: [132.mm, y_pos - 19.mm], size: 10
|
|
138
|
+
pdf.text_box tipo_documento(fin_nf), at: [132.mm, y_pos - 25.mm], size: 10, style: :bold
|
|
139
|
+
|
|
140
|
+
y_pos - 33.mm
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
def gerar_destinatario(pdf, dest, y_pos)
|
|
144
|
+
pdf.stroke_rectangle [0, y_pos], 200.mm, 28.mm
|
|
145
|
+
dest_info = extrair_destinatario(dest)
|
|
146
|
+
|
|
147
|
+
pdf.text_box 'DESTINATÁRIO / TOMADOR DO SERVIÇO', at: [2.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
148
|
+
pdf.text_box dest_info[:nome], at: [2.mm, y_pos - 7.mm], size: 12, style: :bold
|
|
149
|
+
|
|
150
|
+
doc_label = dest_info[:cnpj] ? 'CNPJ' : 'CPF'
|
|
151
|
+
doc_val = dest_info[:cnpj] || dest_info[:cpf]
|
|
152
|
+
pdf.text_box "#{doc_label}: #{formatar_cnpj_cpf(doc_val)}", at: [2.mm, y_pos - 13.mm], size: 10
|
|
153
|
+
|
|
154
|
+
ie_text = case dest_info[:ind_ie_dest]
|
|
155
|
+
when '1' then dest_info[:ie]
|
|
156
|
+
when '2' then 'ISENTO'
|
|
157
|
+
else 'NÃO CONTRIBUINTE'
|
|
158
|
+
end
|
|
159
|
+
pdf.text_box "IE: #{ie_text}", at: [70.mm, y_pos - 13.mm], size: 10
|
|
160
|
+
|
|
161
|
+
pdf.text_box montar_endereco_linha(dest_info), at: [2.mm, y_pos - 18.mm], size: 10, width: 195.mm
|
|
162
|
+
|
|
163
|
+
cidade_cep = "#{dest_info[:bairro]} - #{dest_info[:municipio]}/#{dest_info[:uf]} - " \
|
|
164
|
+
"CEP: #{formatar_cep(dest_info[:cep])}"
|
|
165
|
+
pdf.text_box cidade_cep, at: [2.mm, y_pos - 23.mm], size: 10
|
|
166
|
+
|
|
167
|
+
y_pos - 32.mm
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
# ----------------------------
|
|
171
|
+
# QR CODE
|
|
172
|
+
# ----------------------------
|
|
173
|
+
def gerar_chave_protocolo(pdf, prot, y_pos)
|
|
174
|
+
pdf.stroke_rectangle [0, y_pos], 200.mm, 30.mm
|
|
175
|
+
|
|
176
|
+
chave = extrair_chave_acesso(prot)
|
|
177
|
+
renderizar_qrcode(pdf, chave, y_pos)
|
|
178
|
+
renderizar_info_chave(pdf, chave, y_pos)
|
|
179
|
+
renderizar_info_protocolo(pdf, prot, y_pos)
|
|
180
|
+
renderizar_texto_consulta(pdf, y_pos)
|
|
181
|
+
|
|
182
|
+
y_pos - 33.mm
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
def extrair_chave_acesso(prot)
|
|
186
|
+
# Try to get chave from protocol (inside infProt)
|
|
187
|
+
chave = prot&.at_xpath('xmlns:infProt/xmlns:chNFCom', 'xmlns' => ns)&.text
|
|
188
|
+
# Fallback to extracting from infNFCom Id attribute
|
|
189
|
+
chave ||= xml_doc.at_xpath('//xmlns:infNFCom', 'xmlns' => ns)&.[]('Id')&.gsub('NFCom', '')
|
|
190
|
+
chave
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
def renderizar_qrcode(pdf, chave, y_pos)
|
|
194
|
+
return unless chave
|
|
195
|
+
|
|
196
|
+
# Use Qrcode builder to generate SVG
|
|
197
|
+
ambiente_symbol = tipo_ambiente == 1 ? :producao : :homologacao
|
|
198
|
+
qr_builder = Nfcom::Builder::Qrcode.new(chave, ambiente_symbol)
|
|
199
|
+
qr_svg = qr_builder.gerar_qrcode_svg
|
|
200
|
+
|
|
201
|
+
# Render SVG (prawn-svg converts string to SVG)
|
|
202
|
+
pdf.svg qr_svg, at: [2.mm, y_pos - 2.mm], width: 26.mm, height: 26.mm, enable_web_requests: false
|
|
203
|
+
rescue StandardError => e
|
|
204
|
+
warn "QR Code error: #{e.message}"
|
|
205
|
+
pdf.stroke_rectangle [2.mm, y_pos - 2.mm], 22.mm, 22.mm
|
|
206
|
+
pdf.text_box 'QR', at: [6.mm, y_pos - 9.mm], size: 10, style: :bold
|
|
207
|
+
end
|
|
208
|
+
|
|
209
|
+
def renderizar_info_chave(pdf, chave, y_pos)
|
|
210
|
+
pdf.text_box 'CHAVE DE ACESSO', at: [30.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
211
|
+
pdf.text_box formatar_chave_acesso(chave), at: [30.mm, y_pos - 7.mm], size: 10, width: 165.mm
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
def renderizar_info_protocolo(pdf, prot, y_pos)
|
|
215
|
+
# Protocol data is nested inside infProt
|
|
216
|
+
protocolo = prot&.at_xpath('xmlns:infProt/xmlns:nProt', 'xmlns' => ns)&.text
|
|
217
|
+
dh_recbto = prot&.at_xpath('xmlns:infProt/xmlns:dhRecbto', 'xmlns' => ns)&.text
|
|
218
|
+
|
|
219
|
+
return unless protocolo
|
|
220
|
+
|
|
221
|
+
pdf.text_box 'PROTOCOLO DE AUTORIZAÇÃO', at: [30.mm, y_pos - 13.mm], size: 10, style: :bold
|
|
222
|
+
pdf.text_box "#{protocolo} - #{formatar_data_hora_xml(dh_recbto)}", at: [30.mm, y_pos - 18.mm], size: 10
|
|
223
|
+
end
|
|
224
|
+
|
|
225
|
+
def renderizar_texto_consulta(pdf, y_pos)
|
|
226
|
+
texto = 'Consulte a autenticidade deste documento através do QR Code ou da ' \
|
|
227
|
+
'chave de acesso no site da SEFAZ'
|
|
228
|
+
pdf.text_box texto, at: [30.mm, y_pos - 24.mm], size: 8
|
|
229
|
+
end
|
|
230
|
+
|
|
231
|
+
# ----------------------------
|
|
232
|
+
# EXTRACTION METHODS
|
|
233
|
+
# ----------------------------
|
|
234
|
+
# rubocop:disable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
235
|
+
def extrair_emitente(emit)
|
|
236
|
+
return {} unless emit
|
|
237
|
+
|
|
238
|
+
{
|
|
239
|
+
cnpj: emit.at_xpath('xmlns:CNPJ', 'xmlns' => ns)&.text,
|
|
240
|
+
nome: emit.at_xpath('xmlns:xNome', 'xmlns' => ns)&.text,
|
|
241
|
+
ie: emit.at_xpath('xmlns:IE', 'xmlns' => ns)&.text,
|
|
242
|
+
logradouro: emit.at_xpath('xmlns:enderEmit/xmlns:xLgr', 'xmlns' => ns)&.text,
|
|
243
|
+
numero: emit.at_xpath('xmlns:enderEmit/xmlns:nro', 'xmlns' => ns)&.text,
|
|
244
|
+
complemento: emit.at_xpath('xmlns:enderEmit/xmlns:xCpl', 'xmlns' => ns)&.text,
|
|
245
|
+
bairro: emit.at_xpath('xmlns:enderEmit/xmlns:xBairro', 'xmlns' => ns)&.text,
|
|
246
|
+
municipio: emit.at_xpath('xmlns:enderEmit/xmlns:xMun', 'xmlns' => ns)&.text,
|
|
247
|
+
uf: emit.at_xpath('xmlns:enderEmit/xmlns:UF', 'xmlns' => ns)&.text,
|
|
248
|
+
cep: emit.at_xpath('xmlns:enderEmit/xmlns:CEP', 'xmlns' => ns)&.text
|
|
249
|
+
}
|
|
250
|
+
end
|
|
251
|
+
|
|
252
|
+
def extrair_destinatario(dest)
|
|
253
|
+
return {} unless dest
|
|
254
|
+
|
|
255
|
+
{
|
|
256
|
+
nome: dest.at_xpath('xmlns:xNome', 'xmlns' => ns)&.text,
|
|
257
|
+
cnpj: dest.at_xpath('xmlns:CNPJ', 'xmlns' => ns)&.text,
|
|
258
|
+
cpf: dest.at_xpath('xmlns:CPF', 'xmlns' => ns)&.text,
|
|
259
|
+
ie: dest.at_xpath('xmlns:IE', 'xmlns' => ns)&.text,
|
|
260
|
+
ind_ie_dest: dest.at_xpath('xmlns:indIEDest', 'xmlns' => ns)&.text,
|
|
261
|
+
logradouro: dest.at_xpath('xmlns:enderDest/xmlns:xLgr', 'xmlns' => ns)&.text,
|
|
262
|
+
numero: dest.at_xpath('xmlns:enderDest/xmlns:nro', 'xmlns' => ns)&.text,
|
|
263
|
+
complemento: dest.at_xpath('xmlns:enderDest/xmlns:xCpl', 'xmlns' => ns)&.text,
|
|
264
|
+
bairro: dest.at_xpath('xmlns:enderDest/xmlns:xBairro', 'xmlns' => ns)&.text,
|
|
265
|
+
municipio: dest.at_xpath('xmlns:enderDest/xmlns:xMun', 'xmlns' => ns)&.text,
|
|
266
|
+
uf: dest.at_xpath('xmlns:enderDest/xmlns:UF', 'xmlns' => ns)&.text,
|
|
267
|
+
cep: dest.at_xpath('xmlns:enderDest/xmlns:CEP', 'xmlns' => ns)&.text
|
|
268
|
+
}
|
|
269
|
+
end
|
|
270
|
+
# rubocop:enable Metrics/CyclomaticComplexity, Metrics/PerceivedComplexity
|
|
271
|
+
|
|
272
|
+
# ----------------------------
|
|
273
|
+
# HELPER METHODS
|
|
274
|
+
# ----------------------------
|
|
275
|
+
def montar_endereco_linha(info)
|
|
276
|
+
return '' unless info
|
|
277
|
+
|
|
278
|
+
line = "#{info[:logradouro]}, #{info[:numero]}"
|
|
279
|
+
line += " - #{info[:complemento]}" if info[:complemento] && !info[:complemento].to_s.strip.empty?
|
|
280
|
+
line += " - #{info[:bairro]}" if info[:bairro] && !info[:bairro].to_s.strip.empty?
|
|
281
|
+
line
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
def formatar_cnpj(cnpj)
|
|
285
|
+
return '' unless cnpj
|
|
286
|
+
|
|
287
|
+
cnpj.gsub!(/\D/, '')
|
|
288
|
+
return cnpj unless cnpj.length == 14
|
|
289
|
+
|
|
290
|
+
"#{cnpj[0..1]}.#{cnpj[2..4]}.#{cnpj[5..7]}/#{cnpj[8..11]}-#{cnpj[12..13]}"
|
|
291
|
+
end
|
|
292
|
+
|
|
293
|
+
def formatar_cpf(cpf)
|
|
294
|
+
return '' unless cpf
|
|
295
|
+
|
|
296
|
+
cpf.gsub!(/\D/, '')
|
|
297
|
+
return cpf unless cpf.length == 11
|
|
298
|
+
|
|
299
|
+
"#{cpf[0..2]}.#{cpf[3..5]}.#{cpf[6..8]}-#{cpf[9..10]}"
|
|
300
|
+
end
|
|
301
|
+
|
|
302
|
+
def formatar_cnpj_cpf(value)
|
|
303
|
+
return '' unless value
|
|
304
|
+
|
|
305
|
+
value.length == 14 ? formatar_cnpj(value) : formatar_cpf(value)
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
def apenas_numeros(str)
|
|
309
|
+
str.to_s.gsub(/\D/, '')
|
|
310
|
+
end
|
|
311
|
+
|
|
312
|
+
def formatar_cep(cep)
|
|
313
|
+
return '' unless cep
|
|
314
|
+
|
|
315
|
+
cep.gsub!(/\D/, '')
|
|
316
|
+
return cep unless cep.length == 8
|
|
317
|
+
|
|
318
|
+
"#{cep[0..1]}.#{cep[2..4]}-#{cep[5..7]}"
|
|
319
|
+
end
|
|
320
|
+
|
|
321
|
+
def tipo_documento(fin_nfcom)
|
|
322
|
+
case fin_nfcom
|
|
323
|
+
when '0' then 'NFCom Normal'
|
|
324
|
+
when '3' then 'NFCom de Substituição'
|
|
325
|
+
when '4' then 'NFCom de Ajuste'
|
|
326
|
+
else 'NFCom'
|
|
327
|
+
end
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
def formatar_chave_acesso(chave)
|
|
331
|
+
return '' unless chave
|
|
332
|
+
|
|
333
|
+
chave.scan(/.{4}/).join(' ')
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def tipo_ambiente
|
|
337
|
+
@tipo_ambiente ||= xml_doc.at_xpath('//xmlns:ide/xmlns:tpAmb', 'xmlns' => ns)&.text&.to_i || 2
|
|
338
|
+
end
|
|
339
|
+
|
|
340
|
+
def formatar_data_hora_xml(datetime)
|
|
341
|
+
return '' unless datetime
|
|
342
|
+
|
|
343
|
+
DateTime.parse(datetime).strftime('%d/%m/%Y %H:%M:%S')
|
|
344
|
+
rescue ArgumentError
|
|
345
|
+
datetime
|
|
346
|
+
end
|
|
347
|
+
|
|
348
|
+
# ----------------------------
|
|
349
|
+
# ASSINANTE
|
|
350
|
+
# ----------------------------
|
|
351
|
+
def gerar_assinante(pdf, assinante, y_pos)
|
|
352
|
+
pdf.stroke_rectangle [0, y_pos], 200.mm, 18.mm
|
|
353
|
+
pdf.text_box 'DADOS DO ASSINANTE', at: [2.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
354
|
+
|
|
355
|
+
cod = assinante.at_xpath('xmlns:iCodAssinante', 'xmlns' => ns)&.text
|
|
356
|
+
tipo_serv = assinante.at_xpath('xmlns:tpServUtil', 'xmlns' => ns)&.text
|
|
357
|
+
contrato = assinante.at_xpath('xmlns:nContrato', 'xmlns' => ns)&.text
|
|
358
|
+
|
|
359
|
+
pdf.text_box "Código: #{cod}", at: [2.mm, y_pos - 7.mm], size: 10
|
|
360
|
+
pdf.text_box "Tipo Serviço: #{tipo_servico_texto(tipo_serv)}", at: [50.mm, y_pos - 7.mm], size: 10
|
|
361
|
+
pdf.text_box "Contrato: #{contrato}", at: [2.mm, y_pos - 12.mm], size: 10 if contrato
|
|
362
|
+
|
|
363
|
+
y_pos - 21.mm
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def tipo_servico_texto(codigo)
|
|
367
|
+
tipos = {
|
|
368
|
+
'1' => 'Telefonia',
|
|
369
|
+
'2' => 'Comunicação de dados',
|
|
370
|
+
'3' => 'TV por Assinatura',
|
|
371
|
+
'4' => 'Provimento de acesso à Internet',
|
|
372
|
+
'5' => 'Multimídia',
|
|
373
|
+
'6' => 'Outros',
|
|
374
|
+
'7' => 'Vários'
|
|
375
|
+
}
|
|
376
|
+
tipos[codigo] || codigo
|
|
377
|
+
end
|
|
378
|
+
|
|
379
|
+
# ----------------------------
|
|
380
|
+
# FATURAMENTO
|
|
381
|
+
# ----------------------------
|
|
382
|
+
def gerar_faturamento(pdf, gfat, y_pos)
|
|
383
|
+
pdf.stroke_rectangle [0, y_pos], 200.mm, 22.mm
|
|
384
|
+
pdf.text_box 'INFORMAÇÕES DE FATURAMENTO', at: [2.mm, y_pos - 2.mm], size: 10, style: :bold
|
|
385
|
+
|
|
386
|
+
compet = gfat.at_xpath('xmlns:CompetFat', 'xmlns' => ns)&.text
|
|
387
|
+
venc = gfat.at_xpath('xmlns:dVencFat', 'xmlns' => ns)&.text
|
|
388
|
+
periodo_ini = gfat.at_xpath('xmlns:dPerUsoIni', 'xmlns' => ns)&.text
|
|
389
|
+
periodo_fim = gfat.at_xpath('xmlns:dPerUsoFim', 'xmlns' => ns)&.text
|
|
390
|
+
cod_barras = gfat.at_xpath('xmlns:codBarras', 'xmlns' => ns)&.text
|
|
391
|
+
|
|
392
|
+
pdf.text_box "Competência: #{formatar_competencia(compet)}", at: [2.mm, y_pos - 7.mm], size: 10
|
|
393
|
+
pdf.text_box "Vencimento: #{formatar_data_simples(venc)}", at: [50.mm, y_pos - 7.mm], size: 10
|
|
394
|
+
|
|
395
|
+
# Service period (if provided)
|
|
396
|
+
if periodo_ini && periodo_fim
|
|
397
|
+
periodo_texto = "#{formatar_data_simples(periodo_ini)} a #{formatar_data_simples(periodo_fim)}"
|
|
398
|
+
pdf.text_box "Período de Uso: #{periodo_texto}", at: [100.mm, y_pos - 7.mm], size: 10
|
|
399
|
+
end
|
|
400
|
+
|
|
401
|
+
if cod_barras
|
|
402
|
+
pdf.text_box 'Código de Barras:', at: [2.mm, y_pos - 13.mm], size: 10
|
|
403
|
+
pdf.text_box formatar_codigo_barras(cod_barras), at: [2.mm, y_pos - 18.mm], size: 8
|
|
404
|
+
end
|
|
405
|
+
|
|
406
|
+
y_pos - 25.mm
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def formatar_competencia(comp)
|
|
410
|
+
return '' unless comp
|
|
411
|
+
|
|
412
|
+
"#{comp[4..5]}/#{comp[0..3]}"
|
|
413
|
+
end
|
|
414
|
+
|
|
415
|
+
def formatar_data_simples(data)
|
|
416
|
+
return '' unless data
|
|
417
|
+
|
|
418
|
+
Date.parse(data).strftime('%d/%m/%Y')
|
|
419
|
+
rescue ArgumentError
|
|
420
|
+
data
|
|
421
|
+
end
|
|
422
|
+
|
|
423
|
+
def formatar_codigo_barras(codigo)
|
|
424
|
+
Nfcom::Models::Fatura::CodigoDeBarras.new(codigo).linha_digitavel
|
|
425
|
+
rescue StandardError
|
|
426
|
+
''
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
# ----------------------------
|
|
430
|
+
# ITENS
|
|
431
|
+
# ----------------------------
|
|
432
|
+
def gerar_itens(pdf, itens, y_pos)
|
|
433
|
+
y_pos -= 5.mm
|
|
434
|
+
pdf.move_cursor_to(y_pos)
|
|
435
|
+
|
|
436
|
+
pdf.text 'DISCRIMINAÇÃO DOS SERVIÇOS', size: 10, style: :bold
|
|
437
|
+
pdf.move_down 4.mm
|
|
438
|
+
|
|
439
|
+
table_data = [['Item', 'Código', 'Descrição', 'Classe', 'CFOP', 'Unid', 'Qtd', 'Vl Unit', 'Vl Total']]
|
|
440
|
+
|
|
441
|
+
itens.each do |item|
|
|
442
|
+
prod = item.at_xpath('xmlns:prod', 'xmlns' => ns)
|
|
443
|
+
table_data << [
|
|
444
|
+
item['nItem'],
|
|
445
|
+
prod.at_xpath('xmlns:cProd', 'xmlns' => ns)&.text,
|
|
446
|
+
prod.at_xpath('xmlns:xProd', 'xmlns' => ns)&.text,
|
|
447
|
+
prod.at_xpath('xmlns:cClass', 'xmlns' => ns)&.text,
|
|
448
|
+
prod.at_xpath('xmlns:CFOP', 'xmlns' => ns)&.text,
|
|
449
|
+
unidade_texto(prod.at_xpath('xmlns:uMed', 'xmlns' => ns)&.text),
|
|
450
|
+
formatar_numero(prod.at_xpath('xmlns:qFaturada', 'xmlns' => ns)&.text, 2),
|
|
451
|
+
formatar_moeda(prod.at_xpath('xmlns:vItem', 'xmlns' => ns)&.text),
|
|
452
|
+
formatar_moeda(prod.at_xpath('xmlns:vProd', 'xmlns' => ns)&.text)
|
|
453
|
+
]
|
|
454
|
+
end
|
|
455
|
+
|
|
456
|
+
pdf.table(table_data, header: true, width: 200.mm,
|
|
457
|
+
cell_style: { size: 9, padding: [3, 4], border_width: 0.5 }) do |t|
|
|
458
|
+
t.row(0).font_style = :bold
|
|
459
|
+
t.row(0).background_color = 'EEEEEE'
|
|
460
|
+
t.columns(6..8).align = :right
|
|
461
|
+
end
|
|
462
|
+
|
|
463
|
+
pdf.cursor
|
|
464
|
+
end
|
|
465
|
+
|
|
466
|
+
def unidade_texto(codigo)
|
|
467
|
+
{ '1' => 'Min', '2' => 'MB', '3' => 'GB', '4' => 'UN' }[codigo] || codigo
|
|
468
|
+
end
|
|
469
|
+
|
|
470
|
+
def formatar_numero(valor, decimais = 2)
|
|
471
|
+
return '0,00' unless valor
|
|
472
|
+
|
|
473
|
+
format("%.#{decimais}f", valor.to_f).gsub('.', ',')
|
|
474
|
+
end
|
|
475
|
+
|
|
476
|
+
def formatar_moeda(valor)
|
|
477
|
+
return 'R$ 0,00' unless valor
|
|
478
|
+
|
|
479
|
+
"R$ #{format('%.2f', valor.to_f).gsub('.', ',')}"
|
|
480
|
+
end
|
|
481
|
+
|
|
482
|
+
# ----------------------------
|
|
483
|
+
# TOTAIS
|
|
484
|
+
# ----------------------------
|
|
485
|
+
def gerar_totais(pdf, total, y_pos)
|
|
486
|
+
pdf.move_cursor_to(y_pos)
|
|
487
|
+
pdf.move_down 3.mm
|
|
488
|
+
|
|
489
|
+
icms_tot = total.at_xpath('xmlns:ICMSTot', 'xmlns' => ns)
|
|
490
|
+
|
|
491
|
+
totals_data = [
|
|
492
|
+
['Base Cálculo ICMS', formatar_moeda(icms_tot&.at_xpath('xmlns:vBC', 'xmlns' => ns)&.text)],
|
|
493
|
+
['Valor ICMS', formatar_moeda(icms_tot&.at_xpath('xmlns:vICMS', 'xmlns' => ns)&.text)],
|
|
494
|
+
['Valor PIS', formatar_moeda(total.at_xpath('xmlns:vPIS', 'xmlns' => ns)&.text)],
|
|
495
|
+
['Valor COFINS', formatar_moeda(total.at_xpath('xmlns:vCOFINS', 'xmlns' => ns)&.text)],
|
|
496
|
+
['Desconto', formatar_moeda(total.at_xpath('xmlns:vDesc', 'xmlns' => ns)&.text)],
|
|
497
|
+
['Outras Despesas', formatar_moeda(total.at_xpath('xmlns:vOutro', 'xmlns' => ns)&.text)],
|
|
498
|
+
['VALOR TOTAL', formatar_moeda(total.at_xpath('xmlns:vNF', 'xmlns' => ns)&.text)]
|
|
499
|
+
]
|
|
500
|
+
|
|
501
|
+
pdf.float do
|
|
502
|
+
pdf.table(totals_data, position: :right, width: 75.mm,
|
|
503
|
+
cell_style: { size: 10, padding: [3, 5], border_width: 0.5 },
|
|
504
|
+
column_widths: { 0 => 37.mm, 1 => 38.mm }) do |t|
|
|
505
|
+
t.column(1).align = :right
|
|
506
|
+
t.row(-1).font_style = :bold
|
|
507
|
+
t.row(-1).background_color = 'EEEEEE'
|
|
508
|
+
t.row(-1).size = 11
|
|
509
|
+
end
|
|
510
|
+
end
|
|
511
|
+
|
|
512
|
+
pdf.move_down 40.mm
|
|
513
|
+
pdf.cursor
|
|
514
|
+
end
|
|
515
|
+
|
|
516
|
+
# ----------------------------
|
|
517
|
+
# INFORMAÇÕES ADICIONAIS
|
|
518
|
+
# ----------------------------
|
|
519
|
+
def gerar_info_adicional(pdf, inf_adic)
|
|
520
|
+
inf_cpl = inf_adic.xpath('xmlns:infCpl', 'xmlns' => ns)
|
|
521
|
+
return unless inf_cpl.any?
|
|
522
|
+
|
|
523
|
+
pdf.move_down 3.mm
|
|
524
|
+
pdf.text 'INFORMAÇÕES COMPLEMENTARES', size: 10, style: :bold
|
|
525
|
+
pdf.move_down 2.mm
|
|
526
|
+
|
|
527
|
+
inf_cpl.each do |node|
|
|
528
|
+
text = node.text.to_s.strip
|
|
529
|
+
next if text.empty?
|
|
530
|
+
|
|
531
|
+
pdf.text text, size: 9
|
|
532
|
+
pdf.move_down 2.mm
|
|
533
|
+
end
|
|
534
|
+
end
|
|
535
|
+
|
|
536
|
+
# ----------------------------
|
|
537
|
+
# RODAPÉ
|
|
538
|
+
# ----------------------------
|
|
539
|
+
def gerar_rodape(pdf, ide, prot)
|
|
540
|
+
pdf.move_down 5.mm
|
|
541
|
+
|
|
542
|
+
c_stat = prot&.at_xpath('xmlns:cStat', 'xmlns' => ns)&.text
|
|
543
|
+
if c_stat == '100'
|
|
544
|
+
pdf.fill_color '006400'
|
|
545
|
+
pdf.text 'NOTA FISCAL AUTORIZADA', size: 11, style: :bold, align: :center
|
|
546
|
+
pdf.fill_color '000000'
|
|
547
|
+
elsif c_stat
|
|
548
|
+
pdf.fill_color 'FF0000'
|
|
549
|
+
pdf.text "STATUS: #{c_stat}", size: 11, style: :bold, align: :center
|
|
550
|
+
pdf.fill_color '000000'
|
|
551
|
+
end
|
|
552
|
+
|
|
553
|
+
tp_amb = ide.at_xpath('xmlns:tpAmb', 'xmlns' => ns)&.text
|
|
554
|
+
return unless tp_amb == '2'
|
|
555
|
+
|
|
556
|
+
pdf.move_down 3.mm
|
|
557
|
+
pdf.fill_color 'FF0000'
|
|
558
|
+
pdf.text 'DOCUMENTO EMITIDO EM AMBIENTE DE HOMOLOGAÇÃO - SEM VALOR FISCAL',
|
|
559
|
+
size: 10, style: :bold, align: :center
|
|
560
|
+
pdf.fill_color '000000'
|
|
561
|
+
end
|
|
562
|
+
end
|
|
563
|
+
end
|
|
564
|
+
end
|