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