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,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
|