@ -1,39 +1,44 @@
import math
import re
from itertools import groupby
from decimal import Decimal , ROUND_HALF_DOWN
from django . utils import timezone
from . rules import is_creator , not_submitted , leads_excursion
from members . rules import is_leader , statement_not_submitted
from django . core . exceptions import ValidationError
from django . db import models
from django . db . models import Sum
from django . utils . translation import gettext_lazy as _
from django . utils . html import format_html
from members . models import Member , Freizeit , OEFFENTLICHE_ANREISE , MUSKELKRAFT_ANREISE
from django . conf import settings
import rules
from contrib . media import media_path
from contrib . models import CommonModel
from contrib . rules import has_global_perm
from utils import cvt_to_decimal , RestrictedFileField
from members . pdf import render_tex_with_attachments
from django . conf import settings
from django . db import models
from django . db . models import Sum
from django . utils import timezone
from django . utils . html import format_html
from django . utils . translation import gettext_lazy as _
from mailer . mailutils import send as send_mail
from contrib . media import media_path
from members . models import Freizeit
from members . models import Member
from members . models import MUSKELKRAFT_ANREISE
from members . models import OEFFENTLICHE_ANREISE
from members . pdf import render_tex_with_attachments
from members . rules import is_leader
from members . rules import statement_not_submitted
from schwifty import IBAN
import re
from utils import cvt_to_decimal
from utils import RestrictedFileField
from . rules import is_creator
from . rules import leads_excursion
from . rules import not_submitted
# Create your models here.
class Ledger ( models . Model ) :
name = models . CharField ( verbose_name = _ ( ' Name ' ) , max_length = 30 )
name = models . CharField ( verbose_name = _ ( " Name " ) , max_length = 30 )
def __str__ ( self ) :
return self . name
class Meta :
verbose_name = _ ( ' Ledger ' )
verbose_name_plural = _ ( ' Ledgers ' )
verbose_name = _ ( " Ledger " )
verbose_name_plural = _ ( " Ledgers " )
class TransactionIssue :
@ -51,88 +56,123 @@ class StatementManager(models.Manager):
class Statement ( CommonModel ) :
MISSING_LEDGER , NON_MATCHING_TRANSACTIONS , INVALID_ALLOWANCE_TO , INVALID_TOTAL , VALID = 0 , 1 , 2 , 3 , 4
MISSING_LEDGER , NON_MATCHING_TRANSACTIONS , INVALID_ALLOWANCE_TO , INVALID_TOTAL , VALID = (
0 ,
1 ,
2 ,
3 ,
4 ,
)
UNSUBMITTED , SUBMITTED , CONFIRMED = 0 , 1 , 2
STATUS_CHOICES = [ ( UNSUBMITTED , _ ( ' In preparation ' ) ) ,
( SUBMITTED , _ ( ' Submitted ' ) ) ,
( CONFIRMED , _ ( ' Completed ' ) ) ]
STATUS_CSS_CLASS = { SUBMITTED : ' submitted ' ,
CONFIRMED : ' confirmed ' ,
UNSUBMITTED : ' unsubmitted ' }
short_description = models . CharField ( verbose_name = _ ( ' Short description ' ) ,
max_length = 30 ,
blank = False )
explanation = models . TextField ( verbose_name = _ ( ' Explanation ' ) , blank = True )
excursion = models . OneToOneField ( Freizeit , verbose_name = _ ( ' Associated excursion ' ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL )
allowance_to = models . ManyToManyField ( Member , verbose_name = _ ( ' Pay allowance to ' ) ,
related_name = ' receives_allowance_for_statements ' ,
blank = True ,
help_text = _ ( ' The youth leaders to which an allowance should be paid. ' ) )
subsidy_to = models . ForeignKey ( Member , verbose_name = _ ( ' Pay subsidy to ' ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
related_name = ' receives_subsidy_for_statements ' ,
help_text = _ ( ' The person that should receive the subsidy for night and travel costs. Typically the person who paid for them. ' ) )
ljp_to = models . ForeignKey ( Member , verbose_name = _ ( ' Pay ljp contributions to ' ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
related_name = ' receives_ljp_for_statements ' ,
help_text = _ ( ' The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted. ' ) )
night_cost = models . DecimalField ( verbose_name = _ ( ' Price per night ' ) , default = 0 , decimal_places = 2 , max_digits = 5 )
status = models . IntegerField ( verbose_name = _ ( ' Status ' ) ,
choices = STATUS_CHOICES ,
default = UNSUBMITTED )
submitted_date = models . DateTimeField ( verbose_name = _ ( ' Submitted on ' ) , default = None , null = True )
confirmed_date = models . DateTimeField ( verbose_name = _ ( ' Paid on ' ) , default = None , null = True )
created_by = models . ForeignKey ( Member , verbose_name = _ ( ' Created by ' ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = ' created_statements ' )
submitted_by = models . ForeignKey ( Member , verbose_name = _ ( ' Submitted by ' ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = ' submitted_statements ' )
confirmed_by = models . ForeignKey ( Member , verbose_name = _ ( ' Authorized by ' ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = ' confirmed_statements ' )
STATUS_CHOICES = [
( UNSUBMITTED , _ ( " In preparation " ) ) ,
( SUBMITTED , _ ( " Submitted " ) ) ,
( CONFIRMED , _ ( " Completed " ) ) ,
]
STATUS_CSS_CLASS = { SUBMITTED : " submitted " , CONFIRMED : " confirmed " , UNSUBMITTED : " unsubmitted " }
short_description = models . CharField (
verbose_name = _ ( " Short description " ) , max_length = 30 , blank = False
)
explanation = models . TextField ( verbose_name = _ ( " Explanation " ) , blank = True )
excursion = models . OneToOneField (
Freizeit ,
verbose_name = _ ( " Associated excursion " ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
)
allowance_to = models . ManyToManyField (
Member ,
verbose_name = _ ( " Pay allowance to " ) ,
related_name = " receives_allowance_for_statements " ,
blank = True ,
help_text = _ ( " The youth leaders to which an allowance should be paid. " ) ,
)
subsidy_to = models . ForeignKey (
Member ,
verbose_name = _ ( " Pay subsidy to " ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
related_name = " receives_subsidy_for_statements " ,
help_text = _ (
" The person that should receive the subsidy for night and travel costs. Typically the person who paid for them. "
) ,
)
ljp_to = models . ForeignKey (
Member ,
verbose_name = _ ( " Pay ljp contributions to " ) ,
null = True ,
blank = True ,
on_delete = models . SET_NULL ,
related_name = " receives_ljp_for_statements " ,
help_text = _ (
" The person that should receive the ljp contributions for the participants. Should be only selected if an ljp request was submitted. "
) ,
)
night_cost = models . DecimalField (
verbose_name = _ ( " Price per night " ) , default = 0 , decimal_places = 2 , max_digits = 5
)
status = models . IntegerField (
verbose_name = _ ( " Status " ) , choices = STATUS_CHOICES , default = UNSUBMITTED
)
submitted_date = models . DateTimeField ( verbose_name = _ ( " Submitted on " ) , default = None , null = True )
confirmed_date = models . DateTimeField ( verbose_name = _ ( " Paid on " ) , default = None , null = True )
created_by = models . ForeignKey (
Member ,
verbose_name = _ ( " Created by " ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = " created_statements " ,
)
submitted_by = models . ForeignKey (
Member ,
verbose_name = _ ( " Submitted by " ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = " submitted_statements " ,
)
confirmed_by = models . ForeignKey (
Member ,
verbose_name = _ ( " Authorized by " ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = " confirmed_statements " ,
)
class Meta ( CommonModel . Meta ) :
verbose_name = _ ( ' Statement ' )
verbose_name_plural = _ ( ' Statements ' )
permissions = [
( ' may_edit_submitted_statements ' , ' Is allowed to edit submitted statements ' )
]
verbose_name = _ ( " Statement " )
verbose_name_plural = _ ( " Statements " )
permissions = [ ( " may_edit_submitted_statements " , " Is allowed to edit submitted statements " ) ]
rules_permissions = {
# All users may add draft statements.
' add_obj ' : rules . is_staff ,
" add_obj " : rules . is_staff ,
# All users may view their own statements and statements of excursions they are responsible for.
' view_obj ' : is_creator | leads_excursion | has_global_perm ( ' finance.view_global_statement ' ) ,
" view_obj " : is_creator
| leads_excursion
| has_global_perm ( " finance.view_global_statement " ) ,
# All users may change relevant (see above) draft statements.
' change_obj ' : ( not_submitted & ( is_creator | leads_excursion ) ) | has_global_perm ( ' finance.change_global_statement ' ) ,
" change_obj " : ( not_submitted & ( is_creator | leads_excursion ) )
| has_global_perm ( " finance.change_global_statement " ) ,
# All users may delete relevant (see above) draft statements.
' delete_obj ' : not_submitted & ( is_creator | leads_excursion | has_global_perm ( ' finance.delete_global_statement ' ) ) ,
" delete_obj " : not_submitted
& ( is_creator | leads_excursion | has_global_perm ( " finance.delete_global_statement " ) ) ,
}
@property
def title ( self ) :
if self . excursion is not None :
return _ ( ' Excursion %(excursion)s ' ) % { ' excursion ' : str ( self . excursion ) }
return _ ( " Excursion %(excursion)s " ) % { " excursion " : str ( self . excursion ) }
else :
return self . short_description
@ -149,10 +189,13 @@ class Statement(CommonModel):
def status_badge ( self ) :
code = Statement . STATUS_CSS_CLASS [ self . status ]
return format_html ( f ' <span class= " statement- { code } " > { Statement . STATUS_CHOICES [ self . status ] [ 1 ] } </span> ' )
status_badge . short_description = _ ( ' Status ' )
return format_html (
f ' <span class= " statement- { code } " > { Statement . STATUS_CHOICES [ self . status ] [ 1 ] } </span> '
)
status_badge . short_description = _ ( " Status " )
status_badge . allow_tags = True
status_badge . admin_order_field = ' status '
status_badge . admin_order_field = " status "
def submit ( self , submitter = None ) :
self . status = self . SUBMITTED
@ -174,7 +217,9 @@ class Statement(CommonModel):
total still differs from the transaction total . )
- If the statement is associated with an excursion : allowances , subsidies , LJP paiment and org fee .
"""
needed_paiments = [ ( b . paid_by , b . amount ) for b in self . bill_set . all ( ) if b . costs_covered and b . paid_by ]
needed_paiments = [
( b . paid_by , b . amount ) for b in self . bill_set . all ( ) if b . costs_covered and b . paid_by
]
if self . excursion is not None :
needed_paiments . extend ( [ ( yl , self . allowance_per_yl ) for yl in self . allowance_to . all ( ) ] )
@ -189,10 +234,20 @@ class Statement(CommonModel):
needed_paiments . append ( ( self . ljp_to , self . paid_ljp_contributions ) )
needed_paiments = sorted ( needed_paiments , key = lambda p : p [ 0 ] . pk )
target = dict ( map ( lambda p : ( p [ 0 ] , sum ( [ x [ 1 ] for x in p [ 1 ] ] ) ) , groupby ( needed_paiments , lambda p : p [ 0 ] ) ) )
target = dict (
map (
lambda p : ( p [ 0 ] , sum ( [ x [ 1 ] for x in p [ 1 ] ] ) ) ,
groupby ( needed_paiments , lambda p : p [ 0 ] ) ,
)
)
transactions = sorted ( self . transaction_set . all ( ) , key = lambda trans : trans . member . pk )
current = dict ( map ( lambda p : ( p [ 0 ] , sum ( [ t . amount for t in p [ 1 ] ] ) ) , groupby ( transactions , lambda trans : trans . member ) ) )
current = dict (
map (
lambda p : ( p [ 0 ] , sum ( [ t . amount for t in p [ 1 ] ] ) ) ,
groupby ( transactions , lambda trans : trans . member ) ,
)
)
issues = [ ]
for member , amount in target . items ( ) :
@ -274,8 +329,9 @@ class Statement(CommonModel):
def is_valid ( self ) :
return self . validity == Statement . VALID
is_valid . boolean = True
is_valid . short_description = _ ( ' Ready to confirm ' )
is_valid . short_description = _ ( " Ready to confirm " )
def confirm ( self , confirmer = None ) :
if not self . submitted :
@ -303,7 +359,13 @@ class Statement(CommonModel):
if not bill . paid_by :
return False
ref = " {} : {} " . format ( str ( self ) , bill . short_description )
Transaction ( statement = self , member = bill . paid_by , amount = bill . amount , confirmed = False , reference = ref ) . save ( )
Transaction (
statement = self ,
member = bill . paid_by ,
amount = bill . amount ,
confirmed = False ,
reference = ref ,
) . save ( )
# excursion specific
if self . excursion is None :
@ -311,23 +373,46 @@ class Statement(CommonModel):
# allowance
for yl in self . allowance_to . all ( ) :
ref = _ ( " Allowance for %(excu)s " ) % { ' excu ' : self . excursion . name }
Transaction ( statement = self , member = yl , amount = self . allowance_per_yl , confirmed = False , reference = ref ) . save ( )
ref = _ ( " Allowance for %(excu)s " ) % { " excu " : self . excursion . name }
Transaction (
statement = self ,
member = yl ,
amount = self . allowance_per_yl ,
confirmed = False ,
reference = ref ,
) . save ( )
# subsidies (i.e. night and transportation costs)
if self . subsidy_to :
ref = _ ( " Night and travel costs for %(excu)s " ) % { ' excu ' : self . excursion . name }
Transaction ( statement = self , member = self . subsidy_to , amount = self . total_subsidies , confirmed = False , reference = ref ) . save ( )
ref = _ ( " Night and travel costs for %(excu)s " ) % { " excu " : self . excursion . name }
Transaction (
statement = self ,
member = self . subsidy_to ,
amount = self . total_subsidies ,
confirmed = False ,
reference = ref ,
) . save ( )
if self . total_org_fee :
# if no subsidy receiver is given but org fees have to be paid. Just pick one of allowance receivers
ref = _ ( " reduced by org fee " )
Transaction ( statement = self , member = self . org_fee_payant , amount = - self . total_org_fee , confirmed = False , reference = ref ) . save ( )
Transaction (
statement = self ,
member = self . org_fee_payant ,
amount = - self . total_org_fee ,
confirmed = False ,
reference = ref ,
) . save ( )
if self . ljp_to :
ref = _ ( " LJP-Contribution %(excu)s " ) % { ' excu ' : self . excursion . name }
Transaction ( statement = self , member = self . ljp_to , amount = self . paid_ljp_contributions ,
confirmed = False , reference = ref ) . save ( )
ref = _ ( " LJP-Contribution %(excu)s " ) % { " excu " : self . excursion . name }
Transaction (
statement = self ,
member = self . ljp_to ,
amount = self . paid_ljp_contributions ,
confirmed = False ,
reference = ref ,
) . save ( )
return True
@ -335,11 +420,15 @@ class Statement(CommonModel):
# to minimize the number of needed bank transactions, we bundle transactions from same ledger to
# same member
transactions = self . transaction_set . all ( )
if any ( ( t . ledger is None for t in transactions ) ) :
if any ( t . ledger is None for t in transactions ) :
return
sort_key = lambda trans : ( trans . member . pk , trans . ledger . pk )
group_key = lambda trans : ( trans . member , trans . ledger )
def sort_key ( trans ) :
return ( trans . member . pk , trans . ledger . pk )
def group_key ( trans ) :
return ( trans . member , trans . ledger )
transactions = sorted ( transactions , key = sort_key )
for pair , transaction_group in groupby ( transactions , group_key ) :
member , ledger = pair
@ -347,10 +436,16 @@ class Statement(CommonModel):
if len ( grp ) == 1 :
continue
new_amount = sum ( ( trans . amount for trans in grp ) )
new_ref = " , " . join ( ( f " { trans . reference } EUR { trans . amount : .2f } " for trans in grp ) )
Transaction ( statement = self , member = member , amount = new_amount , confirmed = False , reference = new_ref ,
ledger = ledger ) . save ( )
new_amount = sum ( trans . amount for trans in grp )
new_ref = " , " . join ( f " { trans . reference } EUR { trans . amount : .2f } " for trans in grp )
Transaction (
statement = self ,
member = member ,
amount = new_amount ,
confirmed = False ,
reference = new_ref ,
ledger = ledger ,
) . save ( )
for trans in grp :
trans . delete ( )
@ -367,7 +462,7 @@ class Statement(CommonModel):
def bills_without_proof ( self ) :
""" Returns the bills that lack a proof file """
return [ bill for bill in self . bill_set . all ( ) if not bill . proof ]
@property
def total_bills_theoretic ( self ) :
return sum ( [ bill . amount for bill in self . bill_set . all ( ) ] )
@ -382,8 +477,10 @@ class Statement(CommonModel):
if self . excursion is None :
return 0
if self . excursion . tour_approach == MUSKELKRAFT_ANREISE \
or self . excursion . tour_approach == OEFFENTLICHE_ANREISE :
if (
self . excursion . tour_approach == MUSKELKRAFT_ANREISE
or self . excursion . tour_approach == OEFFENTLICHE_ANREISE
) :
return 0.15
else :
return 0.1
@ -431,9 +528,7 @@ class Statement(CommonModel):
@property
def total_per_yl ( self ) :
return self . transportation_per_yl \
+ self . allowance_per_yl \
+ self . nights_per_yl
return self . transportation_per_yl + self . allowance_per_yl + self . nights_per_yl
@property
def real_per_yl ( self ) :
@ -447,7 +542,11 @@ class Statement(CommonModel):
""" participants older than 26.99 years need to pay a specified organisation fee per person per day. """
if self . excursion is None :
return 0
return cvt_to_decimal ( settings . EXCURSION_ORG_FEE * self . excursion . duration * self . excursion . old_participant_count )
return cvt_to_decimal (
settings . EXCURSION_ORG_FEE
* self . excursion . duration
* self . excursion . old_participant_count
)
@property
def total_org_fee ( self ) :
@ -480,7 +579,7 @@ class Statement(CommonModel):
@property
def theoretical_total_staff ( self ) :
"""
the sum of subsidies and allowances if all eligible youth leaders would collect them .
the sum of subsidies and allowances if all eligible youth leaders would collect them .
"""
return self . total_per_yl * self . real_staff_count
@ -495,7 +594,6 @@ class Statement(CommonModel):
def total_staff_paid ( self ) :
return self . total_staff - self . total_org_fee
@property
def real_staff_count ( self ) :
if self . excursion is None :
@ -511,22 +609,26 @@ class Statement(CommonModel):
return 0
else :
return self . excursion . approved_staff_count
@property
def paid_ljp_contributions ( self ) :
if hasattr ( self . excursion , ' ljpproposal ' ) and self . ljp_to :
if hasattr ( self . excursion , " ljpproposal " ) and self . ljp_to :
if self . excursion . theoretic_ljp_participant_count < 5 :
return 0
return cvt_to_decimal (
min (
# if total costs are more than the max amount of the LJP contribution, we pay the max amount, reduced by taxes
( 1 - settings . LJP_TAX ) * settings . LJP_CONTRIBUTION_PER_DAY * self . excursion . ljp_participant_count * self . excursion . ljp_duration ,
( 1 - settings . LJP_TAX )
* settings . LJP_CONTRIBUTION_PER_DAY
* self . excursion . ljp_participant_count
* self . excursion . ljp_duration ,
# if the total costs are less than the max amount, we pay up to 90% of the total costs, reduced by taxes
( 1 - settings . LJP_TAX ) * 0.9 * ( float ( self . total_bills_not_covered ) + float ( self . total_staff ) ) ,
( 1 - settings . LJP_TAX )
* 0.9
* ( float ( self . total_bills_not_covered ) + float ( self . total_staff ) ) ,
# we never pay more than the maximum costs of the trip
float ( self . total_bills_not_covered )
float ( self . total_bills_not_covered ) ,
)
)
else :
@ -534,7 +636,7 @@ class Statement(CommonModel):
@property
def total ( self ) :
return self . total_bills + self . total_staff_paid + self . paid_ljp_contributions
return self . total_bills + self . total_staff_paid + self . paid_ljp_contributions
@property
def total_theoretic ( self ) :
@ -548,60 +650,61 @@ class Statement(CommonModel):
def total_pretty ( self ) :
return " {} € " . format ( self . total )
total_pretty . short_description = _ ( ' Total ' )
total_pretty . admin_order_field = ' total '
total_pretty . short_description = _ ( " Total " )
total_pretty . admin_order_field = " total "
def template_context ( self ) :
context = {
' total_bills ' : self . total_bills ,
' total_bills_theoretic ' : self . total_bills_theoretic ,
' bills_covered ' : self . bills_covered ,
' total ' : self . total ,
" total_bills " : self . total_bills ,
" total_bills_theoretic " : self . total_bills_theoretic ,
" bills_covered " : self . bills_covered ,
" total " : self . total ,
}
if self . excursion :
excursion_context = {
' nights ' : self . excursion . night_count ,
' price_per_night ' : self . real_night_cost ,
' duration ' : self . excursion . duration ,
' staff_count ' : self . real_staff_count ,
' kilometers_traveled ' : self . excursion . kilometers_traveled ,
' means_of_transport ' : self . excursion . get_tour_approach ( ) ,
' euro_per_km ' : self . euro_per_km ,
' allowance_per_day ' : settings . ALLOWANCE_PER_DAY ,
' allowances_paid ' : self . allowances_paid ,
' nights_per_yl ' : self . nights_per_yl ,
' allowance_per_yl ' : self . allowance_per_yl ,
' total_allowance ' : self . total_allowance ,
' transportation_per_yl ' : self . transportation_per_yl ,
' total_per_yl ' : self . total_per_yl ,
' total_staff ' : self . total_staff ,
' total_allowance ' : self . total_allowance ,
' theoretical_total_staff ' : self . theoretical_total_staff ,
' real_staff_count ' : self . real_staff_count ,
' total_subsidies ' : self . total_subsidies ,
' total_allowance ' : self . total_allowance ,
' subsidy_to ' : self . subsidy_to ,
' allowance_to ' : self . allowance_to ,
' paid_ljp_contributions ' : self . paid_ljp_contributions ,
' ljp_to ' : self . ljp_to ,
' theoretic_ljp_participant_count ' : self . excursion . theoretic_ljp_participant_count ,
' participant_count ' : self . excursion . participant_count ,
' total_seminar_days ' : self . excursion . total_seminar_days ,
' ljp_tax ' : settings . LJP_TAX * 100 ,
' total_org_fee_theoretical ' : self . total_org_fee_theoretical ,
' total_org_fee ' : self . total_org_fee ,
' old_participant_count ' : self . excursion . old_participant_count ,
' total_staff_paid ' : self . total_staff_paid ,
' org_fee ' : cvt_to_decimal ( settings . EXCURSION_ORG_FEE ) ,
" nights " : self . excursion . night_count ,
" price_per_night " : self . real_night_cost ,
" duration " : self . excursion . duration ,
" staff_count " : self . real_staff_count ,
" kilometers_traveled " : self . excursion . kilometers_traveled ,
" means_of_transport " : self . excursion . get_tour_approach ( ) ,
" euro_per_km " : self . euro_per_km ,
" allowance_per_day " : settings . ALLOWANCE_PER_DAY ,
" allowances_paid " : self . allowances_paid ,
" nights_per_yl " : self . nights_per_yl ,
" allowance_per_yl " : self . allowance_per_yl ,
" total_allowance " : self . total_allowance ,
" transportation_per_yl " : self . transportation_per_yl ,
" total_per_yl " : self . total_per_yl ,
" total_staff " : self . total_staff ,
" theoretical_total_staff " : self . theoretical_total_staff ,
" real_staff_count " : self . real_staff_count ,
" total_subsidies " : self . total_subsidies ,
" subsidy_to " : self . subsidy_to ,
" allowance_to " : self . allowance_to ,
" paid_ljp_contributions " : self . paid_ljp_contributions ,
" ljp_to " : self . ljp_to ,
" theoretic_ljp_participant_count " : self . excursion . theoretic_ljp_participant_count ,
" participant_count " : self . excursion . participant_count ,
" total_seminar_days " : self . excursion . total_seminar_days ,
" ljp_tax " : settings . LJP_TAX * 100 ,
" total_org_fee_theoretical " : self . total_org_fee_theoretical ,
" total_org_fee " : self . total_org_fee ,
" old_participant_count " : self . excursion . old_participant_count ,
" total_staff_paid " : self . total_staff_paid ,
" org_fee " : cvt_to_decimal ( settings . EXCURSION_ORG_FEE ) ,
}
return dict ( context , * * excursion_context )
else :
return context
def grouped_bills ( self ) :
return self . bill_set . values ( ' short_description ' ) \
. order_by ( ' short_description ' ) \
. annotate ( amount = Sum ( ' amount ' ) )
return (
self . bill_set . values ( " short_description " )
. order_by ( " short_description " )
. annotate ( amount = Sum ( " amount " ) )
)
def send_summary ( self , cc = None ) :
"""
@ -609,29 +712,34 @@ class Statement(CommonModel):
"""
excursion = self . excursion
context = dict ( statement = self . template_context ( ) , excursion = excursion , settings = settings )
pdf_filename = f " { excursion . code } _ { excursion . name } _Zuschussbeleg " if excursion else f " Abrechnungsbeleg "
pdf_filename = (
f " { excursion . code } _ { excursion . name } _Zuschussbeleg " if excursion else " Abrechnungsbeleg "
)
attachments = [ bill . proof . path for bill in self . bills_covered if bill . proof ]
filename = render_tex_with_attachments ( pdf_filename , ' finance/statement_summary.tex ' ,
context , attachments , save_only = True )
send_mail ( _ ( ' Statement summary for %(title)s ' ) % { ' title ' : self . title } ,
settings . SEND_STATEMENT_SUMMARY . format ( statement = self . title ) ,
sender = settings . DEFAULT_SENDING_MAIL ,
recipients = [ settings . SEKTION_FINANCE_MAIL ] ,
cc = cc ,
attachments = [ media_path ( filename ) ] )
filename = render_tex_with_attachments (
pdf_filename , " finance/statement_summary.tex " , context , attachments , save_only = True
)
send_mail (
_ ( " Statement summary for %(title)s " ) % { " title " : self . title } ,
settings . SEND_STATEMENT_SUMMARY . format ( statement = self . title ) ,
sender = settings . DEFAULT_SENDING_MAIL ,
recipients = [ settings . SEKTION_FINANCE_MAIL ] ,
cc = cc ,
attachments = [ media_path ( filename ) ] ,
)
class StatementOnExcursionProxy ( Statement ) :
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Statement ' )
verbose_name_plural = _ ( ' Statements ' )
verbose_name = _ ( " Statement " )
verbose_name_plural = _ ( " Statements " )
rules_permissions = {
# This is used as an inline on excursions, so we check for excursion permissions.
' add_obj ' : is_leader ,
' view_obj ' : is_leader | has_global_perm ( ' members.view_global_freizeit ' ) ,
' change_obj ' : is_leader & statement_not_submitted ,
' delete_obj ' : is_leader & statement_not_submitted ,
" add_obj " : is_leader ,
" view_obj " : is_leader | has_global_perm ( " members.view_global_freizeit " ) ,
" change_obj " : is_leader & statement_not_submitted ,
" delete_obj " : is_leader & statement_not_submitted ,
}
@ -645,13 +753,15 @@ class StatementUnSubmitted(Statement):
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Statement in preparation ' )
verbose_name_plural = _ ( ' Statements in preparation ' )
verbose_name = _ ( " Statement in preparation " )
verbose_name_plural = _ ( " Statements in preparation " )
rules_permissions = {
' add_obj ' : rules . is_staff ,
' view_obj ' : is_creator | leads_excursion | has_global_perm ( ' finance.view_global_statementunsubmitted ' ) ,
' change_obj ' : is_creator | leads_excursion ,
' delete_obj ' : is_creator | leads_excursion ,
" add_obj " : rules . is_staff ,
" view_obj " : is_creator
| leads_excursion
| has_global_perm ( " finance.view_global_statementunsubmitted " ) ,
" change_obj " : is_creator | leads_excursion ,
" delete_obj " : is_creator | leads_excursion ,
}
@ -665,10 +775,10 @@ class StatementSubmitted(Statement):
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Submitted statement ' )
verbose_name_plural = _ ( ' Submitted statements ' )
verbose_name = _ ( " Submitted statement " )
verbose_name_plural = _ ( " Submitted statements " )
permissions = [
( ' process_statementsubmitted ' , ' Can manage submitted statements. ' ) ,
( " process_statementsubmitted " , " Can manage submitted statements. " ) ,
]
@ -682,111 +792,135 @@ class StatementConfirmed(Statement):
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Paid statement ' )
verbose_name_plural = _ ( ' Paid statements ' )
verbose_name = _ ( " Paid statement " )
verbose_name_plural = _ ( " Paid statements " )
permissions = [
( ' may_manage_confirmed_statements ' , ' Can view and manage confirmed statements. ' ) ,
( " may_manage_confirmed_statements " , " Can view and manage confirmed statements. " ) ,
]
class Bill ( CommonModel ) :
statement = models . ForeignKey ( Statement , verbose_name = _ ( ' Statement ' ) , on_delete = models . CASCADE )
short_description = models . CharField ( verbose_name = _ ( ' Short description ' ) , max_length = 30 , blank = False )
explanation = models . TextField ( verbose_name = _ ( ' Explanation ' ) , blank = True )
amount = models . DecimalField ( verbose_name = _ ( ' Amount ' ) , max_digits = 6 , decimal_places = 2 , default = 0 )
paid_by = models . ForeignKey ( Member , verbose_name = _ ( ' Paid by ' ) , null = True ,
on_delete = models . SET_NULL )
costs_covered = models . BooleanField ( verbose_name = _ ( ' Covered ' ) , default = False )
refunded = models . BooleanField ( verbose_name = _ ( ' Refunded ' ) , default = False )
proof = RestrictedFileField ( verbose_name = _ ( ' Proof ' ) ,
upload_to = ' bill_images ' ,
blank = True ,
max_upload_size = 5 ,
content_types = [ ' application/pdf ' ,
' image/jpeg ' ,
' image/png ' ,
' image/gif ' ] )
statement = models . ForeignKey ( Statement , verbose_name = _ ( " Statement " ) , on_delete = models . CASCADE )
short_description = models . CharField (
verbose_name = _ ( " Short description " ) , max_length = 30 , blank = False
)
explanation = models . TextField ( verbose_name = _ ( " Explanation " ) , blank = True )
amount = models . DecimalField (
verbose_name = _ ( " Amount " ) , max_digits = 6 , decimal_places = 2 , default = 0
)
paid_by = models . ForeignKey (
Member , verbose_name = _ ( " Paid by " ) , null = True , on_delete = models . SET_NULL
)
costs_covered = models . BooleanField ( verbose_name = _ ( " Covered " ) , default = False )
refunded = models . BooleanField ( verbose_name = _ ( " Refunded " ) , default = False )
proof = RestrictedFileField (
verbose_name = _ ( " Proof " ) ,
upload_to = " bill_images " ,
blank = True ,
max_upload_size = 5 ,
content_types = [ " application/pdf " , " image/jpeg " , " image/png " , " image/gif " ] ,
)
def __str__ ( self ) :
return " {} ( {} €) " . format ( self . short_description , self . amount )
def pretty_amount ( self ) :
return " {} € " . format ( self . amount )
pretty_amount . admin_order_field = ' amount '
pretty_amount . short_description = _ ( ' Amount ' )
pretty_amount . admin_order_field = " amount "
pretty_amount . short_description = _ ( " Amount " )
class Meta ( CommonModel . Meta ) :
verbose_name = _ ( ' Bill ' )
verbose_name_plural = _ ( ' Bills ' )
verbose_name = _ ( " Bill " )
verbose_name_plural = _ ( " Bills " )
class BillOnExcursionProxy ( Bill ) :
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Bill ' )
verbose_name_plural = _ ( ' Bills ' )
verbose_name = _ ( " Bill " )
verbose_name_plural = _ ( " Bills " )
rules_permissions = {
' add_obj ' : leads_excursion & not_submitted ,
' view_obj ' : leads_excursion | has_global_perm ( ' finance.view_global_billonexcursionproxy ' ) ,
' change_obj ' : ( leads_excursion | has_global_perm ( ' finance.change_global_billonexcursionproxy ' ) ) & not_submitted ,
' delete_obj ' : ( leads_excursion | has_global_perm ( ' finance.delete_global_billonexcursionproxy ' ) ) & not_submitted ,
" add_obj " : leads_excursion & not_submitted ,
" view_obj " : leads_excursion
| has_global_perm ( " finance.view_global_billonexcursionproxy " ) ,
" change_obj " : (
leads_excursion | has_global_perm ( " finance.change_global_billonexcursionproxy " )
)
& not_submitted ,
" delete_obj " : (
leads_excursion | has_global_perm ( " finance.delete_global_billonexcursionproxy " )
)
& not_submitted ,
}
class BillOnStatementProxy ( Bill ) :
class Meta ( CommonModel . Meta ) :
proxy = True
verbose_name = _ ( ' Bill ' )
verbose_name_plural = _ ( ' Bills ' )
verbose_name = _ ( " Bill " )
verbose_name_plural = _ ( " Bills " )
rules_permissions = {
' add_obj ' : ( is_creator | leads_excursion ) & not_submitted ,
' view_obj ' : is_creator | leads_excursion | has_global_perm ( ' finance.view_global_billonstatementproxy ' ) ,
' change_obj ' : ( is_creator | leads_excursion | has_global_perm ( ' finance.change_global_billonstatementproxy ' ) )
& ( not_submitted | has_global_perm ( ' finance.process_statementsubmitted ' ) ) ,
' delete_obj ' : ( is_creator | leads_excursion | has_global_perm ( ' finance.delete_global_billonstatementproxy ' ) )
& not_submitted ,
" add_obj " : ( is_creator | leads_excursion ) & not_submitted ,
" view_obj " : is_creator
| leads_excursion
| has_global_perm ( " finance.view_global_billonstatementproxy " ) ,
" change_obj " : (
is_creator
| leads_excursion
| has_global_perm ( " finance.change_global_billonstatementproxy " )
)
& ( not_submitted | has_global_perm ( " finance.process_statementsubmitted " ) ) ,
" delete_obj " : (
is_creator
| leads_excursion
| has_global_perm ( " finance.delete_global_billonstatementproxy " )
)
& not_submitted ,
}
class Transaction ( models . Model ) :
reference = models . TextField ( verbose_name = _ ( ' Reference ' ) )
amount = models . DecimalField ( max_digits = 6 , decimal_places = 2 , verbose_name = _ ( ' Amount ' ) )
member = models . ForeignKey ( Member , verbose_name = _ ( ' Recipient ' ) ,
on_delete = models . CASCADE )
ledger = models . ForeignKey ( Ledger , blank = False , null = True , default = None , verbose_name = _ ( ' Ledger ' ) ,
on_delete = models . SET_NULL )
statement = models . ForeignKey ( Statement , verbose_name = _ ( ' Statement ' ) ,
on_delete = models . CASCADE )
confirmed = models . BooleanField ( verbose_name = _ ( ' Paid ' ) , default = False )
confirmed_date = models . DateTimeField ( verbose_name = _ ( ' Paid on ' ) , default = None , null = True )
confirmed_by = models . ForeignKey ( Member , verbose_name = _ ( ' Authorized by ' ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = ' confirmed_transactions ' )
reference = models . TextField ( verbose_name = _ ( " Reference " ) )
amount = models . DecimalField ( max_digits = 6 , decimal_places = 2 , verbose_name = _ ( " Amount " ) )
member = models . ForeignKey ( Member , verbose_name = _ ( " Recipient " ) , on_delete = models . CASCADE )
ledger = models . ForeignKey (
Ledger ,
blank = False ,
null = True ,
default = None ,
verbose_name = _ ( " Ledger " ) ,
on_delete = models . SET_NULL ,
)
statement = models . ForeignKey ( Statement , verbose_name = _ ( " Statement " ) , on_delete = models . CASCADE )
confirmed = models . BooleanField ( verbose_name = _ ( " Paid " ) , default = False )
confirmed_date = models . DateTimeField ( verbose_name = _ ( " Paid on " ) , default = None , null = True )
confirmed_by = models . ForeignKey (
Member ,
verbose_name = _ ( " Authorized by " ) ,
blank = True ,
null = True ,
on_delete = models . SET_NULL ,
related_name = " confirmed_transactions " ,
)
def __str__ ( self ) :
return " T# {} " . format ( self . pk )
@staticmethod
def escape_reference ( reference ) :
umlaut_map = {
' ä ' : ' ae ' , ' ö ' : ' oe ' , ' ü ' : ' ue ' ,
' Ä ' : ' Ae ' , ' Ö ' : ' Oe ' , ' Ü ' : ' Ue ' ,
' ß ' : ' ss '
}
pattern = re . compile ( ' | ' . join ( umlaut_map . keys ( ) ) )
umlaut_map = { " ä " : " ae " , " ö " : " oe " , " ü " : " ue " , " Ä " : " Ae " , " Ö " : " Oe " , " Ü " : " Ue " , " ß " : " ss " }
pattern = re . compile ( " | " . join ( umlaut_map . keys ( ) ) )
int_reference = pattern . sub ( lambda x : umlaut_map [ x . group ( ) ] , reference )
allowed_chars = r " [^a-z0-9 /?: ., ' +-] "
clean_reference = re . sub ( allowed_chars , ' ' , int_reference , flags = re . IGNORECASE )
clean_reference = re . sub ( allowed_chars , " " , int_reference , flags = re . IGNORECASE )
return clean_reference
def code ( self ) :
if self . amount == 0 :
return " "
@ -796,7 +930,7 @@ class Transaction(models.Model):
bic = iban . bic
reference = self . escape_reference ( self . reference )
# also escaping receiver as umlaute are also not allowed here
receiver = self . escape_reference ( f " { self . member . prename } { self . member . lastname } " )
return f """ BCD
@ -812,13 +946,14 @@ EUR{self.amount}
{ reference } """
class Meta :
verbose_name = _ ( ' Transaction ' )
verbose_name_plural = _ ( ' Transactions ' )
verbose_name = _ ( " Transaction " )
verbose_name_plural = _ ( " Transactions " )
class Receipt ( models . Model ) :
short_description = models . CharField ( verbose_name = _ ( ' Short description ' ) , max_length = 30 )
ledger = models . ForeignKey ( Ledger , blank = False , null = False , verbose_name = _ ( ' Ledger ' ) ,
on_delete = models . CASCADE )
short_description = models . CharField ( verbose_name = _ ( " Short description " ) , max_length = 30 )
ledger = models . ForeignKey (
Ledger , blank = False , null = False , verbose_name = _ ( " Ledger " ) , on_delete = models . CASCADE
)
amount = models . DecimalField ( max_digits = 6 , decimal_places = 2 )
comments = models . TextField ( )