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.
- checksums.yaml +7 -0
- data/COMPLIANCE.md +149 -0
- data/LICENSE +21 -0
- data/README.md +404 -0
- data/doc/FUENTES.md +505 -0
- data/lib/generators/verifactu/install/install_generator.rb +61 -0
- data/lib/generators/verifactu/install/templates/instalar_verifactu.rb.tt +19 -0
- data/lib/generators/verifactu/install/templates/verifactu.rb.tt +64 -0
- data/lib/verifactu-rails.rb +31 -0
- data/lib/verifactu_rails/certificado.rb +122 -0
- data/lib/verifactu_rails/consulta.rb +279 -0
- data/lib/verifactu_rails/desglose.rb +396 -0
- data/lib/verifactu_rails/envio.rb +153 -0
- data/lib/verifactu_rails/error.rb +31 -0
- data/lib/verifactu_rails/formato.rb +199 -0
- data/lib/verifactu_rails/huella.rb +130 -0
- data/lib/verifactu_rails/importe.rb +80 -0
- data/lib/verifactu_rails/libro/autochequeo.rb +67 -0
- data/lib/verifactu_rails/libro/cadena.rb +110 -0
- data/lib/verifactu_rails/libro/migracion.rb +92 -0
- data/lib/verifactu_rails/libro/reconciliacion.rb +224 -0
- data/lib/verifactu_rails/libro/registro.rb +74 -0
- data/lib/verifactu_rails/libro/remesa.rb +120 -0
- data/lib/verifactu_rails/libro.rb +89 -0
- data/lib/verifactu_rails/qr.rb +56 -0
- data/lib/verifactu_rails/railtie.rb +30 -0
- data/lib/verifactu_rails/registro.rb +833 -0
- data/lib/verifactu_rails/respuesta.rb +154 -0
- data/lib/verifactu_rails/schemas/ConsultaLR.xsd +54 -0
- data/lib/verifactu_rails/schemas/EventosSIF.xsd +823 -0
- data/lib/verifactu_rails/schemas/PROCEDENCIA.md +64 -0
- data/lib/verifactu_rails/schemas/RespuestaConsultaLR.xsd +201 -0
- data/lib/verifactu_rails/schemas/RespuestaSuministro.xsd +139 -0
- data/lib/verifactu_rails/schemas/RespuestaValRegistNoVeriFactu.xsd +103 -0
- data/lib/verifactu_rails/schemas/SuministroInformacion.xsd +1390 -0
- data/lib/verifactu_rails/schemas/SuministroLR.xsd +25 -0
- data/lib/verifactu_rails/schemas/catalog.xml +5 -0
- data/lib/verifactu_rails/schemas/xmldsig-core-schema.xsd +318 -0
- data/lib/verifactu_rails/sistema_informatico.rb +88 -0
- data/lib/verifactu_rails/transporte.rb +127 -0
- data/lib/verifactu_rails/version.rb +5 -0
- 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
|