Compare commits
8 Commits
e7dbae1ab7
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
4e1a100b2a | ||
|
|
746d00bb66 | ||
|
|
dd8484a800 | ||
|
|
67cf2ae6fe | ||
|
|
8e83dd2276 | ||
|
|
86c04159b0 | ||
|
|
380fd7ca9f | ||
|
|
740a5ac758 |
@@ -58,18 +58,85 @@ async function needsInitialSync(tenantId: string): Promise<boolean> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ejecuta sincronización para un tenant
|
* Ejecuta sincronización para un tenant.
|
||||||
|
* Para despachos, itera cada contribuyente con FIEL activa.
|
||||||
|
* Para legacy (Horux 360), sync a nivel de tenant.
|
||||||
*/
|
*/
|
||||||
async function syncTenant(tenantId: string): Promise<void> {
|
async function syncTenant(tenantId: string): Promise<void> {
|
||||||
try {
|
try {
|
||||||
// Verificar si hay sync activo
|
const tenant = await prisma.tenant.findUnique({
|
||||||
|
where: { id: tenantId },
|
||||||
|
select: { rfc: true, databaseName: true },
|
||||||
|
});
|
||||||
|
if (!tenant) {
|
||||||
|
console.log(`[SAT Cron] Tenant ${tenantId} no encontrado, omitiendo`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDespacho = isDespachoTenant(tenant.rfc);
|
||||||
|
|
||||||
|
if (isDespacho) {
|
||||||
|
// Modo despacho: iterar contribuyentes con FIEL
|
||||||
|
const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
|
||||||
|
const { rows: contribuyentes } = await pool.query(`
|
||||||
|
SELECT c.entidad_id as id, c.rfc
|
||||||
|
FROM contribuyentes c
|
||||||
|
JOIN fiel_contribuyente f ON f.contribuyente_id = c.entidad_id
|
||||||
|
WHERE f.is_active = true AND f.valid_until > NOW()
|
||||||
|
`);
|
||||||
|
|
||||||
|
if (contribuyentes.length === 0) {
|
||||||
|
// Fallback: algunos despachos tienen FIEL a nivel de tenant (legacy) en lugar de per-contribuyente
|
||||||
|
const fielCentral = await prisma.fielCredential.findUnique({
|
||||||
|
where: { tenantId },
|
||||||
|
select: { isActive: true, validUntil: true },
|
||||||
|
});
|
||||||
|
if (fielCentral?.isActive && new Date() < fielCentral.validUntil) {
|
||||||
|
console.log(`[SAT Cron] Tenant ${tenantId} (despacho) sin FIEL per-contribuyente, usando FIEL central como fallback`);
|
||||||
|
const status = await getSyncStatus(tenantId);
|
||||||
|
if (status.hasActiveSync) {
|
||||||
|
console.log(`[SAT Cron] Tenant ${tenantId} ya tiene sync activo, omitiendo`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
const needsInitial = await needsInitialSync(tenantId);
|
||||||
|
const syncType = needsInitial ? 'initial' : 'daily';
|
||||||
|
console.log(`[SAT Cron] Iniciando sync ${syncType} para tenant ${tenantId} (fallback FIEL central)`);
|
||||||
|
const jobId = await startSync(tenantId, syncType);
|
||||||
|
console.log(`[SAT Cron] Job ${jobId} iniciado para tenant ${tenantId}`);
|
||||||
|
} else {
|
||||||
|
console.log(`[SAT Cron] Tenant ${tenantId} (despacho) sin contribuyentes con FIEL ni FIEL central, omitiendo`);
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const contrib of contribuyentes) {
|
||||||
|
try {
|
||||||
|
const status = await getSyncStatus(tenantId);
|
||||||
|
if (status.hasActiveSync) {
|
||||||
|
console.log(`[SAT Cron] Tenant ${tenantId} ya tiene sync activo, omitiendo contribuyente ${contrib.rfc}`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const needsInitial = await needsInitialSync(tenantId);
|
||||||
|
const syncType = needsInitial ? 'initial' : 'daily';
|
||||||
|
|
||||||
|
console.log(`[SAT Cron] Iniciando sync ${syncType} para contribuyente ${contrib.rfc} (tenant ${tenantId})`);
|
||||||
|
const jobId = await startSync(tenantId, syncType, undefined, undefined, contrib.id);
|
||||||
|
console.log(`[SAT Cron] Job ${jobId} iniciado para contribuyente ${contrib.rfc}`);
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error(`[SAT Cron] Error sincronizando contribuyente ${contrib.rfc} (tenant ${tenantId}):`, error.message);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Modo legacy (Horux 360)
|
||||||
const status = await getSyncStatus(tenantId);
|
const status = await getSyncStatus(tenantId);
|
||||||
if (status.hasActiveSync) {
|
if (status.hasActiveSync) {
|
||||||
console.log(`[SAT Cron] Tenant ${tenantId} ya tiene sync activo, omitiendo`);
|
console.log(`[SAT Cron] Tenant ${tenantId} ya tiene sync activo, omitiendo`);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Determinar tipo de sync
|
|
||||||
const needsInitial = await needsInitialSync(tenantId);
|
const needsInitial = await needsInitialSync(tenantId);
|
||||||
const syncType = needsInitial ? 'initial' : 'daily';
|
const syncType = needsInitial ? 'initial' : 'daily';
|
||||||
|
|
||||||
@@ -122,16 +189,25 @@ async function runSyncJob(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtiene los tenants Enterprise activos con FIEL configurada.
|
* Planes excluidos del incremental (Custom y Mi empresa no reciben sync frecuente).
|
||||||
*/
|
*/
|
||||||
async function getEnterpriseTenantsWithFiel(): Promise<string[]> {
|
const INCREMENTAL_EXCLUDED_PLANS = new Set(['custom', 'mi_empresa', 'mi_empresa_plus']);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtiene los tenants activos con FIEL configurada que son elegibles para sync incremental.
|
||||||
|
* Incluye todos los planes EXCEPTO Custom y Mi empresa / Mi empresa plus.
|
||||||
|
*/
|
||||||
|
async function getIncrementalTenantsWithFiel(): Promise<string[]> {
|
||||||
const tenants = await prisma.tenant.findMany({
|
const tenants = await prisma.tenant.findMany({
|
||||||
where: { active: true, plan: 'enterprise' },
|
where: { active: true },
|
||||||
select: { id: true },
|
select: { id: true, plan: true },
|
||||||
});
|
});
|
||||||
|
|
||||||
const result: string[] = [];
|
const result: string[] = [];
|
||||||
for (const tenant of tenants) {
|
for (const tenant of tenants) {
|
||||||
|
if (INCREMENTAL_EXCLUDED_PLANS.has(tenant.plan || '')) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
if (await hasFielConfigured(tenant.id)) {
|
if (await hasFielConfigured(tenant.id)) {
|
||||||
result.push(tenant.id);
|
result.push(tenant.id);
|
||||||
}
|
}
|
||||||
@@ -170,7 +246,8 @@ async function incrementalSyncTenant(tenantId: string): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Ejecuta el job incremental de 6 horas para todos los tenants Enterprise.
|
* Ejecuta el job incremental de 6 horas para todos los tenants elegibles.
|
||||||
|
* Elegibles = todos los planes EXCEPTO Custom y Mi empresa.
|
||||||
*/
|
*/
|
||||||
async function runIncrementalSyncJob(): Promise<void> {
|
async function runIncrementalSyncJob(): Promise<void> {
|
||||||
if (isIncrementalRunning) {
|
if (isIncrementalRunning) {
|
||||||
@@ -179,11 +256,11 @@ async function runIncrementalSyncJob(): Promise<void> {
|
|||||||
}
|
}
|
||||||
|
|
||||||
isIncrementalRunning = true;
|
isIncrementalRunning = true;
|
||||||
console.log('[SAT Cron Inc] Iniciando ciclo incremental Enterprise');
|
console.log('[SAT Cron Inc] Iniciando ciclo incremental');
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const tenantIds = await getEnterpriseTenantsWithFiel();
|
const tenantIds = await getIncrementalTenantsWithFiel();
|
||||||
console.log(`[SAT Cron Inc] ${tenantIds.length} tenants Enterprise con FIEL`);
|
console.log(`[SAT Cron Inc] ${tenantIds.length} tenants elegibles con FIEL`);
|
||||||
|
|
||||||
if (tenantIds.length === 0) return;
|
if (tenantIds.length === 0) return;
|
||||||
|
|
||||||
@@ -480,7 +557,7 @@ export function startSatSyncJob(): void {
|
|||||||
console.log(`[SAT Cron] Retry programado cada hora`);
|
console.log(`[SAT Cron] Retry programado cada hora`);
|
||||||
console.log(`[Opinion Cron] Programado para: ${OPINION_CRON_SCHEDULE} (America/Mexico_City)`);
|
console.log(`[Opinion Cron] Programado para: ${OPINION_CRON_SCHEDULE} (America/Mexico_City)`);
|
||||||
console.log(`[CSF Cron] Programado para: ${CSF_CRON_SCHEDULE} (America/Mexico_City)`);
|
console.log(`[CSF Cron] Programado para: ${CSF_CRON_SCHEDULE} (America/Mexico_City)`);
|
||||||
console.log(`[SAT Cron Inc] Incremental Enterprise programado para: ${INCREMENTAL_CRON_SCHEDULE} (America/Mexico_City)`);
|
console.log(`[SAT Cron Inc] Incremental programado para: ${INCREMENTAL_CRON_SCHEDULE} (America/Mexico_City)`);
|
||||||
console.log(`[Subscription Cron] Lifecycle programado para: ${SUBSCRIPTION_LIFECYCLE_CRON} (America/Mexico_City)`);
|
console.log(`[Subscription Cron] Lifecycle programado para: ${SUBSCRIPTION_LIFECYCLE_CRON} (America/Mexico_City)`);
|
||||||
console.log(`[SAT Watchdog] Programado para: ${WATCHDOG_CRON_SCHEDULE} (America/Mexico_City)`);
|
console.log(`[SAT Watchdog] Programado para: ${WATCHDOG_CRON_SCHEDULE} (America/Mexico_City)`);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,11 @@
|
|||||||
import { Credential } from '@nodecfdi/credentials/node';
|
import { Credential } from '@nodecfdi/credentials/node';
|
||||||
import { writeFile, mkdir } from 'fs/promises';
|
import { writeFile, mkdir } from 'fs/promises';
|
||||||
import { join } from 'path';
|
import { join } from 'path';
|
||||||
import { prisma } from '../config/database.js';
|
import { prisma, tenantDb } from '../config/database.js';
|
||||||
import { env } from '../config/env.js';
|
import { env } from '../config/env.js';
|
||||||
import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js';
|
import { encryptFielCredentials, encrypt, decryptFielCredentials } from './sat/sat-crypto.service.js';
|
||||||
import { emailService } from './email/email.service.js';
|
import { emailService } from './email/email.service.js';
|
||||||
|
import { isDespachoTenant } from '@horux/shared';
|
||||||
import type { FielStatus } from '@horux/shared';
|
import type { FielStatus } from '@horux/shared';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -305,9 +306,42 @@ export async function getDecryptedFiel(tenantId: string): Promise<{
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Verifica si un tenant tiene FIEL configurada y válida
|
* Verifica si un tenant tiene FIEL configurada y válida.
|
||||||
|
* Para despachos, verifica si hay al menos un contribuyente con FIEL activa.
|
||||||
|
* Para legacy (Horux 360), verifica la FIEL a nivel de tenant.
|
||||||
*/
|
*/
|
||||||
export async function hasFielConfigured(tenantId: string): Promise<boolean> {
|
export async function hasFielConfigured(tenantId: string): Promise<boolean> {
|
||||||
|
// 1. Intentar FIEL a nivel de tenant (modo legacy Horux 360)
|
||||||
const status = await getFielStatus(tenantId);
|
const status = await getFielStatus(tenantId);
|
||||||
return status.configured && !status.isExpired;
|
if (status.configured && !status.isExpired) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Para despachos, verificar FIEL por contribuyente
|
||||||
|
const tenant = await prisma.tenant.findUnique({
|
||||||
|
where: { id: tenantId },
|
||||||
|
select: { rfc: true, databaseName: true },
|
||||||
|
});
|
||||||
|
if (!tenant?.databaseName) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const isDespacho = isDespachoTenant(tenant.rfc);
|
||||||
|
if (!isDespacho) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const pool = await tenantDb.getPool(tenantId, tenant.databaseName);
|
||||||
|
const { rows } = await pool.query(`
|
||||||
|
SELECT 1
|
||||||
|
FROM fiel_contribuyente
|
||||||
|
WHERE is_active = true AND valid_until > NOW()
|
||||||
|
LIMIT 1
|
||||||
|
`);
|
||||||
|
return rows.length > 0;
|
||||||
|
} catch (err: any) {
|
||||||
|
console.error(`[hasFielConfigured] Error consultando FIEL de contribuyentes para tenant ${tenantId}:`, err.message);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -239,10 +239,23 @@ export async function downloadSatPackage(
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Formatea una fecha para el SAT (YYYY-MM-DD HH:mm:ss)
|
* Formatea una fecha para el SAT (YYYY-MM-DD HH:mm:ss) en hora de México.
|
||||||
|
* El SAT opera en zona horaria America/Mexico_City (UTC-6 estándar).
|
||||||
*/
|
*/
|
||||||
function formatDateForSat(date: Date): string {
|
function formatDateForSat(date: Date): string {
|
||||||
const pad = (n: number) => n.toString().padStart(2, '0');
|
const pad = (n: string) => n.padStart(2, '0');
|
||||||
return `${date.getFullYear()}-${pad(date.getMonth() + 1)}-${pad(date.getDate())} ` +
|
const parts = new Intl.DateTimeFormat('en-US', {
|
||||||
`${pad(date.getHours())}:${pad(date.getMinutes())}:${pad(date.getSeconds())}`;
|
timeZone: 'America/Mexico_City',
|
||||||
|
year: 'numeric',
|
||||||
|
month: '2-digit',
|
||||||
|
day: '2-digit',
|
||||||
|
hour: '2-digit',
|
||||||
|
minute: '2-digit',
|
||||||
|
second: '2-digit',
|
||||||
|
hour12: false,
|
||||||
|
}).formatToParts(date);
|
||||||
|
|
||||||
|
const get = (type: string) => pad(parts.find(p => p.type === type)?.value || '00');
|
||||||
|
|
||||||
|
return `${get('year')}-${get('month')}-${get('day')} ${get('hour')}:${get('minute')}:${get('second')}`;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ import { useDebounce } from '@horux/shared-ui';
|
|||||||
import { Header } from '@/components/layouts/header';
|
import { Header } from '@/components/layouts/header';
|
||||||
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Popover, PopoverTrigger, PopoverContent } from '@horux/shared-ui';
|
import { Card, CardContent, CardHeader, CardTitle, CardDescription, Button, Input, Label, Select, SelectContent, SelectItem, SelectTrigger, SelectValue, Dialog, DialogContent, DialogHeader, DialogTitle, DialogDescription, DialogFooter, Popover, PopoverTrigger, PopoverContent } from '@horux/shared-ui';
|
||||||
import { useCfdis, useCfdiConceptos, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi';
|
import { useCfdis, useCfdiConceptos, useCreateCfdi, useDeleteCfdi } from '@/lib/hooks/use-cfdi';
|
||||||
import { getCfdis, createManyCfdis, searchEmisores, searchReceptores, type EmisorReceptor } from '@/lib/api/cfdi';
|
import { getCfdis, createManyCfdis, searchEmisores, searchReceptores, getAllCfdiConceptos, type EmisorReceptor } from '@/lib/api/cfdi';
|
||||||
import { cancelarFactura, downloadPdf, downloadXml } from '@/lib/api/facturacion';
|
import { cancelarFactura, downloadPdf, downloadXml } from '@/lib/api/facturacion';
|
||||||
import type { CfdiFilters, CfdiConceptoFilters, TipoCfdi, Cfdi } from '@horux/shared';
|
import type { CfdiFilters, CfdiConceptoFilters, TipoCfdi, Cfdi } from '@horux/shared';
|
||||||
import type { CreateCfdiData } from '@/lib/api/cfdi';
|
import type { CreateCfdiData } from '@/lib/api/cfdi';
|
||||||
@@ -462,9 +462,16 @@ export default function CfdiPage() {
|
|||||||
'RFC Receptor': cfdi.rfcReceptor,
|
'RFC Receptor': cfdi.rfcReceptor,
|
||||||
'Nombre Receptor': cfdi.nombreReceptor,
|
'Nombre Receptor': cfdi.nombreReceptor,
|
||||||
'Subtotal': cfdi.subtotal,
|
'Subtotal': cfdi.subtotal,
|
||||||
|
'Subtotal MXN': cfdi.subtotalMxn,
|
||||||
'IVA': cfdi.ivaTraslado,
|
'IVA': cfdi.ivaTraslado,
|
||||||
|
'ISR Retención': cfdi.isrRetencion,
|
||||||
|
'IVA Retención': cfdi.ivaRetencion,
|
||||||
|
'Descuento': cfdi.descuento,
|
||||||
'Total': cfdi.total,
|
'Total': cfdi.total,
|
||||||
'Moneda': cfdi.moneda,
|
'Moneda': cfdi.moneda,
|
||||||
|
'Método de Pago': cfdi.metodoPago || '',
|
||||||
|
'Forma de Pago': cfdi.formaPago || '',
|
||||||
|
'Saldo Insoluto': cfdi.saldoInsoluto || '',
|
||||||
'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado',
|
'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado',
|
||||||
'Fecha Cancelación': cfdi.fechaCancelacion
|
'Fecha Cancelación': cfdi.fechaCancelacion
|
||||||
? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX')
|
? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX')
|
||||||
@@ -495,6 +502,64 @@ export default function CfdiPage() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const exportConceptosToExcel = async () => {
|
||||||
|
if (!conceptosData?.data.length) return;
|
||||||
|
|
||||||
|
setExporting(true);
|
||||||
|
try {
|
||||||
|
const allFilters: CfdiConceptoFilters = { ...conceptoFilters, page: 1, limit: 10000 };
|
||||||
|
const allData = await getAllCfdiConceptos(allFilters);
|
||||||
|
const rows = allData.data;
|
||||||
|
|
||||||
|
if (!rows.length) {
|
||||||
|
alert('No hay datos para exportar');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const exportData = rows.map(c => ({
|
||||||
|
'Fecha CFDI': c.cfdiFechaEmision ? new Date(c.cfdiFechaEmision).toLocaleDateString('es-MX') : '',
|
||||||
|
'UUID': c.cfdiUuid || '',
|
||||||
|
'Tipo Comprobante': formatTipoComprobante(c.cfdiTipoComprobante),
|
||||||
|
'Estatus CFDI': c.cfdiStatus === 'Vigente' || c.cfdiStatus === '1' ? 'Vigente' : 'Cancelado',
|
||||||
|
'RFC Emisor': c.cfdiRfcEmisor || '',
|
||||||
|
'Nombre Emisor': c.cfdiNombreEmisor || '',
|
||||||
|
'RFC Receptor': c.cfdiRfcReceptor || '',
|
||||||
|
'Nombre Receptor': c.cfdiNombreReceptor || '',
|
||||||
|
'Clave ProdServ': c.claveProdServ || '',
|
||||||
|
'No. Identificación': c.noIdentificacion || '',
|
||||||
|
'Descripción': c.descripcion,
|
||||||
|
'Cantidad': c.cantidad,
|
||||||
|
'Unidad': c.claveUnidad || c.unidad || '',
|
||||||
|
'Valor Unitario': c.valorUnitario,
|
||||||
|
'Importe': c.importe,
|
||||||
|
'Descuento': c.descuento,
|
||||||
|
'IVA Trasladado': c.ivaTraslado,
|
||||||
|
'IVA Retención': c.ivaRetencion,
|
||||||
|
'ISR Retención': c.isrRetencion,
|
||||||
|
}));
|
||||||
|
|
||||||
|
const ws = XLSX.utils.json_to_sheet(exportData);
|
||||||
|
const wb = XLSX.utils.book_new();
|
||||||
|
XLSX.utils.book_append_sheet(wb, ws, 'Conceptos');
|
||||||
|
|
||||||
|
const colWidths = Object.keys(exportData[0]).map(key => ({
|
||||||
|
wch: Math.max(key.length, ...exportData.map(row => String(row[key as keyof typeof row]).length))
|
||||||
|
}));
|
||||||
|
ws['!cols'] = colWidths;
|
||||||
|
|
||||||
|
const excelBuffer = XLSX.write(wb, { bookType: 'xlsx', type: 'array' });
|
||||||
|
const blob = new Blob([excelBuffer], { type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet' });
|
||||||
|
|
||||||
|
const fileName = `conceptos_${new Date().toISOString().split('T')[0]}.xlsx`;
|
||||||
|
saveAs(blob, fileName);
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Error exporting conceptos:', error);
|
||||||
|
alert('Error al exportar conceptos');
|
||||||
|
} finally {
|
||||||
|
setExporting(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
const handleDownloadPdf = async (facturapiId: string | null) => {
|
const handleDownloadPdf = async (facturapiId: string | null) => {
|
||||||
if (!facturapiId) return;
|
if (!facturapiId) return;
|
||||||
try {
|
try {
|
||||||
@@ -540,9 +605,16 @@ export default function CfdiPage() {
|
|||||||
'RFC Receptor': cfdi.rfcReceptor,
|
'RFC Receptor': cfdi.rfcReceptor,
|
||||||
'Nombre Receptor': cfdi.nombreReceptor,
|
'Nombre Receptor': cfdi.nombreReceptor,
|
||||||
'Subtotal': cfdi.subtotal,
|
'Subtotal': cfdi.subtotal,
|
||||||
|
'Subtotal MXN': cfdi.subtotalMxn,
|
||||||
'IVA': cfdi.ivaTraslado,
|
'IVA': cfdi.ivaTraslado,
|
||||||
|
'ISR Retención': cfdi.isrRetencion,
|
||||||
|
'IVA Retención': cfdi.ivaRetencion,
|
||||||
|
'Descuento': cfdi.descuento,
|
||||||
'Total': cfdi.total,
|
'Total': cfdi.total,
|
||||||
'Moneda': cfdi.moneda,
|
'Moneda': cfdi.moneda,
|
||||||
|
'Método de Pago': cfdi.metodoPago || '',
|
||||||
|
'Forma de Pago': cfdi.formaPago || '',
|
||||||
|
'Saldo Insoluto': cfdi.saldoInsoluto || '',
|
||||||
'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado',
|
'Estatus': cfdi.status === 'Vigente' || cfdi.status === '1' ? 'Vigente' : 'Cancelado',
|
||||||
'Fecha Cancelación': cfdi.fechaCancelacion
|
'Fecha Cancelación': cfdi.fechaCancelacion
|
||||||
? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX')
|
? new Date(cfdi.fechaCancelacion).toLocaleDateString('es-MX')
|
||||||
@@ -1025,8 +1097,8 @@ export default function CfdiPage() {
|
|||||||
</Select>
|
</Select>
|
||||||
</div>
|
</div>
|
||||||
<div className="flex gap-2">
|
<div className="flex gap-2">
|
||||||
{data && data.data.length > 0 && (
|
{((activeTab === 'cfdis' && data && data.data.length > 0) || (activeTab === 'conceptos' && conceptosData && conceptosData.data.length > 0)) && (
|
||||||
<Button variant="outline" onClick={exportToExcel} disabled={exporting}>
|
<Button variant="outline" onClick={activeTab === 'cfdis' ? exportToExcel : exportConceptosToExcel} disabled={exporting}>
|
||||||
{exporting ? (
|
{exporting ? (
|
||||||
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
<Loader2 className="h-4 w-4 mr-1 animate-spin" />
|
||||||
) : (
|
) : (
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ Wants=postgresql.service
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/root/Horux/apps/api
|
WorkingDirectory=/root/HoruxDespachos/apps/api
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
||||||
ExecStart=/root/.local/share/pnpm/pnpm dev
|
ExecStart=/root/.local/share/pnpm/pnpm start
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|
||||||
|
|||||||
@@ -6,10 +6,10 @@ Wants=horux-api.service
|
|||||||
[Service]
|
[Service]
|
||||||
Type=simple
|
Type=simple
|
||||||
User=root
|
User=root
|
||||||
WorkingDirectory=/root/Horux/apps/web
|
WorkingDirectory=/root/HoruxDespachos/apps/web
|
||||||
Environment=NODE_ENV=production
|
Environment=NODE_ENV=production
|
||||||
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
Environment=PATH=/root/.local/share/pnpm:/usr/local/bin:/usr/bin:/bin
|
||||||
ExecStart=/root/.local/share/pnpm/pnpm dev
|
ExecStart=/root/.local/share/pnpm/pnpm start
|
||||||
Restart=always
|
Restart=always
|
||||||
RestartSec=10
|
RestartSec=10
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user