cfdi40 0.0.1.alfa → 0.0.2
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/Gemfile.lock +1 -1
- data/README.md +10 -5
- data/README_es-MX.md +62 -0
- data/cfdi40.gemspec +2 -2
- data/lib/cfdi40/comprobante.rb +215 -20
- data/lib/cfdi40/concepto.rb +126 -0
- data/lib/cfdi40/conceptos.rb +4 -0
- data/lib/cfdi40/emisor.rb +7 -0
- data/lib/cfdi40/impuestos.rb +28 -0
- data/lib/cfdi40/node.rb +122 -0
- data/lib/cfdi40/receptor.rb +11 -0
- data/lib/cfdi40/sat_csd.rb +83 -0
- data/lib/cfdi40/schema_validator.rb +29 -0
- data/lib/cfdi40/traslado.rb +9 -0
- data/lib/cfdi40/traslados.rb +17 -0
- data/lib/cfdi40/version.rb +1 -1
- data/lib/cfdi40.rb +12 -0
- data/lib/xsd/README.md +30 -0
- data/lib/xsd/catCFDI.xsd +162329 -0
- data/lib/xsd/cfdv40.xsd +856 -0
- data/lib/xsd/tdCFDI.xsd +157 -0
- data/lib/xslt/CartaPorte20.xslt +615 -0
- data/lib/xslt/GastosHidrocarburos10.xslt +171 -0
- data/lib/xslt/IngresosHidrocarburos.xslt +39 -0
- data/lib/xslt/Pagos20.xslt +233 -0
- data/lib/xslt/TuristaPasajeroExtranjero.xslt +40 -0
- data/lib/xslt/aerolineas.xslt +50 -0
- data/lib/xslt/cadenaoriginal.xslt +405 -0
- data/lib/xslt/cadenaoriginal_local.xslt +405 -0
- data/lib/xslt/certificadodedestruccion.xslt +60 -0
- data/lib/xslt/cfdiregistrofiscal.xslt +19 -0
- data/lib/xslt/consumodeCombustibles11.xslt +94 -0
- data/lib/xslt/detallista.xslt +42 -0
- data/lib/xslt/divisas.xslt +13 -0
- data/lib/xslt/donat11.xslt +13 -0
- data/lib/xslt/iedu.xslt +26 -0
- data/lib/xslt/implocal.xslt +39 -0
- data/lib/xslt/ine11.xslt +30 -0
- data/lib/xslt/leyendasFisc.xslt +28 -0
- data/lib/xslt/nomina12.xslt +412 -0
- data/lib/xslt/notariospublicos.xslt +301 -0
- data/lib/xslt/obrasarteantiguedades.xslt +33 -0
- data/lib/xslt/pagoenespecie.xslt +39 -0
- data/lib/xslt/pfic.xslt +13 -0
- data/lib/xslt/renovacionysustitucionvehiculos.xslt +152 -0
- data/lib/xslt/servicioparcialconstruccion.xslt +44 -0
- data/lib/xslt/utilerias.xslt +22 -0
- data/lib/xslt/valesdedespensa.xslt +70 -0
- data/lib/xslt/vehiculousado.xslt +63 -0
- metadata +50 -13
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 000e715fcb57e869f47058bb05bc2cadc558f188db6da41e1c0d0240873d910b
|
4
|
+
data.tar.gz: 2ab314d7b2477308a4cc073f8ebe35a31098b8ac5ecb23aa0dd4d43babc95ae8
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: fccd3b919ade4540d90a2b9ff81778e68cd900f121c7e92d4e4bb192585d5bb0d921a1a148bca0df23a5f26ab7da0dc5de165d7f068352701a93f2936d65792e
|
7
|
+
data.tar.gz: ae39a825b18d9f3cbbbd8403b6afb4a920bc4abd61479777d35d774bf5fe44426dd1b472856f966f95b10b50b04ee28773c121431f6d3c4990127bacb02fd09a
|
data/Gemfile.lock
CHANGED
data/README.md
CHANGED
@@ -1,14 +1,19 @@
|
|
1
1
|
# Cfdi40
|
2
2
|
|
3
|
-
Tool for read, create,
|
3
|
+
Tool for read, create, validate and sign CFDIs version 4.0
|
4
4
|
|
5
5
|
CFDI (Comprobante Fiscal Digital por Internet) are XML documents
|
6
6
|
regulated by mexican goverment for tax purpouses.
|
7
7
|
|
8
|
-
|
8
|
+
Please see `README_es-MX.md`
|
9
|
+
|
10
|
+
TODO: Document, document, document
|
11
|
+
|
12
|
+
## Features
|
9
13
|
|
10
|
-
*
|
11
|
-
|
14
|
+
* XML generation and sign.
|
15
|
+
|
16
|
+
## Future features
|
12
17
|
|
13
18
|
## Installation
|
14
19
|
|
@@ -48,7 +53,7 @@ Bug reports and pull requests are welcome on GitHub at
|
|
48
53
|
https://github.com/[USERNAME]/cfdi40. This project is intended to be a
|
49
54
|
safe, welcoming space for collaboration, and contributors are expected
|
50
55
|
to adhere to the [code of
|
51
|
-
conduct](https://github.com/
|
56
|
+
conduct](https://github.com/israelbz/cfdi40/blob/master/CODE_OF_CONDUCT.md).
|
52
57
|
|
53
58
|
## License
|
54
59
|
|
data/README_es-MX.md
ADDED
@@ -0,0 +1,62 @@
|
|
1
|
+
# Cfdi40
|
2
|
+
|
3
|
+
Herramienta para crear, leer, validar y firmam CFDis en
|
4
|
+
versión 4.0.
|
5
|
+
|
6
|
+
El CFDi (Comprobante Fiscal Digital por internet) es un
|
7
|
+
documento en formato XML usado en México.
|
8
|
+
|
9
|
+
Esta herramienta tiene la intención de simplificar la
|
10
|
+
generación de los archivos XML, para ello, se pretende
|
11
|
+
que esta herramienta:
|
12
|
+
|
13
|
+
* Ofrezca una interfaz simple para colocar la
|
14
|
+
información esencial para la elaboración del CFDi.
|
15
|
+
* Realice los cálculos complementarios como impuestos,
|
16
|
+
totales, etcétera.
|
17
|
+
* Valide el CFDi contra los CSD
|
18
|
+
* Selle el CFDi.
|
19
|
+
|
20
|
+
# Uso
|
21
|
+
|
22
|
+
## Ejemplo básico
|
23
|
+
|
24
|
+
# Inicia un cfdi
|
25
|
+
cfdi = Cfdi40.new
|
26
|
+
|
27
|
+
# Datos del emisor
|
28
|
+
cfdi.lugar_expedicion = '06000'
|
29
|
+
cfdi.emisor.regimen_fiscal = '612'
|
30
|
+
|
31
|
+
# Datos del receptor
|
32
|
+
cfdi.receptor.nombre = 'JUAN PUEBLO BUENO'
|
33
|
+
cfdi.receptor.rfc = 'XAXX010101000'
|
34
|
+
cfdi.receptor.domicilio_fiscal = '06000'
|
35
|
+
cfdi.receptor.regimen_fiscal = '616'
|
36
|
+
cfdi.receptor.uso_cfdi = 'G03'
|
37
|
+
|
38
|
+
# Agrega un concepto en pesos,
|
39
|
+
# precio final al cliente (neto)
|
40
|
+
# causa IVA con tasa de 16% (default)
|
41
|
+
cfdi.add_concepto(
|
42
|
+
clave_prod_serv: '81111500',
|
43
|
+
clave_unidad: "E48",
|
44
|
+
descripcion: 'Prueba de concepto',
|
45
|
+
precio_neto: 40
|
46
|
+
)
|
47
|
+
|
48
|
+
# Archivos CSD
|
49
|
+
cfdi.cert_path = '/path_to/certificado.cer'
|
50
|
+
cfdi.key_path = '/path_to/llave_privada.key'
|
51
|
+
cfdi.key_pass = 'contraseña'
|
52
|
+
|
53
|
+
# Genera CFDI firmado
|
54
|
+
xml_string = cfdi.to_xml
|
55
|
+
|
56
|
+
|
57
|
+
# ¿Que sigue?
|
58
|
+
|
59
|
+
* Complemento de pagos
|
60
|
+
* Retenciones
|
61
|
+
* IEPS
|
62
|
+
* Complemento para colegiaturas
|
data/cfdi40.gemspec
CHANGED
@@ -14,7 +14,7 @@ Gem::Specification.new do |spec|
|
|
14
14
|
"regulated by Mexican Government"
|
15
15
|
spec.homepage = "https://github.com/israelbz/cfdi40"
|
16
16
|
spec.license = "MIT"
|
17
|
-
spec.required_ruby_version = ">= 2.
|
17
|
+
spec.required_ruby_version = ">= 2.3.3"
|
18
18
|
|
19
19
|
spec.metadata["allowed_push_host"] = "https://rubygems.org"
|
20
20
|
|
@@ -34,7 +34,7 @@ Gem::Specification.new do |spec|
|
|
34
34
|
spec.require_paths = ["lib"]
|
35
35
|
|
36
36
|
# Uncomment to register a new dependency of your gem
|
37
|
-
spec.add_dependency "nokogiri", "
|
37
|
+
spec.add_dependency "nokogiri", ">= 1.10.10"
|
38
38
|
|
39
39
|
# For more information and examples about making a new gem, check out our
|
40
40
|
# guide at: https://bundler.io/guides/creating_gem.html
|
data/lib/cfdi40/comprobante.rb
CHANGED
@@ -1,30 +1,225 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
# Create and Read XML documents
|
4
|
-
|
5
|
-
|
4
|
+
module Cfdi40
|
5
|
+
class Comprobante < Node
|
6
|
+
define_namespace "xsi", "http://www.w3.org/2001/XMLSchema-instance"
|
7
|
+
define_namespace "cfdi", "http://www.sat.gob.mx/cfd/4"
|
8
|
+
define_attribute :schema_location,
|
9
|
+
xml_attribute: 'xsi:schemaLocation',
|
10
|
+
readonly: true,
|
11
|
+
default: "http://www.sat.gob.mx/cfd/4 " \
|
12
|
+
"http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
|
13
|
+
define_attribute :version, xml_attribute: 'Version', readonly: true, default: '4.0'
|
14
|
+
define_attribute :serie, xml_attribute: 'Serie'
|
15
|
+
define_attribute :folio, xml_attribute: 'Folio'
|
16
|
+
define_attribute :fecha, xml_attribute: 'Fecha'
|
17
|
+
define_attribute :sello, xml_attribute: 'Sello', readonly: true
|
18
|
+
define_attribute :forma_pago, xml_attribute: 'FormaPago'
|
19
|
+
define_attribute :no_certificado, xml_attribute: 'NoCertificado'
|
20
|
+
define_attribute :certificado, xml_attribute: 'Certificado'
|
21
|
+
define_attribute :condiciones_de_pago, xml_attribute: 'CondicionesDePago'
|
22
|
+
define_attribute :subtotal, xml_attribute: 'SubTotal', format: :t_Importe
|
23
|
+
define_attribute :descuento, xml_attribute: 'Descuento', format: :t_Importe
|
24
|
+
define_attribute :moneda, xml_attribute: 'Moneda', default: 'MXN'
|
25
|
+
define_attribute :tipo_cambio, xml_attribute: 'TipoCambio'
|
26
|
+
define_attribute :total, xml_attribute: 'Total', format: :t_Importe
|
27
|
+
define_attribute :tipo_de_comprobante, xml_attribute: 'TipoDeComprobante', default: 'I'
|
28
|
+
define_attribute :exportacion, xml_attribute: 'Exportacion', default: '01'
|
29
|
+
define_attribute :metodo_pago, xml_attribute: 'MetodoPago'
|
30
|
+
define_attribute :lugar_expedicion, xml_attribute: 'LugarExpedicion'
|
31
|
+
define_attribute :confirmacion, xml_attribute: 'Confirmacion'
|
6
32
|
|
7
|
-
|
8
|
-
|
9
|
-
|
33
|
+
attr_reader :emisor, :receptor, :x509_cert, :conceptos, :private_key
|
34
|
+
attr_reader :errors
|
35
|
+
attr_writer :key_der, :key_pass
|
10
36
|
|
11
|
-
|
12
|
-
|
13
|
-
|
37
|
+
def initialize
|
38
|
+
super
|
39
|
+
@errors = []
|
40
|
+
@conceptos = Conceptos.new
|
41
|
+
@emisor = Emisor.new
|
42
|
+
@receptor = Receptor.new
|
43
|
+
@sat_csd = SatCsd.new
|
44
|
+
@fecha ||= Time.now.strftime("%Y-%m-%dT%H:%M:%S")
|
45
|
+
@children_nodes = [@emisor, @receptor, @conceptos]
|
46
|
+
set_defaults
|
47
|
+
end
|
48
|
+
|
49
|
+
# Accept a path to read the certificate.
|
50
|
+
# Certificate is a X509 file. SAT generates those files in
|
51
|
+
# DER format.
|
52
|
+
def cert_path=(path)
|
53
|
+
self.cert_der = File.read(path)
|
54
|
+
end
|
55
|
+
|
56
|
+
def cert_der=(cert_data)
|
57
|
+
@sat_csd ||= SatCsd.new
|
58
|
+
@sat_csd.cert_der = cert_data
|
59
|
+
emisor.rfc = @sat_csd.rfc
|
60
|
+
emisor.nombre = @sat_csd.name
|
61
|
+
@no_certificado = @sat_csd.no_certificado
|
62
|
+
@certificado = @sat_csd.cert64
|
63
|
+
true
|
64
|
+
end
|
65
|
+
|
66
|
+
def key_path=(path)
|
67
|
+
@key_der = File.read(path)
|
68
|
+
end
|
69
|
+
|
70
|
+
def sign
|
71
|
+
@sat_csd ||= SatCsd.new
|
72
|
+
load_private_key if @sat_csd.private_key.nil?
|
73
|
+
return unless @sat_csd.private_key
|
74
|
+
|
75
|
+
raise Error, 'Key and certificate not match' unless @sat_csd.valid_pair?
|
76
|
+
|
77
|
+
digest = @sat_csd.private_key.sign(OpenSSL::Digest.new('SHA256'), original_content)
|
78
|
+
@sello = Base64.strict_encode64 digest
|
79
|
+
@docxml = nil
|
80
|
+
end
|
81
|
+
|
82
|
+
# clave_prod_serv
|
83
|
+
# no_identificacion
|
84
|
+
# cantidad
|
85
|
+
# clave_unidad
|
86
|
+
# unidad
|
87
|
+
# descripcion
|
88
|
+
# valor_unitario
|
89
|
+
# importe
|
90
|
+
# descuento
|
91
|
+
# objeto_imp
|
92
|
+
def add_concepto(attributes = {})
|
93
|
+
concepto = Concepto.new
|
94
|
+
attributes.each do |key, value|
|
95
|
+
method_name = "#{key}=".to_sym
|
96
|
+
if concepto.respond_to?(method_name)
|
97
|
+
concepto.public_send(method_name, value)
|
98
|
+
else
|
99
|
+
raise Error, ":#{key} no se puede asignar al concepto"
|
100
|
+
end
|
101
|
+
end
|
102
|
+
concepto.calculate!
|
103
|
+
@conceptos.children_nodes << concepto
|
104
|
+
calculate!
|
105
|
+
concepto
|
106
|
+
end
|
107
|
+
|
108
|
+
def to_s
|
109
|
+
to_xml
|
110
|
+
end
|
111
|
+
|
112
|
+
def to_xml
|
113
|
+
sign
|
114
|
+
docxml.to_xml
|
115
|
+
end
|
116
|
+
|
117
|
+
def valid?
|
118
|
+
schema_validator = SchemaValidator.new(to_s)
|
119
|
+
return true if schema_validator.valid?
|
120
|
+
|
121
|
+
@errors = schema_validator.errors
|
122
|
+
@errors.empty?
|
123
|
+
end
|
124
|
+
|
125
|
+
def cadena_original
|
126
|
+
original_content
|
127
|
+
end
|
128
|
+
|
129
|
+
def original_content
|
130
|
+
xslt = Nokogiri::XSLT(File.open('lib/xslt/cadenaoriginal_local.xslt'))
|
131
|
+
transformed = xslt.transform(docxml)
|
132
|
+
# The ampersand (&) char must be used in original content
|
133
|
+
# even though the documentation indicates otherwise
|
134
|
+
transformed.children.to_s.gsub('&', '&').strip
|
135
|
+
end
|
136
|
+
|
137
|
+
private
|
138
|
+
|
139
|
+
def docxml
|
140
|
+
return @docxml if defined?(@docxml) && !@docxml.nil?
|
141
|
+
|
142
|
+
@docxml = Nokogiri::XML::Document.new("1.0")
|
143
|
+
@docxml.encoding = "utf-8"
|
144
|
+
add_root_node
|
145
|
+
@docxml
|
146
|
+
end
|
147
|
+
|
148
|
+
def add_root_node
|
149
|
+
self.xml_document = @docxml
|
150
|
+
self.xml_parent = @docxml
|
151
|
+
create_xml_node
|
152
|
+
end
|
153
|
+
|
154
|
+
def calculate!
|
155
|
+
@subtotal = @conceptos.children_nodes.map(&:importe).sum
|
156
|
+
@total = @conceptos.children_nodes.map(&:importe_neto).sum
|
157
|
+
summarize_traslados
|
158
|
+
end
|
159
|
+
|
160
|
+
def summarize_traslados
|
161
|
+
impuestos.total_impuestos_trasladados = 0
|
162
|
+
traslados.children_nodes = []
|
163
|
+
traslados_summary.each do |key, value|
|
164
|
+
#TODO: Sumar los impuestos y agregarlos a los nodos globales de traslados
|
165
|
+
traslado = Traslado.new
|
166
|
+
traslado.impuesto, traslado.tasa_o_cuota, traslado.tipo_factor = key
|
167
|
+
traslado.base = value[:base]
|
168
|
+
traslado.importe = value[:importe]
|
169
|
+
traslados.children_nodes << traslado
|
170
|
+
impuestos.total_impuestos_trasladados += value[:importe]
|
171
|
+
end
|
172
|
+
end
|
173
|
+
|
174
|
+
def concepto_nodes
|
175
|
+
@conceptos.children_nodes
|
176
|
+
end
|
177
|
+
|
178
|
+
# Returns a hash with a summary.
|
179
|
+
# The key is an Array ['impuesto, 'tasa_o_cuota', 'TipoFactor] and the value is
|
180
|
+
# another hash the sum of 'Importe' and 'Base'
|
181
|
+
def traslados_summary
|
182
|
+
summary = {}
|
183
|
+
concepto_nodes.map(&:traslado_nodes).flatten.each do |traslado|
|
184
|
+
key = [traslado.impuesto, traslado.tasa_o_cuota, traslado.tipo_factor]
|
185
|
+
summary[key] ||= { base: 0, importe: 0 }
|
186
|
+
summary[key][:base] += traslado.base
|
187
|
+
summary[key][:importe] += traslado.importe
|
188
|
+
end
|
189
|
+
summary
|
190
|
+
end
|
191
|
+
|
192
|
+
# TODO: Este método tiene que ser 'impuestos'
|
193
|
+
# si nos atenemos a que los que acaban con _node buscan en los hijos
|
194
|
+
# y los que no terminan con _node crean el nodo
|
195
|
+
def impuestos
|
196
|
+
return @impuestos if defined?(@impuestos)
|
197
|
+
|
198
|
+
@impuestos = Impuestos.new
|
199
|
+
@children_nodes << @impuestos
|
200
|
+
@impuestos
|
201
|
+
end
|
202
|
+
|
203
|
+
def impuestos_node
|
204
|
+
children_nodes.select { |n| n.is_a?(Impuestos)}.first
|
205
|
+
end
|
206
|
+
|
207
|
+
def traslados
|
208
|
+
return nil if impuestos_node.nil?
|
14
209
|
|
15
|
-
|
210
|
+
impuestos_node.traslados
|
211
|
+
end
|
16
212
|
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
root.add_namespace "xsi", "http://www.w3.org/2001/XMLSchema-instance"
|
22
|
-
root.add_namespace "cfdi", "http://www.sat.gob.mx/cfd/4"
|
23
|
-
root["xsi:schemaLocation"] = "http://www.sat.gob.mx/cfd/3 " \
|
24
|
-
"http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd"
|
25
|
-
root["Version"] = "4.0"
|
213
|
+
# Eliminar
|
214
|
+
def traslado_iva_node
|
215
|
+
impuestos_node.traslado_iva
|
216
|
+
end
|
26
217
|
|
27
|
-
|
28
|
-
|
218
|
+
def load_private_key
|
219
|
+
return unless defined?(@key_der) && defined?(@key_pass)
|
220
|
+
|
221
|
+
@sat_csd ||= SatCsd.new
|
222
|
+
@sat_csd.set_crypted_private_key(@key_der, @key_pass)
|
223
|
+
end
|
29
224
|
end
|
30
225
|
end
|
@@ -0,0 +1,126 @@
|
|
1
|
+
# Represents node 'concepto'
|
2
|
+
#
|
3
|
+
# * Attribute +Importe+ represente gross amount. Gross amount id before taxes and the result of multiply
|
4
|
+
# +ValorUnitario+ by +Cantidad+
|
5
|
+
#
|
6
|
+
module Cfdi40
|
7
|
+
class Concepto < Node
|
8
|
+
define_attribute :clave_prod_serv, xml_attribute: 'ClaveProdServ'
|
9
|
+
define_attribute :no_identificacion,xml_attribute: 'NoIdentificacion'
|
10
|
+
define_attribute :cantidad, xml_attribute: 'Cantidad', default: 1
|
11
|
+
define_attribute :clave_unidad, xml_attribute: 'ClaveUnidad'
|
12
|
+
define_attribute :unidad, xml_attribute: 'Unidad'
|
13
|
+
define_attribute :descripcion, xml_attribute: 'Descripcion'
|
14
|
+
define_attribute :valor_unitario, xml_attribute: 'ValorUnitario', format: :t_Importe
|
15
|
+
define_attribute :importe, xml_attribute: 'Importe', format: :t_Importe
|
16
|
+
define_attribute :descuento, xml_attribute: 'Descuento', format: :t_Importe
|
17
|
+
define_attribute :objeto_impuestos, xml_attribute: 'ObjetoImp', default: '01'
|
18
|
+
|
19
|
+
attr_accessor :tasa_iva, :tasa_ieps, :precio_neto, :precio_bruto
|
20
|
+
attr_reader :iva, :ieps, :base_iva, :importe_neto, :importe_bruto
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@tasa_iva = 0.16
|
24
|
+
@tasa_ieps = 0
|
25
|
+
super
|
26
|
+
end
|
27
|
+
|
28
|
+
# Calculate taxes, amounts from gross price
|
29
|
+
# or net price
|
30
|
+
def calculate!
|
31
|
+
set_defaults
|
32
|
+
assign_objeto_imp
|
33
|
+
if defined?(@precio_neto) && !@precio_neto.nil?
|
34
|
+
calculate_from_net_price
|
35
|
+
elsif defined?(@precio_bruto) && !@precio_bruto.nil?
|
36
|
+
calculate_from_gross_price
|
37
|
+
elsif !self.valor_unitario.nil?
|
38
|
+
@precio_bruto = valor_unitario
|
39
|
+
calculate_from_gross_price
|
40
|
+
end
|
41
|
+
add_info_to_traslado_iva
|
42
|
+
# TODO: add_info_to_traslado_ieps if @ieps > 0
|
43
|
+
true
|
44
|
+
end
|
45
|
+
|
46
|
+
def objeto_impuestos?
|
47
|
+
objeto_impuestos == '02'
|
48
|
+
end
|
49
|
+
|
50
|
+
def traslado_nodes
|
51
|
+
return [] if impuestos_node.nil?
|
52
|
+
|
53
|
+
impuestos_node.traslado_nodes
|
54
|
+
end
|
55
|
+
|
56
|
+
private
|
57
|
+
|
58
|
+
def calculate_from_net_price
|
59
|
+
set_defaults
|
60
|
+
@importe_neto = precio_neto * cantidad
|
61
|
+
breakdown_taxes
|
62
|
+
update_xml_attributes
|
63
|
+
end
|
64
|
+
|
65
|
+
def breakdown_taxes
|
66
|
+
@base_iva = @importe_neto / (1 + tasa_iva)
|
67
|
+
@iva = @importe_neto - @base_iva
|
68
|
+
@base_ieps = @base_iva / (1 + tasa_ieps)
|
69
|
+
@ieps = @base_iva - @base_ieps
|
70
|
+
@importe_bruto = @base_ieps
|
71
|
+
@precio_bruto = @importe_bruto / @cantidad
|
72
|
+
end
|
73
|
+
|
74
|
+
def calculate_from_gross_price
|
75
|
+
@importe_bruto = @precio_bruto * cantidad
|
76
|
+
add_taxes
|
77
|
+
update_xml_attributes
|
78
|
+
end
|
79
|
+
|
80
|
+
def add_taxes
|
81
|
+
@base_ieps = @importe_bruto
|
82
|
+
@ieps = @base_ieps * tasa_ieps
|
83
|
+
@base_iva = @base_ieps + @ieps
|
84
|
+
@iva = @base_iva * tasa_iva
|
85
|
+
@importe_neto = @base_iva + @iva
|
86
|
+
@precio_neto = @importe_neto / cantidad
|
87
|
+
end
|
88
|
+
|
89
|
+
def update_xml_attributes
|
90
|
+
self.importe = @importe_bruto
|
91
|
+
self.valor_unitario = @precio_bruto
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_info_to_traslado_iva
|
95
|
+
return unless @iva > 0
|
96
|
+
|
97
|
+
traslado_iva_node.importe = @iva
|
98
|
+
traslado_iva_node.base = @base_iva
|
99
|
+
traslado_iva_node.tasa_o_cuota = @tasa_iva
|
100
|
+
end
|
101
|
+
|
102
|
+
def assign_objeto_imp
|
103
|
+
return if objeto_impuestos == '03'
|
104
|
+
|
105
|
+
self.objeto_impuestos = (@tasa_iva > 0 || @tasa_ieps > 0 ? '02' : '01')
|
106
|
+
end
|
107
|
+
|
108
|
+
def impuestos_node
|
109
|
+
return @impuestos_node if defined?(@impuestos_node)
|
110
|
+
return nil unless objeto_impuestos?
|
111
|
+
|
112
|
+
@impuestos_node = children_nodes.select { |child| child.is_a?(Impuestos) }.first
|
113
|
+
return if @impuestos_node
|
114
|
+
|
115
|
+
@impuestos_node = Impuestos.new
|
116
|
+
self.children_nodes << @impuestos_node
|
117
|
+
@impuestos_node
|
118
|
+
end
|
119
|
+
|
120
|
+
def traslado_iva_node
|
121
|
+
return nil unless impuestos_node.is_a?(Impuestos)
|
122
|
+
|
123
|
+
impuestos_node.traslado_iva
|
124
|
+
end
|
125
|
+
end
|
126
|
+
end
|
@@ -0,0 +1,28 @@
|
|
1
|
+
module Cfdi40
|
2
|
+
class Impuestos < Node
|
3
|
+
define_attribute :total_impuestos_retenidos, xml_attribute: 'TotalImpuestosRetenidos', format: :t_Importe
|
4
|
+
define_attribute :total_impuestos_trasladados, xml_attribute: 'TotalImpuestosTrasladados', format: :t_Importe
|
5
|
+
|
6
|
+
def traslados
|
7
|
+
return @traslados if defined?(@traslados)
|
8
|
+
|
9
|
+
@traslados = Traslados.new
|
10
|
+
self.children_nodes << @traslados
|
11
|
+
@traslados
|
12
|
+
end
|
13
|
+
|
14
|
+
def traslados_node
|
15
|
+
children_nodes.select { |n| n.is_a?(Traslados)}.first
|
16
|
+
end
|
17
|
+
|
18
|
+
def traslado_nodes
|
19
|
+
return [] if traslados_node.nil?
|
20
|
+
|
21
|
+
traslados_node.traslado_nodes
|
22
|
+
end
|
23
|
+
|
24
|
+
def traslado_iva
|
25
|
+
traslados.traslado_iva
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
data/lib/cfdi40/node.rb
ADDED
@@ -0,0 +1,122 @@
|
|
1
|
+
module Cfdi40
|
2
|
+
class Node
|
3
|
+
# Nokigir XML Document for the xml_node
|
4
|
+
attr_accessor :xml_document, :xml_parent, :children_nodes
|
5
|
+
|
6
|
+
def initialize
|
7
|
+
self.class.verify_class_variables
|
8
|
+
@children_nodes = []
|
9
|
+
end
|
10
|
+
|
11
|
+
# Use class variables to define attributes used to create nodes
|
12
|
+
# Class variables are the same for children classes, so are organized by
|
13
|
+
# the name of the class.
|
14
|
+
def self.verify_class_variables
|
15
|
+
@@attributes ||= {}
|
16
|
+
@@attributes[name] ||= {}
|
17
|
+
@@namespaces ||= {}
|
18
|
+
@@namespaces[name] ||= {}
|
19
|
+
@@default_values ||= {}
|
20
|
+
@@default_values[name] ||= {}
|
21
|
+
@@formats ||= {}
|
22
|
+
@@formats[name] ||= {}
|
23
|
+
end
|
24
|
+
|
25
|
+
def self.define_attribute(accessor, xml_attribute:, default: nil, format: nil, readonly: false)
|
26
|
+
verify_class_variables
|
27
|
+
if readonly
|
28
|
+
attr_reader accessor.to_sym
|
29
|
+
else
|
30
|
+
attr_accessor accessor.to_sym
|
31
|
+
end
|
32
|
+
@@attributes[name][accessor.to_sym] = xml_attribute
|
33
|
+
if default
|
34
|
+
@@default_values[name][accessor.to_sym] = default
|
35
|
+
end
|
36
|
+
if format
|
37
|
+
@@formats[name][accessor.to_sym] = format
|
38
|
+
end
|
39
|
+
end
|
40
|
+
|
41
|
+
def self.define_namespace(namespace, value)
|
42
|
+
verify_class_variables
|
43
|
+
@@namespaces[name][namespace] = value
|
44
|
+
end
|
45
|
+
|
46
|
+
def self.namespaces
|
47
|
+
@@namespaces[name]
|
48
|
+
end
|
49
|
+
|
50
|
+
def self.attributes
|
51
|
+
@@attributes[name]
|
52
|
+
end
|
53
|
+
|
54
|
+
def self.default_values
|
55
|
+
@@default_values[name]
|
56
|
+
end
|
57
|
+
|
58
|
+
def self.formats
|
59
|
+
@@formats[name]
|
60
|
+
end
|
61
|
+
|
62
|
+
def set_defaults
|
63
|
+
return if self.class.default_values.nil?
|
64
|
+
|
65
|
+
self.class.default_values.each do |accessor, value|
|
66
|
+
next unless attibute_is_null?(accessor)
|
67
|
+
|
68
|
+
instance_variable_set "@#{accessor}".to_sym, value
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def attibute_is_null?(accessor)
|
73
|
+
return true unless instance_variable_defined?("@#{accessor}".to_sym)
|
74
|
+
|
75
|
+
instance_variable_get("@#{accessor}".to_sym).nil?
|
76
|
+
end
|
77
|
+
|
78
|
+
def create_xml_node
|
79
|
+
set_defaults
|
80
|
+
if self.respond_to?(:before_add, true)
|
81
|
+
self.before_add
|
82
|
+
end
|
83
|
+
xml_node = xml_document.create_element("cfdi:#{self.class.name.split('::').last}")
|
84
|
+
add_namespaces_to(xml_node)
|
85
|
+
add_attributes_to(xml_node)
|
86
|
+
add_children_to(xml_node)
|
87
|
+
xml_parent.add_child xml_node
|
88
|
+
end
|
89
|
+
|
90
|
+
def add_namespaces_to(xml_node)
|
91
|
+
self.class.namespaces.each do |namespace, value|
|
92
|
+
xml_node.add_namespace namespace, value
|
93
|
+
end
|
94
|
+
end
|
95
|
+
|
96
|
+
def add_attributes_to(node)
|
97
|
+
self.class.attributes.each do |object_accessor, xml_attribute|
|
98
|
+
next unless respond_to?(object_accessor)
|
99
|
+
next if public_send(object_accessor).nil?
|
100
|
+
|
101
|
+
node[xml_attribute] = formated_value(object_accessor)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def add_children_to(xml_node)
|
106
|
+
children_nodes.each do |node|
|
107
|
+
node.xml_document = xml_document
|
108
|
+
node.xml_parent = xml_node
|
109
|
+
node.create_xml_node
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
def formated_value(accessor)
|
114
|
+
case self.class.formats[accessor]
|
115
|
+
when :t_Importe
|
116
|
+
sprintf("%0.6f", public_send(accessor).to_f)
|
117
|
+
else
|
118
|
+
public_send(accessor)
|
119
|
+
end
|
120
|
+
end
|
121
|
+
end
|
122
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
module Cfdi40
|
2
|
+
class Receptor < Node
|
3
|
+
define_attribute :rfc, xml_attribute: 'Rfc'
|
4
|
+
define_attribute :nombre, xml_attribute: 'Nombre'
|
5
|
+
define_attribute :domicilio_fiscal, xml_attribute: 'DomicilioFiscalReceptor'
|
6
|
+
define_attribute :residencia_fiscal, xml_attribute: 'ResidenciaFiscal'
|
7
|
+
define_attribute :num_reg_id_trib, xml_attribute: 'NumRegIdTrib'
|
8
|
+
define_attribute :regimen_fiscal, xml_attribute: 'RegimenFiscalReceptor'
|
9
|
+
define_attribute :uso_cfdi, xml_attribute: 'UsoCFDI'
|
10
|
+
end
|
11
|
+
end
|