Emisión Errónea
- Nómina generada al empleado equivocado
- Período incorrecto
- Documento duplicado
- Empleado ya no trabaja en la empresa
La Nota de Ajuste de Eliminación se utiliza para anular completamente un documento de nómina electrónica previamente emitido. Una vez eliminado, el documento pierde validez legal.
Emisión Errónea
Cambios Administrativos
Situaciones Legales
Proceso Interno
NominaIndividualDeAjuste (igual que Reemplazo)2 (Eliminación)<?xml version="1.0" encoding="UTF-8"?><NominaIndividualDeAjuste xmlns="dian:gov:co:facturaelectronica:NominaIndividualDeAjuste" xmlns:xs="http://www.w3.org/2001/XMLSchema-instance" xmlns:ds="http://www.w3.org/2000/09/xmldsig#" xmlns:ext="urn:oasis:names:specification:ubl:schema:xsd:CommonExtensionComponents-2" xmlns:xades="http://uri.etsi.org/01903/v1.3.2#" xmlns:xades141="http://uri.etsi.org/01903/v1.4.1#" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" SchemaLocation="" xsi:schemaLocation="dian:gov:co:facturaelectronica:NominaIndividualDeAjuste NominaIndividualDeAjusteElectronicaXSD.xsd">
<ext:UBLExtensions/>
<!-- TipoNota: 2 = Eliminación --> <TipoNota>2</TipoNota>
<Eliminar> <!-- Referencia al documento a eliminar --> <EliminandoPredecesor NumeroPred="NOM35921" CUNEPred="a1b2c3d4e5f6g7h8i9j0k1l2m3n4o5p6q7r8s9t0u1v2w3x4y5z6a7b8c9d0e1f2" FechaGenPred="2024-11-20"/>
<!-- Nuevo consecutivo para la eliminación --> <NumeroSecuenciaXML CodigoTrabajador="001" Prefijo="DEL" Consecutivo="00001" Numero="DEL00001"/>
<LugarGeneracionXML Pais="CO" DepartamentoEstado="68" MunicipioCiudad="68001" Idioma="es"/>
<InformacionGeneral Version="V1.0: Documento Soporte de Pago de Nómina Electrónica" Ambiente="2" TipoXML="103" CUNE="" EncripCUNE="" FechaGen="2024-11-27" HoraGen="14:30:00-05:00" PeriodoNomina="5" TipoMoneda="COP" TRM="1"/>
<Notas>Anulación de nómina NOM35921 por emisión al empleado incorrecto. Autorizado por: Gerencia de RRHH. Motivo: Error administrativo.</Notas>
<!-- Datos del empleador (obligatorio) --> <Empleador RazonSocial="MI EMPRESA SAS" NIT="901143311" DV="8" Pais="CO" DepartamentoEstado="68" MunicipioCiudad="68001" Direccion="Calle 123 # 45-67"/> </Eliminar>
</NominaIndividualDeAjuste>| Aspecto | Reemplazo | Eliminación |
|---|---|---|
| Nodo contenedor | <Reemplazar> | <Eliminar> |
| Nodo predecesor | ReemplazandoPredecesor | EliminandoPredecesor |
| Contenido | TODO el documento corregido | Solo datos de referencia |
| Devengados/Deducciones | Incluidos | NO se incluyen |
| Trabajador | Incluido | NO se incluye |
| Pago | Incluido | NO se incluye |
| Efecto | Reemplaza documento | Anula documento |
Obligatorios:
ext:UBLExtensionsTipoNota (valor: 2)Eliminar (nodo contenedor)EliminandoPredecesorNumeroSecuenciaXMLLugarGeneracionXMLInformacionGeneralEmpleadorNotas (altamente recomendado)NO se incluyen:
PeriodoTrabajadorPagoFechasPagosDevengadosDeduccionesTotales
<?phpclass Nomina_Service_EliminacionGenerator{ private $db;
public function __construct() { $this->db = Zend_Db_Table::getDefaultAdapter(); }
/** * Genera XML de Nota de Ajuste - Eliminación * * @param int $nominaOriginalId ID de la nómina a eliminar * @param string $motivo Motivo de la eliminación * @param string $autorizadoPor Persona que autoriza * @return string XML de eliminación */ public function generarEliminacion( $nominaOriginalId, $motivo, $autorizadoPor ) { // 1. Obtener nómina original $nominaOriginal = $this->obtenerNomina($nominaOriginalId);
if (!$nominaOriginal) { throw new Exception("Nómina no encontrada"); }
// 2. Verificar estado autorizado if ($nominaOriginal['estado'] !== 'AUTORIZADO') { throw new Exception("Solo se pueden eliminar nóminas autorizadas"); }
// 3. Verificar CUNE if (empty($nominaOriginal['cune'])) { throw new Exception("La nómina no tiene CUNE"); }
// 4. Verificar que no está ya eliminada if ($nominaOriginal['eliminada']) { throw new Exception("Esta nómina ya fue eliminada"); }
// 5. Generar XML $xml = $this->generarXMLEliminacion( $nominaOriginal, $motivo, $autorizadoPor );
// 6. Validar $validator = new Nomina_Service_XMLValidator(); if (!$validator->validar($xml)) { throw new Exception( 'XML de eliminación inválido: ' . implode(', ', $validator->getErrores()) ); }
return $xml; }
private function generarXMLEliminacion($original, $motivo, $autorizado) { $dom = new DOMDocument('1.0', 'UTF-8'); $dom->formatOutput = true;
// Nodo raíz $root = $dom->createElementNS( 'dian:gov:co:facturaelectronica:NominaIndividualDeAjuste', 'NominaIndividualDeAjuste' );
// Namespaces... $dom->appendChild($root);
// UBLExtensions $ext = $dom->createElement('ext:UBLExtensions'); $root->appendChild($ext);
// TipoNota = 2 (Eliminación) $tipoNota = $dom->createElement('TipoNota', '2'); $root->appendChild($tipoNota);
// Nodo Eliminar $eliminar = $dom->createElement('Eliminar'); $root->appendChild($eliminar);
// EliminandoPredecesor $predecesor = $dom->createElement('EliminandoPredecesor'); $predecesor->setAttribute('NumeroPred', $original['numero']); $predecesor->setAttribute('CUNEPred', $original['cune']); $predecesor->setAttribute('FechaGenPred', $original['fecha_generacion']); $eliminar->appendChild($predecesor);
// NumeroSecuenciaXML (nuevo consecutivo) $nuevoConsecutivo = $this->obtenerSiguienteConsecutivo('DEL'); $numSecuencia = $dom->createElement('NumeroSecuenciaXML'); $numSecuencia->setAttribute('CodigoTrabajador', $original['codigo_trabajador']); $numSecuencia->setAttribute('Prefijo', 'DEL'); $numSecuencia->setAttribute('Consecutivo', $nuevoConsecutivo); $numSecuencia->setAttribute('Numero', 'DEL' . $nuevoConsecutivo); $eliminar->appendChild($numSecuencia);
// LugarGeneracionXML $lugar = $dom->createElement('LugarGeneracionXML'); $lugar->setAttribute('Pais', 'CO'); $lugar->setAttribute('DepartamentoEstado', $original['departamento']); $lugar->setAttribute('MunicipioCiudad', $original['municipio']); $lugar->setAttribute('Idioma', 'es'); $eliminar->appendChild($lugar);
// InformacionGeneral $infoGeneral = $dom->createElement('InformacionGeneral'); $infoGeneral->setAttribute('Version', 'V1.0: Documento Soporte de Pago de Nómina Electrónica'); $infoGeneral->setAttribute('Ambiente', '2'); $infoGeneral->setAttribute('TipoXML', '103'); $infoGeneral->setAttribute('CUNE', ''); $infoGeneral->setAttribute('EncripCUNE', ''); $infoGeneral->setAttribute('FechaGen', date('Y-m-d')); $infoGeneral->setAttribute('HoraGen', date('H:i:s-05:00')); $infoGeneral->setAttribute('PeriodoNomina', $original['periodo_nomina']); $infoGeneral->setAttribute('TipoMoneda', 'COP'); $infoGeneral->setAttribute('TRM', '1'); $eliminar->appendChild($infoGeneral);
// Notas (justificación) $notas = $dom->createElement( 'Notas', sprintf( 'Anulación de nómina %s. Motivo: %s. Autorizado por: %s. Fecha: %s', $original['numero'], $motivo, $autorizado, date('Y-m-d H:i:s') ) ); $eliminar->appendChild($notas);
// Empleador $empleador = $this->crearNodoEmpleador($dom, $original); $eliminar->appendChild($empleador);
return $dom->saveXML(); }
private function crearNodoEmpleador($dom, $datos) { $empleador = $dom->createElement('Empleador'); $empleador->setAttribute('RazonSocial', $datos['empleador_razon_social']); $empleador->setAttribute('NIT', $datos['empleador_nit']); $empleador->setAttribute('DV', $datos['empleador_dv']); $empleador->setAttribute('Pais', 'CO'); $empleador->setAttribute('DepartamentoEstado', $datos['empleador_departamento']); $empleador->setAttribute('MunicipioCiudad', $datos['empleador_municipio']); $empleador->setAttribute('Direccion', $datos['empleador_direccion']);
return $empleador; }
private function obtenerSiguienteConsecutivo($prefijo) { $ultimo = $this->db->fetchOne(" SELECT MAX(CAST(consecutivo AS UNSIGNED)) FROM nomina_electronica WHERE prefijo = ? ", [$prefijo]);
return str_pad((int)$ultimo + 1, 5, '0', STR_PAD_LEFT); }}<?phpfunction registrarEliminacionEnBD( $nominaOriginalId, $xmlEliminacion, $transactionID, $motivo, $autorizadoPor, $usuarioId) { $db = Zend_Db_Table::getDefaultAdapter();
try { $db->beginTransaction();
// 1. Obtener nómina original $original = $db->fetchRow( "SELECT * FROM nomina_electronica WHERE id = ?", [$nominaOriginalId] );
// 2. Insertar nota de eliminación $eliminacionId = $db->insert('nomina_electronica', [ 'empleado_id' => $original['empleado_id'], 'periodo_id' => $original['periodo_id'], 'prefijo' => 'DEL', 'consecutivo' => extraerConsecutivo($xmlEliminacion), 'numero' => extraerNumero($xmlEliminacion), 'tipo_documento' => 'ELIMINACION', 'nomina_original_id' => $nominaOriginalId, 'cune_original' => $original['cune'], 'estado' => 'ENVIADO', 'transaction_id' => $transactionID, 'xml_original' => gzcompress($xmlEliminacion), 'fecha_envio' => new Zend_Db_Expr('NOW()'), 'created_at' => new Zend_Db_Expr('NOW()') ]);
// 3. Marcar nómina original como ELIMINADA $db->update('nomina_electronica', [ 'estado' => 'ELIMINADA', 'eliminada' => 1, 'eliminacion_id' => $eliminacionId, 'fecha_eliminacion' => new Zend_Db_Expr('NOW()'), 'updated_at' => new Zend_Db_Expr('NOW()') ], "id = $nominaOriginalId");
// 4. Registrar en auditoría $db->insert('nomina_auditoria', [ 'nomina_id' => $nominaOriginalId, 'eliminacion_id' => $eliminacionId, 'accion' => 'ELIMINACION', 'motivo' => $motivo, 'autorizado_por' => $autorizadoPor, 'usuario_id' => $usuarioId, 'ip_address' => $_SERVER['REMOTE_ADDR'], 'user_agent' => $_SERVER['HTTP_USER_AGENT'], 'fecha' => new Zend_Db_Expr('NOW()'), 'observaciones' => "Eliminación de nómina {$original['numero']}" ]);
$db->commit();
// Log Zend_Log::warning("Nómina eliminada", [ 'original_id' => $nominaOriginalId, 'eliminacion_id' => $eliminacionId, 'motivo' => $motivo, 'autorizado_por' => $autorizadoPor, 'usuario_id' => $usuarioId ]);
return $eliminacionId;
} catch (Exception $e) { $db->rollBack(); throw $e; }}<?phpclass Nomina_ElectronicaController extends Zend_Controller_Action{ public function eliminarAction() { // Verificar permisos $auth = Zend_Auth::getInstance(); $usuario = $auth->getIdentity();
if (!$usuario->tienePermiso('nomina.eliminar')) { throw new Exception("No tiene permisos para eliminar nóminas"); }
// Obtener parámetros $nominaId = $this->getRequest()->getParam('id'); $motivo = $this->getRequest()->getParam('motivo'); $confirmacion = $this->getRequest()->getParam('confirmacion');
// Validar confirmación if ($confirmacion !== 'CONFIRMO_ELIMINACION') { throw new Exception("Debe confirmar la eliminación"); }
// Validar motivo if (empty($motivo) || strlen($motivo) < 20) { throw new Exception("Debe proporcionar un motivo detallado (mínimo 20 caracteres)"); }
try { // Generar XML $generator = new Nomina_Service_EliminacionGenerator(); $xml = $generator->generarEliminacion( $nominaId, $motivo, $usuario->nombre_completo );
// Enviar a Facturatech $xmlBase64 = base64_encode($xml); $soapClient = new Dataemunah_Facturatech_SoapClient(); $response = $soapClient->uploadDocument($xmlBase64);
if (!$response['success']) { throw new Exception("Error al enviar eliminación: " . $response['mensaje']); }
// Registrar en BD $eliminacionId = registrarEliminacionEnBD( $nominaId, $xml, $response['transaccionID'], $motivo, $usuario->nombre_completo, $usuario->id );
// Enviar notificación al empleado $this->enviarNotificacionEliminacion($nominaId, $motivo);
// Respuesta $this->_helper->json([ 'success' => true, 'message' => 'Nómina eliminada exitosamente', 'eliminacion_id' => $eliminacionId, 'transaction_id' => $response['transaccionID'] ]);
} catch (Exception $e) { $this->_helper->json([ 'success' => false, 'message' => 'Error al eliminar nómina: ' . $e->getMessage() ], 500); } }}CREATE TABLE nomina_auditoria ( id INT PRIMARY KEY AUTO_INCREMENT, nomina_id INT NOT NULL, eliminacion_id INT, accion ENUM('CREACION', 'MODIFICACION', 'ELIMINACION', 'DESCARGA') NOT NULL, motivo TEXT, autorizado_por VARCHAR(255), usuario_id INT, ip_address VARCHAR(45), user_agent TEXT, fecha TIMESTAMP DEFAULT CURRENT_TIMESTAMP, observaciones TEXT,
INDEX idx_nomina (nomina_id), INDEX idx_usuario (usuario_id), INDEX idx_fecha (fecha), INDEX idx_accion (accion),
FOREIGN KEY (nomina_id) REFERENCES nomina_electronica(id), FOREIGN KEY (eliminacion_id) REFERENCES nomina_electronica(id), FOREIGN KEY (usuario_id) REFERENCES usuarios(id)) ENGINE=InnoDB;Eliminar presenteEliminandoPredecesor correcto