diff --git a/core/__pycache__/models.cpython-311.pyc b/core/__pycache__/models.cpython-311.pyc index 508f24d..a225337 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 e4ccd77..0f074b3 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 8e87872..219f92b 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/migrations/0019_transaction.py b/core/migrations/0019_transaction.py new file mode 100644 index 0000000..6cb89ed --- /dev/null +++ b/core/migrations/0019_transaction.py @@ -0,0 +1,37 @@ +# Generated by Django 5.2.7 on 2026-01-24 04:34 + +import django.db.models.deletion +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('core', '0018_remove_appsetting_annual_fee_and_more'), + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('amount', models.DecimalField(decimal_places=2, max_digits=10, verbose_name='Amount')), + ('transaction_type', models.CharField(choices=[('PAYMENT', 'Payment'), ('REFUND', 'Refund')], default='PAYMENT', max_length=20)), + ('status', models.CharField(choices=[('PENDING', 'Pending'), ('COMPLETED', 'Completed'), ('FAILED', 'Failed'), ('CANCELLED', 'Cancelled')], default='COMPLETED', max_length=20)), + ('description', models.TextField(blank=True, verbose_name='Description')), + ('payment_method', models.CharField(blank=True, max_length=100, verbose_name='Payment Method')), + ('reference_number', models.CharField(blank=True, max_length=100, verbose_name='Reference Number')), + ('receipt_number', models.CharField(blank=True, max_length=20, unique=True, verbose_name='Receipt Number')), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'verbose_name': 'Transaction', + 'verbose_name_plural': 'Transactions', + 'ordering': ['-created_at'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0019_transaction.cpython-311.pyc b/core/migrations/__pycache__/0019_transaction.cpython-311.pyc new file mode 100644 index 0000000..9e3ab9d Binary files /dev/null and b/core/migrations/__pycache__/0019_transaction.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index cf5ba9c..08412f8 100644 --- a/core/models.py +++ b/core/models.py @@ -380,4 +380,42 @@ def sync_user_groups(sender, instance, **kwargs): instance.user.groups.remove(*other_groups) # Add user to the correct group - instance.user.groups.add(group) \ No newline at end of file + instance.user.groups.add(group) +class Transaction(models.Model): + TRANSACTION_TYPES = ( + ('PAYMENT', _('Payment')), + ('REFUND', _('Refund')), + ) + STATUS_CHOICES = ( + ('PENDING', _('Pending')), + ('COMPLETED', _('Completed')), + ('FAILED', _('Failed')), + ('CANCELLED', _('Cancelled')), + ) + user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='transactions') + amount = models.DecimalField(_('Amount'), max_digits=10, decimal_places=2) + transaction_type = models.CharField(max_length=20, choices=TRANSACTION_TYPES, default='PAYMENT') + status = models.CharField(max_length=20, choices=STATUS_CHOICES, default='COMPLETED') + description = models.TextField(_('Description'), blank=True) + payment_method = models.CharField(_('Payment Method'), max_length=100, blank=True) + reference_number = models.CharField(_('Reference Number'), max_length=100, blank=True) + receipt_number = models.CharField(_('Receipt Number'), max_length=20, unique=True, blank=True) + + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + verbose_name = _('Transaction') + verbose_name_plural = _('Transactions') + ordering = ['-created_at'] + + def save(self, *args, **kwargs): + if not self.receipt_number: + # Generate a unique receipt number: REC-YYYYMMDD-XXXX + date_str = timezone.now().strftime('%Y%m%d') + random_str = ''.join(random.choices(string.ascii_uppercase + string.digits, k=4)) + self.receipt_number = f"REC-{date_str}-{random_str}" + super().save(*args, **kwargs) + + def __str__(self): + return f"{self.receipt_number} - {self.user.username} ({self.amount})" diff --git a/core/templates/core/admin_dashboard.html b/core/templates/core/admin_dashboard.html index 9ed20dd..37bf693 100644 --- a/core/templates/core/admin_dashboard.html +++ b/core/templates/core/admin_dashboard.html @@ -303,6 +303,19 @@

{% trans "Quick Actions" %}

+ + +
diff --git a/core/templates/core/admin_financials.html b/core/templates/core/admin_financials.html new file mode 100644 index 0000000..f767ef9 --- /dev/null +++ b/core/templates/core/admin_financials.html @@ -0,0 +1,71 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+
+
+

{% trans "Financial Overview" %}

+

{% trans "Manage all platform transactions, payments, and revenue." %}

+
+
+
{% trans "Total Revenue" %}
+

{{ total_revenue }}

+
+
+ +
+
+
{% trans "Recent Transactions" %}
+
+
+
+ + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + {% endfor %} + +
{% trans "User" %}{% trans "Role" %}{% trans "Receipt #" %}{% trans "Date" %}{% trans "Amount" %}{% trans "Status" %}{% trans "Action" %}
+ {{ transaction.user.username }}
+ {{ transaction.user.email }} +
+ {{ transaction.user.profile.get_role_display }} + {{ transaction.receipt_number }}{{ transaction.created_at|date:"Y-m-d" }}{{ transaction.amount }} + + {{ transaction.get_status_display }} + + + + + {% if transaction.transaction_type == 'PAYMENT' and transaction.status == 'COMPLETED' %} + + + + {% endif %} + +
+
+
+
+
+{% endblock %} diff --git a/core/templates/core/financial_history.html b/core/templates/core/financial_history.html new file mode 100644 index 0000000..ff1e3af --- /dev/null +++ b/core/templates/core/financial_history.html @@ -0,0 +1,67 @@ +{% extends 'base.html' %} +{% load i18n %} + +{% block content %} +
+ + +
+
+ {% if transactions %} +
+ + + + + + + + + + + + + + {% for transaction in transactions %} + + + + + + + + + + {% endfor %} + +
{% trans "Receipt #" %}{% trans "Date" %}{% trans "Description" %}{% trans "Type" %}{% trans "Amount" %}{% trans "Status" %}{% trans "Action" %}
{{ transaction.receipt_number }}{{ transaction.created_at|date:"Y-m-d H:i" }}{{ transaction.description }} + {% if transaction.transaction_type == 'PAYMENT' %} + {% trans "Payment" %} + {% else %} + {% trans "Refund" %} + {% endif %} + {{ transaction.amount }} + + {{ transaction.get_status_display }} + + + + {% trans "Receipt" %} + +
+
+ {% else %} +
+ +

{% trans "No transactions found." %}

+
+ {% endif %} +
+
+
+{% endblock %} diff --git a/core/templates/core/receipt.html b/core/templates/core/receipt.html new file mode 100644 index 0000000..2bb48bf --- /dev/null +++ b/core/templates/core/receipt.html @@ -0,0 +1,111 @@ +{% extends 'base.html' %} +{% load i18n static %} + +{% block content %} +
+
+
+
+
+
+ {% if app_settings.logo %} + {{ app_settings.app_name }} + {% else %} +

{{ app_settings.app_name|default:"MASAR CARGO" }}

+ {% endif %} +

+ {{ app_settings.contact_address|linebreaksbr }}
+ {% trans "Phone:" %} {{ app_settings.contact_phone }}
+ {% trans "Email:" %} {{ app_settings.contact_email }} +

+
+
+

{% trans "Receipt" %}

+

{% trans "Receipt #" %}: {{ transaction.receipt_number }}

+

{% trans "Date" %}: {{ transaction.created_at|date:"Y-m-d" }}

+
+
+ +
+ +
+
+
{% trans "Bill To" %}
+

{{ transaction.user.get_full_name|default:transaction.user.username }}

+

{{ transaction.user.email }}

+

{{ transaction.user.profile.full_phone_number }}

+
+
+
{% trans "Payment Method" %}
+

{{ transaction.payment_method|default:"N/A" }}

+
+
+ +
+ + + + + + + + + + + + + + + + + + + +
{% trans "Description" %}{% trans "Total" %}
{{ transaction.description }}{{ transaction.amount }}
{% trans "Total Paid" %}{{ transaction.amount }}
+
+ +
+

{% trans "Note:" %} {% trans "This is an electronically generated receipt and does not require a physical signature." %}

+
+ +
+

{% trans "Thank you for choosing MASAR CARGO!" %}

+ {% if app_settings.registration_number %} +

{% trans "CR:" %} {{ app_settings.registration_number }} | {% trans "VAT:" %} {{ app_settings.tax_number }}

+ {% endif %} +
+
+ +
+ + + {% trans "Back to History" %} + +
+
+
+
+ + +{% endblock %} diff --git a/core/templates/core/shipper_dashboard.html b/core/templates/core/shipper_dashboard.html index 4a72bcb..e0ddd95 100644 --- a/core/templates/core/shipper_dashboard.html +++ b/core/templates/core/shipper_dashboard.html @@ -17,10 +17,65 @@ {% block content %}
-
-

{% trans "Shipper Dashboard" %}

-

{% trans "Manage your shipping offers and active shipments." %}

+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+

{% trans "Shipper Dashboard" %}

+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+

{% trans "Manage your shipping offers and active shipments." %}

+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+
+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
{% trans "Add A Bid" %} diff --git a/core/templates/core/truck_owner_dashboard.html b/core/templates/core/truck_owner_dashboard.html index d7b068a..fc33194 100644 --- a/core/templates/core/truck_owner_dashboard.html +++ b/core/templates/core/truck_owner_dashboard.html @@ -18,10 +18,65 @@ {% block content %}
-
-

{% trans "Truck Owner Dashboard" %}

-

{% trans "Manage your fleet and incoming shipping offers." %}

+
+
+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+

{% trans "Truck Owner Dashboard" %}

+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+

{% trans "Manage your fleet and incoming shipping offers." %}

+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
+
+
+
+
+
+
{% trans "Financials" %}
+

{% trans "Payments" %}

+ {% trans "View History" %} +
+
+
+
{% trans "Add New Truck" %} diff --git a/core/urls.py b/core/urls.py index a2f06e9..23d8bc1 100644 --- a/core/urls.py +++ b/core/urls.py @@ -24,4 +24,8 @@ urlpatterns = [ path("terms-of-service/", views.terms_of_service, name="terms_of_service"), path("subscription-expired/", views.subscription_expired, name="subscription_expired"), path("subscription-renew/", views.renew_subscription, name="renew_subscription"), + path("financial-history/", views.financial_history, name="financial_history"), + path("receipt//", views.transaction_receipt, name="transaction_receipt"), + path("admin/financials/", views.admin_financials, name="admin_financials"), + path("admin/refund//", views.issue_refund, name="issue_refund"), ] \ No newline at end of file diff --git a/core/views.py b/core/views.py index 8ee03a0..454b70b 100644 --- a/core/views.py +++ b/core/views.py @@ -3,7 +3,7 @@ from django.shortcuts import render, redirect, get_object_or_404 from django.contrib.auth.decorators import login_required from django.contrib.auth import login, authenticate, logout from django.utils import timezone -from .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City, AppSetting, Banner, HomeSection +from .models import Profile, Truck, Shipment, Bid, Message, OTPCode, Country, City, AppSetting, Banner, HomeSection, Transaction from .forms import TruckForm, ShipmentForm, BidForm, UserRegistrationForm, OTPVerifyForm, ShipperOfferForm, RenewSubscriptionForm from django.contrib import messages from django.utils.translation import gettext as _ @@ -485,6 +485,15 @@ def renew_subscription(request): if form.is_valid(): profile = request.user.profile plan = form.cleaned_data['subscription_plan'] + + # Calculate amount based on role and plan + app_settings = AppSetting.objects.first() + amount = 0 + if profile.role == 'SHIPPER': + amount = app_settings.shipper_monthly_fee if plan == 'MONTHLY' else app_settings.shipper_annual_fee + elif profile.role == 'TRUCK_OWNER': + amount = app_settings.truck_owner_monthly_fee if plan == 'MONTHLY' else app_settings.truck_owner_annual_fee + profile.subscription_plan = plan profile.is_subscription_active = True @@ -494,7 +503,17 @@ def renew_subscription(request): profile.subscription_expiry = timezone.now().date() + timedelta(days=365) profile.save() - + + # Create Transaction record + Transaction.objects.create( + user=request.user, + amount=amount, + transaction_type='PAYMENT', + status='COMPLETED', + description=f"Subscription Renewal: {plan}", + payment_method="Online Payment" + ) + # Notifications expiry_date = profile.subscription_expiry.strftime('%Y-%m-%d') msg = _("Your subscription for MASAR CARGO has been successfully renewed! Your new expiry date is %(date)s. Thank you for using our service.") % {"date": expiry_date} @@ -512,7 +531,55 @@ def renew_subscription(request): [request.user.email], fail_silently=True, ) - + messages.success(request, _("Subscription renewed successfully!")) return redirect('dashboard') return redirect('subscription_expired') + +@login_required +def financial_history(request): + transactions = Transaction.objects.filter(user=request.user) + return render(request, 'core/financial_history.html', {'transactions': transactions}) + +@login_required +def transaction_receipt(request, receipt_number): + transaction = get_object_or_404(Transaction, receipt_number=receipt_number, user=request.user) + app_settings = AppSetting.objects.first() + return render(request, 'core/receipt.html', { + 'transaction': transaction, + 'app_settings': app_settings + }) + +@login_required +def admin_financials(request): + if request.user.profile.role != 'ADMIN': + return redirect('dashboard') + transactions = Transaction.objects.all() + total_revenue = sum(t.amount for t in transactions if t.transaction_type == 'PAYMENT' and t.status == 'COMPLETED') + return render(request, 'core/admin_financials.html', { + 'transactions': transactions, + 'total_revenue': total_revenue + }) + +@login_required +def issue_refund(request, receipt_number): + if request.user.profile.role != 'ADMIN': + return redirect('dashboard') + + transaction = get_object_or_404(Transaction, receipt_number=receipt_number) + if transaction.transaction_type == 'REFUND': + messages.error(request, _("This is already a refund transaction.")) + return redirect('admin_financials') + + # Create a refund transaction + refund = Transaction.objects.create( + user=transaction.user, + amount=transaction.amount, + transaction_type='REFUND', + status='COMPLETED', + description=f"Refund for Receipt: {transaction.receipt_number}", + payment_method=transaction.payment_method + ) + + messages.success(request, _("Refund issued successfully! Receipt: %(receipt)s") % {'receipt': refund.receipt_number}) + return redirect('admin_financials')