Files
TimeTracker/templates/invoices/edit.html
T
Dries Peeters 9b7aa3a938 security: Add CSRF token protection to all POST forms" -m " Complete CSRF protection implementation across the entire application. Fixed 31 HTML forms and 4 JavaScript dynamic form generators that were missing CSRF tokens.
Affected modules: Projects, Clients, Tasks, Invoices, Comments, Admin, Search

- All HTML forms now include csrf_token hidden input
- JavaScript forms retrieve token from meta tag in base.html
- API endpoints properly exempted for JSON operations
- 58 POST forms + 4 dynamic JS forms now protected

Security impact: HIGH - Closes critical CSRF vulnerability
Files modified: 20 templates
2025-10-11 09:01:58 +02:00

381 lines
21 KiB
HTML

{% extends "base.html" %}
{% block title %}{{ _('Edit Invoice') }} {{ invoice.invoice_number }} - TimeTracker{% endblock %}
{% block content %}
<div class="container-fluid">
<div class="row">
<div class="col-12">
<div class="d-flex justify-content-between align-items-center mb-4">
<h1 class="h3 mb-0">
<i class="fas fa-edit text-primary"></i>
{{ _('Edit Invoice') }} {{ invoice.invoice_number }}
</h1>
<div class="btn-group" role="group">
<a href="{{ url_for('invoices.view_invoice', invoice_id=invoice.id) }}" class="btn btn-secondary">
<i class="fas fa-eye"></i> {{ _('View') }}
</a>
<a href="{{ url_for('invoices.list_invoices') }}" class="btn btn-outline-secondary">
<i class="fas fa-arrow-left"></i> {{ _('Back') }}
</a>
</div>
</div>
<form method="POST">
<input type="hidden" name="csrf_token" value="{{ csrf_token() }}">
<div class="row">
<div class="col-lg-8">
<!-- Invoice Details -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ _('Invoice Details') }}</h6>
</div>
<div class="card-body">
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="client_name" class="form-label">{{ _('Client Name *') }}</label>
<input type="text" class="form-control" id="client_name" name="client_name"
value="{{ invoice.client_name }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="client_email" class="form-label">{{ _('Client Email') }}</label>
<input type="email" class="form-control" id="client_email" name="client_email"
value="{{ invoice.client_email or '' }}" placeholder="client@example.com">
</div>
</div>
</div>
<div class="mb-3">
<label for="client_address" class="form-label">{{ _('Client Address') }}</label>
<textarea class="form-control" id="client_address" name="client_address"
rows="3" placeholder="Client billing address">{{ invoice.client_address or '' }}</textarea>
</div>
<div class="row">
<div class="col-md-6">
<div class="mb-3">
<label for="due_date" class="form-label">{{ _('Due Date *') }}</label>
<input type="date" class="form-control" id="due_date" name="due_date"
value="{{ invoice.due_date.strftime('%Y-%m-%d') }}" required>
</div>
</div>
<div class="col-md-6">
<div class="mb-3">
<label for="tax_rate" class="form-label">{{ _('Tax Rate (%)') }}</label>
<input type="number" class="form-control" id="tax_rate" name="tax_rate"
value="{{ "%.2f"|format(invoice.tax_rate) }}" min="0" max="100" step="0.01">
</div>
</div>
</div>
<div class="mb-3">
<label for="notes" class="form-label">{{ _('Notes') }}</label>
<textarea class="form-control" id="notes" name="notes"
rows="3" placeholder="Additional notes for the client">{{ invoice.notes or '' }}</textarea>
</div>
<div class="mb-3">
<label for="terms" class="form-label">{{ _('Terms & Conditions') }}</label>
<textarea class="form-control" id="terms" name="terms"
rows="3" placeholder="Payment terms and conditions">{{ invoice.terms or '' }}</textarea>
</div>
</div>
</div>
<!-- Invoice Items -->
<div class="card shadow mb-4">
<div class="card-header py-3 d-flex justify-content-between align-items-center">
<h6 class="m-0 font-weight-bold text-primary">{{ _('Invoice Items') }}</h6>
<button type="button" class="btn btn-sm btn-outline-primary" onclick="addItemRow()">
<i class="fas fa-plus"></i> {{ _('Add Item') }}
</button>
</div>
<div class="card-body">
<div class="table-responsive">
<table class="table table-bordered" id="itemsTable">
<thead class="table-light">
<tr>
<th style="width: 40%">{{ _('Description *') }}</th>
<th style="width: 20%">{{ _('Quantity (Hours) *') }}</th>
<th style="width: 20%">{{ _('Unit Price *') }}</th>
<th style="width: 20%">{{ _('Total') }}</th>
<th style="width: 10%">{{ _('Actions') }}</th>
</tr>
</thead>
<tbody id="itemsTableBody">
{% for item in invoice.items %}
<tr class="item-row">
<td>
<input type="hidden" name="item_id[]" value="{{ item.id }}">
<input type="text" class="form-control" name="description[]"
value="{{ item.description }}" required>
</td>
<td>
<input type="number" class="form-control quantity-input"
name="quantity[]" value="{{ "%.2f"|format(item.quantity) }}"
min="0" step="0.01" required onchange="calculateRowTotal(this)">
</td>
<td>
<input type="number" class="form-control price-input"
name="unit_price[]" value="{{ "%.2f"|format(item.unit_price) }}"
min="0" step="0.01" required onchange="calculateRowTotal(this)">
</td>
<td>
<input type="text" class="form-control row-total"
value="{{ "%.2f"|format(item.total_amount) }} {{ currency }}" readonly>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeItemRow(this)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
{% endfor %}
</tbody>
<tfoot class="table-light">
<tr>
<td colspan="2" class="text-end"><strong>{{ _('Subtotal:') }}</strong></td>
<td colspan="2">
<input type="text" class="form-control" id="subtotal"
value="{{ "%.2f"|format(invoice.subtotal) }} {{ currency }}" readonly>
</td>
<td></td>
</tr>
<tr>
<td colspan="2" class="text-end">
<strong>{{ _('Tax') }} ({{ "%.2f"|format(invoice.tax_rate) }}%):</strong>
</td>
<td colspan="2">
<input type="text" class="form-control" id="taxAmount"
value="{{ "%.2f"|format(invoice.tax_amount) }} {{ currency }}" readonly>
</td>
<td></td>
</tr>
<tr class="table-primary">
<td colspan="2" class="text-end"><strong>{{ _('Total Amount:') }}</strong></td>
<td colspan="2">
<input type="text" class="form-control" id="totalAmount"
value="{{ "%.2f"|format(invoice.total_amount) }} {{ currency }}" readonly>
</td>
<td></td>
</tr>
</tfoot>
</table>
</div>
</div>
</div>
</div>
<div class="col-lg-4">
<!-- Project Information -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ _('Project Information') }}</h6>
</div>
<div class="card-body">
<div class="mb-3">
<strong>{{ _('Project:') }}</strong><br>
<span class="text-muted">{{ invoice.project.name }}</span>
</div>
<div class="mb-3">
<strong>{{ _('Client:') }}</strong><br>
<span class="text-muted">{{ invoice.project.client }}</span>
</div>
<div class="mb-3">
<strong>{{ _('Hourly Rate:') }}</strong><br>
<span class="text-muted">
{% if invoice.project.hourly_rate %}
{{ "%.2f"|format(invoice.project.hourly_rate) }} {{ currency }}
{% else %}
{{ _('Not set') }}
{% endif %}
</span>
</div>
<div class="mb-3">
<strong>{{ _('Billing Reference:') }}</strong><br>
<span class="text-muted">{{ invoice.project.billing_ref or 'None' }}</span>
</div>
</div>
</div>
<!-- Quick Actions -->
<div class="card shadow mb-4">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ _('Quick Actions') }}</h6>
</div>
<div class="card-body">
<div class="mb-3">
<a href="{{ url_for('invoices.generate_from_time', invoice_id=invoice.id) }}"
class="btn btn-primary w-100">
<i class="fas fa-clock"></i> {{ _('Generate from Time Entries') }}
</a>
</div>
<div class="mb-3">
<button type="button" class="btn btn-outline-secondary w-100" onclick="useProjectRate()">
<i class="fas fa-dollar-sign"></i> {{ _('Use Project Hourly Rate') }}
</button>
</div>
<div class="mb-3">
<button type="button" class="btn btn-outline-info w-100" onclick="calculateTotals()">
<i class="fas fa-calculator"></i> {{ _('Recalculate Totals') }}
</button>
</div>
</div>
</div>
<!-- Totals Summary -->
<div class="card shadow">
<div class="card-header py-3">
<h6 class="m-0 font-weight-bold text-primary">{{ _('Totals Summary') }}</h6>
</div>
<div class="card-body">
<div class="row mb-2">
<div class="col-6"><strong>{{ _('Subtotal:') }}</strong></div>
<div class="col-6 text-end" id="summarySubtotal">
{{ "%.2f"|format(invoice.subtotal) }} {{ currency }}
</div>
</div>
<div class="row mb-2">
<div class="col-6"><strong>{{ _('Tax:') }}</strong></div>
<div class="col-6 text-end" id="summaryTax">
{{ "%.2f"|format(invoice.tax_amount) }} {{ currency }}
</div>
</div>
<hr>
<div class="row">
<div class="col-6"><strong>{{ _('Total:') }}</strong></div>
<div class="col-6 text-end" id="summaryTotal">
{{ "%.2f"|format(invoice.total_amount) }} {{ currency }}
</div>
</div>
</div>
</div>
</div>
</div>
<!-- Form Actions -->
<div class="row mt-4">
<div class="col-12">
<div class="card shadow">
<div class="card-body">
<div class="d-flex justify-content-between">
<a href="{{ url_for('invoices.view_invoice', invoice_id=invoice.id) }}" class="btn btn-secondary">
<i class="fas fa-times me-2"></i>{{ _('Cancel') }}
</a>
<button type="submit" class="btn btn-primary">
<i class="fas fa-save me-2"></i>{{ _('Update Invoice') }}
</button>
</div>
</div>
</div>
</div>
</div>
</form>
</div>
</div>
</div>
{% endblock %}
{% block extra_js %}
<script>
let itemCounter = {{ invoice.items.count() }};
function addItemRow() {
itemCounter++;
const newRow = `
<tr class="item-row">
<td>
<input type="hidden" name="item_id[]" value="">
<input type="text" class="form-control" name="description[]" placeholder="Item description" required>
</td>
<td>
<input type="number" class="form-control quantity-input" name="quantity[]"
value="1.00" min="0" step="0.01" required onchange="calculateRowTotal(this)">
</td>
<td>
<input type="number" class="form-control price-input" name="unit_price[]"
value="0.00" min="0" step="0.01" required onchange="calculateRowTotal(this)">
</td>
<td>
<input type="text" class="form-control row-total" value="0.00 {{ currency }}" readonly>
</td>
<td>
<button type="button" class="btn btn-sm btn-outline-danger" onclick="removeItemRow(this)">
<i class="fas fa-trash"></i>
</button>
</td>
</tr>
`;
document.getElementById('itemsTableBody').insertAdjacentHTML('beforeend', newRow);
}
function removeItemRow(button) {
const itemRows = document.querySelectorAll('.item-row');
if (itemRows.length > 1) {
button.closest('tr').remove();
calculateTotals();
} else {
alert('{{ _('At least one item is required.') }}');
}
}
function calculateRowTotal(input) {
const row = input.closest('tr');
const quantity = parseFloat(row.querySelector('.quantity-input').value) || 0;
const price = parseFloat(row.querySelector('.price-input').value) || 0;
const total = quantity * price;
row.querySelector('.row-total').value = total.toFixed(2) + ' {{ currency }}';
calculateTotals();
}
function calculateTotals() {
let subtotal = 0;
document.querySelectorAll('.item-row').forEach(function(row) {
const quantity = parseFloat(row.querySelector('.quantity-input').value) || 0;
const price = parseFloat(row.querySelector('.price-input').value) || 0;
subtotal += quantity * price;
});
const taxRate = parseFloat(document.getElementById('tax_rate').value) || 0;
const taxAmount = subtotal * (taxRate / 100);
const total = subtotal + taxAmount;
// Update summary fields
document.getElementById('subtotal').value = subtotal.toFixed(2) + ' {{ currency }}';
document.getElementById('taxAmount').value = taxAmount.toFixed(2) + ' {{ currency }}';
document.getElementById('totalAmount').value = total.toFixed(2) + ' {{ currency }}';
// Update summary display
document.getElementById('summarySubtotal').textContent = subtotal.toFixed(2) + ' {{ currency }}';
document.getElementById('summaryTax').textContent = taxAmount.toFixed(2) + ' {{ currency }}';
document.getElementById('summaryTotal').textContent = total.toFixed(2) + ' {{ currency }}';
}
function useProjectRate() {
const projectRate = {{ invoice.project.hourly_rate or 0 }};
if (projectRate > 0) {
document.querySelectorAll('.price-input').forEach(function(input) {
input.value = projectRate.toFixed(2);
calculateRowTotal(input);
});
} else {
alert('{{ _('No hourly rate set for this project.') }}');
}
}
// Initialize calculations on page load
document.addEventListener('DOMContentLoaded', function() {
calculateTotals();
// Set minimum due date to today
const today = new Date().toISOString().split('T')[0];
document.getElementById('due_date').setAttribute('min', today);
});
</script>
{% endblock %}