|
|
@@ -0,0 +1,841 @@
|
|
|
+"use client"
|
|
|
+
|
|
|
+import { useState, useCallback, useMemo } from "react"
|
|
|
+import { Button } from "@/components/ui/button"
|
|
|
+import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
|
|
+import { Input } from "@/components/ui/input"
|
|
|
+import { Label } from "@/components/ui/label"
|
|
|
+import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
|
|
+import { Textarea } from "@/components/ui/textarea"
|
|
|
+import { toast } from "sonner"
|
|
|
+import { Trash2, Plus, Download } from "lucide-react"
|
|
|
+
|
|
|
+// Interfaces TypeScript para tipos estrictos
|
|
|
+interface InfoTributaria {
|
|
|
+ ambiente: string
|
|
|
+ tipoEmision: string
|
|
|
+ razonSocial: string
|
|
|
+ nombreComercial: string
|
|
|
+ ruc: string
|
|
|
+ claveAcceso: string
|
|
|
+ estab: string
|
|
|
+ ptoEmi: string
|
|
|
+ secuencial: string
|
|
|
+ dirMatriz: string
|
|
|
+}
|
|
|
+
|
|
|
+interface InfoFactura {
|
|
|
+ fechaEmision: string
|
|
|
+ dirEstablecimiento: string
|
|
|
+ contribuyenteEspecial: string
|
|
|
+ obligadoContabilidad: string
|
|
|
+ tipoIdentificacionComprador: string
|
|
|
+ razonSocialComprador: string
|
|
|
+ identificacionComprador: string
|
|
|
+ direccionComprador: string
|
|
|
+ totalSinImpuestos: string
|
|
|
+ totalDescuento: string
|
|
|
+ codigoPorcentaje: string
|
|
|
+ baseImponible: string
|
|
|
+ valorImpuesto: string
|
|
|
+ importeTotal: string
|
|
|
+ formaPago: string
|
|
|
+ emailComprador: string
|
|
|
+}
|
|
|
+
|
|
|
+interface DetalleItem {
|
|
|
+ id: string
|
|
|
+ codigoPrincipal: string
|
|
|
+ descripcion: string
|
|
|
+ cantidad: string
|
|
|
+ precioUnitario: string
|
|
|
+ descuento: string
|
|
|
+ precioTotalSinImpuesto: string
|
|
|
+ codigoPorcentaje: string
|
|
|
+ tarifa: string
|
|
|
+ baseImponible: string
|
|
|
+ valorImpuesto: string
|
|
|
+}
|
|
|
+
|
|
|
+interface FacturaData {
|
|
|
+ infoTributaria: InfoTributaria
|
|
|
+ infoFactura: InfoFactura
|
|
|
+ detalles: DetalleItem[]
|
|
|
+}
|
|
|
+
|
|
|
+// Componente para item de detalle
|
|
|
+function DetalleItemForm({
|
|
|
+ item,
|
|
|
+ onChange,
|
|
|
+ onRemove
|
|
|
+}: {
|
|
|
+ item: DetalleItem
|
|
|
+ onChange: (item: DetalleItem) => void
|
|
|
+ onRemove: () => void
|
|
|
+}) {
|
|
|
+ const handleChange = (field: keyof DetalleItem, value: string) => {
|
|
|
+ const updatedItem = { ...item, [field]: value }
|
|
|
+
|
|
|
+ // Cálculos automáticos
|
|
|
+ if (field === 'cantidad' || field === 'precioUnitario' || field === 'descuento') {
|
|
|
+ const cantidad = parseFloat(updatedItem.cantidad) || 0
|
|
|
+ const precioUnitario = parseFloat(updatedItem.precioUnitario) || 0
|
|
|
+ const descuento = parseFloat(updatedItem.descuento) || 0
|
|
|
+
|
|
|
+ const subtotal = cantidad * precioUnitario
|
|
|
+ const precioTotalSinImpuesto = Math.max(0, subtotal - descuento)
|
|
|
+
|
|
|
+ updatedItem.precioTotalSinImpuesto = precioTotalSinImpuesto.toFixed(2)
|
|
|
+ updatedItem.baseImponible = precioTotalSinImpuesto.toFixed(2)
|
|
|
+
|
|
|
+ // Calcular IVA (12% por defecto)
|
|
|
+ const tarifa = parseFloat(updatedItem.tarifa) || 12
|
|
|
+ updatedItem.valorImpuesto = (precioTotalSinImpuesto * tarifa / 100).toFixed(2)
|
|
|
+ }
|
|
|
+
|
|
|
+ if (field === 'tarifa') {
|
|
|
+ const baseImponible = parseFloat(updatedItem.baseImponible) || 0
|
|
|
+ const tarifa = parseFloat(value) || 12
|
|
|
+ updatedItem.valorImpuesto = (baseImponible * tarifa / 100).toFixed(2)
|
|
|
+ }
|
|
|
+
|
|
|
+ onChange(updatedItem)
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <Card className="p-4">
|
|
|
+ <div className="flex justify-between items-center mb-4">
|
|
|
+ <h4 className="text-sm font-medium">Detalle del Producto</h4>
|
|
|
+ <Button
|
|
|
+ type="button"
|
|
|
+ variant="destructive"
|
|
|
+ size="sm"
|
|
|
+ onClick={onRemove}
|
|
|
+ >
|
|
|
+ <Trash2 className="h-4 w-4" />
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`codigo-${item.id}`}>Código Principal</Label>
|
|
|
+ <Input
|
|
|
+ id={`codigo-${item.id}`}
|
|
|
+ value={item.codigoPrincipal}
|
|
|
+ onChange={(e) => handleChange('codigoPrincipal', e.target.value)}
|
|
|
+ placeholder="PROD001"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`descripcion-${item.id}`}>Descripción</Label>
|
|
|
+ <Input
|
|
|
+ id={`descripcion-${item.id}`}
|
|
|
+ value={item.descripcion}
|
|
|
+ onChange={(e) => handleChange('descripcion', e.target.value)}
|
|
|
+ placeholder="Descripción del producto"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`cantidad-${item.id}`}>Cantidad</Label>
|
|
|
+ <Input
|
|
|
+ id={`cantidad-${item.id}`}
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ value={item.cantidad}
|
|
|
+ onChange={(e) => handleChange('cantidad', e.target.value)}
|
|
|
+ placeholder="1"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`precio-unitario-${item.id}`}>Precio Unitario</Label>
|
|
|
+ <Input
|
|
|
+ id={`precio-unitario-${item.id}`}
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ value={item.precioUnitario}
|
|
|
+ onChange={(e) => handleChange('precioUnitario', e.target.value)}
|
|
|
+ placeholder="10.00"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`descuento-${item.id}`}>Descuento</Label>
|
|
|
+ <Input
|
|
|
+ id={`descuento-${item.id}`}
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ value={item.descuento}
|
|
|
+ onChange={(e) => handleChange('descuento', e.target.value)}
|
|
|
+ placeholder="0.00"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`precio-total-${item.id}`}>Total sin Impuesto</Label>
|
|
|
+ <Input
|
|
|
+ id={`precio-total-${item.id}`}
|
|
|
+ value={item.precioTotalSinImpuesto}
|
|
|
+ readOnly
|
|
|
+ className="bg-muted"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`codigo-porcentaje-${item.id}`}>Código IVA</Label>
|
|
|
+ <Select value={item.codigoPorcentaje} onValueChange={(value) => handleChange('codigoPorcentaje', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Seleccionar" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="2">2 - IVA</SelectItem>
|
|
|
+ <SelectItem value="0">0 - No objeto de IVA</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`tarifa-${item.id}`}>Tarifa IVA (%)</Label>
|
|
|
+ <Select value={item.tarifa} onValueChange={(value) => handleChange('tarifa', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue placeholder="Seleccionar" />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="12">12%</SelectItem>
|
|
|
+ <SelectItem value="0">0%</SelectItem>
|
|
|
+ <SelectItem value="14">14%</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`valor-impuesto-${item.id}`}>Valor Impuesto</Label>
|
|
|
+ <Input
|
|
|
+ id={`valor-impuesto-${item.id}`}
|
|
|
+ value={item.valorImpuesto}
|
|
|
+ readOnly
|
|
|
+ className="bg-muted"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </Card>
|
|
|
+ )
|
|
|
+}
|
|
|
+
|
|
|
+// Funciones de validación específicas para Ecuador
|
|
|
+const validateRUC = (ruc: string): boolean => {
|
|
|
+ // Validación básica de RUC ecuatoriano (13 dígitos)
|
|
|
+ return /^\d{13}$/.test(ruc)
|
|
|
+}
|
|
|
+
|
|
|
+const validateCedula = (cedula: string): boolean => {
|
|
|
+ // Validación básica de cédula ecuatoriana (10 dígitos)
|
|
|
+ return /^\d{10}$/.test(cedula)
|
|
|
+}
|
|
|
+
|
|
|
+const validateClaveAcceso = (clave: string): boolean => {
|
|
|
+ // Validación básica de clave de acceso SRI (49 dígitos)
|
|
|
+ return /^\d{49}$/.test(clave)
|
|
|
+}
|
|
|
+
|
|
|
+export default function FacturaPage() {
|
|
|
+ // Estado inicial
|
|
|
+ const [infoTributaria, setInfoTributaria] = useState<InfoTributaria>({
|
|
|
+ ambiente: '1',
|
|
|
+ tipoEmision: '1',
|
|
|
+ razonSocial: '',
|
|
|
+ nombreComercial: '',
|
|
|
+ ruc: '',
|
|
|
+ claveAcceso: '',
|
|
|
+ estab: '001',
|
|
|
+ ptoEmi: '001',
|
|
|
+ secuencial: '000000001',
|
|
|
+ dirMatriz: ''
|
|
|
+ })
|
|
|
+
|
|
|
+ const [infoFactura, setInfoFactura] = useState<InfoFactura>({
|
|
|
+ fechaEmision: new Date().toISOString().split('T')[0],
|
|
|
+ dirEstablecimiento: '',
|
|
|
+ contribuyenteEspecial: '',
|
|
|
+ obligadoContabilidad: 'SI',
|
|
|
+ tipoIdentificacionComprador: '04',
|
|
|
+ razonSocialComprador: '',
|
|
|
+ identificacionComprador: '',
|
|
|
+ direccionComprador: '',
|
|
|
+ totalSinImpuestos: '0.00',
|
|
|
+ totalDescuento: '0.00',
|
|
|
+ codigoPorcentaje: '2',
|
|
|
+ baseImponible: '0.00',
|
|
|
+ valorImpuesto: '0.00',
|
|
|
+ importeTotal: '0.00',
|
|
|
+ formaPago: '01',
|
|
|
+ emailComprador: ''
|
|
|
+ })
|
|
|
+
|
|
|
+ const [detalles, setDetalles] = useState<DetalleItem[]>([
|
|
|
+ {
|
|
|
+ id: '1',
|
|
|
+ codigoPrincipal: '',
|
|
|
+ descripcion: '',
|
|
|
+ cantidad: '1',
|
|
|
+ precioUnitario: '0.00',
|
|
|
+ descuento: '0.00',
|
|
|
+ precioTotalSinImpuesto: '0.00',
|
|
|
+ codigoPorcentaje: '2',
|
|
|
+ tarifa: '12',
|
|
|
+ baseImponible: '0.00',
|
|
|
+ valorImpuesto: '0.00'
|
|
|
+ }
|
|
|
+ ])
|
|
|
+
|
|
|
+ const [xmlGenerado, setXmlGenerado] = useState('')
|
|
|
+
|
|
|
+ // Calcular totales automáticamente
|
|
|
+ const calcularTotales = useCallback(() => {
|
|
|
+ let totalSinImpuestos = 0
|
|
|
+ let totalDescuento = 0
|
|
|
+ let baseImponibleIVA = 0
|
|
|
+ let valorIVA = 0
|
|
|
+
|
|
|
+ detalles.forEach(item => {
|
|
|
+ const subtotal = parseFloat(item.precioTotalSinImpuesto) || 0
|
|
|
+ const descuento = parseFloat(item.descuento) || 0
|
|
|
+ const impuesto = parseFloat(item.valorImpuesto) || 0
|
|
|
+
|
|
|
+ totalSinImpuestos += subtotal
|
|
|
+ totalDescuento += descuento
|
|
|
+ baseImponibleIVA += subtotal
|
|
|
+ valorIVA += impuesto
|
|
|
+ })
|
|
|
+
|
|
|
+ const importeTotal = totalSinImpuestos + valorIVA
|
|
|
+
|
|
|
+ setInfoFactura(prev => ({
|
|
|
+ ...prev,
|
|
|
+ totalSinImpuestos: totalSinImpuestos.toFixed(2),
|
|
|
+ totalDescuento: totalDescuento.toFixed(2),
|
|
|
+ baseImponible: baseImponibleIVA.toFixed(2),
|
|
|
+ valorImpuesto: valorIVA.toFixed(2),
|
|
|
+ importeTotal: importeTotal.toFixed(2)
|
|
|
+ }))
|
|
|
+ }, [detalles])
|
|
|
+
|
|
|
+ // Recalcular totales cuando cambian los detalles
|
|
|
+ useMemo(() => {
|
|
|
+ calcularTotales()
|
|
|
+ }, [detalles, calcularTotales])
|
|
|
+
|
|
|
+ // Manejar cambios en info tributaria
|
|
|
+ const handleInfoTributariaChange = (field: keyof InfoTributaria, value: string) => {
|
|
|
+ setInfoTributaria(prev => ({ ...prev, [field]: value }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Manejar cambios en info factura
|
|
|
+ const handleInfoFacturaChange = (field: keyof InfoFactura, value: string) => {
|
|
|
+ setInfoFactura(prev => ({ ...prev, [field]: value }))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Agregar nuevo detalle
|
|
|
+ const agregarDetalle = () => {
|
|
|
+ const nuevoDetalle: DetalleItem = {
|
|
|
+ id: Date.now().toString(),
|
|
|
+ codigoPrincipal: '',
|
|
|
+ descripcion: '',
|
|
|
+ cantidad: '1',
|
|
|
+ precioUnitario: '0.00',
|
|
|
+ descuento: '0.00',
|
|
|
+ precioTotalSinImpuesto: '0.00',
|
|
|
+ codigoPorcentaje: '2',
|
|
|
+ tarifa: '12',
|
|
|
+ baseImponible: '0.00',
|
|
|
+ valorImpuesto: '0.00'
|
|
|
+ }
|
|
|
+ setDetalles(prev => [...prev, nuevoDetalle])
|
|
|
+ }
|
|
|
+
|
|
|
+ // Actualizar detalle
|
|
|
+ const actualizarDetalle = (id: string, updatedItem: DetalleItem) => {
|
|
|
+ setDetalles(prev => prev.map(item => item.id === id ? updatedItem : item))
|
|
|
+ }
|
|
|
+
|
|
|
+ // Eliminar detalle
|
|
|
+ const eliminarDetalle = (id: string) => {
|
|
|
+ if (detalles.length > 1) {
|
|
|
+ setDetalles(prev => prev.filter(item => item.id !== id))
|
|
|
+ toast.success('Item eliminado')
|
|
|
+ } else {
|
|
|
+ toast.error('Debe haber al menos un item')
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // Generar XML
|
|
|
+ const generarXML = () => {
|
|
|
+ // Validaciones básicas
|
|
|
+ if (!validateRUC(infoTributaria.ruc)) {
|
|
|
+ toast.error('RUC inválido. Debe tener 13 dígitos')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!validateClaveAcceso(infoTributaria.claveAcceso)) {
|
|
|
+ toast.error('Clave de acceso inválida. Debe tener 49 dígitos')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (!infoFactura.identificacionComprador) {
|
|
|
+ toast.error('La identificación del comprador es requerida')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ if (infoFactura.tipoIdentificacionComprador === '04' && !validateRUC(infoFactura.identificacionComprador)) {
|
|
|
+ toast.error('RUC del comprador inválido')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ // Generar XML (simulación del código PHP)
|
|
|
+ let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
+<factura id="comprobante" version="1.1.0">
|
|
|
+ <infoTributaria>
|
|
|
+ <ambiente>${infoTributaria.ambiente}</ambiente>
|
|
|
+ <tipoEmision>${infoTributaria.tipoEmision}</tipoEmision>
|
|
|
+ <razonSocial>${infoTributaria.razonSocial}</razonSocial>
|
|
|
+ <nombreComercial>${infoTributaria.nombreComercial}</nombreComercial>
|
|
|
+ <ruc>${infoTributaria.ruc}</ruc>
|
|
|
+ <claveAcceso>${infoTributaria.claveAcceso}</claveAcceso>
|
|
|
+ <codDoc>01</codDoc>
|
|
|
+ <estab>${infoTributaria.estab}</estab>
|
|
|
+ <ptoEmi>${infoTributaria.ptoEmi}</ptoEmi>
|
|
|
+ <secuencial>${infoTributaria.secuencial}</secuencial>
|
|
|
+ <dirMatriz>${infoTributaria.dirMatriz}</dirMatriz>
|
|
|
+ </infoTributaria>
|
|
|
+
|
|
|
+ <infoFactura>
|
|
|
+ <fechaEmision>${infoFactura.fechaEmision}</fechaEmision>
|
|
|
+ <dirEstablecimiento>${infoFactura.dirEstablecimiento}</dirEstablecimiento>
|
|
|
+ <contribuyenteEspecial>${infoFactura.contribuyenteEspecial}</contribuyenteEspecial>
|
|
|
+ <obligadoContabilidad>${infoFactura.obligadoContabilidad}</obligadoContabilidad>
|
|
|
+ <tipoIdentificacionComprador>${infoFactura.tipoIdentificacionComprador}</tipoIdentificacionComprador>
|
|
|
+ <razonSocialComprador>${infoFactura.razonSocialComprador}</razonSocialComprador>
|
|
|
+ <identificacionComprador>${infoFactura.identificacionComprador}</identificacionComprador>
|
|
|
+ <direccionComprador>${infoFactura.direccionComprador}</direccionComprador>
|
|
|
+ <totalSinImpuestos>${infoFactura.totalSinImpuestos}</totalSinImpuestos>
|
|
|
+ <totalDescuento>${infoFactura.totalDescuento}</totalDescuento>
|
|
|
+
|
|
|
+ <totalConImpuestos>
|
|
|
+ <totalImpuesto>
|
|
|
+ <codigo>2</codigo>
|
|
|
+ <codigoPorcentaje>${infoFactura.codigoPorcentaje}</codigoPorcentaje>
|
|
|
+ <baseImponible>${infoFactura.baseImponible}</baseImponible>
|
|
|
+ <valor>${infoFactura.valorImpuesto}</valor>
|
|
|
+ </totalImpuesto>
|
|
|
+ </totalConImpuestos>
|
|
|
+
|
|
|
+ <propina>0.00</propina>
|
|
|
+ <importeTotal>${infoFactura.importeTotal}</importeTotal>
|
|
|
+ <moneda>DOLAR</moneda>
|
|
|
+
|
|
|
+ <pagos>
|
|
|
+ <pago>
|
|
|
+ <formaPago>${infoFactura.formaPago}</formaPago>
|
|
|
+ <total>${infoFactura.importeTotal}</total>
|
|
|
+ </pago>
|
|
|
+ </pagos>
|
|
|
+ </infoFactura>
|
|
|
+
|
|
|
+ <detalles>`
|
|
|
+
|
|
|
+ detalles.forEach(item => {
|
|
|
+ xml += `
|
|
|
+ <detalle>
|
|
|
+ <codigoPrincipal>${item.codigoPrincipal}</codigoPrincipal>
|
|
|
+ <descripcion>${item.descripcion}</descripcion>
|
|
|
+ <cantidad>${item.cantidad}</cantidad>
|
|
|
+ <precioUnitario>${item.precioUnitario}</precioUnitario>
|
|
|
+ <descuento>${item.descuento}</descuento>
|
|
|
+ <precioTotalSinImpuesto>${item.precioTotalSinImpuesto}</precioTotalSinImpuesto>
|
|
|
+
|
|
|
+ <impuestos>
|
|
|
+ <impuesto>
|
|
|
+ <codigo>2</codigo>
|
|
|
+ <codigoPorcentaje>${item.codigoPorcentaje}</codigoPorcentaje>
|
|
|
+ <tarifa>${item.tarifa}</tarifa>
|
|
|
+ <baseImponible>${item.baseImponible}</baseImponible>
|
|
|
+ <valor>${item.valorImpuesto}</valor>
|
|
|
+ </impuesto>
|
|
|
+ </impuestos>
|
|
|
+ </detalle>`
|
|
|
+ })
|
|
|
+
|
|
|
+ xml += `
|
|
|
+ </detalles>
|
|
|
+
|
|
|
+ <infoAdicional>
|
|
|
+ <campoAdicional nombre="Email">${infoFactura.emailComprador}</campoAdicional>
|
|
|
+ </infoAdicional>
|
|
|
+</factura>`
|
|
|
+
|
|
|
+ setXmlGenerado(xml)
|
|
|
+ toast.success('XML generado exitosamente')
|
|
|
+ }
|
|
|
+
|
|
|
+ // Descargar XML
|
|
|
+ const descargarXML = () => {
|
|
|
+ if (!xmlGenerado) {
|
|
|
+ toast.error('Debe generar el XML primero')
|
|
|
+ return
|
|
|
+ }
|
|
|
+
|
|
|
+ const blob = new Blob([xmlGenerado], { type: 'application/xml' })
|
|
|
+ const url = URL.createObjectURL(blob)
|
|
|
+ const a = document.createElement('a')
|
|
|
+ a.href = url
|
|
|
+ a.download = `factura_${infoTributaria.ruc}_${new Date().getTime()}.xml`
|
|
|
+ document.body.appendChild(a)
|
|
|
+ a.click()
|
|
|
+ document.body.removeChild(a)
|
|
|
+ URL.revokeObjectURL(url)
|
|
|
+ toast.success('XML descargado')
|
|
|
+ }
|
|
|
+
|
|
|
+ return (
|
|
|
+ <div className="min-h-screen bg-background p-8">
|
|
|
+ <div className="mx-auto max-w-6xl space-y-6">
|
|
|
+ {/* Header */}
|
|
|
+ <div className="text-center">
|
|
|
+ <h1 className="text-4xl font-bold tracking-tight">Factura Electrónica SRI Ecuador</h1>
|
|
|
+ <p className="mt-2 text-muted-foreground">
|
|
|
+ Generador de XML para facturación electrónica符合SRI
|
|
|
+ </p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* Info Tributaria */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Información Tributaria</CardTitle>
|
|
|
+ <CardDescription>Datos del emisor de la factura</CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ambiente">Ambiente</Label>
|
|
|
+ <Select value={infoTributaria.ambiente} onValueChange={(value) => handleInfoTributariaChange('ambiente', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="1">1 - Pruebas</SelectItem>
|
|
|
+ <SelectItem value="2">2 - Producción</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="tipoEmision">Tipo Emisión</Label>
|
|
|
+ <Select value={infoTributaria.tipoEmision} onValueChange={(value) => handleInfoTributariaChange('tipoEmision', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="1">1 - Normal</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ruc">RUC *</Label>
|
|
|
+ <Input
|
|
|
+ id="ruc"
|
|
|
+ value={infoTributaria.ruc}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('ruc', e.target.value)}
|
|
|
+ placeholder="13 dígitos"
|
|
|
+ maxLength={13}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="razonSocial">Razón Social *</Label>
|
|
|
+ <Input
|
|
|
+ id="razonSocial"
|
|
|
+ value={infoTributaria.razonSocial}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('razonSocial', e.target.value)}
|
|
|
+ placeholder="Empresa S.A."
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="nombreComercial">Nombre Comercial *</Label>
|
|
|
+ <Input
|
|
|
+ id="nombreComercial"
|
|
|
+ value={infoTributaria.nombreComercial}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('nombreComercial', e.target.value)}
|
|
|
+ placeholder="Nombre Comercial"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="claveAcceso">Clave de Acceso *</Label>
|
|
|
+ <Input
|
|
|
+ id="claveAcceso"
|
|
|
+ value={infoTributaria.claveAcceso}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('claveAcceso', e.target.value)}
|
|
|
+ placeholder="49 dígitos"
|
|
|
+ maxLength={49}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="estab">Establecimiento</Label>
|
|
|
+ <Input
|
|
|
+ id="estab"
|
|
|
+ value={infoTributaria.estab}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('estab', e.target.value)}
|
|
|
+ placeholder="001"
|
|
|
+ maxLength={3}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="ptoEmi">Punto Emisión</Label>
|
|
|
+ <Input
|
|
|
+ id="ptoEmi"
|
|
|
+ value={infoTributaria.ptoEmi}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('ptoEmi', e.target.value)}
|
|
|
+ placeholder="001"
|
|
|
+ maxLength={3}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="secuencial">Secuencial</Label>
|
|
|
+ <Input
|
|
|
+ id="secuencial"
|
|
|
+ value={infoTributaria.secuencial}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('secuencial', e.target.value)}
|
|
|
+ placeholder="000000001"
|
|
|
+ maxLength={9}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2 lg:col-span-3">
|
|
|
+ <Label htmlFor="dirMatriz">Dirección Matriz *</Label>
|
|
|
+ <Input
|
|
|
+ id="dirMatriz"
|
|
|
+ value={infoTributaria.dirMatriz}
|
|
|
+ onChange={(e) => handleInfoTributariaChange('dirMatriz', e.target.value)}
|
|
|
+ placeholder="Av. Principal 123 y Secundaria"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Info Factura */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Información de Factura</CardTitle>
|
|
|
+ <CardDescription>Datos de la factura y comprador</CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="fechaEmision">Fecha Emisión *</Label>
|
|
|
+ <Input
|
|
|
+ id="fechaEmision"
|
|
|
+ type="date"
|
|
|
+ value={infoFactura.fechaEmision}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('fechaEmision', e.target.value)}
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="dirEstablecimiento">Dirección Establecimiento *</Label>
|
|
|
+ <Input
|
|
|
+ id="dirEstablecimiento"
|
|
|
+ value={infoFactura.dirEstablecimiento}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('dirEstablecimiento', e.target.value)}
|
|
|
+ placeholder="Av. Secundaria 456"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="contribuyenteEspecial">Contribuyente Especial</Label>
|
|
|
+ <Input
|
|
|
+ id="contribuyenteEspecial"
|
|
|
+ value={infoFactura.contribuyenteEspecial}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('contribuyenteEspecial', e.target.value)}
|
|
|
+ placeholder="Opcional"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="obligadoContabilidad">Obligado Contabilidad</Label>
|
|
|
+ <Select value={infoFactura.obligadoContabilidad} onValueChange={(value) => handleInfoFacturaChange('obligadoContabilidad', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="SI">SI</SelectItem>
|
|
|
+ <SelectItem value="NO">NO</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="tipoIdentificacionComprador">Tipo Identificación Comprador</Label>
|
|
|
+ <Select value={infoFactura.tipoIdentificacionComprador} onValueChange={(value) => handleInfoFacturaChange('tipoIdentificacionComprador', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="04">04 - RUC</SelectItem>
|
|
|
+ <SelectItem value="05">05 - Cédula</SelectItem>
|
|
|
+ <SelectItem value="06">06 - Pasaporte</SelectItem>
|
|
|
+ <SelectItem value="07">07 - Consumidor Final</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="identificacionComprador">Identificación Comprador *</Label>
|
|
|
+ <Input
|
|
|
+ id="identificacionComprador"
|
|
|
+ value={infoFactura.identificacionComprador}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('identificacionComprador', e.target.value)}
|
|
|
+ placeholder="Según tipo seleccionado"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="razonSocialComprador">Razón Social Comprador *</Label>
|
|
|
+ <Input
|
|
|
+ id="razonSocialComprador"
|
|
|
+ value={infoFactura.razonSocialComprador}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('razonSocialComprador', e.target.value)}
|
|
|
+ placeholder="Nombre del comprador"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="direccionComprador">Dirección Comprador *</Label>
|
|
|
+ <Input
|
|
|
+ id="direccionComprador"
|
|
|
+ value={infoFactura.direccionComprador}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('direccionComprador', e.target.value)}
|
|
|
+ placeholder="Dirección del comprador"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="formaPago">Forma de Pago</Label>
|
|
|
+ <Select value={infoFactura.formaPago} onValueChange={(value) => handleInfoFacturaChange('formaPago', value)}>
|
|
|
+ <SelectTrigger>
|
|
|
+ <SelectValue />
|
|
|
+ </SelectTrigger>
|
|
|
+ <SelectContent>
|
|
|
+ <SelectItem value="01">01 - Efectivo</SelectItem>
|
|
|
+ <SelectItem value="15">15 - Transferencia</SelectItem>
|
|
|
+ <SelectItem value="16">16 - Tarjeta Crédito</SelectItem>
|
|
|
+ <SelectItem value="17">17 - Tarjeta Débito</SelectItem>
|
|
|
+ <SelectItem value="20">20 - Otros</SelectItem>
|
|
|
+ </SelectContent>
|
|
|
+ </Select>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor="emailComprador">Email Comprador</Label>
|
|
|
+ <Input
|
|
|
+ id="emailComprador"
|
|
|
+ type="email"
|
|
|
+ value={infoFactura.emailComprador}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('emailComprador', e.target.value)}
|
|
|
+ placeholder="email@ejemplo.com"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Detalles */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <div className="flex justify-between items-center">
|
|
|
+ <div>
|
|
|
+ <CardTitle>Detalles de Productos/Servicios</CardTitle>
|
|
|
+ <CardDescription>Agregar los productos o servicios de la factura</CardDescription>
|
|
|
+ </div>
|
|
|
+ <Button type="button" onClick={agregarDetalle} className="flex items-center gap-2">
|
|
|
+ <Plus className="h-4 w-4" />
|
|
|
+ Agregar Item
|
|
|
+ </Button>
|
|
|
+ </div>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent className="space-y-4">
|
|
|
+ {detalles.map((item) => (
|
|
|
+ <DetalleItemForm
|
|
|
+ key={item.id}
|
|
|
+ item={item}
|
|
|
+ onChange={(updatedItem) => actualizarDetalle(item.id, updatedItem)}
|
|
|
+ onRemove={() => eliminarDetalle(item.id)}
|
|
|
+ />
|
|
|
+ ))}
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Totales */}
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>Resumen de Totales</CardTitle>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <div className="grid grid-cols-1 md:grid-cols-3 gap-4">
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Total Sin Impuestos</Label>
|
|
|
+ <Input value={infoFactura.totalSinImpuestos} readOnly className="bg-muted" />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Valor IVA</Label>
|
|
|
+ <Input value={infoFactura.valorImpuesto} readOnly className="bg-muted" />
|
|
|
+ </div>
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label>Importe Total</Label>
|
|
|
+ <Input value={infoFactura.importeTotal} readOnly className="bg-muted font-bold text-lg" />
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+
|
|
|
+ {/* Acciones */}
|
|
|
+ <div className="flex gap-4">
|
|
|
+ <Button onClick={generarXML} className="flex-1">
|
|
|
+ Generar XML
|
|
|
+ </Button>
|
|
|
+ {xmlGenerado && (
|
|
|
+ <Button variant="outline" onClick={descargarXML} className="flex items-center gap-2">
|
|
|
+ <Download className="h-4 w-4" />
|
|
|
+ Descargar XML
|
|
|
+ </Button>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+
|
|
|
+ {/* XML Generado */}
|
|
|
+ {xmlGenerado && (
|
|
|
+ <Card>
|
|
|
+ <CardHeader>
|
|
|
+ <CardTitle>XML Generado</CardTitle>
|
|
|
+ <CardDescription>XML generado para el SRI</CardDescription>
|
|
|
+ </CardHeader>
|
|
|
+ <CardContent>
|
|
|
+ <Textarea
|
|
|
+ value={xmlGenerado}
|
|
|
+ readOnly
|
|
|
+ rows={20}
|
|
|
+ className="font-mono text-sm"
|
|
|
+ />
|
|
|
+ </CardContent>
|
|
|
+ </Card>
|
|
|
+ )}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ )
|
|
|
+}
|