2018-08-29 22:52:32 +00:00
import os
2018-08-29 23:35:37 +00:00
from django . contrib import messages
2018-09-01 08:25:45 +00:00
from django . core . exceptions import ValidationError
2020-02-12 12:10:41 +00:00
from django . db import models
2019-03-28 06:04:53 +00:00
from django . utils . text import slugify
2018-08-29 22:52:32 +00:00
2019-03-28 06:04:53 +00:00
from utils . models import CampRelatedModel , CreatedUpdatedModel , UUIDModel
2020-02-12 12:10:41 +00:00
from . email import (
send_accountingsystem_expense_email ,
send_accountingsystem_revenue_email ,
send_expense_approved_email ,
send_expense_rejected_email ,
send_revenue_approved_email ,
send_revenue_rejected_email ,
)
2018-08-29 22:52:32 +00:00
2019-06-16 12:32:24 +00:00
2019-03-30 05:54:45 +00:00
class ChainManager ( models . Manager ) :
"""
ChainManager adds ' expenses_total ' and ' revenues_total ' to the Chain qs
"""
2019-06-16 12:32:24 +00:00
2019-03-30 05:54:45 +00:00
def get_queryset ( self ) :
qs = super ( ) . get_queryset ( )
2019-06-16 12:32:24 +00:00
qs = qs . annotate ( expenses_total = models . Sum ( " credebtors__expenses__amount " ) )
qs = qs . annotate ( revenues_total = models . Sum ( " credebtors__revenues__amount " ) )
2019-03-30 05:54:45 +00:00
return qs
2019-03-28 06:04:53 +00:00
class Chain ( CreatedUpdatedModel , UUIDModel ) :
"""
A chain of Credebtors . Used to group when several Creditors / Debtors
belong to the same Chain / company , like XL Byg stores or Netto stores .
"""
2019-06-16 12:32:24 +00:00
2019-03-28 06:04:53 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " name " ]
2019-03-28 06:04:53 +00:00
2019-03-30 05:54:45 +00:00
objects = ChainManager ( )
2019-03-28 06:04:53 +00:00
name = models . CharField (
max_length = 100 ,
unique = True ,
2019-06-16 12:32:24 +00:00
help_text = ' A short name for this Chain, like " Netto " or " XL Byg " . 100 characters or fewer. ' ,
2019-03-28 06:04:53 +00:00
)
slug = models . SlugField (
unique = True ,
2019-03-28 06:16:02 +00:00
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The url slug for this Chain. Leave blank to auto generate a slug. " ,
2019-03-28 06:04:53 +00:00
)
notes = models . TextField (
2019-06-16 12:32:24 +00:00
help_text = " Any notes for this Chain. Will be shown to anyone creating Expenses or Revenues for this Chain. " ,
2019-03-28 06:04:53 +00:00
blank = True ,
)
def __str__ ( self ) :
return self . name
def save ( self , * * kwargs ) :
if not self . slug :
self . slug = slugify ( self . name )
super ( Chain , self ) . save ( * * kwargs )
2019-03-30 05:54:45 +00:00
@property
def expenses ( self ) :
return Expense . objects . filter ( creditor__chain__pk = self . pk )
@property
def revenues ( self ) :
return Revenue . objects . filter ( debtor__chain__pk = self . pk )
class CredebtorManager ( models . Manager ) :
"""
CredebtorManager adds ' expenses_total ' and ' revenues_total ' to the Credebtor qs
"""
2019-06-16 12:32:24 +00:00
2019-03-30 05:54:45 +00:00
def get_queryset ( self ) :
qs = super ( ) . get_queryset ( )
2019-06-16 12:32:24 +00:00
qs = qs . annotate ( expenses_total = models . Sum ( " expenses__amount " ) )
qs = qs . annotate ( revenues_total = models . Sum ( " revenues__amount " ) )
2019-03-30 05:54:45 +00:00
return qs
2019-03-28 06:04:53 +00:00
class Credebtor ( CreatedUpdatedModel , UUIDModel ) :
"""
The Credebtor model represents the specific " instance " of a Chain ,
like " XL Byg Rønne " or " Netto Gelsted " .
The model is used for both creditors and debtors , since there is a
lot of overlap between them .
"""
2019-06-16 12:32:24 +00:00
2019-03-28 06:04:53 +00:00
class Meta :
2019-06-16 12:32:24 +00:00
ordering = [ " name " ]
unique_together = ( " chain " , " slug " )
2019-03-28 06:04:53 +00:00
2019-03-30 05:54:45 +00:00
objects = CredebtorManager ( )
2019-03-28 06:04:53 +00:00
chain = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" economy.Chain " ,
2019-03-28 06:04:53 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " credebtors " ,
help_text = " The Chain to which this Credebtor belongs. " ,
2019-03-28 06:04:53 +00:00
)
name = models . CharField (
max_length = 100 ,
unique = True ,
2019-06-16 12:32:24 +00:00
help_text = ' The name of this Credebtor, like " XL Byg Rønne " or " Netto Gelsted " . 100 characters or fewer. ' ,
2019-03-28 06:04:53 +00:00
)
slug = models . SlugField (
2019-03-28 06:16:02 +00:00
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The url slug for this Credebtor. Leave blank to auto generate a slug. " ,
2019-03-28 06:04:53 +00:00
)
2019-06-16 12:32:24 +00:00
address = models . TextField ( help_text = " The address of this Credebtor. " )
2019-03-28 06:04:53 +00:00
notes = models . TextField (
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " Any notes for this Credebtor. Shown when creating an Expense or Revenue for this Credebtor. " ,
2019-03-28 06:04:53 +00:00
)
def __str__ ( self ) :
return self . name
def save ( self , * * kwargs ) :
"""
Generate slug as needed
"""
if not self . slug :
self . slug = slugify ( self . name )
super ( Credebtor , self ) . save ( * * kwargs )
2018-11-20 16:12:32 +00:00
class Revenue ( CampRelatedModel , UUIDModel ) :
"""
The Revenue model represents any type of income for BornHack .
2019-03-28 06:04:53 +00:00
2020-02-12 12:10:41 +00:00
Most Revenue objects will have a FK to the Invoice model ,
2019-03-28 06:04:53 +00:00
but only if the revenue relates directly to an Invoice in our system .
Other Revenue objects ( such as money returned from bottle deposits ) will
not have a related BornHack Invoice object .
2018-11-20 16:12:32 +00:00
"""
2019-06-16 12:32:24 +00:00
2018-11-20 16:12:32 +00:00
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
2018-11-20 16:12:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " revenues " ,
help_text = " The camp to which this revenue belongs " ,
2018-11-20 16:12:32 +00:00
)
2019-03-28 06:04:53 +00:00
debtor = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" economy.Credebtor " ,
2019-03-28 06:04:53 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " revenues " ,
help_text = " The Debtor to which this revenue belongs " ,
2019-03-28 06:04:53 +00:00
)
2018-11-20 16:12:32 +00:00
user = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" auth.User " ,
2018-11-20 16:12:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " revenues " ,
help_text = " The user who submitted this revenue " ,
2018-11-20 16:12:32 +00:00
)
amount = models . DecimalField (
decimal_places = 2 ,
max_digits = 12 ,
2019-06-16 12:32:24 +00:00
help_text = " The amount of this revenue in DKK. Must match the amount on the documentation uploaded below. " ,
2018-11-20 16:12:32 +00:00
)
description = models . CharField (
max_length = 200 ,
2019-06-16 12:32:24 +00:00
help_text = " A short description of this revenue. Please keep it meningful as it helps the Economy team a lot when categorising revenue. 200 characters or fewer. " ,
2018-11-20 16:12:32 +00:00
)
invoice = models . ImageField (
2019-06-16 12:32:24 +00:00
help_text = " The invoice file for this revenue. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted, as well as PDF. " ,
upload_to = " revenues/ " ,
2018-11-20 16:12:32 +00:00
)
2019-01-20 15:42:50 +00:00
invoice_date = models . DateField (
2019-06-16 12:32:24 +00:00
help_text = " The invoice date for this Revenue. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD. "
2019-01-20 14:33:05 +00:00
)
2018-11-20 16:12:32 +00:00
invoice_fk = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" shop.Invoice " ,
2018-11-20 16:12:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " revenues " ,
help_text = " The Invoice object to which this Revenue object relates. Can be None if this revenue does not have a related BornHack Invoice. " ,
2018-11-20 16:12:32 +00:00
blank = True ,
null = True ,
)
responsible_team = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" teams.Team " ,
2018-11-20 16:12:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " revenues " ,
help_text = " The team to which this revenue belongs. When in doubt pick the Economy team. " ,
2018-11-20 16:12:32 +00:00
)
approved = models . NullBooleanField (
default = None ,
2019-06-16 12:32:24 +00:00
help_text = " True if this Revenue has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet. " ,
2018-11-20 16:12:32 +00:00
)
notes = models . TextField (
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " Economy Team notes for this revenue. Only visible to the Economy team and the submitting user. " ,
2018-11-20 16:12:32 +00:00
)
def clean ( self ) :
if self . amount < 0 :
2019-06-16 12:32:24 +00:00
raise ValidationError ( " Amount of a Revenue object can not be negative " )
2018-11-20 16:12:32 +00:00
@property
def invoice_filename ( self ) :
return os . path . basename ( self . invoice . file . name )
@property
def approval_status ( self ) :
2020-02-12 12:10:41 +00:00
if self . approved is None :
2018-11-20 16:12:32 +00:00
return " Pending approval "
2020-02-12 12:10:41 +00:00
elif self . approved :
2018-11-20 16:12:32 +00:00
return " Approved "
else :
return " Rejected "
def approve ( self , request ) :
"""
This method marks a revenue as approved .
Approving a revenue triggers an email to the economy system , and another email to the user who submitted the revenue
"""
if request . user == self . user :
2019-06-16 12:32:24 +00:00
messages . error (
request ,
" You cannot approve your own revenues, aka. the anti-stein-bagger defense " ,
)
2018-11-20 16:12:32 +00:00
return
# mark as approved and save
self . approved = True
self . save ( )
# send email to economic for this revenue
send_accountingsystem_revenue_email ( revenue = self )
# send email to the user
send_revenue_approved_email ( revenue = self )
# message to the browser
messages . success ( request , " Revenue %s approved " % self . pk )
def reject ( self , request ) :
"""
This method marks a revenue as not approved .
Not approving a revenue triggers an email to the user who submitted the revenue in the first place .
"""
# mark as not approved and save
self . approved = False
self . save ( )
# send email to the user
send_revenue_rejected_email ( revenue = self )
# message to the browser
messages . success ( request , " Revenue %s rejected " % self . pk )
2018-08-29 22:52:32 +00:00
class Expense ( CampRelatedModel , UUIDModel ) :
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " expenses " ,
help_text = " The camp to which this expense belongs " ,
2018-08-29 22:52:32 +00:00
)
2019-03-28 06:04:53 +00:00
creditor = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" economy.Credebtor " ,
2019-03-28 06:04:53 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " expenses " ,
help_text = " The Creditor to which this expense belongs " ,
2019-03-28 06:04:53 +00:00
)
2018-08-29 22:52:32 +00:00
user = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" auth.User " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " expenses " ,
help_text = " The user to which this expense belongs " ,
2018-08-29 22:52:32 +00:00
)
amount = models . DecimalField (
decimal_places = 2 ,
max_digits = 12 ,
2019-06-16 12:32:24 +00:00
help_text = " The amount of this expense in DKK. Must match the amount on the invoice uploaded below. " ,
2018-08-29 22:52:32 +00:00
)
description = models . CharField (
max_length = 200 ,
2019-06-16 12:32:24 +00:00
help_text = " A short description of this expense. Please keep it meningful as it helps the Economy team a lot when categorising expenses. 200 characters or fewer. " ,
2018-08-29 22:52:32 +00:00
)
paid_by_bornhack = models . BooleanField (
default = True ,
help_text = " Leave checked if this expense was paid by BornHack. Uncheck if you need a reimbursement for this expense. " ,
)
invoice = models . ImageField (
2019-06-16 12:32:24 +00:00
help_text = " The invoice for this expense. Please make sure the amount on the invoice matches the amount you entered above. All common image formats are accepted. " ,
upload_to = " expenses/ " ,
2018-08-29 22:52:32 +00:00
)
2019-01-20 15:42:50 +00:00
invoice_date = models . DateField (
2019-06-16 12:32:24 +00:00
help_text = " The invoice date for this Expense. This must match the invoice date on the documentation uploaded below. Format is YYYY-MM-DD. "
2019-01-20 14:33:05 +00:00
)
2018-08-29 22:52:32 +00:00
responsible_team = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" teams.Team " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " expenses " ,
help_text = " The team to which this Expense belongs. A team responsible will need to approve the expense. When in doubt pick the Economy team. " ,
2018-08-29 22:52:32 +00:00
)
approved = models . NullBooleanField (
default = None ,
2019-06-16 12:32:24 +00:00
help_text = " True if this expense has been approved by the responsible team. False if it has been rejected. Blank if noone has decided yet. " ,
2018-08-29 22:52:32 +00:00
)
reimbursement = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" economy.Reimbursement " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " expenses " ,
2018-08-29 22:52:32 +00:00
null = True ,
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " The reimbursement for this expense, if any. This is a dual-purpose field. If expense.paid_by_bornhack is true then expense.reimbursement references the reimbursement which this expense is created to cover. If expense.paid_by_bornhack is false then expense.reimbursement references the reimbursement which reimbursed this expense. " ,
2018-08-29 22:52:32 +00:00
)
notes = models . TextField (
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " Economy Team notes for this expense. Only visible to the Economy team and the submitting user. " ,
2018-08-29 22:52:32 +00:00
)
2018-09-01 08:25:45 +00:00
def clean ( self ) :
if self . amount < 0 :
2019-06-16 12:32:24 +00:00
raise ValidationError ( " Amount of an expense can not be negative " )
2018-09-01 08:25:45 +00:00
2018-08-29 22:52:32 +00:00
@property
def invoice_filename ( self ) :
return os . path . basename ( self . invoice . file . name )
@property
def approval_status ( self ) :
2020-02-12 12:10:41 +00:00
if self . approved is None :
2018-08-29 22:52:32 +00:00
return " Pending approval "
2020-02-12 12:10:41 +00:00
elif self . approved :
2018-08-29 22:52:32 +00:00
return " Approved "
else :
return " Rejected "
2018-08-29 23:35:37 +00:00
def approve ( self , request ) :
2018-08-29 22:52:32 +00:00
"""
This method marks an expense as approved .
Approving an expense triggers an email to the economy system , and another email to the user who submitted the expense in the first place .
"""
2018-08-29 23:35:37 +00:00
if request . user == self . user :
2019-06-16 12:32:24 +00:00
messages . error (
request ,
" You cannot approve your own expenses, aka. the anti-stein-bagger defense " ,
)
2018-08-29 23:35:37 +00:00
return
2018-08-30 15:54:31 +00:00
# mark as approved and save
2018-08-29 22:52:32 +00:00
self . approved = True
self . save ( )
2018-08-30 15:54:31 +00:00
# send email to economic for this expense
2018-11-20 16:12:32 +00:00
send_accountingsystem_expense_email ( expense = self )
2018-08-29 22:52:32 +00:00
2018-08-30 15:54:31 +00:00
# send email to the user
send_expense_approved_email ( expense = self )
# message to the browser
2018-08-29 23:35:37 +00:00
messages . success ( request , " Expense %s approved " % self . pk )
def reject ( self , request ) :
2018-08-29 22:52:32 +00:00
"""
This method marks an expense as not approved .
Not approving an expense triggers an email to the user who submitted the expense in the first place .
"""
2018-08-30 15:54:31 +00:00
# mark as not approved and save
2018-08-29 22:52:32 +00:00
self . approved = False
self . save ( )
2018-08-30 15:54:31 +00:00
# send email to the user
send_expense_rejected_email ( expense = self )
2018-08-29 22:52:32 +00:00
2018-08-30 15:54:31 +00:00
# message to the browser
2018-08-29 23:35:37 +00:00
messages . success ( request , " Expense %s rejected " % self . pk )
2018-08-29 22:52:32 +00:00
2018-08-30 15:54:31 +00:00
2018-08-29 22:52:32 +00:00
class Reimbursement ( CampRelatedModel , UUIDModel ) :
"""
2020-02-12 12:10:41 +00:00
A reimbursement covers one or more expenses .
2018-08-29 22:52:32 +00:00
"""
2019-06-16 12:32:24 +00:00
2018-08-29 22:52:32 +00:00
camp = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" camps.Camp " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " reimbursements " ,
help_text = " The camp to which this reimbursement belongs " ,
2018-08-29 22:52:32 +00:00
)
user = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" auth.User " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " created_reimbursements " ,
help_text = " The economy team member who created this reimbursement. " ,
2018-08-29 22:52:32 +00:00
)
reimbursement_user = models . ForeignKey (
2019-06-16 12:32:24 +00:00
" auth.User " ,
2018-08-29 22:52:32 +00:00
on_delete = models . PROTECT ,
2019-06-16 12:32:24 +00:00
related_name = " reimbursements " ,
help_text = " The user this reimbursement belongs to. " ,
2018-08-29 22:52:32 +00:00
)
notes = models . TextField (
blank = True ,
2019-06-16 12:32:24 +00:00
help_text = " Economy Team notes for this reimbursement. Only visible to the Economy team and the related user. " ,
2018-08-29 22:52:32 +00:00
)
paid = models . BooleanField (
default = False ,
help_text = " Check when this reimbursement has been paid to the user " ,
)
2018-11-20 16:12:32 +00:00
@property
def covered_expenses ( self ) :
"""
Returns a queryset of all expenses covered by this reimbursement . Does not include the expense that paid for the reimbursement .
"""
return self . expenses . filter ( paid_by_bornhack = False )
2018-08-29 22:52:32 +00:00
@property
def amount ( self ) :
"""
The total amount for a reimbursement is calculated by adding up the amounts for all the related expenses
"""
amount = 0
2018-08-30 10:39:43 +00:00
for expense in self . expenses . filter ( paid_by_bornhack = False ) :
2018-08-29 22:52:32 +00:00
amount + = expense . amount
return amount