Verifactu initial invoice creation
This commit is contained in:
parent
33078ee86c
commit
aa918f7ec0
|
|
@ -430,7 +430,7 @@ class Company extends BaseModel
|
|||
|
||||
public function verifactu_logs(): HasMany
|
||||
{
|
||||
return $this->hasMany(VerifactuLog::class);
|
||||
return $this->hasMany(VerifactuLog::class)->orderBy('id', 'DESC');
|
||||
}
|
||||
|
||||
public function task_schedulers(): HasMany
|
||||
|
|
|
|||
|
|
@ -11,15 +11,17 @@
|
|||
|
||||
namespace App\Services\EDocument\Standards;
|
||||
|
||||
use App\DataMapper\Tax\BaseRule;
|
||||
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\RegistroAnterior;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\SistemaInformatico;
|
||||
use App\Services\EDocument\Standards\Verifactu\Models\Invoice as VerifactuInvoice;
|
||||
|
||||
class Verifactu extends AbstractService
|
||||
|
|
@ -98,7 +100,7 @@ class Verifactu extends AbstractService
|
|||
$this->v_invoice
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura($this->invoice->number) //invoice number
|
||||
->setNombreRazonEmisor($this->company->present()->name())) //company name
|
||||
->setNombreRazonEmisor($this->company->present()->name()) //company name
|
||||
->setTipoFactura($this->calculateInvoiceType()) //invoice type
|
||||
->setDescripcionOperacion('')// Not manadatory - max chars 500
|
||||
->setCuotaTotal($this->invoice->total_taxes) //total taxes
|
||||
|
|
@ -130,14 +132,14 @@ class Verifactu extends AbstractService
|
|||
$desglose = new Desglose();
|
||||
|
||||
//Combine the line taxes with invoice taxes here to get a total tax amount
|
||||
$taxes = array_merge($calc->getTaxMap()->merge($calc->getTotalTaxMap())->toArray());
|
||||
$taxes = $calc->getTaxMap();
|
||||
|
||||
$desglose_iva = [];
|
||||
|
||||
foreach ($taxes as $tax) {
|
||||
|
||||
$desglose_iva[] = [
|
||||
'Impuesto' => '01' //tax type
|
||||
'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
|
||||
|
|
@ -148,9 +150,73 @@ class Verifactu extends AbstractService
|
|||
};
|
||||
|
||||
$desglose->setDesgloseIVA($desglose_iva);
|
||||
$invoice->setDesglose($desglose);
|
||||
|
||||
$this->v_invoice->setDesglose($desglose);
|
||||
|
||||
// Encadenamiento
|
||||
$encadenamiento = new Encadenamiento();
|
||||
|
||||
// Get the previous invoice 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');
|
||||
|
||||
}
|
||||
|
||||
$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';
|
||||
}
|
||||
|
||||
private function calculateInvoiceType(): string
|
||||
|
|
|
|||
|
|
@ -0,0 +1,157 @@
|
|||
<?php
|
||||
|
||||
namespace App\Services\EDocument\Standards\Verifactu\Models;
|
||||
|
||||
/**
|
||||
* RegistroAnterior - Previous Record Information
|
||||
*
|
||||
* This class represents the previous record information required for Verifactu e-invoicing
|
||||
* chain linking. It contains the details of the previous invoice in the chain.
|
||||
*/
|
||||
class RegistroAnterior extends BaseXmlModel
|
||||
{
|
||||
protected string $idEmisorFactura;
|
||||
protected string $numSerieFactura;
|
||||
protected string $fechaExpedicionFactura;
|
||||
protected string $huella;
|
||||
|
||||
public function toXml(\DOMDocument $doc): \DOMElement
|
||||
{
|
||||
$root = $this->createElement($doc, 'RegistroAnterior');
|
||||
|
||||
$root->appendChild($this->createElement($doc, 'IDEmisorFactura', $this->idEmisorFactura));
|
||||
$root->appendChild($this->createElement($doc, 'NumSerieFactura', $this->numSerieFactura));
|
||||
$root->appendChild($this->createElement($doc, 'FechaExpedicionFactura', $this->fechaExpedicionFactura));
|
||||
$root->appendChild($this->createElement($doc, 'Huella', $this->huella));
|
||||
|
||||
return $root;
|
||||
}
|
||||
|
||||
public static function fromDOMElement(\DOMElement $element): self
|
||||
{
|
||||
$registroAnterior = new self();
|
||||
|
||||
// Handle IDEmisorFactura
|
||||
$idEmisorFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'IDEmisorFactura')->item(0);
|
||||
if ($idEmisorFactura) {
|
||||
$registroAnterior->setIdEmisorFactura($idEmisorFactura->nodeValue);
|
||||
}
|
||||
|
||||
// Handle NumSerieFactura
|
||||
$numSerieFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'NumSerieFactura')->item(0);
|
||||
if ($numSerieFactura) {
|
||||
$registroAnterior->setNumSerieFactura($numSerieFactura->nodeValue);
|
||||
}
|
||||
|
||||
// Handle FechaExpedicionFactura
|
||||
$fechaExpedicionFactura = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'FechaExpedicionFactura')->item(0);
|
||||
if ($fechaExpedicionFactura) {
|
||||
$registroAnterior->setFechaExpedicionFactura($fechaExpedicionFactura->nodeValue);
|
||||
}
|
||||
|
||||
// Handle Huella
|
||||
$huella = $element->getElementsByTagNameNS(self::XML_NAMESPACE, 'Huella')->item(0);
|
||||
if ($huella) {
|
||||
$registroAnterior->setHuella($huella->nodeValue);
|
||||
}
|
||||
|
||||
return $registroAnterior;
|
||||
}
|
||||
|
||||
public static function fromXml($xml): self
|
||||
{
|
||||
if ($xml instanceof \DOMElement) {
|
||||
return static::fromDOMElement($xml);
|
||||
}
|
||||
|
||||
if (!is_string($xml)) {
|
||||
throw new \InvalidArgumentException('Input must be either a string or DOMElement');
|
||||
}
|
||||
|
||||
// Enable user error handling for XML parsing
|
||||
$previousErrorSetting = libxml_use_internal_errors(true);
|
||||
|
||||
try {
|
||||
$doc = new \DOMDocument();
|
||||
if (!$doc->loadXML($xml)) {
|
||||
$errors = libxml_get_errors();
|
||||
libxml_clear_errors();
|
||||
throw new \DOMException('Failed to load XML: ' . ($errors ? $errors[0]->message : 'Invalid XML format'));
|
||||
}
|
||||
return static::fromDOMElement($doc->documentElement);
|
||||
} finally {
|
||||
// Restore previous error handling setting
|
||||
libxml_use_internal_errors($previousErrorSetting);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the NIF of the invoice issuer from the previous record
|
||||
*/
|
||||
public function getIdEmisorFactura(): string
|
||||
{
|
||||
return $this->idEmisorFactura;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the NIF of the invoice issuer from the previous record
|
||||
*/
|
||||
public function setIdEmisorFactura(string $idEmisorFactura): self
|
||||
{
|
||||
$this->idEmisorFactura = $idEmisorFactura;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the invoice number from the previous record
|
||||
*/
|
||||
public function getNumSerieFactura(): string
|
||||
{
|
||||
return $this->numSerieFactura;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the invoice number from the previous record
|
||||
*/
|
||||
public function setNumSerieFactura(string $numSerieFactura): self
|
||||
{
|
||||
$this->numSerieFactura = $numSerieFactura;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the invoice issue date from the previous record
|
||||
*/
|
||||
public function getFechaExpedicionFactura(): string
|
||||
{
|
||||
return $this->fechaExpedicionFactura;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the invoice issue date from the previous record
|
||||
*
|
||||
* @param string $fechaExpedicionFactura Date in DD-MM-YYYY format
|
||||
*/
|
||||
public function setFechaExpedicionFactura(string $fechaExpedicionFactura): self
|
||||
{
|
||||
$this->fechaExpedicionFactura = $fechaExpedicionFactura;
|
||||
return $this;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the digital fingerprint/hash from the previous record
|
||||
*/
|
||||
public function getHuella(): string
|
||||
{
|
||||
return $this->huella;
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the digital fingerprint/hash from the previous record
|
||||
*/
|
||||
public function setHuella(string $huella): self
|
||||
{
|
||||
$this->huella = $huella;
|
||||
return $this;
|
||||
}
|
||||
}
|
||||
|
|
@ -38,6 +38,84 @@ class WSTest extends TestCase
|
|||
$this->assertTrue($success);
|
||||
}
|
||||
|
||||
|
||||
//@todo - need to confirm that building the xml and sending works.
|
||||
public function test_verifactu_invoice_model_can_build_xml()
|
||||
{
|
||||
|
||||
// Generate current timestamp in the correct format
|
||||
$currentTimestamp = now()->setTimezone('Europe/Madrid')->format('Y-m-d\TH:i:s');
|
||||
|
||||
nlog($currentTimestamp);
|
||||
|
||||
$invoice = new Invoice();
|
||||
$invoice
|
||||
->setIdVersion('1.0')
|
||||
->setIdFactura('FAC2023002')
|
||||
->setFechaExpedicionFactura('02-01-2025')
|
||||
->setRefExterna('REF-123')
|
||||
->setNombreRazonEmisor('Empresa Ejemplo SL')
|
||||
->setTipoFactura('F1')
|
||||
->setDescripcionOperacion('Venta de productos varios')
|
||||
->setCuotaTotal(210.00)
|
||||
->setImporteTotal(1000.00)
|
||||
->setFechaHoraHusoGenRegistro($currentTimestamp)
|
||||
->setTipoHuella('01')
|
||||
->setHuella('PLACEHOLDER_HUELLA');
|
||||
// Add emitter
|
||||
$emisor = new PersonaFisicaJuridica();
|
||||
$emisor
|
||||
->setNif('A39200019')
|
||||
->setRazonSocial('Empresa Ejemplo SL');
|
||||
$invoice->setTercero($emisor);
|
||||
|
||||
// Add breakdown
|
||||
$desglose = new Desglose();
|
||||
$desglose->setDesgloseFactura([
|
||||
'Impuesto' => '01',
|
||||
'ClaveRegimen' => '01',
|
||||
'CalificacionOperacion' => 'S1',
|
||||
'BaseImponibleOimporteNoSujeto' => 1000.00,
|
||||
'TipoImpositivo' => 21,
|
||||
'CuotaRepercutida' => 210.00
|
||||
]);
|
||||
$invoice->setDesglose($desglose);
|
||||
|
||||
|
||||
$destinatarios = [];
|
||||
$destinatario = new PersonaFisicaJuridica();
|
||||
|
||||
$destinatario
|
||||
->setNif('A39200020')
|
||||
->setNombreRazon('Empresa Ejemplo SL VV');
|
||||
|
||||
$destinatarios[] = $destinatario;
|
||||
|
||||
$invoice->setDestinatarios($destinatarios);
|
||||
|
||||
// Add information system
|
||||
$sistema = new SistemaInformatico();
|
||||
$sistema
|
||||
->setNombreRazon('Sistema de Facturación')
|
||||
->setNif('A39200019')
|
||||
->setNombreSistemaInformatico('SistemaFacturacion')
|
||||
->setIdSistemaInformatico('01')
|
||||
->setVersion('1.0')
|
||||
->setNumeroInstalacion('INST-001');
|
||||
$invoice->setSistemaInformatico($sistema);
|
||||
|
||||
// Add chain
|
||||
$encadenamiento = new Encadenamiento();
|
||||
$encadenamiento->setPrimerRegistro('S');
|
||||
$invoice->setEncadenamiento($encadenamiento);
|
||||
|
||||
$soapXml = $invoice->toSoapEnvelope();
|
||||
|
||||
$this->assertNotNull($soapXml);
|
||||
|
||||
nlog($soapXml);
|
||||
}
|
||||
|
||||
//@todo - need to confirm that building the xml and sending works.
|
||||
public function test_generated_invoice_xml_can_send_to_web_service()
|
||||
{
|
||||
|
|
@ -65,17 +143,6 @@ class WSTest extends TestCase
|
|||
->setTipoHuella('01')
|
||||
->setHuella('PLACEHOLDER_HUELLA');
|
||||
|
||||
|
||||
|
||||
// <sum:Cabecera>
|
||||
// <!-- ObligadoEmision: The computer system submitting on behalf of the invoice issuer -->
|
||||
// <sum1:ObligadoEmision>
|
||||
// <sum1:NombreRazon>CERTIFICADO FISICA PRUEBAS</sum1:NombreRazon>
|
||||
// <sum1:NIF>99999910G</sum1:NIF>
|
||||
// </sum1:ObligadoEmision>
|
||||
// </sum:Cabecera>
|
||||
|
||||
|
||||
// Add emitter
|
||||
$emisor = new PersonaFisicaJuridica();
|
||||
$emisor
|
||||
|
|
@ -83,6 +150,9 @@ class WSTest extends TestCase
|
|||
->setRazonSocial('Empresa Ejemplo SL');
|
||||
$invoice->setTercero($emisor);
|
||||
|
||||
|
||||
|
||||
|
||||
// Add breakdown
|
||||
$desglose = new Desglose();
|
||||
$desglose->setDesgloseFactura([
|
||||
|
|
|
|||
Loading…
Reference in New Issue