2017-04-24 15:51:12 +00:00
#
2023-04-04 16:39:45 +00:00
# Copyright 2023 © AE UTBM
# ae@utbm.fr / ae.info@utbm.fr
2017-04-24 15:51:12 +00:00
#
2023-04-04 16:39:45 +00:00
# This file is part of the website of the UTBM Student Association (AE UTBM),
# https://ae.utbm.fr.
2017-04-24 15:51:12 +00:00
#
2024-09-22 23:37:25 +00:00
# You can find the source code of the website at https://github.com/ae-utbm/sith
2017-04-24 15:51:12 +00:00
#
2023-04-04 16:39:45 +00:00
# LICENSED UNDER THE GNU GENERAL PUBLIC LICENSE VERSION 3 (GPLv3)
2024-09-23 08:25:27 +00:00
# SEE : https://raw.githubusercontent.com/ae-utbm/sith/master/LICENSE
2023-04-04 16:39:45 +00:00
# OR WITHIN THE LOCAL FILE "LICENSE"
2017-04-24 15:51:12 +00:00
#
#
2022-11-23 11:23:17 +00:00
from __future__ import annotations
2024-06-24 11:07:36 +00:00
import base64
import os
import random
import string
2024-07-04 08:19:24 +00:00
from datetime import date , datetime , timedelta
from datetime import timezone as tz
2024-11-05 18:40:59 +00:00
from decimal import Decimal
2024-10-04 11:41:15 +00:00
from typing import Self , Tuple
2017-04-24 15:51:12 +00:00
2024-06-24 11:07:36 +00:00
from dict2xml import dict2xml
2016-03-28 12:54:35 +00:00
from django . conf import settings
2018-10-18 23:21:57 +00:00
from django . core . validators import MinLengthValidator
2024-06-24 11:07:36 +00:00
from django . db import models
2024-11-05 18:40:59 +00:00
from django . db . models import Exists , F , OuterRef , Q , QuerySet , Subquery , Sum , Value
from django . db . models . functions import Coalesce , Concat , Length
2016-08-01 22:32:55 +00:00
from django . forms import ValidationError
2024-06-24 11:07:36 +00:00
from django . urls import reverse
from django . utils import timezone
2017-05-14 02:36:04 +00:00
from django . utils . functional import cached_property
2024-06-24 11:07:36 +00:00
from django . utils . translation import gettext_lazy as _
from django_countries . fields import CountryField
2024-09-26 15:55:53 +00:00
from phonenumber_field . modelfields import PhoneNumberField
2016-03-28 12:54:35 +00:00
2024-06-24 11:07:36 +00:00
from accounting . models import CurrencyField
from club . models import Club
2024-09-14 19:51:35 +00:00
from core . fields import ResizedImageField
2024-06-24 11:07:36 +00:00
from core . models import Group , Notification , User
2023-03-24 14:32:05 +00:00
from core . utils import get_start_of_semester
2024-12-15 23:15:21 +00:00
from counter . apps import PAYMENT_METHOD
2024-12-17 01:42:07 +00:00
from sith . settings import SITH_MAIN_CLUB
2016-12-10 00:58:30 +00:00
from subscription . models import Subscription
2016-03-28 12:54:35 +00:00
2017-06-12 07:47:24 +00:00
2024-11-05 18:40:59 +00:00
class CustomerQuerySet ( models . QuerySet ) :
def update_amount ( self ) - > int :
""" Update the amount of all customers selected by this queryset.
The result is given as the sum of all refills minus the sum of all purchases .
Returns :
The number of updated rows .
Warnings :
The execution time of this query grows really quickly .
When updating 500 customers , it may take around a second .
If you try to update all customers at once , the execution time
goes up to tens of seconds .
Use this either on a small subset of the ` Customer ` table ,
or execute it inside an independent task
( like a Celery task or a management command ) .
"""
money_in = Subquery (
Refilling . objects . filter ( customer = OuterRef ( " pk " ) )
. values ( " customer_id " ) # group by customer
. annotate ( res = Sum ( F ( " amount " ) , default = 0 ) )
. values ( " res " )
)
money_out = Subquery (
Selling . objects . filter ( customer = OuterRef ( " pk " ) )
. values ( " customer_id " )
. annotate ( res = Sum ( F ( " unit_price " ) * F ( " quantity " ) , default = 0 ) )
. values ( " res " )
)
return self . update ( amount = Coalesce ( money_in - money_out , Decimal ( " 0 " ) ) )
2016-05-30 10:23:59 +00:00
class Customer ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Customer data of a User.
It adds some basic customers ' information, such as the account ID, and
is used by other accounting classes as reference to the customer , rather than using User .
2016-05-30 10:23:59 +00:00
"""
2018-10-04 19:29:19 +00:00
2019-10-05 17:09:15 +00:00
user = models . OneToOneField ( User , primary_key = True , on_delete = models . CASCADE )
2018-10-04 19:29:19 +00:00
account_id = models . CharField ( _ ( " account id " ) , max_length = 10 , unique = True )
2022-11-23 11:23:17 +00:00
amount = CurrencyField ( _ ( " amount " ) , default = 0 )
2018-10-04 19:29:19 +00:00
recorded_products = models . IntegerField ( _ ( " recorded product " ) , default = 0 )
2016-05-30 10:23:59 +00:00
2024-11-05 18:40:59 +00:00
objects = CustomerQuerySet . as_manager ( )
2016-05-30 10:23:59 +00:00
class Meta :
2018-10-04 19:29:19 +00:00
verbose_name = _ ( " customer " )
verbose_name_plural = _ ( " customers " )
ordering = [ " account_id " ]
2016-05-30 10:23:59 +00:00
def __str__ ( self ) :
2016-08-14 17:28:14 +00:00
return " %s - %s " % ( self . user . username , self . account_id )
2016-05-30 10:23:59 +00:00
2024-06-27 13:48:07 +00:00
def save ( self , * args , allow_negative = False , is_selling = False , * * kwargs ) :
2024-07-12 07:34:16 +00:00
""" is_selling : tell if the current action is a selling
2024-06-27 13:48:07 +00:00
allow_negative : ignored if not a selling . Allow a selling to put the account in negative
2024-07-12 07:34:16 +00:00
Those two parameters avoid blocking the save method of a customer if his account is negative .
2024-06-27 13:48:07 +00:00
"""
if self . amount < 0 and ( is_selling and not allow_negative ) :
raise ValidationError ( _ ( " Not enough money " ) )
super ( ) . save ( * args , * * kwargs )
def get_absolute_url ( self ) :
return reverse ( " core:user_account " , kwargs = { " user_id " : self . user . pk } )
2017-07-21 19:39:49 +00:00
@property
def can_record ( self ) :
2017-08-15 00:09:44 +00:00
return self . recorded_products > - settings . SITH_ECOCUP_LIMIT
2017-07-21 19:39:49 +00:00
def can_record_more ( self , number ) :
2017-08-15 00:09:44 +00:00
return self . recorded_products - number > = - settings . SITH_ECOCUP_LIMIT
2017-07-21 19:39:49 +00:00
2017-03-27 21:24:25 +00:00
@property
2022-09-25 19:29:42 +00:00
def can_buy ( self ) - > bool :
2024-07-12 07:34:16 +00:00
""" Check if whether this customer has the right to purchase any item.
2022-09-25 19:29:42 +00:00
This must be not confused with the Product . can_be_sold_to ( user )
method as the present method returns an information
about a customer whereas the other tells something
about the relation between a User ( not a Customer ,
don ' t mix them) and a Product.
"""
2023-03-04 14:01:08 +00:00
subscription = self . user . subscriptions . order_by ( " subscription_end " ) . last ( )
2023-03-24 14:32:05 +00:00
if subscription is None :
return False
return ( date . today ( ) - subscription . subscription_end ) < timedelta ( days = 90 )
2017-03-27 21:24:25 +00:00
2022-11-28 16:03:46 +00:00
@classmethod
2022-11-23 11:23:17 +00:00
def get_or_create ( cls , user : User ) - > Tuple [ Customer , bool ] :
2024-07-12 07:34:16 +00:00
""" Work in pretty much the same way as the usual get_or_create method,
2022-11-23 11:23:17 +00:00
but with the default field replaced by some under the hood .
If the user has an account , return it as is .
Else create a new account with no money on it and a new unique account id
Example : : :
user = User . objects . get ( pk = 1 )
account , created = Customer . get_or_create ( user )
if created :
print ( f " created a new account with id { account . id } " )
else :
print ( f " user has already an account, with { account . id } € on it "
2022-11-28 16:03:46 +00:00
"""
2022-11-23 11:23:17 +00:00
if hasattr ( user , " customer " ) :
return user . customer , False
# account_id are always a number with a letter appended
2022-11-28 16:03:46 +00:00
account_id = (
Customer . objects . order_by ( Length ( " account_id " ) , " account_id " )
. values ( " account_id " )
. last ( )
)
if account_id is None :
# legacy from the old site
2022-11-23 11:23:17 +00:00
account = cls . objects . create ( user = user , account_id = " 1504a " )
return account , True
2022-11-28 16:03:46 +00:00
account_id = account_id [ " account_id " ]
2022-11-23 11:23:17 +00:00
account_num = int ( account_id [ : - 1 ] )
2022-11-28 16:03:46 +00:00
while Customer . objects . filter ( account_id = account_id ) . exists ( ) :
2022-11-23 11:23:17 +00:00
# when entering the first iteration, we are using an already existing account id
# so the loop should always execute at least one time
account_num + = 1
account_id = f " { account_num } { random . choice ( string . ascii_lowercase ) } "
2022-11-28 16:03:46 +00:00
2022-11-23 11:23:17 +00:00
account = cls . objects . create ( user = user , account_id = account_id )
return account , True
2016-07-19 22:28:49 +00:00
2016-10-21 11:09:46 +00:00
def get_full_url ( self ) :
2024-10-15 09:36:26 +00:00
return f " https:// { settings . SITH_URL } { self . get_absolute_url ( ) } "
2016-10-21 11:09:46 +00:00
2022-11-28 16:03:46 +00:00
class BillingInfo ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Represent the billing information of a user, which are required
by the 3 D - Secure v2 system used by the etransaction module .
2022-11-28 16:03:46 +00:00
"""
customer = models . OneToOneField (
Customer , related_name = " billing_infos " , on_delete = models . CASCADE
)
# declaring surname and name even though they are already defined
# in User add some redundancy, but ensures that the billing infos
# shall stay correct, whatever shenanigans the user commits on its profile
2023-01-09 16:46:34 +00:00
first_name = models . CharField ( _ ( " First name " ) , max_length = 22 )
last_name = models . CharField ( _ ( " Last name " ) , max_length = 22 )
2022-11-28 16:03:46 +00:00
address_1 = models . CharField ( _ ( " Address 1 " ) , max_length = 50 )
address_2 = models . CharField ( _ ( " Address 2 " ) , max_length = 50 , blank = True , null = True )
zip_code = models . CharField ( _ ( " Zip code " ) , max_length = 16 ) # code postal
city = models . CharField ( _ ( " City " ) , max_length = 50 )
country = CountryField ( blank_label = _ ( " Country " ) )
2024-09-26 15:55:53 +00:00
# This table was created during the A22 semester.
# However, later on, CA asked for the phone number to be added to the billing info.
# As the table was already created, this new field had to be nullable,
# even tough it is required by the bank and shouldn't be null.
# If one day there is no null phone number remaining,
# please make the field non-nullable.
phone_number = PhoneNumberField ( _ ( " Phone number " ) , null = True , blank = False )
2024-06-27 13:48:07 +00:00
def __str__ ( self ) :
return f " { self . first_name } { self . last_name } "
2022-11-28 16:03:46 +00:00
def to_3dsv2_xml ( self ) - > str :
2024-07-12 07:34:16 +00:00
""" Convert the data from this model into a xml usable
2022-11-28 16:03:46 +00:00
by the online paying service of the Crédit Agricole bank .
2024-07-12 07:34:16 +00:00
see : ` https : / / www . ca - moncommerce . com / espace - client - mon - commerce / up2pay - e - transactions / ma - documentation / manuel - dintegration - focus - 3 ds - v2 / principes - generaux / #integration-3dsv2-developpeur-webmaster`.
2022-11-28 16:03:46 +00:00
"""
data = {
2022-12-10 19:41:35 +00:00
" Address " : {
" FirstName " : self . first_name ,
" LastName " : self . last_name ,
" Address1 " : self . address_1 ,
" ZipCode " : self . zip_code ,
" City " : self . city ,
2022-12-12 21:54:31 +00:00
" CountryCode " : self . country . numeric , # ISO-3166-1 numeric code
2024-09-26 15:55:53 +00:00
" MobilePhone " : self . phone_number . as_national . replace ( " " , " " ) ,
" CountryCodeMobilePhone " : f " + { self . phone_number . country_code } " ,
2022-11-28 16:03:46 +00:00
}
}
if self . address_2 :
2022-12-10 19:41:35 +00:00
data [ " Address " ] [ " Address2 " ] = self . address_2
xml = dict2xml ( data , wrap = " Billing " , newlines = False )
2023-01-06 19:02:45 +00:00
return ' <?xml version= " 1.0 " encoding= " UTF-8 " ?> ' + xml
2022-11-28 16:03:46 +00:00
2024-10-06 20:24:20 +00:00
class AccountDumpQuerySet ( models . QuerySet ) :
def ongoing ( self ) - > Self :
""" Filter dump operations that are not completed yet. """
return self . filter ( dump_operation = None )
class AccountDump ( models . Model ) :
""" The process of dumping an account. """
customer = models . ForeignKey (
Customer , related_name = " dumps " , on_delete = models . CASCADE
)
warning_mail_sent_at = models . DateTimeField (
help_text = _ (
" When the mail warning that the account was about to be dumped was sent. "
)
)
warning_mail_error = models . BooleanField (
default = False ,
help_text = _ ( " Set this to True if the warning mail received an error " ) ,
)
dump_operation = models . OneToOneField (
" Selling " ,
null = True ,
blank = True ,
on_delete = models . CASCADE ,
help_text = _ ( " The operation that emptied the account. " ) ,
)
objects = AccountDumpQuerySet . as_manager ( )
class Meta :
constraints = [
models . UniqueConstraint (
fields = [ " customer " ] ,
condition = Q ( dump_operation = None ) ,
name = " unique_ongoing_dump " ,
) ,
]
def __str__ ( self ) :
status = " ongoing " if self . dump_operation is None else " finished "
return f " { self . customer } - { status } "
2024-10-16 21:10:12 +00:00
@cached_property
def amount ( self ) :
return (
self . dump_operation . unit_price
if self . dump_operation
else self . customer . amount
)
2024-10-06 20:24:20 +00:00
2016-05-30 10:23:59 +00:00
class ProductType ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" A product type.
Useful only for categorizing .
2016-05-30 10:23:59 +00:00
"""
2018-10-04 19:29:19 +00:00
name = models . CharField ( _ ( " name " ) , max_length = 30 )
description = models . TextField ( _ ( " description " ) , null = True , blank = True )
comment = models . TextField ( _ ( " comment " ) , null = True , blank = True )
2024-09-14 19:51:35 +00:00
icon = ResizedImageField (
height = 70 , force_format = " WEBP " , upload_to = " products " , null = True , blank = True
)
2016-05-30 10:23:59 +00:00
2022-11-16 19:41:24 +00:00
# priority holds no real backend logic but helps to handle the order in which
# the items are to be shown to the user
priority = models . PositiveIntegerField ( default = 0 )
2016-07-27 18:05:45 +00:00
class Meta :
2018-10-04 19:29:19 +00:00
verbose_name = _ ( " product type " )
2022-11-16 19:41:24 +00:00
ordering = [ " -priority " , " name " ]
2016-07-27 18:05:45 +00:00
2024-06-27 13:48:07 +00:00
def __str__ ( self ) :
return self . name
def get_absolute_url ( self ) :
return reverse ( " counter:producttype_list " )
2016-05-30 10:23:59 +00:00
def is_owned_by ( self , user ) :
2024-07-12 07:34:16 +00:00
""" Method to see if that object can be edited by the given user. """
2023-05-02 10:36:59 +00:00
if user . is_anonymous :
return False
2024-10-15 09:36:26 +00:00
return user . is_in_group ( pk = settings . SITH_GROUP_ACCOUNTING_ADMIN_ID )
2016-05-30 10:23:59 +00:00
2017-06-12 07:47:24 +00:00
2016-05-30 10:23:59 +00:00
class Product ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" A product, with all its related information. """
2018-10-04 19:29:19 +00:00
name = models . CharField ( _ ( " name " ) , max_length = 64 )
2024-12-09 11:09:12 +00:00
description = models . TextField ( _ ( " description " ) , default = " " )
2018-10-04 19:29:19 +00:00
product_type = models . ForeignKey (
ProductType ,
related_name = " products " ,
verbose_name = _ ( " product type " ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
)
code = models . CharField ( _ ( " code " ) , max_length = 16 , blank = True )
2024-12-09 11:09:12 +00:00
purchase_price = CurrencyField (
_ ( " purchase price " ) ,
help_text = _ ( " Initial cost of purchasing the product " ) ,
)
2018-10-04 19:29:19 +00:00
selling_price = CurrencyField ( _ ( " selling price " ) )
2024-12-09 11:09:12 +00:00
special_selling_price = CurrencyField (
_ ( " special selling price " ) ,
help_text = _ ( " Price for barmen during their permanence " ) ,
)
2024-09-14 19:51:35 +00:00
icon = ResizedImageField (
height = 70 ,
force_format = " WEBP " ,
upload_to = " products " ,
null = True ,
blank = True ,
verbose_name = _ ( " icon " ) ,
2018-10-04 19:29:19 +00:00
)
2019-10-05 17:05:56 +00:00
club = models . ForeignKey (
Club , related_name = " products " , verbose_name = _ ( " club " ) , on_delete = models . CASCADE
)
2018-10-04 19:29:19 +00:00
limit_age = models . IntegerField ( _ ( " limit age " ) , default = 0 )
tray = models . BooleanField ( _ ( " tray price " ) , default = False )
buying_groups = models . ManyToManyField (
Group , related_name = " products " , verbose_name = _ ( " buying groups " ) , blank = True
)
2016-09-04 13:49:25 +00:00
archived = models . BooleanField ( _ ( " archived " ) , default = False )
2016-05-30 10:23:59 +00:00
2016-07-27 18:05:45 +00:00
class Meta :
2018-10-04 19:29:19 +00:00
verbose_name = _ ( " product " )
2016-07-27 18:05:45 +00:00
2024-06-27 13:48:07 +00:00
def __str__ ( self ) :
2024-12-09 11:09:12 +00:00
return f " { self . name } ( { self . code } ) "
2024-06-27 13:48:07 +00:00
def get_absolute_url ( self ) :
return reverse ( " counter:product_list " )
2017-07-21 19:39:49 +00:00
@property
def is_record_product ( self ) :
2024-10-15 09:36:26 +00:00
return self . id == settings . SITH_ECOCUP_CONS
2017-07-21 19:39:49 +00:00
@property
def is_unrecord_product ( self ) :
2024-10-15 09:36:26 +00:00
return self . id == settings . SITH_ECOCUP_DECO
2017-07-21 19:39:49 +00:00
2016-07-20 16:48:18 +00:00
def is_owned_by ( self , user ) :
2024-07-12 07:34:16 +00:00
""" Method to see if that object can be edited by the given user. """
2023-05-02 10:36:59 +00:00
if user . is_anonymous :
return False
2024-10-15 09:36:26 +00:00
return user . is_in_group (
2023-05-02 10:36:59 +00:00
pk = settings . SITH_GROUP_ACCOUNTING_ADMIN_ID
2024-10-15 09:36:26 +00:00
) or user . is_in_group ( pk = settings . SITH_GROUP_COUNTER_ADMIN_ID )
2016-05-30 10:23:59 +00:00
2022-09-25 19:29:42 +00:00
def can_be_sold_to ( self , user : User ) - > bool :
2024-07-12 07:34:16 +00:00
""" Check if whether the user given in parameter has the right to buy
2022-09-25 19:29:42 +00:00
this product or not .
This must be not confused with the Customer . can_buy ( )
method as the present method returns an information
about the relation between a User and a Product ,
whereas the other tells something about a Customer
( and not a user , they are not the same model ) .
2024-07-12 07:34:16 +00:00
Returns :
True if the user can buy this product else False
2024-07-27 22:09:39 +00:00
2024-09-17 21:42:05 +00:00
Warning :
2024-07-27 22:09:39 +00:00
This performs a db query , thus you can quickly have
a N + 1 queries problem if you call it in a loop .
Hopefully , you can avoid that if you prefetch the buying_groups :
` ` ` python
user = User . objects . get ( username = " foobar " )
products = [
p
for p in Product . objects . prefetch_related ( " buying_groups " )
if p . can_be_sold_to ( user )
]
` ` `
2022-09-25 19:29:42 +00:00
"""
2024-07-27 22:09:39 +00:00
buying_groups = list ( self . buying_groups . all ( ) )
if not buying_groups :
2022-09-25 19:29:42 +00:00
return True
2024-10-15 09:36:26 +00:00
return any ( user . is_in_group ( pk = group . id ) for group in buying_groups )
2022-09-25 19:29:42 +00:00
2022-12-19 19:55:33 +00:00
@property
def profit ( self ) :
return self . selling_price - self . purchase_price
2017-06-12 07:47:24 +00:00
2022-12-17 15:31:12 +00:00
class CounterQuerySet ( models . QuerySet ) :
2024-10-09 22:06:22 +00:00
def annotate_has_barman ( self , user : User ) - > Self :
2024-07-12 07:34:16 +00:00
""" Annotate the queryset with the `user_is_barman` field.
2022-12-17 15:31:12 +00:00
For each counter , this field has value True if the user
is a barman of this counter , else False .
2024-07-12 07:34:16 +00:00
Args :
user : the user we want to check if he is a barman
2022-12-17 15:31:12 +00:00
2024-07-12 07:34:16 +00:00
Examples :
` ` ` python
2022-12-17 15:31:12 +00:00
sli = User . objects . get ( username = " sli " )
counters = (
Counter . objects
. annotate_has_barman ( sli ) # add the user_has_barman boolean field
. filter ( has_annotated_barman = True ) # keep only counters where this user is barman
)
print ( " Sli est barman dans les comptoirs suivants : " )
for counter in counters :
print ( f " - { counter . name } " )
2024-07-12 07:34:16 +00:00
` ` `
2022-12-17 15:31:12 +00:00
"""
subquery = user . counters . filter ( pk = OuterRef ( " pk " ) )
return self . annotate ( has_annotated_barman = Exists ( subquery ) )
2024-10-09 22:06:22 +00:00
def annotate_is_open ( self ) - > Self :
""" Annotate tue queryset with the `is_open` field.
For each counter , if ` is_open = True ` , then the counter is currently opened .
Else the counter is closed .
"""
return self . annotate (
is_open = Exists (
Permanency . objects . filter ( counter_id = OuterRef ( " pk " ) , end = None )
)
)
def handle_timeout ( self ) - > int :
""" Disconnect the barmen who are inactive in the given counters.
Returns :
The number of affected rows ( ie , the number of timeouted permanences )
"""
timeout = timezone . now ( ) - timedelta ( minutes = settings . SITH_BARMAN_TIMEOUT )
return Permanency . objects . filter (
counter__in = self , end = None , activity__lt = timeout
) . update ( end = F ( " activity " ) )
2022-12-17 15:31:12 +00:00
2016-03-28 12:54:35 +00:00
class Counter ( models . Model ) :
2018-10-04 19:29:19 +00:00
name = models . CharField ( _ ( " name " ) , max_length = 30 )
2019-10-05 17:05:56 +00:00
club = models . ForeignKey (
Club , related_name = " counters " , verbose_name = _ ( " club " ) , on_delete = models . CASCADE
)
2018-10-04 19:29:19 +00:00
products = models . ManyToManyField (
Product , related_name = " counters " , verbose_name = _ ( " products " ) , blank = True
)
type = models . CharField (
_ ( " counter type " ) ,
max_length = 255 ,
choices = [ ( " BAR " , _ ( " Bar " ) ) , ( " OFFICE " , _ ( " Office " ) ) , ( " EBOUTIC " , _ ( " Eboutic " ) ) ] ,
)
sellers = models . ManyToManyField (
User , verbose_name = _ ( " sellers " ) , related_name = " counters " , blank = True
)
edit_groups = models . ManyToManyField (
Group , related_name = " editable_counters " , blank = True
)
view_groups = models . ManyToManyField (
Group , related_name = " viewable_counters " , blank = True
)
token = models . CharField ( _ ( " token " ) , max_length = 30 , null = True , blank = True )
2016-03-28 12:54:35 +00:00
2022-12-17 15:31:12 +00:00
objects = CounterQuerySet . as_manager ( )
2016-07-27 18:05:45 +00:00
class Meta :
2018-10-04 19:29:19 +00:00
verbose_name = _ ( " counter " )
2016-07-27 18:05:45 +00:00
2016-03-28 12:54:35 +00:00
def __str__ ( self ) :
return self . name
2016-03-29 08:30:24 +00:00
2024-07-21 08:44:43 +00:00
def get_absolute_url ( self ) - > str :
2016-07-22 11:34:34 +00:00
if self . type == " EBOUTIC " :
2018-10-04 19:29:19 +00:00
return reverse ( " eboutic:main " )
return reverse ( " counter:details " , kwargs = { " counter_id " : self . id } )
2016-03-29 08:30:24 +00:00
2024-07-21 08:44:43 +00:00
def __getattribute__ ( self , name : str ) :
2024-06-27 13:48:07 +00:00
if name == " edit_groups " :
return Group . objects . filter (
name = self . club . unix_name + settings . SITH_BOARD_SUFFIX
) . all ( )
return object . __getattribute__ ( self , name )
2024-07-21 08:44:43 +00:00
def is_owned_by ( self , user : User ) - > bool :
2023-05-02 10:36:59 +00:00
if user . is_anonymous :
return False
2016-08-29 14:07:14 +00:00
mem = self . club . get_membership_for ( user )
if mem and mem . role > = 7 :
return True
2023-05-02 10:36:59 +00:00
return user . is_in_group ( pk = settings . SITH_GROUP_COUNTER_ADMIN_ID )
2016-07-17 22:47:56 +00:00
2024-07-21 08:44:43 +00:00
def can_be_viewed_by ( self , user : User ) - > bool :
2017-01-04 18:39:37 +00:00
if self . type == " BAR " :
2016-08-01 14:36:16 +00:00
return True
2023-05-02 10:36:59 +00:00
return user . is_board_member or user in self . sellers . all ( )
2016-04-19 17:58:57 +00:00
2024-07-21 08:44:43 +00:00
def gen_token ( self ) - > None :
2024-07-12 07:34:16 +00:00
""" Generate a new token for this counter. """
2018-10-04 19:29:19 +00:00
self . token = " " . join (
2024-07-21 08:44:43 +00:00
random . choice ( string . ascii_letters + string . digits ) for _ in range ( 30 )
2018-10-04 19:29:19 +00:00
)
2016-09-26 09:17:00 +00:00
self . save ( )
2017-05-14 02:36:04 +00:00
@cached_property
2024-07-21 08:44:43 +00:00
def barmen_list ( self ) - > list [ User ] :
2024-10-09 22:06:22 +00:00
""" Returns the barman list as list of User. """
return [
p . user for p in self . permanencies . filter ( end = None ) . select_related ( " user " )
]
2016-04-19 17:58:57 +00:00
2024-07-21 08:44:43 +00:00
def get_random_barman ( self ) - > User :
2024-07-12 07:34:16 +00:00
""" Return a random user being currently a barman. """
2024-07-21 08:44:43 +00:00
return random . choice ( self . barmen_list )
2016-05-31 17:32:15 +00:00
2024-07-21 08:44:43 +00:00
def update_activity ( self ) - > None :
2024-07-12 07:34:16 +00:00
""" Update the barman activity to prevent timeout. """
2024-07-21 08:44:43 +00:00
self . permanencies . filter ( end = None ) . update ( activity = timezone . now ( ) )
2016-09-12 15:34:33 +00:00
2024-07-21 08:44:43 +00:00
def can_refill ( self ) - > bool :
2024-07-12 07:34:16 +00:00
""" Show if the counter authorize the refilling with physic money. """
2023-01-10 21:26:46 +00:00
if self . type != " BAR " :
return False
2024-07-21 08:44:43 +00:00
# at least one of the barmen is in the AE board
2022-04-27 13:38:55 +00:00
ae = Club . objects . get ( unix_name = SITH_MAIN_CLUB [ " unix_name " ] )
2024-07-21 08:44:43 +00:00
return any ( ae . get_membership_for ( barman ) for barman in self . barmen_list )
2022-04-20 12:01:33 +00:00
2023-03-24 14:32:05 +00:00
def get_top_barmen ( self ) - > QuerySet :
2024-07-12 07:34:16 +00:00
""" Return a QuerySet querying the office hours stats of all the barmen of all time
2023-03-24 14:32:05 +00:00
of this counter , ordered by descending number of hours .
Each element of the QuerySet corresponds to a barman and has the following data :
- the full name ( first name + last name ) of the barman
- the nickname of the barman
- the promo of the barman
- the total number of office hours the barman did attend
"""
return (
self . permanencies . exclude ( end = None )
. annotate (
name = Concat ( F ( " user__first_name " ) , Value ( " " ) , F ( " user__last_name " ) )
)
. annotate ( nickname = F ( " user__nick_name " ) )
. annotate ( promo = F ( " user__promo " ) )
. values ( " user " , " name " , " nickname " , " promo " )
. annotate ( perm_sum = Sum ( F ( " end " ) - F ( " start " ) ) )
. exclude ( perm_sum = None )
. order_by ( " -perm_sum " )
)
2024-06-26 10:28:00 +00:00
def get_top_customers ( self , since : datetime | date | None = None ) - > QuerySet :
2024-07-12 07:34:16 +00:00
""" Return a QuerySet querying the money spent by customers of this counter
2023-03-24 14:32:05 +00:00
since the specified date , ordered by descending amount of money spent .
Each element of the QuerySet corresponds to a customer and has the following data :
2024-06-26 10:28:00 +00:00
2024-07-12 07:34:16 +00:00
- the full name ( first name + last name ) of the customer
- the nickname of the customer
- the amount of money spent by the customer
Args :
since : timestamp from which to perform the calculation
2023-03-24 14:32:05 +00:00
"""
2023-09-07 21:11:58 +00:00
if since is None :
since = get_start_of_semester ( )
2024-06-26 10:28:00 +00:00
if isinstance ( since , date ) :
2024-07-04 08:19:24 +00:00
since = datetime ( since . year , since . month , since . day , tzinfo = tz . utc )
2023-03-24 14:32:05 +00:00
return (
self . sellings . filter ( date__gte = since )
. annotate (
name = Concat (
F ( " customer__user__first_name " ) ,
Value ( " " ) ,
F ( " customer__user__last_name " ) ,
)
)
. annotate ( nickname = F ( " customer__user__nick_name " ) )
. annotate ( promo = F ( " customer__user__promo " ) )
2023-09-07 21:11:58 +00:00
. annotate ( user = F ( " customer__user " ) )
. values ( " user " , " promo " , " name " , " nickname " )
2023-03-24 14:32:05 +00:00
. annotate (
selling_sum = Sum (
F ( " unit_price " ) * F ( " quantity " ) , output_field = CurrencyField ( )
)
)
. filter ( selling_sum__gt = 0 )
. order_by ( " -selling_sum " )
)
2024-06-26 10:28:00 +00:00
def get_total_sales ( self , since : datetime | date | None = None ) - > CurrencyField :
2024-07-12 07:34:16 +00:00
""" Compute and return the total turnover of this counter since the given date.
By default , the date is the start of the current semester .
Args :
since : timestamp from which to perform the calculation
Returns :
Total revenue earned at this counter .
2023-03-24 14:32:05 +00:00
"""
2023-09-07 21:11:58 +00:00
if since is None :
since = get_start_of_semester ( )
2023-03-24 14:32:05 +00:00
if isinstance ( since , date ) :
2024-07-04 08:19:24 +00:00
since = datetime ( since . year , since . month , since . day , tzinfo = tz . utc )
2024-07-21 08:44:43 +00:00
return self . sellings . filter ( date__gte = since ) . aggregate (
total = Sum (
F ( " quantity " ) * F ( " unit_price " ) ,
default = 0 ,
output_field = CurrencyField ( ) ,
)
2023-03-24 14:32:05 +00:00
) [ " total " ]
2024-12-15 20:33:43 +00:00
def customer_is_barman ( self , customer : Customer | User ) - > bool :
2024-12-17 00:41:45 +00:00
""" Check if this counter is a `bar` and if the customer is currently logged in.
This is useful to compute special prices . """
2024-12-15 20:33:43 +00:00
2024-12-17 00:41:45 +00:00
# Customer and User are two different tables,
# but they share the same primary key
return self . type == " BAR " and any ( b . pk == customer . pk for b in self . barmen_list )
2024-12-15 20:33:43 +00:00
2017-06-12 07:47:24 +00:00
2024-10-04 11:41:15 +00:00
class RefillingQuerySet ( models . QuerySet ) :
def annotate_total ( self ) - > Self :
""" Annotate the Queryset with the total amount.
The total is just the sum of the amounts for each row .
If no grouping is involved ( like in most queries ) ,
this is just the same as doing nothing and fetching the
` amount ` attribute .
However , it may be useful when there is a ` group by ` clause
in the query , or when other models are queried and having
a common interface is helpful ( e . g . ` Selling . objects . annotate_total ( ) `
and ` Refilling . objects . annotate_total ( ) ` will both have the ` total ` field ) .
"""
return self . annotate ( total = Sum ( " amount " ) )
2016-05-31 17:32:15 +00:00
class Refilling ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Handle the refilling. """
2018-10-04 19:29:19 +00:00
2019-10-05 17:05:56 +00:00
counter = models . ForeignKey (
Counter , related_name = " refillings " , blank = False , on_delete = models . CASCADE
)
2018-10-04 19:29:19 +00:00
amount = CurrencyField ( _ ( " amount " ) )
operator = models . ForeignKey (
2019-10-05 17:05:56 +00:00
User ,
related_name = " refillings_as_operator " ,
blank = False ,
on_delete = models . CASCADE ,
)
customer = models . ForeignKey (
Customer , related_name = " refillings " , blank = False , on_delete = models . CASCADE
2018-10-04 19:29:19 +00:00
)
date = models . DateTimeField ( _ ( " date " ) )
payment_method = models . CharField (
_ ( " payment method " ) ,
max_length = 255 ,
2024-12-15 23:15:21 +00:00
choices = PAYMENT_METHOD ,
default = " CARD " ,
2018-10-04 19:29:19 +00:00
)
bank = models . CharField (
_ ( " bank " ) , max_length = 255 , choices = settings . SITH_COUNTER_BANK , default = " OTHER "
)
is_validated = models . BooleanField ( _ ( " is validated " ) , default = False )
2016-05-31 17:32:15 +00:00
2024-10-04 11:41:15 +00:00
objects = RefillingQuerySet . as_manager ( )
2016-07-27 18:05:45 +00:00
class Meta :
verbose_name = _ ( " refilling " )
2016-05-31 17:32:15 +00:00
def __str__ ( self ) :
2018-10-04 19:29:19 +00:00
return " Refilling: %.2f for %s " % (
self . amount ,
self . customer . user . get_display_name ( ) ,
)
2016-05-31 17:32:15 +00:00
def save ( self , * args , * * kwargs ) :
2016-08-18 17:52:20 +00:00
if not self . date :
2016-08-18 19:06:10 +00:00
self . date = timezone . now ( )
2016-05-31 17:32:15 +00:00
self . full_clean ( )
2016-08-01 22:32:55 +00:00
if not self . is_validated :
self . customer . amount + = self . amount
self . customer . save ( )
self . is_validated = True
2017-09-02 10:42:07 +00:00
if self . customer . user . preferences . notify_on_refill :
2018-10-04 19:29:19 +00:00
Notification (
user = self . customer . user ,
url = reverse (
" core:user_account_detail " ,
kwargs = {
" user_id " : self . customer . user . id ,
" year " : self . date . year ,
" month " : self . date . month ,
} ,
) ,
param = str ( self . amount ) ,
type = " REFILLING " ,
) . save ( )
2024-06-27 12:46:43 +00:00
super ( ) . save ( * args , * * kwargs )
2016-05-31 17:32:15 +00:00
2024-06-27 13:48:07 +00:00
def is_owned_by ( self , user ) :
if user . is_anonymous :
return False
return user . is_owner ( self . counter ) and self . payment_method != " CARD "
def delete ( self , * args , * * kwargs ) :
self . customer . amount - = self . amount
self . customer . save ( )
super ( ) . delete ( * args , * * kwargs )
2017-06-12 07:47:24 +00:00
2024-10-04 11:41:15 +00:00
class SellingQuerySet ( models . QuerySet ) :
def annotate_total ( self ) - > Self :
""" Annotate the Queryset with the total amount of the sales.
The total is considered as the sum of ( unit_price * quantity ) .
"""
return self . annotate ( total = Sum ( F ( " unit_price " ) * F ( " quantity " ) ) )
2016-05-31 17:32:15 +00:00
class Selling ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Handle the sellings. """
2018-10-04 19:29:19 +00:00
2016-08-18 01:04:50 +00:00
label = models . CharField ( _ ( " label " ) , max_length = 64 )
2018-10-04 19:29:19 +00:00
product = models . ForeignKey (
Product ,
related_name = " sellings " ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
)
counter = models . ForeignKey (
Counter ,
related_name = " sellings " ,
null = True ,
blank = False ,
on_delete = models . SET_NULL ,
)
club = models . ForeignKey (
Club , related_name = " sellings " , null = True , blank = False , on_delete = models . SET_NULL
)
unit_price = CurrencyField ( _ ( " unit price " ) )
quantity = models . IntegerField ( _ ( " quantity " ) )
seller = models . ForeignKey (
User ,
related_name = " sellings_as_operator " ,
null = True ,
blank = False ,
on_delete = models . SET_NULL ,
)
customer = models . ForeignKey (
Customer ,
related_name = " buyings " ,
null = True ,
blank = False ,
on_delete = models . SET_NULL ,
)
date = models . DateTimeField ( _ ( " date " ) )
payment_method = models . CharField (
_ ( " payment method " ) ,
max_length = 255 ,
choices = [ ( " SITH_ACCOUNT " , _ ( " Sith account " ) ) , ( " CARD " , _ ( " Credit card " ) ) ] ,
default = " SITH_ACCOUNT " ,
)
is_validated = models . BooleanField ( _ ( " is validated " ) , default = False )
2016-05-31 17:32:15 +00:00
2024-10-04 11:41:15 +00:00
objects = SellingQuerySet . as_manager ( )
2016-07-27 18:05:45 +00:00
class Meta :
verbose_name = _ ( " selling " )
2016-05-31 17:32:15 +00:00
def __str__ ( self ) :
2018-10-04 19:29:19 +00:00
return " Selling: %d x %s ( %f ) for %s " % (
self . quantity ,
self . label ,
self . quantity * self . unit_price ,
self . customer . user . get_display_name ( ) ,
)
2016-05-31 17:32:15 +00:00
2024-06-27 12:57:40 +00:00
def save ( self , * args , allow_negative = False , * * kwargs ) :
2024-07-12 07:34:16 +00:00
""" allow_negative : Allow this selling to use more money than available for this user. """
2016-08-18 17:52:20 +00:00
if not self . date :
2016-08-18 19:06:10 +00:00
self . date = timezone . now ( )
2016-05-31 17:32:15 +00:00
self . full_clean ( )
2016-08-01 22:32:55 +00:00
if not self . is_validated :
self . customer . amount - = self . quantity * self . unit_price
2017-08-15 00:09:44 +00:00
self . customer . save ( allow_negative = allow_negative , is_selling = True )
2016-08-01 22:32:55 +00:00
self . is_validated = True
2024-07-27 22:09:39 +00:00
user = self . customer . user
if user . was_subscribed :
2018-10-04 19:29:19 +00:00
if (
self . product
and self . product . id == settings . SITH_PRODUCT_SUBSCRIPTION_ONE_SEMESTER
) :
2016-12-20 22:27:54 +00:00
sub = Subscription (
2024-07-27 22:09:39 +00:00
member = user ,
2018-10-04 19:29:19 +00:00
subscription_type = " un-semestre " ,
2017-06-12 07:47:24 +00:00
payment_method = " EBOUTIC " ,
location = " EBOUTIC " ,
)
2016-12-20 22:27:54 +00:00
sub . subscription_start = Subscription . compute_start ( )
sub . subscription_start = Subscription . compute_start (
2018-10-04 19:29:19 +00:00
duration = settings . SITH_SUBSCRIPTIONS [ sub . subscription_type ] [
" duration "
]
)
2016-12-20 22:27:54 +00:00
sub . subscription_end = Subscription . compute_end (
2018-10-04 19:29:19 +00:00
duration = settings . SITH_SUBSCRIPTIONS [ sub . subscription_type ] [
" duration "
] ,
start = sub . subscription_start ,
)
2016-12-20 22:27:54 +00:00
sub . save ( )
2018-10-04 19:29:19 +00:00
elif (
self . product
and self . product . id == settings . SITH_PRODUCT_SUBSCRIPTION_TWO_SEMESTERS
) :
2016-12-20 22:27:54 +00:00
sub = Subscription (
2024-07-27 22:09:39 +00:00
member = user ,
2018-10-04 19:29:19 +00:00
subscription_type = " deux-semestres " ,
2017-06-12 07:47:24 +00:00
payment_method = " EBOUTIC " ,
location = " EBOUTIC " ,
)
2016-12-20 22:27:54 +00:00
sub . subscription_start = Subscription . compute_start ( )
sub . subscription_start = Subscription . compute_start (
2018-10-04 19:29:19 +00:00
duration = settings . SITH_SUBSCRIPTIONS [ sub . subscription_type ] [
" duration "
]
)
2016-12-20 22:27:54 +00:00
sub . subscription_end = Subscription . compute_end (
2018-10-04 19:29:19 +00:00
duration = settings . SITH_SUBSCRIPTIONS [ sub . subscription_type ] [
" duration "
] ,
start = sub . subscription_start ,
)
2016-12-20 22:27:54 +00:00
sub . save ( )
2024-07-27 22:09:39 +00:00
if user . preferences . notify_on_click :
2017-09-02 10:42:07 +00:00
Notification (
2024-07-27 22:09:39 +00:00
user = user ,
2018-10-04 19:29:19 +00:00
url = reverse (
" core:user_account_detail " ,
kwargs = {
2024-07-27 22:09:39 +00:00
" user_id " : user . id ,
2018-10-04 19:29:19 +00:00
" year " : self . date . year ,
" month " : self . date . month ,
} ,
) ,
2017-09-02 10:42:07 +00:00
param = " %d x %s " % ( self . quantity , self . label ) ,
type = " SELLING " ,
) . save ( )
2024-06-27 12:46:43 +00:00
super ( ) . save ( * args , * * kwargs )
2024-07-27 22:09:39 +00:00
if hasattr ( self . product , " eticket " ) :
self . send_mail_customer ( )
2019-10-13 15:29:08 +00:00
2024-07-27 22:09:39 +00:00
def is_owned_by ( self , user : User ) - > bool :
2024-06-27 13:48:07 +00:00
if user . is_anonymous :
return False
2024-07-27 22:09:39 +00:00
return self . payment_method != " CARD " and user . is_owner ( self . counter )
2024-06-27 13:48:07 +00:00
2024-07-27 22:09:39 +00:00
def can_be_viewed_by ( self , user : User ) - > bool :
2024-06-27 13:48:07 +00:00
if (
not hasattr ( self , " customer " ) or self . customer is None
) : # Customer can be set to Null
return False
return user == self . customer . user
def delete ( self , * args , * * kwargs ) :
if self . payment_method == " SITH_ACCOUNT " :
self . customer . amount + = self . quantity * self . unit_price
self . customer . save ( )
super ( ) . delete ( * args , * * kwargs )
def send_mail_customer ( self ) :
event = self . product . eticket . event_title or _ ( " Unknown event " )
subject = _ ( " Eticket bought for the event %(event)s " ) % { " event " : event }
message_html = _ (
" You bought an eticket for the event %(event)s . \n You can download it directly from this link %(eticket)s . \n You can also retrieve all your e-tickets on your account page %(url)s . "
) % {
" event " : event ,
2024-10-15 09:36:26 +00:00
" url " : (
f ' <a href= " { self . customer . get_full_url ( ) } " > '
f " { self . customer . get_full_url ( ) } </a> "
2024-06-27 13:48:07 +00:00
) ,
2024-10-15 09:36:26 +00:00
" eticket " : (
f ' <a href= " { self . get_eticket_full_url ( ) } " > '
f " { self . get_eticket_full_url ( ) } </a> "
2024-06-27 13:48:07 +00:00
) ,
}
message_txt = _ (
2024-10-15 09:36:26 +00:00
" You bought an eticket for the event %(event)s . \n "
" You can download it directly from this link %(eticket)s . \n "
" You can also retrieve all your e-tickets on your account page %(url)s . "
2024-06-27 13:48:07 +00:00
) % {
" event " : event ,
" url " : self . customer . get_full_url ( ) ,
" eticket " : self . get_eticket_full_url ( ) ,
}
2024-07-27 22:09:39 +00:00
self . customer . user . email_user (
subject , message_txt , html_message = message_html , fail_silently = True
)
2024-06-27 13:48:07 +00:00
2019-10-13 15:29:08 +00:00
def get_eticket_full_url ( self ) :
eticket_url = reverse ( " counter:eticket_pdf " , kwargs = { " selling_id " : self . id } )
2024-10-15 09:36:26 +00:00
return f " https:// { settings . SITH_URL } { eticket_url } "
2016-05-31 17:32:15 +00:00
2017-06-12 07:47:24 +00:00
2016-07-18 11:22:50 +00:00
class Permanency ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" A permanency of a barman, on a counter.
This aims at storing a traceability of who was barman where and when .
Mainly for ~ ~ dick size contest ~ ~ establishing the top 10 barmen of the semester .
2016-07-18 11:22:50 +00:00
"""
2018-10-04 19:29:19 +00:00
2019-10-05 17:05:56 +00:00
user = models . ForeignKey (
User ,
related_name = " permanencies " ,
verbose_name = _ ( " user " ) ,
on_delete = models . CASCADE ,
)
2018-10-04 19:29:19 +00:00
counter = models . ForeignKey (
2019-10-05 17:05:56 +00:00
Counter ,
related_name = " permanencies " ,
verbose_name = _ ( " counter " ) ,
on_delete = models . CASCADE ,
2018-10-04 19:29:19 +00:00
)
start = models . DateTimeField ( _ ( " start date " ) )
end = models . DateTimeField ( _ ( " end date " ) , null = True , db_index = True )
activity = models . DateTimeField ( _ ( " last activity date " ) , auto_now = True )
2016-07-18 11:22:50 +00:00
2016-07-27 18:05:45 +00:00
class Meta :
verbose_name = _ ( " permanency " )
2016-07-18 11:22:50 +00:00
def __str__ ( self ) :
2018-10-04 19:29:19 +00:00
return " %s in %s from %s (last activity: %s ) to %s " % (
self . user ,
self . counter ,
self . start . strftime ( " % Y- % m- %d % H: % M: % S " ) ,
self . activity . strftime ( " % Y- % m- %d % H: % M: % S " ) ,
self . end . strftime ( " % Y- % m- %d % H: % M: % S " ) if self . end else " " ,
)
2017-06-12 07:47:24 +00:00
2022-12-19 19:55:33 +00:00
@property
def duration ( self ) :
if self . end is None :
return self . activity - self . start
return self . end - self . start
2016-07-18 11:22:50 +00:00
2016-08-26 18:57:04 +00:00
class CashRegisterSummary ( models . Model ) :
2018-10-04 19:29:19 +00:00
user = models . ForeignKey (
2019-10-05 17:05:56 +00:00
User ,
related_name = " cash_summaries " ,
verbose_name = _ ( " user " ) ,
on_delete = models . CASCADE ,
2018-10-04 19:29:19 +00:00
)
counter = models . ForeignKey (
2019-10-05 17:05:56 +00:00
Counter ,
related_name = " cash_summaries " ,
verbose_name = _ ( " counter " ) ,
on_delete = models . CASCADE ,
2018-10-04 19:29:19 +00:00
)
date = models . DateTimeField ( _ ( " date " ) )
comment = models . TextField ( _ ( " comment " ) , null = True , blank = True )
emptied = models . BooleanField ( _ ( " emptied " ) , default = False )
2016-08-26 18:57:04 +00:00
class Meta :
verbose_name = _ ( " cash register summary " )
def __str__ ( self ) :
return " At %s by %s - Total: %s € " % ( self . counter , self . user , self . get_total ( ) )
2024-06-27 13:48:07 +00:00
def save ( self , * args , * * kwargs ) :
if not self . id :
self . date = timezone . now ( )
return super ( ) . save ( * args , * * kwargs )
def get_absolute_url ( self ) :
return reverse ( " counter:cash_summary_list " )
2016-09-29 12:54:03 +00:00
def __getattribute__ ( self , name ) :
2018-10-04 19:29:19 +00:00
if name [ : 5 ] == " check " :
checks = self . items . filter ( check = True ) . order_by ( " value " ) . all ( )
if name == " ten_cents " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 0.1 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " twenty_cents " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 0.2 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " fifty_cents " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 0.5 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " one_euro " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 1 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " two_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 2 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " five_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 5 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " ten_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 10 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " twenty_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 20 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " fifty_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 50 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " hundred_euros " :
2024-06-26 09:24:56 +00:00
return self . items . filter ( value = 100 , is_check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " check_1 " :
2024-10-15 09:36:26 +00:00
return checks [ 0 ] if len ( checks ) > 0 else None
2018-10-04 19:29:19 +00:00
elif name == " check_2 " :
2024-10-15 09:36:26 +00:00
return checks [ 1 ] if len ( checks ) > 1 else None
2018-10-04 19:29:19 +00:00
elif name == " check_3 " :
2024-10-15 09:36:26 +00:00
return checks [ 2 ] if len ( checks ) > 2 else None
2018-10-04 19:29:19 +00:00
elif name == " check_4 " :
2024-10-15 09:36:26 +00:00
return checks [ 3 ] if len ( checks ) > 3 else None
2018-10-04 19:29:19 +00:00
elif name == " check_5 " :
2024-10-15 09:36:26 +00:00
return checks [ 4 ] if len ( checks ) > 4 else None
2016-09-29 12:54:03 +00:00
else :
return object . __getattribute__ ( self , name )
2016-09-13 00:04:49 +00:00
def is_owned_by ( self , user ) :
2024-07-12 07:34:16 +00:00
""" Method to see if that object can be edited by the given user. """
2023-05-02 10:36:59 +00:00
if user . is_anonymous :
return False
2024-10-15 09:36:26 +00:00
return user . is_in_group ( pk = settings . SITH_GROUP_COUNTER_ADMIN_ID )
2016-09-13 00:04:49 +00:00
2016-08-26 18:57:04 +00:00
def get_total ( self ) :
t = 0
for it in self . items . all ( ) :
t + = it . quantity * it . value
return t
2017-06-12 07:47:24 +00:00
2016-08-26 18:57:04 +00:00
class CashRegisterSummaryItem ( models . Model ) :
2018-10-04 19:29:19 +00:00
cash_summary = models . ForeignKey (
2019-10-05 17:05:56 +00:00
CashRegisterSummary ,
related_name = " items " ,
verbose_name = _ ( " cash summary " ) ,
on_delete = models . CASCADE ,
2018-10-04 19:29:19 +00:00
)
2016-08-26 18:57:04 +00:00
value = CurrencyField ( _ ( " value " ) )
2018-10-04 19:29:19 +00:00
quantity = models . IntegerField ( _ ( " quantity " ) , default = 0 )
2024-06-26 09:24:56 +00:00
is_check = models . BooleanField (
_ ( " check " ) ,
default = False ,
help_text = _ ( " True if this is a bank check, else False " ) ,
)
2016-08-26 18:57:04 +00:00
class Meta :
verbose_name = _ ( " cash register summary item " )
2016-05-30 10:23:59 +00:00
2024-06-27 13:48:07 +00:00
def __str__ ( self ) :
return str ( self . value )
2017-06-12 07:47:24 +00:00
2016-10-03 17:30:05 +00:00
class Eticket ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Eticket can be linked to a product an allows PDF generation. """
2018-10-04 19:29:19 +00:00
product = models . OneToOneField (
2019-10-05 17:09:15 +00:00
Product ,
related_name = " eticket " ,
verbose_name = _ ( " product " ) ,
on_delete = models . CASCADE ,
2018-10-04 19:29:19 +00:00
)
banner = models . ImageField (
upload_to = " etickets " , null = True , blank = True , verbose_name = _ ( " banner " )
)
event_date = models . DateField ( _ ( " event date " ) , null = True , blank = True )
event_title = models . CharField (
_ ( " event title " ) , max_length = 64 , null = True , blank = True
)
secret = models . CharField ( _ ( " secret " ) , max_length = 64 , unique = True )
2016-10-03 17:30:05 +00:00
def __str__ ( self ) :
2024-06-27 13:48:07 +00:00
return self . product . name
2016-10-03 17:30:05 +00:00
def save ( self , * args , * * kwargs ) :
if not self . id :
self . secret = base64 . b64encode ( os . urandom ( 32 ) )
2024-06-27 12:46:43 +00:00
return super ( ) . save ( * args , * * kwargs )
2016-10-03 17:30:05 +00:00
2024-06-27 13:48:07 +00:00
def get_absolute_url ( self ) :
return reverse ( " counter:eticket_list " )
2016-10-03 17:30:05 +00:00
def is_owned_by ( self , user ) :
2024-07-12 07:34:16 +00:00
""" Method to see if that object can be edited by the given user. """
2023-05-02 10:36:59 +00:00
if user . is_anonymous :
return False
return user . is_in_group ( pk = settings . SITH_GROUP_COUNTER_ADMIN_ID )
2016-10-03 17:30:05 +00:00
def get_hash ( self , string ) :
2017-06-12 07:47:24 +00:00
import hashlib
import hmac
2018-10-04 19:29:19 +00:00
return hmac . new (
bytes ( self . secret , " utf-8 " ) , bytes ( string , " utf-8 " ) , hashlib . sha1
) . hexdigest ( )
2018-10-17 23:16:26 +00:00
class StudentCard ( models . Model ) :
2024-07-12 07:34:16 +00:00
""" Alternative way to connect a customer into a counter.
2018-10-17 23:16:26 +00:00
We are using Mifare DESFire EV1 specs since it ' s used for izly cards
https : / / www . nxp . com / docs / en / application - note / AN10927 . pdf
2024-07-12 07:34:16 +00:00
UID is 7 byte long that means 14 hexa characters .
2018-10-17 23:16:26 +00:00
"""
UID_SIZE = 14
2024-06-27 13:48:07 +00:00
uid = models . CharField (
_ ( " uid " ) , max_length = UID_SIZE , unique = True , validators = [ MinLengthValidator ( 4 ) ]
)
2024-12-08 15:07:25 +00:00
customer = models . OneToOneField (
2024-06-27 13:48:07 +00:00
Customer ,
2024-12-08 15:07:25 +00:00
related_name = " student_card " ,
verbose_name = _ ( " student card " ) ,
2024-06-27 13:48:07 +00:00
on_delete = models . CASCADE ,
)
2024-12-08 15:07:25 +00:00
class Meta :
verbose_name = _ ( " student card " )
verbose_name_plural = _ ( " student cards " )
2024-06-27 13:48:07 +00:00
def __str__ ( self ) :
return self . uid
2018-10-19 17:25:55 +00:00
@staticmethod
2024-12-08 15:07:25 +00:00
def is_valid ( uid : str ) - > bool :
2018-10-19 17:25:55 +00:00
return (
2019-05-24 06:38:15 +00:00
( uid . isupper ( ) or uid . isnumeric ( ) )
2019-05-14 13:51:14 +00:00
and len ( uid ) == StudentCard . UID_SIZE
2018-11-11 21:56:51 +00:00
and not StudentCard . objects . filter ( uid = uid ) . exists ( )
2018-10-19 17:25:55 +00:00
)
@staticmethod
2018-11-11 21:56:51 +00:00
def can_create ( customer , user ) :
2018-10-19 17:25:55 +00:00
return user . pk == customer . user . pk or user . is_board_member or user . is_root
2019-05-14 13:13:14 +00:00
def can_be_edited_by ( self , obj ) :
2018-10-19 17:25:55 +00:00
if isinstance ( obj , User ) :
2018-11-11 21:56:51 +00:00
return StudentCard . can_create ( self . customer , obj )
2018-10-19 17:25:55 +00:00
return False