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