249 lines
11 KiB
HTML
249 lines
11 KiB
HTML
{% extends 'base.html' %}
|
|
{% load static %}
|
|
|
|
{% block content %}
|
|
<div class="container mt-5">
|
|
<div class="row justify-content-center">
|
|
<div class="col-lg-8">
|
|
<div class="card shadow-lg border-0">
|
|
<div class="card-header text-white py-3" style="background-color: var(--primary-color); border-top-left-radius: 1rem; border-top-right-radius: 1rem;">
|
|
<h4 class="mb-0 fw-bold"><i class="fas fa-file-invoice-dollar me-2"></i>Receipt Generator</h4>
|
|
</div>
|
|
<div class="card-body p-4">
|
|
<form method="post" id="receiptForm">
|
|
{% csrf_token %}
|
|
|
|
<!-- Header Info -->
|
|
<div class="row g-3 mb-4">
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold text-secondary">Date</label>
|
|
{{ form.date }}
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold text-secondary">Vendor Name</label>
|
|
{{ form.vendor }}
|
|
<div class="form-text text-muted small"><i class="fas fa-info-circle"></i> Will appear as the main title on the receipt.</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<label class="form-label fw-bold text-secondary">Payment Method</label>
|
|
{{ form.payment_method }}
|
|
</div>
|
|
<div class="col-12">
|
|
<label class="form-label fw-bold text-secondary">Description</label>
|
|
{{ form.description }}
|
|
</div>
|
|
</div>
|
|
|
|
<hr class="text-muted opacity-25">
|
|
|
|
<!-- Line Items -->
|
|
<div class="d-flex justify-content-between align-items-center mb-3">
|
|
<h5 class="fw-bold text-dark m-0">Items</h5>
|
|
<button type="button" id="add-item" class="btn btn-sm btn-outline-secondary rounded-pill">
|
|
<i class="fas fa-plus me-1"></i> Add Line
|
|
</button>
|
|
</div>
|
|
|
|
{{ items.management_form }}
|
|
<div id="items-container">
|
|
{% for form in items %}
|
|
<div class="item-row row g-2 align-items-center mb-2">
|
|
{{ form.id }}
|
|
<div class="col-12 col-md-7">
|
|
{{ form.product }}
|
|
</div>
|
|
<div class="col-10 col-md-4">
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0">R</span>
|
|
{{ form.amount }}
|
|
</div>
|
|
</div>
|
|
<div class="col-2 col-md-1 text-center">
|
|
{% if items.can_delete %}
|
|
<div class="form-check d-none">
|
|
{{ form.DELETE }}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
{% endif %}
|
|
</div>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
|
|
<hr class="text-muted opacity-25 mt-4">
|
|
|
|
<!-- VAT and Totals -->
|
|
<div class="row">
|
|
<div class="col-md-6 mb-3 mb-md-0">
|
|
<label class="form-label d-block fw-bold text-secondary mb-2">VAT Configuration (15%)</label>
|
|
<div class="card bg-light border-0 p-3">
|
|
{% for radio in form.vat_type %}
|
|
<div class="form-check mb-2">
|
|
{{ radio.tag }}
|
|
<label class="form-check-label" for="{{ radio.id_for_label }}">
|
|
{{ radio.choice_label }}
|
|
</label>
|
|
</div>
|
|
{% endfor %}
|
|
</div>
|
|
</div>
|
|
<div class="col-md-6">
|
|
<div class="p-3 rounded" style="background-color: #f8fafc; border: 1px solid #e2e8f0;">
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-secondary">Subtotal (Excl. VAT):</span>
|
|
<span class="fw-bold">R <span id="display-subtotal">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between mb-2">
|
|
<span class="text-secondary">VAT (15%):</span>
|
|
<span class="fw-bold">R <span id="display-vat">0.00</span></span>
|
|
</div>
|
|
<div class="d-flex justify-content-between border-top pt-2 mt-2">
|
|
<span class="h5 mb-0 fw-bold">Total:</span>
|
|
<span class="h5 mb-0" style="color: var(--accent-color);">R <span id="display-total">0.00</span></span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<div class="d-grid mt-5">
|
|
<button type="submit" class="btn btn-accent btn-lg shadow-sm">
|
|
<i class="fas fa-paper-plane me-2"></i>Create & Send Receipt
|
|
</button>
|
|
</div>
|
|
</form>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- Empty Form Template for JS -->
|
|
<div id="empty-form" class="d-none">
|
|
<div class="item-row row g-2 align-items-center mb-2">
|
|
<div class="col-12 col-md-7">
|
|
{{ items.empty_form.product }}
|
|
</div>
|
|
<div class="col-10 col-md-4">
|
|
<div class="input-group">
|
|
<span class="input-group-text bg-light border-end-0">R</span>
|
|
{{ items.empty_form.amount }}
|
|
</div>
|
|
</div>
|
|
<div class="col-2 col-md-1 text-center">
|
|
<div class="form-check d-none">
|
|
{{ items.empty_form.DELETE }}
|
|
</div>
|
|
<button type="button" class="btn btn-outline-danger btn-sm delete-row rounded-circle" title="Remove">
|
|
<i class="fas fa-times"></i>
|
|
</button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
document.addEventListener('DOMContentLoaded', function() {
|
|
const itemsContainer = document.getElementById('items-container');
|
|
const addItemBtn = document.getElementById('add-item');
|
|
const totalForms = document.getElementById('id_items-TOTAL_FORMS');
|
|
const emptyFormHtml = document.getElementById('empty-form').innerHTML;
|
|
|
|
// Elements for calculation
|
|
const displaySubtotal = document.getElementById('display-subtotal');
|
|
const displayVat = document.getElementById('display-vat');
|
|
const displayTotal = document.getElementById('display-total');
|
|
const vatRadios = document.querySelectorAll('input[name="vat_type"]');
|
|
|
|
function updateCalculations() {
|
|
let sum = 0;
|
|
const amounts = document.querySelectorAll('.item-row:not(.deleted) .item-amount');
|
|
amounts.forEach(input => {
|
|
const val = parseFloat(input.value) || 0;
|
|
sum += val;
|
|
});
|
|
|
|
let vat = 0;
|
|
let total = 0;
|
|
let subtotal = 0;
|
|
|
|
// Find selected VAT type
|
|
let vatType = 'INCLUDED'; // Default
|
|
vatRadios.forEach(r => {
|
|
if (r.checked) vatType = r.value;
|
|
});
|
|
|
|
if (vatType === 'INCLUDED') {
|
|
// Reverse calc: Total is Sum. VAT is part of it.
|
|
// Amount = Base + VAT
|
|
// Base = Amount / 1.15
|
|
// VAT = Amount - Base
|
|
total = sum;
|
|
const base = total / 1.15;
|
|
vat = total - base;
|
|
subtotal = base;
|
|
} else if (vatType === 'EXCLUDED') {
|
|
// Forward calc: Sum is Subtotal. Add VAT on top.
|
|
subtotal = sum;
|
|
vat = subtotal * 0.15;
|
|
total = subtotal + vat;
|
|
} else {
|
|
// NONE
|
|
subtotal = sum;
|
|
vat = 0;
|
|
total = sum;
|
|
}
|
|
|
|
displaySubtotal.textContent = subtotal.toFixed(2);
|
|
displayVat.textContent = vat.toFixed(2);
|
|
displayTotal.textContent = total.toFixed(2);
|
|
}
|
|
|
|
// Add Row
|
|
addItemBtn.addEventListener('click', function() {
|
|
const formIdx = parseInt(totalForms.value);
|
|
const newHtml = emptyFormHtml.replace(/__prefix__/g, formIdx);
|
|
const newRow = document.createElement('div');
|
|
newRow.innerHTML = newHtml; // wrapper
|
|
|
|
// Unwrap logic to append cleanly
|
|
while (newRow.firstChild) {
|
|
itemsContainer.appendChild(newRow.firstChild);
|
|
}
|
|
|
|
totalForms.value = formIdx + 1;
|
|
updateCalculations();
|
|
});
|
|
|
|
// Delete Row (Event Delegation)
|
|
itemsContainer.addEventListener('click', function(e) {
|
|
if (e.target.closest('.delete-row')) {
|
|
const row = e.target.closest('.item-row');
|
|
const deleteCheckbox = row.querySelector('input[name$="-DELETE"]');
|
|
if (deleteCheckbox) {
|
|
deleteCheckbox.checked = true;
|
|
row.classList.add('d-none', 'deleted');
|
|
} else {
|
|
row.remove();
|
|
}
|
|
updateCalculations();
|
|
}
|
|
});
|
|
|
|
// Input Changes
|
|
itemsContainer.addEventListener('input', function(e) {
|
|
if (e.target.classList.contains('item-amount')) {
|
|
updateCalculations();
|
|
}
|
|
});
|
|
|
|
// VAT Change
|
|
vatRadios.forEach(r => {
|
|
r.addEventListener('change', updateCalculations);
|
|
});
|
|
|
|
// Initial Calc
|
|
updateCalculations();
|
|
});
|
|
</script>
|
|
{% endblock %} |