iteracion 2
This commit is contained in:
@@ -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');
|
||||
|
||||
Reference in New Issue
Block a user