diff --git a/selfserv/settings.example.py b/selfserv/settings.example.py
index 13a8023..1aa4c52 100644
--- a/selfserv/settings.example.py
+++ b/selfserv/settings.example.py
@@ -32,6 +32,12 @@ PROMO_LOGO_URL = 'https://placehold.it/2000x500'
PROMO_LOGO_LINK = 'https://example.com'
PROMO_GROUPS_MANDATORY = ['All Years']
+ABA_USER_NAME = 'Society Name'
+ABA_BANK_NAME = 'CBA'
+ABA_BANK_CODE = 0
+ABA_SRC_BSB = '000-000'
+ABA_SRC_ACC = '00000000'
+
# Application definition
INSTALLED_APPS = [
diff --git a/sstreasury/aba.py b/sstreasury/aba.py
new file mode 100644
index 0000000..db7a2d2
--- /dev/null
+++ b/sstreasury/aba.py
@@ -0,0 +1,65 @@
+# Society Self-Service
+# Copyright © 2018–2020 Yingtong Li (RunasSudo)
+#
+# 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.brad-smith.info/blog/archives/405
+# https://www.cemtexaba.com/aba-format/cemtex-aba-file-format-details
+# https://ddkonline.blogspot.com/2009/01/aba-bank-payment-file-format-australian.html
+
+def write_descriptive(f, reel_seq=1, bank_name='', user_name='', bank_code=0, description='', date=None):
+ if reel_seq < 0 or reel_seq > 99 or len(bank_name) > 3 or len(user_name) > 26 or bank_code < 0 or bank_code > 999999 or len(description) > 12:
+ raise Exception('Invalid ABA data')
+
+ f.write(b'0') # Record Type 0
+ f.write(b' ' * 17) # Blank
+ f.write('{:02}'.format(reel_seq).encode('ascii')) # Reel Sequence Number
+ f.write('{: <3}'.format(bank_name).encode('ascii')) # Financial Institution abbreviation
+ f.write(b' ' * 7) # Blank
+ f.write('{: <26}'.format(user_name).encode('ascii')) # User Preferred Specification
+ f.write('{:06}'.format(bank_code).encode('ascii')) # User Identification Number
+ f.write('{: <12}'.format(description).encode('ascii')) # Description
+ f.write(date.strftime('%d%m%y').encode('ascii')) # Date
+ f.write(b' ' * 40) # Blank
+ f.write(b'\r\n')
+
+def write_detail(f, dest_bsb='', dest_account='', indicator=' ', transaction_code=53, cents=0, dest_name='', reference='', src_bsb='', src_account='', src_name='', tax_withheld=0):
+ if len(dest_bsb.replace('-', '')) != 6 or len(dest_account) > 9 or len(indicator) != 1 or transaction_code < 0 or transaction_code > 99 or len(dest_name) > 32 or len(reference) > 18 or len(src_bsb.replace('-', '')) != 6 or len(src_account) > 9 or len(src_name) > 16:
+ raise Exception('Invalid ABA data')
+
+ f.write(b'1') # Record Type 1
+ f.write('{}-{}'.format(dest_bsb[:3], dest_bsb[-3:]).encode('ascii')) # BSB
+ f.write('{: >9}'.format(dest_account).encode('ascii')) # Account Number
+ f.write(indicator.encode('ascii')) # Indicator
+ f.write('{:02}'.format(transaction_code).encode('ascii')) # Transaction Code
+ f.write('{:010}'.format(cents).encode('ascii')) # Amount
+ f.write('{: <32}'.format(dest_name).encode('ascii')) # Title of Account
+ f.write('{: <18}'.format(reference).encode('ascii')) # Lodgement Reference
+ f.write('{}-{}'.format(src_bsb[:3], src_bsb[-3:]).encode('ascii')) # Trace BSB
+ f.write('{: >9}'.format(src_account).encode('ascii')) # Trace Account Number
+ f.write('{: <16}'.format(src_name).encode('ascii')) # Name of Remitter
+ f.write('{:08}'.format(tax_withheld).encode('ascii')) # Amount of Withholding Tax
+ f.write(b'\r\n')
+
+def write_total(f, credit_cents=0, num_detail_records=0):
+ f.write(b'7') # Record Type 7
+ f.write(b'999-999') # BSB Format Filler
+ f.write(b' ' * 12) # Blank
+ f.write('{:010}'.format(credit_cents).encode('ascii')) # File (User) Net Total Amount
+ f.write('{:010}'.format(credit_cents).encode('ascii')) # File (User) Credit Total Amount
+ f.write(b'0' * 10) # File (User) Debit Total Amount
+ f.write(b' ' * 24) # Blank
+ f.write('{:06}'.format(num_detail_records).encode('ascii')) # File (user) count of Records Type 1
+ f.write(b' ' * 40) # Blank
+ f.write(b'\r\n')
diff --git a/sstreasury/jinja2/sstreasury/base.html b/sstreasury/jinja2/sstreasury/base.html
index 83b3b36..c732892 100644
--- a/sstreasury/jinja2/sstreasury/base.html
+++ b/sstreasury/jinja2/sstreasury/base.html
@@ -2,7 +2,7 @@
{#
Society Self-Service
- Copyright © 2018 Yingtong Li (RunasSudo)
+ Copyright © 2018–2020 Yingtong Li (RunasSudo)
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
@@ -34,7 +34,10 @@
Reimbursements
diff --git a/sstreasury/jinja2/sstreasury/budget_list.html b/sstreasury/jinja2/sstreasury/budget_list.html
index b46e00f..446679d 100644
--- a/sstreasury/jinja2/sstreasury/budget_list.html
+++ b/sstreasury/jinja2/sstreasury/budget_list.html
@@ -2,7 +2,7 @@
{#
Society Self-Service
- Copyright © 2018-2019 Yingtong Li (RunasSudo)
+ Copyright © 2018–2020 Yingtong Li (RunasSudo)
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
@@ -24,7 +24,7 @@
- Name |
+ Name |
Status |
View |
diff --git a/sstreasury/jinja2/sstreasury/claim_list.html b/sstreasury/jinja2/sstreasury/claim_list.html
index 92ff02c..91195c2 100644
--- a/sstreasury/jinja2/sstreasury/claim_list.html
+++ b/sstreasury/jinja2/sstreasury/claim_list.html
@@ -2,7 +2,7 @@
{#
Society Self-Service
- Copyright © 2018-2019 Yingtong Li (RunasSudo)
+ Copyright © 2018–2020 Yingtong Li (RunasSudo)
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
@@ -24,7 +24,7 @@
- Name |
+ Purpose |
Status |
View |
diff --git a/sstreasury/jinja2/sstreasury/claim_processing.html b/sstreasury/jinja2/sstreasury/claim_processing.html
new file mode 100644
index 0000000..0086e60
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/claim_processing.html
@@ -0,0 +1,61 @@
+{% extends 'sstreasury/base.html' %}
+
+{#
+ Society Self-Service
+ Copyright © 2018–2020 Yingtong Li (RunasSudo)
+
+ 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 .
+#}
+
+{% block title %}Claims processing{% endblock %}
+
+{% block maincontent %}
+ Claims processing
+
+
+{% endblock %}
+
+{% block head %}
+ {{ super() }}
+{% endblock %}
diff --git a/sstreasury/jinja2/sstreasury/claim_view.html b/sstreasury/jinja2/sstreasury/claim_view.html
index 5501358..937bffd 100644
--- a/sstreasury/jinja2/sstreasury/claim_view.html
+++ b/sstreasury/jinja2/sstreasury/claim_view.html
@@ -50,6 +50,12 @@
{% endif %}
+ {% if claim.state == import('sstreasury.models').ClaimState.APPROVED.value and request.user.groups.filter(name='Treasury').exists() %}
+
+
This claim has been approved and is now awaiting payment. To pay this claim, access the Claims processing page.
+
+ {% endif %}
+
diff --git a/sstreasury/jinja2/sstreasury/email/claim_paid.md b/sstreasury/jinja2/sstreasury/email/claim_paid.md
new file mode 100644
index 0000000..a76dbbc
--- /dev/null
+++ b/sstreasury/jinja2/sstreasury/email/claim_paid.md
@@ -0,0 +1,3 @@
+Your reimbursement claim titled *{{ claim.purpose }}* (RE-{{ claim.id }}) has been paid.
+
+{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
diff --git a/sstreasury/models.py b/sstreasury/models.py
index 222e00a..a633eef 100644
--- a/sstreasury/models.py
+++ b/sstreasury/models.py
@@ -20,6 +20,7 @@ from django.db import models
from django.utils import timezone
from jsonfield import JSONField
+from decimal import Decimal
from enum import Enum
class DescriptionEnum(Enum):
@@ -199,6 +200,12 @@ class ReimbursementClaim(models.Model):
payee_bsb = models.CharField(max_length=7)
payee_account = models.TextField(max_length=20)
+ def get_total(self):
+ total = Decimal(0)
+ for item in self.items:
+ total += Decimal(item['Unit price']) * item['Units']
+ return total
+
def update_state(self, user, state):
self.state = state.value
self.save()
diff --git a/sstreasury/urls.py b/sstreasury/urls.py
index 9583951..629ef12 100644
--- a/sstreasury/urls.py
+++ b/sstreasury/urls.py
@@ -31,5 +31,6 @@ urlpatterns = [
path('claims/view//print', views.claim_print, name='claim_print'),
path('claims/edit/', views.claim_edit, name='claim_edit'),
path('claims/action/', views.claim_action, name='claim_action'),
+ path('claims/processing', views.claim_processing, name='claim_processing'),
path('', views.index, name='treasury'),
]
diff --git a/sstreasury/views.py b/sstreasury/views.py
index 7e7f01d..e609edc 100644
--- a/sstreasury/views.py
+++ b/sstreasury/views.py
@@ -1,5 +1,5 @@
# Society Self-Service
-# Copyright © 2018-2019 Yingtong Li (RunasSudo)
+# Copyright © 2018–2020 Yingtong Li (RunasSudo)
#
# 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
@@ -19,16 +19,20 @@ from django.contrib.auth.models import User
from django.core.exceptions import PermissionDenied
from django.core.validators import validate_email
+from django.conf import settings
from django.db import transaction
+from django.http import HttpResponse
from django.shortcuts import render, redirect
from django.urls import reverse
from django.utils import timezone
from django.views import generic
+from . import aba
from . import models
from ssmain.email import Emailer
import functools
+import io
import itertools
import json
@@ -565,3 +569,44 @@ def claim_action(request, claim):
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})
return redirect(reverse('claim_view', kwargs={'id': claim.id}))
+
+@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)]
+
+ 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())
+
+ for claim in claims:
+ aba.write_detail(aba_file, dest_bsb=claim.payee_bsb, dest_account=claim.payee_account, cents=claim.get_total()*100, dest_name=claim.payee_name, reference='RE-{}'.format(claim.id), src_bsb=settings.ABA_SRC_BSB, src_account=settings.ABA_SRC_ACC, src_name=settings.ABA_USER_NAME)
+
+ aba.write_total(aba_file, credit_cents=sum(c.get_total() for c in claims)*100, num_detail_records=len(claims))
+ aba_file.flush()
+
+ response = HttpResponse(aba_file.getvalue(), content_type='text/plain')
+ response['Content-Disposition'] = 'attachment; filename="claims.aba"'
+ return response
+
+ 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
+ })