facturacr 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.
Files changed (53) hide show
  1. checksums.yaml +7 -0
  2. data/.DS_Store +0 -0
  3. data/.gitignore +9 -0
  4. data/.travis.yml +4 -0
  5. data/Gemfile +4 -0
  6. data/LICENSE.txt +7 -0
  7. data/README.md +181 -0
  8. data/Rakefile +10 -0
  9. data/bin/console +14 -0
  10. data/bin/setup +7 -0
  11. data/bin/signer/signer.jar +0 -0
  12. data/config/config.yml +10 -0
  13. data/exe/facturacr +3 -0
  14. data/facturacr.gemspec +31 -0
  15. data/lib/facturacr.rb +37 -0
  16. data/lib/facturacr/api.rb +68 -0
  17. data/lib/facturacr/api/document_status.rb +36 -0
  18. data/lib/facturacr/builder.rb +223 -0
  19. data/lib/facturacr/cli.rb +116 -0
  20. data/lib/facturacr/cli/generate.rb +133 -0
  21. data/lib/facturacr/configuration.rb +51 -0
  22. data/lib/facturacr/credit_note.rb +33 -0
  23. data/lib/facturacr/debit_note.rb +33 -0
  24. data/lib/facturacr/document.rb +171 -0
  25. data/lib/facturacr/document/code.rb +4 -0
  26. data/lib/facturacr/document/exoneration.rb +49 -0
  27. data/lib/facturacr/document/fax.rb +24 -0
  28. data/lib/facturacr/document/identification_document.rb +41 -0
  29. data/lib/facturacr/document/issuer.rb +56 -0
  30. data/lib/facturacr/document/item.rb +67 -0
  31. data/lib/facturacr/document/location.rb +49 -0
  32. data/lib/facturacr/document/phone.rb +22 -0
  33. data/lib/facturacr/document/phone_type.rb +37 -0
  34. data/lib/facturacr/document/receiver.rb +59 -0
  35. data/lib/facturacr/document/reference.rb +46 -0
  36. data/lib/facturacr/document/regulation.rb +26 -0
  37. data/lib/facturacr/document/summary.rb +64 -0
  38. data/lib/facturacr/document/tax.rb +44 -0
  39. data/lib/facturacr/invoice.rb +34 -0
  40. data/lib/facturacr/signed_document.rb +19 -0
  41. data/lib/facturacr/signer/signer.rb +245 -0
  42. data/lib/facturacr/ticket.rb +34 -0
  43. data/lib/facturacr/version.rb +3 -0
  44. data/lib/facturacr/xml_document.rb +161 -0
  45. data/resources/FacturaElectronica_V.4.2.xsd +1571 -0
  46. data/resources/NotaCreditoElectronica_V4.2.xsd +1657 -0
  47. data/resources/credit_note.xml +86 -0
  48. data/resources/data.yml +73 -0
  49. data/resources/debit_note.xml +86 -0
  50. data/resources/invoice.xml +94 -0
  51. data/resources/pruebas.xml +37 -0
  52. data/resources/test.p12 +0 -0
  53. metadata +235 -0
@@ -0,0 +1,51 @@
1
+ require 'yaml'
2
+ require 'erb'
3
+ module FE
4
+ class Configuration
5
+
6
+ attr_accessor :api_username
7
+ attr_accessor :api_password
8
+ attr_accessor :key_path
9
+ attr_accessor :key_password
10
+ attr_accessor :documents_endpoint
11
+ attr_accessor :authentication_endpoint
12
+ attr_accessor :api_client_id
13
+ attr_accessor :environment
14
+ attr_accessor :file_path
15
+ attr_accessor :mode
16
+
17
+
18
+ def initialize
19
+ @environment = "development"
20
+ @mode = :manual
21
+ @api_username = "changeme"
22
+ @api_password = "changeme"
23
+ @key_path = "resources/test.p12"
24
+ @key_password = "test123"
25
+ @api_client_id = 'api-stag'
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"
28
+ end
29
+
30
+ def read_config_file
31
+ if file? && @file_path && File.exists?(@file_path)
32
+ template = ERB.new(File.read(@file_path))
33
+ result = YAML.load(template.result(binding))
34
+ result[@environment].each do |k,v|
35
+ if respond_to?(k)
36
+ self.send("#{k}=",v)
37
+ end
38
+ end
39
+ end
40
+ end
41
+
42
+ def manual?
43
+ @mode.to_sym.eql?(:manual)
44
+ end
45
+
46
+ def file?
47
+ @mode.to_sym.eql?(:file)
48
+ end
49
+
50
+ end
51
+ end
@@ -0,0 +1,33 @@
1
+ require 'facturacr/document/regulation'
2
+ module FE
3
+
4
+ class CreditNote < Document
5
+
6
+ def initialize(args={})
7
+ @date = args[:date]
8
+ @issuer = args[:issuer]
9
+ @receiver = args[:receiver]
10
+ @items = args[:items]
11
+ @number = args[:number]
12
+ @condition = args[:condition]
13
+ @payment_type = args[:payment_type] || "01"
14
+ @document_type = "03"
15
+ @credit_term = args[:credit_term]
16
+ @summary = args[:summary]
17
+ @regulation = args[:regulation] ||= FE::Document::Regulation.new
18
+ @security_code = args[:security_code]
19
+ @document_situation = args[:document_situation]
20
+ @references = args[:references]
21
+ @namespaces = {
22
+ "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
23
+ "xmlns:xsd"=>"http://www.w3.org/2001/XMLSchema",
24
+ "xmlns"=>"https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/notaCreditoElectronica"#,
25
+ }
26
+ end
27
+
28
+ def document_tag
29
+ "NotaCreditoElectronica"
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,33 @@
1
+ require 'facturacr/document/regulation'
2
+ module FE
3
+
4
+ class DebitNote < Document
5
+
6
+ def initialize(args={})
7
+ @date = args[:date]
8
+ @issuer = args[:issuer]
9
+ @receiver = args[:receiver]
10
+ @items = args[:items]
11
+ @number = args[:number]
12
+ @condition = args[:condition]
13
+ @payment_type = args[:payment_type] || "01"
14
+ @document_type = "02"
15
+ @credit_term = args[:credit_term]
16
+ @summary = args[:summary]
17
+ @regulation = args[:regulation] ||= FE::Document::Regulation.new
18
+ @security_code = args[:security_code]
19
+ @document_situation = args[:document_situation]
20
+ @references = args[:references]
21
+ @namespaces = {
22
+ "xmlns:xsi"=>"http://www.w3.org/2001/XMLSchema-instance",
23
+ "xmlns:xsd"=>"http://www.w3.org/2001/XMLSchema",
24
+ "xmlns"=>"https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/notaDebitoElectronica"
25
+ }
26
+ end
27
+
28
+ def document_tag
29
+ "NotaDebitoElectronica"
30
+ end
31
+
32
+ end
33
+ end
@@ -0,0 +1,171 @@
1
+ require 'active_model'
2
+
3
+ module FE
4
+ class Document
5
+ include ActiveModel::Validations
6
+
7
+ CONDITIONS = {
8
+ "01"=>"Contado",
9
+ "02"=>"Crédito",
10
+ "03"=>"Consignación",
11
+ "04"=>"Apartado",
12
+ "05"=>"Arrendamiento con Opción de Compra",
13
+ "06"=>"Arrendamiento en Función Financiera",
14
+ "99"=>"Otros"
15
+ }
16
+ PAYMENT_TYPES = {
17
+ "01"=>"Efectivo",
18
+ "02"=>"Tarjeta",
19
+ "03"=>"Cheque",
20
+ "04"=>"Transferencia",
21
+ "05"=>"Recaudado por Terceros",
22
+ "99"=>"Otros"
23
+ }
24
+ DOCUMENT_TYPES = {
25
+ "01"=> "Factura Electronica",
26
+ "02"=> "Nota de débito",
27
+ "03"=> "Nota de crédito",
28
+ "04"=> "Tiquete Electrónico",
29
+ "05"=> "Nota de despacho",
30
+ "06"=> "Contrato",
31
+ "07"=> "Procedimiento",
32
+ "08"=> "Comprobante Emitido en Contingencia",
33
+ "99"=> "Otros"
34
+
35
+ }
36
+ DOCUMENT_SITUATION = {
37
+ "1" => "Normal",
38
+ "2" => "Contingencia",
39
+ "3" => "Sin Internet"
40
+ }
41
+
42
+ attr_accessor :serial, :date, :issuer, :receiver, :condition, :credit_term,
43
+ :payment_type, :service_type, :reference_information,
44
+ :regulation, :number, :document_type, :security_code,
45
+ :items, :references, :namespaces, :summary, :document_situation
46
+
47
+ validates :date, presence: true
48
+ validates :number, presence: true
49
+ validates :issuer, presence: true
50
+ validates :condition, presence: true, inclusion: CONDITIONS.keys
51
+ validates :credit_term, presence: true, if: ->{condition.eql?("02")}
52
+ validates :payment_type, presence: true, inclusion: PAYMENT_TYPES.keys
53
+ validates :document_type, presence: true, inclusion: DOCUMENT_TYPES.keys
54
+ validates :document_situation, presence: true, inclusion: DOCUMENT_SITUATION.keys
55
+ validates :summary, presence: true
56
+ validates :regulation, presence: true
57
+ validates :security_code, presence: true, length: {is: 8}
58
+ validates :references, presence: true, if: -> {document_type.eql?("02") || document_type.eql?("03")}
59
+
60
+
61
+ def initialize
62
+ raise "Subclasses must implement this method"
63
+ end
64
+
65
+ def document_name
66
+ raise "Subclasses must implement this method"
67
+ end
68
+
69
+ def key
70
+ raise "Documento inválido: #{errors.messages}" unless valid?
71
+ country = "506"
72
+ day = "%02d" % @date.day
73
+ month = "%02d" % @date.month
74
+ year = "%02d" % (@date.year - 2000)
75
+ id_number = @issuer.identification_document.id_number
76
+
77
+ type = @document_situation
78
+ security_code = @security_code
79
+
80
+ result = "#{country}#{day}#{month}#{year}#{id_number}#{sequence}#{type}#{security_code}"
81
+ raise "The key is invalid: #{result}" unless result.length.eql?(50)
82
+
83
+ result
84
+ end
85
+
86
+ def headquarters
87
+ @headquarters ||= "001"
88
+ end
89
+
90
+ def terminal
91
+ @terminal ||= "00001"
92
+ end
93
+
94
+ def sequence
95
+ cons = ("%010d" % @number)
96
+ "#{headquarters}#{terminal}#{@document_type}#{cons}"
97
+ end
98
+
99
+ def build_xml
100
+ raise "Documento inválido: #{errors.messages}" unless valid?
101
+ builder = Nokogiri::XML::Builder.new
102
+
103
+ builder.send(document_tag, @namespaces) do |xml|
104
+ xml.Clave key
105
+ xml.NumeroConsecutivo sequence
106
+ xml.FechaEmision @date.xmlschema
107
+ issuer.build_xml(xml)
108
+ receiver.build_xml(xml) if receiver.present?
109
+ xml.CondicionVenta @condition
110
+ xml.PlazoCredito @credit_term if @credit_term.present? && @condition.eql?("02")
111
+ xml.MedioPago @payment_type
112
+ xml.DetalleServicio do |x|
113
+ @items.each do |item|
114
+ item.build_xml(x)
115
+ end
116
+ end
117
+
118
+ summary.build_xml(xml)
119
+
120
+ if references.present?
121
+ references.each do |r|
122
+ r.build_xml(xml)
123
+ end
124
+ end
125
+
126
+ regulation.build_xml(xml)
127
+ end
128
+
129
+ builder
130
+ end
131
+
132
+ def generate
133
+ build_xml.to_xml(:save_with => Nokogiri::XML::Node::SaveOptions::AS_XML)
134
+ end
135
+
136
+ def api_payload
137
+ payload = {}
138
+ payload[:clave] = key
139
+ payload[:fecha] = @date.xmlschema
140
+ payload[:emisor] = {
141
+ tipoIdentificacion: @issuer.identification_document.document_type,
142
+ numeroIdentificacion: @issuer.identification_document.id_number
143
+ }
144
+ if @receiver.identification_document.present?
145
+ payload[:receptor] = {
146
+ tipoIdentificacion: @receiver.identification_document.document_type,
147
+ numeroIdentificacion: @receiver.identification_document.id_number
148
+ }
149
+ end
150
+ payload
151
+ end
152
+
153
+ end
154
+
155
+
156
+ end
157
+
158
+ require 'facturacr/document/code'
159
+ require 'facturacr/document/exoneration'
160
+ require 'facturacr/document/fax'
161
+ require 'facturacr/document/identification_document'
162
+ require 'facturacr/document/issuer'
163
+ require 'facturacr/document/item'
164
+ require 'facturacr/document/location'
165
+ require 'facturacr/document/phone_type'
166
+ require 'facturacr/document/phone'
167
+ require 'facturacr/document/receiver'
168
+ require 'facturacr/document/reference'
169
+ require 'facturacr/document/regulation'
170
+ require 'facturacr/document/summary'
171
+ require 'facturacr/document/tax'
@@ -0,0 +1,4 @@
1
+ module FE
2
+
3
+ end
4
+
@@ -0,0 +1,49 @@
1
+ module FE
2
+ class Document
3
+ class Exoneration
4
+ include ActiveModel::Validations
5
+
6
+
7
+ DOCUMENT_TYPES = {
8
+ "01" => "Compras Autorizadas",
9
+ "02" => "Ventas exentas a diplomáticos",
10
+ "03" => "Orden de Compra (Instituciones Públicas y otros organismos)",
11
+ "04" => "Exenciones Dirección General de Hacienda",
12
+ "05" => "Zonas Francas",
13
+ "99" => "Otros"
14
+ }
15
+ attr_accessor :document_type, :document_number, :institution, :date, :total_tax, :percentage
16
+
17
+ validates :document_type, presence: true, inclusion: DOCUMENT_TYPES.keys
18
+ validates :document_number, presence: true
19
+ validates :institution, presence: true
20
+ validates :date, presence: true
21
+ validates :total_tax,presence: true
22
+ validates :percentage, presence: true
23
+
24
+ def initialize(args={})
25
+ @document_type = args[:document_type]
26
+ @document_number = args[:document_number]
27
+ @institution = args[:institution]
28
+ @date = args[:date].xmlschema
29
+ @total_tax = args[:total_tax]
30
+ @percentage = args[:percentage]
31
+ end
32
+
33
+ def build_xml(node)
34
+ raise "Invalid Record: #{errors.messages}" unless valid?
35
+ node = Nokogiri::XML::Builder.new if node.nil?
36
+
37
+ node.Exoneracion do |xml|
38
+ xml.TipoDocument @document_type
39
+ xml.NumeroDocumento @document_number
40
+ xml.NombreInstitucion @institution
41
+ xml.FechaEmision @date
42
+ xml.MontoImpuesto @total_tax
43
+ xml.PorcentajeCompra @percentage
44
+ end
45
+ end
46
+
47
+ end
48
+ end
49
+ end
@@ -0,0 +1,24 @@
1
+ require "facturacr/document"
2
+ require 'facturacr/document/phone_type'
3
+ require 'active_model'
4
+ require 'nokogiri'
5
+
6
+ module FE
7
+ class Document
8
+ class Fax < PhoneType
9
+ include ActiveModel::Validations
10
+
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={})
18
+ super('Fax', args[:country_code], args[:number])
19
+ end
20
+
21
+ end
22
+
23
+ end
24
+ end
@@ -0,0 +1,41 @@
1
+ require "facturacr/document"
2
+ require 'active_model'
3
+ require 'nokogiri'
4
+
5
+ module FE
6
+ class Document
7
+
8
+
9
+ class IdentificationDocument
10
+ include ActiveModel::Validations
11
+
12
+ TYPES = {'01'=>'Cédula Fisica', '02'=>'Cédula Jurídica', '03'=>'DIMEX', '04'=>'NITE'}
13
+
14
+ attr_accessor :document_type, :id_number, :raw_id_number
15
+
16
+ validates :document_type, presence: true, inclusion: TYPES.keys
17
+ validates :id_number, presence: true, length: {is: 12}
18
+
19
+ def initialize(args={})
20
+
21
+ @document_type = args[:type]
22
+ @raw_id_number = args[:number]
23
+ @id_number = "%012d" % args[:number]
24
+
25
+ end
26
+
27
+ def build_xml(node)
28
+ raise "Invalid Record: #{errors.messages}" unless valid?
29
+ node = Nokogiri::XML::Builder.new if node.nil?
30
+ node.Identificacion do |x|
31
+ x.Tipo document_type
32
+ x.Numero raw_id_number
33
+ end
34
+ end
35
+ def to_xml(builder)
36
+ build_xml(builder).to_xml
37
+ end
38
+ end
39
+
40
+ end
41
+ end
@@ -0,0 +1,56 @@
1
+ require "facturacr/document"
2
+ require 'active_model'
3
+ require 'nokogiri'
4
+
5
+ module FE
6
+ class Document
7
+
8
+
9
+ class Issuer
10
+ include ActiveModel::Validations
11
+
12
+ attr_accessor :name, :identification_document, :comercial_name, :location, :phone, :fax, :email
13
+
14
+ validates :name, presence: true, length: { maximum: 80 }
15
+ validates :identification_document, presence: true
16
+ validates :comercial_name, length: {maximum: 80}
17
+ validates :location, presence: true
18
+ validates :email, presence: true, format: {with: /\s*\w+([-+.']\w+)*@\w+([-.]\w+)*\.\w+([-.]\w+)*\s*/}
19
+
20
+ def initialize(args={})
21
+ @name = args[:name]
22
+ @identification_document = args[:identification_document]
23
+ @comercial_name = args[:comercial_name]
24
+ @location = args[:location]
25
+ @phone = args[:phone]
26
+ @fax = args[:fax]
27
+ @email = args[:email]
28
+ end
29
+
30
+ def build_xml(node)
31
+ raise "IdentificationDocument is invalid" if @identification_document.nil? || !@identification_document.is_a?(IdentificationDocument)
32
+ raise "Location is invalid" if @location.nil? || !@location.is_a?(Location)
33
+ raise "Phone is invalid" if !@phone.nil? && !@phone.is_a?(Phone)
34
+ raise "Fax is invalid" if !@fax.nil? && !@fax.is_a?(Fax)
35
+
36
+ raise "Issuer is invalid: #{errors.messages}" unless valid?
37
+
38
+ node = Nokogiri::XML::Builder.new if node.nil?
39
+ node.Emisor do |xml|
40
+ xml.Nombre @name
41
+ identification_document.build_xml(xml)
42
+ xml.NombreComercial @comercial_name if @comercial_name
43
+ location.build_xml(xml)
44
+ phone.build_xml(xml) if phone.present?
45
+ fax.build_xml(xml) if fax.present?
46
+ xml.CorreoElectronico @email
47
+ end
48
+ end
49
+
50
+ def to_xml(builder)
51
+ build_xml(builder).to_xml
52
+ end
53
+ end
54
+
55
+ end
56
+ end