Inital commit

This commit is contained in:
MonkeyStrongTogether
2026-05-22 17:48:03 +02:00
parent b691418e68
commit 87ec426fed
36 changed files with 2286 additions and 0 deletions

177
splitchat/core/models.py Normal file
View File

@@ -0,0 +1,177 @@
import uuid
from decimal import Decimal
from django.db import models
from django.contrib.auth.models import User
from django.utils import timezone
class ChatRoom(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
name = models.CharField(max_length=100)
description = models.TextField(blank=True)
invite_code = models.CharField(max_length=12, unique=True, blank=True)
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_rooms')
created_at = models.DateTimeField(auto_now_add=True)
def save(self, *args, **kwargs):
if not self.invite_code:
self.invite_code = uuid.uuid4().hex[:8].upper()
super().save(*args, **kwargs)
def __str__(self):
return self.name
@property
def member_count(self):
return self.memberships.filter(is_active=True).count()
class Membership(models.Model):
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='memberships')
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='memberships')
joined_at = models.DateTimeField(auto_now_add=True)
is_active = models.BooleanField(default=True)
is_admin = models.BooleanField(default=False)
class Meta:
unique_together = ('user', 'room')
def __str__(self):
return f"{self.user.username} in {self.room.name}"
class Message(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='messages')
sender = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='messages')
content = models.TextField()
msg_type = models.CharField(max_length=20, default='chat',
choices=[('chat', 'Chat'), ('system', 'System'), ('event', 'Event'), ('expense', 'Expense')])
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['created_at']
def __str__(self):
return f"{self.sender}: {self.content[:50]}"
class Event(models.Model):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
room = models.ForeignKey(ChatRoom, on_delete=models.CASCADE, related_name='events')
created_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='created_events')
title = models.CharField(max_length=200)
description = models.TextField(blank=True)
location = models.CharField(max_length=200, blank=True)
event_date = models.DateTimeField(null=True, blank=True)
created_at = models.DateTimeField(auto_now_add=True)
is_settled = models.BooleanField(default=False)
class Meta:
ordering = ['-created_at']
def __str__(self):
return self.title
@property
def total_amount(self):
return self.expenses.aggregate(total=models.Sum('amount'))['total'] or Decimal('0')
def compute_balances(self):
"""Returns dict: {user_id: net_balance} positive=owed, negative=owes"""
balances = {}
for expense in self.expenses.prefetch_related('splits'):
payer_id = expense.paid_by_id
balances[payer_id] = balances.get(payer_id, Decimal('0')) + expense.amount
for split in expense.splits.all():
uid = split.user_id
balances[uid] = balances.get(uid, Decimal('0')) - split.amount
return balances
def compute_settlements(self):
"""Simplify debts: returns list of (debtor_user, creditor_user, amount)"""
balances = self.compute_balances()
# Fetch users
user_ids = list(balances.keys())
users = {u.id: u for u in User.objects.filter(id__in=user_ids)}
debtors = sorted(
[(uid, -bal) for uid, bal in balances.items() if bal < -Decimal('0.01')],
key=lambda x: -x[1]
)
creditors = sorted(
[(uid, bal) for uid, bal in balances.items() if bal > Decimal('0.01')],
key=lambda x: -x[1]
)
settlements = []
i, j = 0, 0
debtors = [[uid, amt] for uid, amt in debtors]
creditors = [[uid, amt] for uid, amt in creditors]
while i < len(debtors) and j < len(creditors):
debtor_id, debt = debtors[i]
creditor_id, credit = creditors[j]
amount = min(debt, credit)
if amount > Decimal('0.01'):
settlements.append((users.get(debtor_id), users.get(creditor_id), round(amount, 2)))
debtors[i][1] -= amount
creditors[j][1] -= amount
if debtors[i][1] < Decimal('0.01'):
i += 1
if creditors[j][1] < Decimal('0.01'):
j += 1
return settlements
class Expense(models.Model):
SPLIT_EQUAL = 'equal'
SPLIT_CUSTOM = 'custom'
SPLIT_CHOICES = [(SPLIT_EQUAL, 'Equal'), (SPLIT_CUSTOM, 'Custom')]
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
event = models.ForeignKey(Event, on_delete=models.CASCADE, related_name='expenses')
description = models.CharField(max_length=200)
amount = models.DecimalField(max_digits=10, decimal_places=2)
paid_by = models.ForeignKey(User, on_delete=models.SET_NULL, null=True, related_name='paid_expenses')
split_type = models.CharField(max_length=10, choices=SPLIT_CHOICES, default=SPLIT_EQUAL)
created_at = models.DateTimeField(auto_now_add=True)
class Meta:
ordering = ['-created_at']
def __str__(self):
return f"{self.description}: ${self.amount}"
class ExpenseSplit(models.Model):
expense = models.ForeignKey(Expense, on_delete=models.CASCADE, related_name='splits')
user = models.ForeignKey(User, on_delete=models.CASCADE, related_name='expense_splits')
amount = models.DecimalField(max_digits=10, decimal_places=2)
class Meta:
unique_together = ('expense', 'user')
def __str__(self):
return f"{self.user.username}: ${self.amount}"
class UserProfile(models.Model):
user = models.OneToOneField(User, on_delete=models.CASCADE, related_name='profile')
display_name = models.CharField(max_length=100, blank=True)
avatar_color = models.CharField(max_length=7, default='#6366f1')
def __str__(self):
return f"Profile({self.user.username})"
@property
def name(self):
return self.display_name or self.user.get_full_name() or self.user.username
def get_avatar_initials(self):
name = self.name
parts = name.split()
if len(parts) >= 2:
return (parts[0][0] + parts[-1][0]).upper()
return name[:2].upper()