cfdi40 0.0.1.alfa → 0.0.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +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
|