facturacr 0.1.4 → 1.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.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: aea851ecee75d42e2a0b8f958ead2ac23d0a4cb1
4
- data.tar.gz: 1484b00510fe834c3c29eab1cb5115504cd2512b
3
+ metadata.gz: db381bbaa82171a178d543ba768b109dda40ce9b
4
+ data.tar.gz: 3dd2171473e12c9f578c9d21d12d18fe600d587d
5
5
  SHA512:
6
- metadata.gz: e1c76bad034b6de2b6530f4c1bbf0304dff3b70491deceaad4734cb580f5845bfd9ad3e4a4150154caca4f1eefe071eabf42bd6701a0139716d8a73fb3a150d9
7
- data.tar.gz: 20219ed607264ac15ecc9c65fcf66e00c477b3a379a73736b9b04213287673b78e0073c494ae5a032bad37697cce717a91f0271f3e9214b0f5d6d9679149fc49
6
+ metadata.gz: 86db7abd0fc67508f335f56de8d561a7fdbc7bc55147821a2bb07c919f3d2f7e0d5159d6edcf2e971c9914677f11d4d58e1c6b50e533a6b669d028ea220252db
7
+ data.tar.gz: f16d24d241f22cc0e70d4672d46528dffbbe1b6e7fb85d21add0754635658c2d39341de002aff4192a0128ce9130db47c4ca0376d01c3e689431387a83bedf93
data/README.md CHANGED
@@ -5,7 +5,7 @@ Esta librería implementa los procesos de facturación electrónica del Minister
5
5
  Actualmente cuenta con las siguientes características:
6
6
 
7
7
  - Generación de XML
8
- - Firmado de XML (utilizando JAVA)
8
+ - Firmado de XML (XADES-EPES en ruby y java)
9
9
  - Comunicación con el API del ministerio de hacienda
10
10
  - Línea de comando para realizar los procesos
11
11
 
@@ -71,13 +71,20 @@ FE.configure do |config|
71
71
  end
72
72
  ```
73
73
 
74
- Para firmar documentos. (Debe tener java instalado)
74
+ Para firmar documentos, existen dos métodos:
75
+ -Con java, se invoca un JAR con 4 argumentos. Este método es muy lento debido a que el proceso principal ejecuta el jar en otro proceso. Fue la primera implementación mientras se desarrollaba la implementación nativa en Ruby.
75
76
  ```ruby
76
-
77
77
  signer = FE::JavaSigner.new FE.configuration.key_path, FE.configuration.key_password, "/path/to/unsigned.xml", "/path/to/signed.xml"
78
78
  signer.sign
79
79
  ```
80
-
80
+ -La implementación nativa en ruby:
81
+ ```ruby
82
+ xml_provider = FE::DataProvider.new :string, "<?xml version="1.0"?><FacturaElectronica> ... </FacturaElectornica>"
83
+ key_provider = FE::DataProvider.new :file, FE.configuration.key_path
84
+ signer = FE::Signer.new xml_provider: xml_provider, key_provider: key_provider, pin: FE.configuration.key_password, output_path: "/path/to/signed.xml"
85
+ signer.sign
86
+ ```
87
+ Esta implementación permite además evitar la lectura y escritura de archivos en el disco duro, lo que permite realizar el firmado de manera mucho más rápida.
81
88
 
82
89
  Para enviar documentos al API
83
90
  ```ruby
@@ -169,12 +176,6 @@ Para enviar documentos
169
176
 
170
177
  $ facturacr send_document /path/to/signed.xml
171
178
 
172
- ## To Do
173
-
174
- - Mejorar y extender la generación de XML
175
- - Implemntar un mapeo más sencillo para generación de XML
176
- - Implementar Aceptación y Rechazo
177
- - Finalizar el firmador en ruby
178
179
 
179
180
  ## Development
180
181
 
data/facturacr.gemspec CHANGED
@@ -19,13 +19,13 @@ Gem::Specification.new do |spec|
19
19
 
20
20
  spec.add_development_dependency "bundler", "~> 1.10"
21
21
  spec.add_development_dependency "rake", "~> 10.0"
22
- spec.add_development_dependency "minitest"
23
- spec.add_development_dependency "minitest-colorize"
22
+ spec.add_development_dependency "minitest", '~> 5.11'
23
+ spec.add_development_dependency "minitest-colorize", '~> 0.0'
24
24
 
25
- spec.add_dependency 'rest-client'
26
- spec.add_dependency 'nokogiri'
27
- spec.add_dependency 'colorize'
28
- spec.add_dependency 'thor'
29
- spec.add_dependency 'activemodel'
30
- spec.add_dependency 'awesome_print'
25
+ spec.add_dependency 'rest-client', '~> 2.0'
26
+ spec.add_dependency 'nokogiri', '~> 1.8'
27
+ spec.add_dependency 'colorize', '~> 0.8'
28
+ spec.add_dependency 'thor', '~> 0.20'
29
+ spec.add_dependency 'activemodel', '>= 3.2'
30
+ spec.add_dependency 'awesome_print', '~> 1.8'
31
31
  end
data/lib/facturacr.rb CHANGED
@@ -11,6 +11,7 @@ require 'facturacr/xml_document'
11
11
  require 'facturacr/api'
12
12
  require 'facturacr/builder'
13
13
  require 'facturacr/version'
14
+ require 'facturacr/data_provider'
14
15
  require 'facturacr/signer/signer'
15
16
  require 'facturacr/reception_message'
16
17
 
data/lib/facturacr/api.rb CHANGED
@@ -7,7 +7,7 @@ require 'json'
7
7
  module FE
8
8
  class Api
9
9
 
10
- attr_accessor :authentication_endpoint, :document_endpoint, :username, :password, :client_id, :errors
10
+ attr_accessor :authentication_endpoint, :document_endpoint, :username, :password, :client_id, :errors, :check_location
11
11
 
12
12
  def initialize(configuration = nil)
13
13
  @authentication_endpoint = (configuration || FE.configuration).authentication_endpoint
@@ -32,7 +32,11 @@ module FE
32
32
  def send_document(payload)
33
33
  authenticate
34
34
  response = RestClient.post "#{@document_endpoint}/recepcion", payload.to_json, {:Authorization=> "bearer #{@token}", content_type: :json}
35
- return true if response.code.eql?(200) || response.code.eql?(202)
35
+ if response.code.eql?(200) || response.code.eql?(202)
36
+ @check_location = response.headers[:location]
37
+ puts "CheckLocation: #{@check_location}"
38
+ return true
39
+ end
36
40
  rescue => e
37
41
  @errors[:request] = {message: e.message, response: e.response}
38
42
  return false
@@ -92,7 +92,12 @@ module FE
92
92
  taxes = []
93
93
  if txs.is_a?(Array) && txs.first.is_a?(Hash)
94
94
  txs.each do |t|
95
- taxes << FE::Document::Tax.new(code: "01", rate: 13, total: 13)
95
+ exo = t.delete(:exoneration)
96
+ if exo && exo.is_a?(Hash)
97
+ t[:exoneration] = FE::Document::Exoneration.new(exo)
98
+ end
99
+
100
+ taxes << FE::Document::Tax.new(t)
96
101
  end
97
102
  else
98
103
  taxes = txs
@@ -258,5 +263,6 @@ module FE
258
263
  FE::Document::Reference.new args
259
264
  end
260
265
 
266
+
261
267
  end
262
268
  end
data/lib/facturacr/cli.rb CHANGED
@@ -3,29 +3,7 @@ require 'awesome_print'
3
3
  require 'facturacr'
4
4
  require 'facturacr/cli/generate'
5
5
  require 'fileutils'
6
- =begin
7
- require 'facturacr/document'
8
- require 'facturacr/document/fax'
9
- require 'facturacr/document/identification_document'
10
- require 'facturacr/document/issuer'
11
- require 'facturacr/document/receiver'
12
- require 'facturacr/document/location'
13
- require 'facturacr/document/phone_type'
14
- require 'facturacr/document/phone'
15
- require 'facturacr/document/item'
16
- require 'facturacr/document/tax'
17
- require 'facturacr/document/summary'
18
- require 'facturacr/document/regulation'
19
- require 'facturacr/document/reference'
20
6
 
21
- require 'facturacr/invoice'
22
- require 'facturacr/credit_note'
23
- require 'facturacr/signer/signer'
24
- require 'facturacr/api'
25
- require 'facturacr/signed_document'
26
- require 'facturacr/builder'
27
- require 'facturacr/xml_document'
28
- =end
29
7
  module FE
30
8
 
31
9
  module Utils
@@ -56,28 +34,37 @@ module FE
56
34
  desc "sign XML_IN XML_OUT", "signs the xml document and stores the signed document in the output path"
57
35
  method_option :config_file, aliases: '-c', desc: "default configuration file", default: "tmp/config.yml"
58
36
  def sign(xml_in, xml_out)
37
+ start = Time.now
59
38
  FE::Utils.configure(options[:config_file])
60
- signer = FE::JavaSigner.new FE.configuration.key_path, FE.configuration.key_password, xml_in, xml_out
39
+
40
+ key_provider = FE::DataProvider.new(:file, FE.configuration.key_path)
41
+ document_provider = FE::DataProvider.new(:file, xml_in)
42
+ signer_args = {xml_provider: document_provider, key_provider: key_provider, pin: FE.configuration.key_password, output_path: xml_out }
43
+ signer = FE::Signer.new signer_args
61
44
  signer.sign
45
+ puts "Signature: #{Time.now - start} sec.".blue
62
46
  end
63
47
 
64
48
  desc "send_document SIGNED_XML", "sends the SIGNED_XML file to the API"
65
49
  method_option :config_file, aliases: '-c', desc: "default configuration file", default: "tmp/config.yml"
66
50
  def send_document(path)
67
51
  FE::Utils.configure(options[:config_file])
68
- xml_document = FE::XmlDocument.new(path)
52
+ xml_document = FE::XmlDocument.new(DataProvider.new :file, path)
69
53
  document = xml_document.document
70
- if document.is_a?(FE::ReceptionMessage)
71
- document.receiver_id_type = "02"
72
- end
73
54
  signed_document = FE::SignedDocument.new(document,path)
74
55
  api = FE::Api.new
75
- if api.send_document(signed_document.payload)
56
+ payload = signed_document.payload
57
+ if api.send_document(payload)
76
58
  puts "Document Sent".green
77
59
  puts "KEY: #{document.key}"
78
60
  puts "Wait 5 seconds before check..."
79
61
  sleep 5
80
- invoke :check, [document.key], :config_file=>options[:config_file]
62
+ if document.is_a?(FE::ReceptionMessage)
63
+ check_key = api.check_location.split("/").last
64
+ else
65
+ check_key = document.key
66
+ end
67
+ invoke :check, [check_key], :config_file=>options[:config_file]
81
68
  else
82
69
  puts "ERROR".red
83
70
  ap api.errors
@@ -11,6 +11,7 @@ module FE
11
11
  method_option :data_path, default: "tmp/data.yml"
12
12
  method_option :output_path, desc: "path to save the output"
13
13
  def invoice
14
+ start = Time.now
14
15
  data = YAML.load_file(options[:data_path]).with_indifferent_access
15
16
  builder = FE::Builder.new
16
17
 
@@ -27,6 +28,8 @@ module FE
27
28
  write(invoice, output_path)
28
29
  print_details(invoice)
29
30
  sign(output_path, options) if options[:sign]
31
+ end_at = Time.now
32
+ puts "Total Time: #{end_at - start} sec."
30
33
  send_document("#{output_path}.signed.xml") if options[:sign] && options[:send]
31
34
 
32
35
  end
@@ -140,7 +143,7 @@ module FE
140
143
  method_option :document_situation, desc: "the situation of the document", default: "1"
141
144
  method_option :config_file, aliases: '-c', desc: "default configuration file", default: "tmp/config.yml"
142
145
  def reception_message
143
- doc = XmlDocument.new(options[:xml_in])
146
+ doc = XmlDocument.new(DataProvider.new(:file, options[:xml_in]))
144
147
 
145
148
  builder = FE::Builder.new
146
149
 
@@ -150,17 +153,17 @@ module FE
150
153
  key: doc.document.key,
151
154
  date: doc.document.date,
152
155
  number: options[:number],
153
- issuer_id_number: doc.document.receiver.identification_document.id_number,
154
- issuer_id_type: doc.document.receiver.identification_document.document_type,
155
- receiver_id_number: doc.document.issuer.identification_document.id_number,
156
- receiver_id_type: doc.document.issuer.identification_document.document_type,
156
+ issuer_id_number: doc.document.issuer.identification_document.id_number,
157
+ issuer_id_type: doc.document.issuer.identification_document.document_type,
158
+ receiver_id_number: doc.document.receiver.identification_document.id_number,
159
+ receiver_id_type: doc.document.receiver.identification_document.document_type,
157
160
  message: options[:action],
158
161
  tax: doc.document.summary.tax_total,
159
162
  total: doc.document.summary.net_total,
160
163
  document_situation: options[:document_situation]
161
164
  })
162
165
 
163
- message.security_code = (SecureRandom.random_number * (10**8)).round.to_s
166
+ message.security_code = 8.times.map{ rand(0..9) }.join
164
167
  if options[:output_path]
165
168
  output_path = options[:output_path]
166
169
  else
@@ -0,0 +1,21 @@
1
+ module FE
2
+
3
+ class DataProvider
4
+ SOURCES = [:string, :file]
5
+
6
+ attr_accessor :contents
7
+
8
+ def initialize(source, data)
9
+ source = source.to_s.to_sym
10
+ raise ArgumentError, "source (#{source}) is not valid" if !SOURCES.include?(source)
11
+ raise ArgumentError, "#{data} does not exist" if soruce.eql?(:file) && !File.exists?(data)
12
+
13
+ if source.eql?(:string)
14
+ @contents = data
15
+ elsif source.eql?(:file)
16
+ @contents = File.read(data)
17
+ end
18
+ end
19
+ end
20
+
21
+ end
@@ -147,6 +147,7 @@ module FE
147
147
  numeroIdentificacion: @receiver.identification_document.id_number
148
148
  }
149
149
  end
150
+
150
151
  payload
151
152
  end
152
153
 
@@ -12,7 +12,7 @@ module FE
12
12
  "05" => "Zonas Francas",
13
13
  "99" => "Otros"
14
14
  }
15
- attr_accessor :document_type, :document_number, :institution, :date, :total_tax, :percentage
15
+ attr_accessor :document_type, :document_number, :institution, :date, :total_tax, :percentage, :net_total
16
16
 
17
17
  validates :document_type, presence: true, inclusion: DOCUMENT_TYPES.keys
18
18
  validates :document_number, presence: true
@@ -25,9 +25,11 @@ module FE
25
25
  @document_type = args[:document_type]
26
26
  @document_number = args[:document_number]
27
27
  @institution = args[:institution]
28
- @date = args[:date].xmlschema
28
+ @date = args[:date]
29
29
  @total_tax = args[:total_tax]
30
- @percentage = args[:percentage]
30
+ if args[:net_total].present?
31
+ @percentage = ((@total_tax / args[:net_total].to_f)*100).to_i
32
+ end
31
33
  end
32
34
 
33
35
  def build_xml(node)
@@ -35,10 +37,10 @@ module FE
35
37
  node = Nokogiri::XML::Builder.new if node.nil?
36
38
 
37
39
  node.Exoneracion do |xml|
38
- xml.TipoDocument @document_type
40
+ xml.TipoDocumento @document_type
39
41
  xml.NumeroDocumento @document_number
40
42
  xml.NombreInstitucion @institution
41
- xml.FechaEmision @date
43
+ xml.FechaEmision @date.xmlschema
42
44
  xml.MontoImpuesto @total_tax
43
45
  xml.PorcentajeCompra @percentage
44
46
  end
@@ -4,7 +4,7 @@ module FE
4
4
  include ActiveModel::Validations
5
5
 
6
6
  attr_accessor :line_number, :code, :quantity, :unit, :description, :unit_price, :total,
7
- :discount, :discount_reason, :subtotal, :taxes, :exoneration, :net_total
7
+ :discount, :discount_reason, :subtotal, :taxes, :net_total
8
8
 
9
9
  validates :line_number, presence: true
10
10
  validates :quantity, presence: true, numericality: {greater_than: 0}
@@ -49,7 +49,7 @@ module FE
49
49
  x.Detalle @description
50
50
  x.PrecioUnitario @unit_price
51
51
  x.MontoTotal @total
52
- x.Discount @discount if @discount.present?
52
+ x.MontoDescuento @discount if @discount.present?
53
53
  x.NaturalezaDescuento @discount_reason if @discount_reason.present?
54
54
  x.SubTotal @subtotal
55
55
  @taxes.each do |tax|
@@ -19,13 +19,14 @@ module FE
19
19
  "11"=>"Impuesto Selectivo de Consumo Compras Autorizadas",
20
20
  "99"=>"Otros"
21
21
  }
22
- attr_accessor :code, :rate, :total
22
+ attr_accessor :code, :rate, :total, :exoneration
23
23
 
24
24
  validates :code, presence: true, inclusion: TAX_CODES.keys
25
25
  def initialize(args={})
26
26
  @code = args[:code]
27
27
  @rate = args[:rate]
28
28
  @total = args[:total]
29
+ @exoneration = args[:exoneration]
29
30
  end
30
31
 
31
32
  def build_xml(node)
@@ -36,6 +37,9 @@ module FE
36
37
  xml.Codigo @code
37
38
  xml.Tarifa @rate
38
39
  xml.Monto @total
40
+ if @exoneration.present?
41
+ @exoneration.build_xml(xml)
42
+ end
39
43
  end
40
44
  end
41
45
 
@@ -9,7 +9,7 @@ module FE
9
9
  "2" => "Aceptacion Parcial",
10
10
  "3" => "Rechazado"
11
11
  }
12
- attr_accessor :key, :date, :issuer_id_number, :receiver_id_number, :message, :details, :tax, :total, :number, :receiver_id_type, :security_code, :document_situation
12
+ attr_accessor :key, :date, :issuer_id_number, :receiver_id_number, :message, :details, :tax, :total, :number, :receiver_id_type, :security_code, :document_situation, :issuer_id_type
13
13
 
14
14
  validates :date, presence: true
15
15
  validates :issuer_id_number, presence: true, length: {is: 12}
@@ -19,10 +19,13 @@ module FE
19
19
  validates :total, presence: true, numericality: true
20
20
  validates :number, presence: true
21
21
  validates :security_code, presence: true, length: {is: 8}
22
+ validates :issuer_id_type, presence: true
23
+ validates :receiver_id_type, presence: true
22
24
 
23
25
  def initialize(args = {})
24
26
  @key = args[:key]
25
27
  @date = args[:date]
28
+ @issuer_id_type = args[:issuer_id_type]
26
29
  @issuer_id_number = args[:issuer_id_number]
27
30
  @receiver_id_type = args[:receiver_id_type]
28
31
  @receiver_id_number = args[:receiver_id_number]
@@ -40,22 +43,6 @@ module FE
40
43
  }
41
44
  end
42
45
 
43
- def key
44
- raise "Documento inválido: #{errors.messages}" unless valid?
45
- country = "506"
46
- day = "%02d" % @date.day
47
- month = "%02d" % @date.month
48
- year = "%02d" % (@date.year - 2000)
49
- id_number = @receiver_id_number.rjust(12,"0")
50
-
51
- situation = @document_situation
52
- security_code = @security_code
53
-
54
- result = "#{country}#{day}#{month}#{year}#{id_number}#{sequence}#{situation}#{security_code}"
55
- raise "The key is invalid: #{result}" unless result.length.eql?(50)
56
-
57
- result
58
- end
59
46
 
60
47
  def headquarters
61
48
  @headquarters ||= "001"
@@ -84,15 +71,15 @@ module FE
84
71
  builder = Nokogiri::XML::Builder.new
85
72
 
86
73
  builder.MensajeReceptor(@namespaces) do |xml|
87
- xml.Clave key
74
+ xml.Clave @key
88
75
  xml.NumeroCedulaEmisor @issuer_id_number
89
76
  xml.FechaEmisionDoc @date.xmlschema
90
77
  xml.Mensaje @message
91
78
  xml.DetalleMensaje @details if @details
92
- xml.MontoTotalImpuesto @tax if @tax
79
+ xml.MontoTotalImpuesto @tax.to_f
93
80
  xml.TotalFactura @total
94
81
  xml.NumeroCedulaReceptor @receiver_id_number
95
- xml.NumConsecutivoReceptor sequence
82
+ xml.NumeroConsecutivoReceptor sequence
96
83
  end
97
84
 
98
85
  builder
@@ -103,16 +90,32 @@ module FE
103
90
  end
104
91
 
105
92
  def api_payload
93
+
106
94
  payload = {}
107
- payload[:clave] = key
95
+ payload[:clave] = @key
108
96
  payload[:fecha] = @date.xmlschema
109
97
  payload[:emisor] = {
110
- tipoIdentificacion: @receiver_id_type,
98
+ tipoIdentificacion: infer_id_type(@issuer_id_number),
99
+ numeroIdentificacion: @issuer_id_number
100
+ }
101
+ payload[:receptor] = {
102
+ tipoIdentificacion: infer_id_type(@receiver_id_number),
111
103
  numeroIdentificacion: @receiver_id_number
112
104
  }
105
+ payload[:consecutivoReceptor] = sequence
113
106
  payload
114
107
  end
115
108
 
109
+ def infer_id_type(id_number)
110
+ if id_number.to_i.to_s.size == 9
111
+ "01"
112
+ elsif id_number.to_i.to_s.size == 10
113
+ "02"
114
+ elsif id_number.to_i.to_S.size == 11
115
+ "03"
116
+ end
117
+ end
116
118
 
117
119
  end
120
+
118
121
  end