facturacr 0.1.2
Sign up to get free protection for your applications and to get access to all the features.
- 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,67 @@
|
|
1
|
+
module FE
|
2
|
+
class Document
|
3
|
+
class Item
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
attr_accessor :line_number, :code, :quantity, :unit, :description, :unit_price, :total,
|
7
|
+
:discount, :discount_reason, :subtotal, :taxes, :exoneration, :net_total
|
8
|
+
|
9
|
+
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 }
|
13
|
+
validates :unit_price, presence: true
|
14
|
+
validates :total, presence: true
|
15
|
+
validates :discount, numericality: { grater_than: 0}, if: ->{ discount.present? }
|
16
|
+
validates :discount_reason, presence: true, if: ->{ discount.present? }
|
17
|
+
validates :subtotal, presence: true
|
18
|
+
validates :net_total, presence: true
|
19
|
+
|
20
|
+
|
21
|
+
def initialize(args={})
|
22
|
+
@line_number = args[:line_number]
|
23
|
+
@code = args[:code]
|
24
|
+
@quantity = args[:quantity]
|
25
|
+
@unit = args[:unit]
|
26
|
+
@description = args[:description]
|
27
|
+
@unit_price = args[:unit_price]
|
28
|
+
@total = args[:total]
|
29
|
+
@discount = args[:discount]
|
30
|
+
@discount_reason = args[:discount_reason]
|
31
|
+
@subtotal = args[:subtotal]
|
32
|
+
@taxes = args[:taxes] || []
|
33
|
+
@net_total = args[:net_total]
|
34
|
+
end
|
35
|
+
|
36
|
+
def build_xml(node)
|
37
|
+
raise "Item invalid: #{errors.messages}" unless valid?
|
38
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
39
|
+
node.LineaDetalle do |x|
|
40
|
+
x.NumeroLinea @line_number
|
41
|
+
if @code.present?
|
42
|
+
x.Codigo do |x2|
|
43
|
+
x2.Tipo "01"
|
44
|
+
x2.Codigo @code
|
45
|
+
end
|
46
|
+
end
|
47
|
+
x.Cantidad @quantity
|
48
|
+
x.UnidadMedida @unit
|
49
|
+
x.Detalle @description
|
50
|
+
x.PrecioUnitario @unit_price
|
51
|
+
x.MontoTotal @total
|
52
|
+
x.Discount @discount if @discount.present?
|
53
|
+
x.NaturalezaDescuento @discount_reason if @discount_reason.present?
|
54
|
+
x.SubTotal @subtotal
|
55
|
+
@taxes.each do |tax|
|
56
|
+
tax.build_xml(x)
|
57
|
+
end
|
58
|
+
if @exoneration.present?
|
59
|
+
@exoneration.build_xml(x)
|
60
|
+
end
|
61
|
+
x.MontoTotalLinea @net_total
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
@@ -0,0 +1,49 @@
|
|
1
|
+
require "facturacr/document"
|
2
|
+
require 'active_model'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module FE
|
6
|
+
class Document
|
7
|
+
|
8
|
+
|
9
|
+
class Location
|
10
|
+
include ActiveModel::Validations
|
11
|
+
|
12
|
+
attr_accessor :province, :county,:district,:neighborhood, :others
|
13
|
+
|
14
|
+
validates :province, presence: true, length: { is: 1 }
|
15
|
+
validates :county, presence: true, length: { is: 2 }
|
16
|
+
validates :district, presence: true, length: { is: 2 }
|
17
|
+
validates :neighborhood, length: { is: 2 }, allow_blank: true
|
18
|
+
validates :others, presence: true, length: { maximum: 160 }
|
19
|
+
|
20
|
+
def initialize(args={})
|
21
|
+
|
22
|
+
@province = args[:province]
|
23
|
+
@county = args[:county]
|
24
|
+
@district = args[:district]
|
25
|
+
@neighborhood = args[:neighborhood]
|
26
|
+
@others = args[:others]
|
27
|
+
|
28
|
+
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_xml(node)
|
32
|
+
raise "Location invalid: #{errors.messages}" unless valid?
|
33
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
34
|
+
node.Ubicacion do |x|
|
35
|
+
x.Provincia @province
|
36
|
+
x.Canton @county
|
37
|
+
x.Distrito @district
|
38
|
+
x.Barrio @neighborhood unless @neighborhood.nil?
|
39
|
+
x.OtrasSenas @others
|
40
|
+
end
|
41
|
+
end
|
42
|
+
|
43
|
+
def to_xml(builder)
|
44
|
+
build_xml(builder).to_xml
|
45
|
+
end
|
46
|
+
|
47
|
+
end
|
48
|
+
end
|
49
|
+
end
|
@@ -0,0 +1,22 @@
|
|
1
|
+
require "facturacr/document"
|
2
|
+
require 'facturacr/document/phone_type'
|
3
|
+
require 'active_model'
|
4
|
+
|
5
|
+
module FE
|
6
|
+
class Document
|
7
|
+
class Phone < PhoneType
|
8
|
+
include ActiveModel::Validations
|
9
|
+
|
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={})
|
17
|
+
super('Telefono', args[:country_code],args[:number])
|
18
|
+
end
|
19
|
+
|
20
|
+
end
|
21
|
+
end
|
22
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
require "facturacr/document"
|
2
|
+
require 'active_model'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module FE
|
6
|
+
class Document
|
7
|
+
class PhoneType
|
8
|
+
include ActiveModel::Validations
|
9
|
+
|
10
|
+
attr_accessor :tag_name, :country_code, :number
|
11
|
+
|
12
|
+
validates :tag_name, presence: true, inclusion: ['Telefono','Fax']
|
13
|
+
validates :country_code, presence: true, length: { maximum: 3 }
|
14
|
+
validates :number, presence: true, length: {maximum: 20}, format: {with: /\d+/}
|
15
|
+
|
16
|
+
|
17
|
+
def initialize(tag_name, country_code, number)
|
18
|
+
@tag_name = tag_name
|
19
|
+
@country_code = country_code
|
20
|
+
@number = number
|
21
|
+
end
|
22
|
+
|
23
|
+
def build_xml(node)
|
24
|
+
raise "Invalid Record: #{errors.messages}" unless valid?
|
25
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
26
|
+
node.send(tag_name) do |xml|
|
27
|
+
xml.CodigoPais country_code
|
28
|
+
xml.NumTelefono number
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
def to_xml(builder)
|
33
|
+
build_xml(builder).to_xml
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
require "facturacr/document"
|
2
|
+
require 'active_model'
|
3
|
+
require 'nokogiri'
|
4
|
+
|
5
|
+
module FE
|
6
|
+
class Document
|
7
|
+
|
8
|
+
|
9
|
+
class Receiver
|
10
|
+
include ActiveModel::Validations
|
11
|
+
|
12
|
+
attr_accessor :name, :identification_document,:foreign_id_number, :comercial_name, :location, :phone, :fax, :email
|
13
|
+
|
14
|
+
validates :name, presence: true, length: { maximum: 80 }
|
15
|
+
validates :comercial_name, length: { maximum: 80 }
|
16
|
+
validates :foreign_id_number, length: { maximum: 20 }
|
17
|
+
|
18
|
+
|
19
|
+
|
20
|
+
def initialize(args={})
|
21
|
+
|
22
|
+
@name = args[:name]
|
23
|
+
@identification_document = args[:identification_document]
|
24
|
+
@comercial_name = args[:comercial_name]
|
25
|
+
@location = args[:location]
|
26
|
+
@phone = args[:phone]
|
27
|
+
@fax = args[:fax]
|
28
|
+
@email = args[:email]
|
29
|
+
@foreign_id_number = args[:foreign_id_number]
|
30
|
+
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_xml(node)
|
34
|
+
raise "IdentificationDocument is invalid" if !@identification_document.nil? && !@identification_document.is_a?(IdentificationDocument)
|
35
|
+
raise "Location is invalid" if !@location.nil? && !@location.is_a?(Location)
|
36
|
+
raise "Phone is invalid" if !@phone.nil? && !@phone.is_a?(Phone)
|
37
|
+
raise "Fax is invalid" if !@fax.nil? && !@fax.is_a?(Fax)
|
38
|
+
raise "Reciever is invalid: #{errors.messages}" unless valid?
|
39
|
+
|
40
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
41
|
+
node.Receptor do |xml|
|
42
|
+
xml.Nombre @name
|
43
|
+
@identification_document.build_xml(xml) if @identification_document.present?
|
44
|
+
xml.IdentificacionExtranjer foreign_id_number if @foreign_id_number.present?
|
45
|
+
xml.NombreComercial @comercial_name if @comercial_name.present?
|
46
|
+
@location.build_xml(xml) if @location.present?
|
47
|
+
@phone.build_xml(xml) if @phone.present?
|
48
|
+
@fax.build_xml(xml) if @fax.present?
|
49
|
+
xml.CorreElectronico @email if @email.present?
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def to_xml(builder)
|
54
|
+
build_xml(builder).to_xml
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
end
|
59
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
require 'facturacr/document'
|
2
|
+
|
3
|
+
module FE
|
4
|
+
class Document
|
5
|
+
class Reference
|
6
|
+
include ActiveModel::Validations
|
7
|
+
|
8
|
+
attr_accessor :document_type, :number, :date, :code, :reason
|
9
|
+
|
10
|
+
REFERENCE_CODES = {
|
11
|
+
"01" => "Anula Documento de referencia",
|
12
|
+
"02" => "Corrige texto documento de referencia",
|
13
|
+
"03" => "Corrige monto",
|
14
|
+
"04" => "Referencia a otro documento",
|
15
|
+
"05" => "Sustituye comprobante provisional por contingencia",
|
16
|
+
"99" => "Otros"
|
17
|
+
}
|
18
|
+
|
19
|
+
validates :document_type, presence: true, inclusion: FE::Document::DOCUMENT_TYPES.keys
|
20
|
+
validates :number, presence: true, length: {is: 50}
|
21
|
+
validates :date, presence: true
|
22
|
+
validates :code, presence: true, length: {is: 2}, inclusion: REFERENCE_CODES.keys
|
23
|
+
validates :reason, presence: true, length: {maximum: 180}
|
24
|
+
|
25
|
+
def initialize(args={})
|
26
|
+
@document_type = args[:document_type]
|
27
|
+
@number = args[:number]
|
28
|
+
@date = args[:date]
|
29
|
+
@code = args[:code]
|
30
|
+
@reason = args[:reason]
|
31
|
+
end
|
32
|
+
|
33
|
+
def build_xml(node)
|
34
|
+
raise "Reference Invalid: #{errors.messages}" unless valid?
|
35
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
36
|
+
node.InformacionReferencia do |xml|
|
37
|
+
xml.TipoDoc @document_type
|
38
|
+
xml.Numero @number
|
39
|
+
xml.FechaEmision @date.xmlschema
|
40
|
+
xml.Codigo @code
|
41
|
+
xml.Razon @reason
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,26 @@
|
|
1
|
+
module FE
|
2
|
+
class Document
|
3
|
+
class Regulation
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
attr_accessor :number, :date
|
7
|
+
|
8
|
+
validates :number, presence: true
|
9
|
+
validates :date, presence: true
|
10
|
+
|
11
|
+
def initialize(args={})
|
12
|
+
@number = args[:number] ||= "DGT-R-48-2016"
|
13
|
+
@date = args[:date] ||= "20-02-2017 13:22:22"
|
14
|
+
end
|
15
|
+
|
16
|
+
def build_xml(node)
|
17
|
+
raise "Regulation Invalid: #{errors.messages}" unless valid?
|
18
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
19
|
+
node.Normativa do |xml|
|
20
|
+
xml.NumeroResolucion @number
|
21
|
+
xml.FechaResolucion @date
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
@@ -0,0 +1,64 @@
|
|
1
|
+
module FE
|
2
|
+
class Document
|
3
|
+
class Summary
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
attr_accessor :currency, :exchange_rate, :services_taxable_total, :services_exent_total,
|
7
|
+
:goods_taxable_total,:goods_exent_total, :taxable_total, :exent_total,
|
8
|
+
:subtotal, :discount_total, :gross_total, :tax_total, :net_total
|
9
|
+
|
10
|
+
validates :exchange_rate, presence: true, if: -> { currency.present? }
|
11
|
+
|
12
|
+
validate :totals_ok?
|
13
|
+
|
14
|
+
def initialize(args={})
|
15
|
+
@currency = args[:currency]
|
16
|
+
@exchange_rate = args[:exchange_rate]
|
17
|
+
@services_taxable_total = args[:services_taxable_total].to_f
|
18
|
+
@services_exent_total = args[:services_exent_total].to_f
|
19
|
+
@goods_taxable_total = args[:goods_taxable_total].to_f
|
20
|
+
@goods_exent_total = args[:goods_exent_total].to_f
|
21
|
+
@taxable_total = args[:taxable_total].to_f
|
22
|
+
@exent_total = args[:exent_total].to_f
|
23
|
+
@subtotal = args[:subtotal].to_f
|
24
|
+
@discount_total = args[:discount_total].to_f
|
25
|
+
@gross_total = args[:gross_total].to_f
|
26
|
+
@tax_total = args[:tax_total].to_f
|
27
|
+
@net_total = args[:net_total].to_f
|
28
|
+
end
|
29
|
+
|
30
|
+
def build_xml(node)
|
31
|
+
unless valid?
|
32
|
+
raise "Summary invalid: #{errors.messages}"
|
33
|
+
end
|
34
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
35
|
+
|
36
|
+
node.ResumenFactura do |xml|
|
37
|
+
xml.CodigoMoneda @currency if @currency.present?
|
38
|
+
xml.TipoCambio @exchange_rate if @exchange_rate.present?
|
39
|
+
xml.TotalServGravados @services_taxable_total
|
40
|
+
xml.TotalServExentos @services_exent_total
|
41
|
+
xml.TotalMercanciasGravadas @goods_taxable_total
|
42
|
+
xml.TotalMercanciasExentas @goods_exent_total
|
43
|
+
xml.TotalGravado @taxable_total
|
44
|
+
xml.TotalExento @exent_total
|
45
|
+
xml.TotalVenta @subtotal
|
46
|
+
xml.TotalDescuentos @discount_total
|
47
|
+
xml.TotalVentaNeta @gross_total
|
48
|
+
xml.TotalImpuesto @tax_total
|
49
|
+
xml.TotalComprobante @net_total
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
private
|
54
|
+
|
55
|
+
def totals_ok?
|
56
|
+
errors[:taxable_total] << "invalid amount" unless @taxable_total == @services_taxable_total + @goods_taxable_total
|
57
|
+
errors[:exent_total] << "invalid amount" unless @exent_total == @services_exent_total + @goods_exent_total
|
58
|
+
errors[:subtotal] << "invalid amount" unless @subtotal == @taxable_total + @exent_total
|
59
|
+
errors[:gross_total] << "invalid amount" unless @gross_total == @subtotal - @discount_total
|
60
|
+
errors[:net_total] << "invalid amount" unless @net_total == @gross_total + @tax_total
|
61
|
+
end
|
62
|
+
end
|
63
|
+
end
|
64
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
module FE
|
2
|
+
class Document
|
3
|
+
class Tax
|
4
|
+
include ActiveModel::Validations
|
5
|
+
|
6
|
+
TAX_CODES = {
|
7
|
+
"01"=>"Impuesto General sobre las Ventas",
|
8
|
+
"02"=>"Impuesto Selectivo de Consumo",
|
9
|
+
"03"=>"Impuesto Único a los combustibles",
|
10
|
+
"04"=>"Impuesto específico de bebidas alcohólicas",
|
11
|
+
"05"=>"Impuesto Específico sobre las bebidas envasadas sin contenido alcóholico y jabones de tocador",
|
12
|
+
"06"=>"Impuesto a los Productos de Tabaco",
|
13
|
+
"07"=>"Servicio",
|
14
|
+
"12"=>"Impuesto específico al cemento",
|
15
|
+
"98"=>"Otros",
|
16
|
+
"08"=>"Impuesto General sobre las ventas diplomáticos",
|
17
|
+
"09"=>"Impuesto general sobre las ventas Compras autorizadas",
|
18
|
+
"10"=>"Impuesto general sobre las ventas instituciones públicas y otros organismos",
|
19
|
+
"11"=>"Impuesto Selectivo de Consumo Compras Autorizadas",
|
20
|
+
"99"=>"Otros"
|
21
|
+
}
|
22
|
+
attr_accessor :code, :rate, :total
|
23
|
+
|
24
|
+
validates :code, presence: true, inclusion: TAX_CODES.keys
|
25
|
+
def initialize(args={})
|
26
|
+
@code = args[:code]
|
27
|
+
@rate = args[:rate]
|
28
|
+
@total = args[:total]
|
29
|
+
end
|
30
|
+
|
31
|
+
def build_xml(node)
|
32
|
+
raise "Invalida Record: #{errors.messages}" unless valid?
|
33
|
+
node = Nokogiri::XML::Builder.new if node.nil?
|
34
|
+
|
35
|
+
node.Impuesto do |xml|
|
36
|
+
xml.Codigo @code
|
37
|
+
xml.Tarifa @rate
|
38
|
+
xml.Monto @total
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,34 @@
|
|
1
|
+
require 'facturacr/document'
|
2
|
+
|
3
|
+
module FE
|
4
|
+
|
5
|
+
class Invoice < Document
|
6
|
+
|
7
|
+
def initialize(args={})
|
8
|
+
@date = args[:date]
|
9
|
+
@issuer = args[:issuer]
|
10
|
+
@receiver = args[:receiver]
|
11
|
+
@items = args[:items]
|
12
|
+
@number = args[:number]
|
13
|
+
@condition = args[:condition]
|
14
|
+
@payment_type = args[:payment_type] || "01"
|
15
|
+
@document_type = "01"
|
16
|
+
@credit_term = args[:credit_term]
|
17
|
+
@summary = args[:summary]
|
18
|
+
@regulation = args[:regulation] ||= FE::Document::Regulation.new
|
19
|
+
@security_code = args[:security_code]
|
20
|
+
@document_situation = args[:document_situation]
|
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/facturaElectronica"#,
|
25
|
+
#"xsi:schemaLocation"=>"https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/facturaElectronica https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/facturaElectronica.xsd"
|
26
|
+
}
|
27
|
+
end
|
28
|
+
|
29
|
+
def document_tag
|
30
|
+
"FacturaElectronica"
|
31
|
+
end
|
32
|
+
|
33
|
+
end
|
34
|
+
end
|