Code Coverage
 
Lines
Functions and Methods
Classes and Traits
Total
36.19% covered (danger)
36.19%
152 / 420
7.14% covered (danger)
7.14%
1 / 14
CRAP
n/a
0 / 0
_sii_eu_countries
0.00% covered (danger)
0.00%
0 / 4
0.00% covered (danger)
0.00%
0 / 1
2
_sii_recipient_is_spain
100.00% covered (success)
100.00%
1 / 1
100.00% covered (success)
100.00%
1 / 1
1
_sii_recipient_validate_spanish_taxid
50.00% covered (danger)
50.00%
5 / 10
0.00% covered (danger)
0.00%
0 / 1
8.12
_sii_recipient_from_client
83.33% covered (success)
83.33%
10 / 12
0.00% covered (danger)
0.00%
0 / 1
5.12
_sii_recipient_uses_nif
75.00% covered (warning)
75.00%
3 / 4
0.00% covered (danger)
0.00%
0 / 1
3.14
_sii_recipient_idotro
0.00% covered (danger)
0.00%
0 / 18
0.00% covered (danger)
0.00%
0 / 1
30
_sii_contraparte_identification_xml
16.67% covered (danger)
16.67%
2 / 12
0.00% covered (danger)
0.00%
0 / 1
13.26
_sii_contraparte_xml
87.50% covered (success)
87.50%
7 / 8
0.00% covered (danger)
0.00%
0 / 1
2.01
_sii_recap_period_from_substituted
71.43% covered (warning)
71.43%
10 / 14
0.00% covered (danger)
0.00%
0 / 1
5.58
_sii_substituted_invoices_xml
88.89% covered (success)
88.89%
16 / 18
0.00% covered (danger)
0.00%
0 / 1
6.05
sii_enrich_change_last_from_ticketdata
0.00% covered (danger)
0.00%
0 / 60
0.00% covered (danger)
0.00%
0 / 1
1892
_build_sii_request_AltaFacturas
98.98% covered (success)
98.98%
97 / 98
0.00% covered (danger)
0.00%
0 / 1
17
_build_sii_request_RectificacionFacturas
0.00% covered (danger)
0.00%
0 / 103
0.00% covered (danger)
0.00%
0 / 1
210
_build_sii_request_BajaFacturas
0.00% covered (danger)
0.00%
0 / 57
0.00% covered (danger)
0.00%
0 / 1
90
1<?php
2    include_once __DIR__.'/../legacy/session/session.php';
3    function _sii_eu_countries(){
4        return [
5            'AT', 'BE', 'BG', 'CY', 'CZ', 'DE', 'DK', 'EE', 'ES', 'FI', 'FR', 'GR', 'HR', 'HU',
6            'IE', 'IT', 'LT', 'LU', 'LV', 'MT', 'NL', 'PL', 'PT', 'RO', 'SE', 'SI', 'SK',
7        ];
8    }
9
10    function _sii_recipient_is_spain(?string $country){
11        return strtoupper(trim((string)$country)) === 'ES';
12    }
13
14    function _sii_recipient_validate_spanish_taxid(string $taxid){
15        $taxid = strtoupper(preg_replace('/[\s.\-\/]/', '', $taxid) ?? '');
16        if (strlen($taxid) !== 9){
17            return false;
18        }
19        if (preg_match('/^\d{8}[A-Z]$/', $taxid) === 1){
20            return true;
21        }
22        if (preg_match('/^[A-Z]\d{7}[A-Z0-9]$/', $taxid) === 1){
23            return true;
24        }
25        if (preg_match('/^[A-Z]\d{8}$/', $taxid) === 1){
26            return true;
27        }
28
29        return false;
30    }
31
32    function _sii_recipient_from_client(?array $client){
33        if (!is_array($client)){
34            return null;
35        }
36
37        $name = trim((string)($client['name'] ?? ''));
38        $taxid = trim((string)($client['taxid'] ?? $client['taxId'] ?? ''));
39        if (($name === '') || ($taxid === '')){
40            return null;
41        }
42
43        $country = strtoupper(trim((string)($client['country'] ?? '')));
44
45        return [
46            'name' => $name,
47            'taxId' => $taxid,
48            'country' => ($country !== '') ? $country : null,
49        ];
50    }
51
52    function _sii_recipient_uses_nif(array $recipient){
53        $country = $recipient['country'] ?? null;
54        if (($country !== null) && !_sii_recipient_is_spain($country)){
55            return false;
56        }
57
58        return _sii_recipient_validate_spanish_taxid((string)($recipient['taxId'] ?? ''));
59    }
60
61    function _sii_recipient_idotro(array $recipient){
62        if (_sii_recipient_uses_nif($recipient)){
63            return null;
64        }
65
66        $taxid = (string)($recipient['taxId'] ?? '');
67        $country = $recipient['country'] ?? null;
68        if ($country === null){
69            return [
70                'codigoPais' => '',
71                'idType' => '06',
72                'id' => substr($taxid, 0, 20),
73            ];
74        }
75
76        $idType = in_array($country, _sii_eu_countries(), true) ? '02' : '04';
77        if (strlen($taxid) > 20){
78            $taxid = substr($taxid, 0, 20);
79        }
80
81        return [
82            'codigoPais' => $country,
83            'idType' => $idType,
84            'id' => $taxid,
85        ];
86    }
87
88    function _sii_contraparte_identification_xml(array $recipient){
89        if (_sii_recipient_uses_nif($recipient)){
90            return '<sii:NIF>'.htmlspecialchars((string) $recipient['taxId']).'</sii:NIF>'.PHP_EOL;
91        }
92
93        $idOtro = _sii_recipient_idotro($recipient);
94        if (!is_array($idOtro)){
95            return '';
96        }
97
98        $xml = '<sii:IDOtro>'.PHP_EOL;
99        if (!empty($idOtro['codigoPais'])){
100            $xml .= '<sii:CodigoPais>'.htmlspecialchars((string) $idOtro['codigoPais']).'</sii:CodigoPais>'.PHP_EOL;
101        }
102        $xml .= '<sii:IDType>'.htmlspecialchars((string) $idOtro['idType']).'</sii:IDType>'.PHP_EOL;
103        $xml .= '<sii:ID>'.htmlspecialchars((string) $idOtro['id']).'</sii:ID>'.PHP_EOL;
104        $xml .= '</sii:IDOtro>'.PHP_EOL;
105
106        return $xml;
107    }
108
109    function _sii_contraparte_xml(?array $client){
110        $recipient = _sii_recipient_from_client($client);
111        if ($recipient === null){
112            return '';
113        }
114
115        $xml = '<sii:Contraparte>'.PHP_EOL;
116        $xml .= '<sii:NombreRazon>'.htmlspecialchars((string) $recipient['name']).'</sii:NombreRazon>'.PHP_EOL;
117        $xml .= _sii_contraparte_identification_xml($recipient);
118        $xml .= '</sii:Contraparte>'.PHP_EOL;
119
120        return $xml;
121    }
122
123    /**
124     * @param array<int, array{series?: string, invoice?: string, created?: string}> $substituted
125     * @return array{year: string, month: string}
126     */
127    function _sii_recap_period_from_substituted(array $substituted){
128        $maxTs = 0;
129        foreach ($substituted as $entry){
130            $ts = strtotime((string) ($entry['created'] ?? ''));
131            if ($ts && ($ts > $maxTs)){
132                $maxTs = $ts;
133            }
134        }
135
136        if ($maxTs === 0){
137            return [
138                'year' => date('Y'),
139                'month' => date('m'),
140            ];
141        }
142
143        return [
144            'year' => date('Y', $maxTs),
145            'month' => date('m', $maxTs),
146        ];
147    }
148
149    /**
150     * Bloque FacturasAgrupadas (SII v1.1) para F3 recapitulativa.
151     *
152     * @param array<int, array{series?: string, invoice?: string, created?: string}> $substituted
153     */
154    function _sii_substituted_invoices_xml(array $substituted){
155        if (count($substituted) === 0){
156            return '';
157        }
158
159        $xml = '<sii:FacturasAgrupadas>'.PHP_EOL;
160        foreach ($substituted as $entry){
161            $series = trim((string) ($entry['series'] ?? ''));
162            $invoice = trim((string) ($entry['invoice'] ?? ''));
163            if (($series === '') || ($invoice === '')){
164                continue;
165            }
166
167            $ivce = $series.$invoice;
168            $created = (string) ($entry['created'] ?? '');
169            $ts = strtotime($created);
170            $date = ($ts) ? date('d-m-Y', $ts) : $created;
171
172            $xml .= '<sii:IDFacturaAgrupada>'.PHP_EOL;
173            $xml .= '<sii:NumSerieFacturaEmisor>'.htmlspecialchars($ivce).'</sii:NumSerieFacturaEmisor>'.PHP_EOL;
174            $xml .= '<sii:FechaExpedicionFacturaEmisor>'.$date.'</sii:FechaExpedicionFacturaEmisor>'.PHP_EOL;
175            $xml .= '</sii:IDFacturaAgrupada>'.PHP_EOL;
176        }
177        $xml .= '</sii:FacturasAgrupadas>'.PHP_EOL;
178
179        return $xml;
180    }
181
182    /*
183        Representación (certificado TLS): el envío SOAP usa el PKCS#12 corporativo (mismo que VeriFactu),
184        no el PEM del local. En el XML, Cabecera/Titular e IDEmisorFactura siguen siendo el obligado
185        tributario (negocio / NIF del PLACE): son los datos del suministro, no del certificado cliente.
186    */
187
188    // https://www.agenciatributaria.es/static_files/AEAT/Contenidos_Comunes/La_Agencia_Tributaria/Modelos_y_formularios/Suministro_inmediato_informacion/FicherosSuministros/V_1_1/SII_Descripcion_ServicioWeb_v1.1.pdf
189    /*
190        Tipos de comunicación:
191            A0 - Alta de facturas/registro
192            A1 - Modificación de facturas/registros (errores registrales)
193            A4 - Modificación Factura Régimen de Viajeros
194            A5 - Alta de las devoluciones del IVA de viajeros
195            A6 - Modificacion de las devoluciones del IVA de viajeros
196
197        Tipo de factura emitida:
198            F1 - Factura (art. 6, 7.2 y 7.3 del RD 1619/2012)
199            F2 - Factura Simplificada y y Facturas sin identificación del destinatario art. 6.1.d) RD 1619/2012
200            F3 - Factura emitida en sustitución de facturas simplificadas facturadas y declaradas
201            F4 - Asiento resumen de facturas
202            R1 - Factura Rectificativa (Error fundado en derecho y Art. 80 Uno Dos y Seis LIVA)
203            R2 - Factura Rectificativa (Art. 80.3)
204            R3 - Factura Rectificativa (Art. 80.4)
205            R4 - Factura Rectificativa (Resto)
206            R5 - Factura Rectificativa en facturas simplificadas
207
208        Tipo de Factura Rectificativa
209            S - Por sustitución
210            I - Por diferencias
211
212        Clave de régimen especial o trascendencia en facturas emitidas:
213            01 - Operación de régimen general.
214            02 - Exportación.
215            03 - Operaciones a las que se aplique el régimen especial de bienes usados, objetos de arte, antigüedades y objetos de colección.
216            04 - Régimen especial del oro de inversión.
217            05 - Régimen especial de las agencias de viajes.
218            06 - Régimen especial grupo de entidades en IVA (Nivel Avanzado)
219            07 - Régimen especial del criterio de caja.
220            08 - Operaciones sujetas al IPSI / IGIC (Impuesto sobre la Producción, los Servicios y la Importación / Impuesto General Indirecto Canario).
221            09 - Facturación de las prestaciones de servicios de agencias de viaje que actúan como mediadoras en nombre y por cuenta ajena (D.A.4ª RD1619/2012)
222            10 - Cobros por cuenta de terceros de honorarios profesionales o de derechos derivados de la propiedad industrial, de autor u otros por cuenta de sus socios, asociados o colegiados efectuados por sociedades, asociaciones, colegios profesionales u otras entidades que realicen estas funciones de cobro.
223            11 - Operaciones de arrendamiento de local de negocio sujetas a retención
224            12 - Operaciones de arrendamiento de local de negocio no sujetos a retención.
225            13 - Operaciones de arrendamiento de local de negocio sujetas y no sujetas a retención.
226            14 - Factura con IVA pendiente de devengo en certificaciones de obra cuyo destinatario sea una Administración Pública.
227            15 - Factura con IVA pendiente de devengo en operaciones de tracto sucesivo.
228            16 - Primer semestre 2017 y otras facturas anteriores a la inclusión en el SII
229
230        Calificación del tipo de operación Sujeta/ No Exenta
231            S1 - No exenta- Sin inversion sujeto pasivo
232            S2 - No exenta - Con Inversion sujeto pasivo
233            S3 - No exenta - Sin inversion sujeto pasivo y con Inversion sujeto pasivo
234    */
235
236    /**
237     * Enrich TICKETCHANGE-shaped rows for SII XML using TICKETDATA.registry JSON (same source as TicketBAI / VeriFactu).
238     *
239     * @param \ConxHelper $conx
240     * @param array<string, mixed> $lastRow TICKETCHANGE row (modified in place when TICKETDATA exists)
241     */
242    function sii_enrich_change_last_from_ticketdata(\ConxHelper $conx, array &$lastRow): void
243    {
244        if ($conx->tenant === null) {
245            return;
246        }
247
248        $ticketId = $lastRow['ticket'] ?? null;
249        if ($ticketId === null || $ticketId === '') {
250            return;
251        }
252
253        $results = [];
254        $res = $conx->tenant->query('TICKETDATA', ['ticket' => $ticketId], $results);
255        if (!$res || count($results) === 0) {
256            return;
257        }
258
259        $raw = $results[0]['registry'] ?? null;
260        if ($raw === null || $raw === '') {
261            return;
262        }
263
264        $registry = json_decode((string) $raw, true);
265        if (!is_array($registry)) {
266            addlog(__FILE__, LogLevel::Error, 'Invalid JSON in TICKETDATA for ticket ['.$ticketId.']');
267            return;
268        }
269
270        $series = isset($lastRow['series']) ? (string) $lastRow['series'] : '';
271        $invoice = isset($lastRow['invoice']) ? (string) $lastRow['invoice'] : '';
272
273        $changes = $registry['changes'] ?? null;
274        $entry = null;
275        if (is_array($changes)) {
276            for ($i = count($changes) - 1; $i >= 0; $i--) {
277                $c = $changes[$i];
278                if (!is_array($c)) {
279                    continue;
280                }
281                $cs = isset($c['series']) ? (string) $c['series'] : '';
282                $ci = isset($c['invoice']) ? (string) $c['invoice'] : '';
283                if ($cs === $series && $ci === $invoice) {
284                    $entry = $c;
285                    break;
286                }
287            }
288            if ($entry === null && count($changes) > 0 && is_array($changes[count($changes) - 1])) {
289                $entry = $changes[count($changes) - 1];
290            }
291        }
292
293        if (!is_array($entry)) {
294            return;
295        }
296
297        if (array_key_exists('total', $entry) && $entry['total'] !== null) {
298            $lastRow['total'] = $entry['total'];
299        }
300        if (array_key_exists('base', $entry) && $entry['base'] !== null) {
301            $lastRow['base'] = $entry['base'];
302        }
303        if (array_key_exists('taxes', $entry) && $entry['taxes'] !== null) {
304            $lastRow['taxes'] = $entry['taxes'];
305        }
306        if (array_key_exists('reason', $entry) && $entry['reason'] !== null && $entry['reason'] !== '') {
307            $lastRow['reason'] = $entry['reason'];
308        }
309        if (array_key_exists('date', $entry) && $entry['date'] !== null && $entry['date'] !== '') {
310            $lastRow['created'] = $entry['date'];
311        }
312
313        $totalsTaxes = $registry['commercial']['totals']['taxes'] ?? null;
314        if (is_array($totalsTaxes) && count($totalsTaxes) > 0) {
315            $tax0 = $totalsTaxes[0];
316            if (is_array($tax0) && isset($tax0['rate'])) {
317                $lastRow['rate'] = $tax0['rate'];
318            }
319        } elseif (isset($lastRow['base'], $lastRow['taxes']) && (float) $lastRow['base'] != 0.0) {
320            $b = (float) $lastRow['base'];
321            $t = (float) $lastRow['taxes'];
322            $lastRow['rate'] = round(100.0 * $t / $b, 2);
323        }
324
325        $recipient = $registry['recipient'] ?? $registry['customer'] ?? null;
326        if (is_array($recipient) && (($recipient['name'] ?? null) || ($recipient['taxId'] ?? null))) {
327            $lastRow['client'] = [
328                'name' => $recipient['name'] ?? '',
329                'taxid' => $recipient['taxId'] ?? '',
330                'country' => $recipient['country'] ?? '',
331            ];
332        }
333    }
334
335    function _build_sii_request_AltaFacturas($place, &$changes, &$siixsd, &$xml){
336        $retval = ApplicationError::Success;
337
338        if (success($retval)){
339            $merchant = $place['business'];
340            $taxid = $place['taxid'];
341            $comtype = 'A0';
342        }
343
344        if (success($retval)){
345            $xml = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
346            $xml .= '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'.PHP_EOL;
347            foreach($siixsd as $key => $xsd){
348                $xml .= ' xmlns:'.$key.'="'.$xsd.'"'.PHP_EOL;
349            }
350            $xml .= '>'.PHP_EOL;
351            $xml .= '<SOAP-ENV:Header/>'.PHP_EOL;
352            $xml .= '<SOAP-ENV:Body>'.PHP_EOL;
353            $xml .= '<siiLR:SuministroLRFacturasEmitidas>'.PHP_EOL;
354            $xml .= '<sii:Cabecera>'.PHP_EOL;
355
356            $xml .= '<sii:IDVersionSii>1.1</sii:IDVersionSii>'.PHP_EOL;
357            $xml .= '<sii:Titular>'.PHP_EOL;
358            $xml .= '<sii:NombreRazon>'.$merchant.'</sii:NombreRazon>'.PHP_EOL;
359            $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
360            $xml .= '</sii:Titular>'.PHP_EOL;
361            $xml .= '<sii:TipoComunicacion>'.$comtype.'</sii:TipoComunicacion>'.PHP_EOL;
362            $xml .= '</sii:Cabecera>'.PHP_EOL;
363
364            foreach($changes as &$change){
365                $__siiConx = $GLOBALS['__SII_ENRICH_CONX'] ?? null;
366                if ($__siiConx instanceof \ConxHelper && !empty($change['last']['ticket'])) {
367                    sii_enrich_change_last_from_ticketdata($__siiConx, $change['last']);
368                }
369
370                $ticketdata = [];
371
372                $ivce = $change['last']['series'].$change['last']['invoice'];
373                $date = strtotime($change['last']['created']);
374
375                $isRecap = ($change['last']['reason'] == 'A');
376                $substituted = $isRecap ? ($change['last']['substituted'] ?? []) : [];
377
378                if ($isRecap && count($substituted) === 0){
379                    addlog(__FILE__, LogLevel::Warning, "Alta Factura recapitulativa [".$ivce."]: sin tickets sustituidos válidos — omitida del XML");
380                    continue;
381                }
382    
383                $ticketdata['invoice'] = $ivce;
384                if ($isRecap){
385                    $periodData = $change['last']['substituted_period'] ?? _sii_recap_period_from_substituted($substituted);
386                    $ticketdata['year'] = $periodData['year'];
387                    $ticketdata['period'] = $periodData['month'];
388                    $ticketdata['type'] = 'F3';
389                }
390                else {
391                    $ticketdata['year'] = date('Y', $date);
392                    $ticketdata['period'] = date('m', $date);
393                    $ticketdata['type'] = ($change['last']['client']) ? 'F1': 'F2';
394                }
395                $ticketdata['special'] = '01';
396                $ticketdata['exent'] = 'S1';
397                $ticketdata['date'] = date('d-m-Y', $date);
398                $ticketdata['price'] = number_format((float)$change['last']['total'], 2, '.', '');
399                $ticketdata['tax_rate'] = $change['last']['rate'];
400                $ticketdata['base'] =  number_format((float)$change['last']['base'], 2, '.', ''); ;
401                $ticketdata['tax'] = number_format((float)$change['last']['taxes'], 2, '.', '');
402                $ticketdata['reason'] = $change['last']['reason'] == 'A' ? 'Factura recapitulativa' : 'Comanda restaurante';
403
404                $ticketdata['toname'] = $change['last']['client'] ? $change['last']['client']['name'] : null; 
405                $ticketdata['totaxid'] = $change['last']['client'] ? $change['last']['client']['taxid'] : null; 
406
407                addlog(__FILE__, LogLevel::Information, "Alta Factura: [".$ticketdata['invoice']."] (".$ticketdata['price']."€)");
408
409                $xml .= '<siiLR:RegistroLRFacturasEmitidas>'.PHP_EOL;
410                $xml .= '<sii:PeriodoLiquidacion>'.PHP_EOL;
411                $xml .= '<sii:Ejercicio>'.$ticketdata['year'].'</sii:Ejercicio>'.PHP_EOL;
412                $xml .= '<sii:Periodo>'.$ticketdata['period'].'</sii:Periodo>'.PHP_EOL;
413                $xml .= '</sii:PeriodoLiquidacion>'.PHP_EOL;
414    
415                $xml .= '<siiLR:IDFactura>'.PHP_EOL;
416                $xml .= '<sii:IDEmisorFactura>'.PHP_EOL;
417                $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
418                $xml .= '</sii:IDEmisorFactura>'.PHP_EOL;
419    
420                $xml .= '<sii:NumSerieFacturaEmisor>'.$ticketdata['invoice'].'</sii:NumSerieFacturaEmisor>'.PHP_EOL;
421                $xml .= '<sii:FechaExpedicionFacturaEmisor>'.$ticketdata['date'].'</sii:FechaExpedicionFacturaEmisor>'.PHP_EOL;
422                $xml .= '</siiLR:IDFactura>'.PHP_EOL;
423
424                $xml .= '<siiLR:FacturaExpedida>'.PHP_EOL;
425                $xml .= '<sii:TipoFactura>'.$ticketdata['type'].'</sii:TipoFactura>'.PHP_EOL;
426                if ($ticketdata['type'] === 'F3'){
427                    $xml .= _sii_substituted_invoices_xml($substituted);
428                }
429                $xml .= '<sii:ClaveRegimenEspecialOTrascendencia>'.$ticketdata['special'].'</sii:ClaveRegimenEspecialOTrascendencia>'.PHP_EOL;
430                $xml .= '<sii:ImporteTotal>'.$ticketdata['price'].'</sii:ImporteTotal>'.PHP_EOL;
431                $xml .= '<sii:DescripcionOperacion>'.$ticketdata['reason'].'</sii:DescripcionOperacion>'.PHP_EOL;
432
433                if ($change['last']['client']){
434                    $xml .= _sii_contraparte_xml($change['last']['client']);
435                }
436
437                $xml .= '<sii:TipoDesglose>'.PHP_EOL;
438                $xml .= '<sii:DesgloseFactura>'.PHP_EOL;
439                $xml .= '<sii:Sujeta>'.PHP_EOL;
440                $xml .= '<sii:NoExenta>'.PHP_EOL;
441                $xml .= '<sii:TipoNoExenta>'.$ticketdata['exent'].'</sii:TipoNoExenta>'.PHP_EOL;
442                $xml .= '<sii:DesgloseIVA>'.PHP_EOL;
443                $xml .= '<sii:DetalleIVA>'.PHP_EOL;
444                $xml .= '<sii:TipoImpositivo>'.$ticketdata['tax_rate'].'</sii:TipoImpositivo>'.PHP_EOL;
445                $xml .= '<sii:BaseImponible>'.$ticketdata['base'].'</sii:BaseImponible>'.PHP_EOL;
446                $xml .= '<sii:CuotaRepercutida>'.$ticketdata['tax'].'</sii:CuotaRepercutida>'.PHP_EOL;
447                $xml .= '</sii:DetalleIVA>'.PHP_EOL;
448                $xml .= '</sii:DesgloseIVA>'.PHP_EOL;
449                $xml .= '</sii:NoExenta>'.PHP_EOL;
450                $xml .= '</sii:Sujeta>'.PHP_EOL;
451                $xml .= '</sii:DesgloseFactura>'.PHP_EOL;
452                $xml .= '</sii:TipoDesglose>'.PHP_EOL;
453                $xml .= '</siiLR:FacturaExpedida>'.PHP_EOL;    
454
455                $xml .= '</siiLR:RegistroLRFacturasEmitidas>'.PHP_EOL;
456            } unset($change);
457
458            $xml .= '</siiLR:SuministroLRFacturasEmitidas>'.PHP_EOL;
459            $xml .= '</SOAP-ENV:Body>'.PHP_EOL;
460            $xml .= '</SOAP-ENV:Envelope>'.PHP_EOL;                
461        }
462
463        return $retval;
464    }
465
466    function _build_sii_request_RectificacionFacturas($place, &$changes, &$siixsd, &$xml){
467        $retval = ApplicationError::Success;
468
469        if (success($retval)){
470            $merchant = $place['business'];
471            $taxid = $place['taxid'];
472            $comtype = 'A0';
473        }
474
475        if (success($retval)){
476            $xml = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
477            $xml .= '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'.PHP_EOL;
478            foreach($siixsd as $key => $xsd){
479                $xml .= ' xmlns:'.$key.'="'.$xsd.'"'.PHP_EOL;
480            }
481            $xml .= '>'.PHP_EOL;
482            $xml .= '<SOAP-ENV:Header/>'.PHP_EOL;
483            $xml .= '<SOAP-ENV:Body>'.PHP_EOL;
484            $xml .= '<siiLR:SuministroLRFacturasEmitidas>'.PHP_EOL;
485            $xml .= '<sii:Cabecera>'.PHP_EOL;
486
487            $xml .= '<sii:IDVersionSii>1.1</sii:IDVersionSii>'.PHP_EOL;
488            $xml .= '<sii:Titular>'.PHP_EOL;
489            $xml .= '<sii:NombreRazon>'.$merchant.'</sii:NombreRazon>'.PHP_EOL;
490            $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
491            $xml .= '</sii:Titular>'.PHP_EOL;
492            $xml .= '<sii:TipoComunicacion>'.$comtype.'</sii:TipoComunicacion>'.PHP_EOL;
493            $xml .= '</sii:Cabecera>'.PHP_EOL;
494
495            foreach($changes as &$change){
496                $__siiConx = $GLOBALS['__SII_ENRICH_CONX'] ?? null;
497                if ($__siiConx instanceof \ConxHelper) {
498                    if (!empty($change['last']['ticket'])) {
499                        sii_enrich_change_last_from_ticketdata($__siiConx, $change['last']);
500                    }
501                    if (!empty($change['sent']['ticket'])) {
502                        sii_enrich_change_last_from_ticketdata($__siiConx, $change['sent']);
503                    }
504                }
505
506                $ticketdata = [];
507
508                $ivce = $change['last']['series'].$change['last']['invoice'];
509                $date = strtotime($change['last']['created']);
510
511                if (!$change['sent']){
512                    addlog(__FILE__, LogLevel::Warning, "ERROR: Rectificación Factura: [".$ivce."] Sin información de envío al SII");
513                    continue;
514                }
515
516                $ticketdata['invoice'] = $ivce;
517                $ticketdata['year'] = date('Y', $date);
518                $ticketdata['period'] = date('m', $date);
519
520                $ticketdata['special'] = '01';
521                $ticketdata['exent'] = 'S1';
522                $ticketdata['date'] = date('d-m-Y', $date);
523                $ticketdata['price'] = number_format((float)$change['last']['total'], 2, '.', '');
524                $ticketdata['rate'] = number_format((float)$change['last']['rate'], 2, '.', '');
525                $ticketdata['base'] = number_format((float)$change['last']['base'], 2, '.', '');
526                $ticketdata['tax'] = number_format((float)$change['last']['tax'], 2, '.', '');
527                $ticketdata['reason'] = 'Cambios comanda';
528
529                $ticketdata['toname'] = $change['last']['client'] ? $change['last']['client']['name'] : null; 
530                $ticketdata['totaxid'] = $change['last']['client'] ? $change['last']['client']['taxid'] : null; 
531
532                addlog(__FILE__, LogLevel::Information, "Rectificación Factura: [".$ticketdata['invoice']."] (".$ticketdata['rprice']."€ -> ".$ticketdata['price']."€)");
533
534                $xml .= '<siiLR:RegistroLRFacturasEmitidas>'.PHP_EOL;
535                $xml .= '<sii:PeriodoLiquidacion>'.PHP_EOL;
536                $xml .= '<sii:Ejercicio>'.$ticketdata['year'].'</sii:Ejercicio>'.PHP_EOL;
537                $xml .= '<sii:Periodo>'.$ticketdata['period'].'</sii:Periodo>'.PHP_EOL;
538                $xml .= '</sii:PeriodoLiquidacion>'.PHP_EOL;
539    
540                $xml .= '<siiLR:IDFactura>'.PHP_EOL;
541                $xml .= '<sii:IDEmisorFactura>'.PHP_EOL;
542                $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
543                $xml .= '</sii:IDEmisorFactura>'.PHP_EOL;
544    
545                $xml .= '<sii:NumSerieFacturaEmisor>'.$ticketdata['invoice'].'</sii:NumSerieFacturaEmisor>'.PHP_EOL;
546                $xml .= '<sii:FechaExpedicionFacturaEmisor>'.$ticketdata['date'].'</sii:FechaExpedicionFacturaEmisor>'.PHP_EOL;
547                $xml .= '</siiLR:IDFactura>'.PHP_EOL;
548
549                $xml .= '<siiLR:FacturaExpedida>'.PHP_EOL;
550
551                if ($change['last']['reason'] == 'I'){
552                    $ticketdata['type'] = 'F3';
553
554                    $xml .= '<sii:TipoFactura>'.$ticketdata['type'].'</sii:TipoFactura>'.PHP_EOL;
555                }
556                else {
557                    $ticketdata['type'] = ($change['last']['client']) ? 'R4': 'R5';
558
559                    $ticketdata['rtype'] = 'S';
560                    $ticketdata['rbase'] = number_format((float)$change['sent']['base'], 2, '.', '');
561                    $ticketdata['rtax'] = number_format((float)$change['sent']['tax'], 2, '.', '');
562    
563                    $xml .= '<sii:TipoFactura>'.$ticketdata['type'].'</sii:TipoFactura>'.PHP_EOL;
564                    $xml .= '<sii:TipoRectificativa>'.$ticketdata['rtype'].'</sii:TipoRectificativa>'.PHP_EOL;
565                    $xml .= '<sii:ImporteRectificacion>'.PHP_EOL;
566                    $xml .= '<sii:BaseRectificada>'.$ticketdata['rbase'].'</sii:BaseRectificada>'.PHP_EOL;
567                    $xml .= '<sii:CuotaRectificada>'.$ticketdata['rtax'].'</sii:CuotaRectificada>'.PHP_EOL;
568                    $xml .= '</sii:ImporteRectificacion>'.PHP_EOL;
569                }
570
571                $xml .= '<sii:ClaveRegimenEspecialOTrascendencia>'.$ticketdata['special'].'</sii:ClaveRegimenEspecialOTrascendencia>'.PHP_EOL;
572                $xml .= '<sii:ImporteTotal>'.$ticketdata['price'].'</sii:ImporteTotal>'.PHP_EOL;
573                $xml .= '<sii:DescripcionOperacion>'.$ticketdata['reason'].'</sii:DescripcionOperacion>'.PHP_EOL;
574
575                if ($change['last']['client']){
576                    $xml .= _sii_contraparte_xml($change['last']['client']);
577                }
578
579                $xml .= '<sii:TipoDesglose>'.PHP_EOL;
580                $xml .= '<sii:DesgloseFactura>'.PHP_EOL;
581                $xml .= '<sii:Sujeta>'.PHP_EOL;
582                $xml .= '<sii:NoExenta>'.PHP_EOL;
583                $xml .= '<sii:TipoNoExenta>'.$ticketdata['exent'].'</sii:TipoNoExenta>'.PHP_EOL;
584                $xml .= '<sii:DesgloseIVA>'.PHP_EOL;
585                $xml .= '<sii:DetalleIVA>'.PHP_EOL;
586                $xml .= '<sii:TipoImpositivo>'.$ticketdata['rate'].'</sii:TipoImpositivo>'.PHP_EOL;
587                $xml .= '<sii:BaseImponible>'.$ticketdata['base'].'</sii:BaseImponible>'.PHP_EOL;
588                $xml .= '<sii:CuotaRepercutida>'.$ticketdata['tax'].'</sii:CuotaRepercutida>'.PHP_EOL;
589                $xml .= '</sii:DetalleIVA>'.PHP_EOL;
590                $xml .= '</sii:DesgloseIVA>'.PHP_EOL;
591                $xml .= '</sii:NoExenta>'.PHP_EOL;
592                $xml .= '</sii:Sujeta>'.PHP_EOL;
593                $xml .= '</sii:DesgloseFactura>'.PHP_EOL;
594                $xml .= '</sii:TipoDesglose>'.PHP_EOL;
595                $xml .= '</siiLR:FacturaExpedida>'.PHP_EOL;    
596
597                $xml .= '</siiLR:RegistroLRFacturasEmitidas>'.PHP_EOL;    
598            } unset($change);
599
600            $xml .= '</siiLR:SuministroLRFacturasEmitidas>'.PHP_EOL;
601            $xml .= '</SOAP-ENV:Body>'.PHP_EOL;
602            $xml .= '</SOAP-ENV:Envelope>'.PHP_EOL;                
603        }
604
605        return $retval;
606    }
607
608    function _build_sii_request_BajaFacturas($place, &$changes, &$siixsd, &$xml){
609        $retval = ApplicationError::Success;
610
611        if (success($retval)){
612            $merchant = $place['business'];
613            $taxid = $place['taxid'];
614        }
615
616        if (success($retval)){
617            $xml = '<?xml version="1.0" encoding="UTF-8"?>'.PHP_EOL;
618            $xml .= '<SOAP-ENV:Envelope xmlns:SOAP-ENV="http://schemas.xmlsoap.org/soap/envelope/"'.PHP_EOL;
619            foreach($siixsd as $key => $xsd){
620                $xml .= ' xmlns:'.$key.'="'.$xsd.'"'.PHP_EOL;
621            }
622            $xml .= '>'.PHP_EOL;
623            $xml .= '<SOAP-ENV:Header/>'.PHP_EOL;
624            $xml .= '<SOAP-ENV:Body>'.PHP_EOL;
625            $xml .= '<siiLR:BajaLRFacturasEmitidas>'.PHP_EOL;
626            $xml .= '<sii:Cabecera>'.PHP_EOL;
627
628            $xml .= '<sii:IDVersionSii>1.1</sii:IDVersionSii>'.PHP_EOL;
629            $xml .= '<sii:Titular>'.PHP_EOL;
630            $xml .= '<sii:NombreRazon>'.$merchant.'</sii:NombreRazon>'.PHP_EOL;
631            $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
632            $xml .= '</sii:Titular>'.PHP_EOL;
633            $xml .= '</sii:Cabecera>'.PHP_EOL;
634
635            foreach($changes as &$change){
636                $__siiConx = $GLOBALS['__SII_ENRICH_CONX'] ?? null;
637                if ($__siiConx instanceof \ConxHelper) {
638                    if (!empty($change['last']['ticket'])) {
639                        sii_enrich_change_last_from_ticketdata($__siiConx, $change['last']);
640                    }
641                    if (!empty($change['sent']['ticket'])) {
642                        sii_enrich_change_last_from_ticketdata($__siiConx, $change['sent']);
643                    }
644                }
645
646                $ticketdata = [];
647
648                $ivce = $change['last']['series'].$change['last']['invoice'];
649                
650                if (!$change['sent']){
651                    addlog(__FILE__, LogLevel::Warning, "ERROR: Baja Factura: [".$ivce."] Sin información de envío al SII");
652                    continue;
653                }                
654
655                $ivce = $change['sent']['series'].$change['sent']['invoice'];
656                $date = strtotime($change['sent']['created']);
657                
658                $ticketdata['invoice'] = $ivce;
659                $ticketdata['year'] = date('Y', $date);
660                $ticketdata['period'] = date('m', $date);
661                $ticketdata['date'] = date('d-m-Y', $date);
662
663                addlog(__FILE__, LogLevel::Information, "Baja Factura: [".$ivce."]");
664
665                $xml .= '<siiLR:RegistroLRBajaExpedidas>'.PHP_EOL;
666                $xml .= '<sii:PeriodoLiquidacion>'.PHP_EOL;
667                $xml .= '<sii:Ejercicio>'.$ticketdata['year'].'</sii:Ejercicio>'.PHP_EOL;
668                $xml .= '<sii:Periodo>'.$ticketdata['period'].'</sii:Periodo>'.PHP_EOL;
669                $xml .= '</sii:PeriodoLiquidacion>'.PHP_EOL;
670    
671                $xml .= '<siiLR:IDFactura>'.PHP_EOL;
672                $xml .= '<sii:IDEmisorFactura>'.PHP_EOL;
673                $xml .= '<sii:NIF>'.$taxid.'</sii:NIF>'.PHP_EOL;
674                $xml .= '</sii:IDEmisorFactura>'.PHP_EOL;
675    
676                $xml .= '<sii:NumSerieFacturaEmisor>'.$ivce.'</sii:NumSerieFacturaEmisor>'.PHP_EOL;
677                $xml .= '<sii:FechaExpedicionFacturaEmisor>'.$ticketdata['date'].'</sii:FechaExpedicionFacturaEmisor>'.PHP_EOL;
678                $xml .= '</siiLR:IDFactura>'.PHP_EOL;
679
680                $xml .= '</siiLR:RegistroLRBajaExpedidas>'.PHP_EOL;
681            } unset($change);
682
683            $xml .= '</siiLR:BajaLRFacturasEmitidas>'.PHP_EOL;
684            $xml .= '</SOAP-ENV:Body>'.PHP_EOL;
685            $xml .= '</SOAP-ENV:Envelope>'.PHP_EOL;                
686        }
687
688        return $retval;
689    }
690?>