facturacr 1.0.1 → 1.0.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 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