iteracion 2

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