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.
- checksums.yaml +7 -0
- data/.DS_Store +0 -0
- data/.gitignore +9 -0
- data/.travis.yml +4 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +7 -0
- data/README.md +181 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +7 -0
- data/bin/signer/signer.jar +0 -0
- data/config/config.yml +10 -0
- data/exe/facturacr +3 -0
- data/facturacr.gemspec +31 -0
- data/lib/facturacr.rb +37 -0
- data/lib/facturacr/api.rb +68 -0
- data/lib/facturacr/api/document_status.rb +36 -0
- data/lib/facturacr/builder.rb +223 -0
- data/lib/facturacr/cli.rb +116 -0
- data/lib/facturacr/cli/generate.rb +133 -0
- data/lib/facturacr/configuration.rb +51 -0
- data/lib/facturacr/credit_note.rb +33 -0
- data/lib/facturacr/debit_note.rb +33 -0
- data/lib/facturacr/document.rb +171 -0
- data/lib/facturacr/document/code.rb +4 -0
- data/lib/facturacr/document/exoneration.rb +49 -0
- data/lib/facturacr/document/fax.rb +24 -0
- data/lib/facturacr/document/identification_document.rb +41 -0
- data/lib/facturacr/document/issuer.rb +56 -0
- data/lib/facturacr/document/item.rb +67 -0
- data/lib/facturacr/document/location.rb +49 -0
- data/lib/facturacr/document/phone.rb +22 -0
- data/lib/facturacr/document/phone_type.rb +37 -0
- data/lib/facturacr/document/receiver.rb +59 -0
- data/lib/facturacr/document/reference.rb +46 -0
- data/lib/facturacr/document/regulation.rb +26 -0
- data/lib/facturacr/document/summary.rb +64 -0
- data/lib/facturacr/document/tax.rb +44 -0
- data/lib/facturacr/invoice.rb +34 -0
- data/lib/facturacr/signed_document.rb +19 -0
- data/lib/facturacr/signer/signer.rb +245 -0
- data/lib/facturacr/ticket.rb +34 -0
- data/lib/facturacr/version.rb +3 -0
- data/lib/facturacr/xml_document.rb +161 -0
- data/resources/FacturaElectronica_V.4.2.xsd +1571 -0
- data/resources/NotaCreditoElectronica_V4.2.xsd +1657 -0
- data/resources/credit_note.xml +86 -0
- data/resources/data.yml +73 -0
- data/resources/debit_note.xml +86 -0
- data/resources/invoice.xml +94 -0
- data/resources/pruebas.xml +37 -0
- data/resources/test.p12 +0 -0
- 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,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
|