* * This program is free software; you can redistribute it and/or modify * it under the terms of the GNU General Public License as published by * the Free Software Foundation; either version 3 of the License, or * (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU General Public License for more details. * * You should have received a copy of the GNU General Public License * along with this program. If not, see . */ /** * \file cfdixml/src/CFDI40Generator.php * \ingroup cfdixml * \brief Clase principal para la generación de CFDI versión 4.0 */ require_once __DIR__ . '/Certificado.php'; require_once __DIR__ . '/CadenaOriginalBuilder.php'; class CFDI40Generator { private $xml; private $root; private $certificado; private $emisor; private $receptor; private $conceptos = []; private $impuestos = []; private $complementos = []; private $cadenaOriginalBuilder; // Nueva propiedad public function __construct() { $this->xml = new \DOMDocument('1.0', 'UTF-8'); $this->xml->formatOutput = true; $this->certificado = new Certificado(); // Si no se proporciona xsltPath, usar la URL del SAT por defecto $xsltPath = $xsltPath ?? 'http://www.sat.gob.mx/sitio_internet/cfd/4/cadenaoriginal_4_0/cadenaoriginal_4_0.xslt'; $this->cadenaOriginalBuilder = new CadenaOriginalBuilder($xsltPath); } /** * Creates the root node of the CFDI (Comprobante Fiscal Digital por Internet). * * @param array $datos An associative array containing the necessary data for the CFDI. * @return CFDIGenerator Returns the instance of the CFDIGenerator class for method chaining. * * @throws Exception If any required attribute is missing in the $datos array. */ public function createComprobante($datos) { // Crear el nodo raíz del CFDI $this->root = $this->xml->createElement('cfdi:Comprobante'); $this->xml->appendChild($this->root); // Agregar namespaces $this->root->setAttribute('xmlns:cfdi', 'http://www.sat.gob.mx/cfd/4'); $this->root->setAttribute('xmlns:xsi', 'http://www.w3.org/2001/XMLSchema-instance'); $this->root->setAttribute('xsi:schemaLocation', 'http://www.sat.gob.mx/cfd/4 http://www.sat.gob.mx/sitio_internet/cfd/4/cfdv40.xsd'); // Atributos obligatorios del CFDI 4.0 $this->root->setAttribute('Version', '4.0'); $this->root->setAttribute('Fecha', date('Y-m-d\TH:i:s')); $this->root->setAttribute('Moneda', $datos['Moneda'] ?? 'MXN'); $this->root->setAttribute('TipoDeComprobante', $datos['TipoDeComprobante']); $this->root->setAttribute('Exportacion', $datos['Exportacion'] ?? '01'); $this->root->setAttribute('LugarExpedicion', $datos['LugarExpedicion']); // Atributos opcionales if (isset($datos['Serie'])) $this->root->setAttribute('Serie', $datos['Serie']); if (isset($datos['Folio'])) $this->root->setAttribute('Folio', $datos['Folio']); if (isset($datos['FormaPago'])) $this->root->setAttribute('FormaPago', $datos['FormaPago']); if (isset($datos['MetodoPago'])) $this->root->setAttribute('MetodoPago', $datos['MetodoPago']); return $this; } /** * Adds the Emisor (Issuer) node to the CFDI XML structure. * * This function creates and appends the 'cfdi:Emisor' element to the root of the XML document, * setting the required attributes for the issuer of the CFDI. * * @param array $datos An associative array containing the issuer's information with the following keys: * - 'Rfc': The RFC (Registro Federal de Contribuyentes) of the issuer. * - 'Nombre': The legal name of the issuer. * - 'RegimenFiscal': The tax regime code under which the issuer operates. * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function agregarEmisor($datos) { $emisor = $this->xml->createElement('cfdi:Emisor'); $this->root->appendChild($emisor); $emisor->setAttribute('Rfc', $datos['Rfc']); $emisor->setAttribute('Nombre', $datos['Nombre']); $emisor->setAttribute('RegimenFiscal', $datos['RegimenFiscal']); $this->emisor = $datos; return $this; } /** * Adds the Receptor (Receiver) node to the CFDI XML structure. * * This function creates and appends the 'cfdi:Receptor' element to the root of the XML document, * setting the required attributes for the receiver of the CFDI. * * @param array $datos An associative array containing the receiver's information with the following keys: * - 'Rfc': The RFC (Registro Federal de Contribuyentes) of the receiver. * - 'Nombre': The legal name of the receiver. * - 'DomicilioFiscalReceptor': The tax address of the receiver. * - 'RegimenFiscalReceptor': The tax regime code under which the receiver operates. * - 'UsoCFDI': The intended use of the CFDI. * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function agregarReceptor($datos) { $receptor = $this->xml->createElement('cfdi:Receptor'); $this->root->appendChild($receptor); $receptor->setAttribute('Rfc', $datos['Rfc']); $receptor->setAttribute('Nombre', $datos['Nombre']); $receptor->setAttribute('DomicilioFiscalReceptor', $datos['DomicilioFiscalReceptor']); $receptor->setAttribute('RegimenFiscalReceptor', $datos['RegimenFiscalReceptor']); $receptor->setAttribute('UsoCFDI', $datos['UsoCFDI']); $this->receptor = $datos; return $this; } /** * Adds a concept (item or service) to the CFDI. * * This method appends a new concept to the internal array of concepts. * Each concept represents an item or service being invoiced in the CFDI. * * @param array $concepto An associative array containing the details of the concept. * Expected keys include: * - ClaveProdServ: Product or service key * - Cantidad: Quantity * - ClaveUnidad: Unit key * - Descripcion: Description * - ValorUnitario: Unit value * - Importe: Total amount * - ObjetoImp: Tax object * - NoIdentificacion: (Optional) Identification number * - Impuestos: (Optional) Taxes associated with the concept * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function agregarConcepto($concepto) { $this->conceptos[] = $concepto; return $this; } /** * Adds all concepts to the CFDI XML structure. * * This method creates the 'cfdi:Conceptos' node in the XML document and populates it * with all the concepts that have been added to the CFDI. For each concept, it sets * the required attributes and optionally adds identification and tax information. * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function agregarConceptos() { $conceptosNode = $this->xml->createElement('cfdi:Conceptos'); $this->root->appendChild($conceptosNode); foreach ($this->conceptos as $datos) { $concepto = $this->xml->createElement('cfdi:Concepto'); $conceptosNode->appendChild($concepto); // Atributos obligatorios $concepto->setAttribute('ClaveProdServ', $datos['ClaveProdServ']); $concepto->setAttribute('Cantidad', $datos['Cantidad']); $concepto->setAttribute('ClaveUnidad', $datos['ClaveUnidad']); $concepto->setAttribute('Descripcion', $datos['Descripcion']); $concepto->setAttribute('ValorUnitario', $datos['ValorUnitario']); $concepto->setAttribute('Importe', $datos['Importe']); $concepto->setAttribute('ObjetoImp', $datos['ObjetoImp']); // Atributos opcionales if (isset($datos['NoIdentificacion'])) { $concepto->setAttribute('NoIdentificacion', $datos['NoIdentificacion']); } // Agregar impuestos del concepto si existen if (isset($datos['Impuestos'])) { $this->agregarImpuestosConcepto($concepto, $datos['Impuestos']); } } return $this; } /** * Adds tax information to a concept node in the CFDI XML structure. * * This method creates and appends tax-related nodes (Traslados and Retenciones) * to the given concept node. It handles both transferred taxes (like IVA and IEPS) * and withheld taxes. * * @param DOMElement $concepto The concept node to which tax information will be added. * @param array $impuestos An associative array containing tax information: * - 'Traslados': Array of transferred taxes (IVA, IEPS) * - 'Retenciones': Array of withheld taxes * * @return void This method doesn't return a value, it modifies the XML structure directly. */ private function agregarImpuestosConcepto($concepto, $impuestos) { $impuestosNode = $this->xml->createElement('cfdi:Impuestos'); $concepto->appendChild($impuestosNode); // Agregar traslados if (isset($impuestos['Traslados'])) { $impuestosNode = $this->xml->createElement('cfdi:Impuestos'); $concepto->appendChild($impuestosNode); // Agregar traslados (IEPS e IVA) if (isset($impuestos['Traslados'])) { $trasladosNode = $this->xml->createElement('cfdi:Traslados'); $impuestosNode->appendChild($trasladosNode); // Ordenamos los traslados: primero IEPS (003), luego IVA (002) usort($impuestos['Traslados'], function ($a, $b) { return strcmp($b['Impuesto'], $a['Impuesto']); }); foreach ($impuestos['Traslados'] as $traslado) { $trasladoNode = $this->xml->createElement('cfdi:Traslado'); $trasladosNode->appendChild($trasladoNode); $trasladoNode->setAttribute('Base', $traslado['Base']); $trasladoNode->setAttribute('Impuesto', $traslado['Impuesto']); $trasladoNode->setAttribute('TipoFactor', $traslado['TipoFactor']); if ($traslado['TipoFactor'] !== 'Exento') { $trasladoNode->setAttribute('TasaOCuota', $traslado['TasaOCuota']); $trasladoNode->setAttribute('Importe', $traslado['Importe']); } } } } // Agregar retenciones if (isset($impuestos['Retenciones'])) { $retencionesNode = $this->xml->createElement('cfdi:Retenciones'); $impuestosNode->appendChild($retencionesNode); foreach ($impuestos['Retenciones'] as $retencion) { $retencionNode = $this->xml->createElement('cfdi:Retencion'); $retencionesNode->appendChild($retencionNode); $retencionNode->setAttribute('Base', $retencion['Base']); $retencionNode->setAttribute('Impuesto', $retencion['Impuesto']); $retencionNode->setAttribute('TipoFactor', $retencion['TipoFactor']); $retencionNode->setAttribute('TasaOCuota', $retencion['TasaOCuota']); $retencionNode->setAttribute('Importe', $retencion['Importe']); } } } /** * Calculates taxes (IEPS and IVA) for a given product or service. * * This function computes the taxes based on the unit price, quantity, and tax rates. * It first calculates IEPS (if applicable) and then IVA on the total amount including IEPS. * * @param float $precioUnitario The unit price of the product or service. * @param float $cantidad The quantity of the product or service. * @param float $tasaIEPS The IEPS (Special Tax on Production and Services) rate. Defaults to 0. * @param float $tasaIVA The IVA (Value Added Tax) rate. Defaults to 0.16 (16%). * * @return array Returns an associative array containing the calculated taxes under the 'Traslados' key. * Each tax (IEPS and/or IVA) is represented as an array with 'Base', 'Impuesto', * 'TipoFactor', 'TasaOCuota', and 'Importe' keys. */ public function calcularImpuestos($precioUnitario, $cantidad, $tasaIEPS = 0, $tasaIVA = 0.16) { $importe = $precioUnitario * $cantidad; $impuestos = ['Traslados' => []]; // Si hay IEPS, calcularlo primero if ($tasaIEPS > 0) { $importeIEPS = $importe * $tasaIEPS; $impuestos['Traslados'][] = [ 'Base' => number_format($importe, 2, '.', ''), 'Impuesto' => '003', // Clave para IEPS 'TipoFactor' => 'Tasa', 'TasaOCuota' => number_format($tasaIEPS, 6, '.', ''), 'Importe' => number_format($importeIEPS, 2, '.', '') ]; // La base para el IVA incluye el IEPS $importe += $importeIEPS; } // Calcular IVA sobre el importe + IEPS if ($tasaIVA > 0) { $importeIVA = $importe * $tasaIVA; $impuestos['Traslados'][] = [ 'Base' => number_format($importe, 2, '.', ''), 'Impuesto' => '002', // Clave para IVA 'TipoFactor' => 'Tasa', 'TasaOCuota' => number_format($tasaIVA, 6, '.', ''), 'Importe' => number_format($importeIVA, 2, '.', '') ]; } return $impuestos; } /** * Adds a concept (item or service) to the CFDI with calculated taxes. * * This method calculates the taxes for a given concept based on its unit value and quantity, * then adds the concept with its calculated taxes to the CFDI. * * @param array $datos An associative array containing the concept details (e.g., 'ValorUnitario', 'Cantidad'). * @param float $tasaIEPS The IEPS (Special Tax on Production and Services) rate. Defaults to 0. * @param float $tasaIVA The IVA (Value Added Tax) rate. Defaults to 0.16 (16%). * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function agregarConceptoConImpuestos($datos, $tasaIEPS = 0, $tasaIVA = 0.16, $retenciones = []) { // Calcular importe base $importe = $datos['ValorUnitario'] * $datos['Cantidad']; // Calcular impuestos $impuestos = $this->calcularImpuestos( floatval($datos['ValorUnitario']), floatval($datos['Cantidad']), $tasaIEPS, $tasaIVA ); // Agregar retenciones si existen if (!empty($retenciones)) { $impuestos['Retenciones'] = array_map(function($retencion) use ($importe) { return [ 'Base' => $importe, 'Impuesto' => $retencion['Impuesto'], // '001' para ISR, '002' para IVA 'TipoFactor' => 'Tasa', 'TasaOCuota' => number_format($retencion['TasaOCuota'], 6, '.', ''), 'Importe' => number_format($importe * $retencion['TasaOCuota'], 2, '.', '') ]; }, $retenciones); } $datos['Impuestos'] = $impuestos; $this->agregarConcepto($datos); return $this; } /** * Calculates and sets the total amounts for the CFDI. * * This method iterates through all the concepts (items or services) in the CFDI, * calculating the subtotal, total transferred taxes, total withheld taxes, and the overall total. * It then sets these calculated values as attributes on the root element of the CFDI. * * @return CFDI40Generator Returns the current instance of the class for method chaining. */ public function calcularTotales() { $subtotal = 0; $total = 0; $totalImpuestosTrasladados = 0; $totalImpuestosRetenidos = 0; foreach ($this->conceptos as $concepto) { $subtotal += floatval($concepto['Importe']); if (isset($concepto['Impuestos'])) { if (isset($concepto['Impuestos']['Traslados'])) { foreach ($concepto['Impuestos']['Traslados'] as $traslado) { if ($traslado['TipoFactor'] !== 'Exento') { $totalImpuestosTrasladados += floatval($traslado['Importe']); } } } if (isset($concepto['Impuestos']['Retenciones'])) { foreach ($concepto['Impuestos']['Retenciones'] as $retencion) { $totalImpuestosRetenidos += floatval($retencion['Importe']); } } } } $total = $subtotal + $totalImpuestosTrasladados - $totalImpuestosRetenidos; $this->root->setAttribute('SubTotal', number_format($subtotal, 2, '.', '')); $this->root->setAttribute('Total', number_format($total, 2, '.', '')); return $this; } /** * Seals the CFDI (Comprobante Fiscal Digital por Internet) by generating and adding the required certificate information and digital signature. * * This method performs the following steps: * 1. Generates the original string (cadena original) for the CFDI. * 2. Processes the certificate to obtain its data. * 3. Generates the digital signature (sello) using the private key. * 4. Adds the certificate number, certificate, and digital signature to the CFDI as attributes. * * @param string $certificadoPath The file path to the digital certificate (.cer file). * @param string $llavePrivadaPath The file path to the private key (.key file). * @param string $password The password to decrypt the private key. * * @return CFDI40Generator Returns the current instance of the class for method chaining. * * @throws \Exception If there's an error processing the certificate or generating the digital signature. */ public function sellarCFDI($certificadoPath, $llavePrivadaPath, $password) { // Generar cadena original $cadenaOriginal = $this->generarCadenaOriginal(); // Obtener datos del certificado $certificadoData = $this->certificado->procesarCertificado($certificadoPath); // Generar sello $sello = $this->certificado->generarSello($cadenaOriginal, $llavePrivadaPath, $password); // Agregar atributos al comprobante $this->root->setAttribute('NoCertificado', $certificadoData['noCertificado']); $this->root->setAttribute('Certificado', $certificadoData['certificado']); $this->root->setAttribute('Sello', $sello); return $this; } /** * Genera la cadena original del CFDI usando el XSLT oficial del SAT * * @return string La cadena original del CFDI * @throws Exception Si hay error al generar la cadena original */ private function generarCadenaOriginal() { try { // Verificar que el XML esté completo if (!$this->root || !$this->xml) { throw new \Exception("El CFDI no está completamente generado"); } // Obtener el XML actual $xmlString = $this->xml->saveXML(); // Generar y retornar la cadena original $cadenaOriginal = $this->cadenaOriginalBuilder->buildCadenaOriginal($xmlString); // Verificar que la cadena original se generó correctamente if (empty($cadenaOriginal)) { throw new \Exception("Error al generar la cadena original: resultado vacío"); } return $cadenaOriginal; } catch (\Exception $e) { throw new \Exception("Error al generar la cadena original: " . $e->getMessage()); } } /** * Retrieves the XML representation of the CFDI. * * This method returns the complete XML document of the CFDI (Comprobante Fiscal Digital por Internet) * that has been generated and processed by this class. * * @return string The XML string representation of the CFDI. */ public function getXML() { return $this->xml->saveXML(); } }