iteracion 2

This commit is contained in:
Nachou-u
2026-09-07 18:44:32 -03:00
parent a103a47ae6
commit 09a3f6fb37
93 changed files with 7394 additions and 349 deletions
+174 -66
View File
@@ -8,7 +8,7 @@
{% if form.instance.pk %}Modificar Gasto Registrado{% else %}Registro de Nueva Erogación{% endif %}
</h2>
<form method="post" enctype="multipart/form-data">
<form method="post" enctype="multipart/form-data" x-data="expenseForm()">
{% csrf_token %}
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
@@ -73,9 +73,15 @@
{% if form.currency.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.currency.errors }}</div>{% endif %}
</div>
<div class="form-group">
<label for="{{ form.responsible_person.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Responsable Solicitante</label>
{{ form.responsible_person }}
<div class="form-group" style="background:#f8fafc; padding:0.75rem; border-radius:6px; border:1px solid #e2e8f0;">
<label style="font-weight: 700; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
{{ form.is_reimbursement }} ¿Es Rendición de Gastos?
</label>
<div id="responsible-person-container" style="margin-top:0.5rem;" x-show="isReimbursement" x-transition x-cloak>
<label for="{{ form.responsible_person.id_for_label }}" style="font-weight: 600; font-size: 0.75rem; color: var(--efintel-dark);">Responsable de Rendición</label>
{{ form.responsible_person }}
<small style="color:var(--efintel-gray); font-size:0.68rem; display:block;">Si NO es rendición, se imputa directamente al Centro de Costos I+D+i.</small>
</div>
{% if form.responsible_person.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.responsible_person.errors }}</div>{% endif %}
</div>
</div>
@@ -92,14 +98,14 @@
{{ form.vendor }}
<div style="margin-top: 0.5rem;">
<label style="font-size: 0.75rem; display: flex; align-items: center; gap: 0.35rem; cursor: pointer; color: var(--efintel-blue); font-weight: 600;">
<input type="checkbox" id="toggle-new-vendor" style="width: auto; margin: 0; cursor: pointer;"> Registrar un nuevo proveedor
<input type="checkbox" id="toggle-new-vendor" style="width: auto; margin: 0; cursor: pointer;" x-model="isNewVendor"> Registrar un nuevo proveedor
</label>
</div>
{% if form.vendor.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.vendor.errors }}</div>{% endif %}
</div>
</div>
<div id="new-vendor-fields" style="display: none; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; background: #f0fdf4; padding: 1.25rem; border-radius: 6px; border: 1px solid #bbf7d0; box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); align-items: end;">
<div id="new-vendor-fields" x-show="isNewVendor" x-transition x-cloak style="grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem; background: #f0fdf4; padding: 1.25rem; border-radius: 6px; border: 1px solid #bbf7d0; box-shadow: inset 0 2px 4px 0 rgba(0, 0, 0, 0.05); align-items: end; display: grid;">
<div class="form-group" style="display:flex; flex-direction:column;">
<label for="id_new_vendor_rut" style="font-weight: 600; font-size: 0.85rem; color: #166534; margin-bottom: 0.35rem;">RUT del Nuevo Proveedor</label>
{{ form.new_vendor_rut }}
@@ -164,20 +170,67 @@
{% endif %}
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 2rem; background: var(--efintel-light); padding: 1.5rem; border-radius: 6px;">
<h4 style="grid-column: 1 / -1; margin: 0 0 1rem 0; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.3rem;"><i data-lucide="map-pin" style="width:14px;"></i> Datos Logísticos (Opcional)</h4>
<!-- Requerimiento 14: El apartado de vehículo SOLO aplica para erogaciones de Combustible (3101) -->
<div id="vehicle-log-container" style="display: none; margin-bottom: 2rem; background: #fffbebfb; padding: 1.5rem; border-radius: 8px; border: 1px solid #fef3c7;">
<h4 style="margin: 0 0 1rem 0; font-size: 0.9rem; color: #92400e; display:flex; align-items:center; gap:0.4rem;">
<i data-lucide="car" style="width:16px;"></i> Informes Bitácora de Vehículo y Combustible (Requerido para Categoría Combustibles)
</h4>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1rem;">
<div class="form-group">
<label for="{{ form.vehicle_ownership.id_for_label }}" style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Modalidad / Vehículo</label>
{{ form.vehicle_ownership }}
</div>
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Patente Vehículo</label>
{{ form.v_license_plate }}
</div>
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Conductor / Responsable</label>
{{ form.v_driver_name }}
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr 1fr; gap: 1rem;">
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Odómetro Inicial (KM)</label>
{{ form.v_odometer_start }}
</div>
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Odómetro Final (KM)</label>
{{ form.v_odometer_end }}
</div>
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Origen</label>
{{ form.v_route_origin }}
</div>
<div class="form-group">
<label style="font-size: 0.75rem; font-weight: 600; color:#92400e;">Destino / Faena</label>
{{ form.v_route_destination }}
</div>
</div>
</div>
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 2rem; background: var(--efintel-light); padding: 1.25rem; border-radius: 6px;">
<h4 style="grid-column: 1 / -1; margin: 0 0 0.75rem 0; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.3rem;"><i data-lucide="map-pin" style="width:14px;"></i> Ubiación Geográfica Operativa (Opcional)</h4>
<div class="form-group">
<label for="{{ form.region.id_for_label }}" style="font-size: 0.75rem; font-weight: 600;">Región Operativa</label>
{{ form.region }}
<select name="region" id="id_region" x-model="selectedRegion" @change="updateComunas()">
<option value="">--- Seleccione Región ---</option>
<template x-for="(communes, regionName) in regionsData" :key="regionName">
<option :value="regionName" x-text="regionName" :selected="regionName === selectedRegion"></option>
</template>
</select>
</div>
<div class="form-group">
<label for="{{ form.comuna.id_for_label }}" style="font-size: 0.75rem; font-weight: 600;">Comuna</label>
{{ form.comuna }}
</div>
<div class="form-group">
<label for="{{ form.vehicle_ownership.id_for_label }}" style="font-size: 0.75rem; font-weight: 600;">Vehículo Asignado</label>
{{ form.vehicle_ownership }}
<select name="comuna" id="id_comuna" x-model="selectedComuna">
<option value="">--- Seleccione Comuna ---</option>
<template x-for="comunaName in availableComunas" :key="comunaName">
<option :value="comunaName" x-text="comunaName" :selected="comunaName === selectedComuna"></option>
</template>
</select>
</div>
</div>
@@ -196,6 +249,98 @@
{{ regions_communes_json|safe }}
</script>
<style>
[x-cloak] { display: none !important; }
</style>
<script>
document.addEventListener('alpine:init', () => {
Alpine.data('expenseForm', () => ({
isReimbursement: document.getElementById('id_is_reimbursement')?.checked || false,
isNewVendor: false,
amountClp: document.getElementById('id_amount_clp')?.value || '',
taxType: document.getElementById('id_tax_type')?.value || 'EXENTO',
taxAmountClp: document.getElementById('id_tax_amount_clp')?.value || '0',
totalAmountClp: document.getElementById('id_total_amount_clp')?.value || '0',
newVendorRut: '',
rutError: false,
regionsData: JSON.parse(document.getElementById('regions-communes-data').textContent || '{}'),
selectedRegion: '{{ form.instance.region|default:"" }}',
selectedComuna: '{{ form.instance.comuna|default:"" }}',
availableComunas: [],
init() {
if (this.selectedRegion) {
this.updateComunas();
}
},
updateComunas() {
this.availableComunas = this.regionsData[this.selectedRegion] || [];
if (!this.availableComunas.includes(this.selectedComuna)) {
this.selectedComuna = '';
}
},
formatAmount() {
let cleanVal = this.amountClp.replace(/\./g, '').replace(/\D/g, '');
if (cleanVal) {
this.amountClp = cleanVal.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
} else {
this.amountClp = '';
}
},
async calculateTax() {
let cleanVal = this.amountClp.replace(/\./g, '');
let amount = parseFloat(cleanVal) || 0;
if (amount > 0 && this.taxType) {
try {
const response = await fetch(`/api/v1/sii/calculate-tax?amount=${amount}&tax_type=${this.taxType}`);
const data = await response.json();
this.taxAmountClp = data.tax_amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
this.totalAmountClp = data.total_amount.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
} catch (e) {
console.error('Error calculating tax', e);
}
} else {
this.taxAmountClp = '0';
this.totalAmountClp = '0';
}
},
async validateRut() {
if (!this.newVendorRut) {
this.rutError = false;
return;
}
try {
const response = await fetch(`/api/v1/sii/validate-rut?rut=${this.newVendorRut}`);
const data = await response.json();
if (data.is_valid) {
this.newVendorRut = data.formatted_rut;
this.rutError = false;
} else {
this.rutError = true;
if (typeof Swal !== 'undefined') {
Swal.fire({
icon: 'error',
title: 'RUT Inválido',
text: data.error_message || "El formato del RUT no es válido.",
confirmButtonColor: 'var(--efintel-blue)'
});
}
}
} catch (e) {
console.error('Error validating RUT', e);
}
}
}));
});
</script>
<script>
document.querySelectorAll('input:not([type="checkbox"]), select, textarea').forEach(el => {
el.style.width = '100%';
@@ -207,63 +352,26 @@
el.style.boxSizing = 'border-box';
});
const amountInput = document.getElementById('id_amount_clp');
const taxTypeSelect = document.getElementById('id_tax_type');
const taxAmountInput = document.getElementById('id_tax_amount_clp');
const totalAmountInput = document.getElementById('id_total_amount_clp');
// AlpineJS manages tax calculation and toggles now.
function formatNumber(num) {
return num.toString().replace(/\B(?=(\d{3})+(?!\d))/g, ".");
}
function cleanNumberString(str) {
return str.replace(/\./g, '');
}
function calculateTaxes() {
const cleanVal = cleanNumberString(amountInput.value);
const amount = parseFloat(cleanVal) || 0;
const taxType = taxTypeSelect.value;
let taxAmount = 0;
if (taxType === 'IVA_19') {
taxAmount = amount * 0.19;
} else if (taxType === 'HONORARIOS_13_75') {
taxAmount = amount * 0.1375;
}
taxAmountInput.value = formatNumber(Math.round(taxAmount));
totalAmountInput.value = formatNumber(Math.round(amount + taxAmount));
}
amountInput.addEventListener('input', (e) => {
let cursorPosition = e.target.selectionStart;
const oldLength = e.target.value.length;
const cleanVal = cleanNumberString(e.target.value).replace(/\D/g, '');
if (cleanVal) {
e.target.value = formatNumber(cleanVal);
// Requerimiento 14: Apartado de Vehículo SOLO para Combustibles (3101)
const vehicleLogContainer = document.getElementById('vehicle-log-container');
function toggleVehicleLog() {
if (!categorySelect || !vehicleLogContainer) return;
const selText = categorySelect.options[categorySelect.selectedIndex] ? categorySelect.options[categorySelect.selectedIndex].textContent.toLowerCase() : '';
const selVal = categorySelect.value;
if (selText.includes('3101') || selText.includes('combustible')) {
vehicleLogContainer.style.display = 'block';
} else {
e.target.value = '';
vehicleLogContainer.style.display = 'none';
}
const newLength = e.target.value.length;
cursorPosition = cursorPosition + (newLength - oldLength);
e.target.setSelectionRange(cursorPosition, cursorPosition);
calculateTaxes();
});
taxTypeSelect.addEventListener('change', calculateTaxes);
// Initial formatting on load
if (amountInput.value) {
const cleanVal = cleanNumberString(amountInput.value).replace(/\D/g, '');
amountInput.value = formatNumber(cleanVal);
}
calculateTaxes();
if (categorySelect) {
categorySelect.addEventListener('change', toggleVehicleLog);
toggleVehicleLog();
}
// Custom Searchable Dropdown for Category
const categorySelect = document.getElementById('id_category');
const searchInput = document.getElementById('category-search-input');
const dropdownList = document.getElementById('category-dropdown-list');