2017-04-24 15:51:12 +00:00
# -*- coding:utf-8 -*
#
# Copyright 2016,2017
# - Skia <skia@libskia.so>
#
# Ce fichier fait partie du site de l'Association des Étudiants de l'UTBM,
# http://ae.utbm.fr.
#
# This program is free software; you can redistribute it and/or modify it under
# the terms of the GNU General Public License a published by the Free Software
# Foundation; either version 3 of the License, or (at your option) any later
# version.
#
# This program is distributed in the hope that it will be useful, but WITHOUT
# ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
# FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
# details.
#
# You should have received a copy of the GNU General Public License along with
# this program; if not, write to the Free Sofware Foundation, Inc., 59 Temple
# Place - Suite 330, Boston, MA 02111-1307, USA.
#
#
2017-06-12 07:47:24 +00:00
from django . db import models
2022-12-10 19:41:35 +00:00
from django . db . models . functions import Length
2022-08-03 22:26:43 +00:00
from django . utils . translation import gettext_lazy as _
2016-04-19 17:58:57 +00:00
from django . utils import timezone
2016-03-28 12:54:35 +00:00
from django . conf import settings
2019-10-06 11:28:56 +00:00
from django . urls import reverse
2018-10-18 23:21:57 +00:00
from django . core . validators import MinLengthValidator
2016-08-01 22:32:55 +00:00
from django . forms import ValidationError
2017-05-14 02:36:04 +00:00
from django . utils . functional import cached_property
2016-03-28 12:54:35 +00:00
2017-03-27 21:24:25 +00:00
from datetime import timedelta , date
2016-08-04 22:50:25 +00:00
import random
import string
2016-10-03 17:30:05 +00:00
import os
import base64
2016-10-16 16:52:04 +00:00
import datetime
2022-11-28 16:03:46 +00:00
from dict2xml import dict2xml
2016-04-19 17:58:57 +00:00
2022-12-10 19:41:35 +00:00
from sith . settings import SITH_COUNTER_OFFICES , SITH_MAIN_CLUB
2022-04-20 12:01:33 +00:00
from club . models import Club , Membership
2016-05-30 10:23:59 +00:00
from accounting . models import CurrencyField
2016-12-08 18:47:28 +00:00
from core . models import Group , User , Notification
2016-12-10 00:58:30 +00:00
from subscription . models import Subscription
2016-03-28 12:54:35 +00:00
2022-11-28 16:03:46 +00:00
from django_countries . fields import CountryField
2017-06-12 07:47:24 +00:00
2016-05-30 10:23:59 +00:00
class Customer ( models . Model ) :
"""
2022-11-28 16:03:46 +00:00
This class extends a user to make a customer . It adds some basic customers ' information, such as the account ID, and
2016-05-30 10:23:59 +00:00
is used by other accounting classes as reference to the customer , rather than using User
"""
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 )
amount = CurrencyField ( _ ( " amount " ) )
recorded_products = models . IntegerField ( _ ( " recorded product " ) , default = 0 )
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
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 :
"""
Check if whether this customer has the right to
purchase any item .
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.
"""
2018-10-04 19:29:19 +00:00
return self . user . subscriptions . last ( ) and (
date . today ( )
- self . user . subscriptions . order_by ( " subscription_end " )
. last ( )
. subscription_end
) < timedelta ( days = 90 )
2017-03-27 21:24:25 +00:00
2022-11-28 16:03:46 +00:00
@classmethod
def new_for_user ( cls , user : User ) :
"""
Create a new Customer instance for the user given in parameter without saving it
The account if is automatically generated and the amount set at 0
"""
# account_id are number with a letter appended
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
return cls ( user = user , account_id = " 1504a " , amount = 0 )
account_id = account_id [ " account_id " ]
num = int ( account_id [ : - 1 ] )
while Customer . objects . filter ( account_id = account_id ) . exists ( ) :
num + = 1
account_id = str ( num ) + random . choice ( string . ascii_lowercase )
return cls ( user = user , account_id = account_id , amount = 0 )
2016-07-19 22:28:49 +00:00
2017-08-15 00:09:44 +00:00
def save ( self , allow_negative = False , is_selling = False , * args , * * kwargs ) :
2017-08-15 12:03:56 +00:00
"""
2020-08-27 13:59:42 +00:00
is_selling : tell if the current action is a selling
allow_negative : ignored if not a selling . Allow a selling to put the account in negative
Those two parameters avoid blocking the save method of a customer if his account is negative
2017-08-15 12:03:56 +00:00
"""
2017-08-15 00:09:44 +00:00
if self . amount < 0 and ( is_selling and not allow_negative ) :
2016-08-01 22:32:55 +00:00
raise ValidationError ( _ ( " Not enough money " ) )
super ( Customer , self ) . save ( * args , * * kwargs )
2016-09-21 12:09:16 +00:00
def recompute_amount ( self ) :
self . amount = 0
for r in self . refillings . all ( ) :
self . amount + = r . amount
for s in self . buyings . filter ( payment_method = " SITH_ACCOUNT " ) :
self . amount - = s . quantity * s . unit_price
self . save ( )
2016-10-21 11:09:46 +00:00
def get_absolute_url ( self ) :
2018-10-04 19:29:19 +00:00
return reverse ( " core:user_account " , kwargs = { " user_id " : self . user . pk } )
2016-10-21 11:09:46 +00:00
def get_full_url ( self ) :
2018-10-04 19:29:19 +00:00
return " " . join ( [ " 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 ) :
"""
Represent the billing information of a user , which are required
by the 3 D - Secure v2 system used by the etransaction module
"""
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 " ) )
def to_3dsv2_xml ( self ) - > str :
"""
Convert the data from this model into a xml usable
by the online paying service of the Crédit Agricole bank .
2022-12-10 19:41:35 +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
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
def __str__ ( self ) :
return f " { self . first_name } { self . last_name } "
2016-05-30 10:23:59 +00:00
class ProductType ( models . Model ) :
"""
This describes a product type
Useful only for categorizing , changes are made at the product level for now
"""
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 )
icon = models . ImageField ( 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
2016-05-30 10:23:59 +00:00
def is_owned_by ( self , user ) :
"""
Method to see if that object can be edited by the given user
"""
2016-12-10 00:29:56 +00:00
if user . is_in_group ( settings . SITH_GROUP_ACCOUNTING_ADMIN_ID ) :
2016-05-30 10:23:59 +00:00
return True
return False
def __str__ ( self ) :
return self . name
2016-07-27 18:05:45 +00:00
def get_absolute_url ( self ) :
2018-10-04 19:29:19 +00:00
return reverse ( " counter:producttype_list " )
2016-07-27 18:05:45 +00:00
2017-06-12 07:47:24 +00:00
2016-05-30 10:23:59 +00:00
class Product ( models . Model ) :
"""
This describes a product , with all its related informations
"""
2018-10-04 19:29:19 +00:00
name = models . CharField ( _ ( " name " ) , max_length = 64 )
description = models . TextField ( _ ( " description " ) , blank = True )
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 )
purchase_price = CurrencyField ( _ ( " purchase price " ) )
selling_price = CurrencyField ( _ ( " selling price " ) )
special_selling_price = CurrencyField ( _ ( " special selling price " ) )
icon = models . ImageField (
upload_to = " products " , null = True , blank = True , verbose_name = _ ( " icon " )
)
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 )
parent_product = models . ForeignKey (
" self " ,
related_name = " children_products " ,
verbose_name = _ ( " parent product " ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
)
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
2017-07-21 19:39:49 +00:00
@property
def is_record_product ( self ) :
2017-08-15 00:09:44 +00:00
return settings . SITH_ECOCUP_CONS == self . id
2017-07-21 19:39:49 +00:00
@property
def is_unrecord_product ( self ) :
2017-08-15 00:09:44 +00:00
return settings . SITH_ECOCUP_DECO == self . id
2017-07-21 19:39:49 +00:00
2016-07-20 16:48:18 +00:00
def is_owned_by ( self , user ) :
2016-05-30 10:23:59 +00:00
"""
Method to see if that object can be edited by the given user
"""
2018-10-04 19:29:19 +00:00
if user . is_in_group (
settings . SITH_GROUP_ACCOUNTING_ADMIN_ID
) or user . is_in_group ( settings . SITH_GROUP_COUNTER_ADMIN_ID ) :
2016-05-30 10:23:59 +00:00
return True
return False
2016-07-27 15:23:02 +00:00
def get_absolute_url ( self ) :
2018-10-04 19:29:19 +00:00
return reverse ( " counter:product_list " )
2016-07-27 15:23:02 +00:00
2022-09-25 19:29:42 +00:00
def can_be_sold_to ( self , user : User ) - > bool :
"""
Check if whether the user given in parameter has the right to buy
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 ) .
: return : True if the user can buy this product else False
"""
if not self . buying_groups . exists ( ) :
return True
for group in self . buying_groups . all ( ) :
if user . is_in_group ( group . name ) :
return True
return False
2022-12-19 19:55:33 +00:00
@property
def profit ( self ) :
return self . selling_price - self . purchase_price
2022-09-25 19:29:42 +00:00
def __str__ ( self ) :
return " %s ( %s ) " % ( self . name , self . code )
2017-06-12 07:47:24 +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
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 __getattribute__ ( self , name ) :
2016-08-01 14:36:16 +00:00
if name == " edit_groups " :
2018-10-04 19:29:19 +00:00
return Group . objects . filter (
name = self . club . unix_name + settings . SITH_BOARD_SUFFIX
) . all ( )
2016-03-28 12:54:35 +00:00
return object . __getattribute__ ( self , name )
def __str__ ( self ) :
return self . name
2016-03-29 08:30:24 +00:00
def get_absolute_url ( self ) :
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
2016-08-01 14:36:16 +00:00
def is_owned_by ( self , user ) :
2016-08-29 14:07:14 +00:00
mem = self . club . get_membership_for ( user )
if mem and mem . role > = 7 :
return True
2016-12-10 00:29:56 +00:00
return user . is_in_group ( settings . SITH_GROUP_COUNTER_ADMIN_ID )
2016-07-17 22:47:56 +00:00
2016-03-29 08:30:24 +00:00
def can_be_viewed_by ( self , user ) :
2017-01-04 18:39:37 +00:00
if self . type == " BAR " :
2016-08-01 14:36:16 +00:00
return True
2018-10-04 19:29:19 +00:00
return (
user . is_in_group ( settings . SITH_MAIN_BOARD_GROUP )
or user in self . sellers . all ( )
)
2016-04-19 17:58:57 +00:00
2016-09-26 09:17:00 +00:00
def gen_token ( self ) :
""" Generate a new token for this counter """
2018-10-04 19:29:19 +00:00
self . token = " " . join (
random . choice ( string . ascii_letters + string . digits ) for x in range ( 30 )
)
2016-09-26 09:17:00 +00:00
self . save ( )
2016-08-26 18:56:16 +00:00
def add_barman ( self , user ) :
2016-07-18 11:22:50 +00:00
"""
Logs a barman in to the given counter
A user is stored as a tuple with its login time
"""
2016-08-26 18:56:16 +00:00
Permanency ( user = user , counter = self , start = timezone . now ( ) , end = None ) . save ( )
def del_barman ( self , user ) :
2016-07-18 11:22:50 +00:00
"""
Logs a barman out and store its permanency
"""
2016-08-26 18:56:16 +00:00
perm = Permanency . objects . filter ( counter = self , user = user , end = None ) . all ( )
for p in perm :
p . end = p . activity
p . save ( )
2016-07-18 11:22:50 +00:00
2017-05-14 02:36:04 +00:00
@cached_property
def barmen_list ( self ) :
return self . get_barmen_list ( )
2016-08-06 10:37:36 +00:00
def get_barmen_list ( self ) :
2016-07-18 11:22:50 +00:00
"""
2016-07-21 18:03:31 +00:00
Returns the barman list as list of User
2016-07-18 11:22:50 +00:00
Also handle the timeout of the barmen
"""
2016-08-26 18:56:16 +00:00
pl = Permanency . objects . filter ( counter = self , end = None ) . all ( )
2016-04-19 17:58:57 +00:00
bl = [ ]
2016-08-26 18:56:16 +00:00
for p in pl :
2018-10-04 19:29:19 +00:00
if timezone . now ( ) - p . activity < timedelta (
minutes = settings . SITH_BARMAN_TIMEOUT
) :
2016-08-26 18:56:16 +00:00
bl . append ( p . user )
2016-04-19 17:58:57 +00:00
else :
2016-08-26 18:56:16 +00:00
p . end = p . activity
p . save ( )
2016-04-19 17:58:57 +00:00
return bl
2016-08-06 10:37:36 +00:00
def get_random_barman ( self ) :
2016-09-12 15:34:33 +00:00
"""
Return a random user being currently a barman
"""
2016-08-06 10:37:36 +00:00
bl = self . get_barmen_list ( )
2016-08-18 19:06:10 +00:00
return bl [ random . randrange ( 0 , len ( bl ) ) ]
2016-05-31 17:32:15 +00:00
2016-09-12 15:34:33 +00:00
def update_activity ( self ) :
"""
Update the barman activity to prevent timeout
"""
for p in Permanency . objects . filter ( counter = self , end = None ) . all ( ) :
2017-06-12 07:47:24 +00:00
p . save ( ) # Update activity
2016-09-12 15:34:33 +00:00
2016-08-05 18:01:23 +00:00
def is_open ( self ) :
2017-05-14 02:36:04 +00:00
return len ( self . barmen_list ) > 0
2016-08-05 18:01:23 +00:00
2016-10-16 16:52:04 +00:00
def is_inactive ( self ) :
"""
2016-11-10 12:51:56 +00:00
Returns True if the counter self is inactive from SITH_COUNTER_MINUTE_INACTIVE ' s value minutes, else False
2016-10-16 16:52:04 +00:00
"""
2018-10-04 19:29:19 +00:00
return self . is_open ( ) and (
( timezone . now ( ) - self . permanencies . order_by ( " -activity " ) . first ( ) . activity )
> datetime . timedelta ( minutes = settings . SITH_COUNTER_MINUTE_INACTIVE )
)
2016-10-16 16:52:04 +00:00
2016-08-06 10:37:36 +00:00
def barman_list ( self ) :
2016-09-12 15:34:33 +00:00
"""
Returns the barman id list
"""
2016-08-06 10:37:36 +00:00
return [ b . id for b in self . get_barmen_list ( ) ]
2022-04-20 12:01:33 +00:00
def can_refill ( self ) :
2022-05-22 09:56:53 +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
if self . id in SITH_COUNTER_OFFICES :
# If the counter is either 'AE' or 'BdF', refills are authorized
2022-05-22 09:56:53 +00:00
return True
2022-04-20 12:01:33 +00:00
is_ae_member = False
2022-04-27 13:38:55 +00:00
ae = Club . objects . get ( unix_name = SITH_MAIN_CLUB [ " unix_name " ] )
2022-04-20 12:01:33 +00:00
for barman in self . get_barmen_list ( ) :
2022-05-01 22:04:00 +00:00
if ae . get_membership_for ( barman ) :
2022-04-20 12:01:33 +00:00
is_ae_member = True
return is_ae_member
2017-06-12 07:47:24 +00:00
2016-05-31 17:32:15 +00:00
class Refilling ( models . Model ) :
"""
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 ,
choices = settings . SITH_COUNTER_PAYMENT_METHOD ,
default = " CASH " ,
)
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
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
2016-08-18 19:06:10 +00:00
def is_owned_by ( self , user ) :
2016-08-29 14:07:14 +00:00
return user . is_owner ( self . counter ) and self . payment_method != " CARD "
2016-05-31 17:32:15 +00:00
2016-08-18 19:06:10 +00:00
def delete ( self , * args , * * kwargs ) :
self . customer . amount - = self . amount
self . customer . save ( )
super ( Refilling , self ) . delete ( * args , * * kwargs )
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 ( )
2016-06-26 18:07:29 +00:00
super ( Refilling , self ) . save ( * args , * * kwargs )
2016-05-31 17:32:15 +00:00
2017-06-12 07:47:24 +00:00
2016-05-31 17:32:15 +00:00
class Selling ( models . Model ) :
"""
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
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
2016-08-18 19:06:10 +00:00
def is_owned_by ( self , user ) :
2016-08-29 14:07:14 +00:00
return user . is_owner ( self . counter ) and self . payment_method != " CARD "
2016-08-18 19:06:10 +00:00
2016-10-03 17:30:05 +00:00
def can_be_viewed_by ( self , user ) :
2019-10-17 09:11:13 +00:00
if (
not hasattr ( self , " customer " ) or self . customer is None
) : # Customer can be set to Null
2019-10-16 19:21:51 +00:00
return False
2016-10-03 17:30:05 +00:00
return user == self . customer . user
2016-08-18 19:06:10 +00:00
def delete ( self , * args , * * kwargs ) :
2016-11-10 12:51:56 +00:00
if self . payment_method == " SITH_ACCOUNT " :
self . customer . amount + = self . quantity * self . unit_price
self . customer . save ( )
2016-08-18 19:06:10 +00:00
super ( Selling , self ) . delete ( * args , * * kwargs )
2016-10-21 11:09:46 +00:00
def send_mail_customer ( self ) :
2016-10-25 19:53:40 +00:00
event = self . product . eticket . event_title or _ ( " Unknown event " )
2018-10-04 19:29:19 +00:00
subject = _ ( " Eticket bought for the event %(event)s " ) % { " event " : event }
2016-10-21 11:09:46 +00:00
message_html = _ (
2019-10-13 22:32:11 +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 . "
2016-10-21 11:09:46 +00:00
) % {
2018-10-04 19:29:19 +00:00
" event " : event ,
" url " : " " . join (
(
' <a href= " ' ,
self . customer . get_full_url ( ) ,
' " > ' ,
self . customer . get_full_url ( ) ,
" </a> " ,
)
) ,
2019-10-13 22:32:11 +00:00
" eticket " : " " . join (
(
' <a href= " ' ,
self . get_eticket_full_url ( ) ,
' " > ' ,
self . get_eticket_full_url ( ) ,
" </a> " ,
)
) ,
2016-10-21 11:09:46 +00:00
}
message_txt = _ (
2019-10-13 22:32:11 +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 . "
2019-10-13 15:29:08 +00:00
) % {
" event " : event ,
" url " : self . customer . get_full_url ( ) ,
2019-10-13 22:32:11 +00:00
" eticket " : self . get_eticket_full_url ( ) ,
2019-10-13 15:29:08 +00:00
}
2018-10-04 19:29:19 +00:00
self . customer . user . email_user ( subject , message_txt , html_message = message_html )
2016-10-21 11:09:46 +00:00
2017-08-14 11:52:58 +00:00
def save ( self , allow_negative = False , * args , * * kwargs ) :
2017-08-15 12:03:56 +00:00
"""
2020-08-27 13:59:42 +00:00
allow_negative : Allow this selling to use more money than available for this user
2017-08-15 12:03:56 +00:00
"""
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
2016-12-20 22:27:54 +00:00
u = User . objects . filter ( id = self . customer . user . id ) . first ( )
2017-02-24 01:59:59 +00:00
if u . 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 (
2017-06-12 07:47:24 +00:00
member = u ,
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
u = User . objects . filter ( id = self . customer . user . id ) . first ( )
sub = Subscription (
2017-06-12 07:47:24 +00:00
member = u ,
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 ( )
2017-09-02 10:42:07 +00:00
if self . customer . user . preferences . notify_on_click :
Notification (
user = self . customer . user ,
2018-10-04 19:29:19 +00:00
url = reverse (
" core:user_account_detail " ,
kwargs = {
" user_id " : self . customer . user . id ,
" 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 ( )
2016-05-31 17:32:15 +00:00
super ( Selling , self ) . save ( * args , * * kwargs )
2019-10-13 15:29:08 +00:00
try :
# The product has no id until it's saved
if self . product . eticket :
self . send_mail_customer ( )
except :
pass
def get_eticket_full_url ( self ) :
eticket_url = reverse ( " counter:eticket_pdf " , kwargs = { " selling_id " : self . id } )
2019-10-13 22:32:11 +00:00
return " " . join ( [ " 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 ) :
"""
This class aims at storing a traceability of who was barman where and when
"""
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 ( ) )
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 " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 0.1 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " twenty_cents " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 0.2 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " fifty_cents " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 0.5 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " one_euro " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 1 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " two_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 2 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " five_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 5 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " ten_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 10 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " twenty_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 20 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " fifty_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 50 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " hundred_euros " :
2016-09-29 12:54:03 +00:00
return self . items . filter ( value = 100 , check = False ) . first ( )
2018-10-04 19:29:19 +00:00
elif name == " check_1 " :
2016-09-29 12:54:03 +00:00
return checks [ 0 ] if 0 < len ( checks ) else None
2018-10-04 19:29:19 +00:00
elif name == " check_2 " :
2016-09-29 12:54:03 +00:00
return checks [ 1 ] if 1 < len ( checks ) else None
2018-10-04 19:29:19 +00:00
elif name == " check_3 " :
2016-09-29 12:54:03 +00:00
return checks [ 2 ] if 2 < len ( checks ) else None
2018-10-04 19:29:19 +00:00
elif name == " check_4 " :
2016-09-29 12:54:03 +00:00
return checks [ 3 ] if 3 < len ( checks ) else None
2018-10-04 19:29:19 +00:00
elif name == " check_5 " :
2016-09-29 12:54:03 +00:00
return checks [ 4 ] if 4 < len ( checks ) else None
else :
return object . __getattribute__ ( self , name )
2016-09-13 00:04:49 +00:00
def is_owned_by ( self , user ) :
"""
Method to see if that object can be edited by the given user
"""
2016-12-10 00:29:56 +00:00
if user . is_in_group ( settings . SITH_GROUP_COUNTER_ADMIN_ID ) :
2016-09-13 00:04:49 +00:00
return True
return False
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
def save ( self , * args , * * kwargs ) :
if not self . id :
self . date = timezone . now ( )
return super ( CashRegisterSummary , self ) . save ( * args , * * kwargs )
2016-09-29 12:54:03 +00:00
def get_absolute_url ( self ) :
2018-10-04 19:29:19 +00:00
return reverse ( " counter:cash_summary_list " )
2016-09-29 12:54:03 +00:00
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 )
check = models . BooleanField ( _ ( " check " ) , default = 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
2017-06-12 07:47:24 +00:00
2016-10-03 17:30:05 +00:00
class Eticket ( models . Model ) :
"""
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 ) :
return " %s " % ( self . product . name )
def get_absolute_url ( self ) :
2018-10-04 19:29:19 +00:00
return reverse ( " counter:eticket_list " )
2016-10-03 17:30:05 +00:00
def save ( self , * args , * * kwargs ) :
if not self . id :
self . secret = base64 . b64encode ( os . urandom ( 32 ) )
return super ( Eticket , self ) . save ( * args , * * kwargs )
def is_owned_by ( self , user ) :
"""
Method to see if that object can be edited by the given user
"""
2016-12-10 00:29:56 +00:00
return user . is_in_group ( 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 ) :
"""
Alternative way to connect a customer into a counter
We are using Mifare DESFire EV1 specs since it ' s used for izly cards
https : / / www . nxp . com / docs / en / application - note / AN10927 . pdf
UID is 7 byte long that means 14 hexa characters
"""
UID_SIZE = 14
2018-10-19 17:25:55 +00:00
@staticmethod
def is_valid ( uid ) :
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
2018-10-18 23:21:57 +00:00
uid = models . CharField (
_ ( " uid " ) , max_length = 14 , unique = True , validators = [ MinLengthValidator ( 4 ) ]
)
2018-10-17 23:16:26 +00:00
customer = models . ForeignKey (
Customer ,
related_name = " student_cards " ,
verbose_name = _ ( " student cards " ) ,
null = False ,
blank = False ,
2019-10-05 17:05:56 +00:00
on_delete = models . CASCADE ,
2018-10-17 23:16:26 +00:00
)