sw_fac 0.1.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +8 -0
- data/.travis.yml +5 -0
- data/Gemfile +6 -0
- data/Gemfile.lock +26 -0
- data/LICENSE.txt +21 -0
- data/README.md +39 -0
- data/Rakefile +10 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/cadena/ComercioExterior11.xslt +181 -0
- data/lib/cadena/Pagos10.xslt +165 -0
- data/lib/cadena/TuristaPasajeroExtranjero.xslt +40 -0
- data/lib/cadena/aerolineas.xslt +50 -0
- data/lib/cadena/cadena33.xslt +349 -0
- data/lib/cadena/certificadodedestruccion.xslt +60 -0
- data/lib/cadena/cfdiregistrofiscal.xslt +19 -0
- data/lib/cadena/cfdv33.xsd +737 -0
- data/lib/cadena/consumodecombustibles.xslt +108 -0
- data/lib/cadena/detallista.xslt +42 -0
- data/lib/cadena/divisas.xslt +13 -0
- data/lib/cadena/donat11.xslt +13 -0
- data/lib/cadena/ecc11.xslt +102 -0
- data/lib/cadena/iedu.xslt +26 -0
- data/lib/cadena/implocal.xslt +39 -0
- data/lib/cadena/ine11.xslt +51 -0
- data/lib/cadena/leyendasFisc.xslt +28 -0
- data/lib/cadena/nomina12.xslt +412 -0
- data/lib/cadena/notariospublicos.xslt +301 -0
- data/lib/cadena/obrasarteantiguedades.xslt +33 -0
- data/lib/cadena/pagoenespecie.xslt +39 -0
- data/lib/cadena/pfic.xslt +13 -0
- data/lib/cadena/renovacionysustitucionvehiculos.xslt +152 -0
- data/lib/cadena/servicioparcialconstruccion.xslt +44 -0
- data/lib/cadena/terceros11.xslt +108 -0
- data/lib/cadena/utilerias.xslt +22 -0
- data/lib/cadena/valesdedespensa.xslt +70 -0
- data/lib/cadena/vehiculousado.xslt +63 -0
- data/lib/cadena/ventavehiculos11.xslt +53 -0
- data/lib/sw_fac/config.rb +69 -0
- data/lib/sw_fac/facturacion.rb +616 -0
- data/lib/sw_fac/tools.rb +105 -0
- data/lib/sw_fac/version.rb +3 -0
- data/lib/sw_fac.rb +18 -0
- data/sw_fac.gemspec +38 -0
- data/test/sw_fac_test.rb +11 -0
- data/test/test_helper.rb +4 -0
- metadata +146 -0
@@ -0,0 +1,616 @@
|
|
1
|
+
|
2
|
+
module SwFac
|
3
|
+
class Facturacion < Tools
|
4
|
+
|
5
|
+
def comp_pago(params={})
|
6
|
+
# Sample params
|
7
|
+
# params = {
|
8
|
+
# uuid: '',
|
9
|
+
# venta_folio: '',
|
10
|
+
# cp: '',
|
11
|
+
# receptor_razon: 'Car zone',
|
12
|
+
# receptor_rfc: 'XAXX010101000',
|
13
|
+
# forma_pago: '01',
|
14
|
+
# total: 100.00,
|
15
|
+
# time: '',
|
16
|
+
# modena: '',
|
17
|
+
# line_items: [
|
18
|
+
# {
|
19
|
+
# monto: 60.00,
|
20
|
+
# moneda: '',
|
21
|
+
# },
|
22
|
+
# ]
|
23
|
+
# }
|
24
|
+
|
25
|
+
raise 'Error - la suma de los complementos de pago es mayor al total reportado' if (params[:line_items].inject(0) {|sum, x| sum + x[:monto].to_f }) > params[:total].to_f
|
26
|
+
|
27
|
+
uri = @production ? URI("#{SwFac::UrlProduction}cfdi33/stamp/customv1/b64") : URI("#{SwFac::UrlDev}cfdi33/stamp/customv1/b64")
|
28
|
+
token = @production ? @production_token : @dev_token
|
29
|
+
time = params.fetch(:time, Time.now)
|
30
|
+
|
31
|
+
base_doc = %(<?xml version="1.0" encoding="UTF-8"?>
|
32
|
+
<cfdi:Comprobante xmlns:cfdi="http://www.sat.gob.mx/cfd/3" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:pago10="http://www.sat.gob.mx/Pagos" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd http://www.sat.gob.mx/Pagos http://www.sat.gob.mx/sitio_internet/cfd/Pagos/Pagos10.xsd" Version="3.3" SubTotal="0" Total="0" Moneda="XXX" TipoDeComprobante="P" >
|
33
|
+
<cfdi:Emisor />
|
34
|
+
<cfdi:Receptor UsoCFDI="P01"/>
|
35
|
+
<cfdi:Conceptos>
|
36
|
+
<cfdi:Concepto ClaveProdServ="84111506" Cantidad="1" ClaveUnidad="ACT" Descripcion="Pago" ValorUnitario="0" Importe="0" />
|
37
|
+
</cfdi:Conceptos>
|
38
|
+
<cfdi:Complemento>
|
39
|
+
<pago10:Pagos Version="1.0">
|
40
|
+
<pago10:Pago>
|
41
|
+
</pago10:Pago>
|
42
|
+
</pago10:Pagos>
|
43
|
+
</cfdi:Complemento>
|
44
|
+
</cfdi:Comprobante>)
|
45
|
+
|
46
|
+
base_doc.delete!("\n")
|
47
|
+
base_doc.delete!("\t")
|
48
|
+
|
49
|
+
xml = Nokogiri::XML(base_doc)
|
50
|
+
comprobante = xml.at_xpath("//cfdi:Comprobante")
|
51
|
+
comprobante['Serie'] = 'P'
|
52
|
+
comprobante['Folio'] = params[:venta_folio].to_s
|
53
|
+
comprobante['Fecha'] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
54
|
+
comprobante['LugarExpedicion'] = params[:cp].to_s
|
55
|
+
comprobante['NoCertificado'] = @serial
|
56
|
+
comprobante['Certificado'] = @cadena
|
57
|
+
emisor = xml.at_xpath("//cfdi:Emisor")
|
58
|
+
emisor['Rfc'] = @rfc
|
59
|
+
emisor['Nombre'] = @razon
|
60
|
+
emisor['RegimenFiscal'] = @regimen_fiscal
|
61
|
+
receptor = xml.at_xpath("//cfdi:Receptor")
|
62
|
+
receptor['Nombre'] = params[:receptor_razon].to_s
|
63
|
+
receptor['Rfc'] = params[:receptor_rfc].to_s
|
64
|
+
|
65
|
+
child_pago = xml.at_xpath("//pago10:Pago")
|
66
|
+
child_pago['FechaPago'] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
67
|
+
child_pago['FormaDePagoP'] = params[:forma_pago].to_s
|
68
|
+
child_pago['MonedaP'] = params.fetch(:moneda, 'MXN')
|
69
|
+
child_pago['Monto'] = params[:total].round(2).to_s
|
70
|
+
|
71
|
+
saldo_anterior = params[:total].to_f
|
72
|
+
|
73
|
+
params[:line_items].each_with_index do |line, index|
|
74
|
+
monto = line[:monto].to_f
|
75
|
+
child_pago_relacionado = Nokogiri::XML::Node.new "pago10:DoctoRelacionado", xml
|
76
|
+
child_pago_relacionado['IdDocumento'] = params[:uuid]
|
77
|
+
child_pago_relacionado['MonedaDR'] = line.fetch(:moneda, 'MXN')
|
78
|
+
child_pago_relacionado['MetodoDePagoDR'] = 'PPD'
|
79
|
+
child_pago_relacionado['NumParcialidad'] = (index + 1).to_s
|
80
|
+
|
81
|
+
child_pago_relacionado['ImpSaldoAnt'] = (saldo_anterior).to_s
|
82
|
+
child_pago_relacionado['ImpPagado'] = monto.round(2).to_s
|
83
|
+
child_pago_relacionado['ImpSaldoInsoluto'] = (saldo_anterior - monto).to_s
|
84
|
+
saldo_anterior -= monto
|
85
|
+
|
86
|
+
child_pago.add_child(child_pago_relacionado)
|
87
|
+
end
|
88
|
+
|
89
|
+
# puts '---------------- Xml resultante comprobante de pago -----------------------'
|
90
|
+
# puts xml.to_xml
|
91
|
+
# puts '--------------------------------------------------------'
|
92
|
+
|
93
|
+
path = File.join(File.dirname(__FILE__), *%w[.. tmp])
|
94
|
+
id = SecureRandom.hex
|
95
|
+
|
96
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
97
|
+
File.write("#{path}/tmp_c_#{id}.xml", xml.to_xml)
|
98
|
+
xml_path = "#{path}/tmp_c_#{id}.xml"
|
99
|
+
cadena_path = File.join(File.dirname(__FILE__), *%w[.. cadena cadena33.xslt])
|
100
|
+
|
101
|
+
File.write("#{path}/pem_#{id}.pem", @pem)
|
102
|
+
key_pem_url = "#{path}/pem_#{id}.pem"
|
103
|
+
sello = %x[xsltproc #{cadena_path} #{xml_path} | openssl dgst -sha256 -sign #{key_pem_url} | openssl enc -base64 -A]
|
104
|
+
comprobante['Sello'] = sello
|
105
|
+
|
106
|
+
File.delete("#{xml_path}")
|
107
|
+
File.delete("#{key_pem_url}")
|
108
|
+
|
109
|
+
# puts '------ comprobante de pago antes de timbre -------'
|
110
|
+
# puts xml.to_xml
|
111
|
+
|
112
|
+
base64_xml = Base64.encode64(xml.to_xml)
|
113
|
+
request = Net::HTTP::Post.new(uri)
|
114
|
+
request.basic_auth(token, "")
|
115
|
+
request.content_type = "application/json"
|
116
|
+
request["cache-control"] = 'no-cache'
|
117
|
+
request.body = JSON.dump({
|
118
|
+
"credentials" => {
|
119
|
+
"id" => params[:venta_folio].to_s,
|
120
|
+
"token" => token.to_s
|
121
|
+
},
|
122
|
+
"issuer" => {
|
123
|
+
"rfc" => @rfc
|
124
|
+
},
|
125
|
+
"document" => {
|
126
|
+
"ref-id": params[:venta_folio].to_s,
|
127
|
+
"certificate-number": @serial,
|
128
|
+
"section": "all",
|
129
|
+
"format": "xml",
|
130
|
+
"template": "letter",
|
131
|
+
"type": "application/xml",
|
132
|
+
"content": base64_xml
|
133
|
+
}
|
134
|
+
})
|
135
|
+
|
136
|
+
req_options = {
|
137
|
+
use_ssl: false,
|
138
|
+
}
|
139
|
+
|
140
|
+
json_response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
141
|
+
http.request(request)
|
142
|
+
end
|
143
|
+
puts "-- #{json_response.code} --"
|
144
|
+
puts "-- #{json_response.message} --"
|
145
|
+
# puts "-- Body --"
|
146
|
+
# puts json_response.body
|
147
|
+
# puts '---'
|
148
|
+
response = JSON.parse(json_response.body)
|
149
|
+
|
150
|
+
if json_response.code == '200'
|
151
|
+
decoded_xml = Nokogiri::XML(Base64.decode64(response['content']))
|
152
|
+
timbre = decoded_xml.at_xpath("//cfdi:Complemento").children[1]
|
153
|
+
response = {
|
154
|
+
status: 200,
|
155
|
+
message_error: '',
|
156
|
+
xml: decoded_xml.to_xml,
|
157
|
+
uuid: response['uuid'],
|
158
|
+
fecha_timbrado: timbre['FechaTimbrado'],
|
159
|
+
sello_cfd: timbre['SelloCFD'],
|
160
|
+
sello_sat: timbre['SelloSAT'],
|
161
|
+
no_certificado_sat: timbre['NoCertificadoSAT'],
|
162
|
+
}
|
163
|
+
return response
|
164
|
+
else
|
165
|
+
response = {
|
166
|
+
status: json_response.code,
|
167
|
+
message_error: "Error message: #{json_response.message}, #{response['message']} #{response['error_details']}",
|
168
|
+
xml: '',
|
169
|
+
uuid: '',
|
170
|
+
fecha_timbrado: '',
|
171
|
+
sello_cfd: '',
|
172
|
+
sello_sat: '',
|
173
|
+
no_certificado_sat: '',
|
174
|
+
}
|
175
|
+
return response
|
176
|
+
end
|
177
|
+
|
178
|
+
end
|
179
|
+
|
180
|
+
def nota_credito(params={})
|
181
|
+
# Sample params
|
182
|
+
# params = {
|
183
|
+
# uuid_relacionado: '',
|
184
|
+
# desc: '',
|
185
|
+
# motivo: 'dev, mod',
|
186
|
+
# series: '',
|
187
|
+
# folio: '',
|
188
|
+
# cp: '',
|
189
|
+
# time: '',
|
190
|
+
# receptor_razon: '',
|
191
|
+
# receptor_rfc: '',
|
192
|
+
# uso_cfdi: '',
|
193
|
+
# }
|
194
|
+
|
195
|
+
total = (params[:monto]).to_f
|
196
|
+
subtotal = total / 1.16
|
197
|
+
tax = total - subtotal
|
198
|
+
|
199
|
+
uri = @production ? URI("#{SwFac::UrlProduction}cfdi33/stamp/customv1/b64") : URI("#{SwFac::UrlDev}cfdi33/stamp/customv1/b64")
|
200
|
+
token = @production ? @production_token : @dev_token
|
201
|
+
time = params.fetch(:time, Time.now)
|
202
|
+
|
203
|
+
base_doc = %(<?xml version="1.0" encoding="utf-8"?>
|
204
|
+
<cfdi:Comprobante xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://www.sat.gob.mx/cfd/3 http://www.sat.gob.mx/sitio_internet/cfd/3/cfdv33.xsd" Version="3.3" Serie="#{params.fetch(:series, 'N')}" Folio="#{params[:folio]}" Fecha="#{time.strftime("%Y-%m-%dT%H:%M:%S")}" FormaPago="99" NoCertificado="#{@serial}" Certificado="#{@cadena}" SubTotal="#{subtotal.round(2)}" Moneda="#{params.fetch(:moneda, 'MXN')}" Total="#{total.round(2)}" TipoDeComprobante="E" MetodoPago="PUE" LugarExpedicion="#{params[:cp]}" xmlns:cfdi="http://www.sat.gob.mx/cfd/3">
|
205
|
+
<cfdi:CfdiRelacionados TipoRelacion="01">
|
206
|
+
<cfdi:CfdiRelacionado UUID="#{params[:uuid_relacionado]}" />
|
207
|
+
</cfdi:CfdiRelacionados>
|
208
|
+
<cfdi:Emisor Rfc="#{@rfc}" Nombre="#{@razon}" RegimenFiscal="#{@regimen_fiscal}" />
|
209
|
+
<cfdi:Receptor Rfc="#{params[:receptor_rfc]}" Nombre="#{params[:receptor_razon]}" UsoCFDI="#{params.fetch(:uso_cfdi, 'G03')}" />
|
210
|
+
<cfdi:Conceptos>
|
211
|
+
<cfdi:Concepto ClaveUnidad="ACT" ClaveProdServ="84111506" NoIdentificacion="C" Cantidad="1.00" Unidad="Pieza" Descripcion="#{params.fetch(:desc, 'DICTAMEN CC FACTURA ORIGEN 0')}" ValorUnitario="#{subtotal.round(2)}" Importe="#{subtotal.round(2)}">
|
212
|
+
<cfdi:Impuestos>
|
213
|
+
<cfdi:Traslados>
|
214
|
+
<cfdi:Traslado Base="#{subtotal.round(2)}" Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="#{tax.round(2)}" />
|
215
|
+
</cfdi:Traslados>
|
216
|
+
</cfdi:Impuestos>
|
217
|
+
</cfdi:Concepto>
|
218
|
+
</cfdi:Conceptos>
|
219
|
+
<cfdi:Impuestos TotalImpuestosTrasladados="#{tax.round(2)}">
|
220
|
+
<cfdi:Traslados>
|
221
|
+
<cfdi:Traslado Impuesto="002" TipoFactor="Tasa" TasaOCuota="0.160000" Importe="#{tax.round(2)}" />
|
222
|
+
</cfdi:Traslados>
|
223
|
+
</cfdi:Impuestos>
|
224
|
+
</cfdi:Comprobante>
|
225
|
+
)
|
226
|
+
|
227
|
+
base_doc.delete!("\n")
|
228
|
+
base_doc.delete!("\t")
|
229
|
+
|
230
|
+
xml = Nokogiri::XML(base_doc)
|
231
|
+
comprobante = xml.at_xpath("//cfdi:Comprobante")
|
232
|
+
|
233
|
+
path = File.join(File.dirname(__FILE__), *%w[.. tmp])
|
234
|
+
id = SecureRandom.hex
|
235
|
+
|
236
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
237
|
+
File.write("#{path}/tmp_n_#{id}.xml", xml.to_xml)
|
238
|
+
xml_path = "#{path}/tmp_n_#{id}.xml"
|
239
|
+
cadena_path = File.join(File.dirname(__FILE__), *%w[.. cadena cadena33.xslt])
|
240
|
+
|
241
|
+
File.write("#{path}/pem_#{id}.pem", @pem)
|
242
|
+
key_pem_url = "#{path}/pem_#{id}.pem"
|
243
|
+
sello = %x[xsltproc #{cadena_path} #{xml_path} | openssl dgst -sha256 -sign #{key_pem_url} | openssl enc -base64 -A]
|
244
|
+
comprobante['Sello'] = sello
|
245
|
+
|
246
|
+
File.delete("#{xml_path}")
|
247
|
+
File.delete("#{key_pem_url}")
|
248
|
+
|
249
|
+
puts '------ nota antes de timbre -------'
|
250
|
+
puts xml.to_xml
|
251
|
+
|
252
|
+
base64_xml = Base64.encode64(xml.to_xml)
|
253
|
+
|
254
|
+
request = Net::HTTP::Post.new(uri)
|
255
|
+
request.basic_auth(token, "")
|
256
|
+
request.content_type = "application/json"
|
257
|
+
request["cache-control"] = 'no-cache'
|
258
|
+
request.body = JSON.dump({
|
259
|
+
"credentials" => {
|
260
|
+
"id" => "#{params[:folio]}",
|
261
|
+
"token" => token
|
262
|
+
},
|
263
|
+
"issuer" => {
|
264
|
+
"rfc" => @rfc
|
265
|
+
},
|
266
|
+
"document" => {
|
267
|
+
"ref-id": "#{params[:folio]}",
|
268
|
+
"certificate-number": @serial,
|
269
|
+
"section": "all",
|
270
|
+
"format": "xml",
|
271
|
+
"template": "letter",
|
272
|
+
"type": "application/xml",
|
273
|
+
"content": base64_xml
|
274
|
+
}
|
275
|
+
})
|
276
|
+
|
277
|
+
req_options = {
|
278
|
+
use_ssl: false,
|
279
|
+
}
|
280
|
+
|
281
|
+
json_response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
282
|
+
http.request(request)
|
283
|
+
end
|
284
|
+
|
285
|
+
|
286
|
+
puts "-- #{json_response.code} --"
|
287
|
+
puts "-- #{json_response.message} --"
|
288
|
+
# puts "-- Body --"
|
289
|
+
# puts json_response.body
|
290
|
+
# puts '---'
|
291
|
+
response = JSON.parse(json_response.body)
|
292
|
+
|
293
|
+
if json_response.code == '200'
|
294
|
+
decoded_xml = Nokogiri::XML(Base64.decode64(response['content']))
|
295
|
+
timbre = decoded_xml.at_xpath("//cfdi:Complemento").children.first
|
296
|
+
|
297
|
+
response = {
|
298
|
+
status: 200,
|
299
|
+
message_error: '',
|
300
|
+
xml: decoded_xml.to_xml,
|
301
|
+
uuid: response['uuid'],
|
302
|
+
fecha_timbrado: timbre['FechaTimbrado'],
|
303
|
+
sello_cfd: timbre['SelloCFD'],
|
304
|
+
sello_sat: timbre['SelloSAT'],
|
305
|
+
no_certificado_sat: timbre['NoCertificadoSAT'],
|
306
|
+
}
|
307
|
+
return response
|
308
|
+
else
|
309
|
+
response = {
|
310
|
+
status: json_response.code,
|
311
|
+
message_error: "Error message: #{json_response.message}, #{response['message']} #{response['error_details']}",
|
312
|
+
xml: '',
|
313
|
+
uuid: '',
|
314
|
+
fecha_timbrado: '',
|
315
|
+
sello_cfd: '',
|
316
|
+
sello_sat: '',
|
317
|
+
no_certificado_sat: '',
|
318
|
+
}
|
319
|
+
return response
|
320
|
+
end
|
321
|
+
|
322
|
+
|
323
|
+
end
|
324
|
+
|
325
|
+
def cancela_doc(params={})
|
326
|
+
# Sample params
|
327
|
+
# params = {
|
328
|
+
# uuid: '',
|
329
|
+
# rfc_emisor: '',
|
330
|
+
# key_password: '', # optional
|
331
|
+
# cer_cadena: '', # optional
|
332
|
+
# key_pem: '' # optional
|
333
|
+
# }
|
334
|
+
|
335
|
+
uri = @production ? URI("#{SwFac::UrlProduction}cfdi33/cancel/csd") : URI("#{SwFac::UrlDev}cfdi33/cancel/csd")
|
336
|
+
token = @production ? @production_token : @dev_token
|
337
|
+
time = Time.now
|
338
|
+
|
339
|
+
request = Net::HTTP::Post.new(uri)
|
340
|
+
request["Authorization"] = "bearer #{token}"
|
341
|
+
request.content_type = "application/json"
|
342
|
+
request["Cache-Control"] = 'no-cache'
|
343
|
+
request["Postman-Token"] = '30b35bb8-534d-51c7-6a5c-e2c98a0c9395'
|
344
|
+
request.body = JSON.dump({
|
345
|
+
'uuid': params[:uuid],
|
346
|
+
"password": params.fetch(:key_password, @key_pass),
|
347
|
+
"rfc": params.fetch(:rfc_emisor, @rfc),
|
348
|
+
"b64Cer": params.fetch(:cer_cadena, @cadena),
|
349
|
+
"b64Key": params.fetch(:key_pem, @pem_cadena)
|
350
|
+
})
|
351
|
+
|
352
|
+
req_options = {
|
353
|
+
use_ssl: false,
|
354
|
+
}
|
355
|
+
|
356
|
+
json_response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
357
|
+
http.request(request)
|
358
|
+
end
|
359
|
+
|
360
|
+
puts "-- #{json_response.code} --"
|
361
|
+
puts "-- #{json_response.message} --"
|
362
|
+
# puts "-- Body --"
|
363
|
+
# puts json_response.body
|
364
|
+
# puts '---'
|
365
|
+
response = JSON.parse(json_response.body)
|
366
|
+
|
367
|
+
if json_response.code == '200'
|
368
|
+
decoded_xml = response['data']['acuse']
|
369
|
+
|
370
|
+
response = {
|
371
|
+
status: 200,
|
372
|
+
message_error: '',
|
373
|
+
xml: decoded_xml,
|
374
|
+
}
|
375
|
+
|
376
|
+
return response
|
377
|
+
else
|
378
|
+
|
379
|
+
response ={
|
380
|
+
status: json_response.code,
|
381
|
+
message_error: "Error message: #{json_response.message}, #{response['message']} #{response['error_details']}",
|
382
|
+
xml: '',
|
383
|
+
}
|
384
|
+
|
385
|
+
return response
|
386
|
+
end
|
387
|
+
|
388
|
+
end
|
389
|
+
|
390
|
+
def timbra_doc(params={})
|
391
|
+
### sample params
|
392
|
+
#
|
393
|
+
# params = {
|
394
|
+
# moneda: 'MXN',
|
395
|
+
# series: 'FA',
|
396
|
+
# folio: '003',
|
397
|
+
# forma_pago: '',
|
398
|
+
# metodo_pago: 'PUE',
|
399
|
+
# cp: '47180',
|
400
|
+
# receptor_razon: 'Car zone',
|
401
|
+
# receptor_rfc: '',
|
402
|
+
# uso_cfdi: 'G03',
|
403
|
+
# line_items: [
|
404
|
+
# {
|
405
|
+
# clave_prod_serv: '78181500',
|
406
|
+
# clave_unidad: 'E48',
|
407
|
+
# unidad: 'Servicio',
|
408
|
+
# sku: 'serv001',
|
409
|
+
# cantidad: 1,
|
410
|
+
# descripcion: 'Servicio mano de obra',
|
411
|
+
# valor_unitario: 100.00,
|
412
|
+
# descuento: 0.00,
|
413
|
+
# tax_included: true,
|
414
|
+
# # Optional parameters
|
415
|
+
# # tipo_impuesto: '002'
|
416
|
+
# },
|
417
|
+
# ]
|
418
|
+
|
419
|
+
# }
|
420
|
+
|
421
|
+
|
422
|
+
uri = @production ? URI("#{SwFac::UrlProduction}cfdi33/stamp/customv1/b64") : URI("#{SwFac::UrlDev}cfdi33/stamp/customv1/b64")
|
423
|
+
token = @production ? @production_token : @dev_token
|
424
|
+
time = Time.now
|
425
|
+
|
426
|
+
xml = Nokogiri::XML(SwFac::DocBase)
|
427
|
+
comprobante = xml.at_xpath("//cfdi:Comprobante")
|
428
|
+
comprobante['TipoCambio'] = '1'
|
429
|
+
comprobante['TipoDeComprobante'] = 'I'
|
430
|
+
comprobante['Serie'] = params.fetch(:series, 'FA').to_s
|
431
|
+
comprobante['Folio'] = params.fetch(:folio).to_s
|
432
|
+
comprobante['Fecha'] = time.strftime("%Y-%m-%dT%H:%M:%S")
|
433
|
+
comprobante['FormaPago'] = params.fetch(:forma_pago, '01')
|
434
|
+
comprobante['MetodoPago'] = params.fetch(:metodo_pago, 'PUE')
|
435
|
+
comprobante['LugarExpedicion'] = params.fetch(:cp, '')
|
436
|
+
comprobante['NoCertificado'] = @serial
|
437
|
+
comprobante['Certificado'] = @cadena
|
438
|
+
|
439
|
+
emisor = xml.at_xpath("//cfdi:Emisor")
|
440
|
+
emisor['Nombre'] = @razon
|
441
|
+
emisor['RegimenFiscal'] = @regimen_fiscal
|
442
|
+
emisor['Rfc'] = @rfc
|
443
|
+
|
444
|
+
receptor = xml.at_xpath("//cfdi:Receptor")
|
445
|
+
receptor['Nombre'] = params.fetch(:receptor_razon, '')
|
446
|
+
receptor['Rfc'] = params.fetch(:receptor_rfc, '')
|
447
|
+
receptor['UsoCFDI'] = params.fetch(:uso_cfdi, 'G03')
|
448
|
+
|
449
|
+
impuestos = xml.at_xpath("//cfdi:Impuestos")
|
450
|
+
traslado = xml.at_xpath("//cfdi:Traslado")
|
451
|
+
|
452
|
+
|
453
|
+
conceptos = xml.at_xpath("//cfdi:Conceptos")
|
454
|
+
|
455
|
+
line_items = params[:line_items]
|
456
|
+
|
457
|
+
suma_total = 0.00
|
458
|
+
subtotal = 0.00
|
459
|
+
suma_iva = 0.00
|
460
|
+
|
461
|
+
line_items.each do |line|
|
462
|
+
descuento = line.fetch(:descuento, 0.00).to_f
|
463
|
+
|
464
|
+
if line[:tax_included] == true
|
465
|
+
unitario = ((line[:valor_unitario]).to_f - descuento) / 1.16
|
466
|
+
else
|
467
|
+
unitario = (line[:valor_unitario].to_f) - descuento
|
468
|
+
end
|
469
|
+
|
470
|
+
cantidad = line[:cantidad].to_f
|
471
|
+
total_line = cantidad * unitario
|
472
|
+
total_acumulator = cantidad * unitario * 1.16
|
473
|
+
importe_iva = total_acumulator - total_line
|
474
|
+
|
475
|
+
subtotal += total_line
|
476
|
+
suma_iva += importe_iva
|
477
|
+
suma_total += total_acumulator
|
478
|
+
|
479
|
+
|
480
|
+
child_concepto = Nokogiri::XML::Node.new "cfdi:Concepto", xml
|
481
|
+
child_concepto['ClaveProdServ'] = line[:clave_prod_serv].to_s
|
482
|
+
child_concepto['NoIdentificacion'] = line[:sku].to_s
|
483
|
+
child_concepto['ClaveUnidad'] = line[:clave_unidad].to_s
|
484
|
+
child_concepto['Unidad'] = line[:unidad].to_s
|
485
|
+
child_concepto['Descripcion'] = line[:descripcion].to_s
|
486
|
+
child_concepto['Cantidad'] = cantidad.to_s
|
487
|
+
child_concepto['ValorUnitario'] = unitario.round(6).to_s
|
488
|
+
child_concepto['Importe'] = total_line.round(6).to_s
|
489
|
+
# child_concepto['Descuento'] = line.fetch(:descuento, 0.00).round(6).to_s
|
490
|
+
|
491
|
+
child_impuestos = Nokogiri::XML::Node.new "cfdi:Impuestos", xml
|
492
|
+
child_traslados = Nokogiri::XML::Node.new "cfdi:Traslados", xml
|
493
|
+
child_traslado = Nokogiri::XML::Node.new "cfdi:Traslado", xml
|
494
|
+
child_traslado['Base'] = total_line.round(6).to_s
|
495
|
+
child_traslado['Impuesto'] = line.fetch(:tipo_impuesto, '002')
|
496
|
+
child_traslado['TipoFactor'] = "Tasa"
|
497
|
+
child_traslado['TasaOCuota'] = '0.160000'
|
498
|
+
child_traslado['Importe'] = importe_iva.round(6).to_s
|
499
|
+
|
500
|
+
# Joining all up
|
501
|
+
child_traslados.add_child(child_traslado)
|
502
|
+
child_impuestos.add_child(child_traslados)
|
503
|
+
child_concepto.add_child(child_impuestos)
|
504
|
+
|
505
|
+
conceptos.add_child(child_concepto)
|
506
|
+
|
507
|
+
end
|
508
|
+
|
509
|
+
puts '------ Line -----'
|
510
|
+
puts "Total suma = #{suma_total}"
|
511
|
+
puts "SubTotal suma = #{subtotal}"
|
512
|
+
puts "Suma iva = #{suma_iva}"
|
513
|
+
|
514
|
+
comprobante['SubTotal'] = subtotal.round(2).to_s
|
515
|
+
comprobante['Total'] = suma_total.round(2).to_s
|
516
|
+
comprobante['Moneda'] = params.fetch(:moneda, 'MXN')
|
517
|
+
|
518
|
+
impuestos['TotalImpuestosTrasladados'] = suma_iva.round(2).to_s
|
519
|
+
traslado['Importe'] = suma_iva.round(2).to_s
|
520
|
+
|
521
|
+
path = File.join(File.dirname(__FILE__), *%w[.. tmp])
|
522
|
+
id = SecureRandom.hex
|
523
|
+
|
524
|
+
FileUtils.mkdir_p(path) unless File.exist?(path)
|
525
|
+
File.write("#{path}/tmp_#{id}.xml", xml.to_xml)
|
526
|
+
xml_path = "#{path}/tmp_#{id}.xml"
|
527
|
+
cadena_path = File.join(File.dirname(__FILE__), *%w[.. cadena cadena33.xslt])
|
528
|
+
|
529
|
+
# puts File.read(cadena_path)
|
530
|
+
File.write("#{path}/pem_#{id}.pem", @pem)
|
531
|
+
key_pem_url = "#{path}/pem_#{id}.pem"
|
532
|
+
sello = %x[xsltproc #{cadena_path} #{xml_path} | openssl dgst -sha256 -sign #{key_pem_url} | openssl enc -base64 -A]
|
533
|
+
comprobante['Sello'] = sello
|
534
|
+
|
535
|
+
File.delete("#{xml_path}")
|
536
|
+
File.delete("#{key_pem_url}")
|
537
|
+
|
538
|
+
# puts '---- comprobante sin timbrar------'
|
539
|
+
# puts xml.to_xml
|
540
|
+
# puts '-------------------------'
|
541
|
+
|
542
|
+
base64_xml = Base64.encode64(xml.to_xml)
|
543
|
+
request = Net::HTTP::Post.new(uri)
|
544
|
+
request.basic_auth(token, "")
|
545
|
+
request.content_type = "application/json"
|
546
|
+
request["cache-control"] = 'no-cache'
|
547
|
+
request.body = JSON.dump({
|
548
|
+
"credentials" => {
|
549
|
+
"id" => params.fetch(:folio).to_s,
|
550
|
+
"token" => token
|
551
|
+
},
|
552
|
+
"issuer" => {
|
553
|
+
"rfc" => emisor['Rfc']
|
554
|
+
},
|
555
|
+
"document" => {
|
556
|
+
"ref-id": params.fetch(:folio).to_s,
|
557
|
+
"certificate-number": comprobante['NoCertificado'],
|
558
|
+
"section": "all",
|
559
|
+
"format": "xml",
|
560
|
+
"template": "letter",
|
561
|
+
"type": "application/xml",
|
562
|
+
"content": base64_xml
|
563
|
+
}
|
564
|
+
})
|
565
|
+
|
566
|
+
req_options = {
|
567
|
+
use_ssl: false,
|
568
|
+
}
|
569
|
+
|
570
|
+
json_response = Net::HTTP.start(uri.hostname, uri.port, req_options) do |http|
|
571
|
+
http.request(request)
|
572
|
+
end
|
573
|
+
|
574
|
+
puts "-- #{json_response.code} --"
|
575
|
+
puts "-- #{json_response.message} --"
|
576
|
+
# puts "-- Body --"
|
577
|
+
# puts json_response.body
|
578
|
+
# puts '---'
|
579
|
+
response = JSON.parse(json_response.body)
|
580
|
+
|
581
|
+
if json_response.code == '200'
|
582
|
+
decoded_xml = Nokogiri::XML(Base64.decode64(response['content']))
|
583
|
+
timbre = decoded_xml.at_xpath("//cfdi:Complemento").children.first
|
584
|
+
|
585
|
+
response = {
|
586
|
+
status: 200,
|
587
|
+
message_error: '',
|
588
|
+
xml: decoded_xml.to_xml,
|
589
|
+
uuid: response['uuid'],
|
590
|
+
fecha_timbrado: timbre['FechaTimbrado'],
|
591
|
+
sello_cfd: timbre['SelloCFD'],
|
592
|
+
sello_sat: timbre['SelloSAT'],
|
593
|
+
no_certificado_sat: timbre['NoCertificadoSAT'],
|
594
|
+
}
|
595
|
+
|
596
|
+
return response
|
597
|
+
else
|
598
|
+
|
599
|
+
response ={
|
600
|
+
status: json_response.code,
|
601
|
+
message_error: "Error message: #{json_response.message}, #{response['message']} #{response['error_details']}",
|
602
|
+
xml: '',
|
603
|
+
uuid: '',
|
604
|
+
fecha_timbrado: '',
|
605
|
+
sello_cfd: '',
|
606
|
+
sello_sat: '',
|
607
|
+
no_certificado_sat: '',
|
608
|
+
}
|
609
|
+
|
610
|
+
return response
|
611
|
+
end
|
612
|
+
|
613
|
+
end
|
614
|
+
|
615
|
+
end
|
616
|
+
end
|