verifactu-rails 0.1.0.pre

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (42) hide show
  1. checksums.yaml +7 -0
  2. data/COMPLIANCE.md +149 -0
  3. data/LICENSE +21 -0
  4. data/README.md +404 -0
  5. data/doc/FUENTES.md +505 -0
  6. data/lib/generators/verifactu/install/install_generator.rb +61 -0
  7. data/lib/generators/verifactu/install/templates/instalar_verifactu.rb.tt +19 -0
  8. data/lib/generators/verifactu/install/templates/verifactu.rb.tt +64 -0
  9. data/lib/verifactu-rails.rb +31 -0
  10. data/lib/verifactu_rails/certificado.rb +122 -0
  11. data/lib/verifactu_rails/consulta.rb +279 -0
  12. data/lib/verifactu_rails/desglose.rb +396 -0
  13. data/lib/verifactu_rails/envio.rb +153 -0
  14. data/lib/verifactu_rails/error.rb +31 -0
  15. data/lib/verifactu_rails/formato.rb +199 -0
  16. data/lib/verifactu_rails/huella.rb +130 -0
  17. data/lib/verifactu_rails/importe.rb +80 -0
  18. data/lib/verifactu_rails/libro/autochequeo.rb +67 -0
  19. data/lib/verifactu_rails/libro/cadena.rb +110 -0
  20. data/lib/verifactu_rails/libro/migracion.rb +92 -0
  21. data/lib/verifactu_rails/libro/reconciliacion.rb +224 -0
  22. data/lib/verifactu_rails/libro/registro.rb +74 -0
  23. data/lib/verifactu_rails/libro/remesa.rb +120 -0
  24. data/lib/verifactu_rails/libro.rb +89 -0
  25. data/lib/verifactu_rails/qr.rb +56 -0
  26. data/lib/verifactu_rails/railtie.rb +30 -0
  27. data/lib/verifactu_rails/registro.rb +833 -0
  28. data/lib/verifactu_rails/respuesta.rb +154 -0
  29. data/lib/verifactu_rails/schemas/ConsultaLR.xsd +54 -0
  30. data/lib/verifactu_rails/schemas/EventosSIF.xsd +823 -0
  31. data/lib/verifactu_rails/schemas/PROCEDENCIA.md +64 -0
  32. data/lib/verifactu_rails/schemas/RespuestaConsultaLR.xsd +201 -0
  33. data/lib/verifactu_rails/schemas/RespuestaSuministro.xsd +139 -0
  34. data/lib/verifactu_rails/schemas/RespuestaValRegistNoVeriFactu.xsd +103 -0
  35. data/lib/verifactu_rails/schemas/SuministroInformacion.xsd +1390 -0
  36. data/lib/verifactu_rails/schemas/SuministroLR.xsd +25 -0
  37. data/lib/verifactu_rails/schemas/catalog.xml +5 -0
  38. data/lib/verifactu_rails/schemas/xmldsig-core-schema.xsd +318 -0
  39. data/lib/verifactu_rails/sistema_informatico.rb +88 -0
  40. data/lib/verifactu_rails/transporte.rb +127 -0
  41. data/lib/verifactu_rails/version.rb +5 -0
  42. metadata +124 -0
@@ -0,0 +1,833 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative 'formato'
4
+ require_relative 'importe'
5
+ require_relative 'huella'
6
+ require_relative 'desglose'
7
+ require_relative 'sistema_informatico'
8
+ require_relative 'error'
9
+
10
+ module VerifactuRails
11
+ IDVERSION = '1.0' # sf:VersionType solo admite este valor
12
+ TIPO_HUELLA = '01' # sf:TipoHuellaType solo admite este valor
13
+
14
+ # Espacios de nombres. Son URLs de www2 aunque los XSD se descarguen de
15
+ # prewww2: el targetNamespace de los esquemas apunta a producción en ambos
16
+ # entornos, y es el targetNamespace lo que manda.
17
+ #
18
+ # Los prefijos que se emiten son "sum" y "sum1", los mismos que usa la AEAT en
19
+ # sus ejemplos de petición y los mismos que usa mybooking-es/verifactu-rb. Para
20
+ # un parser XML el prefijo es irrelevante —lo que liga es el URI—, pero
21
+ # coincidir permite comparar nuestra salida con los ejemplos oficiales línea a
22
+ # línea cuando algo se rechaza, que es justo cuando menos ganas hay de traducir
23
+ # prefijos mentalmente.
24
+ NS_SF = 'https://www2.agenciatributaria.gob.es/static_files/common/internet/' \
25
+ 'dep/aplicaciones/es/aeat/tike/cont/ws/SuministroInformacion.xsd'
26
+ NS_LR = 'https://www2.agenciatributaria.gob.es/static_files/common/internet/' \
27
+ 'dep/aplicaciones/es/aeat/tike/cont/ws/SuministroLR.xsd'
28
+
29
+ # Identificación de la factura anterior de la cadena (sf:EncadenamientoFactura-
30
+ # AnteriorType). No basta con la huella previa: el registro anterior se
31
+ # identifica por emisor, serie y fecha además del hash.
32
+ class RegistroAnterior
33
+ attr_reader :id_emisor, :num_serie, :fecha_expedicion, :huella
34
+
35
+ def initialize(id_emisor:, num_serie:, fecha_expedicion:, huella:)
36
+ @id_emisor = Formato.nif(id_emisor, 'IDEmisorFactura anterior')
37
+ @num_serie = Formato.num_serie(num_serie)
38
+ @fecha_expedicion = Formato.fecha(fecha_expedicion)
39
+ @huella = Formato.limitar(huella, 'Huella anterior', 64)
40
+
41
+ unless @huella.match?(Huella::PATRON_HUELLA)
42
+ raise ValidacionError,
43
+ "La huella anterior debe ser SHA-256 hex en MAYÚSCULAS: #{@huella.inspect}"
44
+ end
45
+ end
46
+
47
+ def a_pares
48
+ { 'IDEmisorFactura' => id_emisor, 'NumSerieFactura' => num_serie,
49
+ 'FechaExpedicionFactura' => fecha_expedicion, 'Huella' => huella }
50
+ end
51
+ end
52
+
53
+ # Identificación de una factura ajena a la que este registro se refiere:
54
+ # rectificada o sustituida (sf:IDFacturaARType). Son los mismos tres campos que
55
+ # identifican cualquier factura, sin la huella.
56
+ class IdFactura
57
+ attr_reader :id_emisor, :num_serie, :fecha_expedicion
58
+
59
+ def initialize(id_emisor:, num_serie:, fecha_expedicion:)
60
+ @id_emisor = Formato.nif(id_emisor, 'IDEmisorFactura')
61
+ @num_serie = Formato.num_serie(num_serie)
62
+ @fecha_expedicion = Formato.fecha(fecha_expedicion)
63
+ end
64
+
65
+ def a_pares
66
+ { 'IDEmisorFactura' => id_emisor, 'NumSerieFactura' => num_serie,
67
+ 'FechaExpedicionFactura' => fecha_expedicion }
68
+ end
69
+ end
70
+
71
+ # Base y cuota de lo que se sustituye (sf:DesgloseRectificacionType).
72
+ #
73
+ # Solo tiene sentido en rectificativas POR SUSTITUCIÓN: la factura reexpresa el
74
+ # importe corregido completo, así que hay que declarar cuál era el original. En
75
+ # las incrementales los importes de la propia factura YA son la diferencia, y no
76
+ # hay nada que sustituir.
77
+ class ImporteRectificacion
78
+ attr_reader :base, :cuota, :cuota_recargo
79
+
80
+ def initialize(base:, cuota:, cuota_recargo: nil)
81
+ @base = Importe.formatear(base)
82
+ @cuota = Importe.formatear(cuota)
83
+ @cuota_recargo = cuota_recargo && Importe.formatear(cuota_recargo)
84
+ end
85
+
86
+ def a_pares
87
+ pares = { 'BaseRectificada' => base, 'CuotaRectificada' => cuota }
88
+ pares['CuotaRecargoRectificado'] = cuota_recargo if cuota_recargo
89
+ pares
90
+ end
91
+ end
92
+
93
+ # Identificación de quien no tiene NIF español (sf:IDOtroType).
94
+ #
95
+ # Las reglas NO son las mismas según quién se identifique. Un destinatario
96
+ # admite "no censado" (07) y desde España acepta 03 o 07; un tercero no admite
97
+ # 07 en absoluto y desde España solo acepta 03 (Validaciones v1.2.2, ap. 12 y
98
+ # 13). Por eso lo que varía entra por parámetro: duplicar el método era la vía
99
+ # segura a que las dos copias se desincronizaran.
100
+ module IdOtro
101
+ # Lista L7 del Excel de diseños.
102
+ TIPOS = %w[02 03 04 05 06 07].freeze
103
+
104
+ # sf:CountryType2. Se toman del XSD, que es el contrato.
105
+ PAISES = %w[
106
+ AF AL DE AD AO AI AQ AG SA DZ AR AM AW AU AT AZ BS BH BD BB BE BZ BJ BM BY
107
+ BO BA BW BV BR BN BG BF BI BT CV KY KH CM CA CF CC CO KM CG CD CK KP KR CI
108
+ CR HR CU TD CZ CL CN CY CW DK DM DO EC EG AE ER SK SI ES US EE ET FO PH FI
109
+ FJ FR GA GM GE GS GH GI GD GR GL GU GT GG GN GQ GW GY HT HM HN HK HU IN ID
110
+ IR IQ IE IM IS IL IT JM JP JE JO KZ KE KG KI KW LA LS LV LB LR LY LI LT LU
111
+ XG MO MK MG MY MW MV ML MT FK MP MA MH MU MR YT UM MX FM MD MC MN ME MS MZ
112
+ MM NA NR CX NP NI NE NG NU NF NO NC NZ IO OM NL BQ PK PW PA PG PY PE PN PF
113
+ PL PT PR QA GB RW RO RU RE SB SV WS AS KN SM SX PM VC SH LC ST SN RS SC SL
114
+ SG SY SO LK SZ ZA SD SS SE CH SR TH TW TZ TJ PS TF TL TG TK TO TT TN TC TM
115
+ TR TV UA UG UY UZ VU VA VE VN VG VI WF YE DJ ZM ZW QU XB XU XN
116
+ ].freeze
117
+
118
+ module_function
119
+
120
+ # @param quien [String] para el mensaje de error
121
+ # @param tipos_con_es [Array<String>] IDType admitidos si CodigoPais = "ES"
122
+ # @param prohibidos [Array<String>] IDType que este bloque no admite nunca
123
+ def normalizar(datos, quien:, tipos_con_es:, prohibidos: [])
124
+ tipo = Formato.enumerado(datos.fetch(:id_type), 'IDType', TIPOS - prohibidos)
125
+ pais = datos[:codigo_pais] && Formato.enumerado(datos[:codigo_pais], 'CodigoPais', PAISES)
126
+
127
+ # "No censado" solo tiene sentido en España.
128
+ if tipo == '07' && pais != 'ES'
129
+ raise ValidacionError, "IDType=07 (no censado) exige CodigoPais='ES'"
130
+ end
131
+ if pais == 'ES' && !tipos_con_es.include?(tipo)
132
+ raise ValidacionError,
133
+ "Con CodigoPais='ES', el IDType de #{quien} solo puede ser " \
134
+ "#{tipos_con_es.join(' o ')}"
135
+ end
136
+ # Con NIF-IVA (02) el país va implícito en el propio identificador.
137
+ raise ValidacionError, "IDType=#{tipo} exige CodigoPais" if tipo != '02' && pais.nil?
138
+
139
+ { 'CodigoPais' => pais, 'IDType' => tipo,
140
+ 'ID' => Formato.limitar(datos.fetch(:id), 'ID', 20) }.compact
141
+ end
142
+ end
143
+
144
+ # Una persona identificada por NIF español O por identificación extranjera,
145
+ # nunca las dos (sf:PersonaFisicaJuridicaType lo modela como <choice>).
146
+ # Destinatario y Tercero comparten forma pero no reglas de identificación.
147
+ class Persona
148
+ attr_reader :nombre_razon, :nif, :id_otro
149
+
150
+ # @param id_otro [Hash, nil] para no residentes:
151
+ # { codigo_pais: 'FR', id_type: '02', id: 'FR12345678901' }
152
+ def initialize(nombre_razon:, nif: nil, id_otro: nil)
153
+ if nif.nil? == id_otro.nil?
154
+ raise ValidacionError,
155
+ "Indica exactamente uno de nif: o id_otro: para #{self.class.quien}"
156
+ end
157
+
158
+ @nombre_razon = Formato.limitar(nombre_razon, 'NombreRazon', 120)
159
+ @nif = nif && Formato.nif(nif, "NIF de #{self.class.quien}")
160
+ @id_otro = id_otro && IdOtro.normalizar(id_otro, quien: self.class.quien,
161
+ tipos_con_es: self.class.tipos_con_es,
162
+ prohibidos: self.class.prohibidos)
163
+ end
164
+
165
+ def id_type = id_otro && id_otro['IDType']
166
+
167
+ # Orden EXACTO de sf:PersonaFisicaJuridicaType.
168
+ def a_pares
169
+ pares = { 'NombreRazon' => nombre_razon }
170
+ nif ? pares.merge('NIF' => nif) : pares.merge('IDOtro' => id_otro)
171
+ end
172
+ end
173
+
174
+ # Un destinatario de la factura. Ap. 13: admite pasaporte (03) o no censado
175
+ # (07) desde España; un residente español con NIF se identifica con NIF.
176
+ class Destinatario < Persona
177
+ def self.quien = 'un destinatario'
178
+ def self.tipos_con_es = %w[03 07]
179
+ def self.prohibidos = []
180
+ end
181
+
182
+ # El tercero que expide la factura o genera el registro. Ap. 12: aquí NO cabe
183
+ # "no censado" y desde España solo pasaporte, a diferencia del destinatario.
184
+ class Tercero < Persona
185
+ def self.quien = 'un tercero'
186
+ def self.tipos_con_es = %w[03]
187
+ def self.prohibidos = %w[07]
188
+ end
189
+
190
+ # Registro de facturación de ALTA.
191
+ #
192
+ # El punto entero de esta clase: la huella y el XML salen de los MISMOS campos
193
+ # ya normalizados. Calcular una por un lado y montar el otro por otro es el bug
194
+ # número uno del dominio, porque la AEAT recalcula la huella sobre el XML que
195
+ # recibe. Aquí eso no puede pasar por construcción.
196
+ class RegistroAlta
197
+ TIPOS_FACTURA = %w[F1 F2 F3 R1 R2 R3 R4 R5].freeze
198
+ TIPOS_RECTIFICATIVA = %w[S I].freeze # S = sustitutiva, I = incremental
199
+ MAXIMO_REFERENCIADAS = 1000 # maxOccurs del esquema
200
+
201
+ # RechazoPrevio en un ALTA admite tres valores (la anulación solo dos):
202
+ # N no hubo rechazo previo de la AEAT
203
+ # S lo hubo, y el registro sí existe en la AEAT
204
+ # X el registro NO existe en la AEAT, se remitiera o no antes. Es la vía
205
+ # para subsanar algo que está en el SIF del obligado pero nunca llegó,
206
+ # típicamente al migrar desde NO VERI*FACTU.
207
+ RECHAZOS_PREVIOS = %w[N S X].freeze
208
+
209
+ # Destinatarios: obligatorio en unos tipos, prohibido en otros
210
+ # (Validaciones v1.2.2, ap. 13). F2 y R5 son las simplificadas, donde por
211
+ # definición no se identifica al destinatario.
212
+ #
213
+ # OJO al reutilizar esta lista: otras dos reglas coinciden con ella por
214
+ # motivos distintos -el NIF-IVA de un destinatario (ap. 13) y la inversión
215
+ # del sujeto pasivo (ap. 15.4) exigen los mismos tipos-, así que si la AEAT
216
+ # cambia una sola de las tres habrá que separarlas.
217
+ TIPOS_CON_DESTINATARIO = %w[F1 F3 R1 R2 R3 R4].freeze
218
+ TIPOS_SIN_DESTINATARIO = %w[F2 R5].freeze
219
+
220
+ # Ap. 11. Quién emitió la factura, si no fue el propio obligado.
221
+ EMISORES = %w[D T].freeze # D = destinatario, T = tercero
222
+
223
+ # Ap. 14. El cupón solo cabe en estos dos tipos.
224
+ TIPOS_CON_CUPON = %w[R1 R5].freeze
225
+
226
+ # Entrada en vigor de la Orden HAC/1177/2024.
227
+ FECHA_MINIMA = Date.new(2024, 10, 28)
228
+
229
+ # Ap. 15.8. Una factura simplificada (F2) no puede pasar de 3.000 €, sumando
230
+ # base y cuota de todas las líneas del desglose. El margen lo concede la
231
+ # propia AEAT.
232
+ MAXIMO_SIMPLIFICADA = BigDecimal('3000.00')
233
+ MARGEN_SIMPLIFICADA = BigDecimal('10.00')
234
+
235
+ # Ap. 3.1.3.10. Obligatorio a partir de cien millones, en valor absoluto.
236
+ UMBRAL_MACRODATO = BigDecimal('100000000.00')
237
+
238
+ # Ap. 3.1.3.8 y 3.1.3.9: cada marca solo cabe en unos tipos de factura.
239
+ TIPOS_SIMPLIFICADA_CUALIFICADA = %w[F1 F3 R1 R2 R3 R4].freeze
240
+ TIPOS_SIN_IDENTIF_DESTINATARIO = %w[F2 R5].freeze
241
+
242
+ # Ap. 15.6.4: los tipos que el grupo de entidades de nivel avanzado excluye.
243
+ TIPOS_SIN_DESTINATARIO_NI_SUSTITUTIVA = %w[F2 F3 R5].freeze
244
+ # Ap. 15.6.9.
245
+ TIPOS_IVA_PENDIENTE_AAPP = %w[F1 R1 R2 R3 R4].freeze
246
+ PREFIJOS_NIF_AAPP = %w[P Q S V].freeze
247
+
248
+ attr_reader :id_emisor, :num_serie, :fecha_expedicion, :nombre_razon_emisor,
249
+ :tipo_factura, :descripcion_operacion, :desglose, :cuota_total,
250
+ :importe_total, :sistema_informatico, :fecha_hora_gen,
251
+ :destinatarios, :fecha_operacion, :tipo_rectificativa,
252
+ :facturas_rectificadas, :facturas_sustituidas,
253
+ :importe_rectificacion, :subsanacion, :rechazo_previo,
254
+ :simplificada_cualificada, :sin_identif_destinatario, :macrodato,
255
+ :num_registro_acuerdo, :id_acuerdo_sistema, :emitida_por,
256
+ :tercero, :cupon
257
+
258
+ # @param subsanacion ['S', 'N', true, false, nil] marca el alta como
259
+ # subsanación de un registro ya generado. Es el ÚNICO mecanismo para
260
+ # corregir un registro que la AEAT aceptó con errores.
261
+ # @param rechazo_previo ['N', 'S', 'X', nil] ver RECHAZOS_PREVIOS.
262
+ def initialize(id_emisor:, num_serie:, fecha_expedicion:, nombre_razon_emisor:,
263
+ tipo_factura:, descripcion_operacion:, desglose:,
264
+ cuota_total:, importe_total:, sistema_informatico:,
265
+ fecha_hora_gen:, destinatarios: [], fecha_operacion: nil,
266
+ tipo_rectificativa: nil, facturas_rectificadas: [],
267
+ facturas_sustituidas: [], importe_rectificacion: nil,
268
+ subsanacion: nil, rechazo_previo: nil,
269
+ simplificada_cualificada: nil, sin_identif_destinatario: nil,
270
+ macrodato: nil, num_registro_acuerdo: nil, id_acuerdo_sistema: nil,
271
+ emitida_por: nil, tercero: nil, cupon: nil)
272
+ @id_emisor = Formato.nif(id_emisor, 'IDEmisorFactura')
273
+ @num_serie = Formato.num_serie(num_serie)
274
+ @fecha_expedicion = Formato.fecha(fecha_expedicion)
275
+ @nombre_razon_emisor = Formato.limitar(nombre_razon_emisor, 'NombreRazonEmisor', 120)
276
+ @tipo_factura = Formato.enumerado(tipo_factura, 'TipoFactura', TIPOS_FACTURA)
277
+ @descripcion_operacion = Formato.limitar(descripcion_operacion, 'DescripcionOperacion', 500)
278
+ @desglose = desglose.is_a?(Desglose) ? desglose : Desglose.new(desglose)
279
+ @cuota_total = Importe.formatear(cuota_total)
280
+ @importe_total = Importe.formatear(importe_total)
281
+ @sistema_informatico = Formato.objeto(sistema_informatico, 'sistema_informatico',
282
+ SistemaInformatico)
283
+ @fecha_hora_gen = Formato.marca_temporal(fecha_hora_gen)
284
+ @destinatarios = Formato.coleccion(destinatarios, 'destinatarios', Destinatario,
285
+ maximo: MAXIMO_REFERENCIADAS)
286
+ @fecha_operacion = fecha_operacion && Formato.fecha(fecha_operacion)
287
+ @tipo_rectificativa = tipo_rectificativa &&
288
+ Formato.enumerado(tipo_rectificativa, 'TipoRectificativa', TIPOS_RECTIFICATIVA)
289
+ @facturas_rectificadas = Formato.coleccion(facturas_rectificadas, 'facturas_rectificadas',
290
+ IdFactura, maximo: MAXIMO_REFERENCIADAS)
291
+ @facturas_sustituidas = Formato.coleccion(facturas_sustituidas, 'facturas_sustituidas',
292
+ IdFactura, maximo: MAXIMO_REFERENCIADAS)
293
+ @importe_rectificacion = importe_rectificacion &&
294
+ Formato.objeto(importe_rectificacion, 'importe_rectificacion',
295
+ ImporteRectificacion)
296
+ @subsanacion = Formato.si_no(subsanacion, 'Subsanacion')
297
+ @rechazo_previo = rechazo_previo &&
298
+ Formato.enumerado(rechazo_previo, 'RechazoPrevio', RECHAZOS_PREVIOS)
299
+ @simplificada_cualificada = Formato.si_no(simplificada_cualificada, 'FacturaSimplificadaArt7273')
300
+ @sin_identif_destinatario = Formato.si_no(sin_identif_destinatario,
301
+ 'FacturaSinIdentifDestinatarioArt61d')
302
+ @macrodato = Formato.si_no(macrodato, 'Macrodato')
303
+ @num_registro_acuerdo = num_registro_acuerdo &&
304
+ Formato.limitar(num_registro_acuerdo, 'NumRegistroAcuerdoFacturacion', 15)
305
+ @id_acuerdo_sistema = id_acuerdo_sistema &&
306
+ Formato.limitar(id_acuerdo_sistema, 'IdAcuerdoSistemaInformatico', 16)
307
+ @emitida_por = emitida_por &&
308
+ Formato.enumerado(emitida_por, 'EmitidaPorTerceroODestinatario', EMISORES)
309
+ @tercero = tercero && Formato.objeto(tercero, 'tercero', Tercero)
310
+ @cupon = Formato.si_no(cupon, 'Cupon')
311
+
312
+ validar_fecha_expedicion!(fecha_expedicion)
313
+ validar_subsanacion!
314
+ validar_marcas!
315
+ validar_macrodato!
316
+ validar_limite_simplificada!
317
+ validar_destinatarios!
318
+ validar_rectificativa!
319
+ validar_sustitutiva!
320
+ validar_emisor_tercero!
321
+ validar_cupon!
322
+ validar_desglose_cruzado!
323
+ end
324
+
325
+ # FechaOperacion, y la de expedición si aquella no se informa. Es la fecha
326
+ # contra la que la AEAT mide las ventanas temporales de tipos y recargos
327
+ # (Validaciones v1.2.2, ap. 15.1 y 15.3).
328
+ def fecha_referencia
329
+ Date.strptime(fecha_operacion || fecha_expedicion, '%d-%m-%Y')
330
+ end
331
+
332
+ def rectificativa? = tipo_factura.start_with?('R')
333
+
334
+ # Huella de este registro. `anterior` es nil solo en el primer registro de la
335
+ # cadena de ese SIF y NIF obligado (una sola cadena, no una por serie).
336
+ #
337
+ # @param anterior [RegistroAnterior, nil]
338
+ def huella(anterior: nil)
339
+ Huella.alta(
340
+ id_emisor: id_emisor, num_serie: num_serie,
341
+ fecha_expedicion: fecha_expedicion, tipo_factura: tipo_factura,
342
+ cuota_total: cuota_total, importe_total: importe_total,
343
+ fecha_hora_gen: fecha_hora_gen,
344
+ huella_anterior: anterior&.huella
345
+ )
346
+ end
347
+
348
+ # Emite el subárbol sf:RegistroAlta dentro de un builder ya posicionado.
349
+ def construir(xml, anterior: nil)
350
+ propia = huella(anterior: anterior)
351
+
352
+ xml['sum1'].RegistroAlta do
353
+ xml['sum1'].IDVersion IDVERSION
354
+ xml['sum1'].IDFactura do
355
+ xml['sum1'].IDEmisorFactura id_emisor
356
+ xml['sum1'].NumSerieFactura num_serie
357
+ xml['sum1'].FechaExpedicionFactura fecha_expedicion
358
+ end
359
+ xml['sum1'].NombreRazonEmisor nombre_razon_emisor
360
+ xml['sum1'].Subsanacion subsanacion if subsanacion
361
+ xml['sum1'].RechazoPrevio rechazo_previo if rechazo_previo
362
+ xml['sum1'].TipoFactura tipo_factura
363
+ xml['sum1'].TipoRectificativa tipo_rectificativa if tipo_rectificativa
364
+ construir_referenciadas(xml, 'FacturasRectificadas', 'IDFacturaRectificada',
365
+ facturas_rectificadas)
366
+ construir_referenciadas(xml, 'FacturasSustituidas', 'IDFacturaSustituida',
367
+ facturas_sustituidas)
368
+ if importe_rectificacion
369
+ xml['sum1'].ImporteRectificacion do
370
+ importe_rectificacion.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
371
+ end
372
+ end
373
+ xml['sum1'].FechaOperacion fecha_operacion if fecha_operacion
374
+ xml['sum1'].DescripcionOperacion descripcion_operacion
375
+ xml['sum1'].FacturaSimplificadaArt7273 simplificada_cualificada if simplificada_cualificada
376
+ if sin_identif_destinatario
377
+ xml['sum1'].FacturaSinIdentifDestinatarioArt61d sin_identif_destinatario
378
+ end
379
+ xml['sum1'].Macrodato macrodato if macrodato
380
+ xml['sum1'].EmitidaPorTerceroODestinatario emitida_por if emitida_por
381
+ xml['sum1'].Tercero { construir_persona(xml, tercero) } if tercero
382
+ construir_destinatarios(xml)
383
+ xml['sum1'].Cupon cupon if cupon
384
+ construir_desglose(xml)
385
+ xml['sum1'].CuotaTotal cuota_total
386
+ xml['sum1'].ImporteTotal importe_total
387
+ construir_encadenamiento(xml, anterior)
388
+ construir_sistema(xml)
389
+ xml['sum1'].FechaHoraHusoGenRegistro fecha_hora_gen
390
+ xml['sum1'].NumRegistroAcuerdoFacturacion num_registro_acuerdo if num_registro_acuerdo
391
+ xml['sum1'].IdAcuerdoSistemaInformatico id_acuerdo_sistema if id_acuerdo_sistema
392
+ xml['sum1'].TipoHuella TIPO_HUELLA
393
+ xml['sum1'].Huella propia
394
+ end
395
+ end
396
+
397
+ private
398
+
399
+ def construir_destinatarios(xml)
400
+ return if destinatarios.empty?
401
+
402
+ xml['sum1'].Destinatarios do
403
+ destinatarios.each do |d|
404
+ xml['sum1'].IDDestinatario { construir_persona(xml, d) }
405
+ end
406
+ end
407
+ end
408
+
409
+ # sf:PersonaFisicaJuridicaType. Lo comparten Destinatario y Tercero, que
410
+ # tienen la misma forma aunque no las mismas reglas de identificación.
411
+ def construir_persona(xml, persona)
412
+ xml['sum1'].NombreRazon persona.nombre_razon
413
+ if persona.nif
414
+ xml['sum1'].NIF persona.nif
415
+ else
416
+ xml['sum1'].IDOtro { persona.id_otro.each { |k, v| xml['sum1'].send(k, v) } }
417
+ end
418
+ end
419
+
420
+ def construir_desglose(xml)
421
+ xml['sum1'].Desglose do
422
+ desglose.detalles.each do |detalle|
423
+ xml['sum1'].DetalleDesglose do
424
+ detalle.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
425
+ end
426
+ end
427
+ end
428
+ end
429
+
430
+ def construir_encadenamiento(xml, anterior)
431
+ xml['sum1'].Encadenamiento do
432
+ if anterior.nil?
433
+ xml['sum1'].PrimerRegistro 'S'
434
+ else
435
+ xml['sum1'].RegistroAnterior do
436
+ anterior.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
437
+ end
438
+ end
439
+ end
440
+ end
441
+
442
+ def construir_sistema(xml)
443
+ xml['sum1'].SistemaInformatico do
444
+ sistema_informatico.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
445
+ end
446
+ end
447
+
448
+ def construir_referenciadas(xml, envoltorio, elemento, facturas)
449
+ return if facturas.empty?
450
+
451
+ xml['sum1'].send(envoltorio) do
452
+ facturas.each do |factura|
453
+ xml['sum1'].send(elemento) do
454
+ factura.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
455
+ end
456
+ end
457
+ end
458
+ end
459
+
460
+ # Validaciones v1.2.2, ap. 3.1.3.1. La fecha se compara ya normalizada para
461
+ # que dé igual si entró como Date o como cadena.
462
+ def validar_fecha_expedicion!(_original)
463
+ # Se reparsea SIEMPRE la fecha ya normalizada, en vez de mirar el objeto
464
+ # original. Con `original.is_a?(Date)` un DateTime entraba con su hora
465
+ # puesta, y como Date.today es medianoche, `DateTime.now > Date.today` es
466
+ # cierto: se rechazaba como "futura" una factura de hoy. Time no lo sufría
467
+ # porque no es un Date, así que la asimetría además era invisible.
468
+ fecha = Date.strptime(@fecha_expedicion, '%d-%m-%Y')
469
+
470
+ if fecha < FECHA_MINIMA
471
+ raise ValidacionError,
472
+ "FechaExpedicionFactura no puede ser anterior a " \
473
+ "#{FECHA_MINIMA.strftime('%d-%m-%Y')}, entrada en vigor de VERI*FACTU: " \
474
+ "#{@fecha_expedicion}"
475
+ end
476
+ return unless fecha > Date.today
477
+
478
+ raise ValidacionError,
479
+ "FechaExpedicionFactura no puede ser futura: #{@fecha_expedicion}"
480
+ end
481
+
482
+ # Ap. 3.1.3.2. Un RechazoPrevio distinto de "N" solo tiene sentido dentro de
483
+ # una subsanación: las combinaciones (Subsanacion=N, RechazoPrevio=S) y
484
+ # (Subsanacion=N, RechazoPrevio=X) no se admiten (lista L17).
485
+ def validar_subsanacion!
486
+ return if rechazo_previo.nil? || rechazo_previo == 'N'
487
+ return if subsanacion == 'S'
488
+
489
+ raise ValidacionError,
490
+ "RechazoPrevio=#{rechazo_previo} exige subsanacion: 'S'. " \
491
+ 'Un rechazo previo solo se declara al subsanar el registro rechazado.'
492
+ end
493
+
494
+ # Ap. 3.1.3.8 y 3.1.3.9: cada marca solo cabe en su familia de tipos.
495
+ def validar_marcas!
496
+ if simplificada_cualificada && !TIPOS_SIMPLIFICADA_CUALIFICADA.include?(tipo_factura)
497
+ raise ValidacionError,
498
+ "FacturaSimplificadaArt7273 no cabe con TipoFactura #{tipo_factura} " \
499
+ "(solo #{TIPOS_SIMPLIFICADA_CUALIFICADA.join(', ')})"
500
+ end
501
+ return unless sin_identif_destinatario && !TIPOS_SIN_IDENTIF_DESTINATARIO.include?(tipo_factura)
502
+
503
+ raise ValidacionError,
504
+ "FacturaSinIdentifDestinatarioArt61d no cabe con TipoFactura #{tipo_factura} " \
505
+ "(solo #{TIPOS_SIN_IDENTIF_DESTINATARIO.join(', ')})"
506
+ end
507
+
508
+ # Ap. 3.1.3.10. Se comprueba en valor absoluto: una rectificativa negativa de
509
+ # cien millones es igual de macrodato que la factura que corrige.
510
+ def validar_macrodato!
511
+ supera = BigDecimal(importe_total).abs >= UMBRAL_MACRODATO
512
+ return if supera == (macrodato == 'S')
513
+
514
+ if supera
515
+ raise ValidacionError,
516
+ "ImporteTotal #{importe_total} alcanza el umbral de macrodato: " \
517
+ "exige macrodato: 'S'"
518
+ end
519
+
520
+ raise ValidacionError,
521
+ "macrodato: 'S' exige un ImporteTotal de al menos " \
522
+ "#{UMBRAL_MACRODATO.to_s('F')} en valor absoluto (#{importe_total})"
523
+ end
524
+
525
+ # Ap. 15.8. El tope de las simplificadas se mide sobre el desglose, no sobre
526
+ # ImporteTotal, y decae si hay acuerdo de facturación o si la factura se
527
+ # acoge al art. 6.1.d) del RD 1619/2012.
528
+ def validar_limite_simplificada!
529
+ return unless tipo_factura == 'F2'
530
+ return if num_registro_acuerdo || sin_identif_destinatario == 'S'
531
+
532
+ suma = desglose.detalles.sum do |d|
533
+ BigDecimal(d.base_imponible) + BigDecimal(d.cuota_repercutida || '0')
534
+ end
535
+ return if suma <= MAXIMO_SIMPLIFICADA + MARGEN_SIMPLIFICADA
536
+
537
+ raise ValidacionError,
538
+ "Una factura simplificada (F2) no puede superar #{MAXIMO_SIMPLIFICADA.to_s('F')} " \
539
+ "sumando base y cuota del desglose (suma: #{Importe.formatear(suma)}). " \
540
+ 'Si hay acuerdo de facturación indica num_registro_acuerdo:, y si se ' \
541
+ "acoge al art. 6.1.d) indica sin_identif_destinatario: 'S'."
542
+ end
543
+
544
+ def validar_destinatarios!
545
+ if TIPOS_CON_DESTINATARIO.include?(tipo_factura) && destinatarios.empty?
546
+ raise ValidacionError,
547
+ "TipoFactura #{tipo_factura} exige al menos un destinatario"
548
+ end
549
+ return unless TIPOS_SIN_DESTINATARIO.include?(tipo_factura) && !destinatarios.empty?
550
+
551
+ raise ValidacionError,
552
+ "TipoFactura #{tipo_factura} es simplificada y no admite destinatarios"
553
+ end
554
+
555
+ # NO se implementa la última regla del ap. 13 ("si un destinatario se
556
+ # identifica por IDOtro con IDType=02, TipoFactura debe ser F1/F3/R1-R4")
557
+ # porque es REDUNDANTE con las dos de arriba: los ocho tipos de factura se
558
+ # reparten entre los que exigen destinatario (F1, F3, R1-R4) y los que lo
559
+ # prohíben (F2, R5), así que si hay un destinatario el tipo ya está en esa
560
+ # lista, se identifique como se identifique. Añadir el código habría dejado
561
+ # una comprobación que no puede fallar nunca: parece que cubre algo y no
562
+ # cubre nada.
563
+
564
+ # Ap. 11 y 12. Quién emitió la factura y el bloque Tercero se condicionan
565
+ # mutuamente, en las dos direcciones.
566
+ def validar_emisor_tercero!
567
+ if emitida_por == 'T' && tercero.nil?
568
+ raise ValidacionError,
569
+ "emitida_por: 'T' exige el bloque tercero: con quien expidió la factura"
570
+ end
571
+ if emitida_por == 'D' && destinatarios.empty?
572
+ raise ValidacionError,
573
+ "emitida_por: 'D' exige destinatarios: la emitió el destinatario, " \
574
+ 'así que hay que identificarlo'
575
+ end
576
+ if tercero && emitida_por != 'T'
577
+ raise ValidacionError,
578
+ "tercero: solo cabe con emitida_por: 'T' (recibido: #{emitida_por.inspect})"
579
+ end
580
+ return unless tercero&.nif == id_emisor
581
+
582
+ raise ValidacionError,
583
+ "El NIF del tercero (#{tercero.nif}) no puede ser el del emisor: si " \
584
+ 'la factura la expide el propio obligado, no hay tercero que declarar'
585
+ end
586
+
587
+ # Ap. 14. La norma dice "sólo se podrá rellenar con S", así que un 'N'
588
+ # explícito no es un valor válido: es un campo que sobra.
589
+ def validar_cupon!
590
+ return if cupon.nil?
591
+
592
+ if cupon == 'N'
593
+ raise ValidacionError,
594
+ "Cupon solo admite 'S'; si la factura no lleva cupón, omite el campo"
595
+ end
596
+ return if TIPOS_CON_CUPON.include?(tipo_factura)
597
+
598
+ raise ValidacionError,
599
+ "Cupon solo cabe con TipoFactura #{TIPOS_CON_CUPON.join(' o ')} " \
600
+ "(recibido #{tipo_factura})"
601
+ end
602
+
603
+ # Reglas que cruzan el desglose con datos del registro. Un Detalle aislado no
604
+ # conoce ni el TipoFactura ni la fecha de la operación, así que estas no
605
+ # pueden vivir en Detalle por mucho que hablen del desglose.
606
+ def validar_desglose_cruzado!
607
+ validar_inversion_sujeto_pasivo!
608
+ validar_grupo_entidades!
609
+ validar_cobro_por_cuenta_de_terceros!
610
+ validar_iva_pendiente_aapp!
611
+ desglose.detalles.each { |d| d.validar_en_fecha!(fecha_referencia) }
612
+ end
613
+
614
+ # Las claves de régimen cuyas reglas (ap. 15.6.4, 15.6.7 y 15.6.9) miran
615
+ # fuera de la línea. El resto de la 15.6 vive en Detalle.
616
+ #
617
+ # Igual que allí, solo cuentan las líneas de IVA o IGIC: la norma acota estas
618
+ # reglas a "Impuesto = 01, 03 o no se cumplimenta".
619
+ def usa_regimen?(clave)
620
+ desglose.detalles.any? do |d|
621
+ d.clave_regimen == clave && (d.iva? || d.impuesto == '03')
622
+ end
623
+ end
624
+
625
+ # Ap. 15.6.4. La segunda condición no se puede cumplir: exige
626
+ # BaseImponibleACoste, que esta gema no emite. Se para aquí en vez de armar
627
+ # un registro que la AEAT va a rechazar, y el mensaje dice por qué.
628
+ def validar_grupo_entidades!
629
+ return unless usa_regimen?('06')
630
+
631
+ if TIPOS_SIN_DESTINATARIO_NI_SUSTITUTIVA.include?(tipo_factura)
632
+ raise ValidacionError,
633
+ 'ClaveRegimen 06 (grupo de entidades, nivel avanzado) no cabe con ' \
634
+ "TipoFactura #{tipo_factura}: la norma excluye " \
635
+ "#{TIPOS_SIN_DESTINATARIO_NI_SUSTITUTIVA.join(', ')}"
636
+ end
637
+
638
+ raise ValidacionError,
639
+ 'ClaveRegimen 06 (grupo de entidades, nivel avanzado) exige ' \
640
+ 'BaseImponibleACoste, un campo que esta gema todavía no emite. Un ' \
641
+ 'registro así lo rechazaría la AEAT, así que no se construye.'
642
+ end
643
+
644
+ # Ap. 15.6.7. La calificación N1 la exige Detalle; aquí van las dos
645
+ # condiciones que necesitan la cabecera.
646
+ def validar_cobro_por_cuenta_de_terceros!
647
+ return unless usa_regimen?('10')
648
+
649
+ unless tipo_factura == 'F1'
650
+ raise ValidacionError,
651
+ 'ClaveRegimen 10 (cobro por cuenta de terceros) exige TipoFactura ' \
652
+ "F1, no #{tipo_factura}"
653
+ end
654
+ return if Array(destinatarios).all?(&:nif)
655
+
656
+ raise ValidacionError,
657
+ 'ClaveRegimen 10 (cobro por cuenta de terceros) exige que TODOS los ' \
658
+ 'destinatarios se identifiquen por NIF, no por IDOtro'
659
+ end
660
+
661
+ # Ap. 15.6.9. El NIF de una Administración Pública empieza por P, Q, S o V,
662
+ # y la norma lo exige explícitamente aquí.
663
+ def validar_iva_pendiente_aapp!
664
+ return unless usa_regimen?('14')
665
+
666
+ unless TIPOS_IVA_PENDIENTE_AAPP.include?(tipo_factura)
667
+ raise ValidacionError,
668
+ 'ClaveRegimen 14 (IVA pendiente de devengo, AAPP) exige TipoFactura ' \
669
+ "#{TIPOS_IVA_PENDIENTE_AAPP.join(', ')}, no #{tipo_factura}"
670
+ end
671
+ validar_fecha_operacion_aapp!
672
+ return if Array(destinatarios).all? { |d| d.nif && PREFIJOS_NIF_AAPP.include?(d.nif[0]) }
673
+
674
+ raise ValidacionError,
675
+ 'ClaveRegimen 14 (IVA pendiente de devengo, AAPP) exige que todos los ' \
676
+ "destinatarios lleven NIF y empiece por #{PREFIJOS_NIF_AAPP.join(', ')}"
677
+ end
678
+
679
+ def validar_fecha_operacion_aapp!
680
+ if fecha_operacion.nil?
681
+ raise ValidacionError,
682
+ 'ClaveRegimen 14 (IVA pendiente de devengo, AAPP) exige FechaOperacion'
683
+ end
684
+ return if Date.strptime(fecha_operacion, '%d-%m-%Y') >
685
+ Date.strptime(fecha_expedicion, '%d-%m-%Y')
686
+
687
+ raise ValidacionError,
688
+ 'ClaveRegimen 14 (IVA pendiente de devengo, AAPP) exige que ' \
689
+ "FechaOperacion (#{fecha_operacion}) sea posterior a la de " \
690
+ "expedición (#{fecha_expedicion})"
691
+ end
692
+
693
+ # Ap. 15.4, primera regla. Coincide en lista con TIPOS_CON_DESTINATARIO pero
694
+ # es otra regla: aquí lo que no cabe es repercutir por inversión del sujeto
695
+ # pasivo en una simplificada.
696
+ def validar_inversion_sujeto_pasivo!
697
+ return unless desglose.detalles.any? { |d| d.calificacion == 'S2' }
698
+ return if TIPOS_CON_DESTINATARIO.include?(tipo_factura)
699
+
700
+ raise ValidacionError,
701
+ "CalificacionOperacion S2 (inversión del sujeto pasivo) no cabe con " \
702
+ "TipoFactura #{tipo_factura}: solo #{TIPOS_CON_DESTINATARIO.join(', ')}"
703
+ end
704
+
705
+ # Coherencia de las rectificativas. El XSD deja casi todo opcional, así que
706
+ # estas reglas no las impone el esquema: sin ellas se puede montar un R1
707
+ # sintácticamente válido que la AEAT rechaza con un error mucho menos claro.
708
+ def validar_rectificativa!
709
+ if rectificativa?
710
+ if tipo_rectificativa.nil?
711
+ raise ValidacionError,
712
+ "TipoFactura #{tipo_factura} es rectificativa y exige " \
713
+ "tipo_rectificativa: 'S' (sustitutiva) o 'I' (incremental)"
714
+ end
715
+ # facturas_rectificadas NO es obligatoria: la AEAT dice literalmente
716
+ # "sólo podrá incluirse esta agrupación (no es obligatoria) si
717
+ # TipoFactura es R1-R5" (Validaciones v1.2.2, ap. 3.1.3.4).
718
+ else
719
+ unless tipo_rectificativa.nil? && facturas_rectificadas.empty?
720
+ raise ValidacionError,
721
+ "TipoFactura #{tipo_factura} no es rectificativa: no admite " \
722
+ 'tipo_rectificativa ni facturas_rectificadas (usa R1-R5)'
723
+ end
724
+ end
725
+
726
+ validar_importe_rectificacion!
727
+ end
728
+
729
+ # Una sustitutiva reexpresa el importe corregido completo, así que hay que
730
+ # declarar cuál era el original. Una incremental ya ES la diferencia.
731
+ # "Sólo deberá incluirse esta agrupación si el campo TipoRectificativa = 'S'.
732
+ # Obligatorio si TipoRectificativa = 'S'." (Validaciones v1.2.2, ap. 3.1.3.6)
733
+ #
734
+ # Las dos direcciones, y "sólo si S" incluye el caso en que TipoRectificativa
735
+ # ni siquiera existe: una F1 con ImporteRectificacion también lo incumple.
736
+ def validar_importe_rectificacion!
737
+ if tipo_rectificativa == 'S'
738
+ return unless importe_rectificacion.nil?
739
+
740
+ raise ValidacionError,
741
+ 'Una rectificativa por sustitución exige importe_rectificacion ' \
742
+ 'con la base y la cuota que se sustituyen'
743
+ end
744
+
745
+ return if importe_rectificacion.nil?
746
+
747
+ raise ValidacionError,
748
+ if tipo_rectificativa == 'I'
749
+ 'Una rectificativa incremental no lleva importe_rectificacion: ' \
750
+ 'sus propios importes ya son la diferencia'
751
+ else
752
+ "TipoFactura #{tipo_factura} no admite importe_rectificacion: " \
753
+ "la AEAT solo lo permite con TipoRectificativa='S'"
754
+ end
755
+ end
756
+
757
+ # F3 es la factura emitida en sustitución de simplificadas. La agrupación
758
+ # tampoco es obligatoria ahí, solo exclusiva de F3 (Validaciones v1.2.2,
759
+ # ap. 3.1.3.5).
760
+ def validar_sustitutiva!
761
+ if tipo_factura != 'F3' && !facturas_sustituidas.empty?
762
+ raise ValidacionError,
763
+ "TipoFactura #{tipo_factura} no admite facturas_sustituidas (usa F3)"
764
+ end
765
+ end
766
+ end
767
+
768
+ # Registro de facturación de ANULACIÓN.
769
+ class RegistroAnulacion
770
+ # A diferencia del alta, aquí RechazoPrevio NO admite "X": ese valor existe
771
+ # para subsanar altas que no constan en la AEAT, y una anulación de algo que
772
+ # no consta se expresa con SinRegistroPrevio.
773
+ RECHAZOS_PREVIOS = %w[N S].freeze
774
+
775
+ attr_reader :id_emisor, :num_serie, :fecha_expedicion,
776
+ :sistema_informatico, :fecha_hora_gen,
777
+ :sin_registro_previo, :rechazo_previo
778
+
779
+ # @param sin_registro_previo ['S', 'N', true, false, nil] la factura que se
780
+ # anula no consta en la AEAT (p. ej. se facturó en NO VERI*FACTU).
781
+ # @param rechazo_previo ['S', 'N', nil] la AEAT rechazó una anulación previa.
782
+ def initialize(id_emisor:, num_serie:, fecha_expedicion:,
783
+ sistema_informatico:, fecha_hora_gen:,
784
+ sin_registro_previo: nil, rechazo_previo: nil)
785
+ @id_emisor = Formato.nif(id_emisor, 'IDEmisorFacturaAnulada')
786
+ @num_serie = Formato.num_serie(num_serie, 'NumSerieFacturaAnulada')
787
+ @fecha_expedicion = Formato.fecha(fecha_expedicion)
788
+ @sistema_informatico = Formato.objeto(sistema_informatico, 'sistema_informatico',
789
+ SistemaInformatico)
790
+ @fecha_hora_gen = Formato.marca_temporal(fecha_hora_gen)
791
+ @sin_registro_previo = Formato.si_no(sin_registro_previo, 'SinRegistroPrevio')
792
+ @rechazo_previo = rechazo_previo &&
793
+ Formato.enumerado(rechazo_previo, 'RechazoPrevio', RECHAZOS_PREVIOS)
794
+ end
795
+
796
+ # Una anulación nunca puede iniciar la cadena, así que `anterior` es
797
+ # obligatorio. Huella.anulacion ya lo exige; lo repetimos aquí para fallar
798
+ # antes de construir nada.
799
+ def huella(anterior:)
800
+ Huella.anulacion(
801
+ id_emisor: id_emisor, num_serie: num_serie,
802
+ fecha_expedicion: fecha_expedicion, fecha_hora_gen: fecha_hora_gen,
803
+ huella_anterior: anterior&.huella
804
+ )
805
+ end
806
+
807
+ def construir(xml, anterior:)
808
+ propia = huella(anterior: anterior)
809
+
810
+ xml['sum1'].RegistroAnulacion do
811
+ xml['sum1'].IDVersion IDVERSION
812
+ xml['sum1'].IDFactura do
813
+ xml['sum1'].IDEmisorFacturaAnulada id_emisor
814
+ xml['sum1'].NumSerieFacturaAnulada num_serie
815
+ xml['sum1'].FechaExpedicionFacturaAnulada fecha_expedicion
816
+ end
817
+ xml['sum1'].SinRegistroPrevio sin_registro_previo if sin_registro_previo
818
+ xml['sum1'].RechazoPrevio rechazo_previo if rechazo_previo
819
+ xml['sum1'].Encadenamiento do
820
+ xml['sum1'].RegistroAnterior do
821
+ anterior.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
822
+ end
823
+ end
824
+ xml['sum1'].SistemaInformatico do
825
+ sistema_informatico.a_pares.each { |campo, valor| xml['sum1'].send(campo, valor) }
826
+ end
827
+ xml['sum1'].FechaHoraHusoGenRegistro fecha_hora_gen
828
+ xml['sum1'].TipoHuella TIPO_HUELLA
829
+ xml['sum1'].Huella propia
830
+ end
831
+ end
832
+ end
833
+ end