facturacr 0.1.2

Sign up to get free protection for your applications and to get access to all the features.
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