facturacr 1.0.1 → 1.0.2

Sign up to get free protection for your applications and to get access to all the features.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: 784afc0b9e27eeef2d3dbd6770bc37c74c268a4e
4
- data.tar.gz: 0c1db15bd3360912262cf5ec191432464ee6a9f0
3
+ metadata.gz: 4bebcdc34802fd5819d42c9d03a493dd1f5978c8
4
+ data.tar.gz: 386e218145ded7382bcad22816258ace2f5145cc
5
5
  SHA512:
6
- metadata.gz: 6e2da292a67d329622eeccd529d0f19232f0c028fc5dd27d53c1184d748f8b121739df10c1c6e392db14566415ad979c9c8aa3cc97c8a69e3f5007f17ccc4a6c
7
- data.tar.gz: 3d88d6d3e26ccf50d3ec7f0a9db6a6f83e160ea61eab479a12a3c1ae54c4eaf9ad24d9a0897ee52c0949498d6b56dd78d330fe05f94da76875230a8506a51ce3
6
+ metadata.gz: 39dca71def73211dc98f2081e6c291fb976755884fde5d436ccc53f89442e08da90e9f853dc6695a9857a5615efd05ef47e168e0fe0d2df219dc0f6078205e87
7
+ data.tar.gz: ff1d1134455f109c881eeb8722a9a56d9ae15f984e8470c29c9db9386f9e941c2013938eb89a23bff01c62725c14b529efe2e7d7503441960598962b4bbb6786
data/.gitignore CHANGED
@@ -8,3 +8,5 @@
8
8
  /pkg/
9
9
  /spec/reports/
10
10
  /tmp/
11
+ .DS_Store
12
+ .idea/
@@ -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, :check_location
10
+ attr_accessor :authentication_endpoint, :document_endpoint, :username, :password, :client_id, :errors, :check_location, :refresh_token
11
11
 
12
12
  def initialize(configuration = nil)
13
13
  @authentication_endpoint = (configuration || FE.configuration).authentication_endpoint
@@ -19,8 +19,15 @@ module FE
19
19
  end
20
20
 
21
21
  def authenticate
22
- response = RestClient.post @authentication_endpoint, auth_data
23
- @token = JSON.parse(response)["access_token"]
22
+ # Backwards compantibility with configurations that still use contain the token operation in the url.
23
+ url = @authentication_endpoint
24
+ if !@authentication_endpoint.end_with?('token')
25
+ url += "/token"
26
+ end
27
+ response = RestClient.post url, auth_data
28
+ json = JSON.parse(response)
29
+ @token = json["access_token"]
30
+ @refresh_token = json["refresh_token"]
24
31
 
25
32
  @token
26
33
  rescue => e
@@ -28,6 +35,18 @@ module FE
28
35
  raise e
29
36
  end
30
37
 
38
+ def logout
39
+ url = @authentication_endpoint
40
+ if @authentication_endpoint.end_with?('token')
41
+ url.gsub!("token","logout")
42
+ else
43
+ url += "/logout"
44
+ end
45
+ response = RestClient.post url, logout_data
46
+ rescue => e
47
+ puts "LOGOUT ERROR: #{e.message}".red
48
+ end
49
+
31
50
 
32
51
  def send_document(payload)
33
52
  authenticate
@@ -75,5 +94,12 @@ module FE
75
94
  scope: ''
76
95
  }
77
96
  end
97
+
98
+ def logout_data
99
+ {
100
+ client_id: @client_id,
101
+ refresh_token: @refresh_token
102
+ }
103
+ end
78
104
  end
79
105
  end
@@ -26,6 +26,8 @@ module FE
26
26
  api = FE::Api.new
27
27
  document_status = api.get_document_status(key)
28
28
  ap document_status.to_h
29
+ puts " (check)=> API Logout"
30
+ api.logout
29
31
  end
30
32
 
31
33
  desc "generate DOCUMENT ARGS", "generate xml documents"
@@ -70,6 +72,8 @@ module FE
70
72
  ap api.errors
71
73
  raise "Sending Document Error"
72
74
  end
75
+ puts " (send)=> API Logout"
76
+ api.logout
73
77
  end
74
78
 
75
79
 
@@ -24,7 +24,7 @@ module FE
24
24
  @key_password = "test123"
25
25
  @api_client_id = 'api-stag'
26
26
  @documents_endpoint = "https://api.comprobanteselectronicos.go.cr/recepcion-sandbox/v1"
27
- @authentication_endpoint = "https://idp.comprobanteselectronicos.go.cr/auth/realms/rut-stag/protocol/openid-connect/token"
27
+ @authentication_endpoint = "https://idp.comprobanteselectronicos.go.cr/auth/realms/rut-stag/protocol/openid-connect"
28
28
  end
29
29
 
30
30
  def read_config_file
@@ -12,7 +12,7 @@ module FE
12
12
  "05"=>"Arrendamiento con Opción de Compra",
13
13
  "06"=>"Arrendamiento en Función Financiera",
14
14
  "99"=>"Otros"
15
- }
15
+ }.freeze
16
16
  PAYMENT_TYPES = {
17
17
  "01"=>"Efectivo",
18
18
  "02"=>"Tarjeta",
@@ -20,7 +20,7 @@ module FE
20
20
  "04"=>"Transferencia",
21
21
  "05"=>"Recaudado por Terceros",
22
22
  "99"=>"Otros"
23
- }
23
+ }.freeze
24
24
  DOCUMENT_TYPES = {
25
25
  "01"=> "Factura Electronica",
26
26
  "02"=> "Nota de débito",
@@ -32,12 +32,12 @@ module FE
32
32
  "08"=> "Comprobante Emitido en Contingencia",
33
33
  "99"=> "Otros"
34
34
 
35
- }
35
+ }.freeze
36
36
  DOCUMENT_SITUATION = {
37
37
  "1" => "Normal",
38
38
  "2" => "Contingencia",
39
39
  "3" => "Sin Internet"
40
- }
40
+ }.freeze
41
41
 
42
42
  attr_accessor :serial, :date, :issuer, :receiver, :condition, :credit_term,
43
43
  :payment_type, :service_type, :reference_information,
@@ -98,7 +98,7 @@ module FE
98
98
 
99
99
  def build_xml
100
100
  raise "Documento inválido: #{errors.messages}" unless valid?
101
- builder = Nokogiri::XML::Builder.new
101
+ builder = Nokogiri::XML::Builder.new(encoding: 'UTF-8')
102
102
 
103
103
  builder.send(document_tag, @namespaces) do |xml|
104
104
  xml.Clave key
@@ -141,7 +141,7 @@ module FE
141
141
  tipoIdentificacion: @issuer.identification_document.document_type,
142
142
  numeroIdentificacion: @issuer.identification_document.id_number
143
143
  }
144
- if @receiver.identification_document.present?
144
+ if @receiver&.identification_document.present?
145
145
  payload[:receptor] = {
146
146
  tipoIdentificacion: @receiver.identification_document.document_type,
147
147
  numeroIdentificacion: @receiver.identification_document.id_number
@@ -2,8 +2,8 @@ module FE
2
2
  class Document
3
3
  class Exoneration
4
4
  include ActiveModel::Validations
5
-
6
-
5
+
6
+
7
7
  DOCUMENT_TYPES = {
8
8
  "01" => "Compras Autorizadas",
9
9
  "02" => "Ventas exentas a diplomáticos",
@@ -11,31 +11,29 @@ module FE
11
11
  "04" => "Exenciones Dirección General de Hacienda",
12
12
  "05" => "Zonas Francas",
13
13
  "99" => "Otros"
14
- }
14
+ }.freeze
15
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
19
19
  validates :institution, presence: true
20
20
  validates :date, presence: true
21
21
  validates :total_tax,presence: true
22
22
  validates :percentage, presence: true
23
-
23
+
24
24
  def initialize(args={})
25
25
  @document_type = args[:document_type]
26
26
  @document_number = args[:document_number]
27
27
  @institution = args[:institution]
28
28
  @date = args[:date]
29
29
  @total_tax = args[:total_tax]
30
- if args[:net_total].present?
31
- @percentage = ((@total_tax / args[:net_total].to_f)*100).to_i
32
- end
30
+ @percentage = ((@total_tax.to_f / args[:net_total].to_f) * 100).to_i if args[:net_total].present?
33
31
  end
34
-
32
+
35
33
  def build_xml(node)
36
34
  raise "Invalid Record: #{errors.messages}" unless valid?
37
35
  node = Nokogiri::XML::Builder.new if node.nil?
38
-
36
+
39
37
  node.Exoneracion do |xml|
40
38
  xml.TipoDocumento @document_type
41
39
  xml.NumeroDocumento @document_number
@@ -45,7 +43,7 @@ module FE
45
43
  xml.PorcentajeCompra @percentage
46
44
  end
47
45
  end
48
-
46
+
49
47
  end
50
48
  end
51
49
  end
@@ -7,18 +7,14 @@ module FE
7
7
  class Document
8
8
  class Fax < PhoneType
9
9
  include ActiveModel::Validations
10
-
10
+
11
11
  attr_accessor :country_code, :number
12
-
13
- validates :country_code, presence: true, length: { maximum: 3 }
14
- validates :number, presence: true, length: {maximum: 20}
15
-
16
-
17
- def initialize(args={})
12
+
13
+ def initialize(args = {})
18
14
  super('Fax', args[:country_code], args[:number])
19
15
  end
20
-
16
+
21
17
  end
22
-
18
+
23
19
  end
24
20
  end
@@ -2,24 +2,37 @@ module FE
2
2
  class Document
3
3
  class Item
4
4
  include ActiveModel::Validations
5
-
6
- attr_accessor :line_number, :code, :quantity, :unit, :description, :unit_price, :total,
5
+
6
+ UNITS = %w[Sp m kg s A K mol cd m² m³ m/s m/s² 1/m kg/m³ A/m² A/m mol/m³ cd/m² 1 rad sr Hz N Pa J W C V F Ω S Wb T H °C lm
7
+ lx Bq Gy Sv kat Pa·s N·m N/m rad/s rad/s² W/m² J/K J/(kg·K) J/kg W/(m·K) J/m³ V/m C/m³ C/m² F/m H/m J/mol J/(mol·K)
8
+ C/kg Gy/s W/sr W/(m²·sr) kat/m³ min h d º ´ ´´ L t Np B eV u ua Unid Gal g Km ln cm mL mm Oz Otros].freeze
9
+
10
+ CODE_TYPES = {
11
+ '01' => 'Código del producto del vendedor',
12
+ '02' => 'Código del producto del comprador',
13
+ '03' => 'Código del producto asignado por la industria',
14
+ '04' => 'Código uso interno',
15
+ '99' => 'Otros'
16
+ }.freeze
17
+
18
+ attr_accessor :line_number, :code_type, :code, :quantity, :unit, :description, :unit_price, :total,
7
19
  :discount, :discount_reason, :subtotal, :taxes, :net_total
8
-
20
+
9
21
  validates :line_number, presence: true
10
- validates :quantity, presence: true, numericality: {greater_than: 0}
11
- validates :unit, presence: true
12
- validates :description, presence: true, length: {maximum: 160 }
22
+ validates :code_type, inclusion: CODE_TYPES.keys, if: -> { code.present? }
23
+ validates :quantity, presence: true, numericality: { greater_than: 0 }
24
+ validates :unit, presence: true, inclusion: UNITS
25
+ validates :description, presence: true, length: { maximum: 160 }
13
26
  validates :unit_price, presence: true
14
27
  validates :total, presence: true
15
- validates :discount, numericality: { grater_than: 0}, if: ->{ discount.present? }
16
- validates :discount_reason, presence: true, if: ->{ discount.present? }
28
+ validates :discount, numericality: { grater_than: 0 }, if: -> { discount.present? }
29
+ validates :discount_reason, presence: true, if: -> { discount.present? }
17
30
  validates :subtotal, presence: true
18
31
  validates :net_total, presence: true
19
-
20
-
21
- def initialize(args={})
32
+
33
+ def initialize(args = {})
22
34
  @line_number = args[:line_number]
35
+ @code_type = args[:code_type].presence || '01'
23
36
  @code = args[:code]
24
37
  @quantity = args[:quantity]
25
38
  @unit = args[:unit]
@@ -32,7 +45,7 @@ module FE
32
45
  @taxes = args[:taxes] || []
33
46
  @net_total = args[:net_total]
34
47
  end
35
-
48
+
36
49
  def build_xml(node)
37
50
  raise "Item invalid: #{errors.messages}" unless valid?
38
51
  node = Nokogiri::XML::Builder.new if node.nil?
@@ -40,7 +53,7 @@ module FE
40
53
  x.NumeroLinea @line_number
41
54
  if @code.present?
42
55
  x.Codigo do |x2|
43
- x2.Tipo "01"
56
+ x2.Tipo @code_type
44
57
  x2.Codigo @code
45
58
  end
46
59
  end
@@ -61,7 +74,6 @@ module FE
61
74
  x.MontoTotalLinea @net_total
62
75
  end
63
76
  end
64
-
65
77
  end
66
78
  end
67
- end
79
+ end
@@ -6,17 +6,13 @@ module FE
6
6
  class Document
7
7
  class Phone < PhoneType
8
8
  include ActiveModel::Validations
9
-
9
+
10
10
  attr_accessor :country_code, :number
11
-
12
- validates :country_code, presence: true, length: { maximum: 3 }
13
- validates :number, presence: true, length: {maximum: 20}
14
-
15
-
16
- def initialize(args={})
11
+
12
+ def initialize(args = {})
17
13
  super('Telefono', args[:country_code],args[:number])
18
14
  end
19
-
20
- end
15
+
16
+ end
21
17
  end
22
18
  end
@@ -4,21 +4,21 @@ require 'nokogiri'
4
4
 
5
5
  module FE
6
6
  class Document
7
-
8
-
7
+
8
+
9
9
  class Receiver
10
10
  include ActiveModel::Validations
11
-
11
+
12
12
  attr_accessor :name, :identification_document,:foreign_id_number, :comercial_name, :location, :phone, :fax, :email
13
-
13
+
14
14
  validates :name, presence: true, length: { maximum: 80 }
15
15
  validates :comercial_name, length: { maximum: 80 }
16
16
  validates :foreign_id_number, length: { maximum: 20 }
17
-
18
-
19
-
17
+
18
+
19
+
20
20
  def initialize(args={})
21
-
21
+
22
22
  @name = args[:name]
23
23
  @identification_document = args[:identification_document]
24
24
  @comercial_name = args[:comercial_name]
@@ -27,33 +27,33 @@ module FE
27
27
  @fax = args[:fax]
28
28
  @email = args[:email]
29
29
  @foreign_id_number = args[:foreign_id_number]
30
-
30
+
31
31
  end
32
-
32
+
33
33
  def build_xml(node)
34
34
  raise "IdentificationDocument is invalid" if !@identification_document.nil? && !@identification_document.is_a?(IdentificationDocument)
35
35
  raise "Location is invalid" if !@location.nil? && !@location.is_a?(Location)
36
36
  raise "Phone is invalid" if !@phone.nil? && !@phone.is_a?(Phone)
37
37
  raise "Fax is invalid" if !@fax.nil? && !@fax.is_a?(Fax)
38
38
  raise "Reciever is invalid: #{errors.messages}" unless valid?
39
-
39
+
40
40
  node = Nokogiri::XML::Builder.new if node.nil?
41
41
  node.Receptor do |xml|
42
42
  xml.Nombre @name
43
43
  @identification_document.build_xml(xml) if @identification_document.present?
44
- xml.IdentificacionExtranjer foreign_id_number if @foreign_id_number.present?
44
+ xml.IdentificacionExtranjero foreign_id_number if @foreign_id_number.present?
45
45
  xml.NombreComercial @comercial_name if @comercial_name.present?
46
46
  @location.build_xml(xml) if @location.present?
47
47
  @phone.build_xml(xml) if @phone.present?
48
48
  @fax.build_xml(xml) if @fax.present?
49
- xml.CorreElectronico @email if @email.present?
50
- end
49
+ xml.CorreoElectronico @email if @email.present?
50
+ end
51
51
  end
52
-
52
+
53
53
  def to_xml(builder)
54
54
  build_xml(builder).to_xml
55
55
  end
56
56
  end
57
-
57
+
58
58
  end
59
- end
59
+ end
@@ -29,7 +29,7 @@ module FE
29
29
 
30
30
  def build_xml(node)
31
31
  unless valid?
32
- raise "Summary invalid: #{errors.messages}"
32
+ raise "Summary invalid: #{errors.messages}"
33
33
  end
34
34
  node = Nokogiri::XML::Builder.new if node.nil?
35
35
 
@@ -53,11 +53,11 @@ module FE
53
53
  private
54
54
 
55
55
  def totals_ok?
56
- errors[:taxable_total] << "invalid amount" if (@taxable_total - (@services_taxable_total + @goods_taxable_total).round(5)).abs > 0.0005
57
- errors[:exent_total] << "invalid amount" if (@exent_total - (@services_exent_total + @goods_exent_total).round(5)).abs > 0.0005
58
- errors[:subtotal] << "invalid amount" if (@subtotal - (@taxable_total + @exent_total).round(5)).abs > 0.0005
59
- errors[:gross_total] << "invalid amount" if (@gross_total - (@subtotal - @discount_total).round(5)).abs > 0.0005
60
- errors[:net_total] << "invalid amount" if (@net_total - (@gross_total + @tax_total).round(5)).abs > 0.0005
56
+ errors.add :taxable_total, :invalid_amount, message: 'invalid amount' if (@taxable_total - (@services_taxable_total + @goods_taxable_total).round(5)).abs > 0.0005
57
+ errors.add :exent_total, :invalid_amount, message: 'invalid amount' if (@exent_total - (@services_exent_total + @goods_exent_total).round(5)).abs > 0.0005
58
+ errors.add :subtotal, :invalid_amount, message: 'invalid amount' if (@subtotal - (@taxable_total + @exent_total).round(5)).abs > 0.0005
59
+ errors.add :gross_total, :invalid_amount, message: 'invalid amount' if (@gross_total - (@subtotal - @discount_total).round(5)).abs > 0.0005
60
+ errors.add :net_total, :invalid_amount, message: 'invalid amount' if (@net_total - (@gross_total + @tax_total).round(5)).abs > 0.0005
61
61
  end
62
62
  end
63
63
  end
@@ -22,6 +22,12 @@ module FE
22
22
  attr_accessor :code, :rate, :total, :exoneration
23
23
 
24
24
  validates :code, presence: true, inclusion: TAX_CODES.keys
25
+ # It is a mandatory field when a tax is added. And it is a decimal number that can be composed of 4 integers and 2 decimals
26
+ validates :rate, presence: true, format: { with: /\A\d{1,4}(\.\d{0,2})?\z/ }
27
+ # It is a mandatory field when a tax is added, it is obtained from the multiplication of the "subtotal" field by "tax rate"
28
+ # And is a decimal number that can be composed of 13 integers and 5 decimals
29
+ validates :total, presence: true, format: { with: /\A\d{1,13}(\.\d{0,5})?\z/ }
30
+
25
31
  def initialize(args={})
26
32
  @code = args[:code]
27
33
  @rate = args[:rate]
@@ -8,7 +8,8 @@ module FE
8
8
  "1" => "Aceptado",
9
9
  "2" => "Aceptacion Parcial",
10
10
  "3" => "Rechazado"
11
- }
11
+ }.freeze
12
+
12
13
  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
14
 
14
15
  validates :date, presence: true
@@ -76,7 +77,7 @@ module FE
76
77
  xml.FechaEmisionDoc @date.xmlschema
77
78
  xml.Mensaje @message
78
79
  xml.DetalleMensaje @details if @details
79
- xml.MontoTotalImpuesto @tax.to_f
80
+ xml.MontoTotalImpuesto @tax.to_f if @tax
80
81
  xml.TotalFactura @total
81
82
  xml.NumeroCedulaReceptor @receiver_id_number
82
83
  xml.NumeroConsecutivoReceptor sequence
@@ -111,7 +112,7 @@ module FE
111
112
  "01"
112
113
  elsif id_number.to_i.to_s.size == 10
113
114
  "02"
114
- elsif id_number.to_i.to_S.size == 11
115
+ elsif id_number.to_i.to_s.size == 11
115
116
  "03"
116
117
  end
117
118
  end
@@ -1,3 +1,3 @@
1
1
  module FE
2
- VERSION = "1.0.1"
2
+ VERSION = "1.0.2"
3
3
  end
@@ -182,5 +182,9 @@ module FE
182
182
  @document.tax = @doc.css("#{root_tag} MontoTotalImpuesto").text
183
183
  end
184
184
  end
185
+
186
+ def has_tax_node?
187
+ @doc.css("#{root_tag} ResumenFactura TotalImpuesto").any?
188
+ end
185
189
  end
186
190
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: facturacr
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.1
4
+ version: 1.0.2
5
5
  platform: ruby
6
6
  authors:
7
7
  - Josef Sauter
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-10-26 00:00:00.000000000 Z
11
+ date: 2018-11-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: bundler
@@ -158,7 +158,6 @@ executables:
158
158
  extensions: []
159
159
  extra_rdoc_files: []
160
160
  files:
161
- - ".DS_Store"
162
161
  - ".gitignore"
163
162
  - ".travis.yml"
164
163
  - Gemfile
data/.DS_Store DELETED
Binary file