|
|
@@ -27,25 +27,27 @@ interface InfoTributaria {
|
|
|
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
|
|
|
+ // Campos adicionales para infoAdicional
|
|
|
+ direccionComprador: string
|
|
|
emailComprador: string
|
|
|
+ telefonoComprador: string
|
|
|
+ // Para pagos
|
|
|
+ plazo: string
|
|
|
+ unidadTiempo: string
|
|
|
}
|
|
|
|
|
|
interface DetalleItem {
|
|
|
id: string
|
|
|
codigoPrincipal: string
|
|
|
+ codigoAuxiliar: string
|
|
|
descripcion: string
|
|
|
cantidad: string
|
|
|
precioUnitario: string
|
|
|
@@ -57,10 +59,18 @@ interface DetalleItem {
|
|
|
valorImpuesto: string
|
|
|
}
|
|
|
|
|
|
+interface TotalImpuesto {
|
|
|
+ codigo: string
|
|
|
+ codigoPorcentaje: string
|
|
|
+ baseImponible: string
|
|
|
+ valor: string
|
|
|
+}
|
|
|
+
|
|
|
interface FacturaData {
|
|
|
infoTributaria: InfoTributaria
|
|
|
infoFactura: InfoFactura
|
|
|
detalles: DetalleItem[]
|
|
|
+ totalesImpuestos: TotalImpuesto[]
|
|
|
}
|
|
|
|
|
|
// Componente para item de detalle
|
|
|
@@ -88,15 +98,29 @@ function DetalleItemForm({
|
|
|
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)
|
|
|
+ // Calcular IVA solo si aplica
|
|
|
+ const tarifa = parseFloat(updatedItem.tarifa) || 0
|
|
|
+ const codigoPorcentaje = updatedItem.codigoPorcentaje
|
|
|
+
|
|
|
+ // No calcular IVA si es "No objeto de IVA" (código 0) o tarifa 0%
|
|
|
+ if (codigoPorcentaje === '0' || tarifa === 0) {
|
|
|
+ updatedItem.valorImpuesto = '0.00'
|
|
|
+ } else {
|
|
|
+ updatedItem.valorImpuesto = (precioTotalSinImpuesto * tarifa / 100).toFixed(2)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
- if (field === 'tarifa') {
|
|
|
+ if (field === 'tarifa' || field === 'codigoPorcentaje') {
|
|
|
const baseImponible = parseFloat(updatedItem.baseImponible) || 0
|
|
|
- const tarifa = parseFloat(value) || 12
|
|
|
- updatedItem.valorImpuesto = (baseImponible * tarifa / 100).toFixed(2)
|
|
|
+ const tarifa = parseFloat(updatedItem.tarifa) || 0
|
|
|
+ const codigoPorcentaje = updatedItem.codigoPorcentaje
|
|
|
+
|
|
|
+ // No calcular IVA si es "No objeto de IVA" (código 0) o tarifa 0%
|
|
|
+ if (codigoPorcentaje === '0' || tarifa === 0) {
|
|
|
+ updatedItem.valorImpuesto = '0.00'
|
|
|
+ } else {
|
|
|
+ updatedItem.valorImpuesto = (baseImponible * tarifa / 100).toFixed(2)
|
|
|
+ }
|
|
|
}
|
|
|
|
|
|
onChange(updatedItem)
|
|
|
@@ -127,6 +151,16 @@ function DetalleItemForm({
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
+ <div className="space-y-2">
|
|
|
+ <Label htmlFor={`codigo-auxiliar-${item.id}`}>Código Auxiliar</Label>
|
|
|
+ <Input
|
|
|
+ id={`codigo-auxiliar-${item.id}`}
|
|
|
+ value={item.codigoAuxiliar}
|
|
|
+ onChange={(e) => handleChange('codigoAuxiliar', e.target.value)}
|
|
|
+ placeholder="AUX001"
|
|
|
+ />
|
|
|
+ </div>
|
|
|
+
|
|
|
<div className="space-y-2">
|
|
|
<Label htmlFor={`descripcion-${item.id}`}>Descripción</Label>
|
|
|
<Input
|
|
|
@@ -198,9 +232,13 @@ function DetalleItemForm({
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
<Label htmlFor={`tarifa-${item.id}`}>Tarifa IVA (%)</Label>
|
|
|
- <Select value={item.tarifa} onValueChange={(value) => handleChange('tarifa', value)}>
|
|
|
+ <Select
|
|
|
+ value={item.tarifa}
|
|
|
+ onValueChange={(value) => handleChange('tarifa', value)}
|
|
|
+ disabled={item.codigoPorcentaje === '0'}
|
|
|
+ >
|
|
|
<SelectTrigger>
|
|
|
- <SelectValue placeholder="Seleccionar" />
|
|
|
+ <SelectValue placeholder={item.codigoPorcentaje === '0' ? 'No aplica' : 'Seleccionar'} />
|
|
|
</SelectTrigger>
|
|
|
<SelectContent>
|
|
|
<SelectItem value="12">12%</SelectItem>
|
|
|
@@ -208,6 +246,11 @@ function DetalleItemForm({
|
|
|
<SelectItem value="14">14%</SelectItem>
|
|
|
</SelectContent>
|
|
|
</Select>
|
|
|
+ {item.codigoPorcentaje === '0' && (
|
|
|
+ <p className="text-xs text-muted-foreground mt-1">
|
|
|
+ No aplica
|
|
|
+ </p>
|
|
|
+ )}
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
@@ -258,26 +301,28 @@ export default function FacturaPage() {
|
|
|
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: ''
|
|
|
+ // Campos adicionales para infoAdicional
|
|
|
+ direccionComprador: '',
|
|
|
+ emailComprador: '',
|
|
|
+ telefonoComprador: '',
|
|
|
+ // Para pagos
|
|
|
+ plazo: '0',
|
|
|
+ unidadTiempo: 'dias'
|
|
|
})
|
|
|
|
|
|
const [detalles, setDetalles] = useState<DetalleItem[]>([
|
|
|
{
|
|
|
id: '1',
|
|
|
codigoPrincipal: '',
|
|
|
+ codigoAuxiliar: '',
|
|
|
descripcion: '',
|
|
|
cantidad: '1',
|
|
|
precioUnitario: '0.00',
|
|
|
@@ -296,28 +341,41 @@ export default function FacturaPage() {
|
|
|
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
|
|
|
})
|
|
|
|
|
|
+ // Calcular totales de impuestos por tipo
|
|
|
+ const totalesImpuestos = new Map<string, { base: number, valor: number }>()
|
|
|
+
|
|
|
+ detalles.forEach(item => {
|
|
|
+ const base = parseFloat(item.baseImponible) || 0
|
|
|
+ const valor = parseFloat(item.valorImpuesto) || 0
|
|
|
+ const key = item.codigoPorcentaje
|
|
|
+
|
|
|
+ if (totalesImpuestos.has(key)) {
|
|
|
+ const current = totalesImpuestos.get(key)!
|
|
|
+ totalesImpuestos.set(key, {
|
|
|
+ base: current.base + base,
|
|
|
+ valor: current.valor + valor
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ totalesImpuestos.set(key, { base, valor })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ const valorIVA = Array.from(totalesImpuestos.values()).reduce((sum, item) => sum + item.valor, 0)
|
|
|
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])
|
|
|
@@ -342,6 +400,7 @@ export default function FacturaPage() {
|
|
|
const nuevoDetalle: DetalleItem = {
|
|
|
id: Date.now().toString(),
|
|
|
codigoPrincipal: '',
|
|
|
+ codigoAuxiliar: '',
|
|
|
descripcion: '',
|
|
|
cantidad: '1',
|
|
|
precioUnitario: '0.00',
|
|
|
@@ -393,7 +452,26 @@ export default function FacturaPage() {
|
|
|
return
|
|
|
}
|
|
|
|
|
|
- // Generar XML (simulación del código PHP)
|
|
|
+ // Calcular totales de impuestos por tipo
|
|
|
+ const totalesImpuestos = new Map<string, { base: number, valor: number }>()
|
|
|
+
|
|
|
+ detalles.forEach(item => {
|
|
|
+ const base = parseFloat(item.baseImponible) || 0
|
|
|
+ const valor = parseFloat(item.valorImpuesto) || 0
|
|
|
+ const key = item.codigoPorcentaje
|
|
|
+
|
|
|
+ if (totalesImpuestos.has(key)) {
|
|
|
+ const current = totalesImpuestos.get(key)!
|
|
|
+ totalesImpuestos.set(key, {
|
|
|
+ base: current.base + base,
|
|
|
+ valor: current.valor + valor
|
|
|
+ })
|
|
|
+ } else {
|
|
|
+ totalesImpuestos.set(key, { base, valor })
|
|
|
+ }
|
|
|
+ })
|
|
|
+
|
|
|
+ // Generar XML según formato real del SRI
|
|
|
let xml = `<?xml version="1.0" encoding="UTF-8"?>
|
|
|
<factura id="comprobante" version="1.1.0">
|
|
|
<infoTributaria>
|
|
|
@@ -413,24 +491,28 @@ export default function FacturaPage() {
|
|
|
<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>
|
|
|
+ <totalConImpuestos>`
|
|
|
+
|
|
|
+ // Agregar múltiples impuestos según los detalles
|
|
|
+ totalesImpuestos.forEach((impuesto, codigoPorcentaje) => {
|
|
|
+ xml += `
|
|
|
<totalImpuesto>
|
|
|
<codigo>2</codigo>
|
|
|
- <codigoPorcentaje>${infoFactura.codigoPorcentaje}</codigoPorcentaje>
|
|
|
- <baseImponible>${infoFactura.baseImponible}</baseImponible>
|
|
|
- <valor>${infoFactura.valorImpuesto}</valor>
|
|
|
- </totalImpuesto>
|
|
|
+ <codigoPorcentaje>${codigoPorcentaje}</codigoPorcentaje>
|
|
|
+ <baseImponible>${impuesto.base.toFixed(2)}</baseImponible>
|
|
|
+ <valor>${impuesto.valor.toFixed(2)}</valor>
|
|
|
+ </totalImpuesto>`
|
|
|
+ })
|
|
|
+
|
|
|
+ xml += `
|
|
|
</totalConImpuestos>
|
|
|
-
|
|
|
<propina>0.00</propina>
|
|
|
<importeTotal>${infoFactura.importeTotal}</importeTotal>
|
|
|
<moneda>DOLAR</moneda>
|
|
|
@@ -439,6 +521,8 @@ export default function FacturaPage() {
|
|
|
<pago>
|
|
|
<formaPago>${infoFactura.formaPago}</formaPago>
|
|
|
<total>${infoFactura.importeTotal}</total>
|
|
|
+ <plazo>${infoFactura.plazo}</plazo>
|
|
|
+ <unidadTiempo>${infoFactura.unidadTiempo}</unidadTiempo>
|
|
|
</pago>
|
|
|
</pagos>
|
|
|
</infoFactura>
|
|
|
@@ -449,9 +533,10 @@ export default function FacturaPage() {
|
|
|
xml += `
|
|
|
<detalle>
|
|
|
<codigoPrincipal>${item.codigoPrincipal}</codigoPrincipal>
|
|
|
+ <codigoAuxiliar>${item.codigoAuxiliar}</codigoAuxiliar>
|
|
|
<descripcion>${item.descripcion}</descripcion>
|
|
|
<cantidad>${item.cantidad}</cantidad>
|
|
|
- <precioUnitario>${item.precioUnitario}</precioUnitario>
|
|
|
+ <precioUnitario>${parseFloat(item.precioUnitario).toFixed(6)}</precioUnitario>
|
|
|
<descuento>${item.descuento}</descuento>
|
|
|
<precioTotalSinImpuesto>${item.precioTotalSinImpuesto}</precioTotalSinImpuesto>
|
|
|
|
|
|
@@ -471,7 +556,9 @@ export default function FacturaPage() {
|
|
|
</detalles>
|
|
|
|
|
|
<infoAdicional>
|
|
|
+ <campoAdicional nombre="Direccion">${infoFactura.direccionComprador}</campoAdicional>
|
|
|
<campoAdicional nombre="Email">${infoFactura.emailComprador}</campoAdicional>
|
|
|
+ <campoAdicional nombre="Telefono">${infoFactura.telefonoComprador}</campoAdicional>
|
|
|
</infoAdicional>
|
|
|
</factura>`
|
|
|
|
|
|
@@ -658,12 +745,12 @@ export default function FacturaPage() {
|
|
|
</div>
|
|
|
|
|
|
<div className="space-y-2">
|
|
|
- <Label htmlFor="contribuyenteEspecial">Contribuyente Especial</Label>
|
|
|
+ <Label htmlFor="telefonoComprador">Teléfono Comprador</Label>
|
|
|
<Input
|
|
|
- id="contribuyenteEspecial"
|
|
|
- value={infoFactura.contribuyenteEspecial}
|
|
|
- onChange={(e) => handleInfoFacturaChange('contribuyenteEspecial', e.target.value)}
|
|
|
- placeholder="Opcional"
|
|
|
+ id="telefonoComprador"
|
|
|
+ value={infoFactura.telefonoComprador}
|
|
|
+ onChange={(e) => handleInfoFacturaChange('telefonoComprador', e.target.value)}
|
|
|
+ placeholder="Teléfono del comprador"
|
|
|
/>
|
|
|
</div>
|
|
|
|
|
|
@@ -793,8 +880,11 @@ export default function FacturaPage() {
|
|
|
<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" />
|
|
|
+ <Label>Valor IVA Total</Label>
|
|
|
+ <Input value={(() => {
|
|
|
+ const totalIVA = detalles.reduce((sum, item) => sum + (parseFloat(item.valorImpuesto) || 0), 0)
|
|
|
+ return totalIVA.toFixed(2)
|
|
|
+ })()} readOnly className="bg-muted" />
|
|
|
</div>
|
|
|
<div className="space-y-2">
|
|
|
<Label>Importe Total</Label>
|