2018-06-26 20:14:16 +10:00
# Society Self-Service
2020-01-05 17:54:09 +11:00
# Copyright © 2018–2020 Yingtong Li (RunasSudo)
2018-06-26 20:14:16 +10:00
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU Affero General Public License as 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 Affero General Public License for more details.
#
# You should have received a copy of the GNU Affero General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
from django . contrib . auth . decorators import login_required
from django . contrib . auth . models import User
2020-02-12 09:53:36 +11:00
from django . core . exceptions import PermissionDenied , ValidationError
2018-06-26 20:14:16 +10:00
from django . core . validators import validate_email
2020-01-05 17:54:09 +11:00
from django . conf import settings
2018-06-26 20:14:16 +10:00
from django . db import transaction
2020-05-13 10:52:31 +10:00
from django . db . models import Q
2020-06-16 12:07:52 +10:00
from django . http import HttpResponse , JsonResponse
2018-06-26 20:14:16 +10:00
from django . shortcuts import render , redirect
from django . urls import reverse
from django . utils import timezone
from django . views import generic
2020-01-05 17:54:09 +11:00
from . import aba
2018-06-26 20:14:16 +10:00
from . import models
2020-02-12 01:03:02 +11:00
from . import xero
2019-06-19 19:46:24 +10:00
from ssmain . email import Emailer
2018-06-26 20:14:16 +10:00
2020-06-16 12:07:52 +10:00
import csv
2020-02-12 09:53:36 +11:00
from datetime import datetime
2019-06-20 01:06:24 +10:00
import functools
2020-01-05 17:54:09 +11:00
import io
2018-06-26 20:14:16 +10:00
import itertools
import json
2020-02-14 15:14:51 +11:00
import zipfile
2018-06-26 20:14:16 +10:00
2019-12-29 00:30:30 +11:00
# HELPER DECORATORS
def uses_budget ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , id ) :
budget = models . Budget . objects . get ( id = id )
revision = budget . budgetrevision_set . reverse ( ) [ 0 ]
return viewfunc ( request , budget , revision )
return func
def budget_viewable ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , budget , revision ) :
if not revision . can_view ( request . user ) :
raise PermissionDenied
return viewfunc ( request , budget , revision )
return func
def budget_editable ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , budget , revision ) :
if not revision . can_edit ( request . user ) :
raise PermissionDenied
return viewfunc ( request , budget , revision )
return func
def uses_claim ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , id ) :
claim = models . ReimbursementClaim . objects . get ( id = id )
return viewfunc ( request , claim )
return func
def claim_viewable ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , claim ) :
if not claim . can_view ( request . user ) :
raise PermissionDenied
return viewfunc ( request , claim )
return func
def claim_editable ( viewfunc ) :
@functools.wraps ( viewfunc )
def func ( request , claim ) :
if not claim . can_edit ( request . user ) :
raise PermissionDenied
return viewfunc ( request , claim )
return func
# HELPER FUNCTIONS
2020-02-12 09:53:36 +11:00
class FormValidationError ( Exception ) :
def __init__ ( self , data , errors ) :
super ( ) . __init__ ( self )
self . data = data
self . errors = errors
2019-12-29 00:30:30 +11:00
def revision_from_form ( budget , revision , form ) :
2020-02-12 09:53:36 +11:00
errors = [ ]
2019-12-29 00:30:30 +11:00
revision . budget = budget
2020-02-12 09:53:36 +11:00
if form [ ' name ' ] :
if len ( form [ ' name ' ] ) > 100 :
errors . append ( ' Title must be at most 100 characters ' )
revision . name = form [ ' name ' ]
else :
errors . append ( ' A title must be specified ' )
2019-12-29 00:30:30 +11:00
2020-02-12 09:53:36 +11:00
if form [ ' date ' ] :
try :
2020-02-23 20:20:35 +11:00
form_date = timezone . make_aware ( datetime . strptime ( form [ ' date ' ] , ' % Y- % m- %d ' ) )
revision . date = form_date
2020-02-12 09:53:36 +11:00
except ValueError :
errors . append ( ' Due date is not a valid date ' )
2020-02-23 20:20:35 +11:00
revision . date = None
2020-02-12 09:53:36 +11:00
else :
errors . append ( ' A due date must be specified ' )
if form [ ' event_dt ' ] :
try :
2020-02-23 20:20:35 +11:00
form_event_dt = timezone . make_aware ( datetime . strptime ( form [ ' event_dt ' ] , ' % Y- % m- %d % H: % M ' ) )
revision . event_dt = form_event_dt
2020-02-12 09:53:36 +11:00
except ValueError :
errors . append ( ' Event date/time is not a valid date-time ' )
2020-02-23 20:20:35 +11:00
revision . event_dt = None
2020-02-12 09:53:36 +11:00
else :
revision . event_dt = None
if form [ ' event_attendees ' ] :
if len ( form [ ' event_attendees ' ] ) > 20 :
errors . append ( ' Event attendees must be at most 20 characters ' )
revision . event_attendees = form [ ' event_attendees ' ]
else :
revision . event_attendees = None
if form [ ' contributors ' ] :
contributors = form [ ' contributors ' ] . split ( ' \n ' )
try :
for contributor in contributors :
validate_email ( contributor . strip ( ) )
except ValidationError :
errors . append ( ' Contributors contains invalid data – type only valid email addresses, one per line ' )
else :
contributors = [ ]
2020-01-17 21:12:07 +11:00
2019-12-29 00:30:30 +11:00
revision . comments = form [ ' comments ' ]
revision . revenue = json . loads ( form [ ' revenue ' ] )
revision . revenue_comments = form [ ' revenue_comments ' ]
revision . expense = json . loads ( form [ ' expense ' ] )
revision . expense_comments = form [ ' expense_comments ' ]
revision . expense_no_emergency_fund = True if form . get ( ' expense_no_emergency_fund ' , False ) else False
2020-02-12 09:53:36 +11:00
if errors :
raise FormValidationError ( revision , errors )
2019-12-29 00:30:30 +11:00
revision . save ( )
for contributor in contributors :
try :
user = User . objects . get ( email = contributor . strip ( ) )
except User . DoesNotExist :
user = User . objects . create_user ( contributor . strip ( ) . split ( ' @ ' ) [ 0 ] , contributor . strip ( ) )
revision . contributors . add ( user )
revision . save ( )
return revision
2020-01-04 16:50:31 +11:00
def claim_from_form ( claim , form , files ) :
2019-12-29 00:30:30 +11:00
claim . purpose = form [ ' purpose ' ]
claim . date = form [ ' date ' ] if form [ ' date ' ] else None
claim . budget_id = form [ ' budget_id ' ]
claim . comments = form [ ' comments ' ]
claim . items = json . loads ( form [ ' items ' ] )
2020-01-04 16:50:31 +11:00
claim . payee_name = form [ ' payee_name ' ]
claim . payee_bsb = form [ ' payee_bsb ' ]
claim . payee_account = form [ ' payee_account ' ]
2019-12-29 00:30:30 +11:00
claim . save ( )
2020-01-04 16:50:31 +11:00
if files :
for f in files . getlist ( ' upload_file ' ) :
claim_receipt = models . ClaimReceipt ( )
claim_receipt . claim = claim
claim_receipt . uploaded_file = f
claim_receipt . save ( )
2019-12-29 00:30:30 +11:00
return claim
# INDEX VIEW
2018-06-26 20:14:16 +10:00
@login_required
def index ( request ) :
return render ( request , ' sstreasury/index.html ' )
2019-12-29 00:30:30 +11:00
# BUDGET VIEWS
2018-06-26 20:14:16 +10:00
@login_required
def budget_list ( request ) :
budgets_action = [ ]
budgets_open = [ ]
budgets_closed = [ ]
for budget in models . Budget . objects . all ( ) :
revision = budget . budgetrevision_set . reverse ( ) [ 0 ]
state = models . BudgetState ( revision . state )
2019-06-20 01:06:24 +10:00
2019-06-20 01:39:29 +10:00
group = None
if request . user . groups . filter ( name = ' Treasury ' ) . exists ( ) and state == models . BudgetState . AWAIT_REVIEW :
group = budgets_action
elif request . user . groups . filter ( name = ' Secretary ' ) . exists ( ) and state == models . BudgetState . ENDORSED :
group = budgets_action
2019-12-28 19:11:22 +11:00
elif request . user . groups . filter ( name = ' Committee ' ) . exists ( ) and state == models . BudgetState . ENDORSED :
group = budgets_action
2020-04-19 21:06:28 +10:00
elif request . user in revision . contributors . all ( ) :
2019-12-28 19:11:22 +11:00
if state in [ models . BudgetState . DRAFT , models . BudgetState . RESUBMIT ] :
group = budgets_action
elif state in [ models . BudgetState . AWAIT_REVIEW , models . BudgetState . ENDORSED ] :
group = budgets_open
else :
group = budgets_closed
2020-02-16 21:15:05 +11:00
elif request . user . groups . filter ( name = ' Treasury ' ) . exists ( ) or request . user . groups . filter ( name = ' Secretary ' ) . exists ( ) or request . user . groups . filter ( name = ' Committee ' ) . exists ( ) :
2019-12-28 19:11:22 +11:00
if state == models . BudgetState . APPROVED :
group = budgets_closed
2019-06-20 01:06:24 +10:00
else :
2019-12-28 19:11:22 +11:00
group = budgets_open
2019-06-20 01:39:29 +10:00
if group is not None :
group . append ( revision )
2018-06-26 20:14:16 +10:00
return render ( request , ' sstreasury/budget_list.html ' , {
' budgets_action ' : budgets_action ,
' budgets_open ' : budgets_open ,
' budgets_closed ' : budgets_closed
} )
@login_required
2019-12-29 00:30:30 +11:00
@uses_budget
def budget_view ( request , budget , revision ) :
2018-06-26 20:14:16 +10:00
if ' revision ' in request . GET :
revision = budget . budgetrevision_set . get ( id = int ( request . GET [ ' revision ' ] ) )
2019-12-28 19:11:22 +11:00
if not revision . can_view ( request . user ) :
raise PermissionDenied
2019-06-20 15:29:57 +10:00
2018-06-26 20:14:16 +10:00
history = list ( itertools . chain ( budget . budgetrevision_set . all ( ) , revision . budget . budgetcomment_set . all ( ) ) )
history . sort ( key = lambda x : x . time , reverse = True )
2020-05-13 10:52:31 +10:00
if revision . state == models . BudgetState . APPROVED . value and ' revision ' not in request . GET :
claims = models . ReimbursementClaim . objects . filter ( Q ( budget_id = str ( budget . id ) ) | Q ( budget_id__endswith = ' - {} ' . format ( budget . id ) ) ) . all ( )
else :
claims = None
2018-06-26 20:14:16 +10:00
return render ( request , ' sstreasury/budget_view.html ' , {
' revision ' : revision ,
' history ' : history ,
2020-05-13 10:52:31 +10:00
' is_latest ' : ' revision ' not in request . GET ,
' claims ' : claims
2018-06-26 20:14:16 +10:00
} )
2019-06-20 15:29:57 +10:00
@login_required
2019-12-29 00:30:30 +11:00
@uses_budget
def budget_print ( request , budget , revision ) :
2019-06-20 15:29:57 +10:00
if ' revision ' in request . GET :
revision = budget . budgetrevision_set . get ( id = int ( request . GET [ ' revision ' ] ) )
2019-12-28 19:11:22 +11:00
if not revision . can_view ( request . user ) :
raise PermissionDenied
2019-06-20 15:29:57 +10:00
return render ( request , ' sstreasury/budget_print.html ' , {
' revision ' : revision ,
' is_latest ' : ' revision ' not in request . GET
} )
2018-06-26 20:14:16 +10:00
@login_required
def budget_new ( request ) :
if request . method == ' POST ' :
2020-02-12 09:53:36 +11:00
try :
with transaction . atomic ( ) :
budget = models . Budget ( )
budget . save ( )
revision = models . BudgetRevision ( )
revision . author = request . user
revision . time = timezone . now ( )
revision . action = models . BudgetAction . CREATE . value
revision . state = models . BudgetState . DRAFT . value
revision = revision_from_form ( budget , revision , request . POST )
except FormValidationError as form_error :
return render ( request , ' sstreasury/budget_edit.html ' , {
' revision ' : form_error . data ,
' contributors ' : request . POST [ ' contributors ' ] ,
' errors ' : form_error . errors
} )
2018-06-26 20:14:16 +10:00
2018-12-07 15:53:46 +11:00
if request . POST [ ' submit ' ] == ' Save ' :
2018-12-07 10:51:43 +11:00
return redirect ( reverse ( ' budget_view ' , kwargs = { ' id ' : budget . id } ) )
else :
return redirect ( reverse ( ' budget_edit ' , kwargs = { ' id ' : budget . id } ) )
2018-06-26 20:14:16 +10:00
else :
budget = models . Budget ( )
revision = models . BudgetRevision ( )
revision . budget = budget
return render ( request , ' sstreasury/budget_edit.html ' , {
' revision ' : revision ,
2020-02-12 09:53:36 +11:00
' contributors ' : request . user . email ,
' errors ' : [ ]
2018-06-26 20:14:16 +10:00
} )
2019-06-20 01:06:24 +10:00
@login_required
@uses_budget
@budget_editable
def budget_edit ( request , budget , revision ) :
if request . method == ' POST ' :
2019-06-19 18:26:34 +10:00
if request . POST [ ' submit ' ] == ' Delete ' :
budget . delete ( )
return redirect ( reverse ( ' budget_list ' ) )
2020-02-12 09:53:36 +11:00
try :
with transaction . atomic ( ) :
new_revision = models . BudgetRevision ( )
new_revision . author = request . user
new_revision . time = timezone . now ( )
new_revision . action = models . BudgetAction . EDIT . value
new_revision . state = revision . state
new_revision = revision_from_form ( budget , new_revision , request . POST )
except FormValidationError as form_error :
return render ( request , ' sstreasury/budget_edit.html ' , {
' revision ' : form_error . data ,
' contributors ' : request . POST [ ' contributors ' ] ,
' errors ' : form_error . errors
} )
2018-06-26 20:14:16 +10:00
2018-12-07 15:53:46 +11:00
if request . POST [ ' submit ' ] == ' Save ' :
2018-12-07 10:51:43 +11:00
return redirect ( reverse ( ' budget_view ' , kwargs = { ' id ' : budget . id } ) )
else :
return redirect ( reverse ( ' budget_edit ' , kwargs = { ' id ' : budget . id } ) )
2018-06-26 20:14:16 +10:00
else :
return render ( request , ' sstreasury/budget_edit.html ' , {
' revision ' : revision ,
2020-02-12 09:53:36 +11:00
' contributors ' : ' \n ' . join ( revision . contributors . all ( ) . values_list ( ' email ' , flat = True ) ) ,
' errors ' : [ ]
2018-06-26 20:14:16 +10:00
} )
@login_required
2019-06-20 01:06:24 +10:00
@uses_budget
2020-05-06 12:00:37 +10:00
@budget_viewable
2019-06-20 01:06:24 +10:00
def budget_action ( request , budget , revision ) :
actions = request . POST [ ' action ' ] . split ( ' , ' )
2018-06-26 20:14:16 +10:00
2019-06-20 01:06:24 +10:00
if ' Comment ' in actions and request . POST . get ( ' comment ' , None ) :
2019-06-19 19:46:24 +10:00
with transaction . atomic ( ) :
comment = models . BudgetComment ( )
comment . budget = budget
comment . author = request . user
comment . time = timezone . now ( )
comment . content = request . POST [ ' comment ' ]
comment . save ( )
emailer = Emailer ( )
for user in User . objects . filter ( groups__name = ' Treasury ' ) :
if user != request . user :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' New comment on budget: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_commented.md ' , { ' revision ' : revision , ' comment ' : comment } )
2019-06-19 19:46:24 +10:00
for user in revision . contributors . all ( ) :
if user != request . user :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' New comment on budget: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_commented.md ' , { ' revision ' : revision , ' comment ' : comment } )
2019-06-19 19:46:24 +10:00
2019-06-20 01:06:24 +10:00
if ' Submit ' in actions :
2019-12-29 00:30:30 +11:00
if not revision . can_submit ( request . user ) :
2019-06-19 19:46:24 +10:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
revision . update_state ( request . user , models . BudgetState . AWAIT_REVIEW )
2019-06-19 19:46:24 +10:00
emailer = Emailer ( )
for user in User . objects . filter ( groups__name = ' Treasury ' ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Action required: Budget submitted: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_submitted_treasurer.md ' , { ' revision ' : revision } )
2019-06-19 19:46:24 +10:00
for user in revision . contributors . all ( ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Budget submitted: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_submitted_drafter.md ' , { ' revision ' : revision } )
2019-06-19 19:46:24 +10:00
2019-06-20 01:06:24 +10:00
if ' Withdraw ' in actions :
2020-01-04 17:14:47 +11:00
if not revision . can_withdraw ( request . user ) :
2019-06-19 19:46:24 +10:00
raise PermissionDenied
2019-12-29 00:30:30 +11:00
with transaction . atomic ( ) :
revision . update_state ( request . user , models . BudgetState . DRAFT )
2018-06-26 20:14:16 +10:00
2019-06-20 01:06:24 +10:00
if ' Endorse ' in actions :
2020-01-04 17:14:47 +11:00
if not revision . can_endorse ( request . user ) :
2019-06-20 01:06:24 +10:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
revision . update_state ( request . user , models . BudgetState . ENDORSED )
2019-06-20 01:06:24 +10:00
emailer = Emailer ( )
2019-06-20 01:39:29 +10:00
for user in User . objects . filter ( groups__name = ' Secretary ' ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Action required: Budget endorsed: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_endorsed_secretary.md ' , { ' revision ' : revision } )
2019-06-20 01:06:24 +10:00
for user in revision . contributors . all ( ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Budget endorsed, awaiting committee approval: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_endorsed_drafter.md ' , { ' revision ' : revision } )
2019-06-20 01:06:24 +10:00
if ' Return ' in actions :
2020-01-04 17:14:47 +11:00
if not revision . can_return ( request . user ) :
2019-06-20 01:06:24 +10:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
revision . update_state ( request . user , models . BudgetState . RESUBMIT )
2019-06-20 01:06:24 +10:00
emailer = Emailer ( )
for user in revision . contributors . all ( ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Action required: Budget returned for re-drafting: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_returned.md ' , { ' revision ' : revision } )
2019-06-20 01:06:24 +10:00
2019-06-20 01:39:29 +10:00
if ' Approve ' in actions :
2020-01-04 17:14:47 +11:00
if not revision . can_approve ( request . user ) :
2019-12-29 00:30:30 +11:00
return PermissionDenied
2019-06-20 01:39:29 +10:00
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
revision . update_state ( request . user , models . BudgetState . APPROVED )
2019-06-20 01:39:29 +10:00
emailer = Emailer ( )
for user in revision . contributors . all ( ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Budget approved: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_approved.md ' , { ' revision ' : revision } )
2019-06-20 01:39:29 +10:00
if ' CmteReturn ' in actions :
2020-01-04 17:14:47 +11:00
if not revision . can_cmtereturn ( request . user ) :
2019-12-29 00:30:30 +11:00
return PermissionDenied
2019-06-20 01:39:29 +10:00
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
revision . update_state ( request . user , models . BudgetState . RESUBMIT )
2019-06-20 01:39:29 +10:00
emailer = Emailer ( )
for user in revision . contributors . all ( ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Action required: Budget returned for re-drafting: {} (BU- {} ) ' . format ( revision . name , budget . id ) , ' sstreasury/email/budget_returned_committee.md ' , { ' revision ' : revision } )
2019-06-20 01:39:29 +10:00
2018-06-26 20:14:16 +10:00
return redirect ( reverse ( ' budget_view ' , kwargs = { ' id ' : budget . id } ) )
2019-12-28 19:11:22 +11:00
@login_required
def claim_list ( request ) :
claims_action = [ ]
claims_open = [ ]
claims_closed = [ ]
for claim in models . ReimbursementClaim . objects . all ( ) :
state = models . ClaimState ( claim . state )
group = None
if request . user . groups . filter ( name = ' Treasury ' ) . exists ( ) and state in [ models . ClaimState . AWAIT_REVIEW , models . ClaimState . APPROVED ] :
group = claims_action
elif request . user == claim . author :
if state in [ models . ClaimState . DRAFT , models . ClaimState . RESUBMIT ] :
group = claims_action
elif state in [ models . ClaimState . AWAIT_REVIEW , models . ClaimState . APPROVED ] :
group = claims_open
else :
group = claims_closed
elif request . user . groups . filter ( name = ' Treasury ' ) . exists ( ) :
2020-02-12 01:11:51 +11:00
if state == models . ClaimState . PAID :
2019-12-28 19:11:22 +11:00
group = claims_closed
else :
group = claims_open
if group is not None :
group . append ( claim )
return render ( request , ' sstreasury/claim_list.html ' , {
' claims_action ' : claims_action ,
' claims_open ' : claims_open ,
' claims_closed ' : claims_closed
} )
@login_required
def claim_new ( request ) :
if request . method == ' POST ' :
with transaction . atomic ( ) :
claim = models . ReimbursementClaim ( )
claim . author = request . user
claim . time = timezone . now ( )
2019-12-28 23:23:56 +11:00
claim . state = models . BudgetState . DRAFT . value
2020-01-04 16:50:31 +11:00
claim = claim_from_form ( claim , request . POST , request . FILES )
2019-12-28 19:11:22 +11:00
claim_history = models . ClaimHistory ( )
claim_history . claim = claim
claim_history . author = request . user
claim_history . state = claim . state
claim_history . time = timezone . now ( )
claim_history . action = models . ClaimAction . CREATE . value
claim_history . save ( )
if request . POST [ ' submit ' ] == ' Save ' :
return redirect ( reverse ( ' claim_view ' , kwargs = { ' id ' : claim . id } ) )
else :
return redirect ( reverse ( ' claim_edit ' , kwargs = { ' id ' : claim . id } ) )
pass
else :
claim = models . ReimbursementClaim ( )
claim . author = request . user
return render ( request , ' sstreasury/claim_edit.html ' , {
' claim ' : claim
} )
2019-12-28 23:23:56 +11:00
@login_required
@uses_claim
@claim_viewable
def claim_view ( request , claim ) :
history = list ( itertools . chain ( claim . claimhistory_set . all ( ) , claim . claimcomment_set . all ( ) ) )
history . sort ( key = lambda x : x . time , reverse = True )
2019-12-29 00:54:44 +11:00
budget = None
if claim . budget_id :
try :
budget = models . Budget . objects . get ( id = claim . budget_id . split ( ' - ' ) [ - 1 ] )
except models . Budget . DoesNotExist :
budget = None
2020-06-16 12:14:49 +10:00
bsb_lookedup = do_bsb_lookup ( claim . payee_bsb )
2019-12-28 23:23:56 +11:00
return render ( request , ' sstreasury/claim_view.html ' , {
' claim ' : claim ,
2019-12-29 00:54:44 +11:00
' budget ' : budget ,
2020-06-16 12:14:49 +10:00
' history ' : history ,
' bsb_lookedup ' : bsb_lookedup
2019-12-28 23:23:56 +11:00
} )
@login_required
@uses_claim
@claim_viewable
def claim_print ( request , claim ) :
2019-12-29 00:54:44 +11:00
budget = None
if claim . budget_id :
try :
budget = models . Budget . objects . get ( id = claim . budget_id . split ( ' - ' ) [ - 1 ] )
except models . Budget . DoesNotExist :
budget = None
2020-06-16 12:14:49 +10:00
bsb_lookedup = do_bsb_lookup ( claim . payee_bsb )
2019-12-28 23:23:56 +11:00
return render ( request , ' sstreasury/claim_print.html ' , {
2019-12-29 00:54:44 +11:00
' claim ' : claim ,
2020-06-16 12:14:49 +10:00
' budget ' : budget ,
' bsb_lookedup ' : bsb_lookedup
2019-12-28 23:23:56 +11:00
} )
@login_required
@uses_claim
@claim_editable
def claim_edit ( request , claim ) :
if request . method == ' POST ' :
2020-01-04 16:50:31 +11:00
if request . POST [ ' submit ' ] . startswith ( ' DeleteFile ' ) :
file_id = int ( request . POST [ ' submit ' ] [ 10 : ] )
claim_receipt = models . ClaimReceipt . objects . get ( id = file_id )
if claim_receipt . claim != claim :
raise PermissionDenied
claim_receipt . delete ( )
claim_receipt . uploaded_file . delete ( save = False )
return redirect ( reverse ( ' claim_edit ' , kwargs = { ' id ' : claim . id } ) )
2019-12-28 23:23:56 +11:00
if request . POST [ ' submit ' ] == ' Delete ' :
claim . delete ( )
return redirect ( reverse ( ' claim_list ' ) )
with transaction . atomic ( ) :
2020-01-04 16:50:31 +11:00
claim = claim_from_form ( claim , request . POST , request . FILES )
2019-12-28 23:23:56 +11:00
claim_history = models . ClaimHistory ( )
claim_history . claim = claim
claim_history . author = request . user
claim_history . state = claim . state
claim_history . time = timezone . now ( )
claim_history . action = models . ClaimAction . EDIT . value
claim_history . save ( )
if request . POST [ ' submit ' ] == ' Save ' :
return redirect ( reverse ( ' claim_view ' , kwargs = { ' id ' : claim . id } ) )
else :
return redirect ( reverse ( ' claim_edit ' , kwargs = { ' id ' : claim . id } ) )
else :
2020-06-16 12:28:07 +10:00
bsb_lookedup = do_bsb_lookup ( claim . payee_bsb )
2019-12-28 23:23:56 +11:00
return render ( request , ' sstreasury/claim_edit.html ' , {
2020-06-16 12:28:07 +10:00
' claim ' : claim ,
' bsb_lookedup ' : bsb_lookedup
2019-12-28 23:23:56 +11:00
} )
@login_required
@uses_claim
@claim_editable
def claim_action ( request , claim ) :
actions = request . POST [ ' action ' ] . split ( ' , ' )
if ' Comment ' in actions and request . POST . get ( ' comment ' , None ) :
with transaction . atomic ( ) :
comment = models . ClaimComment ( )
comment . claim = claim
comment . author = request . user
comment . time = timezone . now ( )
comment . content = request . POST [ ' comment ' ]
comment . save ( )
emailer = Emailer ( )
for user in User . objects . filter ( groups__name = ' Treasury ' ) :
if user != request . user :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' New comment on reimbursement claim: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_commented.md ' , { ' claim ' : claim , ' comment ' : comment } )
2019-12-28 23:23:56 +11:00
if comment . author != request . user :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ comment . author ] , ' New comment on reimbursement claim: {} (RE- {} ) ' . format ( revision . name , claim . id ) , ' sstreasury/email/claim_commented.md ' , { ' claim ' : claim , ' comment ' : comment } )
2019-12-28 23:23:56 +11:00
if ' Submit ' in actions :
2019-12-29 00:30:30 +11:00
if not claim . can_submit ( request . user ) :
2019-12-28 23:23:56 +11:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
claim . update_state ( request . user , models . ClaimState . AWAIT_REVIEW )
2019-12-28 23:23:56 +11:00
emailer = Emailer ( )
for user in User . objects . filter ( groups__name = ' Treasury ' ) :
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ user . email ] , ' Action required: Reimbursement claim submitted: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_submitted_treasurer.md ' , { ' claim ' : claim } )
emailer . send_mail ( [ claim . author . email ] , ' Reimbursement claim submitted: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_submitted_drafter.md ' , { ' claim ' : claim } )
2019-12-28 23:23:56 +11:00
if ' Withdraw ' in actions :
2019-12-29 00:30:30 +11:00
if not claim . can_withdraw ( request . user ) :
2019-12-28 23:23:56 +11:00
raise PermissionDenied
2019-12-29 00:30:30 +11:00
with transaction . atomic ( ) :
claim . update_state ( request . user , models . ClaimState . DRAFT )
2019-12-28 23:23:56 +11:00
if ' Approve ' in actions :
2019-12-29 00:30:30 +11:00
if not claim . can_approve ( request . user ) :
2019-12-28 23:23:56 +11:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
claim . update_state ( request . user , models . ClaimState . APPROVED )
2019-12-28 23:23:56 +11:00
emailer = Emailer ( )
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ claim . author . email ] , ' Claim approved, awaiting payment: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_approved.md ' , { ' claim ' : claim } )
2019-12-28 23:23:56 +11:00
if ' Return ' in actions :
2019-12-29 00:30:30 +11:00
if not claim . can_approve ( request . user ) :
2019-12-28 23:23:56 +11:00
raise PermissionDenied
with transaction . atomic ( ) :
2019-12-29 00:30:30 +11:00
claim . update_state ( request . user , models . ClaimState . RESUBMIT )
2019-12-28 23:23:56 +11:00
emailer = Emailer ( )
2019-12-29 00:54:44 +11:00
emailer . send_mail ( [ claim . author . email ] , ' Action required: Reimbursement claim returned for re-drafting: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_returned.md ' , { ' claim ' : claim } )
2019-12-28 23:23:56 +11:00
return redirect ( reverse ( ' claim_view ' , kwargs = { ' id ' : claim . id } ) )
2020-01-05 17:54:09 +11:00
@login_required
def claim_processing ( request ) :
if not request . user . groups . filter ( name = ' Treasury ' ) . exists ( ) :
raise PermissionDenied
if request . method == ' POST ' :
if request . POST [ ' action ' ] == ' Export ' :
claims = models . ReimbursementClaim . objects . filter ( state = models . ClaimState . APPROVED . value ) . all ( )
claims = [ c for c in claims if request . POST . get ( ' claim_ {} ' . format ( c . id ) , False ) ]
2020-06-01 22:55:51 +10:00
claims . sort ( key = lambda c : ' {} / {} {} / {} ' . format ( c . payee_name . strip ( ) , c . payee_bsb . strip ( ) [ : 3 ] , c . payee_bsb . strip ( ) [ - 3 : ] , c . payee_account . strip ( ) ) )
2020-01-05 17:54:09 +11:00
aba_file = io . BytesIO ( )
aba . write_descriptive ( aba_file , bank_name = settings . ABA_BANK_NAME , user_name = settings . ABA_USER_NAME , bank_code = settings . ABA_BANK_CODE , description = ' Reimburse ' , date = timezone . now ( ) )
2020-06-01 22:55:51 +10:00
# CommBank requires only one entry per payee
num_records = 0
for _ , payee_claims in itertools . groupby ( claims , key = lambda c : ' {} / {} {} / {} ' . format ( c . payee_name . strip ( ) , c . payee_bsb . strip ( ) [ : 3 ] , c . payee_bsb . strip ( ) [ - 3 : ] , c . payee_account . strip ( ) ) ) :
payee_claims = list ( payee_claims )
aba . write_detail (
aba_file ,
dest_bsb = payee_claims [ 0 ] . payee_bsb ,
dest_account = payee_claims [ 0 ] . payee_account ,
cents = sum ( c . get_total ( ) for c in payee_claims ) * 100 ,
2020-06-15 20:37:43 +10:00
dest_name = payee_claims [ 0 ] . payee_name [ : 32 ] ,
2020-06-01 22:55:51 +10:00
reference = ' RE {} ' . format ( ' ' . join ( str ( c . id ) for c in payee_claims ) ) ,
src_bsb = settings . ABA_SRC_BSB ,
src_account = settings . ABA_SRC_ACC ,
src_name = settings . ABA_USER_NAME
)
num_records + = 1
2020-01-05 17:54:09 +11:00
2020-06-01 22:55:51 +10:00
aba . write_total ( aba_file , credit_cents = sum ( c . get_total ( ) for c in claims ) * 100 , num_detail_records = num_records )
2020-01-05 17:54:09 +11:00
aba_file . flush ( )
response = HttpResponse ( aba_file . getvalue ( ) , content_type = ' text/plain ' )
response [ ' Content-Disposition ' ] = ' attachment; filename= " claims.aba " '
return response
2020-02-12 01:03:02 +11:00
if request . POST [ ' action ' ] == ' ExportXero ' :
claims = models . ReimbursementClaim . objects . filter ( state = models . ClaimState . APPROVED . value ) . all ( )
claims = [ c for c in claims if request . POST . get ( ' claim_ {} ' . format ( c . id ) , False ) ]
2020-02-14 15:14:51 +11:00
# Export CSV
with io . StringIO ( ) as csv_file :
csv_writer = xero . new_writer ( csv_file )
for claim in claims :
xero . write_claim ( csv_writer , claim )
# Export resources to ZIP
with io . BytesIO ( ) as zip_file_bytes :
with zipfile . ZipFile ( zip_file_bytes , ' w ' ) as zip_file :
zip_file . writestr ( ' claims.csv ' , csv_file . getvalue ( ) )
for claim in claims :
for claim_receipt in claim . claimreceipt_set . all ( ) :
with claim_receipt . uploaded_file . open ( ) as f :
zip_file . writestr ( ' RE- {} / {} ' . format ( claim . id , claim_receipt . uploaded_file . name . split ( ' / ' ) [ - 1 ] ) , f . read ( ) )
response = HttpResponse ( zip_file_bytes . getvalue ( ) , content_type = ' application/zip ' )
response [ ' Content-Disposition ' ] = ' attachment; filename= " claims.zip " '
2020-02-12 01:03:02 +11:00
return response
2020-01-05 17:54:09 +11:00
if request . POST [ ' action ' ] == ' Pay ' :
claims = models . ReimbursementClaim . objects . filter ( state = models . ClaimState . APPROVED . value ) . all ( )
claims = [ c for c in claims if request . POST . get ( ' claim_ {} ' . format ( c . id ) , False ) ]
for claim in claims :
with transaction . atomic ( ) :
claim . update_state ( request . user , models . ClaimState . PAID )
emailer = Emailer ( )
emailer . send_mail ( [ claim . author . email ] , ' Claim paid: {} (RE- {} ) ' . format ( claim . purpose , claim . id ) , ' sstreasury/email/claim_paid.md ' , { ' claim ' : claim } )
claims = models . ReimbursementClaim . objects . filter ( state = models . ClaimState . APPROVED . value ) . all ( )
return render ( request , ' sstreasury/claim_processing.html ' , {
' claims ' : claims
} )
2020-06-16 12:07:52 +10:00
@login_required
def bsb_lookup ( request ) :
2020-06-16 12:14:49 +10:00
return JsonResponse ( { ' result ' : do_bsb_lookup ( request . GET . get ( ' bsb ' , ' ' ) ) } )
2020-06-16 12:07:52 +10:00
def do_bsb_lookup ( bsb ) :
2020-06-16 12:14:49 +10:00
bsb = ( bsb or ' ' ) . replace ( ' - ' , ' ' ) . replace ( ' ' , ' ' )
if len ( bsb ) != 6 :
return None
bsb = ' {} - {} ' . format ( bsb [ : 3 ] , bsb [ - 3 : ] )
2020-06-16 12:07:52 +10:00
with open ( settings . BSB_FILE_PATH , ' r ' , newline = ' ' ) as f :
reader = csv . reader ( f )
for line in reader :
if line [ 0 ] == bsb :
return ' {} - {} ' . format ( line [ 1 ] , line [ 2 ] )
return None