Initial
This commit is contained in:
10
app/base/models/__init__.py
Executable file
10
app/base/models/__init__.py
Executable file
@@ -0,0 +1,10 @@
|
||||
from app.base.models.account import Account
|
||||
from app.base.models.booking import Booking
|
||||
from app.base.models.booking_type import BookingType
|
||||
from app.base.models.course import Course
|
||||
from app.base.models.course_visit import CourseVisit
|
||||
from app.base.models.note import Note
|
||||
from app.base.models.person import Person
|
||||
from app.base.models.trait import Trait
|
||||
from app.base.models.trait_mapping import TraitMapping
|
||||
from app.base.models.transaction import Transaction
|
||||
32
app/base/models/account.py
Executable file
32
app/base/models/account.py
Executable file
@@ -0,0 +1,32 @@
|
||||
from django.db import models
|
||||
|
||||
from app.base.forms.fields import CurrencyField
|
||||
from app.base.models.person import Person
|
||||
|
||||
|
||||
class Account(models.Model):
|
||||
user = models.OneToOneField(Person, on_delete=models.CASCADE)
|
||||
balance = CurrencyField('Guthaben')
|
||||
locked = models.BooleanField('Gesperrt', default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Konto'
|
||||
verbose_name_plural = 'Konten'
|
||||
|
||||
def __str__(self):
|
||||
return f"{self.user}'s Konto"
|
||||
|
||||
# def change_balance(self, amount):
|
||||
# self.balance = self.balance - amount
|
||||
# self.save()
|
||||
# return True
|
||||
|
||||
# def lock(self):
|
||||
# self.locked = True
|
||||
# self.save()
|
||||
# return True
|
||||
|
||||
# def unlock(self):
|
||||
# self.locked = False
|
||||
# self.save()
|
||||
# return True
|
||||
63
app/base/models/booking.py
Executable file
63
app/base/models/booking.py
Executable file
@@ -0,0 +1,63 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
from django.utils import timezone
|
||||
|
||||
from app.base.forms.fields import DateTimeField, TextField
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from django.db.models import OuterRef
|
||||
from app.base.models.person import Person
|
||||
from app.base.models.booking_type import BookingType
|
||||
|
||||
|
||||
class Booking(models.Model):
|
||||
type: 'models.ForeignKey[BookingType]' = models.ForeignKey(
|
||||
'BookingType', on_delete=models.PROTECT, verbose_name='Art')
|
||||
user: 'models.ForeignKey[Person]' = models.ForeignKey(
|
||||
'Person', on_delete=models.CASCADE, verbose_name='Nutzer:in')
|
||||
begin_time = DateTimeField('Beginn', default=datetime.now)
|
||||
end_time = DateTimeField('Ende', blank=True, null=True)
|
||||
comment = TextField('Kommentar', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Buchung'
|
||||
verbose_name_plural = 'Buchungen'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('booking:detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return 'Buchung von {} am {}: {}-{} Uhr'.format(
|
||||
self.user,
|
||||
self.begin_time.strftime('%d.%m.%Y'),
|
||||
self.begin_time.strftime('%H:%M'),
|
||||
self.end_time.strftime('%H:%M') if self.end_time else '')
|
||||
|
||||
@property
|
||||
def duration(self) -> 'int|None':
|
||||
if self.end_time:
|
||||
return round((self.end_time - self.begin_time).seconds / 60)
|
||||
return None
|
||||
|
||||
@property
|
||||
def calculated_price(self):
|
||||
traits = self.user.traits_at_date(self.begin_time).values_list('trait')
|
||||
traits = set(x[0] for x in traits)
|
||||
return self.type.price_with_traits(self.duration or 0, traits)
|
||||
|
||||
@staticmethod
|
||||
def latest_checkin_query(for_user: 'Person|OuterRef'):
|
||||
objects = Booking.objects.filter(user=for_user)
|
||||
return objects.order_by('-begin_time').values('begin_time')[:1]
|
||||
|
||||
@staticmethod
|
||||
def currently_open_checkin(for_user: 'Person|OuterRef') -> 'Booking|None':
|
||||
return Booking.objects.filter(
|
||||
Q(user=for_user),
|
||||
Q(begin_time__lte=timezone.now()),
|
||||
Q(end_time=None),
|
||||
Q(type__is_checkin=True)
|
||||
).first()
|
||||
36
app/base/models/booking_type.py
Executable file
36
app/base/models/booking_type.py
Executable file
@@ -0,0 +1,36 @@
|
||||
from django.db import models
|
||||
|
||||
from app.base.forms.fields import CurrencyField
|
||||
|
||||
import math
|
||||
from decimal import Decimal
|
||||
from typing import Iterable
|
||||
|
||||
|
||||
class BookingType(models.Model):
|
||||
key = models.CharField('UUID', primary_key=True, max_length=20)
|
||||
label = models.CharField('Bezeichnung', max_length=200)
|
||||
price = CurrencyField('Preis (€)')
|
||||
interval = models.IntegerField('Intervall (Min)', default=60)
|
||||
is_checkin = models.BooleanField('Ist Eincheck-Option', default=False)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Buchungsart'
|
||||
verbose_name_plural = 'Buchungsarten'
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
# return f'{self.label} ({self.price}/{self.interval})'
|
||||
|
||||
def price_with_traits(self, duration: int, traits: Iterable[str]) \
|
||||
-> Decimal:
|
||||
# TODO: make this UI-configurable?
|
||||
# people with "Abo" status
|
||||
if 'abo' in traits or 'bckspc' in traits:
|
||||
return Decimal(0)
|
||||
# Members do not pay for basic subscription
|
||||
if self.key == 'basic' and 'member' in traits:
|
||||
return Decimal(0)
|
||||
# TODO: if needed add more rules here
|
||||
intervals_to_pay = math.ceil(duration / self.interval)
|
||||
return Decimal(intervals_to_pay * self.price)
|
||||
22
app/base/models/course.py
Executable file
22
app/base/models/course.py
Executable file
@@ -0,0 +1,22 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from app.base.forms.fields import TextField
|
||||
from app.base.models.course_visit import CourseVisit
|
||||
|
||||
|
||||
class Course(models.Model):
|
||||
instructed: models.QuerySet[CourseVisit]
|
||||
title = models.CharField('Titel', max_length=280)
|
||||
mandatory = models.BooleanField('Braucht jeder?', default=False)
|
||||
description = TextField('Beschreibung', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Einweisung'
|
||||
verbose_name_plural = 'Einweisungen'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('course:detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return self.title
|
||||
35
app/base/models/course_visit.py
Executable file
35
app/base/models/course_visit.py
Executable file
@@ -0,0 +1,35 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from app.base.forms.fields import DateField
|
||||
|
||||
from datetime import date
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from app.base.models.course import Course
|
||||
from app.base.models.person import Person
|
||||
|
||||
|
||||
class CourseVisit(models.Model):
|
||||
course: 'models.ForeignKey[Course]' = models.ForeignKey(
|
||||
'Course', on_delete=models.CASCADE,
|
||||
related_name='visits', verbose_name='Einweisung')
|
||||
participant: 'models.ForeignKey[Person]' = models.ForeignKey(
|
||||
'Person', on_delete=models.CASCADE,
|
||||
related_name='courses', verbose_name='Wer wurde eingewiesen?')
|
||||
teacher: 'models.ForeignKey[Person]|models.ForeignKey[None]' =\
|
||||
models.ForeignKey(
|
||||
'Person', on_delete=models.SET_NULL, blank=True, null=True,
|
||||
related_name='instructed', verbose_name='Durchgeführt von')
|
||||
date = DateField('Datum', default=date.today)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Teilnahme'
|
||||
verbose_name_plural = 'Teilnahmen'
|
||||
|
||||
def __str__(self):
|
||||
return 'Teilnahme von {} an Einweisung „{}“ am {}'.format(
|
||||
self.participant, self.course, self.date)
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('course-visit:detail', kwargs={'pk': self.pk})
|
||||
15
app/base/models/note.py
Executable file
15
app/base/models/note.py
Executable file
@@ -0,0 +1,15 @@
|
||||
from django.db import models
|
||||
|
||||
from app.base.forms.fields import TextField
|
||||
|
||||
|
||||
class Note(models.Model):
|
||||
user = models.OneToOneField('Person', on_delete=models.CASCADE)
|
||||
text = TextField('Notiz', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Notiz'
|
||||
verbose_name_plural = 'Notizen'
|
||||
|
||||
def __str__(self):
|
||||
return self.text
|
||||
84
app/base/models/person.py
Executable file
84
app/base/models/person.py
Executable file
@@ -0,0 +1,84 @@
|
||||
from django.db import models
|
||||
from django.db.models import Q
|
||||
from django.urls import reverse
|
||||
|
||||
from app.base.forms.fields import DateField
|
||||
from app.base.models.course import Course
|
||||
from app.base.models.booking import Booking
|
||||
|
||||
from datetime import datetime
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from app.base.models import Account, Note, CourseVisit, TraitMapping
|
||||
|
||||
|
||||
class Person(models.Model):
|
||||
uuid = models.CharField('Karten-ID', max_length=200, blank=True)
|
||||
|
||||
first_name = models.CharField('Vorname', max_length=200)
|
||||
last_name = models.CharField('Nachname', max_length=200)
|
||||
email = models.EmailField('Email', blank=True, null=True)
|
||||
phone = models.CharField('Telefon', max_length=200, blank=True, null=True)
|
||||
birth_date = DateField('Geburtsdatum')
|
||||
zip_code = models.CharField('PLZ', max_length=10)
|
||||
city = models.CharField('Stadt', max_length=200)
|
||||
street = models.CharField('Straße', max_length=200)
|
||||
house_nr = models.CharField('Hausnummer', max_length=10)
|
||||
|
||||
identified = models.BooleanField('Ausweis vorgezeigt', default=False)
|
||||
agreed_to_terms_of_service = models.BooleanField(
|
||||
'Nutzungsbedingungen zugestimmt', default=False)
|
||||
|
||||
# related_name
|
||||
account: 'models.OneToOneField[Account]'
|
||||
note: 'models.OneToOneField[Note]'
|
||||
courses: 'models.QuerySet[CourseVisit]'
|
||||
instructed: 'models.QuerySet[CourseVisit]'
|
||||
traits: 'models.QuerySet[TraitMapping]'
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Werkstattnutzer:in'
|
||||
verbose_name_plural = 'Werkstattnutzer:innen'
|
||||
# ordering = ('first_name', 'last_name')
|
||||
# indexes = [
|
||||
# models.Index(fields=['first_name', 'last_name']),
|
||||
# models.Index(Latest,fields=["first_name"],name="first_name_idx"),
|
||||
# ]
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('person:detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return f'{self.first_name} {self.last_name}'
|
||||
|
||||
@property
|
||||
def display_name(self):
|
||||
return f'{self.first_name} {self.last_name}'
|
||||
|
||||
@property
|
||||
def address(self):
|
||||
return f'{self.street} {self.house_nr}, {self.zip_code} {self.city}'
|
||||
|
||||
@property
|
||||
def missing_courses(self):
|
||||
mandatory_courses = Course.objects.filter(mandatory=True)
|
||||
completed = set(self.courses.values_list('course', flat=True))
|
||||
return [x for x in mandatory_courses if x.pk not in completed]
|
||||
|
||||
@property
|
||||
def attributes(self):
|
||||
return self.traits_at_date(datetime.now()).values_list(
|
||||
'pk', 'trait__key', 'trait__label')
|
||||
|
||||
def traits_at_date(self, date: datetime):
|
||||
return self.traits.filter(
|
||||
Q(valid_from__lte=date),
|
||||
Q(valid_until__gte=date) | Q(valid_until=None))
|
||||
|
||||
def last_check_in(self) -> 'datetime|None':
|
||||
obj = Booking.latest_checkin_query(self).first()
|
||||
return obj['begin_time'] if obj else None
|
||||
|
||||
@property
|
||||
def current_checkin(self):
|
||||
return Booking.currently_open_checkin(self)
|
||||
16
app/base/models/trait.py
Executable file
16
app/base/models/trait.py
Executable file
@@ -0,0 +1,16 @@
|
||||
from django.db import models
|
||||
|
||||
from app.base.forms.fields import TextField
|
||||
|
||||
|
||||
class Trait(models.Model):
|
||||
key = models.CharField('UUID', primary_key=True, max_length=20)
|
||||
label = models.CharField('Label', max_length=200)
|
||||
description = TextField('Beschreibung', blank=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Attribut'
|
||||
verbose_name_plural = 'Attribute'
|
||||
|
||||
def __str__(self):
|
||||
return self.label
|
||||
30
app/base/models/trait_mapping.py
Executable file
30
app/base/models/trait_mapping.py
Executable file
@@ -0,0 +1,30 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from app.base.forms.fields import DateField
|
||||
|
||||
from datetime import date
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from app.base.models.person import Person
|
||||
from app.base.models.trait import Trait
|
||||
|
||||
|
||||
class TraitMapping(models.Model):
|
||||
user: 'models.ForeignKey[Person]' = models.ForeignKey(
|
||||
'Person', on_delete=models.CASCADE, related_name='traits',
|
||||
verbose_name='Werkstattnutzer:in')
|
||||
trait: 'models.ForeignKey[Trait]' = models.ForeignKey(
|
||||
'Trait', on_delete=models.CASCADE, verbose_name='Attribut')
|
||||
valid_from = DateField('Gültig von', default=date.today)
|
||||
valid_until = DateField('Gültig bis', blank=True, null=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Attributzuweisung'
|
||||
verbose_name_plural = 'Attributzuweisungen'
|
||||
|
||||
def __str__(self):
|
||||
return f'Attribut „{self.trait}“ für {self.user}'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('trait-mapping:detail', kwargs={'pk': self.pk})
|
||||
26
app/base/models/transaction.py
Executable file
26
app/base/models/transaction.py
Executable file
@@ -0,0 +1,26 @@
|
||||
from django.db import models
|
||||
from django.urls import reverse
|
||||
|
||||
from app.base.forms.fields import CurrencyField, DateTimeField
|
||||
from app.base.models.account import Account
|
||||
|
||||
|
||||
class Transaction(models.Model):
|
||||
account = models.ForeignKey(Account, on_delete=models.CASCADE,
|
||||
verbose_name='Konto')
|
||||
amount = CurrencyField('Betrag')
|
||||
booking = models.OneToOneField('Booking', on_delete=models.CASCADE,
|
||||
verbose_name='Zugehörige Zeitbuchung',
|
||||
null=True, blank=True, default=None)
|
||||
description = models.CharField('Beschreibung', max_length=500)
|
||||
time_stamp = DateTimeField('Datum / Uhrzeit', auto_now_add=True)
|
||||
|
||||
class Meta:
|
||||
verbose_name = 'Transaktion'
|
||||
verbose_name_plural = 'Transaktionen'
|
||||
|
||||
def get_absolute_url(self):
|
||||
return reverse('transaction:detail', kwargs={'pk': self.pk})
|
||||
|
||||
def __str__(self):
|
||||
return f'Transaktion über {self.amount}€ von {self.account}'
|
||||
Reference in New Issue
Block a user