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,19 @@
1
+ require 'base64'
2
+
3
+ module FE
4
+ class SignedDocument
5
+ attr_accessor :document, :path, :base64, :payload
6
+
7
+ def initialize(document, path)
8
+ @document = document
9
+ @path = path
10
+ @base64 = nil
11
+ if File.exist?(@path)
12
+ file = File.open(@path,"rb")
13
+ @base64 = Base64.encode64(file.read).gsub("\n","");
14
+ end
15
+ @payload = document.api_payload
16
+ @payload[:comprobanteXml] = @base64
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,245 @@
1
+ require 'openssl'
2
+ require 'base64'
3
+ require "rexml/document"
4
+ require "rexml/xpath"
5
+
6
+ module FE
7
+ class Signer
8
+ REXML::Document::entity_expansion_limit = 0
9
+ C14N = "http://www.w3.org/TR/2001/REC-xml-c14n-20010315" #"http://www.w3.org/2001/10/xml-exc-c14n#"
10
+ DSIG = "http://www.w3.org/2000/09/xmldsig#"
11
+ NOKOGIRI_OPTIONS = Nokogiri::XML::ParseOptions::STRICT | Nokogiri::XML::ParseOptions::NONET | Nokogiri::XML::ParseOptions::NOENT
12
+ RSA_SHA1 = "http://www.w3.org/2000/09/xmldsig#rsa-sha1"
13
+ RSA_SHA256 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha256"
14
+ RSA_SHA384 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha384"
15
+ RSA_SHA512 = "http://www.w3.org/2001/04/xmldsig-more#rsa-sha512"
16
+ SHA1 = "http://www.w3.org/2000/09/xmldsig#sha1"
17
+ SHA256 = "http://www.w3.org/2001/04/xmlenc#sha256"
18
+ SHA384 = "http://www.w3.org/2001/04/xmldsig-more#sha384"
19
+ SHA512 = "http://www.w3.org/2001/04/xmlenc#sha512"
20
+ ENVELOPED_SIG = "http://www.w3.org/2000/09/xmldsig#enveloped-signature"
21
+ INC_PREFIX_LIST = "#default samlp saml ds xs xsi md"
22
+ NAMESPACES = "#default ds xs xsi xades xsd"
23
+
24
+ XADES = "http://uri.etsi.org/01903/v1.3.2#"
25
+ XADES141 = "http://uri.etsi.org/01903/v1.4.1#"
26
+ SIGNATURE_POLICY = "https://tribunet.hacienda.go.cr/docs/esquemas/2016/v4/Resolucion%20Comprobantes%20Electronicos%20%20DGT-R-48-2016.pdf"
27
+
28
+
29
+ def initialize(key_path, key_password,input_xml, output_path=nil)
30
+ @doc = REXML::Document.new(File.read(input_xml))
31
+ @doc.context[:attribute_quote] = :quote
32
+ @doc << REXML::XMLDecl.new(REXML::XMLDecl::DEFAULT_VERSION,REXML::XMLDecl::DEFAULT_ENCODING, REXML::XMLDecl::DEFAULT_STANDALONE)
33
+ @p12 = OpenSSL::PKCS12.new(File.read("tmp/pruebas.p12"),"8753")
34
+ @x509 = @p12.certificate
35
+ @output_path = output_path
36
+ end
37
+
38
+ def sign
39
+ #Build parts for Digest Calculation
40
+ key_info = build_key_info_element
41
+ signed_properties = build_signed_properties_element
42
+ signed_info_element = build_signed_info_element(key_info,signed_properties)
43
+ # Compute Signature
44
+ signed_info_canon = canonicalize_document(signed_info_element)
45
+ signature_value = compute_signature(@p12.key,algorithm(RSA_SHA256).new,signed_info_canon)
46
+
47
+ # delete parts namespaces
48
+ delete_namespaces(signed_info_element)
49
+ delete_namespaces(key_info)
50
+ delete_namespaces(signed_properties)
51
+
52
+ # Created Signature element and add parts
53
+ signature_element = REXML::Element.new("ds:Signature").add_namespace('ds', DSIG)
54
+ signature_element.add_attribute("Id","xmldsig-#{uuid}")
55
+
56
+ signature_element.add_element(signed_info_element)
57
+ signature_element.add_element("ds:SignatureValue","Id"=>"xmldsig-#{uuid}-sigvalue").text = signature_value
58
+ signature_element.add_element(key_info)
59
+
60
+ object = signature_element.add_element("ds:Object")
61
+ qualifying_properties = object.add_element("xades:QualifyingProperties", {"Target"=>"#xmldsig-#{uuid}"})
62
+ qualifying_properties.add_namespace("xades", XADES)
63
+ qualifying_properties.add_namespace("xades141", XADES141)
64
+
65
+ qualifying_properties.add_element(signed_properties)
66
+
67
+ @doc.root.add_element(signature_element)
68
+
69
+ File.open(@output_path,"w"){|f| f.write(@doc.to_s)} if @output_path
70
+
71
+ @doc
72
+ end
73
+
74
+ private
75
+
76
+ def build_signed_properties_element
77
+ cert_digest = compute_digest(@x509.to_der,algorithm(SHA256))
78
+ policy_digest = compute_digest(@x509.to_der,algorithm(SHA256))
79
+ signing_time = DateTime.now
80
+
81
+ element = REXML::Element.new("xades:SignedProperties",nil,{:attribute_quote=>:quote})
82
+ element.add_namespace("https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/facturaElectronica")
83
+ element.add_namespace("ds","http://www.w3.org/2000/09/xmldsig#")
84
+ element.add_namespace("xades","http://uri.etsi.org/01903/v1.3.2#")
85
+ element.add_namespace("xsd","http://www.w3.org/2001/XMLSchema")
86
+ element.add_namespace("xsi","http://www.w3.org/2001/XMLSchema-instance")
87
+ element.add_namespace("xades141", XADES141)
88
+ element.add_attribute("Id","xmldsig-#{uuid}-signedprops")
89
+
90
+ element.add_element("xades:SigningTime").text = signing_time.rfc3339
91
+ signing_certificate_elem = element.add_element("xades:SigningCertificate")
92
+ cert_elem = signing_certificate_elem.add_element("xades:Cert")
93
+ cert_digest_elem = cert_elem.add_element("xades:CertDigest")
94
+ cert_digest_elem.add_element("ds:DigestMethod", {"Algorithm"=>SHA256})
95
+ cert_digest_elem.add_element("ds:DigestValue").text = cert_digest
96
+
97
+ issuer_serial = cert_elem.add_element("xades:IssuerSerial")
98
+ issuer_serial.add_element("ds:X509IssuerName").text = @x509.issuer.to_a.reverse.map{|c| c[0..1].join("=")}.join(", ")
99
+ issuer_serial.add_element("ds:X509SerialNumber").text = @x509.serial.to_s
100
+
101
+ policy_elem = element.add_element("xades:SignaturePolicyIdentifier")
102
+ policy_id_elem = policy_elem.add_element("xades:SignaturePolicyId")
103
+ sig_policy_id = policy_id_elem.add_element("xades:SigPolicyId")
104
+ sig_policy_id.add_element("xades:Identifier").text = SIGNATURE_POLICY
105
+
106
+ policy_hash = sig_policy_id.add_element("xades:SigPolicyHash")
107
+ policy_hash.add_element("ds:DigestMethod", {"Algorithm"=>"http://www.w3.org/2000/09/xmldsig#sha1"})
108
+ policy_hash.add_element("ds:DigestValue").text = "V8lVVNGDCPen6VELRD1Ja8HARFk="#policy_digest
109
+
110
+ element
111
+ end
112
+
113
+ def build_key_info_element
114
+ key_info = REXML::Element.new("ds:KeyInfo",nil,{:attribute_quote=>:quote})
115
+ key_info.add_namespace("https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/facturaElectronica")
116
+ key_info.add_namespace("ds","http://www.w3.org/2000/09/xmldsig#")
117
+ key_info.add_namespace("xsd","http://www.w3.org/2001/XMLSchema")
118
+ key_info.add_namespace("xsi","http://www.w3.org/2001/XMLSchema-instance")
119
+
120
+ key_info.add_attribute("Id","xmldsig-#{uuid}-keyinfo")
121
+ x509_data = key_info.add_element("ds:X509Data")
122
+ x509_data.add_element("ds:X509Certificate").text = @x509.to_pem.to_s.gsub("-----BEGIN CERTIFICATE-----","").gsub("-----END CERTIFICATE-----","").gsub(/\n|\r/, "")
123
+ key_value = key_info.add_element("ds:KeyValue")
124
+ rsa_value = key_value.add_element("ds:RSAKeyValue")
125
+ rsa_value.add_element("ds:Modulus").text = Base64.encode64(@x509.public_key.params["n"].to_s(2)).gsub("\n","")
126
+ rsa_value.add_element("ds:Exponent").text = Base64.encode64(@x509.public_key.params["e"].to_s(2)).gsub("\n","")
127
+ key_info
128
+ end
129
+
130
+ def build_signed_info_element(key_info_element, signed_props_element)
131
+
132
+ signed_info_element = REXML::Element.new("ds:SignedInfo",nil,{:attribute_quote=>:quote})
133
+ signed_info_element.add_namespace("https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/facturaElectronica")
134
+ signed_info_element.add_namespace("ds","http://www.w3.org/2000/09/xmldsig#")
135
+ signed_info_element.add_namespace("xsd","http://www.w3.org/2001/XMLSchema")
136
+ signed_info_element.add_namespace("xsi","http://www.w3.org/2001/XMLSchema-instance")
137
+ signed_info_element.add_element("ds:CanonicalizationMethod", { "Algorithm"=>C14N })
138
+ signed_info_element.add_element("ds:SignatureMethod", {"Algorithm"=>RSA_SHA256})
139
+
140
+ # Add Ref0
141
+ ref0 = signed_info_element.add_element("ds:Reference",{"Id"=>"xmldsig-#{uuid}-ref0"})
142
+ transforms = ref0.add_element("ds:Transforms")
143
+ transform = transforms.add_element("ds:Transform", {"Algorithm"=>ENVELOPED_SIG})
144
+ digest_method_element = ref0.add_element("ds:DigestMethod", {"Algorithm"=> SHA256})
145
+ ref0.add_element("ds:DigestValue").text = digest_document(@doc,SHA256)
146
+
147
+ # Add KeyInfo Ref
148
+ ref_key_info = signed_info_element.add_element("ds:Reference",{"URI"=>"#xmldsig-#{uuid}-keyinfo"})
149
+ ref_key_info.add_element("ds:DigestMethod", {"Algorithm"=> SHA256})
150
+ ref_key_info.add_element("ds:DigestValue").text = digest_document(key_info_element, SHA256, true)
151
+
152
+ # Add SignedProps Ref
153
+ ref_props = signed_info_element.add_element("ds:Reference",{"URI"=>"#xmldsig-#{uuid}-signedprops", "Type"=>"http://uri.etsi.org/01903#SignedProperties"})
154
+ ref_props.add_element("ds:DigestMethod", {"Algorithm"=> SHA256})
155
+ ref_props.add_element("ds:DigestValue").text = digest_document(signed_props_element, SHA256,true)
156
+
157
+ signed_info_element
158
+ end
159
+
160
+ def digest_document(doc, digest_algorithm=SHA256, strip=false)
161
+ compute_digest(canonicalize_document(doc,strip),algorithm(digest_algorithm))
162
+ end
163
+
164
+ def canonicalize_document(doc,strip=false)
165
+ doc = doc.to_s if doc.is_a?(REXML::Element)
166
+ doc.strip! if strip
167
+ doc.encode("UTF-8")
168
+ noko = Nokogiri::XML(doc) do |config|
169
+ config.options = NOKOGIRI_OPTIONS
170
+ end
171
+
172
+ noko.canonicalize(canon_algorithm(C14N),NAMESPACES.split(" "))
173
+ end
174
+
175
+ def delete_namespaces(element)
176
+ NAMESPACES.split(" ").each do |ns|
177
+ element.delete_namespace(ns)
178
+ end
179
+ end
180
+
181
+ def uuid
182
+ @uuid ||= SecureRandom.uuid
183
+ end
184
+
185
+ def canon_algorithm(element)
186
+ algorithm = element
187
+ if algorithm.is_a?(REXML::Element)
188
+ algorithm = element.attribute('Algorithm').value
189
+ end
190
+
191
+ case algorithm
192
+ when "http://www.w3.org/TR/2001/REC-xml-c14n-20010315",
193
+ "http://www.w3.org/TR/2001/REC-xml-c14n-20010315#WithComments"
194
+ Nokogiri::XML::XML_C14N_1_0
195
+ when "http://www.w3.org/2006/12/xml-c14n11",
196
+ "http://www.w3.org/2006/12/xml-c14n11#WithComments"
197
+ Nokogiri::XML::XML_C14N_1_1
198
+ else
199
+ Nokogiri::XML::XML_C14N_EXCLUSIVE_1_0
200
+ end
201
+ end
202
+
203
+ def algorithm(element)
204
+ algorithm = element
205
+ if algorithm.is_a?(REXML::Element)
206
+ algorithm = element.attribute("Algorithm").value
207
+ end
208
+
209
+ algorithm = algorithm && algorithm =~ /(rsa-)?sha(.*?)$/i && $2.to_i
210
+
211
+ case algorithm
212
+ when 256 then OpenSSL::Digest::SHA256
213
+ when 384 then OpenSSL::Digest::SHA384
214
+ when 512 then OpenSSL::Digest::SHA512
215
+ else
216
+ OpenSSL::Digest::SHA1
217
+ end
218
+ end
219
+
220
+ def compute_signature(private_key, signature_algorithm, document)
221
+ Base64.encode64(private_key.sign(signature_algorithm, document)).gsub(/\n/, "")
222
+ end
223
+
224
+ def compute_digest(document, digest_algorithm)
225
+ digest = digest_algorithm.digest(document)
226
+ Base64.encode64(digest).strip!
227
+ end
228
+
229
+ end
230
+
231
+ class JavaSigner
232
+
233
+ def initialize(key_file,password,path,out_path)
234
+ @key_file = key_file
235
+ @password = password
236
+ @path = path
237
+ @out_path = out_path
238
+ end
239
+
240
+ def sign
241
+ null_device = Gem.win_platform? ? "/nul" : "/dev/null"
242
+ system("java -jar #{FE.bin}/signer/signer.jar #{@key_file} #{@password} #{@path} #{@out_path} 1>#{null_device} 2>#{null_device}")
243
+ end
244
+ end
245
+ end
@@ -0,0 +1,34 @@
1
+ require 'facturacr/document'
2
+
3
+ module FE
4
+
5
+ class Ticket < 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 = "04"
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/tiqueteElectronico",
25
+ "xsi:schemaLocation"=>"https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/tiqueteElectronico https://tribunet.hacienda.go.cr/docs/esquemas/2017/v4.2/tiqueteElectronico.xsd"
26
+ }
27
+ end
28
+
29
+ def document_tag
30
+ "TiqueteElectronico"
31
+ end
32
+
33
+ end
34
+ end
@@ -0,0 +1,3 @@
1
+ module FE
2
+ VERSION = "0.1.2"
3
+ end
@@ -0,0 +1,161 @@
1
+ require 'nokogiri'
2
+
3
+ require 'facturacr/document'
4
+
5
+ module FE
6
+ class XmlDocument
7
+
8
+ attr_accessor :document, :root_tag
9
+
10
+ def initialize(xml_path)
11
+ if File.exist?(xml_path)
12
+ @doc = Nokogiri::XML(File.open(xml_path,"rb")) do |config|
13
+ config.options = Nokogiri::XML::ParseOptions::NOBLANKS | Nokogiri::XML::ParseOptions::NOENT
14
+ end
15
+ root_tag = @doc.elements.first.name
16
+
17
+
18
+ if root_tag.eql?('FacturaElectronica')
19
+ @document = FE::Invoice.new
20
+ elsif root_tag.eql?("NotaCreditoElectronica")
21
+ @document = FE::CreditNote.new
22
+ elsif root_tag.eql?("NotaDebitoElectronica")
23
+ @document = FE::DebitNote.new
24
+ end
25
+ @document.date = DateTime.parse(@doc.css("#{root_tag} FechaEmision").text)
26
+ @key = @doc.css("#{root_tag} Clave").text
27
+ @document.number = @key[31..40].to_i
28
+ @document.document_situation = @key[41]
29
+ @document.security_code = @key[42..-1]
30
+ @document.condition = @doc.css("#{root_tag} CondicionVenta").text
31
+ @document.credit_term = @doc.css("#{root_tag} PlazoCredito").text unless @doc.css("#{root_tag} PlazoCredito").empty?
32
+ @document.payment_type = @doc.css("#{root_tag} MedioPago").first.text
33
+
34
+ @issuer = FE::Document::Issuer.new
35
+ @issuer.identification_document = FE::Document::IdentificationDocument.new type: @doc.css("#{root_tag} Emisor Identificacion Tipo").text, number: @doc.css("#{root_tag} Emisor Identificacion Numero").text.to_i
36
+ @issuer.name = @doc.css("#{root_tag} Emisor Nombre").text
37
+ @issuer.comercial_name = @doc.css("#{root_tag} Emisor NombreComercial").text unless @doc.css("#{root_tag} Emisor NombreComercial").empty?
38
+ location = FE::Document::Location.new
39
+ location.province = @doc.css("#{root_tag} Emisor Ubicacion Provincia").text
40
+ location.county = @doc.css("#{root_tag} Emisor Ubicacion Canton").text
41
+ location.district = @doc.css("#{root_tag} Emisor Ubicacion Distrito").text
42
+ location.others = @doc.css("#{root_tag} Emisor Ubicacion OtrasSenas").text
43
+ @issuer.location = location
44
+
45
+ if !@doc.css("#{root_tag} Emisor Telefono").empty?
46
+ @issuer.phone = FE::Document::Phone.new country_code: @doc.css("#{root_tag} Emisor Telefono CodigoPais").text, number: @doc.css("#{root_tag} Emisor Telefono CodigoPais").text
47
+ end
48
+ if !@doc.css("#{root_tag} Emisor Fax").empty?
49
+ @issuer.fax = FE::Document::Phone.new country_code: @doc.css("#{root_tag} Emisor Telefono CodigoPais").text, number: @doc.css("#{root_tag} Emisor Telefono CodigoPais").text
50
+ end
51
+ @issuer.email = @doc.css("#{root_tag} Emisor CorreoElectronico").text
52
+
53
+ unless @doc.css("#{root_tag} Receptor").empty?
54
+ @receiver = FE::Document::Receiver.new
55
+ @receiver.name = @doc.css("#{root_tag} Receptor Nombre").text
56
+ unless @doc.css("#{root_tag} Receptor Identificacion").empty?
57
+ @receiver.identification_document = FE::Document::IdentificationDocument.new type: @doc.css("#{root_tag} Receptor Identificacion Tipo").text, number: @doc.css("#{root_tag} Receptor Identificacion Numero").text.to_i
58
+ end
59
+
60
+ unless @doc.css("#{root_tag} Receptor IdentificacionExtranjero").empty?
61
+ @receiver.foreign_id_number = @doc.css("#{root_tag} Receptor IdentificacionExtranjero").text
62
+ end
63
+ @receiver.comercial_name = @doc.css("#{root_tag} Receptor NombreComercial").text unless @doc.css("#{root_tag} Receptor NombreComercial").empty?
64
+
65
+ unless @doc.css("#{root_tag} Receptor Ubicacion").empty?
66
+ location = FE::Document::Location.new
67
+ location.province = @doc.css("#{root_tag} Receptor Ubicacion Provincia").text
68
+ location.county = @doc.css("#{root_tag} Receptor Ubicacion Canton").text
69
+ location.district = @doc.css("#{root_tag} Receptor Ubicacion Distrito").text
70
+ location.others = @doc.css("#{root_tag} Receptor Ubicacion OtrasSenas").text
71
+ @receiver.location = location
72
+ end
73
+
74
+ if !@doc.css("#{root_tag} Receptor Telefono").empty?
75
+ @issuer.phone = FE::Document::Phone.new country_code: @doc.css("#{root_tag} Receptor Telefono CodigoPais").text, number: @doc.css("#{root_tag} Receptor Telefono CodigoPais").text
76
+ end
77
+ if !@doc.css("#{root_tag} Receptor Fax").empty?
78
+ @receiver.fax = FE::Document::Phone.new country_code: @doc.css("#{root_tag} Receptor Telefono CodigoPais").text, number: @doc.css("#{root_tag} Receptor Telefono CodigoPais").text
79
+ end
80
+ @receiver.email = @doc.css("#{root_tag} Receptor CorreoElectronico").text unless @doc.css("#{root_tag} Receptor CorreoElectronico").empty?
81
+ end
82
+ @items = []
83
+ @doc.css("#{root_tag} DetalleServicio LineaDetalle").each do |line|
84
+ item = FE::Document::Item.new
85
+ item.line_number = line.css("NumeroLinea").text.to_i
86
+ item.code = line.css("Codigo Codigo").text
87
+ item.quantity = line.css("Cantidad").text
88
+ item.unit = line.css("UnidadMedida").text
89
+ item.description = line.css("Detalle").text
90
+ item.unit_price = line.css("PrecioUnitario").text.to_f
91
+ item.total = line.css("MontoTotal").text.to_f
92
+ item.discount = line.css("MontoDescuento").text.to_f unless line.css("MontoDescuento").empty?
93
+ item.discount_reason = line.css("NaturalezaDescuento").text unless line.css("NaturalezaDescuento").empty?
94
+ item.subtotal = line.css("SubTotal").text.to_f
95
+ item.net_total = line.css("MontoTotalLinea").text.to_f
96
+ item.taxes = []
97
+ line.css("Impuesto").each do |tax|
98
+ item.taxes << FE::Document::Tax.new(code: tax.css("Codigo").text, rate: tax.css("Tarifa").text.to_f, total: tax.css("Monto").text.to_f)
99
+ end
100
+ unless line.css("Exoneracion").empty?
101
+ exo = FE::Document::Exoneration.new
102
+ exo.document_type = line.css("Exoneracion TipoDocumento").text
103
+ exo.document_number = line.css("Exoneracion NumeroDocumento").text
104
+ exo.institution = line.css("Exoneracion NombreInstitucion").text
105
+ exo.date = DateTime.parse(line.css("Exoneracion FechaEmision").text)
106
+ exo.total_tax = line.css("Exoneracion MontoImpuesto").text.to_f
107
+ exo.percentage = line.css("Exoneracion PorcentajeCompra").text.to_i
108
+ item.exoneration = exo
109
+ end
110
+ @items << item
111
+ end
112
+
113
+
114
+ @summary = FE::Document::Summary.new
115
+ sum = @doc.css("#{root_tag} ResumenFactura")
116
+ @summary.currency = sum.css("CodigoMoneda").text
117
+ @summary.exchange_rate = sum.css("TipoCambio").text.to_f
118
+ @summary.services_taxable_total = sum.css("TotalServGravados").text.to_f
119
+ @summary.services_exent_total = sum.css("TotalServExentos").text.to_f
120
+ @summary.goods_taxable_total = sum.css("TotalMercanciasGravadas").text.to_f
121
+ @summary.goods_exent_total = sum.css("TotalMercanciasExentas").text.to_f
122
+ @summary.taxable_total = sum.css("TotalGravado").text.to_f
123
+ @summary.exent_total = sum.css("TotalExento").text.to_f
124
+ @summary.subtotal = sum.css("TotalVenta").text.to_f
125
+ @summary.discount_total = sum.css("TotalDescuentos").text.to_f
126
+ @summary.gross_total = sum.css("TotalVentaNeta").text.to_f
127
+ @summary.tax_total = sum.css("TotalImpuesto").text.to_f
128
+ @summary.net_total = sum.css("TotalComprobante").text.to_f
129
+
130
+ refs = @doc.css("#{root_tag} InformacionReferencia")
131
+ @references = []
132
+ unless refs.empty?
133
+ refs.each do |ref|
134
+ reference = FE::Document::Reference.new
135
+ reference.document_type = ref.css("TipoDoc")
136
+ reference.number = ref.css("Numero")
137
+ reference.date = ref.css("FechaEmision")
138
+ reference.code = ref.css("Codigo")
139
+ reference.reason = ref.css("Razon")
140
+ @references << reference
141
+ end
142
+ end
143
+
144
+ reg = @doc.css("#{root_tag} Normativa")
145
+ @regulation = FE::Document::Regulation.new
146
+ @regulation.number = reg.css("NumeroResolucion").text
147
+ @regulation.date = reg.css("FechaResolucion").text
148
+
149
+
150
+ @document.issuer = @issuer
151
+ @document.receiver = @receiver
152
+ @document.items = @items
153
+ @document.summary = @summary
154
+ @document.references = @references
155
+ @document.regulation = @regulation
156
+ else
157
+ raise "#{xml_path} does not exist"
158
+ end
159
+ end
160
+ end
161
+ end