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,398 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'securerandom'
|
|
4
|
+
|
|
5
|
+
module Nfcom
|
|
6
|
+
module Models
|
|
7
|
+
# Representa uma Nota Fiscal de Comunicação (NF-COM) modelo 62.
|
|
8
|
+
#
|
|
9
|
+
# A Nota é o objeto principal da gem, responsável por agregar todas as
|
|
10
|
+
# informações necessárias para a emissão da NF-COM:
|
|
11
|
+
# emitente, destinatário, fatura, itens/serviços, totais e metadados fiscais.
|
|
12
|
+
#
|
|
13
|
+
# @example Criar uma nota completa
|
|
14
|
+
# nota = Nfcom::Models::Nota.new do |n|
|
|
15
|
+
# n.serie = 1
|
|
16
|
+
# n.numero = 1
|
|
17
|
+
#
|
|
18
|
+
# # Emitente (provedor)
|
|
19
|
+
# n.emitente = Nfcom::Models::Emitente.new(
|
|
20
|
+
# cnpj: '12345678000100',
|
|
21
|
+
# razao_social: 'Provedor Internet LTDA',
|
|
22
|
+
# inscricao_estadual: '0123456789',
|
|
23
|
+
# endereco: { ... }
|
|
24
|
+
# )
|
|
25
|
+
#
|
|
26
|
+
# # Destinatário (cliente)
|
|
27
|
+
# n.destinatario = Nfcom::Models::Destinatario.new(
|
|
28
|
+
# cpf: '12345678901',
|
|
29
|
+
# razao_social: 'João da Silva',
|
|
30
|
+
# endereco: { ... }
|
|
31
|
+
# )
|
|
32
|
+
#
|
|
33
|
+
# # Fatura (obrigatória)
|
|
34
|
+
# n.fatura = Nfcom::Models::Fatura.new(
|
|
35
|
+
# valor_liquido: 99.90,
|
|
36
|
+
# data_vencimento: Date.today + 10
|
|
37
|
+
# )
|
|
38
|
+
#
|
|
39
|
+
# # Adicionar serviços
|
|
40
|
+
# n.add_item(
|
|
41
|
+
# codigo_servico: '0303',
|
|
42
|
+
# descricao: 'Plano Fibra 100MB',
|
|
43
|
+
# classe_consumo: '0303',
|
|
44
|
+
# cfop: '5307',
|
|
45
|
+
# valor_unitario: 99.90
|
|
46
|
+
# )
|
|
47
|
+
# end
|
|
48
|
+
#
|
|
49
|
+
# @example Validar nota antes de emitir
|
|
50
|
+
# if nota.valida?
|
|
51
|
+
# puts 'Nota válida, pronta para emissão'
|
|
52
|
+
# else
|
|
53
|
+
# puts 'Erros encontrados:'
|
|
54
|
+
# nota.erros.each { |erro| puts " - #{erro}" }
|
|
55
|
+
# end
|
|
56
|
+
#
|
|
57
|
+
# @example Emitir nota
|
|
58
|
+
# # A chave de acesso é gerada automaticamente
|
|
59
|
+
# nota.gerar_chave_acesso
|
|
60
|
+
#
|
|
61
|
+
# # Enviar para a SEFAZ
|
|
62
|
+
# client = Nfcom::Client.new
|
|
63
|
+
# resultado = client.autorizar(nota)
|
|
64
|
+
#
|
|
65
|
+
# if resultado[:autorizada]
|
|
66
|
+
# puts 'Nota autorizada!'
|
|
67
|
+
# puts "Chave: #{nota.chave_acesso}"
|
|
68
|
+
# puts "Protocolo: #{nota.protocolo}"
|
|
69
|
+
# end
|
|
70
|
+
#
|
|
71
|
+
# @example Adicionar múltiplos serviços
|
|
72
|
+
# nota.add_item(codigo_servico: '0303', descricao: 'Internet', valor_unitario: 99.90)
|
|
73
|
+
# nota.add_item(codigo_servico: '0304', descricao: 'TV', valor_unitario: 79.90)
|
|
74
|
+
# # O total é recalculado automaticamente
|
|
75
|
+
#
|
|
76
|
+
# @example Nota com informações adicionais
|
|
77
|
+
# nota.informacoes_adicionais = 'Cliente isento de ICMS conforme decreto XYZ'
|
|
78
|
+
#
|
|
79
|
+
# Tipos de Emissão:
|
|
80
|
+
# - :normal (1) - Emissão normal (padrão)
|
|
81
|
+
# - :contingencia (2) - Emissão em contingência (offline)
|
|
82
|
+
#
|
|
83
|
+
# Finalidades:
|
|
84
|
+
# - :normal (0) - Nota fiscal normal (padrão)
|
|
85
|
+
# - :substituicao (3) - Nota de substituição
|
|
86
|
+
# - :ajuste (4) - Nota de ajuste
|
|
87
|
+
#
|
|
88
|
+
# Tipos de Faturamento:
|
|
89
|
+
# - :normal (0) - Faturamento padrão
|
|
90
|
+
# - :centralizado (1) - Faturamento centralizado
|
|
91
|
+
# - :cofaturamento (2) - Cofaturamento
|
|
92
|
+
#
|
|
93
|
+
# Atributos obrigatórios:
|
|
94
|
+
# - serie - Série da nota (padrão: 1)
|
|
95
|
+
# - numero - Número sequencial da nota
|
|
96
|
+
# - emitente - Provedor / empresa emissora
|
|
97
|
+
# - destinatario - Cliente / tomador do serviço
|
|
98
|
+
# - fatura - Informações de cobrança (obrigatória)
|
|
99
|
+
# - itens - Pelo menos um item/serviço
|
|
100
|
+
#
|
|
101
|
+
# Atributos opcionais:
|
|
102
|
+
# - data_emissao - Data/hora de emissão (padrão: Time.now)
|
|
103
|
+
# - tipo_emissao - Tipo de emissão (padrão: :normal)
|
|
104
|
+
# - finalidade - Finalidade da nota (padrão: :normal)
|
|
105
|
+
# - informacoes_adicionais - Texto livre (até 5.000 caracteres)
|
|
106
|
+
#
|
|
107
|
+
# Atributos preenchidos após autorização:
|
|
108
|
+
# - chave_acesso - Chave de acesso (44 dígitos)
|
|
109
|
+
# - codigo_verificacao - Código numérico (cNF, 7 dígitos – usado no XML)
|
|
110
|
+
# - protocolo - Número do protocolo SEFAZ
|
|
111
|
+
# - data_autorizacao - Data/hora da autorização
|
|
112
|
+
# - xml_autorizado - XML completo autorizado pela SEFAZ
|
|
113
|
+
#
|
|
114
|
+
# Funcionalidades automáticas:
|
|
115
|
+
# - Numeração sequencial dos itens
|
|
116
|
+
# - Recalculo automático dos totais
|
|
117
|
+
# - Geração da chave de acesso com dígito verificador
|
|
118
|
+
# - Validação completa de todos os campos obrigatórios
|
|
119
|
+
# - Validação em cascata (emitente, destinatário, fatura e itens)
|
|
120
|
+
#
|
|
121
|
+
# Validações realizadas:
|
|
122
|
+
# - Presença de série, número, emitente, destinatário e fatura
|
|
123
|
+
# - Existência de pelo menos um item
|
|
124
|
+
# - Validação completa do emitente (CNPJ, IE, endereço)
|
|
125
|
+
# - Validação completa do destinatário (CPF/CNPJ, endereço)
|
|
126
|
+
# - Validação da fatura
|
|
127
|
+
# - Validação individual de cada item
|
|
128
|
+
#
|
|
129
|
+
# @note Formato da chave de acesso (44 dígitos):
|
|
130
|
+
# UF (2) + AAMM (4) + CNPJ (14) + Modelo (2) + Série (3) +
|
|
131
|
+
# Número (9) + Tipo Emissão (1) + Código Numérico (8) + DV (1)
|
|
132
|
+
#
|
|
133
|
+
# @note Importante:
|
|
134
|
+
# - O campo cNF no XML possui 7 dígitos
|
|
135
|
+
# - Na chave de acesso, o código numérico é representado com 8 dígitos
|
|
136
|
+
class Nota # rubocop:disable Metrics/ClassLength
|
|
137
|
+
attr_accessor :serie, :numero, :data_emissao, :tipo_emissao, :fatura,
|
|
138
|
+
:finalidade, :emitente, :destinatario, :assinante, :itens, :total,
|
|
139
|
+
:chave_acesso, :codigo_verificacao,
|
|
140
|
+
:protocolo, :data_autorizacao, :xml_autorizado,
|
|
141
|
+
:competencia_fatura, :data_vencimento, :valor_liquido_fatura,
|
|
142
|
+
:chave_nfcom_substituida, :motivo_substituicao
|
|
143
|
+
|
|
144
|
+
attr_reader :metodo_pagamento, :tipo_faturamento, :informacoes_adicionais
|
|
145
|
+
|
|
146
|
+
MAX_INF_CPL = 5
|
|
147
|
+
|
|
148
|
+
def metodo_pagamento=(value)
|
|
149
|
+
if value.is_a?(Symbol)
|
|
150
|
+
unless METODOS_PAGAMENTO.key?(value)
|
|
151
|
+
raise Errors::ValidationError,
|
|
152
|
+
"Método de pagamento inválido: #{value.inspect}. " \
|
|
153
|
+
"Valores válidos: #{METODOS_PAGAMENTO.keys.join(', ')}"
|
|
154
|
+
end
|
|
155
|
+
@metodo_pagamento = METODOS_PAGAMENTO[value]
|
|
156
|
+
else
|
|
157
|
+
value_str = value.to_s
|
|
158
|
+
unless METODOS_PAGAMENTO.values.include?(value_str)
|
|
159
|
+
raise Errors::ValidationError,
|
|
160
|
+
"Método de pagamento inválido: #{value.inspect}. " \
|
|
161
|
+
"Valores válidos: #{METODOS_PAGAMENTO.values.join(', ')}"
|
|
162
|
+
end
|
|
163
|
+
@metodo_pagamento = value_str
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
|
|
167
|
+
def tipo_faturamento=(value)
|
|
168
|
+
if value.is_a?(Symbol)
|
|
169
|
+
unless TIPO_FATURAMENTO.key?(value)
|
|
170
|
+
raise Errors::ValidationError,
|
|
171
|
+
"Tipo de faturamento inválido: #{value.inspect}. " \
|
|
172
|
+
"Valores válidos: #{TIPO_FATURAMENTO.keys.join(', ')}"
|
|
173
|
+
end
|
|
174
|
+
@tipo_faturamento = TIPO_FATURAMENTO[value]
|
|
175
|
+
elsif value.is_a?(Integer) || value.is_a?(String)
|
|
176
|
+
value_int = value.to_i
|
|
177
|
+
unless TIPO_FATURAMENTO.values.include?(value_int)
|
|
178
|
+
raise Errors::ValidationError,
|
|
179
|
+
"Tipo de faturamento inválido: #{value.inspect}. " \
|
|
180
|
+
"Valores válidos: #{TIPO_FATURAMENTO.values.join(', ')}"
|
|
181
|
+
end
|
|
182
|
+
@tipo_faturamento = value_int
|
|
183
|
+
else
|
|
184
|
+
raise Errors::ValidationError,
|
|
185
|
+
'Tipo de faturamento deve ser Symbol, Integer ou String'
|
|
186
|
+
end
|
|
187
|
+
end
|
|
188
|
+
|
|
189
|
+
METODOS_PAGAMENTO = {
|
|
190
|
+
dinheiro: '01',
|
|
191
|
+
cheque: '02',
|
|
192
|
+
cartao_credito: '03',
|
|
193
|
+
cartao_debito: '04',
|
|
194
|
+
credito_loja: '05',
|
|
195
|
+
vale_alimentacao: '10',
|
|
196
|
+
vale_refeicao: '11',
|
|
197
|
+
vale_presente: '12',
|
|
198
|
+
vale_combustivel: '13',
|
|
199
|
+
boleto_bancario: '15',
|
|
200
|
+
deposito_bancario: '16',
|
|
201
|
+
pix: '17',
|
|
202
|
+
transferencia_bancaria: '18',
|
|
203
|
+
programa_fidelidade: '19',
|
|
204
|
+
sem_pagamento: '90',
|
|
205
|
+
outros: '99'
|
|
206
|
+
}.freeze
|
|
207
|
+
|
|
208
|
+
TIPO_EMISSAO = {
|
|
209
|
+
normal: 1,
|
|
210
|
+
contingencia: 2
|
|
211
|
+
}.freeze
|
|
212
|
+
|
|
213
|
+
FINALIDADE = {
|
|
214
|
+
normal: 0,
|
|
215
|
+
substituicao: 3,
|
|
216
|
+
ajuste: 4
|
|
217
|
+
}.freeze
|
|
218
|
+
|
|
219
|
+
TIPO_FATURAMENTO = {
|
|
220
|
+
normal: 0,
|
|
221
|
+
centralizado: 1,
|
|
222
|
+
cofaturamento: 2
|
|
223
|
+
}.freeze
|
|
224
|
+
|
|
225
|
+
def initialize(attributes = {})
|
|
226
|
+
@serie = Nfcom.configuration.serie_padrao || 1
|
|
227
|
+
@data_emissao = Time.now
|
|
228
|
+
@tipo_emissao = :normal
|
|
229
|
+
send(:tipo_faturamento=, :normal)
|
|
230
|
+
@finalidade = :normal
|
|
231
|
+
@itens = []
|
|
232
|
+
@total = Total.new
|
|
233
|
+
|
|
234
|
+
attributes.each do |key, value|
|
|
235
|
+
if key == :emitente && value.is_a?(Hash)
|
|
236
|
+
@emitente = Emitente.new(value)
|
|
237
|
+
elsif key == :fatura && value.is_a?(Hash)
|
|
238
|
+
@fatura = Fatura.new(value)
|
|
239
|
+
elsif key == :destinatario && value.is_a?(Hash)
|
|
240
|
+
@destinatario = Destinatario.new(value)
|
|
241
|
+
elsif respond_to?("#{key}=")
|
|
242
|
+
send("#{key}=", value)
|
|
243
|
+
end
|
|
244
|
+
end
|
|
245
|
+
|
|
246
|
+
yield self if block_given?
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
def add_item(attributes)
|
|
250
|
+
item = if attributes.is_a?(Item)
|
|
251
|
+
attributes
|
|
252
|
+
else
|
|
253
|
+
Item.new(attributes)
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
item.numero_item = itens.length + 1
|
|
257
|
+
itens << item
|
|
258
|
+
recalcular_totais
|
|
259
|
+
item
|
|
260
|
+
end
|
|
261
|
+
|
|
262
|
+
def recalcular_totais
|
|
263
|
+
@total.valor_servicos = itens.sum { |i| i.valor_total.to_f }
|
|
264
|
+
@total.valor_desconto = itens.sum { |i| i.valor_desconto.to_f }
|
|
265
|
+
@total.valor_outras_despesas = itens.sum { |i| i.valor_outras_despesas.to_f }
|
|
266
|
+
@total.calcular_total
|
|
267
|
+
end
|
|
268
|
+
|
|
269
|
+
def gerar_chave_acesso
|
|
270
|
+
# IMPORTANTE: Discrepância no schema NFCom:
|
|
271
|
+
# - Campo cNF no XML: 7 dígitos (ER2)
|
|
272
|
+
# - cNF na chave de acesso: 8 dígitos (para chave ter 44 dígitos no total)
|
|
273
|
+
# Formato da chave: UFAnoMesCNPJModSerieNumTpEmissCodNumDV
|
|
274
|
+
# UF(2) + AAMM(4) + CNPJ(14) + Mod(2) + Serie(3) + Num(9) + TpEmiss(1) + CodNum(8) + DV(1) = 44
|
|
275
|
+
# Exemplo: 26 2601 07159053000107 62 001 000009670 1 01234567 9
|
|
276
|
+
|
|
277
|
+
config = Nfcom.configuration
|
|
278
|
+
uf = config.codigo_uf
|
|
279
|
+
ano_mes = data_emissao.strftime('%y%m')
|
|
280
|
+
cnpj = emitente.cnpj.gsub(/\D/, '')
|
|
281
|
+
modelo = '62'
|
|
282
|
+
serie_fmt = serie.to_s.rjust(3, '0')
|
|
283
|
+
numero_fmt = numero.to_s.rjust(9, '0')
|
|
284
|
+
tipo_emiss = tipo_emissao_codigo.to_s
|
|
285
|
+
|
|
286
|
+
# Gera cNF com 7 dígitos (para o campo XML)
|
|
287
|
+
codigo_numerico_7 = SecureRandom.random_number(10_000_000).to_s.rjust(7, '0')
|
|
288
|
+
|
|
289
|
+
# Mas na chave usa 8 dígitos (padding à esquerda com zero)
|
|
290
|
+
codigo_numerico_8 = codigo_numerico_7.rjust(8, '0')
|
|
291
|
+
|
|
292
|
+
chave_sem_dv = "#{uf}#{ano_mes}#{cnpj}#{modelo}#{serie_fmt}#{numero_fmt}#{tipo_emiss}#{codigo_numerico_8}"
|
|
293
|
+
dv = calcular_digito_verificador(chave_sem_dv)
|
|
294
|
+
|
|
295
|
+
# Armazena o cNF de 7 dígitos (para o XML)
|
|
296
|
+
@codigo_verificacao = codigo_numerico_7
|
|
297
|
+
# Chave completa com 44 dígitos
|
|
298
|
+
@chave_acesso = "#{chave_sem_dv}#{dv}"
|
|
299
|
+
end
|
|
300
|
+
|
|
301
|
+
def tipo_emissao_codigo
|
|
302
|
+
TIPO_EMISSAO[tipo_emissao] || TIPO_EMISSAO[:normal]
|
|
303
|
+
end
|
|
304
|
+
|
|
305
|
+
def finalidade_codigo
|
|
306
|
+
FINALIDADE[finalidade] || FINALIDADE[:normal]
|
|
307
|
+
end
|
|
308
|
+
|
|
309
|
+
def valida?
|
|
310
|
+
erros.empty?
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
def erros # rubocop:disable Metrics/MethodLength
|
|
314
|
+
errors = []
|
|
315
|
+
errors << 'Série é obrigatória' if serie.nil?
|
|
316
|
+
errors << 'Número é obrigatório' if numero.nil?
|
|
317
|
+
errors << 'Emitente é obrigatório' if emitente.nil?
|
|
318
|
+
errors << 'Destinatário é obrigatório' if destinatario.nil?
|
|
319
|
+
errors << 'Deve haver pelo menos um item' if itens.empty?
|
|
320
|
+
|
|
321
|
+
# Validações de schema (formato)
|
|
322
|
+
errors << 'Série inválida (deve ser 0-999)' if serie && !serie.to_s.match?(/\A(0|[1-9]{1}[0-9]{0,2})\z/)
|
|
323
|
+
|
|
324
|
+
if numero && !numero.to_s.match?(/\A[1-9]{1}[0-9]{0,8}\z/)
|
|
325
|
+
errors << 'Número inválido (1-999999999, não pode começar com zero)'
|
|
326
|
+
end
|
|
327
|
+
|
|
328
|
+
if codigo_verificacao && codigo_verificacao.to_s.length != 7
|
|
329
|
+
errors << "cNF inválido: deve ter exatamente 7 dígitos (campo XML), tem #{codigo_verificacao.to_s.length}"
|
|
330
|
+
end
|
|
331
|
+
|
|
332
|
+
if chave_acesso && !chave_acesso.to_s.match?(/\A[0-9]{44}\z/)
|
|
333
|
+
errors << "Chave de acesso inválida (deve ter 44 dígitos, tem #{chave_acesso.to_s.length})"
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
errors.concat(emitente.erros.map { |e| "Emitente: #{e}" }) if emitente && !emitente.valido?
|
|
337
|
+
errors.concat(destinatario.erros.map { |e| "Destinatário: #{e}" }) if destinatario && !destinatario.valido?
|
|
338
|
+
|
|
339
|
+
itens.each_with_index do |item, i|
|
|
340
|
+
errors.concat(item.erros.map { |e| "Item #{i + 1}: #{e}" }) unless item.valido?
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
if fatura.nil?
|
|
344
|
+
errors << 'Fatura é obrigatória'
|
|
345
|
+
elsif !fatura.valido?
|
|
346
|
+
errors.concat(fatura.erros.map { |e| "Fatura: #{e}" })
|
|
347
|
+
end
|
|
348
|
+
|
|
349
|
+
informacoes_adicionais&.each_with_index do |texto, i|
|
|
350
|
+
errors << "Informação adicional #{i + 1} inválida." unless Validators::SchemaValidator.texto_valido?(texto)
|
|
351
|
+
end
|
|
352
|
+
|
|
353
|
+
if finalidade == :substituicao
|
|
354
|
+
if chave_nfcom_substituida.nil?
|
|
355
|
+
errors << 'Chave da NFCom substituída é obrigatória para notas de substituição'
|
|
356
|
+
end
|
|
357
|
+
errors << 'Motivo da substituição é obrigatório' if motivo_substituicao.nil?
|
|
358
|
+
unless chave_nfcom_substituida.nil? || chave_nfcom_substituida.to_s.match?(/\A[0-9]{44}\z/)
|
|
359
|
+
errors << 'Chave da NFCom substituída inválida (deve ter 44 dígitos)'
|
|
360
|
+
end
|
|
361
|
+
end
|
|
362
|
+
|
|
363
|
+
errors
|
|
364
|
+
end
|
|
365
|
+
|
|
366
|
+
def autorizada?
|
|
367
|
+
!protocolo.nil? && !data_autorizacao.nil?
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
def informacoes_adicionais=(value)
|
|
371
|
+
@informacoes_adicionais =
|
|
372
|
+
value
|
|
373
|
+
.to_s
|
|
374
|
+
.split(/\r?\n/)
|
|
375
|
+
.map(&:strip)
|
|
376
|
+
.reject(&:empty?)
|
|
377
|
+
.first(MAX_INF_CPL)
|
|
378
|
+
end
|
|
379
|
+
|
|
380
|
+
private
|
|
381
|
+
|
|
382
|
+
def calcular_digito_verificador(chave)
|
|
383
|
+
# Módulo 11
|
|
384
|
+
multiplicadores = [2, 3, 4, 5, 6, 7, 8, 9]
|
|
385
|
+
soma = 0
|
|
386
|
+
|
|
387
|
+
chave.reverse.chars.each_with_index do |digito, i|
|
|
388
|
+
soma += digito.to_i * multiplicadores[i % 8]
|
|
389
|
+
end
|
|
390
|
+
|
|
391
|
+
resto = soma % 11
|
|
392
|
+
dv = 11 - resto
|
|
393
|
+
dv = 0 if dv >= 10
|
|
394
|
+
dv
|
|
395
|
+
end
|
|
396
|
+
end
|
|
397
|
+
end
|
|
398
|
+
end
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Models
|
|
5
|
+
# Representa os valores totalizadores da NF-COM
|
|
6
|
+
#
|
|
7
|
+
# Esta classe agrega todos os valores da nota fiscal, incluindo valores
|
|
8
|
+
# de serviços, descontos, impostos e o valor total da nota.
|
|
9
|
+
#
|
|
10
|
+
# Responsabilidades:
|
|
11
|
+
# - Armazenar os valores totalizadores da nota
|
|
12
|
+
# - Calcular o valor total da NF-COM
|
|
13
|
+
# - Manter valores de impostos (preenchidos externamente ou derivados)
|
|
14
|
+
#
|
|
15
|
+
# Observações importantes:
|
|
16
|
+
# - Esta classe NÃO calcula impostos por regra fiscal
|
|
17
|
+
# - ICMS, PIS e COFINS devem ser informados pela camada de tributação
|
|
18
|
+
# - O Total apenas consolida os valores
|
|
19
|
+
#
|
|
20
|
+
class Total
|
|
21
|
+
attr_accessor :valor_servicos, :valor_desconto, :valor_outras_despesas,
|
|
22
|
+
:valor_total, :icms_base_calculo, :icms_valor,
|
|
23
|
+
:icms_desonerado, :fcp_valor,
|
|
24
|
+
:pis_valor, :cofins_valor,
|
|
25
|
+
:funttel_valor, :fust_valor,
|
|
26
|
+
:pis_retido, :cofins_retido, :csll_retido, :irrf_retido
|
|
27
|
+
|
|
28
|
+
def initialize(attributes = {})
|
|
29
|
+
@valor_desconto = 0.0
|
|
30
|
+
@valor_outras_despesas = 0.0
|
|
31
|
+
@icms_base_calculo = 0.0
|
|
32
|
+
@icms_valor = 0.0
|
|
33
|
+
@icms_desonerado = 0.0
|
|
34
|
+
@fcp_valor = 0.0
|
|
35
|
+
@pis_valor = 0.0
|
|
36
|
+
@cofins_valor = 0.0
|
|
37
|
+
@funttel_valor = 0.0
|
|
38
|
+
@fust_valor = 0.0
|
|
39
|
+
@pis_retido = 0.0
|
|
40
|
+
@cofins_retido = 0.0
|
|
41
|
+
@csll_retido = 0.0
|
|
42
|
+
@irrf_retido = 0.0
|
|
43
|
+
|
|
44
|
+
attributes.each do |key, value|
|
|
45
|
+
send("#{key}=", value) if respond_to?("#{key}=")
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def calcular_total
|
|
50
|
+
@valor_total = valor_servicos.to_f -
|
|
51
|
+
valor_desconto.to_f +
|
|
52
|
+
valor_outras_despesas.to_f
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def valor_liquido
|
|
56
|
+
valor_total.to_f
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
60
|
+
end
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Parsers
|
|
5
|
+
class Autorizacao < Base
|
|
6
|
+
def parse
|
|
7
|
+
ret = find('//nfcom:retNFCom')
|
|
8
|
+
raise Errors::NotaRejeitada.new('000', 'Resposta inválida') unless ret
|
|
9
|
+
|
|
10
|
+
c_stat = xpath(ret, './/nfcom:cStat')
|
|
11
|
+
x_motivo = xpath(ret, './/nfcom:xMotivo')
|
|
12
|
+
|
|
13
|
+
raise Errors::NotaRejeitada.new(c_stat, x_motivo) unless c_stat == '100'
|
|
14
|
+
|
|
15
|
+
prot = ret.at_xpath('.//nfcom:protNFCom', nfcom_namespaces)
|
|
16
|
+
|
|
17
|
+
{
|
|
18
|
+
autorizada: true,
|
|
19
|
+
protocolo: xpath(prot, './/nfcom:nProt'),
|
|
20
|
+
chave: xpath(prot, './/nfcom:chNFCom'),
|
|
21
|
+
data_autorizacao: xpath(prot, './/nfcom:dhRecbto'),
|
|
22
|
+
xml: prot&.to_xml,
|
|
23
|
+
mensagem: x_motivo
|
|
24
|
+
}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Parsers
|
|
5
|
+
class Base
|
|
6
|
+
attr_reader :document
|
|
7
|
+
|
|
8
|
+
def initialize(http_response)
|
|
9
|
+
@document = Nokogiri::XML(http_response)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def nfcom_namespaces
|
|
15
|
+
{
|
|
16
|
+
'soap' => 'http://www.w3.org/2003/05/soap-envelope',
|
|
17
|
+
'nfcom' => 'http://www.portalfiscal.inf.br/nfcom'
|
|
18
|
+
}
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def xpath(node, path)
|
|
22
|
+
node.at_xpath(path, nfcom_namespaces)&.text
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def find(path)
|
|
26
|
+
document.at_xpath(path, nfcom_namespaces)
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Parsers
|
|
5
|
+
class Consulta < Base
|
|
6
|
+
def parse
|
|
7
|
+
ret = find('//nfcom:retConsSitNFCom')
|
|
8
|
+
raise Errors::SefazError, 'Resposta de consulta inválida' unless ret
|
|
9
|
+
|
|
10
|
+
codigo = xpath(ret, './/nfcom:cStat')
|
|
11
|
+
motivo = xpath(ret, './/nfcom:xMotivo')
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
codigo: codigo,
|
|
15
|
+
motivo: motivo,
|
|
16
|
+
situacao: interpretar_situacao(codigo),
|
|
17
|
+
protocolo: xpath(ret, './/nfcom:protNFCom/nfcom:infProt/nfcom:nProt'),
|
|
18
|
+
data_autorizacao: xpath(ret, './/nfcom:protNFCom/nfcom:infProt/nfcom:dhRecbto')
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def interpretar_situacao(codigo)
|
|
25
|
+
case codigo.to_s
|
|
26
|
+
when '100', '150' then 'Autorizada'
|
|
27
|
+
when '110', '301', '302' then 'Denegada'
|
|
28
|
+
when '101', '151', '155' then 'Cancelada'
|
|
29
|
+
else 'Desconhecida'
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Parsers
|
|
5
|
+
class Inutilizacao < Base
|
|
6
|
+
def parse
|
|
7
|
+
ret = find('//nfcom:retInutNFCom')
|
|
8
|
+
raise Errors::SefazError, 'Resposta de inutilização inválida' unless ret
|
|
9
|
+
|
|
10
|
+
inf = ret.at_xpath('.//nfcom:infInut', nfcom_namespaces)
|
|
11
|
+
codigo = xpath(inf, './/nfcom:cStat')
|
|
12
|
+
motivo = xpath(inf, './/nfcom:xMotivo')
|
|
13
|
+
|
|
14
|
+
{
|
|
15
|
+
inutilizada: codigo.to_s == '102',
|
|
16
|
+
codigo: codigo,
|
|
17
|
+
motivo: motivo,
|
|
18
|
+
protocolo: xpath(inf, './/nfcom:nProt')
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Nfcom
|
|
4
|
+
module Parsers
|
|
5
|
+
class Status < Base
|
|
6
|
+
def parse
|
|
7
|
+
ret = find('//nfcom:retConsStatServNFCom')
|
|
8
|
+
raise Errors::SefazError, 'Resposta de status inválida' unless ret
|
|
9
|
+
|
|
10
|
+
codigo = xpath(ret, './/nfcom:cStat')
|
|
11
|
+
motivo = xpath(ret, './/nfcom:xMotivo')
|
|
12
|
+
|
|
13
|
+
{
|
|
14
|
+
online: codigo.to_s == '107',
|
|
15
|
+
codigo: codigo,
|
|
16
|
+
motivo: motivo,
|
|
17
|
+
tempo_medio: xpath(ret, './/nfcom:tMed'),
|
|
18
|
+
data_hora: xpath(ret, './/nfcom:dhRecbto')
|
|
19
|
+
}
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
end
|