Import ABA export and claim payment
This commit is contained in:
parent
77102d7203
commit
cdb3518b28
@ -32,6 +32,12 @@ PROMO_LOGO_URL = 'https://placehold.it/2000x500'
|
|||||||
PROMO_LOGO_LINK = 'https://example.com'
|
PROMO_LOGO_LINK = 'https://example.com'
|
||||||
PROMO_GROUPS_MANDATORY = ['All Years']
|
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
|
# Application definition
|
||||||
|
|
||||||
INSTALLED_APPS = [
|
INSTALLED_APPS = [
|
||||||
|
65
sstreasury/aba.py
Normal file
65
sstreasury/aba.py
Normal file
@ -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.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
# 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')
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{#
|
{#
|
||||||
Society Self-Service
|
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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -34,7 +34,10 @@
|
|||||||
Reimbursements
|
Reimbursements
|
||||||
<div class="menu">
|
<div class="menu">
|
||||||
<a class="{% if request.resolver_match.url_name == 'claim_list' %}active {% endif %}item" href="{{ url('claim_list') }}">Your reimbursement claims</a>
|
<a class="{% if request.resolver_match.url_name == 'claim_list' %}active {% endif %}item" href="{{ url('claim_list') }}">Your reimbursement claims</a>
|
||||||
<a class="item">Create new claim</a>
|
<a class="{% if request.resolver_match.url_name == 'claim_new' %}active {% endif %}item" href="{{ url('claim_new') }}">Create new claim</a>
|
||||||
|
{% if request.user.groups.filter(name='Treasury').exists() %}
|
||||||
|
<a class="{% if request.resolver_match.url_name == 'claim_processing' %}active {% endif %}item" href="{{ url('claim_processing') }}">Claims processing</a>
|
||||||
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{#
|
{#
|
||||||
Society Self-Service
|
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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<table class="ui selectable celled table">
|
<table class="ui selectable celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="twelve wide">Name</th>
|
<th class="eleven wide">Name</th>
|
||||||
<th class="four wide">Status</th>
|
<th class="four wide">Status</th>
|
||||||
<th class="one wide">View</th>
|
<th class="one wide">View</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
@ -2,7 +2,7 @@
|
|||||||
|
|
||||||
{#
|
{#
|
||||||
Society Self-Service
|
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
|
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
|
it under the terms of the GNU Affero General Public License as published by
|
||||||
@ -24,7 +24,7 @@
|
|||||||
<table class="ui selectable celled table">
|
<table class="ui selectable celled table">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th class="twelve wide">Name</th>
|
<th class="eleven wide">Purpose</th>
|
||||||
<th class="four wide">Status</th>
|
<th class="four wide">Status</th>
|
||||||
<th class="one wide">View</th>
|
<th class="one wide">View</th>
|
||||||
</tr>
|
</tr>
|
||||||
|
61
sstreasury/jinja2/sstreasury/claim_processing.html
Normal file
61
sstreasury/jinja2/sstreasury/claim_processing.html
Normal file
@ -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 <https://www.gnu.org/licenses/>.
|
||||||
|
#}
|
||||||
|
|
||||||
|
{% block title %}Claims processing{% endblock %}
|
||||||
|
|
||||||
|
{% block maincontent %}
|
||||||
|
<h1>Claims processing</h1>
|
||||||
|
|
||||||
|
<form class="ui form" method="POST">
|
||||||
|
<button class="ui small primary labeled icon button" type="submit" name="action" value="Export"><i class="download icon"></i>Export selected claims to ABA</button>
|
||||||
|
<button class="ui small basic primary labeled icon button" type="submit" name="action" value="Pay"><i class="check icon"></i>Mark selected claims as paid</button>
|
||||||
|
|
||||||
|
<table class="ui celled table">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th class="one wide"><input type="checkbox" onchange="$(this.form).find('.claim-checkbox').prop('checked', this.checked);"></th>
|
||||||
|
<th class="nine wide">Purpose</th>
|
||||||
|
<th class="three wide">Payee</th>
|
||||||
|
<th class="two wide">Total</th>
|
||||||
|
<th class="one wide">View</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{% for claim in claims %}
|
||||||
|
<tr>
|
||||||
|
<td><input name="claim_{{ claim.id }}" type="checkbox" class="claim-checkbox"></td>
|
||||||
|
<td>{{ claim.purpose }}</td>
|
||||||
|
<td>{{ claim.payee_name }}</td>
|
||||||
|
<td>{{ '${:.2f}'.format(claim.get_total()) }}</td>
|
||||||
|
<td>
|
||||||
|
<a href="{{ url('claim_view', kwargs={'id': claim.id}) }}" class="ui tiny primary icon button"><i class="eye icon"></i></a>
|
||||||
|
</td>
|
||||||
|
</tr>
|
||||||
|
{% endfor %}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
|
||||||
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
|
</form>
|
||||||
|
{% endblock %}
|
||||||
|
|
||||||
|
{% block head %}
|
||||||
|
{{ super() }}
|
||||||
|
{% endblock %}
|
@ -50,6 +50,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
|
|
||||||
|
{% if claim.state == import('sstreasury.models').ClaimState.APPROVED.value and request.user.groups.filter(name='Treasury').exists() %}
|
||||||
|
<div class="ui message">
|
||||||
|
<p>This claim has been approved and is now awaiting payment. To pay this claim, access the <a href="{{ url('claim_processing') }}">Claims processing</a> page.</p>
|
||||||
|
</div>
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<input type="hidden" name="action" value="">
|
<input type="hidden" name="action" value="">
|
||||||
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
<input type="hidden" name="csrfmiddlewaretoken" value="{{ csrf_token }}">
|
||||||
</form>
|
</form>
|
||||||
|
3
sstreasury/jinja2/sstreasury/email/claim_paid.md
Normal file
3
sstreasury/jinja2/sstreasury/email/claim_paid.md
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
Your reimbursement claim titled *{{ claim.purpose }}* (RE-{{ claim.id }}) has been paid.
|
||||||
|
|
||||||
|
{{ baseurl }}{{ url('claim_view', kwargs={'id': claim.id}) }}
|
@ -20,6 +20,7 @@ from django.db import models
|
|||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from jsonfield import JSONField
|
from jsonfield import JSONField
|
||||||
|
|
||||||
|
from decimal import Decimal
|
||||||
from enum import Enum
|
from enum import Enum
|
||||||
|
|
||||||
class DescriptionEnum(Enum):
|
class DescriptionEnum(Enum):
|
||||||
@ -199,6 +200,12 @@ class ReimbursementClaim(models.Model):
|
|||||||
payee_bsb = models.CharField(max_length=7)
|
payee_bsb = models.CharField(max_length=7)
|
||||||
payee_account = models.TextField(max_length=20)
|
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):
|
def update_state(self, user, state):
|
||||||
self.state = state.value
|
self.state = state.value
|
||||||
self.save()
|
self.save()
|
||||||
|
@ -31,5 +31,6 @@ urlpatterns = [
|
|||||||
path('claims/view/<int:id>/print', views.claim_print, name='claim_print'),
|
path('claims/view/<int:id>/print', views.claim_print, name='claim_print'),
|
||||||
path('claims/edit/<int:id>', views.claim_edit, name='claim_edit'),
|
path('claims/edit/<int:id>', views.claim_edit, name='claim_edit'),
|
||||||
path('claims/action/<int:id>', views.claim_action, name='claim_action'),
|
path('claims/action/<int:id>', views.claim_action, name='claim_action'),
|
||||||
|
path('claims/processing', views.claim_processing, name='claim_processing'),
|
||||||
path('', views.index, name='treasury'),
|
path('', views.index, name='treasury'),
|
||||||
]
|
]
|
||||||
|
@ -1,5 +1,5 @@
|
|||||||
# Society Self-Service
|
# 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
|
# 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
|
# 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.exceptions import PermissionDenied
|
||||||
from django.core.validators import validate_email
|
from django.core.validators import validate_email
|
||||||
|
|
||||||
|
from django.conf import settings
|
||||||
from django.db import transaction
|
from django.db import transaction
|
||||||
|
from django.http import HttpResponse
|
||||||
from django.shortcuts import render, redirect
|
from django.shortcuts import render, redirect
|
||||||
from django.urls import reverse
|
from django.urls import reverse
|
||||||
from django.utils import timezone
|
from django.utils import timezone
|
||||||
from django.views import generic
|
from django.views import generic
|
||||||
|
|
||||||
|
from . import aba
|
||||||
from . import models
|
from . import models
|
||||||
from ssmain.email import Emailer
|
from ssmain.email import Emailer
|
||||||
|
|
||||||
import functools
|
import functools
|
||||||
|
import io
|
||||||
import itertools
|
import itertools
|
||||||
import json
|
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})
|
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}))
|
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
|
||||||
|
})
|
||||||
|
Loading…
Reference in New Issue
Block a user