diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index e3cb086..99b0063 100644 Binary files a/config/__pycache__/settings.cpython-311.pyc and b/config/__pycache__/settings.cpython-311.pyc differ diff --git a/config/__pycache__/urls.cpython-311.pyc b/config/__pycache__/urls.cpython-311.pyc index 0b85e94..ecbd4a1 100644 Binary files a/config/__pycache__/urls.cpython-311.pyc and b/config/__pycache__/urls.cpython-311.pyc differ diff --git a/config/settings.py b/config/settings.py index a2b098a..714f84e 100644 --- a/config/settings.py +++ b/config/settings.py @@ -181,3 +181,8 @@ if EMAIL_USE_SSL: # https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' + +# Authentication +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'home' +LOGOUT_REDIRECT_URL = 'login' \ No newline at end of file diff --git a/config/urls.py b/config/urls.py index bcfc074..2cb6937 100644 --- a/config/urls.py +++ b/config/urls.py @@ -21,9 +21,10 @@ from django.conf.urls.static import static urlpatterns = [ path("admin/", admin.site.urls), + path("accounts/", include("django.contrib.auth.urls")), # Adds login, logout, password management path("", include("core.urls")), ] if settings.DEBUG: urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") - urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) + urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) \ No newline at end of file diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc index 3e95dc8..1b9f2f5 100644 Binary files a/core/__pycache__/forms.cpython-311.pyc and b/core/__pycache__/forms.cpython-311.pyc differ diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index a8b9e8e..c43fb03 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index a816d12..2b150b2 100644 Binary files a/core/__pycache__/urls.cpython-311.pyc and b/core/__pycache__/urls.cpython-311.pyc differ diff --git a/core/__pycache__/views.cpython-311.pyc b/core/__pycache__/views.cpython-311.pyc index 2f4097c..bed590a 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/forms.py b/core/forms.py index 17cd42a..3a149ed 100644 --- a/core/forms.py +++ b/core/forms.py @@ -1,5 +1,6 @@ from django import forms -from .models import WorkLog, Project, Worker, Team +from django.forms import inlineformset_factory +from .models import WorkLog, Project, Worker, Team, ExpenseReceipt, ExpenseLineItem class WorkLogForm(forms.ModelForm): end_date = forms.DateField( @@ -56,4 +57,27 @@ class WorkLogForm(forms.ModelForm): else: self.fields['project'].queryset = projects_qs self.fields['workers'].queryset = workers_qs - self.fields['team'].queryset = teams_qs \ No newline at end of file + self.fields['team'].queryset = teams_qs + +class ExpenseReceiptForm(forms.ModelForm): + class Meta: + model = ExpenseReceipt + fields = ['date', 'vendor', 'description', 'payment_method', 'vat_type'] + widgets = { + 'date': forms.DateInput(attrs={'type': 'date', 'class': 'form-control'}), + 'vendor': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Vendor Name'}), + 'description': forms.Textarea(attrs={'class': 'form-control', 'rows': 2}), + 'payment_method': forms.Select(attrs={'class': 'form-control'}), + 'vat_type': forms.RadioSelect(attrs={'class': 'form-check-input'}), + } + +ExpenseLineItemFormSet = inlineformset_factory( + ExpenseReceipt, ExpenseLineItem, + fields=['product', 'amount'], + extra=1, + can_delete=True, + widgets={ + 'product': forms.TextInput(attrs={'class': 'form-control', 'placeholder': 'Item Name'}), + 'amount': forms.NumberInput(attrs={'class': 'form-control item-amount', 'step': '0.01'}), + } +) diff --git a/core/migrations/0007_expensereceipt_expenselineitem.py b/core/migrations/0007_expensereceipt_expenselineitem.py new file mode 100644 index 0000000..91bd871 --- /dev/null +++ b/core/migrations/0007_expensereceipt_expenselineitem.py @@ -0,0 +1,42 @@ +# Generated by Django 5.2.7 on 2026-02-04 13:11 + +import django.db.models.deletion +import django.utils.timezone +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0006_alter_payrolladjustment_type'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='ExpenseReceipt', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('date', models.DateField(default=django.utils.timezone.now)), + ('vendor', models.CharField(max_length=200)), + ('description', models.TextField(blank=True)), + ('payment_method', models.CharField(choices=[('CASH', 'Cash'), ('CARD', 'Card'), ('EFT', 'EFT'), ('OTHER', 'Other')], default='CARD', max_length=10)), + ('vat_type', models.CharField(choices=[('INCLUDED', 'VAT Included'), ('EXCLUDED', 'VAT Excluded'), ('NONE', 'No VAT')], default='INCLUDED', max_length=10)), + ('subtotal', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('vat_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('total_amount', models.DecimalField(decimal_places=2, default=0, max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='generated_receipts', to=settings.AUTH_USER_MODEL)), + ], + ), + migrations.CreateModel( + name='ExpenseLineItem', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('product', models.CharField(max_length=255, verbose_name='Product/Item')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10)), + ('receipt', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='items', to='core.expensereceipt')), + ], + ), + ] diff --git a/core/migrations/__pycache__/0007_expensereceipt_expenselineitem.cpython-311.pyc b/core/migrations/__pycache__/0007_expensereceipt_expenselineitem.cpython-311.pyc new file mode 100644 index 0000000..499f0df Binary files /dev/null and b/core/migrations/__pycache__/0007_expensereceipt_expenselineitem.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 1f48574..ce5f02d 100644 --- a/core/models.py +++ b/core/models.py @@ -101,4 +101,42 @@ class PayrollAdjustment(models.Model): type = models.CharField(max_length=20, choices=ADJUSTMENT_TYPES, default='DEDUCTION') def __str__(self): - return f"{self.get_type_display()} - {self.amount} for {self.worker.name}" \ No newline at end of file + return f"{self.get_type_display()} - {self.amount} for {self.worker.name}" + +class ExpenseReceipt(models.Model): + VAT_CHOICES = [ + ('INCLUDED', 'VAT Included'), + ('EXCLUDED', 'VAT Excluded'), + ('NONE', 'No VAT'), + ] + PAYMENT_METHODS = [ + ('CASH', 'Cash'), + ('CARD', 'Card'), + ('EFT', 'EFT'), + ('OTHER', 'Other'), + ] + + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='generated_receipts') + date = models.DateField(default=timezone.now) + vendor = models.CharField(max_length=200) + description = models.TextField(blank=True) + payment_method = models.CharField(max_length=10, choices=PAYMENT_METHODS, default='CARD') + vat_type = models.CharField(max_length=10, choices=VAT_CHOICES, default='INCLUDED') + + # Financials (Stored for record keeping) + subtotal = models.DecimalField(max_digits=12, decimal_places=2, default=0) + vat_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0) + total_amount = models.DecimalField(max_digits=12, decimal_places=2, default=0) + + created_at = models.DateTimeField(auto_now_add=True) + + def __str__(self): + return f"Receipt from {self.vendor} - {self.date}" + +class ExpenseLineItem(models.Model): + receipt = models.ForeignKey(ExpenseReceipt, on_delete=models.CASCADE, related_name='items') + product = models.CharField(max_length=255, verbose_name="Product/Item") + amount = models.DecimalField(max_digits=10, decimal_places=2) + + def __str__(self): + return f"{self.product} - {self.amount}" diff --git a/core/templates/base.html b/core/templates/base.html index 65cf9e5..f1305e7 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -12,6 +12,8 @@ + + + +
+| Item | +Amount | +
|---|---|
| {{ item.product }} | +R {{ item.amount }} | +
Subtotal: R {{ receipt.subtotal }}
+VAT ({{ receipt.get_vat_type_display }}): R {{ receipt.vat_amount }}
+Total: R {{ receipt.total_amount }}
+