345 lines
19 KiB
HTML
345 lines
19 KiB
HTML
{% extends 'base.html' %}
|
|
|
|
{% block content %}
|
|
<div class="form-container" style="max-width: 800px; margin: 0 auto;">
|
|
<div class="data-card" style="padding: 2.5rem; background: white; border-radius: 8px; box-shadow: 0 4px 6px -1px rgba(0,0,0,0.05), 0 2px 4px -1px rgba(0,0,0,0.03);">
|
|
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
|
<i data-lucide="{% if form.instance.pk %}edit-3{% else %}plus-circle{% endif %}"></i>
|
|
{% if form.instance.pk %}Modificar Gasto Registrado{% else %}Registro de Nueva Erogación{% endif %}
|
|
</h2>
|
|
|
|
<form method="post" enctype="multipart/form-data">
|
|
{% csrf_token %}
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
<div class="form-group" style="position: relative;">
|
|
<label for="{{ form.project.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Centro de Costo / Proyecto</label>
|
|
<div class="custom-select-wrapper" style="position: relative;">
|
|
{{ form.project }}
|
|
<input type="text" id="project-search-input" placeholder="Escriba código o nombre del proyecto..." autocomplete="off">
|
|
<div id="project-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
|
|
<!-- Items dynamically populated -->
|
|
</div>
|
|
</div>
|
|
{% if form.project.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.project.errors }}</div>{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group" style="position: relative;">
|
|
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría Contable</label>
|
|
<div class="custom-select-wrapper" style="position: relative;">
|
|
{{ form.category }}
|
|
<input type="text" id="category-search-input" placeholder="Escriba código o nombre para buscar..." autocomplete="off">
|
|
<div id="category-dropdown-list" class="custom-dropdown-list" style="display: none; position: absolute; background: white; border: 1px solid var(--efintel-border); border-radius: 4px; box-shadow: 0 4px 6px rgba(0,0,0,0.1); width: 100%; z-index: 1000; max-height: 250px; overflow-y: auto; margin-top: 4px; box-sizing: border-box;">
|
|
<!-- Items dynamically populated -->
|
|
</div>
|
|
</div>
|
|
{% if form.category.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
<div class="form-group">
|
|
<label for="{{ form.amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Neto (CLP)</label>
|
|
{{ form.amount_clp }}
|
|
{% if form.amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.amount_clp.errors }}</div>{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="{{ form.tax_type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Impuesto</label>
|
|
{{ form.tax_type }}
|
|
{% if form.tax_type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_type.errors }}</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
<div class="form-group">
|
|
<label for="{{ form.tax_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Impuesto (CLP)</label>
|
|
{{ form.tax_amount_clp }}
|
|
{% if form.tax_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.tax_amount_clp.errors }}</div>{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="{{ form.total_amount_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Total (CLP)</label>
|
|
{{ form.total_amount_clp }}
|
|
<small style="color: var(--efintel-gray); font-size: 0.7rem;">El equivalente en USD será calculado automáticamente por el sistema.</small>
|
|
{% if form.total_amount_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.total_amount_clp.errors }}</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
<div class="form-group">
|
|
<label for="{{ form.currency.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Moneda Original</label>
|
|
{{ form.currency }}
|
|
{% if form.currency.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.currency.errors }}</div>{% endif %}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
|
<div class="form-group">
|
|
<label for="{{ form.date.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Fecha de Emisión / Gasto</label>
|
|
{{ form.date }}
|
|
{% if form.date.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.date.errors }}</div>{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group">
|
|
<label for="{{ form.vendor.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Proveedor (Opcional)</label>
|
|
{{ form.vendor }}
|
|
{% 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 class="form-group" style="margin-bottom: 1.5rem;">
|
|
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Detalle de Imputación (Justificación)</label>
|
|
{{ form.description }}
|
|
{% if form.description.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>{% endif %}
|
|
</div>
|
|
|
|
<div class="form-group" style="margin-bottom: 1.5rem; background: #f8fafc; padding: 1rem; border-radius: 6px; border: 1px dashed var(--efintel-blue);">
|
|
<label for="{{ form.receipt.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-petroleum); display:flex; align-items:center; gap:0.3rem;"><i data-lucide="paperclip" style="width:14px;"></i> Adjuntar Evidencia (Boleta / Factura)</label>
|
|
{{ form.receipt }}
|
|
<small style="color: var(--efintel-gray); font-size: 0.7rem; display:block; margin-top:0.3rem;">Formatos aceptados: Imágenes (.png, .jpg) o Documentos PDF.</small>
|
|
{% if form.receipt.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.receipt.errors }}</div>{% endif %}
|
|
|
|
{% if form.instance.pk and form.instance.documents.first %}
|
|
<div style="margin-top:0.8rem; font-size:0.8rem;">
|
|
<strong>Evidencia Actual:</strong> <a href="{{ form.instance.documents.first.file.url }}" target="_blank" style="color:var(--efintel-blue);">Ver Archivo Adjunto</a>
|
|
</div>
|
|
{% 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>
|
|
|
|
<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 }}
|
|
</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 }}
|
|
</div>
|
|
</div>
|
|
|
|
<div style="display: flex; gap: 1rem; justify-content: flex-end; border-top: 1px solid var(--efintel-border); padding-top: 1.5rem;">
|
|
<a href="{% url 'expense_list' %}" class="btn-outline" style="padding: 0.75rem 1.5rem; text-decoration: none; color: var(--efintel-gray);">Cancelar</a>
|
|
<button type="submit" class="btn-primary" style="padding: 0.75rem 2rem; background: var(--efintel-blue); font-weight: 600;">
|
|
<i data-lucide="save" style="width: 16px; margin-right: 6px;"></i> Guardar Registro
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.querySelectorAll('input, select, textarea').forEach(el => {
|
|
el.style.width = '100%';
|
|
el.style.padding = '0.65rem';
|
|
el.style.border = '1px solid var(--efintel-border)';
|
|
el.style.borderRadius = '4px';
|
|
el.style.fontFamily = "'Inter', sans-serif";
|
|
el.style.fontSize = '0.9rem';
|
|
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');
|
|
|
|
function calculateTaxes() {
|
|
const amount = parseFloat(amountInput.value) || 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 = taxAmount.toFixed(0);
|
|
totalAmountInput.value = (amount + taxAmount).toFixed(0);
|
|
}
|
|
|
|
amountInput.addEventListener('input', calculateTaxes);
|
|
taxTypeSelect.addEventListener('change', calculateTaxes);
|
|
|
|
// Initial calculation
|
|
calculateTaxes();
|
|
|
|
// 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');
|
|
|
|
if (categorySelect && searchInput && dropdownList) {
|
|
// Hide original select (make it display none so form data is still submitted)
|
|
categorySelect.style.display = 'none';
|
|
|
|
// Extract options from the django-generated select
|
|
const options = Array.from(categorySelect.options)
|
|
.filter(opt => opt.value !== '')
|
|
.map(opt => ({
|
|
value: opt.value,
|
|
text: opt.textContent
|
|
}));
|
|
|
|
// Render option list dynamically
|
|
function renderList(filteredOptions) {
|
|
dropdownList.innerHTML = '';
|
|
if (filteredOptions.length === 0) {
|
|
dropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron categorías</div>';
|
|
return;
|
|
}
|
|
|
|
filteredOptions.forEach(opt => {
|
|
const item = document.createElement('div');
|
|
item.className = 'dropdown-item';
|
|
item.style.padding = '0.65rem 1rem';
|
|
item.style.cursor = 'pointer';
|
|
item.style.fontSize = '0.85rem';
|
|
item.style.borderBottom = '1px solid #f1f5f9';
|
|
item.textContent = opt.text;
|
|
|
|
// Mark parent categories differently (they usually have a badge in representation or just end with standard code)
|
|
// Note: we can detect if it's a main partida (ends in '000: ' or contains 'Partida')
|
|
const isParent = opt.text.includes('1000: ') || opt.text.includes('2000: ') || opt.text.includes('3000: ') || opt.text.includes('4000: ');
|
|
if (isParent) {
|
|
item.style.fontWeight = '700';
|
|
item.style.background = '#f8fafc';
|
|
item.style.color = 'var(--efintel-petroleum)';
|
|
}
|
|
|
|
item.addEventListener('click', () => {
|
|
categorySelect.value = opt.value;
|
|
searchInput.value = opt.text;
|
|
dropdownList.style.display = 'none';
|
|
categorySelect.dispatchEvent(new Event('change'));
|
|
});
|
|
|
|
item.addEventListener('mouseenter', () => {
|
|
item.style.background = 'var(--efintel-light)';
|
|
item.style.color = 'var(--efintel-blue)';
|
|
});
|
|
item.addEventListener('mouseleave', () => {
|
|
item.style.background = isParent ? '#f8fafc' : 'white';
|
|
item.style.color = isParent ? 'var(--efintel-petroleum)' : 'inherit';
|
|
});
|
|
|
|
dropdownList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
// Initialize input value if edit mode
|
|
const selectedOption = categorySelect.options[categorySelect.selectedIndex];
|
|
if (selectedOption && selectedOption.value !== '') {
|
|
searchInput.value = selectedOption.textContent;
|
|
}
|
|
|
|
// Show dropdown list on focus
|
|
searchInput.addEventListener('focus', () => {
|
|
renderList(options);
|
|
dropdownList.style.display = 'block';
|
|
});
|
|
|
|
// Dynamic filtering
|
|
searchInput.addEventListener('input', (e) => {
|
|
const query = e.target.value.toLowerCase();
|
|
const filtered = options.filter(opt => opt.text.toLowerCase().includes(query));
|
|
renderList(filtered);
|
|
});
|
|
|
|
// Close dropdown when clicking outside
|
|
document.addEventListener('click', (e) => {
|
|
if (!searchInput.contains(e.target) && !dropdownList.contains(e.target)) {
|
|
dropdownList.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
// Custom Searchable Dropdown for Project
|
|
const projectSelect = document.getElementById('id_project');
|
|
const projectSearchInput = document.getElementById('project-search-input');
|
|
const projectDropdownList = document.getElementById('project-dropdown-list');
|
|
|
|
if (projectSelect && projectSearchInput && projectDropdownList) {
|
|
projectSelect.style.display = 'none';
|
|
|
|
const projectOptions = Array.from(projectSelect.options)
|
|
.filter(opt => opt.value !== '')
|
|
.map(opt => ({
|
|
value: opt.value,
|
|
text: opt.textContent
|
|
}));
|
|
|
|
function renderProjectList(filteredOptions) {
|
|
projectDropdownList.innerHTML = '';
|
|
if (filteredOptions.length === 0) {
|
|
projectDropdownList.innerHTML = '<div style="padding: 0.65rem; color: var(--efintel-gray); font-size: 0.85rem; text-align: center;">No se encontraron proyectos</div>';
|
|
return;
|
|
}
|
|
|
|
filteredOptions.forEach(opt => {
|
|
const item = document.createElement('div');
|
|
item.className = 'dropdown-item';
|
|
item.style.padding = '0.65rem 1rem';
|
|
item.style.cursor = 'pointer';
|
|
item.style.fontSize = '0.85rem';
|
|
item.style.borderBottom = '1px solid #f1f5f9';
|
|
item.textContent = opt.text;
|
|
|
|
const isPortfolio = opt.text.startsWith('PORT-');
|
|
if (isPortfolio) {
|
|
item.style.fontWeight = '700';
|
|
item.style.background = '#f8fafc';
|
|
item.style.color = 'var(--efintel-petroleum)';
|
|
}
|
|
|
|
item.addEventListener('click', () => {
|
|
projectSelect.value = opt.value;
|
|
projectSearchInput.value = opt.text;
|
|
projectDropdownList.style.display = 'none';
|
|
projectSelect.dispatchEvent(new Event('change'));
|
|
});
|
|
|
|
item.addEventListener('mouseenter', () => {
|
|
item.style.background = 'var(--efintel-light)';
|
|
item.style.color = 'var(--efintel-blue)';
|
|
});
|
|
item.addEventListener('mouseleave', () => {
|
|
item.style.background = isPortfolio ? '#f8fafc' : 'white';
|
|
item.style.color = isPortfolio ? 'var(--efintel-petroleum)' : 'inherit';
|
|
});
|
|
|
|
projectDropdownList.appendChild(item);
|
|
});
|
|
}
|
|
|
|
const selectedProjectOption = projectSelect.options[projectSelect.selectedIndex];
|
|
if (selectedProjectOption && selectedProjectOption.value !== '') {
|
|
projectSearchInput.value = selectedProjectOption.textContent;
|
|
}
|
|
|
|
projectSearchInput.addEventListener('focus', () => {
|
|
renderProjectList(projectOptions);
|
|
projectDropdownList.style.display = 'block';
|
|
});
|
|
|
|
projectSearchInput.addEventListener('input', (e) => {
|
|
const query = e.target.value.toLowerCase();
|
|
const filtered = projectOptions.filter(opt => opt.text.toLowerCase().includes(query));
|
|
renderProjectList(filtered);
|
|
});
|
|
|
|
document.addEventListener('click', (e) => {
|
|
if (!projectSearchInput.contains(e.target) && !projectDropdownList.contains(e.target)) {
|
|
projectDropdownList.style.display = 'none';
|
|
}
|
|
});
|
|
}
|
|
</script>
|
|
{% endblock %}
|