XmlInterface
This commit is contained in:
parent
14fd4063f5
commit
b94316dbed
|
|
@ -402,7 +402,7 @@ class Invoice extends BaseModel
|
|||
|
||||
public function verifactu_logs(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
{
|
||||
return $this->hasMany(VerifactuLog::class);
|
||||
return $this->hasMany(VerifactuLog::class)->orderBy('id', 'desc');
|
||||
}
|
||||
|
||||
public function tasks(): \Illuminate\Database\Eloquent\Relations\HasMany
|
||||
|
|
|
|||
|
|
@ -3,6 +3,7 @@
|
|||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice;
|
||||
|
||||
/**
|
||||
* @property int $id
|
||||
|
|
@ -15,6 +16,7 @@ use Illuminate\Database\Eloquent\Model;
|
|||
* @property string $previous_hash
|
||||
* @property string $status
|
||||
* @property object|null $response
|
||||
* @property string $state
|
||||
* @property \Carbon\Carbon $created_at
|
||||
* @property \Carbon\Carbon $updated_at
|
||||
* @property-read \App\Models\Company $company
|
||||
|
|
@ -38,4 +40,9 @@ class VerifactuLog extends Model
|
|||
{
|
||||
return $this->belongsTo(Invoice::class);
|
||||
}
|
||||
|
||||
public function deserialize()
|
||||
{
|
||||
return Invoice::unserialize($this->state);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Standards\Validation;
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -25,70 +25,16 @@ use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
|||
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||
use App\Services\EDocument\Standards\Verifactu\RegistroAlta;
|
||||
use App\Services\EDocument\Standards\Verifactu\RegistroModificacion;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
||||
use App\Models\VerifactuLog;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\InvoiceModification;
|
||||
|
||||
class Verifactu extends AbstractService
|
||||
{
|
||||
use Taxer;
|
||||
use NumberFormatter;
|
||||
|
||||
private Company $company;
|
||||
|
||||
private InvoiceSum | InvoiceSumInclusive $calc;
|
||||
|
||||
private VerifactuInvoice $v_invoice;
|
||||
|
||||
private array $tax_map = [];
|
||||
|
||||
private float $allowance_total = 0;
|
||||
|
||||
private array $errors = [];
|
||||
|
||||
private string $current_timestamp;
|
||||
|
||||
private array $impuesto_codes = [
|
||||
'01' => 'IVA (Impuesto sobre el Valor Añadido)', // Value Added Tax - Standard Spanish VAT
|
||||
'02' => 'IPSI (Impuesto sobre la Producción, los Servicios y la Importación)', // Production, Services and Import Tax - Ceuta and Melilla
|
||||
'03' => 'IGIC (Impuesto General Indirecto Canario)', // Canary Islands General Indirect Tax
|
||||
'05' => 'Otros (Others)' // Other taxes
|
||||
];
|
||||
|
||||
private array $clave_regimen_codes = [
|
||||
'01' => 'Régimen General', // General Regime - Standard VAT regime for most businesses
|
||||
'02' => 'Régimen Simplificado', // Simplified Regime - For small businesses with simplified accounting
|
||||
'03' => 'Régimen Especial de Agrupaciones de Módulos', // Special Module Grouping Regime - For agricultural activities
|
||||
'04' => 'Régimen Especial del Recargo de Equivalencia', // Special Equivalence Surcharge Regime - For retailers
|
||||
'05' => 'Régimen Especial de las Agencias de Viajes', // Special Travel Agencies Regime
|
||||
'06' => 'Régimen Especial de los Bienes Usados', // Special Used Goods Regime
|
||||
'07' => 'Régimen Especial de los Objetos de Arte', // Special Art Objects Regime
|
||||
'08' => 'Régimen Especial de las Antigüedades', // Special Antiques Regime
|
||||
'09' => 'Régimen Especial de los Objetos de Colección', // Special Collectibles Regime
|
||||
'10' => 'Régimen Especial de los Bienes de Inversión', // Special Investment Goods Regime
|
||||
'11' => 'Régimen Especial de los Servicios', // Special Services Regime
|
||||
'12' => 'Régimen Especial de los Bienes de Inversión y Servicios', // Special Investment Goods and Services Regime
|
||||
'13' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge)
|
||||
'14' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods)
|
||||
'15' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Services)
|
||||
'16' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services)
|
||||
'17' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge)
|
||||
'18' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods)
|
||||
'19' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Services)
|
||||
'20' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)' // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods and Services)
|
||||
];
|
||||
|
||||
private array $calificacion_operacion_codes = [
|
||||
'S1' => 'OPERACIÓN SUJETA Y NO EXENTA - SIN INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - Without Reverse Charge
|
||||
'S2' => 'OPERACIÓN SUJETA Y NO EXENTA - CON INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - With Reverse Charge
|
||||
'N1' => 'OPERACIÓN NO SUJETA ARTÍCULO 7, 14, OTROS', // Non-Subject Operation Article 7, 14, Others
|
||||
'N2' => 'OPERACIÓN NO SUJETA POR REGLAS DE LOCALIZACIÓN' // Non-Subject Operation by Location Rules
|
||||
];
|
||||
|
||||
public function __construct(public Invoice $invoice)
|
||||
{
|
||||
$this->company = $invoice->company;
|
||||
$this->calc = $this->invoice->calc();
|
||||
$this->v_invoice = new VerifactuInvoice();
|
||||
{
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -99,129 +45,20 @@ class Verifactu extends AbstractService
|
|||
public function run(): self
|
||||
{
|
||||
|
||||
$this->current_timestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
|
||||
$v_logs = $this->invoice->verifactu_logs;
|
||||
|
||||
$this->v_invoice
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura($this->invoice->number) //invoice number
|
||||
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
||||
->setTipoFactura('F1') //invoice type
|
||||
->setDescripcionOperacion('')// Not manadatory - max chars 500
|
||||
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
||||
->setImporteTotal($this->invoice->amount) //total invoice amount
|
||||
->setFechaHoraHusoGenRegistro($this->current_timestamp) //creation/submission timestamp
|
||||
->setTipoHuella('01') //sha256
|
||||
->setHuella('PLACEHOLDER_HUELLA');
|
||||
//determine the current status of the invoice.
|
||||
$document = new RegistroAlta($this->invoice);
|
||||
|
||||
/** The business entity that is issuing the invoice */
|
||||
$emisor = new PersonaFisicaJuridica();
|
||||
$emisor->setNif($this->company->settings->vat_number)
|
||||
->setNombreRazon($this->invoice->company->present()->name());
|
||||
|
||||
$this->v_invoice->setTercero($emisor);
|
||||
|
||||
/** The business entity (Client) that is receiving the invoice */
|
||||
$destinatarios = [];
|
||||
$destinatario = new PersonaFisicaJuridica();
|
||||
|
||||
$destinatario
|
||||
->setNif($this->invoice->client->vat_number)
|
||||
->setNombreRazon($this->invoice->client->present()->name());
|
||||
|
||||
$destinatarios[] = $destinatario;
|
||||
|
||||
$this->v_invoice->setDestinatarios($destinatarios);
|
||||
|
||||
// The tax breakdown
|
||||
$desglose = new Desglose();
|
||||
|
||||
//Combine the line taxes with invoice taxes here to get a total tax amount
|
||||
$taxes = $this->calc->getTaxMap();
|
||||
|
||||
$desglose_iva = [];
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
|
||||
$desglose_iva[] = [
|
||||
'Impuesto' => $this->calculateTaxType($tax['name']), //tax type
|
||||
'ClaveRegimen' => '01', //tax regime classification code
|
||||
'CalificacionOperacion' => 'S1', //operation classification code
|
||||
'BaseImponibleOimporteNoSujeto' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount
|
||||
'TipoImpositivo' => $tax['tax_rate'], // Tax Rate
|
||||
'CuotaRepercutida' => $tax['total'] // Tax Amount
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
$desglose->setDesgloseIVA($desglose_iva);
|
||||
|
||||
$this->v_invoice->setDesglose($desglose);
|
||||
|
||||
// Encadenamiento
|
||||
$encadenamiento = new Encadenamiento();
|
||||
|
||||
// Get the previous invoice log
|
||||
/** @var ?VerifactuLog $v_log */
|
||||
$v_log = $this->company->verifactu_logs()->first();
|
||||
|
||||
// We chain the previous hash to the current invoice to ensure consistency
|
||||
if($v_log){
|
||||
|
||||
$registro_anterior = new RegistroAnterior();
|
||||
$registro_anterior->setIDEmisorFactura($v_log->nif);
|
||||
$registro_anterior->setNumSerieFactura($v_log->invoice_number);
|
||||
$registro_anterior->setFechaExpedicionFactura($v_log->date->format('d-m-Y'));
|
||||
$registro_anterior->setHuella($v_log->hash);
|
||||
|
||||
$encadenamiento->setRegistroAnterior($registro_anterior);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
$encadenamiento->setPrimerRegistro('S');
|
||||
//1. new => RegistraAlta
|
||||
if($v_logs->count() >= 1){
|
||||
$v_log = $v_logs->first();
|
||||
|
||||
$document = InvoiceModification::fromInvoice($this->invoice, $v_log->deserialize());
|
||||
}
|
||||
|
||||
$this->v_invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
//Sending system information - We automatically generate the obligado emision from this later
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema
|
||||
->setNombreRazon('Invoice Ninja')
|
||||
->setNif(config('services.verifactu.sender_nif'))
|
||||
->setNombreSistemaInformatico('Invoice Ninja')
|
||||
->setIdSistemaInformatico('01')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('01')
|
||||
->setTipoUsoPosibleSoloVerifactu('S')
|
||||
->setTipoUsoPosibleMultiOT('S')
|
||||
->setIndicadorMultiplesOT('S');
|
||||
|
||||
$this->v_invoice->setSistemaInformatico($sistema);
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function calculateTaxType(string $tax_name): string
|
||||
{
|
||||
if(stripos($tax_name, 'iva') !== false) {
|
||||
return '01';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'igic') !== false) {
|
||||
return '03';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'ipsi') !== false) {
|
||||
return '02';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'otros') !== false) {
|
||||
return '05';
|
||||
}
|
||||
|
||||
return '01';
|
||||
//3. cancelled => RegistroAnulacion
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -1,12 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
||||
|
||||
use RobRichards\XMLSecLibs\XMLSecurityDSig;
|
||||
use RobRichards\XMLSecLibs\XMLSecurityKey;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
|
||||
class Invoice extends BaseXmlModel
|
||||
class Invoice extends BaseXmlModel implements XmlModelInterface
|
||||
{
|
||||
protected string $idVersion;
|
||||
protected string $idFactura;
|
||||
|
|
@ -1202,4 +1212,20 @@ class Invoice extends BaseXmlModel
|
|||
|
||||
return $modificationRecord;
|
||||
}
|
||||
|
||||
public function serialize()
|
||||
{
|
||||
return serialize($this);
|
||||
}
|
||||
|
||||
public static function unserialize(string $data): self
|
||||
{
|
||||
$object = unserialize($data);
|
||||
|
||||
if (!$object instanceof self) {
|
||||
throw new \InvalidArgumentException('Invalid serialized data - not an Invoice object');
|
||||
}
|
||||
|
||||
return $object;
|
||||
}
|
||||
}
|
||||
|
|
@ -8,7 +8,7 @@ namespace App\Services\EDocument\Standards\Verifactu\Models;
|
|||
* This class represents the complete modification structure required for Verifactu e-invoicing
|
||||
* modification operations. It contains both the cancellation record and the modification record.
|
||||
*/
|
||||
class InvoiceModification extends BaseXmlModel
|
||||
class InvoiceModification extends BaseXmlModel implements XmlModelInterface
|
||||
{
|
||||
protected RegistroAnulacion $registroAnulacion;
|
||||
protected RegistroModificacion $registroModificacion;
|
||||
|
|
|
|||
|
|
@ -0,0 +1,22 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
||||
|
||||
interface XmlModelInterface
|
||||
{
|
||||
public function toXmlString(): string;
|
||||
|
||||
public function toXml(\DOMDocument $doc): \DOMElement;
|
||||
|
||||
public function toSoapEnvelope(): string;
|
||||
}
|
||||
|
|
@ -0,0 +1,230 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace App\Services\EDocument\Standards\Verifactu;
|
||||
|
||||
use App\Models\Company;
|
||||
use App\Models\Invoice;
|
||||
use App\Models\Product;
|
||||
use App\Helpers\Invoice\Taxer;
|
||||
use App\DataMapper\Tax\BaseRule;
|
||||
use App\Services\AbstractService;
|
||||
use App\Helpers\Invoice\InvoiceSum;
|
||||
use App\Utils\Traits\NumberFormatter;
|
||||
use App\Helpers\Invoice\InvoiceSumInclusive;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Desglose;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Encadenamiento;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\RegistroAnterior;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\PersonaFisicaJuridica;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
||||
use App\Models\VerifactuLog;
|
||||
|
||||
class RegistroAlta
|
||||
{
|
||||
use Taxer;
|
||||
use NumberFormatter;
|
||||
|
||||
private Company $company;
|
||||
|
||||
private InvoiceSum | InvoiceSumInclusive $calc;
|
||||
|
||||
private VerifactuInvoice $v_invoice;
|
||||
|
||||
private ?VerifactuLog $v_log;
|
||||
|
||||
private array $tax_map = [];
|
||||
|
||||
private float $allowance_total = 0;
|
||||
|
||||
private array $errors = [];
|
||||
|
||||
private string $current_timestamp;
|
||||
|
||||
private array $impuesto_codes = [
|
||||
'01' => 'IVA (Impuesto sobre el Valor Añadido)', // Value Added Tax - Standard Spanish VAT
|
||||
'02' => 'IPSI (Impuesto sobre la Producción, los Servicios y la Importación)', // Production, Services and Import Tax - Ceuta and Melilla
|
||||
'03' => 'IGIC (Impuesto General Indirecto Canario)', // Canary Islands General Indirect Tax
|
||||
'05' => 'Otros (Others)' // Other taxes
|
||||
];
|
||||
|
||||
private array $clave_regimen_codes = [
|
||||
'01' => 'Régimen General', // General Regime - Standard VAT regime for most businesses
|
||||
'02' => 'Régimen Simplificado', // Simplified Regime - For small businesses with simplified accounting
|
||||
'03' => 'Régimen Especial de Agrupaciones de Módulos', // Special Module Grouping Regime - For agricultural activities
|
||||
'04' => 'Régimen Especial del Recargo de Equivalencia', // Special Equivalence Surcharge Regime - For retailers
|
||||
'05' => 'Régimen Especial de las Agencias de Viajes', // Special Travel Agencies Regime
|
||||
'06' => 'Régimen Especial de los Bienes Usados', // Special Used Goods Regime
|
||||
'07' => 'Régimen Especial de los Objetos de Arte', // Special Art Objects Regime
|
||||
'08' => 'Régimen Especial de las Antigüedades', // Special Antiques Regime
|
||||
'09' => 'Régimen Especial de los Objetos de Colección', // Special Collectibles Regime
|
||||
'10' => 'Régimen Especial de los Bienes de Inversión', // Special Investment Goods Regime
|
||||
'11' => 'Régimen Especial de los Servicios', // Special Services Regime
|
||||
'12' => 'Régimen Especial de los Bienes de Inversión y Servicios', // Special Investment Goods and Services Regime
|
||||
'13' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge)
|
||||
'14' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods)
|
||||
'15' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Services)
|
||||
'16' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services)
|
||||
'17' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge)
|
||||
'18' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods)
|
||||
'19' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Servicios)', // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Services)
|
||||
'20' => 'Régimen Especial de los Bienes de Inversión y Servicios (Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios - Inversión del Sujeto Pasivo - Bienes de Inversión y Servicios)' // Special Investment Goods and Services Regime (Reverse Charge - Investment Goods and Services - Reverse Charge - Investment Goods and Services)
|
||||
];
|
||||
|
||||
private array $calificacion_operacion_codes = [
|
||||
'S1' => 'OPERACIÓN SUJETA Y NO EXENTA - SIN INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - Without Reverse Charge
|
||||
'S2' => 'OPERACIÓN SUJETA Y NO EXENTA - CON INVERSIÓN DEL SUJETO PASIVO', // Subject and Non-Exempt Operation - With Reverse Charge
|
||||
'N1' => 'OPERACIÓN NO SUJETA ARTÍCULO 7, 14, OTROS', // Non-Subject Operation Article 7, 14, Others
|
||||
'N2' => 'OPERACIÓN NO SUJETA POR REGLAS DE LOCALIZACIÓN' // Non-Subject Operation by Location Rules
|
||||
];
|
||||
|
||||
public function __construct(public Invoice $invoice)
|
||||
{
|
||||
$this->company = $invoice->company;
|
||||
$this->calc = $this->invoice->calc();
|
||||
$this->v_invoice = new VerifactuInvoice();
|
||||
}
|
||||
|
||||
/**
|
||||
* Entry point for building document
|
||||
*
|
||||
* @return self
|
||||
*/
|
||||
public function run(): self
|
||||
{
|
||||
|
||||
// Get the previous invoice log
|
||||
/** @var ?VerifactuLog $v_log */
|
||||
$this->v_log = $this->company->verifactu_logs()->first();
|
||||
|
||||
$this->current_timestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
|
||||
|
||||
$this->v_invoice
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura($this->invoice->number) //invoice number
|
||||
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
||||
->setTipoFactura('F1') //invoice type
|
||||
->setDescripcionOperacion('')// Not manadatory - max chars 500
|
||||
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
||||
->setImporteTotal($this->invoice->amount) //total invoice amount
|
||||
->setFechaHoraHusoGenRegistro($this->current_timestamp) //creation/submission timestamp
|
||||
->setTipoHuella('01') //sha256
|
||||
->setHuella('PLACEHOLDER_HUELLA');
|
||||
|
||||
/** The business entity that is issuing the invoice */
|
||||
$emisor = new PersonaFisicaJuridica();
|
||||
$emisor->setNif($this->company->settings->vat_number)
|
||||
->setNombreRazon($this->invoice->company->present()->name());
|
||||
|
||||
$this->v_invoice->setTercero($emisor);
|
||||
|
||||
/** The business entity (Client) that is receiving the invoice */
|
||||
$destinatarios = [];
|
||||
$destinatario = new PersonaFisicaJuridica();
|
||||
|
||||
$destinatario
|
||||
->setNif($this->invoice->client->vat_number)
|
||||
->setNombreRazon($this->invoice->client->present()->name());
|
||||
|
||||
$destinatarios[] = $destinatario;
|
||||
|
||||
$this->v_invoice->setDestinatarios($destinatarios);
|
||||
|
||||
// The tax breakdown
|
||||
$desglose = new Desglose();
|
||||
|
||||
//Combine the line taxes with invoice taxes here to get a total tax amount
|
||||
$taxes = $this->calc->getTaxMap();
|
||||
|
||||
$desglose_iva = [];
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
|
||||
$desglose_iva[] = [
|
||||
'Impuesto' => $this->calculateTaxType($tax['name']), //tax type
|
||||
'ClaveRegimen' => '01', //tax regime classification code
|
||||
'CalificacionOperacion' => 'S1', //operation classification code
|
||||
'BaseImponibleOimporteNoSujeto' => $tax['base_amount'] ?? $this->calc->getNetSubtotal(), // taxable base amount
|
||||
'TipoImpositivo' => $tax['tax_rate'], // Tax Rate
|
||||
'CuotaRepercutida' => $tax['total'] // Tax Amount
|
||||
];
|
||||
|
||||
};
|
||||
|
||||
$desglose->setDesgloseIVA($desglose_iva);
|
||||
|
||||
$this->v_invoice->setDesglose($desglose);
|
||||
|
||||
// Encadenamiento
|
||||
$encadenamiento = new Encadenamiento();
|
||||
|
||||
// We chain the previous hash to the current invoice to ensure consistency
|
||||
if($this->v_log){
|
||||
|
||||
$registro_anterior = new RegistroAnterior();
|
||||
$registro_anterior->setIDEmisorFactura($v_log->nif);
|
||||
$registro_anterior->setNumSerieFactura($v_log->invoice_number);
|
||||
$registro_anterior->setFechaExpedicionFactura($v_log->date->format('d-m-Y'));
|
||||
$registro_anterior->setHuella($v_log->hash);
|
||||
|
||||
$encadenamiento->setRegistroAnterior($registro_anterior);
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
$encadenamiento->setPrimerRegistro('S');
|
||||
|
||||
}
|
||||
|
||||
$this->v_invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
//Sending system information - We automatically generate the obligado emision from this later
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema
|
||||
->setNombreRazon('Invoice Ninja')
|
||||
->setNif(config('services.verifactu.sender_nif'))
|
||||
->setNombreSistemaInformatico('Invoice Ninja')
|
||||
->setIdSistemaInformatico('01')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('01')
|
||||
->setTipoUsoPosibleSoloVerifactu('S')
|
||||
->setTipoUsoPosibleMultiOT('S')
|
||||
->setIndicadorMultiplesOT('S');
|
||||
|
||||
$this->v_invoice->setSistemaInformatico($sistema);
|
||||
|
||||
|
||||
return $this;
|
||||
}
|
||||
|
||||
private function calculateTaxType(string $tax_name): string
|
||||
{
|
||||
if(stripos($tax_name, 'iva') !== false) {
|
||||
return '01';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'igic') !== false) {
|
||||
return '03';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'ipsi') !== false) {
|
||||
return '02';
|
||||
}
|
||||
|
||||
if(stripos($tax_name, 'otros') !== false) {
|
||||
return '05';
|
||||
}
|
||||
|
||||
return '01';
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -23,7 +23,8 @@ return new class extends Migration
|
|||
$table->string('previous_hash')->nullable();
|
||||
$table->string('status');
|
||||
|
||||
$table->json('response')->nullable();;
|
||||
$table->json('response')->nullable();
|
||||
$table->text('state')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->foreign('company_id')->references('id')->on('companies')->onDelete('cascade')->onUpdate('cascade');
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ class VerifactuFeatureTest extends TestCase
|
|||
private $client;
|
||||
private $faker;
|
||||
|
||||
private string $test_company_nif = 'A39200019';
|
||||
private string $test_client_nif = 'A39200019';
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
|
|
@ -61,6 +64,7 @@ class VerifactuFeatureTest extends TestCase
|
|||
$settings->postal_code = '28001';
|
||||
$settings->vat_number = 'B12345678'; // Spanish VAT number format
|
||||
$settings->payment_terms = '10';
|
||||
$settings->vat_number = $this->test_company_nif;
|
||||
}
|
||||
|
||||
$this->company = Company::factory()->create([
|
||||
|
|
@ -102,7 +106,7 @@ class VerifactuFeatureTest extends TestCase
|
|||
'state' => 'Madrid',
|
||||
'postal_code' => '28001',
|
||||
'country_id' => 724,
|
||||
'vat_number' => 'B12545678',
|
||||
'vat_number' => $this->test_client_nif,
|
||||
'balance' => 0,
|
||||
'paid_to_date' => 0,
|
||||
'settings' => $client_settings,
|
||||
|
|
@ -146,7 +150,6 @@ class VerifactuFeatureTest extends TestCase
|
|||
'line_items' => $line_items,
|
||||
]);
|
||||
|
||||
|
||||
$invoice = $invoice->calc()
|
||||
->getInvoice()
|
||||
->service()
|
||||
|
|
|
|||
|
|
@ -1,5 +1,15 @@
|
|||
<?php
|
||||
|
||||
/**
|
||||
* Invoice Ninja (https://invoiceninja.com).
|
||||
*
|
||||
* @link https://github.com/invoiceninja/invoiceninja source repository
|
||||
*
|
||||
* @copyright Copyright (c) 2025. Invoice Ninja LLC (https://invoiceninja.com)
|
||||
*
|
||||
* @license https://www.elastic.co/licensing/elastic-license
|
||||
*/
|
||||
|
||||
namespace Tests\Feature\EInvoice\Verifactu;
|
||||
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Cupon;
|
||||
|
|
|
|||
|
|
@ -301,20 +301,19 @@ class VerifactuModificationTest extends TestCase
|
|||
$this->assertStringContainsString('42', $soapXml);
|
||||
$this->assertStringContainsString('242', $soapXml);
|
||||
|
||||
$validXml = $modification->toSoapEnvelope();
|
||||
|
||||
$validXml = $modification->toSoapEnvelope();
|
||||
// Use the new VerifactuDocumentValidator
|
||||
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
|
||||
$validator->validate();
|
||||
$errors = $validator->getVerifactuErrors();
|
||||
|
||||
// Use the new VerifactuDocumentValidator
|
||||
$validator = new \App\Services\EDocument\Standards\Validation\VerifactuDocumentValidator($validXml);
|
||||
$validator->validate();
|
||||
$errors = $validator->getVerifactuErrors();
|
||||
if (!empty($errors)) {
|
||||
|
||||
if (!empty($errors)) {
|
||||
|
||||
nlog('Verifactu Validation Errors:');
|
||||
nlog($validXml);
|
||||
nlog($errors);
|
||||
}
|
||||
nlog('Verifactu Validation Errors:');
|
||||
nlog($validXml);
|
||||
nlog($errors);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in New Issue