mirror of
https://github.com/eitchtee/WYGIWYH.git
synced 2026-07-13 08:12:51 +02:00
feat: multi tenancy support
This commit is contained in:
@@ -8,13 +8,14 @@ from apps.transactions.models import (
|
||||
RecurringTransaction,
|
||||
TransactionEntity,
|
||||
)
|
||||
from apps.common.admin import SharedObjectModelAdmin
|
||||
|
||||
|
||||
@admin.register(Transaction)
|
||||
class TransactionModelAdmin(admin.ModelAdmin):
|
||||
def get_queryset(self, request):
|
||||
# Use the all_objects manager to show all transactions, including deleted ones
|
||||
return self.model.all_objects.all()
|
||||
return self.model.userless_all_objects.all()
|
||||
|
||||
list_filter = ["deleted", "type", "is_paid", "date", "account"]
|
||||
|
||||
@@ -48,19 +49,29 @@ class TransactionInline(admin.TabularInline):
|
||||
|
||||
|
||||
@admin.register(InstallmentPlan)
|
||||
class InstallmentPlanAdmin(admin.ModelAdmin):
|
||||
class InstallmentPlanAdmin(SharedObjectModelAdmin):
|
||||
inlines = [
|
||||
TransactionInline,
|
||||
]
|
||||
|
||||
|
||||
@admin.register(RecurringTransaction)
|
||||
class RecurringTransactionAdmin(admin.ModelAdmin):
|
||||
class RecurringTransactionAdmin(SharedObjectModelAdmin):
|
||||
inlines = [
|
||||
TransactionInline,
|
||||
]
|
||||
|
||||
|
||||
admin.site.register(TransactionCategory)
|
||||
admin.site.register(TransactionTag)
|
||||
admin.site.register(TransactionEntity)
|
||||
@admin.register(TransactionCategory)
|
||||
class TransactionCategoryModelAdmin(SharedObjectModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(TransactionTag)
|
||||
class TransactionTagModelAdmin(SharedObjectModelAdmin):
|
||||
pass
|
||||
|
||||
|
||||
@admin.register(TransactionEntity)
|
||||
class TransactionEntityModelAdmin(SharedObjectModelAdmin):
|
||||
pass
|
||||
|
||||
@@ -184,3 +184,8 @@ class TransactionsFilter(django_filters.FilterSet):
|
||||
self.form.fields["from_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.form.fields["date_start"].widget = AirDatePickerInput()
|
||||
self.form.fields["date_end"].widget = AirDatePickerInput()
|
||||
|
||||
self.form.fields["account"].queryset = Account.objects.all()
|
||||
self.form.fields["category"].queryset = TransactionCategory.objects.all()
|
||||
self.form.fields["tags"].queryset = TransactionTag.objects.all()
|
||||
self.form.fields["entities"].queryset = TransactionEntity.objects.all()
|
||||
|
||||
@@ -29,6 +29,7 @@ from apps.transactions.models import (
|
||||
RecurringTransaction,
|
||||
TransactionEntity,
|
||||
)
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
|
||||
|
||||
class TransactionForm(forms.ModelForm):
|
||||
@@ -94,20 +95,30 @@ class TransactionForm(forms.ModelForm):
|
||||
# if editing a transaction display non-archived items and it's own item even if it's archived
|
||||
if self.instance.id:
|
||||
self.fields["account"].queryset = Account.objects.filter(
|
||||
Q(is_archived=False) | Q(transactions=self.instance.id)
|
||||
).distinct()
|
||||
Q(is_archived=False) | Q(transactions=self.instance.id),
|
||||
)
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
Q(active=True) | Q(transaction=self.instance.id)
|
||||
).distinct()
|
||||
)
|
||||
|
||||
self.fields["tags"].queryset = TransactionTag.objects.filter(
|
||||
Q(active=True) | Q(transaction=self.instance.id)
|
||||
).distinct()
|
||||
)
|
||||
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||
Q(active=True) | Q(transactions=self.instance.id)
|
||||
).distinct()
|
||||
)
|
||||
else:
|
||||
self.fields["account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.all()
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
@@ -405,6 +416,24 @@ class TransferForm(forms.Form):
|
||||
self.fields["to_amount"].widget = ArbitraryDecimalDisplayNumberInput()
|
||||
self.fields["date"].widget = AirDatePickerInput(clear_button=False)
|
||||
|
||||
self.fields["from_account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.fields["from_category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
self.fields["from_tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
|
||||
self.fields["to_account"].queryset = Account.objects.filter(
|
||||
is_archived=False,
|
||||
)
|
||||
|
||||
self.fields["to_category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
self.fields["to_tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
|
||||
def clean(self):
|
||||
cleaned_data = super().clean()
|
||||
from_account = cleaned_data.get("from_account")
|
||||
@@ -536,6 +565,18 @@ class InstallmentPlanForm(forms.ModelForm):
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||
Q(active=True) | Q(installmentplan=self.instance.id)
|
||||
).distinct()
|
||||
else:
|
||||
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
|
||||
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||
active=True
|
||||
)
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_tag = False
|
||||
@@ -781,6 +822,18 @@ class RecurringTransactionForm(forms.ModelForm):
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||
Q(active=True) | Q(recurringtransaction=self.instance.id)
|
||||
).distinct()
|
||||
else:
|
||||
self.fields["account"].queryset = Account.objects.filter(is_archived=False)
|
||||
|
||||
self.fields["category"].queryset = TransactionCategory.objects.filter(
|
||||
active=True
|
||||
)
|
||||
|
||||
self.fields["tags"].queryset = TransactionTag.objects.filter(active=True)
|
||||
|
||||
self.fields["entities"].queryset = TransactionEntity.objects.filter(
|
||||
active=True
|
||||
)
|
||||
|
||||
self.helper = FormHelper()
|
||||
self.helper.form_method = "post"
|
||||
|
||||
@@ -0,0 +1,62 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-05 04:19
|
||||
|
||||
import django.db.models.deletion
|
||||
import django.db.models.manager
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0033_transaction_internal_id'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='installmentplan',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='recurringtransaction',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='transactioncategory',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='transactionentity',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='transactiontag',
|
||||
managers=[
|
||||
('all_objects', django.db.models.manager.Manager()),
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactioncategory',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_categories', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactionentity',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_entities', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactiontag',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='owned_transaction_tags', to=settings.AUTH_USER_MODEL, verbose_name='Owner'),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,37 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-05 04:51
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0034_alter_installmentplan_managers_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='name',
|
||||
field=models.CharField(max_length=255, verbose_name='Name'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transactioncategory',
|
||||
unique_together={('owner', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transactionentity',
|
||||
unique_together={('owner', 'name')},
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transactiontag',
|
||||
unique_together={('owner', 'name')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,76 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-06 00:10
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0035_alter_transactioncategory_name_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterModelManagers(
|
||||
name='transactioncategory',
|
||||
managers=[
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='transactionentity',
|
||||
managers=[
|
||||
],
|
||||
),
|
||||
migrations.AlterModelManagers(
|
||||
name='transactiontag',
|
||||
managers=[
|
||||
],
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactioncategory',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactioncategory',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactionentity',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactionentity',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactiontag',
|
||||
name='shared_with',
|
||||
field=models.ManyToManyField(blank=True, related_name='%(class)s_shared', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AddField(
|
||||
model_name='transactiontag',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('shared', 'Shared'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionentity',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
+28
@@ -0,0 +1,28 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-06 01:47
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0036_alter_transactioncategory_managers_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transactioncategory',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactionentity',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transactiontag',
|
||||
name='visibility',
|
||||
field=models.CharField(choices=[('private', 'Private'), ('public', 'Public')], default='private', max_length=10),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,21 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-07 03:14
|
||||
|
||||
import django.db.models.deletion
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0037_alter_transactioncategory_visibility_and_more'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AddField(
|
||||
model_name='transaction',
|
||||
name='owner',
|
||||
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.CASCADE, related_name='%(class)s_owned', to=settings.AUTH_USER_MODEL),
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,24 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-07 03:16
|
||||
|
||||
from django.conf import settings
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0038_transaction_owner'),
|
||||
migrations.swappable_dependency(settings.AUTH_USER_MODEL),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='internal_id',
|
||||
field=models.TextField(blank=True, null=True, verbose_name='Internal ID'),
|
||||
),
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transaction',
|
||||
unique_together={('owner', 'internal_id')},
|
||||
),
|
||||
]
|
||||
@@ -0,0 +1,22 @@
|
||||
# Generated by Django 5.1.6 on 2025-03-07 03:25
|
||||
|
||||
from django.db import migrations, models
|
||||
|
||||
|
||||
class Migration(migrations.Migration):
|
||||
|
||||
dependencies = [
|
||||
('transactions', '0039_alter_transaction_internal_id_and_more'),
|
||||
]
|
||||
|
||||
operations = [
|
||||
migrations.AlterUniqueTogether(
|
||||
name='transaction',
|
||||
unique_together=set(),
|
||||
),
|
||||
migrations.AlterField(
|
||||
model_name='transaction',
|
||||
name='internal_id',
|
||||
field=models.TextField(blank=True, null=True, unique=True, verbose_name='Internal ID'),
|
||||
),
|
||||
]
|
||||
@@ -15,6 +15,8 @@ from apps.common.functions.decimals import truncate_decimal
|
||||
from apps.common.templatetags.decimal import localize_number, drop_trailing_zeros
|
||||
from apps.currencies.utils.convert import convert
|
||||
from apps.transactions.validators import validate_decimal_places, validate_non_negative
|
||||
from apps.common.middleware.thread_local import get_current_user
|
||||
from apps.common.models import SharedObject, SharedObjectManager, OwnedObject
|
||||
|
||||
logger = logging.getLogger()
|
||||
|
||||
@@ -93,10 +95,40 @@ class SoftDeleteQuerySet(models.QuerySet):
|
||||
class SoftDeleteManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||
return qs.filter(deleted=False)
|
||||
user = get_current_user()
|
||||
if user and not user.is_anonymous:
|
||||
return qs.filter(
|
||||
Q(account__visibility="public")
|
||||
| Q(account__owner=user)
|
||||
| Q(account__shared_with=user)
|
||||
| Q(account__visibility="private", account__owner=None),
|
||||
deleted=False,
|
||||
).distinct()
|
||||
else:
|
||||
return qs.filter(
|
||||
deleted=False,
|
||||
)
|
||||
|
||||
|
||||
class AllObjectsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
user = get_current_user()
|
||||
if user and not user.is_anonymous:
|
||||
return (
|
||||
SoftDeleteQuerySet(self.model, using=self._db)
|
||||
.filter(
|
||||
Q(account__visibility="public")
|
||||
| Q(account__owner=user)
|
||||
| Q(account__shared_with=user)
|
||||
| Q(account__visibility="private", account__owner=None),
|
||||
)
|
||||
.distinct()
|
||||
)
|
||||
else:
|
||||
return SoftDeleteQuerySet(self.model, using=self._db)
|
||||
|
||||
|
||||
class UserlessAllObjectsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
return SoftDeleteQuerySet(self.model, using=self._db)
|
||||
|
||||
@@ -104,11 +136,45 @@ class AllObjectsManager(models.Manager):
|
||||
class DeletedObjectsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||
return qs.filter(deleted=True)
|
||||
user = get_current_user()
|
||||
if user and not user.is_anonymous:
|
||||
return qs.filter(
|
||||
Q(account__visibility="public")
|
||||
| Q(account__owner=user)
|
||||
| Q(account__shared_with=user)
|
||||
| Q(account__visibility="private", account__owner=None),
|
||||
deleted=True,
|
||||
).distinct()
|
||||
else:
|
||||
return qs.filter(
|
||||
deleted=True,
|
||||
)
|
||||
|
||||
|
||||
class TransactionCategory(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
|
||||
class UserlessDeletedObjectsManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
qs = SoftDeleteQuerySet(self.model, using=self._db)
|
||||
return qs.filter(
|
||||
deleted=True,
|
||||
)
|
||||
|
||||
|
||||
class GenericAccountOwnerManager(models.Manager):
|
||||
def get_queryset(self):
|
||||
queryset = super().get_queryset()
|
||||
user = get_current_user()
|
||||
if user and not user.is_anonymous:
|
||||
return queryset.filter(
|
||||
Q(account__visibility="public")
|
||||
| Q(account__owner=user)
|
||||
| Q(account__shared_with=user)
|
||||
| Q(account__visibility="private", account__owner=None),
|
||||
).distinct()
|
||||
return queryset.none()
|
||||
|
||||
|
||||
class TransactionCategory(SharedObject):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
mute = models.BooleanField(default=False, verbose_name=_("Mute"))
|
||||
active = models.BooleanField(
|
||||
default=True,
|
||||
@@ -118,17 +184,21 @@ class TransactionCategory(models.Model):
|
||||
),
|
||||
)
|
||||
|
||||
objects = SharedObjectManager()
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transaction Category")
|
||||
verbose_name_plural = _("Transaction Categories")
|
||||
db_table = "t_categories"
|
||||
unique_together = (("owner", "name"),)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TransactionTag(models.Model):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"), unique=True)
|
||||
class TransactionTag(SharedObject):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
active = models.BooleanField(
|
||||
default=True,
|
||||
verbose_name=_("Active"),
|
||||
@@ -137,16 +207,20 @@ class TransactionTag(models.Model):
|
||||
),
|
||||
)
|
||||
|
||||
objects = SharedObjectManager()
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transaction Tags")
|
||||
verbose_name_plural = _("Transaction Tags")
|
||||
db_table = "tags"
|
||||
unique_together = (("owner", "name"),)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class TransactionEntity(models.Model):
|
||||
class TransactionEntity(SharedObject):
|
||||
name = models.CharField(max_length=255, verbose_name=_("Name"))
|
||||
active = models.BooleanField(
|
||||
default=True,
|
||||
@@ -156,16 +230,20 @@ class TransactionEntity(models.Model):
|
||||
),
|
||||
)
|
||||
|
||||
objects = SharedObjectManager()
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Entity")
|
||||
verbose_name_plural = _("Entities")
|
||||
db_table = "entities"
|
||||
unique_together = (("owner", "name"),)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
class Transaction(OwnedObject):
|
||||
class Type(models.TextChoices):
|
||||
INCOME = "IN", _("Income")
|
||||
EXPENSE = "EX", _("Expense")
|
||||
@@ -249,7 +327,11 @@ class Transaction(models.Model):
|
||||
|
||||
objects = SoftDeleteManager.from_queryset(SoftDeleteQuerySet)()
|
||||
all_objects = AllObjectsManager.from_queryset(SoftDeleteQuerySet)()
|
||||
userless_all_objects = UserlessAllObjectsManager.from_queryset(SoftDeleteQuerySet)()
|
||||
deleted_objects = DeletedObjectsManager.from_queryset(SoftDeleteQuerySet)()
|
||||
userless_deleted_objects = UserlessDeletedObjectsManager.from_queryset(
|
||||
SoftDeleteQuerySet
|
||||
)()
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Transaction")
|
||||
@@ -386,6 +468,9 @@ class InstallmentPlan(models.Model):
|
||||
|
||||
notes = models.TextField(blank=True, verbose_name=_("Notes"))
|
||||
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
objects = GenericAccountOwnerManager() # Default filtered manager
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Installment Plan")
|
||||
verbose_name_plural = _("Installment Plans")
|
||||
@@ -440,7 +525,7 @@ class InstallmentPlan(models.Model):
|
||||
|
||||
transaction_date = self.start_date + delta
|
||||
transaction_reference_date = (self.reference_date + delta).replace(day=1)
|
||||
new_transaction = Transaction.objects.create(
|
||||
new_transaction = Transaction.all_objects.create(
|
||||
account=self.account,
|
||||
type=self.type,
|
||||
date=transaction_date,
|
||||
@@ -500,7 +585,7 @@ class InstallmentPlan(models.Model):
|
||||
existing_transaction.entities.set(self.entities.all())
|
||||
else:
|
||||
# If the transaction doesn't exist, create a new one
|
||||
new_transaction = Transaction.objects.create(
|
||||
new_transaction = Transaction.all_objects.create(
|
||||
account=self.account,
|
||||
type=self.type,
|
||||
date=transaction_date,
|
||||
@@ -587,6 +672,9 @@ class RecurringTransaction(models.Model):
|
||||
verbose_name=_("Last Generated Reference Date"), null=True, blank=True
|
||||
)
|
||||
|
||||
all_objects = models.Manager() # Unfiltered manager
|
||||
objects = GenericAccountOwnerManager() # Default filtered manager
|
||||
|
||||
class Meta:
|
||||
verbose_name = _("Recurring Transaction")
|
||||
verbose_name_plural = _("Recurring Transactions")
|
||||
@@ -624,7 +712,7 @@ class RecurringTransaction(models.Model):
|
||||
)
|
||||
|
||||
def create_transaction(self, date, reference_date):
|
||||
created_transaction = Transaction.objects.create(
|
||||
created_transaction = Transaction.all_objects.create(
|
||||
account=self.account,
|
||||
type=self.type,
|
||||
date=date,
|
||||
|
||||
@@ -34,7 +34,7 @@ def cleanup_deleted_transactions(timestamp=None):
|
||||
|
||||
if not settings.ENABLE_SOFT_DELETE:
|
||||
# Hard delete all soft-deleted transactions
|
||||
deleted_count, _ = Transaction.deleted_objects.all().hard_delete()
|
||||
deleted_count, _ = Transaction.userless_deleted_objects.all().hard_delete()
|
||||
return (
|
||||
f"Hard deleted {deleted_count} transactions (soft deletion disabled)."
|
||||
)
|
||||
@@ -47,7 +47,9 @@ def cleanup_deleted_transactions(timestamp=None):
|
||||
invalidate()
|
||||
|
||||
# Hard delete soft-deleted transactions older than the cutoff date
|
||||
old_transactions = Transaction.deleted_objects.filter(deleted_at__lt=cutoff_date)
|
||||
old_transactions = Transaction.userless_deleted_objects.filter(
|
||||
deleted_at__lt=cutoff_date
|
||||
)
|
||||
deleted_count, _ = old_transactions.hard_delete()
|
||||
|
||||
return f"Hard deleted {deleted_count} objects older than {settings.KEEP_DELETED_TRANSACTIONS_FOR} days."
|
||||
|
||||
@@ -131,6 +131,16 @@ urlpatterns = [
|
||||
views.tag_delete,
|
||||
name="tag_delete",
|
||||
),
|
||||
path(
|
||||
"tags/<int:tag_id>/take-ownership/",
|
||||
views.tag_take_ownership,
|
||||
name="tag_take_ownership",
|
||||
),
|
||||
path(
|
||||
"tags/<int:pk>/share/",
|
||||
views.tag_share,
|
||||
name="tag_share_settings",
|
||||
),
|
||||
path("entities/", views.entities_index, name="entities_index"),
|
||||
path("entities/list/", views.entities_list, name="entities_list"),
|
||||
path(
|
||||
@@ -154,6 +164,16 @@ urlpatterns = [
|
||||
views.entity_delete,
|
||||
name="entity_delete",
|
||||
),
|
||||
path(
|
||||
"entities/<int:entity_id>/take-ownership/",
|
||||
views.entity_take_ownership,
|
||||
name="entity_take_ownership",
|
||||
),
|
||||
path(
|
||||
"entities/<int:pk>/share/",
|
||||
views.entity_share,
|
||||
name="entity_share_settings",
|
||||
),
|
||||
path("categories/", views.categories_index, name="categories_index"),
|
||||
path("categories/list/", views.categories_list, name="categories_list"),
|
||||
path(
|
||||
@@ -177,6 +197,16 @@ urlpatterns = [
|
||||
views.category_delete,
|
||||
name="category_delete",
|
||||
),
|
||||
path(
|
||||
"categories/<int:pk>/share/",
|
||||
views.category_share,
|
||||
name="category_share_settings",
|
||||
),
|
||||
path(
|
||||
"categories/<int:category_id>/take-ownership/",
|
||||
views.category_take_ownership,
|
||||
name="category_take_ownership",
|
||||
),
|
||||
path(
|
||||
"installment-plans/",
|
||||
views.installment_plans_index,
|
||||
|
||||
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.forms import TransactionCategoryForm
|
||||
from apps.transactions.models import TransactionCategory
|
||||
from apps.common.models import SharedObject
|
||||
from apps.common.forms import SharedObjectForm
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -85,6 +87,16 @@ def category_add(request, **kwargs):
|
||||
def category_edit(request, category_id):
|
||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
||||
|
||||
if category.owner and category.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionCategoryForm(request.POST, instance=category)
|
||||
if form.is_valid():
|
||||
@@ -107,15 +119,77 @@ def category_edit(request, category_id):
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def category_share(request, pk):
|
||||
obj = get_object_or_404(TransactionCategory, id=pk)
|
||||
|
||||
if obj.owner and obj.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Configuration saved successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = SharedObjectForm(instance=obj, user=request.user)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"categories/fragments/share.html",
|
||||
{"form": form, "object": obj},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["DELETE"])
|
||||
def category_delete(request, category_id):
|
||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
||||
|
||||
category.delete()
|
||||
|
||||
messages.success(request, _("Category deleted successfully"))
|
||||
if category.owner != request.user and request.user in category.shared_with.all():
|
||||
category.shared_with.remove(request.user)
|
||||
messages.success(request, _("Item no longer shared with you"))
|
||||
else:
|
||||
category.delete()
|
||||
messages.success(request, _("Category deleted successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def category_take_ownership(request, category_id):
|
||||
category = get_object_or_404(TransactionCategory, id=category_id)
|
||||
|
||||
if not category.owner:
|
||||
category.owner = request.user
|
||||
category.visibility = SharedObject.Visibility.private
|
||||
category.save()
|
||||
|
||||
messages.success(request, _("Ownership taken successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
|
||||
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.forms import TransactionEntityForm
|
||||
from apps.transactions.models import TransactionEntity
|
||||
from apps.common.models import SharedObject
|
||||
from apps.common.forms import SharedObjectForm
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -85,6 +87,16 @@ def entity_add(request, **kwargs):
|
||||
def entity_edit(request, entity_id):
|
||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
||||
|
||||
if entity.owner and entity.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionEntityForm(request.POST, instance=entity)
|
||||
if form.is_valid():
|
||||
@@ -113,9 +125,12 @@ def entity_edit(request, entity_id):
|
||||
def entity_delete(request, entity_id):
|
||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
||||
|
||||
entity.delete()
|
||||
|
||||
messages.success(request, _("Entity deleted successfully"))
|
||||
if entity.owner != request.user and request.user in entity.shared_with.all():
|
||||
entity.shared_with.remove(request.user)
|
||||
messages.success(request, _("Item no longer shared with you"))
|
||||
else:
|
||||
entity.delete()
|
||||
messages.success(request, _("Entity deleted successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
@@ -123,3 +138,62 @@ def entity_delete(request, entity_id):
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def entity_take_ownership(request, entity_id):
|
||||
entity = get_object_or_404(TransactionEntity, id=entity_id)
|
||||
|
||||
if not entity.owner:
|
||||
entity.owner = request.user
|
||||
entity.visibility = SharedObject.Visibility.private
|
||||
entity.save()
|
||||
|
||||
messages.success(request, _("Ownership taken successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def entity_share(request, pk):
|
||||
obj = get_object_or_404(TransactionEntity, id=pk)
|
||||
|
||||
if obj.owner and obj.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Configuration saved successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = SharedObjectForm(instance=obj, user=request.user)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"entities/fragments/share.html",
|
||||
{"form": form, "object": obj},
|
||||
)
|
||||
|
||||
@@ -8,6 +8,8 @@ from django.views.decorators.http import require_http_methods
|
||||
from apps.common.decorators.htmx import only_htmx
|
||||
from apps.transactions.forms import TransactionTagForm
|
||||
from apps.transactions.models import TransactionTag
|
||||
from apps.common.models import SharedObject
|
||||
from apps.common.forms import SharedObjectForm
|
||||
|
||||
|
||||
@login_required
|
||||
@@ -85,6 +87,16 @@ def tag_add(request, **kwargs):
|
||||
def tag_edit(request, tag_id):
|
||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
||||
|
||||
if tag.owner and tag.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = TransactionTagForm(request.POST, instance=tag)
|
||||
if form.is_valid():
|
||||
@@ -113,9 +125,12 @@ def tag_edit(request, tag_id):
|
||||
def tag_delete(request, tag_id):
|
||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
||||
|
||||
tag.delete()
|
||||
|
||||
messages.success(request, _("Tag deleted successfully"))
|
||||
if tag.owner != request.user and request.user in tag.shared_with.all():
|
||||
tag.shared_with.remove(request.user)
|
||||
messages.success(request, _("Item no longer shared with you"))
|
||||
else:
|
||||
tag.delete()
|
||||
messages.success(request, _("Tag deleted successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
@@ -123,3 +138,62 @@ def tag_delete(request, tag_id):
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET"])
|
||||
def tag_take_ownership(request, tag_id):
|
||||
tag = get_object_or_404(TransactionTag, id=tag_id)
|
||||
|
||||
if not tag.owner:
|
||||
tag.owner = request.user
|
||||
tag.visibility = SharedObject.Visibility.private
|
||||
tag.save()
|
||||
|
||||
messages.success(request, _("Ownership taken successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
|
||||
@only_htmx
|
||||
@login_required
|
||||
@require_http_methods(["GET", "POST"])
|
||||
def tag_share(request, pk):
|
||||
obj = get_object_or_404(TransactionTag, id=pk)
|
||||
|
||||
if obj.owner and obj.owner != request.user:
|
||||
messages.error(request, _("Only the owner can edit this"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
|
||||
if request.method == "POST":
|
||||
form = SharedObjectForm(request.POST, instance=obj, user=request.user)
|
||||
if form.is_valid():
|
||||
form.save()
|
||||
messages.success(request, _("Configuration saved successfully"))
|
||||
|
||||
return HttpResponse(
|
||||
status=204,
|
||||
headers={
|
||||
"HX-Trigger": "updated, hide_offcanvas",
|
||||
},
|
||||
)
|
||||
else:
|
||||
form = SharedObjectForm(instance=obj, user=request.user)
|
||||
|
||||
return render(
|
||||
request,
|
||||
"tags/fragments/share.html",
|
||||
{"form": form, "object": obj},
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user