iteracion 2
This commit is contained in:
+81
-4
@@ -10,7 +10,8 @@
|
||||
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700;800&family=JetBrains+Mono:wght@500;700;800&display=swap" rel="stylesheet">
|
||||
<script src="https://cdn.jsdelivr.net/npm/chart.js"></script>
|
||||
<script src="https://unpkg.com/lucide@latest"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}">
|
||||
<script defer src="https://cdn.jsdelivr.net/npm/alpinejs@3.x.x/dist/cdn.min.js"></script>
|
||||
<link rel="stylesheet" href="{% static 'css/style.css' %}?v=2.1">
|
||||
<meta name="csrf-token" content="{{ csrf_token }}">
|
||||
</head>
|
||||
<body>
|
||||
@@ -49,12 +50,21 @@
|
||||
<a href="{% url 'dashboard' %}" class="nav-tab {% if request.resolver_match.url_name == 'dashboard' %}active{% endif %}">
|
||||
<i data-lucide="pie-chart" style="width:16px;"></i> Resumen Financiero
|
||||
</a>
|
||||
<a href="{% url 'expense_list' %}" class="nav-tab {% if request.resolver_match.url_name == 'expense_list' %}active{% endif %}">
|
||||
<i data-lucide="truck" style="width:16px;"></i> Operaciones y Gastos
|
||||
<a href="{% url 'expense_list' %}" class="nav-tab {% if request.resolver_match.url_name == 'expense_list' or request.resolver_match.url_name == 'expense_report_list' %}active{% endif %}">
|
||||
<i data-lucide="truck" style="width:16px;"></i> Operaciones y Erogaciones
|
||||
</a>
|
||||
<a href="{% url 'project_list' %}" class="nav-tab {% if request.resolver_match.url_name == 'project_list' %}active{% endif %}">
|
||||
<a href="{% url 'project_list' %}" class="nav-tab {% if 'project' in request.resolver_match.url_name %}active{% endif %}">
|
||||
<i data-lucide="folder-kanban" style="width:16px;"></i> Control de Proyectos
|
||||
</a>
|
||||
<a href="{% url 'procurement:catalog_list' %}" class="nav-tab {% if 'procurement' in request.resolver_match.namespace %}active{% endif %}">
|
||||
<i data-lucide="shopping-cart" style="width:16px;"></i> Compras & OC
|
||||
</a>
|
||||
<a href="{% url 'payroll:employee_list' %}" class="nav-tab {% if 'payroll' in request.resolver_match.namespace %}active{% endif %}">
|
||||
<i data-lucide="users" style="width:16px;"></i> Remuneraciones
|
||||
</a>
|
||||
<a href="{% url 'talent:candidate_list' %}" class="nav-tab {% if 'talent' in request.resolver_match.namespace %}active{% endif %}">
|
||||
<i data-lucide="award" style="width:16px;"></i> Cartera Talento
|
||||
</a>
|
||||
{% if 'Administrador Finanzas' in request.user.groups.all.0.name or 'Administrador' in request.user.groups.all.0.name or request.user.is_superuser %}
|
||||
<a href="{% url 'accounts_payable' %}" class="nav-tab {% if request.resolver_match.url_name == 'accounts_payable' %}active{% endif %}">
|
||||
<i data-lucide="calculator" style="width:16px;"></i> Cuentas por Pagar (CXP)
|
||||
@@ -81,10 +91,77 @@
|
||||
{% block content %}{% endblock %}
|
||||
</main>
|
||||
|
||||
<!-- Requerimiento 3: Modal Universal de Previsualización de Evidencias (JPG/PNG/PDF) -->
|
||||
<div id="evidencePreviewModal" style="display:none; position:fixed; top:0; left:0; width:100%; height:100%; background:rgba(15,23,42,0.85); z-index:9999; backdrop-filter:blur(4px); align-items:center; justify-content:center;">
|
||||
<div style="background:white; border-radius:12px; width:90%; max-width:900px; height:85vh; display:flex; flex-direction:column; overflow:hidden; box-shadow:0 25px 50px -12px rgba(0,0,0,0.5);">
|
||||
<div style="padding:1rem 1.5rem; background:var(--efintel-dark, #1e293b); color:white; display:flex; justify-content:space-between; align-items:center;">
|
||||
<h3 id="previewModalTitle" style="margin:0; font-size:1.1rem; font-weight:700; color:white;">Vista Previa de Evidencia</h3>
|
||||
<div style="display:flex; gap:0.5rem; align-items:center;">
|
||||
<a id="previewModalDownloadBtn" href="#" target="_blank" class="btn-primary" style="padding:0.3rem 0.8rem; font-size:0.8rem; text-decoration:none; background:#0284c7;">
|
||||
<i data-lucide="download" style="width:14px;"></i> Descargar
|
||||
</a>
|
||||
<button onclick="closePreviewModal()" style="background:transparent; border:none; color:white; font-size:1.5rem; cursor:pointer; padding:0 0.5rem;">×</button>
|
||||
</div>
|
||||
</div>
|
||||
<div id="previewModalContent" style="flex:1; background:#f8fafc; display:flex; align-items:center; justify-content:center; padding:1rem; overflow:auto;">
|
||||
<!-- Contenido dinámico embebido (Image o PDF) -->
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<script>
|
||||
if (window.lucide) {
|
||||
lucide.createIcons();
|
||||
}
|
||||
|
||||
function previewDocument(fileUrl, title, isPdf) {
|
||||
const modal = document.getElementById('evidencePreviewModal');
|
||||
const modalTitle = document.getElementById('previewModalTitle');
|
||||
const downloadBtn = document.getElementById('previewModalDownloadBtn');
|
||||
const content = document.getElementById('previewModalContent');
|
||||
|
||||
modalTitle.textContent = title || 'Vista Previa de Evidencia';
|
||||
downloadBtn.href = fileUrl;
|
||||
|
||||
if (isPdf || fileUrl.toLowerCase().endsWith('.pdf')) {
|
||||
content.innerHTML = `<object data="${fileUrl}" type="application/pdf" width="100%" height="100%" style="min-height:70vh; border:none;">
|
||||
<iframe src="${fileUrl}" width="100%" height="100%" style="min-height:70vh; border:none;">
|
||||
Tu navegador no soporta visualización directa de PDF. <a href="${fileUrl}" target="_blank">Haz clic aquí para descargar.</a>
|
||||
</iframe>
|
||||
</object>`;
|
||||
} else {
|
||||
content.innerHTML = `<img src="${fileUrl}" alt="Evidencia" style="max-width:100%; max-height:75vh; object-fit:contain; border-radius:8px; box-shadow:0 4px 6px -1px rgba(0,0,0,0.1);" />`;
|
||||
}
|
||||
|
||||
modal.style.display = 'flex';
|
||||
}
|
||||
|
||||
function closePreviewModal() {
|
||||
const modal = document.getElementById('evidencePreviewModal');
|
||||
const content = document.getElementById('previewModalContent');
|
||||
modal.style.display = 'none';
|
||||
content.innerHTML = '';
|
||||
}
|
||||
|
||||
// Formateador automático global para cualquier formulario presente o futuro
|
||||
function styleAllFormControls() {
|
||||
document.querySelectorAll('form input:not([type="checkbox"]):not([type="radio"]):not([type="submit"]):not([type="button"]):not([type="hidden"]), form select, form textarea').forEach(function(el) {
|
||||
if (!el.classList.contains('form-control') && !el.classList.contains('form-input')) {
|
||||
el.classList.add('form-control');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
document.addEventListener('DOMContentLoaded', styleAllFormControls);
|
||||
const formObserver = new MutationObserver(styleAllFormControls);
|
||||
if (document.body) {
|
||||
formObserver.observe(document.body, { childList: true, subtree: true });
|
||||
}
|
||||
|
||||
// Cerrar modal al presionar Escape
|
||||
document.addEventListener('keydown', function(e) {
|
||||
if (e.key === 'Escape') closePreviewModal();
|
||||
});
|
||||
</script>
|
||||
{% block extra_js %}{% endblock %}
|
||||
</body>
|
||||
|
||||
@@ -15,10 +15,10 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid-3">
|
||||
<div class="kpi-grid-2" style="display:grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 2rem;">
|
||||
<div class="kpi-card accent-blue">
|
||||
<div class="kpi-header">
|
||||
<span class="kpi-title">EJECUCIÓN HISTÓRICA / MES</span>
|
||||
<span class="kpi-title">EJECUCIÓN HISTÓRICA ACUMULADA</span>
|
||||
<div class="kpi-icon"><i data-lucide="credit-card"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-spend">${{ total_all|floatformat:"0"|intcomma }} CLP</div>
|
||||
@@ -27,25 +27,17 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-card accent-turquoise">
|
||||
<div class="kpi-header">
|
||||
<span class="kpi-title">PRESUPUESTO GENERAL I+D+i</span>
|
||||
<div class="kpi-icon"><i data-lucide="wallet"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-remaining">${{ global_budget|floatformat:"0"|intcomma }} CLP</div>
|
||||
<div class="kpi-subtext">
|
||||
<span>Saldo Restante: ${{ remaining_budget|floatformat:"0"|intcomma }} CLP</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-card accent-success">
|
||||
<div class="kpi-header">
|
||||
<span class="kpi-title">PROYECTOS ACTIVOS</span>
|
||||
<span class="kpi-title">PROYECTOS Y CENTROS ACTIVOS</span>
|
||||
<div class="kpi-icon"><i data-lucide="trending-up"></i></div>
|
||||
</div>
|
||||
<div class="kpi-value" id="kpi-evm">{{ project_count }}</div>
|
||||
<div class="kpi-value" id="kpi-evm">{{ project_count_active }} <span style="font-size:1rem; font-weight:500; color:var(--efintel-gray);">/ {{ project_count_total }} Totales</span></div>
|
||||
<div class="kpi-subtext">
|
||||
<span class="status-dot green"></span> <span style="font-weight:600; color:var(--efintel-dark);">En línea</span>
|
||||
<span class="status-dot green"></span>
|
||||
<span style="font-weight:600; color:var(--efintel-dark);">
|
||||
{{ project_count_active }} Activos · {{ project_count_paused }} Pausados · {{ project_count_completed }} Terminados
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -100,9 +92,14 @@
|
||||
{% if expense.documents.all %}
|
||||
<div style="margin-top: 0.25rem; display: flex; flex-direction: column; gap: 0.2rem;">
|
||||
{% for doc in expense.documents.all %}
|
||||
<a href="{{ doc.file.url }}" target="_blank" style="font-size:0.65rem; color:var(--efintel-blue); text-decoration:none; display:inline-flex; align-items:center; gap:0.2rem;" title="{{ doc.title|default:doc.file.name }}">
|
||||
<i data-lucide="paperclip" style="width:10px;"></i> {{ doc.title|default:doc.file.name|truncatechars:25 }}
|
||||
</a>
|
||||
<div style="display:inline-flex; align-items:center; gap:0.3rem;">
|
||||
<a href="{{ doc.file.url }}" target="_blank" style="font-size:0.65rem; color:var(--efintel-blue); text-decoration:none;" title="{{ doc.title|default:doc.file.name }}">
|
||||
<i data-lucide="paperclip" style="width:10px;"></i> {{ doc.title|default:doc.file.name|truncatechars:20 }}
|
||||
</a>
|
||||
<button type="button" onclick="previewDocument('{{ doc.file.url }}', '{{ doc.title|escapejs }}', {% if doc.is_pdf %}true{% else %}false{% endif %})" style="background:none; border:none; color:#0284c7; cursor:pointer; padding:0; font-size:0.65rem;" title="Vista Previa Modal">
|
||||
<i data-lucide="eye" style="width:12px;"></i>
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -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');
|
||||
|
||||
@@ -70,9 +70,14 @@
|
||||
{% if expense.documents.all %}
|
||||
<div style="margin-top: 0.25rem; display: flex; flex-direction: column; gap: 0.2rem;">
|
||||
{% for doc in expense.documents.all %}
|
||||
<a href="{{ doc.file.url }}" target="_blank" style="font-size:0.65rem; color:var(--efintel-blue); text-decoration:none; display:inline-flex; align-items:center; gap:0.2rem;" title="{{ doc.title|default:doc.file.name }}">
|
||||
<i data-lucide="paperclip" style="width:10px;"></i> {{ doc.title|default:doc.file.name|truncatechars:25 }}
|
||||
</a>
|
||||
<div style="display:inline-flex; align-items:center; gap:0.3rem;">
|
||||
<a href="{{ doc.file.url }}" target="_blank" style="font-size:0.65rem; color:var(--efintel-blue); text-decoration:none;" title="{{ doc.title|default:doc.file.name }}">
|
||||
<i data-lucide="paperclip" style="width:10px;"></i> {{ doc.title|default:doc.file.name|truncatechars:20 }}
|
||||
</a>
|
||||
<button type="button" onclick="previewDocument('{{ doc.file.url }}', '{{ doc.title|escapejs }}', {% if doc.is_pdf %}true{% else %}false{% endif %})" style="background:none; border:none; color:#0284c7; cursor:pointer; padding:0; font-size:0.65rem;" title="Vista Previa Modal">
|
||||
<i data-lucide="eye" style="width:12px;"></i> Ver
|
||||
</button>
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-plus" style="color:var(--efintel-blue);"></i> {{ title }}</h2>
|
||||
<p>Creación y edición de informe agrupador de rendición de gastos.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'expense_report_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'expense_report_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="save" style="width:16px;"></i> Guardar Informe Rendición</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,53 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-spreadsheets" style="color:var(--efintel-blue);"></i> Informes y Rendiciones de Gastos</h2>
|
||||
<p>Agrupación de boletas/facturas por comisiones de servicio, viajes a faena y cajas chicas.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'expense_report_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Nueva Rendición
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Título / Propósito</th>
|
||||
<th>Responsable</th>
|
||||
<th>Proyecto</th>
|
||||
<th>Fechas</th>
|
||||
<th>Monto Total (CLP)</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for rep in reports %}
|
||||
<tr>
|
||||
<td><strong>{{ rep.report_code }}</strong></td>
|
||||
<td>{{ rep.title }}</td>
|
||||
<td>{{ rep.user.username }}</td>
|
||||
<td>{{ rep.project.name }}</td>
|
||||
<td>{{ rep.start_date }} al {{ rep.end_date }}</td>
|
||||
<td>${{ rep.total_amount|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td><span class="badge badge-blue">{{ rep.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay informes de rendición registrados. <a href="{% url 'expense_report_create' %}">Crear el primero</a>.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -391,17 +391,23 @@
|
||||
<div id="tab-config" class="tab-content">
|
||||
<div style="margin-bottom: 1.5rem;">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 0.25rem;">Configuración General del Sistema</h2>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Parámetros y límites globales del ERP.</p>
|
||||
<p style="color: var(--efintel-gray); font-size: 0.9rem;">Parámetros de Impuestos y Tasas globales del ERP.</p>
|
||||
</div>
|
||||
<div style="max-width: 500px; display: flex; flex-direction: column; gap: 1.5rem;">
|
||||
<div class="data-card" style="padding: 2rem;">
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem; margin-bottom: 0.5rem;">Presupuesto General Asignado</div>
|
||||
<div style="font-size: 2.5rem; font-weight: 700; color: var(--efintel-petroleum);">${{ settings.global_budget|floatformat:"0"|intcomma|default:"100000" }}</div>
|
||||
<div style="max-width: 600px; display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem;">
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 0.5rem; font-weight:600;">Tasa de IVA (% Dinámico SII)</div>
|
||||
<div style="font-size: 2.2rem; font-weight: 700; color: var(--efintel-petroleum);">{{ settings.iva_rate|default:"19.00" }}%</div>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'settings_edit' %}" class="btn-primary" style="text-decoration: none;">Modificar Presupuesto General</a>
|
||||
<div class="data-card" style="padding: 1.5rem;">
|
||||
<div style="color: var(--text-secondary); font-size: 0.85rem; margin-bottom: 0.5rem; font-weight:600;">Tasa de Cambio USD / CLP</div>
|
||||
<div style="font-size: 2.2rem; font-weight: 700; color: var(--efintel-petroleum);">${{ settings.usd_exchange_rate|floatformat:"0"|default:"950" }}</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="margin-top: 1.5rem;">
|
||||
<a href="{% url 'settings_edit' %}" class="btn-primary" style="text-decoration: none;">
|
||||
<i data-lucide="sliders" style="width:16px;"></i> Modificar Parámetros del Sistema
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Pestaña Bitácora -->
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<!-- Header -->
|
||||
<div class="view-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 2rem;">
|
||||
<div>
|
||||
<div style="display:flex; align-items:center; gap:0.5rem;">
|
||||
<span class="badge badge-blue" style="font-family:var(--font-mono); font-size:0.8rem;">{{ project.code }}</span>
|
||||
<h2 style="margin:0; color:var(--efintel-petroleum); font-size:1.5rem;">{{ project.name }}</h2>
|
||||
<span class="badge {% if project.status == 'ACTIVO' %}badge-emerald{% elif project.status == 'PAUSADA' %}badge-amber{% elif project.status == 'BORRADOR' %}badge-gray{% else %}badge-blue{% endif %}">
|
||||
{{ project.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
<p style="margin-top:0.25rem; font-size:0.9rem; color:var(--efintel-gray);">
|
||||
Director de Proyecto: <strong>{{ project.manager.username|default:"Sin Director Asignado" }}</strong> |
|
||||
Tipo: <strong>{{ project.get_type_display }}</strong> |
|
||||
Categoría: <strong>{{ project.category.name|default:"General I+D+i" }}</strong>
|
||||
</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<a href="{% url 'project_list' %}" class="btn-outline" style="text-decoration:none;">← Volver a Lista</a>
|
||||
{% if is_admin or is_pm %}
|
||||
<a href="{% url 'project_add_phase' project.id %}" class="btn-primary" style="text-decoration:none; background:#0284c7;">
|
||||
<i data-lucide="plus-circle" style="width:14px;"></i> + Agregar Fase de Pago
|
||||
</a>
|
||||
<a href="{% url 'project_edit' project.id %}" class="btn-secondary" style="text-decoration:none;">
|
||||
<i data-lucide="pencil" style="width:14px;"></i> Editar
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requerimiento 9: Tarjetas KPI de Finanzas del Proyecto -->
|
||||
<div style="display: grid; grid-template-columns: repeat(5, 1fr); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div class="data-card" style="padding: 1.25rem;">
|
||||
<div style="font-size:0.75rem; font-weight:600; color:var(--efintel-gray); margin-bottom:0.25rem;">PRESUPUESTO ASIGNADO</div>
|
||||
<div style="font-size:1.3rem; font-weight:800; font-family:var(--font-mono); color:var(--efintel-petroleum);">
|
||||
${{ project.assigned_budget_clp|floatformat:"0"|intcomma }} CLP
|
||||
</div>
|
||||
<div style="font-size:0.7rem; color:var(--efintel-gray); margin-top:0.25rem;">Presupuesto CLP</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="padding: 1.25rem;">
|
||||
<div style="font-size:0.75rem; font-weight:600; color:var(--efintel-gray); margin-bottom:0.25rem;">FORMA DE PAGO</div>
|
||||
<div style="font-size:0.95rem; font-weight:700; color:var(--efintel-dark); line-height:1.2; margin-top:0.25rem;">
|
||||
{{ project.payment_method }}
|
||||
</div>
|
||||
<div style="font-size:0.7rem; color:var(--efintel-gray); margin-top:0.25rem;">Condición Pactada</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="padding: 1.25rem; border-left:4px solid #16a34a;">
|
||||
<div style="font-size:0.75rem; font-weight:600; color:#16a34a; margin-bottom:0.25rem;">INGRESOS TOTALES</div>
|
||||
<div style="font-size:1.3rem; font-weight:800; font-family:var(--font-mono); color:#16a34a;">
|
||||
+${{ total_income|floatformat:"0"|intcomma }} CLP
|
||||
</div>
|
||||
<div style="font-size:0.7rem; color:var(--efintel-gray); margin-top:0.25rem;">Facturado / Cobrado</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="padding: 1.25rem; border-left:4px solid #dc2626;">
|
||||
<div style="font-size:0.75rem; font-weight:600; color:#dc2626; margin-bottom:0.25rem;">EGRESOS ACUMULADOS</div>
|
||||
<div style="font-size:1.3rem; font-weight:800; font-family:var(--font-mono); color:#dc2626;">
|
||||
-${{ total_expenses|floatformat:"0"|intcomma }} CLP
|
||||
</div>
|
||||
<div style="font-size:0.7rem; color:var(--efintel-gray); margin-top:0.25rem;">Erogaciones Liberadas</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="padding: 1.25rem; border-left:4px solid {% if net_balance >= 0 %}#16a34a{% else %}#dc2626{% endif %}; background:{% if net_balance >= 0 %}#f0fdf4{% else %}#fef2f2{% endif %};">
|
||||
<div style="font-size:0.75rem; font-weight:700; color:{% if net_balance >= 0 %}#15803d{% else %}#b91c1c{% endif %}; margin-bottom:0.25rem;">SALDO NETO (A FAVOR)</div>
|
||||
<div style="font-size:1.3rem; font-weight:800; font-family:var(--font-mono); color:{% if net_balance >= 0 %}#15803d{% else %}#b91c1c{% endif %};">
|
||||
{% if net_balance >= 0 %}+{% endif %}${{ net_balance|floatformat:"0"|intcomma }} CLP
|
||||
</div>
|
||||
<div style="font-size:0.7rem; color:{% if net_balance >= 0 %}#166534{% else %}#991b1b{% endif %}; margin-top:0.25rem;">
|
||||
{% if net_balance >= 0 %}🟢 Saldo a Favor Positivo{% else %}🔴 Déficit / Cuenta en Rojo{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requerimiento 10: Tabla de Pago de Fases con Saldo a Favor (+ / -) -->
|
||||
<div class="data-card" style="margin-bottom: 2rem;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center; margin-bottom:1rem; padding-bottom:0.75rem; border-bottom:1px solid #e2e8f0;">
|
||||
<h3 style="margin:0; color:var(--efintel-petroleum); font-size:1.1rem; display:flex; align-items:center; gap:0.5rem;">
|
||||
<i data-lucide="layers" style="color:var(--efintel-blue);"></i> Fases de Pago e Hitos del Proyecto
|
||||
</h3>
|
||||
{% if is_admin or is_pm %}
|
||||
<a href="{% url 'project_add_phase' project.id %}" class="btn-primary" style="padding:0.35rem 0.8rem; font-size:0.8rem; text-decoration:none; background:#0284c7;">
|
||||
+ Agregar Nueva Fase
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>N° Fase</th>
|
||||
<th>Nombre del Hito</th>
|
||||
<th>Fecha Pactada</th>
|
||||
<th>Monto Pactado (CLP)</th>
|
||||
<th>Ingresos (Cobrado)</th>
|
||||
<th>Egresos (Gastado)</th>
|
||||
<th>Saldo a Favor (Positivo / Negativo)</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for phase in phases %}
|
||||
<tr>
|
||||
<td style="font-weight:700; text-align:center;">Fase {{ phase.phase_number }}</td>
|
||||
<td style="font-weight:600; color:var(--efintel-dark);">
|
||||
{{ phase.name }}
|
||||
{% if phase.description %}<br><span style="font-size:0.75rem; color:var(--efintel-gray); font-weight:normal;">{{ phase.description }}</span>{% endif %}
|
||||
</td>
|
||||
<td style="font-size:0.8rem;">{{ phase.due_date|date:"d/m/Y"|default:"Pendiente" }}</td>
|
||||
<td style="font-family:var(--font-mono); font-weight:600;">${{ phase.planned_amount_clp|floatformat:"0"|intcomma }}</td>
|
||||
<td style="font-family:var(--font-mono); color:#16a34a; font-weight:700;">+${{ phase.income_amount_clp|floatformat:"0"|intcomma }}</td>
|
||||
<td style="font-family:var(--font-mono); color:#dc2626; font-weight:700;">-${{ phase.expense_amount_clp|floatformat:"0"|intcomma }}</td>
|
||||
<!-- Requerimiento 10: Reflejar saldo a favor (números negativos y positivos) -->
|
||||
<td>
|
||||
<span style="font-family:var(--font-mono); font-weight:800; padding:0.25rem 0.6rem; border-radius:4px; font-size:0.85rem; {% if phase.balance_clp >= 0 %}background:#dcfce7; color:#15803d;{% else %}background:#fee2e2; color:#b91c1c;{% endif %}">
|
||||
{% if phase.balance_clp >= 0 %}+{% endif %}${{ phase.balance_clp|floatformat:"0"|intcomma }} CLP
|
||||
</span>
|
||||
</td>
|
||||
<td>
|
||||
{% if phase.is_completed %}
|
||||
<span class="badge badge-emerald"><i data-lucide="check" style="width:12px;"></i> Completada</span>
|
||||
{% else %}
|
||||
<span class="badge badge-amber"><i data-lucide="clock" style="width:12px;"></i> En Proceso</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="8" style="text-align:center; padding:2rem; color:var(--efintel-gray);">
|
||||
No se han definido fases de pago para este proyecto.
|
||||
{% if is_admin or is_pm %}<a href="{% url 'project_add_phase' project.id %}">Haga clic aquí para agregar la primera fase.</a>{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Tabla de Erogaciones Asociadas al Proyecto -->
|
||||
<div class="data-card">
|
||||
<h3 style="margin:0 0 1rem 0; color:var(--efintel-petroleum); font-size:1.1rem; display:flex; align-items:center; gap:0.5rem;">
|
||||
<i data-lucide="file-text" style="color:var(--efintel-blue);"></i> Erogaciones Imputadas a este Proyecto ({{ expenses.count }})
|
||||
</h3>
|
||||
<div class="data-table-wrapper">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Fecha</th>
|
||||
<th>Categoría</th>
|
||||
<th>Detalle</th>
|
||||
<th>Responsable</th>
|
||||
<th>Monto Neto (CLP)</th>
|
||||
<th>Monto Total (CLP)</th>
|
||||
<th>Estado Pago</th>
|
||||
<th>Evidencias</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for exp in expenses %}
|
||||
<tr>
|
||||
<td style="font-family:var(--font-mono); font-size:0.75rem;">{{ exp.expense_code|truncatechars:12 }}</td>
|
||||
<td style="font-size:0.8rem;">{{ exp.date|date:"d/m/Y" }}</td>
|
||||
<td><span class="badge badge-blue" style="font-size:0.7rem;">{{ exp.category.name|default:"General" }}</span></td>
|
||||
<td style="font-weight:600; font-size:0.85rem;">{{ exp.description|truncatechars:40 }}</td>
|
||||
<td style="font-size:0.8rem;">{{ exp.responsible_person.username|default:exp.user.username }}</td>
|
||||
<td style="font-family:var(--font-mono); font-weight:600;">${{ exp.amount_clp|floatformat:"0"|intcomma }}</td>
|
||||
<td style="font-family:var(--font-mono); font-weight:700;">${{ exp.total_amount_clp|floatformat:"0"|intcomma }}</td>
|
||||
<td>
|
||||
{% if exp.approval_status == 'LIBERADO_PAGADO' %}
|
||||
<span class="badge badge-emerald">Pagado</span>
|
||||
{% elif exp.approval_status == 'SOLICITADO_FINANZAS' %}
|
||||
<span class="badge badge-blue">Finanzas</span>
|
||||
{% else %}
|
||||
<span class="badge badge-amber">Revisión</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if exp.documents.all %}
|
||||
{% for doc in exp.documents.all %}
|
||||
<button type="button" onclick="previewDocument('{{ doc.file.url }}', '{{ doc.title|escapejs }}', {% if doc.is_pdf %}true{% else %}false{% endif %})" class="btn-outline" style="padding:0.2rem 0.4rem; font-size:0.65rem;" title="Previsualizar Evidencia">
|
||||
<i data-lucide="eye" style="width:10px;"></i> Ver
|
||||
</button>
|
||||
{% endfor %}
|
||||
{% else %}
|
||||
<span style="font-size:0.75rem; color:var(--efintel-gray);">Sin adjunto</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="9" style="text-align:center; padding:2rem; color:var(--efintel-gray);">No hay erogaciones registradas para este proyecto.</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -11,39 +11,71 @@
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<!-- Requerimiento 8: Creación Progresiva Wizard -->
|
||||
<div style="margin-bottom: 2rem; display:flex; justify-content:space-between; background:#f8fafc; padding:1rem; border-radius:6px; border:1px solid #e2e8f0;">
|
||||
<div style="display:flex; gap:0.5rem; align-items:center;">
|
||||
<span class="badge badge-blue">Paso 1: Clasificación & Director</span>
|
||||
<span class="badge badge-gray">→ Paso 2: Presupuesto & Condición Comercial</span>
|
||||
</div>
|
||||
<span style="font-size:0.75rem; color:var(--efintel-gray);">Estado inicial: Borrador o Activo</span>
|
||||
</div>
|
||||
|
||||
<h3 style="font-size:1rem; color:var(--efintel-petroleum); margin-bottom:1rem;">1. Identificación y Responsable del Proyecto</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código</label>
|
||||
<label for="{{ form.code.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Código Correlativo</label>
|
||||
{{ form.code }}
|
||||
{% if form.code.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>
|
||||
{% endif %}
|
||||
{% if form.code.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.code.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.name.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre del Proyecto / Centro</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>
|
||||
{% endif %}
|
||||
{% if form.name.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Centro</label>
|
||||
{{ form.type }}
|
||||
{% if form.type.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.type.errors }}</div>
|
||||
{% endif %}
|
||||
<label for="{{ form.manager.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Director / Manager</label>
|
||||
{{ form.manager }}
|
||||
{% if form.manager.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.manager.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.type.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Tipo de Centro</label>
|
||||
{{ form.type }}
|
||||
{% if form.type.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.type.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Estado del Proyecto</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.status.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Requerimiento 9: Presupuesto Asignado, Forma de Pago e Ingresos Totales -->
|
||||
<h3 style="font-size:1rem; color:var(--efintel-petroleum); margin-top:2rem; margin-bottom:1rem; border-top:1px solid #e2e8f0; padding-top:1.5rem;">2. Presupuesto, Forma de Pago e Ingresos Pactados</h3>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.assigned_budget_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Presupuesto Asignado (CLP)</label>
|
||||
{{ form.assigned_budget_clp }}
|
||||
{% if form.assigned_budget_clp.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.assigned_budget_clp.errors }}</div>
|
||||
{% endif %}
|
||||
{% if form.assigned_budget_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.assigned_budget_clp.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.payment_method.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Forma de Pago</label>
|
||||
{{ form.payment_method }}
|
||||
{% if form.payment_method.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.payment_method.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.total_income_clp.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: #16a34a;">Ingresos Totales Contratados (CLP)</label>
|
||||
{{ form.total_income_clp }}
|
||||
{% if form.total_income_clp.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.total_income_clp.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -51,48 +83,25 @@
|
||||
<div class="form-group">
|
||||
<label for="{{ form.category.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Categoría del Proyecto (Especialidad)</label>
|
||||
{{ form.category }}
|
||||
{% if form.category.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>
|
||||
{% endif %}
|
||||
{% if form.category.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.category.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.status.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Estado del Proyecto</label>
|
||||
{{ form.status }}
|
||||
{% if form.status.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.status.errors }}</div>
|
||||
{% endif %}
|
||||
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Portafolio Padre (Opcional)</label>
|
||||
{{ form.parent }}
|
||||
{% if form.parent.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label for="{{ form.parent.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Portafolio Padre (Opcional)</label>
|
||||
<p style="font-size: 0.75rem; color: var(--text-secondary); margin-bottom: 0.25rem;">Dejar en blanco si este proyecto actúa como un Portafolio principal.</p>
|
||||
{{ form.parent }}
|
||||
{% if form.parent.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.parent.errors }}</div>
|
||||
{% endif %}
|
||||
</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);">Descripción</label>
|
||||
<label for="{{ form.description.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción / Objetivos</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: 2rem;">
|
||||
<label for="{{ form.manager.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Encargado / Responsable del Proyecto</label>
|
||||
{{ form.manager }}
|
||||
{% if form.manager.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.manager.errors }}</div>
|
||||
{% endif %}
|
||||
{% if form.description.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.description.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proyecto</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem; background:var(--efintel-blue);">Guardar Proyecto</button>
|
||||
<a href="{% url 'project_list' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -2,47 +2,122 @@
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<div class="view-header">
|
||||
<div class="view-header" style="display: flex; justify-content: space-between; align-items: center; margin-bottom: 1.5rem;">
|
||||
<div class="view-title">
|
||||
<h2>Proyectos</h2>
|
||||
<p>Monitoreo de Centros de Costo</p>
|
||||
<h2 style="margin:0; color:var(--efintel-petroleum); display:flex; align-items:center; gap:0.5rem;">
|
||||
<i data-lucide="folder-kanban" style="color:var(--efintel-blue);"></i> Control de Proyectos y Centros de Costo
|
||||
</h2>
|
||||
<p style="margin-top:0.25rem; font-size:0.9rem; color:var(--efintel-gray);">Supervisión de presupuesto, forma de pago, ingresos, egresos y saldos por proyecto.</p>
|
||||
</div>
|
||||
<div>
|
||||
{% if is_admin or is_pm %}
|
||||
<a href="{% url 'project_create' %}" class="btn-primary" style="text-decoration: none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Nuevo Proyecto
|
||||
</a>
|
||||
{% endif %}
|
||||
</div>
|
||||
<a href="{% url 'project_create' %}" class="btn-primary">Nuevo Proyecto / Centro</a>
|
||||
</div>
|
||||
|
||||
<div class="project-grid">
|
||||
<!-- Requerimiento 2: Conteo exacto por estado -->
|
||||
<div style="display:grid; grid-template-columns: repeat(4, 1fr); gap: 1rem; margin-bottom: 2rem;">
|
||||
<div class="data-card" style="padding: 1rem; text-align: center; border-left: 4px solid #16a34a;">
|
||||
<div style="font-size: 0.75rem; color: var(--efintel-gray); font-weight:600;">PROYECTOS ACTIVOS</div>
|
||||
<div style="font-size: 1.8rem; font-weight: 800; color: #16a34a;">{{ project_count_active }}</div>
|
||||
</div>
|
||||
<div class="data-card" style="padding: 1rem; text-align: center; border-left: 4px solid #d97706;">
|
||||
<div style="font-size: 0.75rem; color: var(--efintel-gray); font-weight:600;">EN PAUSA</div>
|
||||
<div style="font-size: 1.8rem; font-weight: 800; color: #d97706;">{{ project_count_paused }}</div>
|
||||
</div>
|
||||
<div class="data-card" style="padding: 1rem; text-align: center; border-left: 4px solid #0284c7;">
|
||||
<div style="font-size: 0.75rem; color: var(--efintel-gray); font-weight:600;">TERMINADOS</div>
|
||||
<div style="font-size: 1.8rem; font-weight: 800; color: #0284c7;">{{ project_count_completed }}</div>
|
||||
</div>
|
||||
<div class="data-card" style="padding: 1rem; text-align: center; border-left: 4px solid #64748b;">
|
||||
<div style="font-size: 0.75rem; color: var(--efintel-gray); font-weight:600;">TOTAL EN SISTEMA</div>
|
||||
<div style="font-size: 1.8rem; font-weight: 800; color: #334155;">{{ project_count_total }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="margin-bottom: 2rem;">
|
||||
<form method="get" id="filter-form" style="display: flex; gap: 1rem; align-items: flex-end; flex-wrap: wrap;">
|
||||
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
|
||||
<label for="search" style="font-size: 0.8rem; font-weight: 600;">Buscar por nombre o código</label>
|
||||
<input type="text" name="search" id="search" value="{{ request.GET.search }}" placeholder="Ej: PROY-2026-001" style="width: 100%; padding: 0.65rem; border: 1px solid var(--efintel-border); border-radius: 4px; outline: none; font-family: 'Inter';">
|
||||
</div>
|
||||
<div class="form-group" style="margin-bottom: 0; flex: 1; min-width: 200px;">
|
||||
<label for="status" style="font-size: 0.8rem; font-weight: 600;">Estado</label>
|
||||
<select name="status" id="status" style="width: 100%; padding: 0.65rem; border: 1px solid var(--efintel-border); border-radius: 4px; outline: none; font-family: 'Inter';" onchange="document.getElementById('filter-form').submit();">
|
||||
<option value="">Todos los Estados</option>
|
||||
<option value="ACTIVO" {% if request.GET.status == 'ACTIVO' %}selected{% endif %}>Activo</option>
|
||||
<option value="PAUSADA" {% if request.GET.status == 'PAUSADA' %}selected{% endif %}>En Pausa</option>
|
||||
<option value="TERMINADO" {% if request.GET.status == 'TERMINADO' %}selected{% endif %}>Terminado</option>
|
||||
<option value="BORRADOR" {% if request.GET.status == 'BORRADOR' %}selected{% endif %}>Borrador</option>
|
||||
</select>
|
||||
</div>
|
||||
<button type="submit" class="btn-outline" style="height: 42px;"><i data-lucide="filter" style="width:16px;"></i> Filtrar</button>
|
||||
{% if request.GET.search or request.GET.status %}
|
||||
<a href="{% url 'project_list' %}" class="btn-secondary" style="height: 42px; line-height: 24px; text-decoration: none;">Limpiar</a>
|
||||
{% endif %}
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="project-grid" style="display: grid; grid-template-columns: repeat(auto-fill, minmax(350px, 1fr)); gap: 1.5rem;">
|
||||
{% for project in projects %}
|
||||
<div class="glass-card project-card">
|
||||
<div class="project-header">
|
||||
<div class="project-title">{{ project.name }}</div>
|
||||
</div>
|
||||
<div style="color: var(--text-secondary); font-size: 0.9rem;">
|
||||
<strong>Gestor:</strong> {{ project.manager.username|default:"No asignado" }}
|
||||
</div>
|
||||
<div class="project-stats">
|
||||
<div style="display: flex; flex-direction: column; gap: 0.5rem; width: 100%;">
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Presupuesto Asignado</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-petroleum);">
|
||||
${{ project.assigned_budget_clp|floatformat:"0"|intcomma }}
|
||||
<div class="data-card project-card" style="padding: 1.5rem; display:flex; flex-direction:column; justify-content:space-between; position:relative; background:white;">
|
||||
<div>
|
||||
<div style="display:flex; justify-content:space-between; align-items:flex-start; margin-bottom:0.75rem;">
|
||||
<div>
|
||||
<span class="badge badge-blue" style="font-family:var(--font-mono); font-size:0.7rem;">{{ project.code }}</span>
|
||||
<h3 style="margin:0.25rem 0 0 0; color:var(--efintel-petroleum); font-size:1.1rem; font-weight:700;">
|
||||
<a href="{% url 'project_detail' project.id %}" style="text-decoration:none; color:inherit;">{{ project.name }}</a>
|
||||
</h3>
|
||||
</div>
|
||||
<span class="badge {% if project.status == 'ACTIVO' %}badge-emerald{% elif project.status == 'PAUSADA' %}badge-amber{% elif project.status == 'BORRADOR' %}badge-gray{% else %}badge-blue{% endif %}" style="font-size:0.7rem;">
|
||||
{{ project.get_status_display }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div style="color: var(--text-secondary); font-size: 0.8rem; margin-bottom: 1rem;">
|
||||
<strong>Director:</strong> {{ project.manager.username|default:"No asignado" }} |
|
||||
<strong>Forma Pago:</strong> {{ project.payment_method|truncatechars:20 }}
|
||||
</div>
|
||||
|
||||
<!-- Requerimiento 9: Presupuesto, Ingresos, Egresos y Saldo -->
|
||||
<div style="background:#f8fafc; padding:1rem; border-radius:6px; border:1px solid #e2e8f0; display:flex; flex-direction:column; gap:0.4rem;">
|
||||
<div style="display: flex; justify-content: space-between; font-size:0.8rem;">
|
||||
<span style="color: var(--efintel-gray);">Presupuesto Asignado:</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono);">${{ project.assigned_budget_clp|floatformat:"0"|intcomma }}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size:0.8rem;">
|
||||
<span style="color: #16a34a;">Ingresos Totales:</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: #16a34a;">+${{ project.total_income_clp|floatformat:"0"|intcomma }}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size:0.8rem;">
|
||||
<span style="color: #dc2626;">Egresos Acumulados:</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: #dc2626;">-${{ project.total_expenses_clp|floatformat:"0"|intcomma }}</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; font-size:0.85rem; padding-top:0.4rem; border-top:1px dashed #cbd5e1; margin-top:0.2rem;">
|
||||
<span style="font-weight: 700; color: var(--efintel-dark);">Saldo Neto:</span>
|
||||
<span style="font-weight: 800; font-family: var(--font-mono); {% if project.balance_clp >= 0 %}color:#16a34a;{% else %}color:#dc2626;{% endif %}">
|
||||
{% if project.balance_clp >= 0 %}+{% endif %}${{ project.balance_clp|floatformat:"0"|intcomma }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between;">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Costo Real Acumulado</span>
|
||||
<span style="font-weight: 700; font-family: var(--font-mono); color: var(--efintel-danger);">
|
||||
${{ project.actual_cost_clp|floatformat:"0"|intcomma }}
|
||||
</span>
|
||||
</div>
|
||||
<div style="display: flex; justify-content: space-between; margin-top: 0.5rem; padding-top: 0.5rem; border-top: 1px solid var(--efintel-border);">
|
||||
<span style="font-size: 0.8rem; color: var(--efintel-gray);">Nº Erogaciones (Facturas)</span>
|
||||
<span style="font-weight: 700;">{{ project.expense_count|default:"0" }}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="margin-top: 1.25rem; display:flex; justify-content:space-between; align-items:center; border-top:1px solid #f1f5f9; padding-top:0.75rem;">
|
||||
<span style="font-size:0.75rem; color:var(--efintel-gray);">{{ project.expense_count }} Erogaciones</span>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
{% if is_admin or is_pm %}
|
||||
<a href="{% url 'project_edit' project.id %}" class="btn-secondary" style="padding:0.3rem 0.6rem; font-size:0.75rem; text-decoration:none;" title="Editar Proyecto"><i data-lucide="pencil" style="width:12px;"></i></a>
|
||||
{% endif %}
|
||||
<a href="{% url 'project_detail' project.id %}" class="btn-primary" style="padding:0.3rem 0.8rem; font-size:0.75rem; text-decoration:none; background:var(--efintel-blue);">Ver Detalle & Fases →</a>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% empty %}
|
||||
<div class="glass-card" style="padding: 2rem; text-align: center; grid-column: 1 / -1;">
|
||||
No hay proyectos registrados.
|
||||
<div class="data-card" style="padding: 2rem; text-align: center; grid-column: 1 / -1;">
|
||||
No hay proyectos registrados en este perfil.
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<div class="form-container" style="max-width: 700px; 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);">
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 1.5rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; gap: 0.5rem;">
|
||||
<i data-lucide="layers"></i>
|
||||
Fase de Pago / Hito para {{ project.code }} - {{ project.name }}
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 3fr; gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">N° Fase</label>
|
||||
{{ form.phase_number }}
|
||||
{% if form.phase_number.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem;">{{ form.phase_number.errors }}</div>{% endif %}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Nombre del Hito / Fase</label>
|
||||
{{ form.name }}
|
||||
{% if form.name.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem;">{{ form.name.errors }}</div>{% endif %}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Monto Pactado (CLP)</label>
|
||||
{{ form.planned_amount_clp }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: #16a34a;">Ingreso Cobrado (CLP)</label>
|
||||
{{ form.income_amount_clp }}
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: #dc2626;">Egreso Gastado (CLP)</label>
|
||||
{{ form.expense_amount_clp }}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1rem; margin-bottom: 1.25rem;">
|
||||
<div class="form-group">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Fecha Pactada / Estimada</label>
|
||||
{{ form.due_date }}
|
||||
</div>
|
||||
<div class="form-group" style="display:flex; align-items:center; margin-top:1.5rem;">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark); display:flex; align-items:center; gap:0.5rem; cursor:pointer;">
|
||||
{{ form.is_completed }} ¿Fase Completada?
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom: 1.5rem;">
|
||||
<label style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">Descripción de Entregables / Hitos</label>
|
||||
{{ form.description }}
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; justify-content: flex-end; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<a href="{% url 'project_detail' project.id %}" class="btn-outline" style="padding: 0.6rem 1.5rem; text-decoration: none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; background: var(--efintel-blue);">Guardar Fase de Pago</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -8,11 +8,20 @@
|
||||
<form method="post">
|
||||
{% csrf_token %}
|
||||
|
||||
<div class="form-group">
|
||||
<label for="{{ form.global_budget.id_for_label }}">Presupuesto General Asignado ($)</label>
|
||||
{{ form.global_budget }}
|
||||
{% if form.global_budget.errors %}
|
||||
<div style="color: var(--danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.global_budget.errors }}</div>
|
||||
<div class="form-group" style="margin-bottom:1.5rem;">
|
||||
<label for="{{ form.iva_rate.id_for_label }}">Tasa de IVA (% Dinámico Configurable)</label>
|
||||
{{ form.iva_rate }}
|
||||
{% if form.iva_rate.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.iva_rate.errors }}</div>
|
||||
{% endif %}
|
||||
<small style="color:var(--efintel-gray);">Porcentaje oficial para cálculo de impuesto IVA en facturas y órdenes de compra (Ej: 19.00).</small>
|
||||
</div>
|
||||
|
||||
<div class="form-group" style="margin-bottom:1.5rem;">
|
||||
<label for="{{ form.usd_exchange_rate.id_for_label }}">Tasa de Cambio USD / CLP por Defecto</label>
|
||||
{{ form.usd_exchange_rate }}
|
||||
{% if form.usd_exchange_rate.errors %}
|
||||
<div style="color: var(--efintel-danger); font-size: 0.8rem; margin-top: 0.25rem;">{{ form.usd_exchange_rate.errors }}</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
|
||||
|
||||
@@ -3,12 +3,30 @@
|
||||
{% 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);">
|
||||
<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 %}Editar Proveedor{% else %}Nuevo Proveedor{% endif %}
|
||||
|
||||
<!-- Banner Requerimiento 6: Plan de mitigación de errores para registro de proveedores -->
|
||||
<div id="draft-alert-banner" style="display:none; margin-bottom:1.5rem; background:#eff6ff; border:1px solid #bfdbfe; border-left:4px solid #2563eb; padding:1rem; border-radius:6px;">
|
||||
<div style="display:flex; justify-content:space-between; align-items:center;">
|
||||
<div>
|
||||
<strong style="color:#1e40af; font-size:0.9rem;">Borrador Detectado en Almacenamiento Local (Autosave Mitigación)</strong>
|
||||
<p style="margin:0.25rem 0 0 0; color:#1e3a8a; font-size:0.8rem;">Se detectaron datos no guardados de una sesión anterior. ¿Desea restaurarlos?</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<button type="button" onclick="restoreVendorDraft()" class="btn-primary" style="padding:0.3rem 0.8rem; font-size:0.8rem; background:#2563eb;">Restaurar</button>
|
||||
<button type="button" onclick="discardVendorDraft()" class="btn-secondary" style="padding:0.3rem 0.8rem; font-size:0.8rem;">Descartar</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h2 style="color: var(--efintel-petroleum); margin-bottom: 2rem; border-bottom: 2px solid var(--efintel-light); padding-bottom: 1rem; display: flex; align-items: center; justify-content:space-between;">
|
||||
<span style="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 %}Editar Proveedor{% else %}Nuevo Proveedor (Registro Seguro){% endif %}
|
||||
</span>
|
||||
<span class="badge badge-emerald" style="font-size:0.75rem;"><i data-lucide="shield-check" style="width:12px;"></i> Autosave / Anti-Pérdida Activo</span>
|
||||
</h2>
|
||||
|
||||
<form method="post">
|
||||
<form method="post" id="vendor-form">
|
||||
{% csrf_token %}
|
||||
|
||||
<h3 style="margin-bottom: 1rem; color: var(--efintel-dark); font-size: 1rem;">Información General</h3>
|
||||
@@ -16,7 +34,13 @@
|
||||
<div style="display: grid; grid-template-columns: 1fr 2fr; gap: 1.5rem; margin-bottom: 1.5rem;">
|
||||
<div class="form-group">
|
||||
<label for="{{ form.rut.id_for_label }}" style="font-weight: 600; font-size: 0.85rem; color: var(--efintel-dark);">RUT Proveedor</label>
|
||||
{{ form.rut }}
|
||||
<div style="display:flex; gap:0.3rem;">
|
||||
{{ form.rut }}
|
||||
<button type="button" onclick="consultarSIIVendor()" class="btn-outline" style="padding:0.4rem 0.7rem; font-size:0.75rem; white-space:nowrap; background:#f0f9ff; border-color:#7dd3fc; color:#0369a1;" title="Validar y autocompletar con la API del SII">
|
||||
<i data-lucide="search" style="width:12px;"></i> Consultar SII
|
||||
</button>
|
||||
</div>
|
||||
<span id="sii-status-text" style="font-size:0.7rem; color:var(--efintel-gray); margin-top:0.25rem; display:block;"></span>
|
||||
{% if form.rut.errors %}<div style="color: var(--efintel-danger); font-size: 0.75rem; margin-top: 4px;">{{ form.rut.errors }}</div>{% endif %}
|
||||
</div>
|
||||
|
||||
@@ -70,7 +94,7 @@
|
||||
</div>
|
||||
|
||||
<div style="display: flex; gap: 1rem; padding-top: 1.5rem; border-top: 1px solid var(--efintel-light);">
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem;">Guardar Proveedor</button>
|
||||
<button type="submit" class="btn-primary" style="padding: 0.6rem 2rem; font-size: 0.9rem; background:var(--efintel-blue);">Guardar Proveedor</button>
|
||||
<a href="{% url 'gestion_panel' %}" class="btn-secondary" style="padding: 0.6rem 2rem; font-size: 0.9rem; text-decoration: none;">Cancelar</a>
|
||||
</div>
|
||||
</form>
|
||||
@@ -81,5 +105,99 @@
|
||||
document.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
el.classList.add('form-control');
|
||||
});
|
||||
|
||||
// Requerimiento 6: Autosave en localStorage para mitigación de errores de formulario
|
||||
const DRAFT_KEY = 'vendor_form_draft_{{ form.instance.pk|default:"new" }}';
|
||||
const vendorForm = document.getElementById('vendor-form');
|
||||
const draftBanner = document.getElementById('draft-alert-banner');
|
||||
|
||||
function saveDraft() {
|
||||
const formData = {};
|
||||
const elements = vendorForm.querySelectorAll('input, select, textarea');
|
||||
elements.forEach(el => {
|
||||
if (el.name && el.name !== 'csrfmiddlewaretoken') {
|
||||
formData[el.name] = el.value;
|
||||
}
|
||||
});
|
||||
localStorage.setItem(DRAFT_KEY, JSON.stringify(formData));
|
||||
}
|
||||
|
||||
function checkForDraft() {
|
||||
const saved = localStorage.getItem(DRAFT_KEY);
|
||||
if (saved) {
|
||||
try {
|
||||
const data = JSON.parse(saved);
|
||||
// Verify if saved data has actual input content
|
||||
const hasValues = Object.values(data).some(v => v && v.trim() !== '');
|
||||
if (hasValues && draftBanner) {
|
||||
draftBanner.style.display = 'block';
|
||||
}
|
||||
} catch(e){}
|
||||
}
|
||||
}
|
||||
|
||||
function restoreVendorDraft() {
|
||||
const saved = localStorage.getItem(DRAFT_KEY);
|
||||
if (saved) {
|
||||
const data = JSON.parse(saved);
|
||||
Object.keys(data).forEach(key => {
|
||||
const el = vendorForm.querySelector(`[name="${key}"]`);
|
||||
if (el) el.value = data[key];
|
||||
});
|
||||
if (draftBanner) draftBanner.style.display = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
function discardVendorDraft() {
|
||||
localStorage.removeItem(DRAFT_KEY);
|
||||
if (draftBanner) draftBanner.style.display = 'none';
|
||||
}
|
||||
|
||||
vendorForm.querySelectorAll('input, select, textarea').forEach(el => {
|
||||
el.addEventListener('input', saveDraft);
|
||||
el.addEventListener('change', saveDraft);
|
||||
});
|
||||
|
||||
vendorForm.addEventListener('submit', function() {
|
||||
localStorage.removeItem(DRAFT_KEY);
|
||||
});
|
||||
|
||||
// Check draft on load
|
||||
checkForDraft();
|
||||
|
||||
// Requerimiento 4: Consulta API SII en vivo para autocompletar RUT y Razón Social
|
||||
function consultarSIIVendor() {
|
||||
const rutInput = document.getElementById('{{ form.rut.id_for_label }}');
|
||||
const statusText = document.getElementById('sii-status-text');
|
||||
if (!rutInput || !rutInput.value.trim()) {
|
||||
alert('Por favor ingrese un RUT para consultar en el SII.');
|
||||
return;
|
||||
}
|
||||
|
||||
statusText.textContent = 'Consultando API SII...';
|
||||
statusText.style.color = '#0284c7';
|
||||
|
||||
fetch(`/api/sii/taxpayer-info/?rut=${encodeURIComponent(rutInput.value.trim())}`)
|
||||
.then(r => r.json())
|
||||
.then(data => {
|
||||
if (data.valid) {
|
||||
rutInput.value = data.rut;
|
||||
const nameInput = document.getElementById('{{ form.name.id_for_label }}');
|
||||
const girosInput = document.getElementById('{{ form.business_line.id_for_label }}');
|
||||
if (nameInput && (!nameInput.value || data.source === 'SII_API')) nameInput.value = data.name;
|
||||
if (girosInput && !girosInput.value) girosInput.value = data.business_line;
|
||||
statusText.textContent = `✓ RUT Validador SII OK (${data.source})`;
|
||||
statusText.style.color = '#16a34a';
|
||||
saveDraft();
|
||||
} else {
|
||||
statusText.textContent = `✗ ${data.error || 'RUT inválido'}`;
|
||||
statusText.style.color = '#dc2626';
|
||||
}
|
||||
})
|
||||
.catch(err => {
|
||||
statusText.textContent = 'Error al consultar API SII.';
|
||||
statusText.style.color = '#dc2626';
|
||||
});
|
||||
}
|
||||
</script>
|
||||
{% endblock %}
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger" style="margin-bottom: 1rem;">
|
||||
{% for err in form.non_field_errors %}
|
||||
<p style="margin:0;">{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-grid">
|
||||
{% for field in form %}
|
||||
<div class="form-group {% if 'summary' in field.name or 'address' in field.name or 'description' in field.name or 'feedback' in field.name %}full-width{% endif %}">
|
||||
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-help">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="form-error">
|
||||
{% for err in field.errors %}
|
||||
<span>{{ err }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endfor %}
|
||||
</div>
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="user-plus" style="color:var(--efintel-blue);"></i> {{ title }}</h2>
|
||||
<p>Alta y modificación de ficha de colaborador en el sistema de remuneraciones EFINTEL.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'payroll:employee_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:850px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'payroll:employee_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="save" style="width:16px;"></i> Guardar Ficha de Colaborador</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,60 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="users" style="color:var(--efintel-blue);"></i> Maestro de Colaboradores — Remuneraciones</h2>
|
||||
<p>Ficha del colaborador, contratos, previsión (AFP/Fonasa/Isapre), y cuenta bancaria de abono.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'payroll:employee_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="user-plus" style="width:16px;"></i> + Nuevo Colaborador
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" x-data="{ search: '' }">
|
||||
<div style="padding:1rem; border-bottom:1px solid var(--border-color);">
|
||||
<input type="text" x-model="search" placeholder="Buscar por Nombre, RUT o Cargo..." class="form-control" style="width:100%; max-width:400px;">
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RUT</th>
|
||||
<th>Colaborador</th>
|
||||
<th>Cargo</th>
|
||||
<th>Tipo Contrato</th>
|
||||
<th>Sueldo Base (CLP)</th>
|
||||
<th>Previsión (AFP / Salud)</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for emp in employees %}
|
||||
<tr x-show="search === '' || '{{ emp.full_name|lower }} {{ emp.rut|lower }} {{ emp.job_title|lower }}'.includes(search.toLowerCase())">
|
||||
<td><strong>{{ emp.rut }}</strong></td>
|
||||
<td>{{ emp.full_name }}</td>
|
||||
<td>{{ emp.job_title }}</td>
|
||||
<td><span class="badge badge-blue">{{ emp.get_contract_type_display }}</span></td>
|
||||
<td>${{ emp.base_salary_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td>{{ emp.get_afp_display }} / {{ emp.get_health_system_display }}</td>
|
||||
<td>
|
||||
<a href="{% url 'payroll:employee_edit' emp.pk %}" class="btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem; text-decoration:none;">
|
||||
Editar Ficha
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay colaboradores registrados. <a href="{% url 'payroll:employee_create' %}">Registrar el primero</a>.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="cpu" style="color:var(--efintel-blue);"></i> {{ title|default:"Motor de Cálculo Mensual de Remuneraciones" }}</h2>
|
||||
<p>Procesamiento de sueldos imponibles, leyes sociales (AFP, Salud, AFC) y retenciones de impuestos.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'payroll:slip_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:750px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'payroll:slip_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="zap" style="width:16px;"></i> Ejecutar Motor de Cálculo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="cpu" style="color:var(--efintel-blue);"></i> Motor de Cálculo Mensual de Remuneraciones</h2>
|
||||
<p>Procesamiento masivo o individual de liquidaciones, leyes sociales (AFP, Fonasa/Isapre) e impuestos.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'payroll:slip_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:750px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'payroll:slip_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="zap" style="width:16px;"></i> Ejecutar Motor de Cálculo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,94 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-check" style="color:var(--efintel-blue);"></i> Liquidaciones de Sueldo Mensuales</h2>
|
||||
<p>Procesamiento mensual de haberes, descuentos, emisión en PDF y libro de remuneraciones (LRE).</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<a href="{% url 'payroll:slip_generate' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="cpu" style="width:16px;"></i> Generar Cálculo Mensual
|
||||
</a>
|
||||
<a href="{% url 'payroll:export_previred' %}" class="btn-primary" style="background:#107c41; text-decoration:none;">
|
||||
<i data-lucide="download" style="width:16px;"></i> Archivo Previred
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" x-data="{ search: '' }">
|
||||
<div style="padding:1rem; border-bottom:1px solid var(--border-color); display:flex; gap:1rem; align-items:flex-end;">
|
||||
<form method="get" style="display:flex; gap:1rem; align-items:flex-end;">
|
||||
<div>
|
||||
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Mes</label>
|
||||
<select name="month" class="form-control" style="min-width:120px;">
|
||||
<option value="">Todos</option>
|
||||
<option value="1" {% if selected_month == '1' %}selected{% endif %}>Enero</option>
|
||||
<option value="2" {% if selected_month == '2' %}selected{% endif %}>Febrero</option>
|
||||
<option value="3" {% if selected_month == '3' %}selected{% endif %}>Marzo</option>
|
||||
<option value="4" {% if selected_month == '4' %}selected{% endif %}>Abril</option>
|
||||
<option value="5" {% if selected_month == '5' %}selected{% endif %}>Mayo</option>
|
||||
<option value="6" {% if selected_month == '6' %}selected{% endif %}>Junio</option>
|
||||
<option value="7" {% if selected_month == '7' %}selected{% endif %}>Julio</option>
|
||||
<option value="8" {% if selected_month == '8' %}selected{% endif %}>Agosto</option>
|
||||
<option value="9" {% if selected_month == '9' %}selected{% endif %}>Septiembre</option>
|
||||
<option value="10" {% if selected_month == '10' %}selected{% endif %}>Octubre</option>
|
||||
<option value="11" {% if selected_month == '11' %}selected{% endif %}>Noviembre</option>
|
||||
<option value="12" {% if selected_month == '12' %}selected{% endif %}>Diciembre</option>
|
||||
</select>
|
||||
</div>
|
||||
<div>
|
||||
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Año</label>
|
||||
<input type="number" name="year" value="{{ selected_year }}" class="form-control" min="2000" max="2100" style="width:100px;">
|
||||
</div>
|
||||
<div>
|
||||
<button type="submit" class="btn-primary" style="padding:0.5rem 1rem;">Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
<div style="flex:1;"></div>
|
||||
<div>
|
||||
<label style="font-size:0.8rem; font-weight:600; color:var(--text-secondary);">Buscar Colaborador</label>
|
||||
<input type="text" x-model="search" placeholder="Nombre o RUT..." class="form-control" style="min-width:250px;">
|
||||
</div>
|
||||
</div>
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Mes/Año</th>
|
||||
<th>Colaborador</th>
|
||||
<th>Haberes Imponibles</th>
|
||||
<th>Descuentos</th>
|
||||
<th>Sueldo Líquido (CLP)</th>
|
||||
<th>Firma Electrónica</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for slip in slips %}
|
||||
<tr x-show="search === '' || '{{ slip.employee.full_name|lower }} {{ slip.employee.rut|lower }}'.includes(search.toLowerCase())">
|
||||
<td><strong>{{ slip.month }}/{{ slip.year }}</strong></td>
|
||||
<td>{{ slip.employee.full_name }}</td>
|
||||
<td>${{ slip.taxable_earnings_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td>${{ slip.total_deductions_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td><strong>${{ slip.net_salary_clp|floatformat:"0"|intcomma }} CLP</strong></td>
|
||||
<td>
|
||||
{% if slip.is_signed %}
|
||||
<span class="badge badge-emerald">Firmada</span>
|
||||
{% else %}
|
||||
<span class="badge badge-turquoise">Pendiente Firma</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay liquidaciones emitidas para el periodo actual.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="package-plus" style="color:var(--efintel-blue);"></i> {{ title|default:"Ítem de Catálogo de Compras" }}</h2>
|
||||
<p>Parametrización de bienes, insumos, arriendos (vehículos/oficinas) y licencias técnicas.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:catalog_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'procurement:catalog_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="save" style="width:16px;"></i> Guardar Ítem de Catálogo</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,72 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="shopping-cart" style="color:var(--efintel-blue);"></i> Catálogo de Ítems y Servicios de Compras</h2>
|
||||
<p>Catálogo homologado de bienes, insumos, arriendos y servicios para requisiciones u órdenes de compra.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:catalog_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Nuevo Ítem de Catálogo
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar" style="margin-bottom: 1.5rem;">
|
||||
<form method="get" class="d-flex" style="gap: 1rem; align-items: flex-end;">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="q">Buscar (Código/Nombre)</label>
|
||||
<input type="text" name="q" id="q" class="form-control" value="{{ query }}" placeholder="Ej. Laptops, CAT-001...">
|
||||
</div>
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="category">Categoría</label>
|
||||
<select name="category" id="category" class="form-control">
|
||||
<option value="">Todas</option>
|
||||
{% for cat in categories %}
|
||||
<option value="{{ cat.id }}" {% if cat.id|stringformat:"s" == selected_cat %}selected{% endif %}>{{ cat.name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn-primary" style="height: 38px;">Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Descripción</th>
|
||||
<th>Unidad</th>
|
||||
<th>Precio Ref. (CLP)</th>
|
||||
<th>SLA (Días)</th>
|
||||
<th>Proveedor Preferido</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for item in items %}
|
||||
<tr>
|
||||
<td><strong>{{ item.code }}</strong></td>
|
||||
<td>{{ item.name }}</td>
|
||||
<td>{{ item.get_unit_display }}</td>
|
||||
<td>${{ item.reference_price_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td>{{ item.sla_days }} días</td>
|
||||
<td>{{ item.preferred_vendor.name|default:"Sin asignar" }}</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay ítems registrados en el catálogo. <a href="{% url 'procurement:catalog_create' %}">Crear el primero</a>.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-check-2" style="color:var(--efintel-blue);"></i> {{ title|default:"Generar Orden de Compra (OC)" }}</h2>
|
||||
<p>Emisión oficial de Orden de Compra formal para envío a proveedor y conciliación posterior.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:purchase_order_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'procurement:purchase_order_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="printer" style="width:16px;"></i> Emitir Orden de Compra</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,49 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="package" style="color:var(--efintel-blue);"></i> Órdenes de Compra (OC)</h2>
|
||||
<p>Emisión, envío a proveedores, recepciones conformes y conciliación 3-Way Match.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:purchase_order_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Generar OC
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código OC</th>
|
||||
<th>Proveedor</th>
|
||||
<th>Proyecto</th>
|
||||
<th>Total Neto (CLP)</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for po in purchase_orders %}
|
||||
<tr>
|
||||
<td><strong>{{ po.code }}</strong></td>
|
||||
<td>{{ po.vendor.name }}</td>
|
||||
<td>{{ po.project.name }}</td>
|
||||
<td>${{ po.net_amount_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td><span class="badge badge-blue">{{ po.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay órdenes de compra registradas.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,92 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-check-2" style="color:var(--efintel-blue);"></i> Generar Orden de Compra (OC)</h2>
|
||||
<p>Emisión oficial de Orden de Compra formal para envío a proveedor y conciliación 3-Way Match.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:purchase_order_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form" x-data="chileRegions()">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
|
||||
<!-- Alpine JS for Region and Comuna injection into delivery_address -->
|
||||
<div style="margin-top: 1rem; padding: 1rem; border: 1px solid var(--border-color); border-radius: 4px;">
|
||||
<label>Asistente de Dirección (Región/Comuna)</label>
|
||||
<div style="display: flex; gap: 1rem; margin-top: 0.5rem;">
|
||||
<select x-model="selectedRegion" @change="updateAddress()" class="form-control" style="flex: 1;">
|
||||
<option value="">-- Seleccionar Región --</option>
|
||||
<template x-for="(communes, region) in regions" :key="region">
|
||||
<option :value="region" x-text="region"></option>
|
||||
</template>
|
||||
</select>
|
||||
<select x-model="selectedComuna" @change="updateAddress()" class="form-control" style="flex: 1;" :disabled="!selectedRegion">
|
||||
<option value="">-- Seleccionar Comuna --</option>
|
||||
<template x-for="comuna in getCommunes()" :key="comuna">
|
||||
<option :value="comuna" x-text="comuna"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<small style="color: var(--text-secondary); display: block; margin-top: 0.5rem;">
|
||||
Al seleccionar región y comuna, se agregarán automáticamente al campo "Lugar de Entrega".
|
||||
</small>
|
||||
</div>
|
||||
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'procurement:purchase_order_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="printer" style="width:16px;"></i> Emitir Orden de Compra</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<script>
|
||||
document.addEventListener('alpine:init', () => {
|
||||
Alpine.data('chileRegions', () => ({
|
||||
regions: {
|
||||
"Región de Arica y Parinacota": ["Arica", "Camarones", "Putre", "General Lagos"],
|
||||
"Región de Tarapacá": ["Iquique", "Alto Hospicio", "Pozo Almonte", "Camiña", "Colchane", "Huara", "Pica"],
|
||||
"Región de Antofagasta": ["Antofagasta", "Mejillones", "Sierra Gorda", "Taltal", "Calama", "Ollagüe", "San Pedro de Atacama", "Tocopilla", "María Elena"],
|
||||
"Región de Atacama": ["Copiapó", "Caldera", "Tierra Amarilla", "Chañaral", "Diego de Almagro", "Vallenar", "Alto del Carmen", "Freirina", "Huasco"],
|
||||
"Región de Coquimbo": ["La Serena", "Coquimbo", "Andacollo", "La Higuera", "Paihuano", "Vicuña", "Illapel", "Canela", "Los Vilos", "Salamanca", "Ovalle", "Combarbalá", "Monte Patria", "Punitaqui", "Río Hurtado"],
|
||||
"Región de Valparaíso": ["Valparaíso", "Casablanca", "Concón", "Juan Fernández", "Puchuncaví", "Quintero", "Viña del Mar", "Isla de Pascua", "Los Andes", "Calle Larga", "Rinconada", "San Esteban", "La Ligua", "Cabildo", "Papudo", "Petorca", "Zapallar", "Quillota", "Calera", "Hijuelas", "La Cruz", "Nogales", "San Antonio", "Algarrobo", "Cartagena", "El Quisco", "El Tabo", "Santo Domingo", "San Felipe", "Catemu", "Llaillay", "Panquehue", "Putaendo", "Santa María", "Quilpué", "Limache", "Olmué", "Villa Alemana"],
|
||||
"Región Metropolitana de Santiago": ["Cerrillos", "Cerro Navia", "Conchalí", "El Bosque", "Estación Central", "Huechuraba", "Independencia", "La Cisterna", "La Florida", "La Granja", "La Pintana", "La Reina", "Las Condes", "Lo Barnechea", "Lo Espejo", "Lo Prado", "Macul", "Maipú", "Ñuñoa", "Pedro Aguirre Cerda", "Peñalolén", "Providencia", "Pudahuel", "Quilicura", "Quinta Normal", "Recoleta", "Renca", "Santiago", "San Joaquín", "San Miguel", "San Ramón", "Vitacura", "Puente Alto", "Pirque", "San José de Maipo", "Colina", "Lampa", "Tiltil", "San Bernardo", "Buin", "Calera de Tango", "Paine", "Melipilla", "Alhué", "Curacaví", "María Pinto", "San Pedro", "Talagante", "El Monte", "Isla de Maipo", "Padre Hurtado", "Peñaflor"],
|
||||
"Región del Libertador General Bernardo O'Higgins": ["Rancagua", "Codegua", "Coinco", "Coltauco", "Doñihue", "Graneros", "Las Cabras", "Machalí", "Malloa", "Mostazal", "Olivar", "Peumo", "Pichidegua", "Quinta de Tilcoco", "Rengo", "Requínoa", "San Vicente", "Pichilemu", "La Estrella", "Litueche", "Marchihue", "Navidad", "Paredones", "San Fernando", "Chépica", "Chimbarongo", "Lolol", "Nancagua", "Palmilla", "Peralillo", "Placilla", "Pumanque", "Santa Cruz"],
|
||||
"Región del Maule": ["Talca", "Constitución", "Curepto", "Empedrado", "Maule", "Pelarco", "Pencahue", "Río Claro", "San Clemente", "San Rafael", "Cauquenes", "Chanco", "Pelluhue", "Curicó", "Hualañé", "Licantén", "Molina", "Rauco", "Romeral", "Sagrada Familia", "Teno", "Vichuquén", "Linares", "Colbún", "Longaví", "Parral", "Retiro", "San Javier", "Villa Alegre", "Yerbas Buenas"],
|
||||
"Región de Ñuble": ["Cobquecura", "Coelemu", "Ninhue", "Portezuelo", "Quirihue", "Ránquil", "Treguaco", "Bulnes", "Chillán Viejo", "Chillán", "El Carmen", "Pemuco", "Pinto", "Quillón", "San Ignacio", "Yungay", "Coihueco", "Ñiquén", "San Carlos", "San Fabián", "San Nicolás"],
|
||||
"Región del Biobío": ["Concepción", "Coronel", "Chiguayante", "Florida", "Hualqui", "Lota", "Penco", "San Pedro de la Paz", "Santa Juana", "Talcahuano", "Tomé", "Hualpén", "Lebu", "Arauco", "Cañete", "Contulmo", "Curanilahue", "Los Álamos", "Tirúa", "Los Ángeles", "Antuco", "Cabrero", "Laja", "Mulchén", "Nacimiento", "Negrete", "Quilaco", "Quilleco", "San Rosendo", "Santa Bárbara", "Tucapel", "Yumbel", "Alto Biobío"],
|
||||
"Región de La Araucanía": ["Temuco", "Carahue", "Cunco", "Curarrehue", "Freire", "Galvarino", "Gorbea", "Lautaro", "Loncoche", "Melipeuco", "Nueva Imperial", "Padre las Casas", "Perquenco", "Pitrufquén", "Pucón", "Saavedra", "Teodoro Schmidt", "Toltén", "Vilcún", "Villarrica", "Cholchol", "Angol", "Collipulli", "Curacautín", "Ercilla", "Lonquimay", "Los Sauces", "Lumaco", "Purén", "Renaico", "Traiguén", "Victoria"],
|
||||
"Región de Los Ríos": ["Valdivia", "Corral", "Lanco", "Los Lagos", "Máfil", "Mariquina", "Paillaco", "Panguipulli", "La Unión", "Futrono", "Lago Ranco", "Río Bueno"],
|
||||
"Región de Los Lagos": ["Puerto Montt", "Calbuco", "Cochamó", "Fresia", "Frutillar", "Los Muermos", "Llanquihue", "Maullín", "Puerto Varas", "Castro", "Ancud", "Chonchi", "Curaco de Vélez", "Dalcahue", "Puqueldón", "Queilén", "Quellón", "Quemchi", "Quinchao", "Osorno", "Puerto Octay", "Purranque", "Puyehue", "Río Negro", "San Juan de la Costa", "San Pablo", "Chaitén", "Futaleufú", "Hualaihué", "Palena"],
|
||||
"Región de Aysén del General Carlos Ibáñez del Campo": ["Coihaique", "Lago Verde", "Aysén", "Cisnes", "Guaitecas", "Cochrane", "O'Higgins", "Tortel", "Chile Chico", "Río Ibáñez"],
|
||||
"Región de Magallanes y de la Antártica Chilena": ["Punta Arenas", "Laguna Blanca", "Río Verde", "San Gregorio", "Cabo de Hornos", "Antártica", "Porvenir", "Primavera", "Timaukel", "Natales", "Torres del Paine"]
|
||||
},
|
||||
selectedRegion: '',
|
||||
selectedComuna: '',
|
||||
|
||||
getCommunes() {
|
||||
return this.selectedRegion ? this.regions[this.selectedRegion] : [];
|
||||
},
|
||||
|
||||
updateAddress() {
|
||||
const addressInput = document.getElementById('id_delivery_address');
|
||||
if (addressInput) {
|
||||
let current = addressInput.value.split(' - ')[0]; // keep the base address if present
|
||||
if (this.selectedRegion && this.selectedComuna) {
|
||||
addressInput.value = `${current} - ${this.selectedComuna}, ${this.selectedRegion}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
}));
|
||||
});
|
||||
</script>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="package" style="color:var(--efintel-blue);"></i> Órdenes de Compra (OC)</h2>
|
||||
<p>Emisión, envío a proveedores, recepciones conformes y conciliación 3-Way Match.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:purchase_order_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Generar OC
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar" style="margin-bottom: 1.5rem;">
|
||||
<form method="get" class="d-flex" style="gap: 1rem; align-items: flex-end;">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="status">Estado</label>
|
||||
<select name="status" id="status" class="form-control">
|
||||
<option value="">Todos los Estados</option>
|
||||
<option value="EMITIDA" {% if selected_status == 'EMITIDA' %}selected{% endif %}>Emitida</option>
|
||||
<option value="ENVIADA_PROVEEDOR" {% if selected_status == 'ENVIADA_PROVEEDOR' %}selected{% endif %}>Enviada a Proveedor</option>
|
||||
<option value="RECEPCION_PARCIAL" {% if selected_status == 'RECEPCION_PARCIAL' %}selected{% endif %}>Recepción Parcial</option>
|
||||
<option value="RECEPCION_TOTAL" {% if selected_status == 'RECEPCION_TOTAL' %}selected{% endif %}>Recepción Total</option>
|
||||
<option value="CONCILIADA_MATCH" {% if selected_status == 'CONCILIADA_MATCH' %}selected{% endif %}>Conciliada (3-Way Match)</option>
|
||||
<option value="CERRADA" {% if selected_status == 'CERRADA' %}selected{% endif %}>Cerrada</option>
|
||||
<option value="CANCELADA" {% if selected_status == 'CANCELADA' %}selected{% endif %}>Cancelada</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn-primary" style="height: 38px;">Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código OC</th>
|
||||
<th>Proveedor</th>
|
||||
<th>Proyecto</th>
|
||||
<th>Total Neto (CLP)</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for po in orders %}
|
||||
<tr>
|
||||
<td><strong>{{ po.code }}</strong></td>
|
||||
<td>{{ po.vendor.name }}</td>
|
||||
<td>{{ po.project.name }}</td>
|
||||
<td>${{ po.total_net_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td><span class="badge badge-blue">{{ po.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="5" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay órdenes de compra registradas.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-plus-2" style="color:var(--efintel-blue);"></i> {{ title|default:"Nueva Solicitud / Requisición de Compra" }}</h2>
|
||||
<p>Formulario de requerimiento de insumos o servicios con centro de costos e imputación a proyecto I+D+i.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:requisition_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'procurement:requisition_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="send" style="width:16px;"></i> Enviar Requisición a Aprobación</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,70 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="file-text" style="color:var(--efintel-blue);"></i> Solicitudes y Requisiciones de Compra</h2>
|
||||
<p>Gestión del flujo de requerimientos de abastecimiento I+D+i y aprobación técnica/financiera.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'procurement:requisition_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="plus-circle" style="width:16px;"></i> + Nueva Requisición
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="filter-bar" style="margin-bottom: 1.5rem;">
|
||||
<form method="get" class="d-flex" style="gap: 1rem; align-items: flex-end;">
|
||||
<div class="form-group" style="flex: 1;">
|
||||
<label for="status">Estado</label>
|
||||
<select name="status" id="status" class="form-control">
|
||||
<option value="">Todos los Estados</option>
|
||||
<option value="SOLICITADA" {% if selected_status == 'SOLICITADA' %}selected{% endif %}>Solicitada</option>
|
||||
<option value="APROBADA_TECNICA" {% if selected_status == 'APROBADA_TECNICA' %}selected{% endif %}>Aprobada Técnica</option>
|
||||
<option value="APROBADA_FINANZAS" {% if selected_status == 'APROBADA_FINANZAS' %}selected{% endif %}>Aprobada Finanzas</option>
|
||||
<option value="RECHAZADA" {% if selected_status == 'RECHAZADA' %}selected{% endif %}>Rechazada</option>
|
||||
<option value="CONVERTIDA_OC" {% if selected_status == 'CONVERTIDA_OC' %}selected{% endif %}>Convertida a OC</option>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn-primary" style="height: 38px;">Filtrar</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Código</th>
|
||||
<th>Solicitante</th>
|
||||
<th>Proyecto</th>
|
||||
<th>Prioridad</th>
|
||||
<th>Total Est. (CLP)</th>
|
||||
<th>Estado</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for req in requisitions %}
|
||||
<tr>
|
||||
<td><strong>{{ req.code }}</strong></td>
|
||||
<td>{{ req.requester.username }}</td>
|
||||
<td>{{ req.project.code }} - {{ req.project.name }}</td>
|
||||
<td><span class="badge badge-turquoise">{{ req.get_priority_display }}</span></td>
|
||||
<td>${{ req.total_estimated_clp|floatformat:"0"|intcomma }} CLP</td>
|
||||
<td><span class="badge badge-blue">{{ req.get_status_display }}</span></td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="6" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay requisiciones registradas. <a href="{% url 'procurement:requisition_create' %}">Crear la primera</a>.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,50 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="user" style="color:var(--efintel-blue);"></i> Ficha Técnica: {{ candidate.full_name }}</h2>
|
||||
<p>RUT: {{ candidate.rut }} | {{ candidate.specialty_title }}</p>
|
||||
</div>
|
||||
<div style="display:flex; gap:0.5rem;">
|
||||
<a href="{% url 'talent:candidate_edit' candidate.pk %}" class="btn-primary" style="background:var(--efintel-turquoise); text-decoration:none;">
|
||||
<i data-lucide="edit-3" style="width:16px;"></i> Editar
|
||||
</a>
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="kpi-grid-3" style="margin-bottom:1.5rem;">
|
||||
<div class="kpi-card accent-blue">
|
||||
<div class="kpi-header"><span class="kpi-title">TAXONOMÍA PRINCIPAL</span><div class="kpi-icon"><i data-lucide="layers"></i></div></div>
|
||||
<div class="kpi-value" style="font-size:1.1rem;">{{ candidate.get_category_code_display }}</div>
|
||||
</div>
|
||||
<div class="kpi-card accent-turquoise">
|
||||
<div class="kpi-header"><span class="kpi-title">MADUREZ TECNOLÓGICA</span><div class="kpi-icon"><i data-lucide="cpu"></i></div></div>
|
||||
<div class="kpi-value">TRL {{ candidate.trl_level }}</div>
|
||||
<div class="kpi-subtext"><span>Seniority: {{ candidate.get_seniority_display }}</span></div>
|
||||
</div>
|
||||
<div class="kpi-card accent-emerald">
|
||||
<div class="kpi-header"><span class="kpi-title">ESTADO EN CARTERA</span><div class="kpi-icon"><i data-lucide="shield-check"></i></div></div>
|
||||
<div class="kpi-value" style="font-size:1.2rem;">{{ candidate.get_status_display }}</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="padding:1.5rem;">
|
||||
<h3>Detalles del Perfil</h3>
|
||||
<ul style="line-height:2;">
|
||||
<li><strong>Correo:</strong> {{ candidate.email }}</li>
|
||||
<li><strong>Teléfono:</strong> {{ candidate.phone|default:"No informado" }}</li>
|
||||
<li><strong>Ciudad/Región:</strong> {{ candidate.city }}</li>
|
||||
<li><strong>Pretensión de Renta:</strong> ${{ candidate.monthly_salary_target_clp|floatformat:"0"|intcomma }} CLP</li>
|
||||
<li><strong>Disponibilidad:</strong> {{ candidate.get_availability_display }}</li>
|
||||
{% if candidate.github_url %}<li><strong>GitHub:</strong> <a href="{{ candidate.github_url }}" target="_blank">{{ candidate.github_url }}</a></li>{% endif %}
|
||||
{% if candidate.linkedin_url %}<li><strong>LinkedIn:</strong> <a href="{{ candidate.linkedin_url }}" target="_blank">{{ candidate.linkedin_url }}</a></li>{% endif %}
|
||||
</ul>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,125 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="user-check" style="color:var(--efintel-blue);"></i> {{ title }}</h2>
|
||||
<p>Formulario de alta y edición de especialista I+D+i con taxonomía (categorías 1 a 7) y nivel TRL.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver a Cartera
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:800px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
|
||||
{% if form.non_field_errors %}
|
||||
<div class="alert alert-danger">
|
||||
{% for err in form.non_field_errors %}
|
||||
<p>{{ err }}</p>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
|
||||
<div class="form-grid" x-data="regionComunaData()">
|
||||
{% for field in form %}
|
||||
{% if field.name == 'city' %}
|
||||
<div class="form-group">
|
||||
<label for="region_select" class="form-label">Región</label>
|
||||
<select id="region_select" x-model="selectedRegion" @change="updateComunas" class="form-control" required>
|
||||
<option value="">Seleccione Región...</option>
|
||||
<template x-for="(comunas, region) in regionsData" :key="region">
|
||||
<option :value="region" x-text="region" :selected="region === selectedRegion"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<label for="comuna_select" class="form-label">Comuna / Ciudad</label>
|
||||
<select id="comuna_select" x-model="selectedComuna" @change="updateCity" class="form-control" required>
|
||||
<option value="">Seleccione Comuna...</option>
|
||||
<template x-for="comuna in availableComunas" :key="comuna">
|
||||
<option :value="comuna" x-text="comuna" :selected="comuna === selectedComuna"></option>
|
||||
</template>
|
||||
</select>
|
||||
</div>
|
||||
<!-- Hidden original field to be submitted -->
|
||||
<input type="hidden" name="city" id="id_city" x-model="finalCity" />
|
||||
{% else %}
|
||||
<div class="form-group {% if field.name == 'summary' or field.name == 'address' %}full-width{% endif %}">
|
||||
<label for="{{ field.id_for_label }}" class="form-label">{{ field.label }}</label>
|
||||
{{ field }}
|
||||
{% if field.help_text %}
|
||||
<small class="form-help">{{ field.help_text }}</small>
|
||||
{% endif %}
|
||||
{% if field.errors %}
|
||||
<div class="form-error">
|
||||
{% for err in field.errors %}
|
||||
<span>{{ err }}</span>
|
||||
{% endfor %}
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endfor %}
|
||||
</div>
|
||||
|
||||
<script>
|
||||
function regionComunaData() {
|
||||
const data = {{ regions_json|safe }};
|
||||
const initialCity = "{{ form.city.value|default:'' }}";
|
||||
|
||||
let initReg = "";
|
||||
let initCom = "";
|
||||
|
||||
if (initialCity) {
|
||||
// Find which region has this comuna
|
||||
for (const [reg, coms] of Object.entries(data)) {
|
||||
if (coms.includes(initialCity)) {
|
||||
initReg = reg;
|
||||
initCom = initialCity;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!initReg) {
|
||||
// If not found in communes, might just be free text or old data.
|
||||
initCom = initialCity;
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
regionsData: data,
|
||||
selectedRegion: initReg,
|
||||
selectedComuna: initCom,
|
||||
availableComunas: initReg ? data[initReg] : [],
|
||||
finalCity: initialCity,
|
||||
|
||||
updateComunas() {
|
||||
this.availableComunas = this.selectedRegion ? this.regionsData[this.selectedRegion] : [];
|
||||
this.selectedComuna = "";
|
||||
this.finalCity = "";
|
||||
},
|
||||
|
||||
updateCity() {
|
||||
this.finalCity = this.selectedComuna;
|
||||
}
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="form-actions">
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-secondary">
|
||||
Cancelar
|
||||
</a>
|
||||
<button type="submit" class="btn-primary">
|
||||
<i data-lucide="save"></i> Guardar Especialista
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,131 @@
|
||||
{% extends 'base.html' %}
|
||||
{% load humanize %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="award" style="color:var(--efintel-blue);"></i> Cartera de Especialistas — Talent Pool I+D+i</h2>
|
||||
<p>Directorio maestro, clasificación por taxonomía (1-7), nivel TRL y rúbrica de evaluación 7D.</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'talent:candidate_create' %}" class="btn-primary" style="text-decoration:none;">
|
||||
<i data-lucide="user-plus" style="width:16px;"></i> + Nuevo Especialista
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- KPI Cards -->
|
||||
<div class="kpi-grid-3" style="margin-bottom: 1.5rem;">
|
||||
<div class="kpi-card accent-blue">
|
||||
<div class="kpi-header"><span class="kpi-title">TOTAL CANDIDATOS</span><div class="kpi-icon"><i data-lucide="users"></i></div></div>
|
||||
<div class="kpi-value">{{ stats.total|default:"0" }}</div>
|
||||
<div class="kpi-subtext"><span>Postulantes en Sistema</span></div>
|
||||
</div>
|
||||
<div class="kpi-card accent-emerald">
|
||||
<div class="kpi-header"><span class="kpi-title">APROBADOS EN CARTERA</span><div class="kpi-icon"><i data-lucide="check-circle-2"></i></div></div>
|
||||
<div class="kpi-value">{{ stats.approved|default:"0" }}</div>
|
||||
<div class="kpi-subtext"><span>Listos para Asignación</span></div>
|
||||
</div>
|
||||
<div class="kpi-card accent-turquoise">
|
||||
<div class="kpi-header"><span class="kpi-title">EN RESERVA / EVALUACIÓN</span><div class="kpi-icon"><i data-lucide="clock"></i></div></div>
|
||||
<div class="kpi-value">{{ stats.reserve|add:stats.evaluating|default:"0" }}</div>
|
||||
<div class="kpi-subtext"><span>Banco de Talento Vigoroso</span></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- Filters -->
|
||||
<div class="data-card" style="margin-bottom: 1.5rem; padding: 1.5rem;">
|
||||
<form method="get" class="filter-form" style="display:flex; gap:1rem; flex-wrap:wrap; align-items:flex-end;">
|
||||
<div class="form-group" style="flex:1; min-width:200px;">
|
||||
<label class="form-label">Buscar</label>
|
||||
<input type="text" name="q" class="form-control" value="{{ query|default:'' }}" placeholder="Nombre, RUT, especialidad...">
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; min-width:200px;">
|
||||
<label class="form-label">Categoría</label>
|
||||
<select name="category" class="form-control">
|
||||
<option value="">Todas</option>
|
||||
{% for val, name in categories %}
|
||||
<option value="{{ val }}" {% if val == selected_category %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; min-width:200px;">
|
||||
<label class="form-label">Seniority</label>
|
||||
<select name="seniority" class="form-control">
|
||||
<option value="">Todos</option>
|
||||
{% for val, name in seniorities %}
|
||||
<option value="{{ val }}" {% if val == selected_seniority %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group" style="flex:1; min-width:200px;">
|
||||
<label class="form-label">Estado</label>
|
||||
<select name="status" class="form-control">
|
||||
<option value="">Todos</option>
|
||||
{% for val, name in statuses %}
|
||||
<option value="{{ val }}" {% if val == selected_status %}selected{% endif %}>{{ name }}</option>
|
||||
{% endfor %}
|
||||
</select>
|
||||
</div>
|
||||
<div class="form-group">
|
||||
<button type="submit" class="btn-primary" style="height: 42px;">Filtrar</button>
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-secondary" style="height: 42px; line-height:26px;">Limpiar</a>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
<!-- Data Table -->
|
||||
<div class="data-card">
|
||||
<table class="data-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>RUT</th>
|
||||
<th>Especialista</th>
|
||||
<th>Categoría Taxonomía</th>
|
||||
<th>Seniority</th>
|
||||
<th>TRL</th>
|
||||
<th>Estado</th>
|
||||
<th>Acciones</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{% for c in candidates %}
|
||||
<tr>
|
||||
<td><strong>{{ c.rut }}</strong></td>
|
||||
<td>
|
||||
<a href="{% url 'talent:candidate_detail' c.pk %}" style="text-decoration:none; color:var(--efintel-blue); font-weight:600;">
|
||||
{{ c.full_name }}
|
||||
</a>
|
||||
<div style="font-size:0.75rem; color:var(--text-secondary);">{{ c.specialty_title }}</div>
|
||||
</td>
|
||||
<td><span class="badge badge-blue">{{ c.get_category_code_display }}</span></td>
|
||||
<td>{{ c.get_seniority_display }}</td>
|
||||
<td><span class="badge badge-turquoise">TRL {{ c.trl_level }}</span></td>
|
||||
<td>
|
||||
{% if c.status == 'APROBADO_CARTERA' %}
|
||||
<span class="badge badge-emerald">Aprobado</span>
|
||||
{% elif c.status == 'EN_RESERVA' %}
|
||||
<span class="badge badge-turquoise">En Reserva</span>
|
||||
{% else %}
|
||||
<span class="badge badge-gray">{{ c.get_status_display }}</span>
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
<a href="{% url 'talent:candidate_detail' c.pk %}" class="btn-primary" style="padding:0.25rem 0.5rem; font-size:0.75rem; text-decoration:none;">
|
||||
Ver Ficha
|
||||
</a>
|
||||
</td>
|
||||
</tr>
|
||||
{% empty %}
|
||||
<tr>
|
||||
<td colspan="7" style="text-align:center; padding:2rem; color:var(--text-secondary);">
|
||||
No hay especialistas registrados en el sistema. <a href="{% url 'talent:candidate_create' %}">Registrar primero</a>.
|
||||
</td>
|
||||
</tr>
|
||||
{% endfor %}
|
||||
</tbody>
|
||||
</table>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
@@ -0,0 +1,28 @@
|
||||
{% extends 'base.html' %}
|
||||
|
||||
{% block content %}
|
||||
<section class="page-view active">
|
||||
<div class="view-header">
|
||||
<div class="view-title">
|
||||
<h2><i data-lucide="clipboard-check" style="color:var(--efintel-blue);"></i> Evaluación 7D de Especialista I+D+i</h2>
|
||||
<p>Rúbrica holística en 7 dimensiones ponderadas (Caso práctico 25%, Portafolio 15%, Competencias 15%, etc.).</p>
|
||||
</div>
|
||||
<div>
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-primary" style="background:var(--efintel-gray); text-decoration:none;">
|
||||
<i data-lucide="arrow-left" style="width:16px;"></i> Volver
|
||||
</a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="data-card" style="max-width:850px; margin:0 auto; padding:2rem;">
|
||||
<form method="post" class="modern-form">
|
||||
{% csrf_token %}
|
||||
{% include "partials/_form_grid.html" %}
|
||||
<div style="margin-top:1.5rem; display:flex; gap:1rem; justify-content:flex-end;">
|
||||
<a href="{% url 'talent:candidate_list' %}" class="btn-secondary" style="text-decoration:none;">Cancelar</a>
|
||||
<button type="submit" class="btn-primary"><i data-lucide="check-square" style="width:16px;"></i> Procesar Evaluación 7D</button>
|
||||
</div>
|
||||
</form>
|
||||
</div>
|
||||
</section>
|
||||
{% endblock %}
|
||||
Reference in New Issue
Block a user