diff --git a/config/__pycache__/settings.cpython-311.pyc b/config/__pycache__/settings.cpython-311.pyc index d79d6a7..f942531 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 8cf22af..9c5f7f9 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 291d043..c03dda4 100644 --- a/config/settings.py +++ b/config/settings.py @@ -37,17 +37,11 @@ CSRF_TRUSTED_ORIGINS = [ for host in CSRF_TRUSTED_ORIGINS ] -# Cookies must always be HTTPS-only; SameSite=Lax keeps CSRF working behind the proxy. SESSION_COOKIE_SECURE = True CSRF_COOKIE_SECURE = True SESSION_COOKIE_SAMESITE = "None" CSRF_COOKIE_SAMESITE = "None" -# Quick-start development settings - unsuitable for production -# See https://docs.djangoproject.com/en/5.2/howto/deployment/checklist/ - -# Application definition - INSTALLED_APPS = [ 'django.contrib.admin', 'django.contrib.auth', @@ -65,8 +59,6 @@ MIDDLEWARE = [ 'django.middleware.csrf.CsrfViewMiddleware', 'django.contrib.auth.middleware.AuthenticationMiddleware', 'django.contrib.messages.middleware.MessageMiddleware', - # Disable X-Frame-Options middleware to allow Flatlogic preview iframes. - # 'django.middleware.clickjacking.XFrameOptionsMiddleware', ] X_FRAME_OPTIONS = 'ALLOWALL' @@ -83,7 +75,6 @@ TEMPLATES = [ 'django.template.context_processors.request', 'django.contrib.auth.context_processors.auth', 'django.contrib.messages.context_processors.messages', - # IMPORTANT: do not remove – injects PROJECT_DESCRIPTION/PROJECT_IMAGE_URL and cache-busting timestamp 'core.context_processors.project_context', ], }, @@ -92,10 +83,6 @@ TEMPLATES = [ WSGI_APPLICATION = 'config.wsgi.application' - -# Database -# https://docs.djangoproject.com/en/5.2/ref/settings/#databases - DATABASES = { 'default': { 'ENGINE': 'django.db.backends.mysql', @@ -110,10 +97,6 @@ DATABASES = { }, } - -# Password validation -# https://docs.djangoproject.com/en/5.2/ref/settings/#auth-password-validators - AUTH_PASSWORD_VALIDATORS = [ { 'NAME': 'django.contrib.auth.password_validation.UserAttributeSimilarityValidator', @@ -129,33 +112,22 @@ AUTH_PASSWORD_VALIDATORS = [ }, ] - -# Internationalization -# https://docs.djangoproject.com/en/5.2/topics/i18n/ - LANGUAGE_CODE = 'en-us' - TIME_ZONE = 'UTC' - USE_I18N = True - USE_TZ = True - -# Static files (CSS, JavaScript, Images) -# https://docs.djangoproject.com/en/5.2/howto/static-files/ - STATIC_URL = 'static/' -# Collect static into a separate folder; avoid overlapping with STATICFILES_DIRS. STATIC_ROOT = BASE_DIR / 'staticfiles' - STATICFILES_DIRS = [ BASE_DIR / 'static', BASE_DIR / 'assets', BASE_DIR / 'node_modules', ] -# Email +MEDIA_URL = 'media/' +MEDIA_ROOT = BASE_DIR / 'media' + EMAIL_BACKEND = os.getenv( "EMAIL_BACKEND", "django.core.mail.backends.smtp.EmailBackend" @@ -173,10 +145,11 @@ CONTACT_EMAIL_TO = [ if item.strip() ] -# When both TLS and SSL flags are enabled, prefer SSL explicitly if EMAIL_USE_SSL: EMAIL_USE_TLS = False -# Default primary key field type -# https://docs.djangoproject.com/en/5.2/ref/settings/#default-auto-field + +LOGIN_URL = 'login' +LOGIN_REDIRECT_URL = 'home' +LOGOUT_REDIRECT_URL = 'home' DEFAULT_AUTO_FIELD = 'django.db.models.BigAutoField' diff --git a/config/urls.py b/config/urls.py index bcfc074..87f0aa2 100644 --- a/config/urls.py +++ b/config/urls.py @@ -1,23 +1,7 @@ -""" -URL configuration for config project. - -The `urlpatterns` list routes URLs to views. For more information please see: - https://docs.djangoproject.com/en/5.2/topics/http/urls/ -Examples: -Function views - 1. Add an import: from my_app import views - 2. Add a URL to urlpatterns: path('', views.home, name='home') -Class-based views - 1. Add an import: from other_app.views import Home - 2. Add a URL to urlpatterns: path('', Home.as_view(), name='home') -Including another URLconf - 1. Import the include() function: from django.urls import include, path - 2. Add a URL to urlpatterns: path('blog/', include('blog.urls')) -""" -from django.contrib import admin -from django.urls import include, path from django.conf import settings from django.conf.urls.static import static +from django.contrib import admin +from django.urls import include, path urlpatterns = [ path("admin/", admin.site.urls), @@ -25,5 +9,6 @@ urlpatterns = [ ] if settings.DEBUG: + urlpatterns += static(settings.MEDIA_URL, document_root=settings.MEDIA_ROOT) urlpatterns += static("/assets/", document_root=settings.BASE_DIR / "assets") urlpatterns += static(settings.STATIC_URL, document_root=settings.STATIC_ROOT) diff --git a/core/__pycache__/admin.cpython-311.pyc b/core/__pycache__/admin.cpython-311.pyc index 5e8987a..aaa935c 100644 Binary files a/core/__pycache__/admin.cpython-311.pyc and b/core/__pycache__/admin.cpython-311.pyc differ diff --git a/core/__pycache__/forms.cpython-311.pyc b/core/__pycache__/forms.cpython-311.pyc new file mode 100644 index 0000000..9ff84cf Binary files /dev/null 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 a251b5f..fb3db03 100644 Binary files a/core/__pycache__/models.cpython-311.pyc and b/core/__pycache__/models.cpython-311.pyc differ diff --git a/core/__pycache__/tests.cpython-311.pyc b/core/__pycache__/tests.cpython-311.pyc new file mode 100644 index 0000000..a57262b Binary files /dev/null and b/core/__pycache__/tests.cpython-311.pyc differ diff --git a/core/__pycache__/urls.cpython-311.pyc b/core/__pycache__/urls.cpython-311.pyc index f705988..af00d75 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 2f0989c..6e6731f 100644 Binary files a/core/__pycache__/views.cpython-311.pyc and b/core/__pycache__/views.cpython-311.pyc differ diff --git a/core/admin.py b/core/admin.py index 8c38f3f..ede5b17 100644 --- a/core/admin.py +++ b/core/admin.py @@ -1,3 +1,17 @@ from django.contrib import admin -# Register your models here. +from .models import BusinessProfile, Transaction + + +@admin.register(BusinessProfile) +class BusinessProfileAdmin(admin.ModelAdmin): + list_display = ('business_name', 'user', 'opening_ecash', 'opening_physical_cash', 'current_ecash', 'current_physical_cash') + search_fields = ('business_name', 'user__username', 'user__email') + + +@admin.register(Transaction) +class TransactionAdmin(admin.ModelAdmin): + list_display = ('client_name', 'transaction_type', 'amount', 'service_charge', 'business', 'created_at') + list_filter = ('transaction_type', 'created_at') + search_fields = ('client_name', 'business__business_name', 'created_by__username') + date_hierarchy = 'created_at' diff --git a/core/forms.py b/core/forms.py new file mode 100644 index 0000000..f3cfd94 --- /dev/null +++ b/core/forms.py @@ -0,0 +1,154 @@ +from decimal import Decimal + +from django import forms +from django.contrib.auth.forms import AuthenticationForm, UserCreationForm +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.utils import timezone + +from .models import BusinessProfile, Transaction + + +INPUT_CLASS = 'form-control form-control-lg momo-input' +SELECT_CLASS = 'form-select form-select-lg momo-input' + + +class SignUpForm(UserCreationForm): + first_name = forms.CharField(max_length=150, required=True, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'First name'})) + last_name = forms.CharField(max_length=150, required=True, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Last name'})) + email = forms.EmailField(required=True, widget=forms.EmailInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Email address'})) + + class Meta(UserCreationForm.Meta): + model = User + fields = ('first_name', 'last_name', 'username', 'email') + widgets = { + 'username': forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Username'}), + } + + def __init__(self, *args, **kwargs): + super().__init__(*args, **kwargs) + self.fields['username'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Username'}) + self.fields['password1'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Password'}) + self.fields['password2'].widget.attrs.update({'class': INPUT_CLASS, 'placeholder': 'Confirm password'}) + + def save(self, commit=True): + user = super().save(commit=False) + user.first_name = self.cleaned_data['first_name'] + user.last_name = self.cleaned_data['last_name'] + user.email = self.cleaned_data['email'] + if commit: + user.save() + return user + + +class LoginForm(AuthenticationForm): + username = forms.CharField(widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Username'})) + password = forms.CharField(widget=forms.PasswordInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Password'})) + + +class BusinessProfileForm(forms.ModelForm): + class Meta: + model = BusinessProfile + fields = ['business_name', 'logo', 'opening_ecash', 'opening_physical_cash'] + widgets = { + 'business_name': forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'e.g. BrightPay MoMo Point'}), + 'logo': forms.ClearableFileInput(attrs={'class': 'form-control momo-file', 'accept': 'image/*'}), + 'opening_ecash': forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0'}), + 'opening_physical_cash': forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0'}), + } + help_texts = { + 'opening_ecash': 'Used as your starting e-cash wallet for the first setup.', + 'opening_physical_cash': 'Used as your starting notes/coins balance for the first setup.', + } + + def save(self, commit=True): + profile = super().save(commit=False) + should_sync = not profile.pk or not profile.transactions.exists() + if should_sync: + profile.sync_current_to_opening() + if commit: + profile.save() + return profile + + +class TransactionForm(forms.Form): + client_name = forms.CharField(max_length=120, widget=forms.TextInput(attrs={'class': INPUT_CLASS, 'placeholder': 'Client name'})) + amount = forms.DecimalField(max_digits=12, decimal_places=2, min_value=Decimal('0.01'), widget=forms.NumberInput(attrs={'class': INPUT_CLASS, 'step': '0.01', 'min': '0.01'})) + transaction_type = forms.ChoiceField(choices=Transaction.TYPE_CHOICES, widget=forms.Select(attrs={'class': SELECT_CLASS})) + notes = forms.CharField(required=False, widget=forms.Textarea(attrs={'class': 'form-control momo-input', 'rows': 3, 'placeholder': 'Optional note about the transaction'})) + + def __init__(self, *args, business=None, **kwargs): + self.business = business + super().__init__(*args, **kwargs) + + def clean(self): + cleaned_data = super().clean() + transaction_type = cleaned_data.get('transaction_type') + amount = cleaned_data.get('amount') + if not self.business or not transaction_type or amount is None: + return cleaned_data + + ecash_delta, physical_delta, _ = Transaction.calculate_effect(transaction_type, amount) + if self.business.current_ecash + ecash_delta < 0: + self.add_error('amount', 'Not enough e-cash for this transaction.') + if self.business.current_physical_cash + physical_delta < 0: + self.add_error('amount', 'Not enough physical cash for this transaction.') + return cleaned_data + + def save(self, user): + if not self.business: + raise ValidationError('Business profile is required.') + return Transaction.create_logged_transaction( + business=self.business, + user=user, + client_name=self.cleaned_data['client_name'], + amount=self.cleaned_data['amount'], + transaction_type=self.cleaned_data['transaction_type'], + notes=self.cleaned_data['notes'], + ) + + +class ReportFilterForm(forms.Form): + PERIOD_CHOICES = [ + ('daily', 'Daily'), + ('weekly', 'Weekly'), + ('monthly', 'Monthly'), + ('yearly', 'Yearly'), + ('custom', 'Custom range'), + ] + + period = forms.ChoiceField(choices=PERIOD_CHOICES, initial='daily', widget=forms.Select(attrs={'class': 'form-select momo-input'})) + start_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control momo-input', 'type': 'date'})) + end_date = forms.DateField(required=False, widget=forms.DateInput(attrs={'class': 'form-control momo-input', 'type': 'date'})) + + def clean(self): + cleaned_data = super().clean() + period = cleaned_data.get('period') + start_date = cleaned_data.get('start_date') + end_date = cleaned_data.get('end_date') + if period == 'custom': + if not start_date or not end_date: + raise forms.ValidationError('Choose both a start and end date for a custom report.') + if end_date < start_date: + raise forms.ValidationError('End date cannot be before start date.') + return cleaned_data + + def get_range(self): + today = timezone.localdate() + period = self.cleaned_data.get('period') or 'daily' + if period == 'daily': + return today, today + if period == 'weekly': + start = today - timezone.timedelta(days=today.weekday()) + return start, start + timezone.timedelta(days=6) + if period == 'monthly': + start = today.replace(day=1) + if start.month == 12: + next_month = start.replace(year=start.year + 1, month=1, day=1) + else: + next_month = start.replace(month=start.month + 1, day=1) + return start, next_month - timezone.timedelta(days=1) + if period == 'yearly': + start = today.replace(month=1, day=1) + return start, today.replace(month=12, day=31) + return self.cleaned_data['start_date'], self.cleaned_data['end_date'] diff --git a/core/migrations/0001_initial.py b/core/migrations/0001_initial.py new file mode 100644 index 0000000..ce3ccd4 --- /dev/null +++ b/core/migrations/0001_initial.py @@ -0,0 +1,57 @@ +# Generated by Django 5.2.7 on 2026-04-17 02:21 + +import django.db.models.deletion +from decimal import Decimal +from django.conf import settings +from django.db import migrations, models + + +class Migration(migrations.Migration): + + initial = True + + dependencies = [ + migrations.swappable_dependency(settings.AUTH_USER_MODEL), + ] + + operations = [ + migrations.CreateModel( + name='BusinessProfile', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('business_name', models.CharField(blank=True, max_length=120)), + ('logo', models.FileField(blank=True, null=True, upload_to='business_logos/')), + ('opening_ecash', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('opening_physical_cash', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('current_ecash', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('current_physical_cash', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('updated_at', models.DateTimeField(auto_now=True)), + ('user', models.OneToOneField(on_delete=django.db.models.deletion.CASCADE, related_name='business_profile', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['user__username'], + }, + ), + migrations.CreateModel( + name='Transaction', + fields=[ + ('id', models.BigAutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('client_name', models.CharField(max_length=120)), + ('amount', models.DecimalField(decimal_places=2, max_digits=12)), + ('transaction_type', models.CharField(choices=[('cash_out', 'Cash-out'), ('cash_in', 'Cash-In'), ('sending', 'Sending'), ('airtime', 'Airtime'), ('transfer', 'Transfer'), ('debt', 'Debt'), ('expenditure', 'Expenditure'), ('credit', 'Credit')], max_length=20)), + ('service_charge', models.DecimalField(decimal_places=2, default=Decimal('0.00'), max_digits=12)), + ('notes', models.CharField(blank=True, max_length=255)), + ('ecash_before', models.DecimalField(decimal_places=2, max_digits=12)), + ('ecash_after', models.DecimalField(decimal_places=2, max_digits=12)), + ('physical_before', models.DecimalField(decimal_places=2, max_digits=12)), + ('physical_after', models.DecimalField(decimal_places=2, max_digits=12)), + ('created_at', models.DateTimeField(auto_now_add=True)), + ('business', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='transactions', to='core.businessprofile')), + ('created_by', models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, related_name='momo_transactions', to=settings.AUTH_USER_MODEL)), + ], + options={ + 'ordering': ['-created_at', '-id'], + }, + ), + ] diff --git a/core/migrations/__pycache__/0001_initial.cpython-311.pyc b/core/migrations/__pycache__/0001_initial.cpython-311.pyc new file mode 100644 index 0000000..1c82493 Binary files /dev/null and b/core/migrations/__pycache__/0001_initial.cpython-311.pyc differ diff --git a/core/models.py b/core/models.py index 71a8362..e3297bd 100644 --- a/core/models.py +++ b/core/models.py @@ -1,3 +1,196 @@ -from django.db import models +from decimal import Decimal, ROUND_HALF_UP -# Create your models here. +from django.contrib.auth.models import User +from django.core.exceptions import ValidationError +from django.db import models, transaction + + +class BusinessProfile(models.Model): + user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='business_profile') + business_name = models.CharField(max_length=120, blank=True) + logo = models.FileField(upload_to='business_logos/', blank=True, null=True) + opening_ecash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + opening_physical_cash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + current_ecash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + current_physical_cash = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + created_at = models.DateTimeField(auto_now_add=True) + updated_at = models.DateTimeField(auto_now=True) + + class Meta: + ordering = ['user__username'] + + def __str__(self): + return self.business_name or f"{self.user.username}'s MoMo Business" + + @property + def owner_label(self): + return self.user.get_full_name() or self.user.username + + @property + def total_cash(self): + return (self.current_ecash or Decimal('0.00')) + (self.current_physical_cash or Decimal('0.00')) + + def sync_current_to_opening(self): + self.current_ecash = self.opening_ecash + self.current_physical_cash = self.opening_physical_cash + + +class Transaction(models.Model): + CASH_OUT = 'cash_out' + CASH_IN = 'cash_in' + SENDING = 'sending' + AIRTIME = 'airtime' + TRANSFER = 'transfer' + DEBT = 'debt' + EXPENDITURE = 'expenditure' + CREDIT = 'credit' + + TYPE_CHOICES = [ + (CASH_OUT, 'Cash-out'), + (CASH_IN, 'Cash-In'), + (SENDING, 'Sending'), + (AIRTIME, 'Airtime'), + (TRANSFER, 'Transfer'), + (DEBT, 'Debt'), + (EXPENDITURE, 'Expenditure'), + (CREDIT, 'Credit'), + ] + + business = models.ForeignKey(BusinessProfile, on_delete=models.CASCADE, related_name='transactions') + created_by = models.ForeignKey(User, on_delete=models.CASCADE, related_name='momo_transactions') + client_name = models.CharField(max_length=120) + amount = models.DecimalField(max_digits=12, decimal_places=2) + transaction_type = models.CharField(max_length=20, choices=TYPE_CHOICES) + service_charge = models.DecimalField(max_digits=12, decimal_places=2, default=Decimal('0.00')) + notes = models.CharField(max_length=255, blank=True) + ecash_before = models.DecimalField(max_digits=12, decimal_places=2) + ecash_after = models.DecimalField(max_digits=12, decimal_places=2) + physical_before = models.DecimalField(max_digits=12, decimal_places=2) + physical_after = models.DecimalField(max_digits=12, decimal_places=2) + created_at = models.DateTimeField(auto_now_add=True) + + class Meta: + ordering = ['-created_at', '-id'] + + def __str__(self): + return f"{self.get_transaction_type_display()} · {self.client_name} · {self.amount}" + + @staticmethod + def _round(value: Decimal) -> Decimal: + return value.quantize(Decimal('0.01'), rounding=ROUND_HALF_UP) + + @classmethod + def calculate_effect(cls, transaction_type: str, amount: Decimal) -> tuple[Decimal, Decimal, Decimal]: + amount = cls._round(amount) + fee = Decimal('0.00') + ecash_delta = Decimal('0.00') + physical_delta = Decimal('0.00') + + if transaction_type == cls.CASH_IN: + ecash_delta = -amount + physical_delta = amount + elif transaction_type == cls.CASH_OUT: + ecash_delta = amount + physical_delta = -amount + elif transaction_type in {cls.AIRTIME, cls.TRANSFER}: + ecash_delta = -amount + physical_delta = amount + elif transaction_type == cls.SENDING: + fee = cls._round(amount * Decimal('0.01')) + ecash_delta = -amount + physical_delta = amount + fee + elif transaction_type in {cls.DEBT, cls.EXPENDITURE}: + physical_delta = -amount + elif transaction_type == cls.CREDIT: + physical_delta = amount + else: + raise ValidationError('Unsupported transaction type.') + + return cls._round(ecash_delta), cls._round(physical_delta), cls._round(fee) + + @classmethod + @transaction.atomic + def create_logged_transaction( + cls, + *, + business: BusinessProfile, + user: User, + client_name: str, + amount: Decimal, + transaction_type: str, + notes: str = '', + ): + ecash_delta, physical_delta, fee = cls.calculate_effect(transaction_type, amount) + ecash_before = cls._round(business.current_ecash) + physical_before = cls._round(business.current_physical_cash) + ecash_after = cls._round(ecash_before + ecash_delta) + physical_after = cls._round(physical_before + physical_delta) + + if ecash_after < 0: + raise ValidationError('This transaction would make e-cash go below zero.') + if physical_after < 0: + raise ValidationError('This transaction would make physical cash go below zero.') + + entry = cls.objects.create( + business=business, + created_by=user, + client_name=client_name, + amount=cls._round(amount), + transaction_type=transaction_type, + service_charge=fee, + notes=notes, + ecash_before=ecash_before, + ecash_after=ecash_after, + physical_before=physical_before, + physical_after=physical_after, + ) + + business.current_ecash = ecash_after + business.current_physical_cash = physical_after + business.save(update_fields=['current_ecash', 'current_physical_cash', 'updated_at']) + return entry + + @classmethod + def rebalance_business_ledger(cls, business: BusinessProfile): + business = BusinessProfile.objects.select_for_update().get(pk=business.pk) + ecash_balance = cls._round(business.opening_ecash) + physical_balance = cls._round(business.opening_physical_cash) + + entries = list( + cls.objects.select_for_update() + .filter(business=business) + .order_by('created_at', 'id') + ) + for entry in entries: + ecash_delta, physical_delta, fee = cls.calculate_effect(entry.transaction_type, entry.amount) + ecash_before = ecash_balance + physical_before = physical_balance + ecash_after = cls._round(ecash_before + ecash_delta) + physical_after = cls._round(physical_before + physical_delta) + + cls.objects.filter(pk=entry.pk).update( + service_charge=fee, + ecash_before=ecash_before, + ecash_after=ecash_after, + physical_before=physical_before, + physical_after=physical_after, + ) + + ecash_balance = ecash_after + physical_balance = physical_after + + business.current_ecash = ecash_balance + business.current_physical_cash = physical_balance + business.save(update_fields=['current_ecash', 'current_physical_cash', 'updated_at']) + return business + + @transaction.atomic + def delete_logged_transaction(self): + business = BusinessProfile.objects.select_for_update().get(pk=self.business_id) + transaction_id = self.pk + self.delete() + business = self.__class__.rebalance_business_ledger(business) + return { + 'transaction_id': transaction_id, + 'business': business, + } diff --git a/core/templates/base.html b/core/templates/base.html index 1e7e5fb..1bae298 100644 --- a/core/templates/base.html +++ b/core/templates/base.html @@ -1,25 +1,72 @@ -
-