Changeset 1019420
- Timestamp:
- Jul 9, 2014, 12:22:14 AM (12 years ago)
- Branches:
- master
- Children:
- 952d9fb
- Parents:
- 1a8ef12 (diff), 53a42c4 (diff)
Note: this is a merge changeset, the changes displayed below correspond to the merge itself.
Use the (diff) links above to see all the changes relative to each parent. - git-author:
- Alex Dehnert <adehnert@…> (07/09/14 00:22:14)
- git-committer:
- Alex Dehnert <adehnert@…> (07/09/14 00:22:14)
- Files:
-
- 11 added
- 1 deleted
- 23 edited
- 1 moved
-
client/README (added)
-
client/sapweb.py (added)
-
client/test-rfps.csv (added)
-
.gitignore (modified) (1 diff)
-
INSTALL (deleted)
-
INSTALL.rst (added)
-
client/downloader.py (modified) (1 diff)
-
remit/finance_core/migrations/0004_socket_auth_password.py (modified) (2 diffs)
-
remit/finance_core/models.py (modified) (1 diff)
-
remit/finance_core/urls.py (modified) (1 diff)
-
remit/manage.py (modified) (1 diff)
-
remit/remit_templates/base.html (modified) (3 diffs)
-
remit/remit_templates/index.html (modified) (2 diffs)
-
remit/remit_templates/registration/logged_out.html (modified) (2 diffs)
-
remit/remit_templates/registration/login.html (modified) (2 diffs)
-
remit/remit_templates/vouchers/ReimbursementRequest_review.html (modified) (4 diffs)
-
remit/remit_templates/vouchers/emails/rfp_submitted.txt (added)
-
remit/remit_templates/vouchers/emails/rfps_download.txt (added)
-
remit/remit_templates/vouchers/emails/rfps_updated.txt (added)
-
remit/remit_templates/vouchers/reimbursementrequest_list.html (modified) (3 diffs)
-
remit/remit_templates/vouchers/submit.html (modified) (1 diff)
-
remit/settings/__init__.py (moved) (moved from remit/settings.py) (7 diffs)
-
remit/settings/init-dev.sh (added)
-
remit/settings/local.dev-template.py (added)
-
remit/urls.py (modified) (3 diffs)
-
remit/util/add_accounts (modified) (1 diff)
-
remit/util/add_gl_accounts.py (modified) (1 diff)
-
remit/util/setup.py (modified) (1 diff)
-
remit/util/shortcuts.py (modified) (2 diffs)
-
remit/util/templatetags/latex.py (modified) (1 diff)
-
remit/vouchers/admin.py (modified) (1 diff)
-
remit/vouchers/migrations/0009_add_rfp_integration.py (added)
-
remit/vouchers/models.py (modified) (10 diffs)
-
remit/vouchers/urls.py (modified) (2 diffs)
-
remit/vouchers/views.py (modified) (10 diffs)
-
setup.py (added)
Legend:
- Unmodified
- Added
- Removed
-
.gitignore
r09b7c26 rc386673 3 3 django-treebeard-1.1/ 4 4 django-treebeard-1.1.tar.gz 5 local_settings.py6 local_settings_after.py5 remit/settings/local.py 6 remit/settings/local_after.py 7 7 database_settings 8 8 client/settings.py -
client/downloader.py
rf8a5e4b rfeed77c 36 36 37 37 def getLaTeX(br, latex_file, ): 38 br.open(baseurl + 'vouchers/generate/ ')38 br.open(baseurl + 'vouchers/generate/vouchers.tex') 39 39 if br.viewing_html(): 40 40 print br.response().get_data() -
remit/finance_core/migrations/0004_socket_auth_password.py
r38843de r75ccc48 3 3 from south.db import db 4 4 from south.v2 import DataMigration 5 from django.contrib.auth.hashers import UNUSABLE_PASSWORD 5 import django.contrib.auth.hashers 6 6 from django.db import models 7 from django.db.models import F 7 8 8 9 class Migration(DataMigration): … … 11 12 "Write your forwards methods here." 12 13 issues = orm['auth.user'].objects.filter(password__in=['', 'SocketAuth']) 13 issues.update(password=UNUSABLE_PASSWORD) 14 try: # pre-1.6 15 new_password = django.contrib.auth.hashers.UNUSABLE_PASSWORD 16 except AttributeError: # post-1.6 17 # See https://code.djangoproject.com/ticket/20079 for details on the change. 18 # Ideally, we'd use a different suffix per user, but I don't want 19 # to deal with that, and this is probably acceptably secure. 20 # 21 # Also, it seems a little unlikely that this code will actually 22 # run -- it requires an install that hasn't already run the 23 # migration (notably, esp.mit.edu has already), but *does* have 24 # accounts with SocketAuth passwords. 25 prefix = django.contrib.auth.hashers.UNUSABLE_PASSWORD_PREFIX 26 suffix = django.contrib.auth.hashers.get_random_string(django.contrib.auth.hashers.UNUSABLE_PASSWORD_SUFFIX_LENGTH) 27 new_password = prefix + F("password") + prefix + suffix 28 issues.update(password=new_password) 14 29 15 30 def backwards(self, orm): -
remit/finance_core/models.py
r2fb5a35 r4c6a731 207 207 LAYER_CLOSEOUT = 40 208 208 layers=( 209 (LAYER_BUDGET, 'budget'), 210 (LAYER_ALLOCATION, 'allocation'), 211 (LAYER_EXPENDITURE, 'expenditure'), 212 (LAYER_CLOSEOUT, 'closeout'), 209 # Guessing the meanings here a bit... 210 (LAYER_BUDGET, 'budget'), # requested amount 211 (LAYER_ALLOCATION, 'allocation'), # amount approved 212 (LAYER_EXPENDITURE, 'expenditure'), # spent amount 213 (LAYER_CLOSEOUT, 'closeout'), # difference between spent and allocated 214 215 # Remit was originally developed for the UA, so I'm guessing the layers 216 # reflect a process where the treasurer proposes a budget, the body 217 # approves it (potentially with amendments), committees spend it, and then 218 # you balance the accounts at the end 219 213 220 ) 214 221 def get_layer_by_name(name): -
remit/finance_core/urls.py
red4ccc7 rff623c3 1 from django.conf.urls .defaults import *1 from django.conf.urls import patterns, include, url 2 2 import finance_core.views 3 3 -
remit/manage.py
r92ca60e r5c334f6 1 #!/usr/bin/python 2 from django.core.management import execute_manager 3 try: 4 import settings # Assumed to be in the same directory. 5 except ImportError: 6 import sys 7 sys.stderr.write("Error: Can't find the file 'settings.py' in the directory containing %r. It appears you've customized things.\nYou'll have to run django-admin.py, passing it your settings module.\n(If the file settings.py does indeed exist, it's causing an ImportError somehow.)\n" % __file__) 8 sys.exit(1) 1 #!/usr/bin/env python 2 import os 3 import sys 9 4 10 5 if __name__ == "__main__": 11 execute_manager(settings) 6 os.environ.setdefault("DJANGO_SETTINGS_MODULE", "settings") 7 8 from django.core.management import execute_from_command_line 9 10 execute_from_command_line(sys.argv) -
remit/remit_templates/base.html
rc3e751d r695d358 1 {% load url from future %} 1 2 <!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd"> 2 3 … … 14 15 <body> 15 16 <ul class='tab-navigation'> 16 <li{% ifequal pagename "homepage" %} class='selected'{% endifequal %}><a href="{% url homepage%}">Home</a></li>17 <li{% ifequal pagename "request_reimbursement" %} class='selected'{% endifequal %}><a href="{% url request_reimbursement%}">Request reimbursement</a></li>18 <li{% ifequal pagename "list_requests" %} class='selected'{% endifequal %}><a href="{% url list_requests%}">List requests</a></li>17 <li{% ifequal pagename "homepage" %} class='selected'{% endifequal %}><a href="{% url "homepage" %}">Home</a></li> 18 <li{% ifequal pagename "request_reimbursement" %} class='selected'{% endifequal %}><a href="{% url "request_reimbursement" %}">Request reimbursement</a></li> 19 <li{% ifequal pagename "list_requests" %} class='selected'{% endifequal %}><a href="{% url "list_requests" %}">List requests</a></li> 19 20 {% if perms.finance_core.use_reporting %} 20 <li{% ifequal pagename "reporting" %} class='selected'{% endifequal %}><a href="{% url reporting%}">Reporting</a></li>21 <li{% ifequal pagename "reporting" %} class='selected'{% endifequal %}><a href="{% url "reporting" %}">Reporting</a></li> 21 22 {% endif %} 22 23 {% if user.is_staff %} 23 <li><a href='{% url admin:index%}'>Django Admin</a></li>24 <li><a href='{% url "admin:index" %}'>Django Admin</a></li> 24 25 {% endif %} 25 26 </ul> … … 33 34 <p>Welcome, <span class='username'>{{user.username}}</span>.</p> 34 35 {% endif %} 35 <p><a href='{% url logout%}'>Logout</a></p>36 <p><a href='{% url "logout" %}'>Logout</a></p> 36 37 </div> 37 38 {% endif %} -
remit/remit_templates/index.html
r0671644 r695d358 1 1 {% extends "base.html" %} 2 {% load url from future %} 2 3 3 4 {% block title %}Welcome to Remit{% endblock %} … … 12 13 <ul> 13 14 {% if user.is_anonymous %} 14 <li><a href='{% url login%}'>Login</a></li>15 <li><a href='{% url "login" %}'>Login</a></li> 15 16 {% endif %} 16 <li><a href="{% url request_reimbursement%}">Request reimbursement</a></li>17 <li><a href="{% url list_requests%}">List requests</a></li>17 <li><a href="{% url "request_reimbursement" %}">Request reimbursement</a></li> 18 <li><a href="{% url "list_requests" %}">List requests</a></li> 18 19 {% if perms.finance_core.use_reporting %} 19 <li><a href="{% url reporting%}">Reporting</a></li>20 <li><a href="{% url "reporting" %}">Reporting</a></li> 20 21 {% endif %} 21 22 {% if user.is_staff %} 22 <li><a href='{% url admin:index%}'>Django admin interface</a></li>23 <li><a href='{% url "admin:index" %}'>Django admin interface</a></li> 23 24 {% endif %} 24 25 {% if user.is_authenticated %} 25 <li><a href='{% url logout%}'>Logout</a></li>26 <li><a href='{% url "logout" %}'>Logout</a></li> 26 27 {% endif %} 27 28 </ul> -
remit/remit_templates/registration/logged_out.html
r732d9eb r695d358 1 1 {% extends "base.html" %} 2 {% load url from future %} 2 3 3 4 {% block title %}Logged out of Remit{% endblock %} … … 6 7 <h2>Logged out of Remit</h2> 7 8 8 <p>You have been logged out of Remit. You can return to the <a href='{% url homepage %}'>homepage</a> or <a href='{% url login%}'>login again</a>.</p>9 <p>You have been logged out of Remit. You can return to the <a href='{% url "homepage" %}'>homepage</a> or <a href='{% url "login" %}'>login again</a>.</p> 9 10 {% endblock %} -
remit/remit_templates/registration/login.html
r1ba734f r695d358 1 1 {% extends "base.html" %} 2 {% load url from future %} 2 3 3 4 {% block title %}Log in to Remit{% endblock %} … … 11 12 {% endif %} 12 13 13 <form method="post" action="{% url django.contrib.auth.views.login%}">14 <form method="post" action="{% url "django.contrib.auth.views.login" %}"> 14 15 {% csrf_token %} 15 16 <table> -
remit/remit_templates/vouchers/ReimbursementRequest_review.html
ra5ef930 r638b547 1 1 {% extends "base.html" %} 2 {% load url from future %} 2 3 {% load misc %} 3 4 … … 94 95 <tr><th colspan='2'><input type='submit' name='upload_documentation' value='Upload Documentation' /></th></tr> 95 96 </table> 97 {% csrf_token %} 96 98 </form> 97 99 {% endif %} … … 112 114 <input type='submit' name='send_email' value='{{option.label}}'> 113 115 <input type='hidden' name='email_name' value='{{option.name}}'> 116 {% csrf_token %} 114 117 </form> 115 118 {% endfor %} … … 117 120 {% endif %} 118 121 119 {% if approve_form %}120 <h3>Approve this reimbursement request:</h3>121 {% if approve_message %}122 <p>{{ approve_message }}</p>123 {% else %}124 {% if perms.vouchers.change_reimbursementrequest %}125 <p>Optionally, <a href='{% url admin:vouchers_reimbursementrequest_change rr.pk %}'>change this request first</a>.</p>126 {% endif %}127 <form method="post" action="">128 <table class='pretty-table'>129 {{ approve_form.as_table }}130 122 131 <tr> 132 <th colspan='2'><input type="submit" name="approve" value="Approve" /></td> 133 </tr> 134 </table> 135 </form> 136 {% endif %} 123 {% if show_approve or approve_message %} 124 <h3>Approve this reimbursement request:</h3> 125 {% if approve_message %} 126 <div class='messagebox {{approve_level}}box'> 127 <p>{{approve_message}}</p> 128 </div> 129 {% else %} 130 {% if perms.vouchers.change_reimbursementrequest %} 131 <p>Optionally, <a href='{% url "admin:vouchers_reimbursementrequest_change" rr.pk %}'>change this request first</a>.</p> 132 {% endif %} 133 <form method="post" action="."> 134 <th colspan='2'><input type="submit" name="approve" value="Approve as RFP" /></td> 135 {% csrf_token %} 136 </form> 137 {% endif %} 137 138 {% endif %} 138 139 -
remit/remit_templates/vouchers/reimbursementrequest_list.html
rf6f7aff rbc1da44 1 1 {% extends "base.html" %} 2 {% load url from future %} 2 3 {% load misc %} 3 4 … … 58 59 <td> 59 60 <input type='checkbox' name='select' value='{{object.pk}}'{%ifin object.pk selected_ids %} checked='checked'{%endifin%}></input> 60 <a href='{% url review_requestobject.pk %}'>{{ object.pk }}</a>61 <a href='{% url "review_request" object.pk %}'>{{ object.pk }}</a> 61 62 </td> 62 63 <td> 63 <em><a href='{% url review_requestobject.pk %}'>{{ object.name }}</a></em>64 <em><a href='{% url "review_request" object.pk %}'>{{ object.name }}</a></em> 64 65 {% if object.documentation %}<br /><a href='{{object.documentation.backing_file.url}}'>(view docs)</a>{%endif%} 65 66 </td> … … 139 140 {%endif%} 140 141 {%endif%} 142 143 {% csrf_token %} 141 144 </form> 142 145 -
remit/remit_templates/vouchers/submit.html
rf301a50 r2b31097 19 19 <td>{{comm}}</td> 20 20 </tr> 21 <tr> 22 <th>Recipient type</th> 23 <td>{{request.get_recipient_type_display}}</td> 24 </tr> 21 25 22 26 <tr class='sect-head'> -
remit/settings/__init__.py
rf5bebab rfeed77c 3 3 import sys 4 4 5 SITE_ROOT = os.path.normpath(os.path. dirname(__file__))5 SITE_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..')) 6 6 SITE_WEB_PATH = '' 7 7 DEFAULT_DOMAIN = 'mit.edu' … … 10 10 TEMPLATE_DEBUG = DEBUG 11 11 12 ATOMIC_REQUESTS = True 13 12 14 ADMINS = ( 13 15 ('Remit team', 'remit-default-addr@mit.edu'), 14 16 ) 15 17 SERVER_EMAIL = 'remit-default-addr@mit.edu' 18 DEFAULT_FROM_EMAIL = 'remit-default-addr@mit.edu' 16 19 17 20 GROUP_NAME = 'Remit' 18 21 GROUP_ABBR = 'RM' 19 SIGNATORY_EMAIL = None 22 SIGNATORY_EMAIL = None # used only for paper vouchers (which SAO killed) 20 23 21 24 CC_SUBMITTER = False … … 23 26 MANAGERS = ADMINS 24 27 28 # When changing this, be aware of uses of the budgetarea_pathstr filter 25 29 BASE_COMMITTEE_PATH = ['Accounts', 'Assets', ] 26 30 COMMITTEE_HIERARCHY_LEVELS = 2 … … 53 57 USE_I18N = True 54 58 55 from local_settings import * 59 # Required settings 60 # SITE_URL_BASE: used to construct absolute URLs for use in emails 61 62 from local import * 56 63 57 64 # Absolute path to the directory that holds media. … … 85 92 'django.contrib.sessions.middleware.SessionMiddleware', 86 93 'django.contrib.auth.middleware.AuthenticationMiddleware', 87 'django.middleware.transaction.TransactionMiddleware',88 94 'django.middleware.csrf.CsrfViewMiddleware', 89 95 'django.contrib.messages.middleware.MessageMiddleware', … … 116 122 'django.contrib.sessions', 117 123 'django.contrib.sites', 124 'django.contrib.staticfiles', 118 125 'treebeard', 119 126 'south', … … 126 133 USER_EMAIL_SIGNATURE = "%s Treasury" % (GROUP_NAME,) 127 134 128 from local_ settings_after import *135 from local_after import * -
remit/urls.py
rf5bebab r21360c8 1 from django.conf.urls .defaults import *1 from django.conf.urls import patterns, include, url 2 2 from django.contrib.auth.views import login, logout 3 3 … … 6 6 # Necessary views 7 7 import finance_core.views 8 from util.shortcuts import TemplateViewWithContext 8 9 9 10 # Uncomment the next two lines to enable the admin: … … 13 14 urlpatterns = patterns('', 14 15 # Example: 15 (r'^$', 'django.views.generic.simple.direct_to_template', {'template': 'index.html', 'extra_context': { 'pagename':'homepage' }, },'homepage'),16 url(r'^$', TemplateViewWithContext.as_view(template_name='index.html', extra_context={ 'pagename':'homepage' }), name='homepage'), 16 17 (r'^vouchers/', include('vouchers.urls')), 17 18 (r'^finance_core/', include('finance_core.urls')), -
remit/util/add_accounts
r8132e8c rf298bd7 1 #!/usr/bin/ python1 #!/usr/bin/env python 2 2 import sys 3 3 import os -
remit/util/add_gl_accounts.py
r0df317c rf298bd7 1 #!/usr/bin/ python1 #!/usr/bin/env python 2 2 3 3 import sys -
remit/util/setup.py
ra1e4224 rf298bd7 1 #!/usr/bin/ python1 #!/usr/bin/env python 2 2 import sys 3 3 import os -
remit/util/shortcuts.py
rf7dd5e7 r5f82b6c 2 2 from django.template import RequestContext, Template 3 3 from django.template.loader import get_template 4 from django.views.generic.base import TemplateView 5 from django.views.generic.list import ListView 4 6 5 7 def get_403_response(request, errmsg=None, **extra_context): … … 8 10 page = tmpl.render(ctx, ) 9 11 return HttpResponseForbidden(page) 12 13 14 class TemplateViewWithContext(TemplateView): 15 extra_context = {} 16 17 def get_context_data(self, **kwargs): 18 context = super(TemplateViewWithContext,self).get_context_data(**kwargs) 19 print context 20 context.update(self.extra_context) -
remit/util/templatetags/latex.py
rdcaa9c0 rf298bd7 1 #!/usr/bin/python2 1 # Inspired by and partially copied from ESP's backslash stuff 3 2 -
remit/vouchers/admin.py
r9102ac9 rfeed77c 27 27 list_display_links = ('description', ) 28 28 29 class RFPAdmin(admin.ModelAdmin): 30 list_display = ('pk', 'name', 'rfp_number', 'rfp_submit_time', 'payee_type', 'payee_name', 'item_amount', 'item_co', 'item_gl', 'documentation', ) 31 list_display_links = ('pk', 'name', ) 32 29 33 admin.site.register(vouchers.models.ReimbursementRequest, ReimbursementRequestAdmin) 30 34 admin.site.register(vouchers.models.Voucher, VoucherAdmin) 35 admin.site.register(vouchers.models.RFP, RFPAdmin) -
remit/vouchers/models.py
r42b287c rfeed77c 10 10 11 11 import datetime 12 import re 12 13 13 14 APPROVAL_STATE_PENDING = 0 … … 20 21 ) 21 22 23 recipient_type_choices = ( 24 ('mit', 'MIT student, faculty, or staff', ), 25 ('other', 'Other, including alumnus or affiliate', ), 26 ) 22 27 class ReimbursementRequest(models.Model): 23 28 submitter = models.CharField(max_length=30) # Username of submitter 29 recipient_type = models.CharField(choices=recipient_type_choices, max_length=10, default='other') 24 30 check_to_first_name = models.CharField(max_length=50, verbose_name="check recipient's first name") 25 31 check_to_last_name = models.CharField(max_length=50, verbose_name="check recipient's last name") … … 95 101 def convert_to_rfp(self, ): 96 102 rfp = RFP() 103 max_name_len = RFP._meta.get_field_by_name('name')[0].max_length 104 rfp.name = ("%s: %s" % (self.label(), self.name))[:max_name_len] 105 rfp.payee_type = self.recipient_type 106 rfp.payee_name = "%s %s" % (self.check_to_first_name, self.check_to_last_name) 107 rfp.fill_addr(self.check_to_addr) 108 rfp.item_date = self.incurred_time.date() 109 rfp.item_gl = self.expense_area.get_account_number() 110 rfp.item_co = self.budget_area.get_account_number() 111 rfp.item_amount = self.amount 112 rfp.item_desc = self.description 113 rfp.documentation = self.documentation 97 114 rfp.save() 98 115 self.create_transfers() … … 102 119 self.save() 103 120 104 def approve(self, approver, signatory_name, signatory_email=None, ): 121 def send_approval_email(self, approver, approval_type): 122 tmpl = get_template('vouchers/emails/request_approval_admin.txt') 123 ctx = Context({ 124 'approver': approver, 125 'request': self, 126 'approval_type': approval_type, 127 }) 128 body = tmpl.render(ctx) 129 mail_admins( 130 'Request approval: %s approved $%s [%s]' % ( 131 approver, 132 self.amount, 133 approval_type, 134 ), 135 body, 136 ) 137 138 def approve_as_voucher(self, approver, signatory_name, signatory_email=None, ): 105 139 """Mark a request as approved. 106 140 … … 110 144 """ 111 145 self.convert_to_voucher(signatory_name, signatory_email,) 112 tmpl = get_template('vouchers/emails/request_approval_admin.txt') 113 ctx = Context({ 114 'approver': approver, 115 'request': self, 116 'approval_type': 'voucher', 117 }) 118 body = tmpl.render(ctx) 119 mail_admins( 120 'Request approval: %s approved $%s [voucher]' % ( 121 approver, 122 self.amount, 123 ), 124 body, 125 ) 146 self.send_approval_email(approver, 'voucher') 126 147 127 148 def approve_as_rfp(self, approver, ): 128 149 self.convert_to_rfp() 129 tmpl = get_template('vouchers/emails/request_approval_admin.txt') 130 ctx = Context({ 131 'approver': approver, 132 'request': self, 133 'approval_type': 'RFP', 134 }) 135 body = tmpl.render(ctx) 136 mail_admins( 137 'Request approval: %s approved $%s [RFP]' % ( 138 approver, 139 self.amount, 140 ), 141 body, 142 ) 143 150 self.send_approval_email(approver, 'RFP') 144 151 145 152 def review_link(self, ): … … 162 169 description = models.TextField() 163 170 gl = models.IntegerField() 164 processed = models.BooleanField( )171 processed = models.BooleanField(default=False) 165 172 process_time = models.DateTimeField(blank=True, null=True,) 166 documentation = models.ForeignKey('Documentation', blank=True,null=True, )173 documentation = models.ForeignKey('Documentation', null=True, ) 167 174 168 175 def mailing_addr_lines(self): … … 196 203 197 204 class RFP(models.Model): 205 # Class constants 206 addr_regex = re.compile(r'^(?P<street>.+)\n(?P<city>.+), (?P<state>\w\w) (?P<zip>\d{5}(-\d{4})?)$') 207 addr_error = 'Address must two lines: a street address, followed by "<city>, <two letter state abbreviation> <zip>". For non-US addresses, please contact a Treasurer for help.' 208 209 # Fields 198 210 create_time = models.DateTimeField(default=datetime.datetime.now) 211 rfp_submit_time = models.DateTimeField(default=None, null=True, blank=True) 212 rfp_number = models.IntegerField(default=None, null=True, blank=True) 213 214 name = models.CharField(max_length=25, verbose_name='short description', ) 215 payee_type = models.CharField(choices=recipient_type_choices, max_length=10, default='other') 216 payee_name = models.CharField(max_length=35, ) 217 addr_street = models.CharField(max_length=35, verbose_name='street address', blank=True, ) 218 addr_city = models.CharField(max_length=35, verbose_name='city', blank=True, ) 219 addr_state = models.CharField(max_length=10, verbose_name='state', blank=True, ) 220 addr_zip = models.CharField(max_length=10, verbose_name='zipcode', blank=True, ) 221 item_date = models.DateField() 222 item_gl = models.CharField(max_length=10, verbose_name="item's G/L account") 223 item_co = models.CharField(max_length=12, verbose_name="item's cost object") 224 item_amount = models.DecimalField(max_digits=7, decimal_places=2) 225 item_desc = models.TextField() 226 documentation = models.ForeignKey('Documentation', blank=True, null=True, ) 227 228 def fill_addr(self, address): 229 if address == '': 230 return 231 match = self.addr_regex.match(address) 232 assert match != None 233 self.addr_street = match.group("street") 234 self.addr_city = match.group("city") 235 self.addr_state = match.group("state") 236 self.addr_zip = match.group("zip") 199 237 200 238 def __unicode__(self, ): 201 239 return "RFP: %s" % (self.create_time.strftime(settings.SHORT_DATETIME_FORMAT_F), ) 240 241 class Meta: 242 verbose_name = "RFP" 202 243 203 244 class Documentation(models.Model): … … 316 357 return [ action for action in actions if action.can(user) ] 317 358 318 def bulk_action_approve (http_request, rr):359 def bulk_action_approve_as_voucher(http_request, rr): 319 360 approver = http_request.user 320 361 signatory_name = http_request.user.get_full_name() … … 322 363 return False, "already approved" 323 364 else: 324 rr.approve (approver, signatory_name)365 rr.approve_as_voucher(approver, signatory_name) 325 366 return True, "request approved" 326 367 … … 347 388 if settings.SIGNATORY_EMAIL: 348 389 bulk_request_actions.append(BulkRequestAction( 349 name='approve ',350 label='Approve Requests',351 action=bulk_action_approve ,390 name='approve_as_voucher', 391 label='Approve requests (for vouchers)', 392 action=bulk_action_approve_as_voucher, 352 393 perm_predicate=perm_checker('vouchers.can_approve'), 353 394 )) 354 395 bulk_request_actions.append(BulkRequestAction( 355 396 name='approve_as_rfp', 356 label=' Mark Requests as RFPized',397 label='Approve requests (for RFPizing)', 357 398 action=bulk_action_approve_as_rfp, 358 399 perm_predicate=perm_checker('vouchers.can_approve'), -
remit/vouchers/urls.py
r6e43384 rfeed77c 1 from django.conf.urls.defaults import * 2 from django.views.generic.list_detail import object_list 1 from django.conf.urls import patterns, include, url 3 2 from django.contrib.auth.decorators import permission_required 4 3 import vouchers.views … … 8 7 url(r'list/', vouchers.views.show_requests, name='list_requests', ), 9 8 url(r'reimbursement/', 'vouchers.views.select_request_basics', name='request_reimbursement', ), 10 (r'submit/(?P<term>[\d\w-]+)/(?P<committee>[\d\w-]+)/ ', 'vouchers.views.submit_request', ),9 (r'submit/(?P<term>[\d\w-]+)/(?P<committee>[\d\w-]+)/(?P<recipient_type>mit|other)/', 'vouchers.views.submit_request', ), 11 10 url(r'review/(?P<object_id>\d+)/', 'vouchers.views.review_request', name='review_request', ), 12 url(r'generate/', 'vouchers.views.generate_vouchers', name='generate_vouchers', ), 11 url(r'^generate/vouchers.tex$', 'vouchers.views.generate_vouchers', name='generate_vouchers', ), 12 url(r'^generate/rfps.csv$', 'vouchers.views.generate_rfp_specs', name='generate_rfp_specs', ), 13 13 ) -
remit/vouchers/views.py
r9dee7f6 rfeed77c 1 # -*- coding: utf-8 -*- 1 2 import vouchers.models 2 3 from vouchers.models import ReimbursementRequest, Documentation … … 7 8 from django.shortcuts import render_to_response, get_object_or_404 8 9 from django.template import RequestContext 9 from django.http import Http 404, HttpResponseRedirect10 from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseNotAllowed 10 11 import django.forms 11 from django.forms import Form 12 from django.forms import ModelForm 13 from django.forms import ModelChoiceField 12 from django.forms import ChoiceField, Form, ModelForm, ModelChoiceField 14 13 from django.core.urlresolvers import reverse 15 from django.core.mail import send_mail, mail_admins 14 from django.core.mail import send_mail, mail_admins, EmailMessage 16 15 from django.db.models import Q 17 16 from django.template import Context, Template 18 17 from django.template.loader import get_template 19 from django.views.generic import list_detail 20 18 from django.views.decorators.csrf import ensure_csrf_cookie 19 20 import csv 21 import datetime 21 22 import decimal 22 23 23 24 import settings 24 25 26 27 class CommitteesField(ModelChoiceField): 28 def __init__(self, *args, **kargs): 29 base_area = BudgetArea.get_by_path(settings.BASE_COMMITTEE_PATH) 30 self.strip_levels = base_area.depth 31 areas = (base_area.get_descendants() 32 .filter(depth__lte=base_area.depth+settings.COMMITTEE_HIERARCHY_LEVELS) 33 .exclude(name='Holding') 34 ) 35 ModelChoiceField.__init__(self, queryset=areas, 36 help_text='Select the appropriate committe or other budget area', 37 *args, **kargs) 38 39 def label_from_instance(self, obj,): 40 return obj.indented_name(strip_levels=self.strip_levels) 41 42 class SelectRequestBasicsForm(Form): 43 area = CommitteesField() 44 term = ModelChoiceField(queryset = BudgetTerm.objects.all()) 45 recipient_type = ChoiceField(choices=vouchers.models.recipient_type_choices) 46 47 class DocUploadForm(ModelForm): 48 def clean_backing_file(self, ): 49 f = self.cleaned_data['backing_file'] 50 ext = f.name.rsplit('.')[-1] 51 contenttype = f.content_type 52 if ext != 'pdf': 53 raise django.forms.ValidationError(u"Only PDF files are accepted – you submitted a .%s file" % (ext, )) 54 elif contenttype != 'application/pdf': 55 raise django.forms.ValidationError(u"Only PDF files are accepted – you submitted a %s file" % (contenttype, )) 56 else: 57 return f 58 59 class Meta: 60 model = Documentation 61 fields = ( 62 'label', 63 'backing_file', 64 ) 65 66 67 @user_passes_test(lambda u: u.is_authenticated()) 68 def select_request_basics(http_request, ): 69 if http_request.method == 'POST': # If the form has been submitted... 70 form = SelectRequestBasicsForm(http_request.POST) # A form bound to the POST data 71 if form.is_valid(): # All validation rules pass 72 term = form.cleaned_data['term'].slug 73 area = form.cleaned_data['area'].id 74 recipient_type = form.cleaned_data['recipient_type'] 75 url = reverse(submit_request, args=[term, area, recipient_type],) 76 return HttpResponseRedirect(url) # Redirect after POST 77 else: 78 form = SelectRequestBasicsForm() # An unbound form 79 80 context = { 81 'form':form, 82 'pagename':'request_reimbursement', 83 } 84 return render_to_response('vouchers/select.html', context, context_instance=RequestContext(http_request), ) 85 86 87 class CommitteeBudgetAreasField(ModelChoiceField): 88 def __init__(self, base_area, *args, **kargs): 89 self.strip_levels = base_area.depth 90 areas = base_area.get_descendants() 91 ModelChoiceField.__init__(self, queryset=areas, 92 help_text='In general, this should be a fully indented budget area, not one with children', 93 *args, **kargs) 94 95 def label_from_instance(self, obj,): 96 return obj.indented_name(strip_levels=self.strip_levels) 97 98 class ExpenseAreasField(ModelChoiceField): 99 def __init__(self, *args, **kargs): 100 base_area = vouchers.models.BudgetArea.get_by_path(['Accounts', 'Expenses']) 101 self.strip_levels = base_area.depth 102 areas = base_area.get_descendants() 103 ModelChoiceField.__init__(self, queryset=areas, 104 help_text='In general, this should be a fully indented budget area, not one with children', 105 *args, **kargs) 106 107 def label_from_instance(self, obj,): 108 return obj.indented_name(strip_levels=self.strip_levels) 109 25 110 class RequestForm(ModelForm): 111 expense_area = ExpenseAreasField() 112 113 def __init__(self, *args, **kwargs): 114 super(RequestForm, self).__init__(*args, **kwargs) 115 if self.instance.recipient_type == 'mit': 116 addr_widget = django.forms.HiddenInput() 117 self.fields['check_to_addr'].widget = addr_widget 118 else: 119 rfp = vouchers.models.RFP 120 error_msgs = dict(invalid=rfp.addr_error) 121 addr_widget = self.fields['check_to_addr'].widget 122 addr_field = django.forms.RegexField(regex=rfp.addr_regex, error_messages=error_msgs) 123 addr_field.widget = addr_widget 124 self.fields['check_to_addr'] = addr_field 125 26 126 class Meta: 27 127 model = ReimbursementRequest … … 39 139 ) 40 140 41 42 class CommitteesField(ModelChoiceField):43 def __init__(self, *args, **kargs):44 base_area = BudgetArea.get_by_path(settings.BASE_COMMITTEE_PATH)45 self.strip_levels = base_area.depth46 areas = (base_area.get_descendants()47 .filter(depth__lte=base_area.depth+settings.COMMITTEE_HIERARCHY_LEVELS)48 .exclude(name='Holding')49 )50 ModelChoiceField.__init__(self, queryset=areas,51 help_text='Select the appropriate committe or other budget area',52 *args, **kargs)53 54 def label_from_instance(self, obj,):55 return obj.indented_name(strip_levels=self.strip_levels)56 57 class SelectRequestBasicsForm(Form):58 area = CommitteesField()59 term = ModelChoiceField(queryset = BudgetTerm.objects.all())60 61 class DocUploadForm(ModelForm):62 def clean_backing_file(self, ):63 f = self.cleaned_data['backing_file']64 ext = f.name.rsplit('.')[-1]65 contenttype = f.content_type66 if ext != 'pdf':67 raise django.forms.ValidationError("Only PDF files are accepted --- you submitted a .%s file" % (ext, ))68 elif contenttype != 'application/pdf':69 raise django.forms.ValidationError("Only PDF files are accepted --- you submitted a %s file" % (contenttype, ))70 else:71 return f72 73 class Meta:74 model = Documentation75 fields = (76 'label',77 'backing_file',78 )79 80 81 141 @user_passes_test(lambda u: u.is_authenticated()) 82 def select_request_basics(http_request, ): 83 if http_request.method == 'POST': # If the form has been submitted... 84 form = SelectRequestBasicsForm(http_request.POST) # A form bound to the POST data 85 if form.is_valid(): # All validation rules pass 86 term = form.cleaned_data['term'].slug 87 area = form.cleaned_data['area'].id 88 return HttpResponseRedirect(reverse(submit_request, args=[term, area],)) # Redirect after POST 89 else: 90 form = SelectRequestBasicsForm() # An unbound form 91 92 context = { 93 'form':form, 94 'pagename':'request_reimbursement', 95 } 96 return render_to_response('vouchers/select.html', context, context_instance=RequestContext(http_request), ) 97 98 class CommitteeBudgetAreasField(ModelChoiceField): 99 def __init__(self, base_area, *args, **kargs): 100 self.strip_levels = base_area.depth 101 areas = base_area.get_descendants() 102 ModelChoiceField.__init__(self, queryset=areas, 103 help_text='In general, this should be a fully indented budget area, not one with children', 104 *args, **kargs) 105 106 def label_from_instance(self, obj,): 107 return obj.indented_name(strip_levels=self.strip_levels) 108 109 class ExpenseAreasField(ModelChoiceField): 110 def __init__(self, *args, **kargs): 111 base_area = vouchers.models.BudgetArea.get_by_path(['Accounts', 'Expenses']) 112 self.strip_levels = base_area.depth 113 areas = base_area.get_descendants() 114 ModelChoiceField.__init__(self, queryset=areas, 115 help_text='In general, this should be a fully indented budget area, not one with children', 116 *args, **kargs) 117 118 def label_from_instance(self, obj,): 119 return obj.indented_name(strip_levels=self.strip_levels) 120 121 @user_passes_test(lambda u: u.is_authenticated()) 122 def submit_request(http_request, term, committee): 142 def submit_request(http_request, term, committee, recipient_type, ): 123 143 term_obj = get_object_or_404(BudgetTerm, slug=term) 124 144 comm_obj = get_object_or_404(BudgetArea, pk=committee) … … 127 147 new_request.submitter = http_request.user.username 128 148 new_request.budget_term = term_obj 149 new_request.recipient_type = recipient_type 129 150 130 151 # Prefill from user information (itself prefilled from LDAP now) … … 137 158 form = RequestForm(http_request.POST, instance=new_request) # A form bound to the POST data 138 159 form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj) 139 form.fields['expense_area'] = ExpenseAreasField()140 160 141 161 if form.is_valid(): # All validation rules pass 142 162 request_obj = form.save() 163 print "request_obj==new_request:", request_obj == new_request 143 164 144 165 # Send email … … 170 191 form = RequestForm(instance=new_request, initial=initial, ) # An unbound form 171 192 form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj) 172 form.fields['expense_area'] = ExpenseAreasField()173 193 174 194 context = { 175 195 'term':term_obj, 176 196 'comm':comm_obj, 197 'request': new_request, 177 198 'form':form, 178 199 'pagename':'request_reimbursement', … … 243 264 show_approve = (http_request.user.has_perm('vouchers.can_approve') 244 265 and request_obj.approval_status == vouchers.models.APPROVAL_STATE_PENDING) 266 post_approve = (http_request.method == 'POST' and 'approve' in http_request.REQUEST) 267 approve_message = '' 268 approve_level = '' 245 269 if show_approve: 246 # Voucherize form 247 # Prefill from certs / config 248 initial = {} 249 initial['name'] = '%s %s' % (http_request.user.first_name, http_request.user.last_name, ) 250 if settings.SIGNATORY_EMAIL: 251 initial['email'] = settings.SIGNATORY_EMAIL 252 else: 253 initial['email'] = http_request.user.email 254 255 approve_message = '' 256 if http_request.method == 'POST' and 'approve' in http_request.REQUEST: 257 approve_form = VoucherizeForm(http_request.POST) 258 if approve_form.is_valid(): 259 request_obj.approve( 260 approver=http_request.user, 261 signatory_name=approve_form.cleaned_data['name'], 262 signatory_email=approve_form.cleaned_data['email'], 263 ) 264 approve_message = 'Created new voucher from request' 265 else: 266 approve_form = VoucherizeForm(initial=initial) 270 if not request_obj.documentation: 271 approve_message = "Documentation must be uploaded (above) before approving RFPs." 272 approve_level = 'warn' 273 elif post_approve: 274 request_obj.approve_as_rfp(approver=http_request.user) 275 approve_message = 'Queued RFP from request.' 276 approve_level = 'info' 277 elif post_approve: 278 approve_message = "You attempted to approve a reimbursement request that you could not approve. Most likely, either the voucher has already been approved, or you do not have adequate permissions." 279 approve_level = 'error' 267 280 268 281 context = { … … 271 284 'new': new, 272 285 'doc_form': doc_upload_form, 286 'show_approve': show_approve, 287 'approve_message': approve_message, 288 'approve_level': approve_level, 273 289 } 274 if show_approve:275 context['approve_form'] = approve_form276 context['approve_message'] = approve_message277 290 if show_email: 278 291 context['email_options'] = email_options … … 337 350 return response 338 351 352 # not a view 353 def generate_rfp_specs_download(http_request): 354 # Download unprocessed RFPs 355 rfps = vouchers.models.RFP.objects.all() 356 rfps = rfps.filter(rfp_number=None) 357 response = HttpResponse(mimetype='text/csv') 358 writer = csv.writer(response) 359 cols = ['id', 'name', 360 'payee.mit', 'payee.name', 361 'addr.street', 'addr.city', 'addr.state', 'addr.postal', 362 'item.date', 'item.gl', 'item.co', 'item.amoumnt', 'item.desc', 363 'documentation', 364 ] 365 writer.writerow(cols) 366 for rfp in rfps: 367 item_date = rfp.item_date.strftime("%m/%d/%Y") 368 docs = http_request.build_absolute_uri(rfp.documentation.backing_file.url) 369 writer.writerow((rfp.pk, rfp.name, 370 rfp.payee_type == 'mit', rfp.payee_name, 371 rfp.addr_street, rfp.addr_city, rfp.addr_state, rfp.addr_zip, 372 item_date, rfp.item_gl, rfp.item_co, rfp.item_amount, rfp.item_desc, 373 docs, 374 )) 375 376 # Send mail 377 tmpl = get_template('vouchers/emails/rfps_download.txt') 378 ctx = Context({ 379 'user': http_request.user, 380 'rfps': rfps, 381 }) 382 body = tmpl.render(ctx) 383 mail_admins( 384 'RFPs downloaded: %d by %s' % ( 385 len(rfps), 386 http_request.user, 387 ), 388 body, 389 ) 390 return response 391 392 def send_rfpized_email(rfp): 393 # Should send mail to the submitter, including a coversheet to print and 394 # give to SAO. The attach() method of 395 # https://docs.djangoproject.com/en/dev/topics/email/#emailmessage-objects 396 # may be useful for the coversheet. 397 tmpl = get_template('vouchers/emails/rfp_submitted.txt') 398 ctx = Context({ 399 'rfp': rfp, 400 }) 401 body = tmpl.render(ctx) 402 to = [ rr.check_to_email for rr in rfp.reimbursementrequest_set.all() ] 403 cc = [addr for name, addr in settings.ADMINS] 404 mail = EmailMessage( 405 subject="%sRFP submitted" % (settings.EMAIL_SUBJECT_PREFIX, ), 406 body=body, to=to, cc=cc, 407 ) 408 mail.send() 409 410 # not a view 411 def generate_rfp_specs_results(http_request): 412 reader = csv.DictReader(http_request) 413 rfps = vouchers.models.RFP.objects 414 time = datetime.datetime.now() 415 results = [] 416 dups = 0 417 for line in reader: 418 print line 419 rfp = rfps.get(pk=int(line['id'])) 420 if not rfp.rfp_number: 421 rfp.rfp_number = line['rfp_number'] 422 rfp.rfp_submit_time = time 423 rfp.save() 424 msg = "updated" 425 send_rfpized_email(rfp) 426 else: 427 msg = "additional number: %s" % (line['rfp_number'], ) 428 dups += 1 429 results.append((rfp, msg)) 430 431 # Send mail 432 tmpl = get_template('vouchers/emails/rfps_updated.txt') 433 ctx = Context({ 434 'user': http_request.user, 435 'results': results, 436 'dups': dups, 437 }) 438 body = tmpl.render(ctx) 439 mail_admins( 440 'RFPs created: %d (%d duplicates) by %s' % ( 441 len(results), dups, 442 http_request.user, 443 ), 444 body, 445 ) 446 # For lack of something better to return, just print the email 447 response = HttpResponse(body) 448 return response 449 450 #@user_passes_test(lambda u: u.has_perm('vouchers.generate_vouchers')) 451 @ensure_csrf_cookie 452 def generate_rfp_specs(http_request): 453 if http_request.method == 'GET': 454 return generate_rfp_specs_download(http_request) 455 elif http_request.method == 'POST': 456 # Upload RFP processing results 457 return generate_rfp_specs_results(http_request) 458 else: 459 content = "Expected GET (download unprocessed RFPs) or POST (upload results)" 460 return HttpResponseNotAllowed(["GET", "POST"], content=content) 461 339 462 def get_related_requests_qobj(user, ): 340 463 return Q(submitter=user.username) | Q(check_to_email=user.email) … … 428 551 429 552 # GENERATE THE REQUEST 430 return list_detail.object_list( 431 http_request, 432 queryset=qs, 433 extra_context={ 434 'actions' : actions, 435 'selected_ids' : list_to_keys(selected_rr_ids), 436 'action_message': apply_action_message, 437 'action_errors' : apply_action_errors, 438 'useronly': useronly, 439 'order' : order, 440 'orders' : request_list_orders, 441 'approval_status' : approval_status, 442 'approval_states': vouchers.models.APPROVAL_STATES, 443 'pagename': 'list_requests', 444 }, 445 ) 553 554 context = { 555 'object_list' : qs, 556 'actions' : actions, 557 'selected_ids' : list_to_keys(selected_rr_ids), 558 'action_message': apply_action_message, 559 'action_errors' : apply_action_errors, 560 'useronly': useronly, 561 'order' : order, 562 'orders' : request_list_orders, 563 'approval_status' : approval_status, 564 'approval_states': vouchers.models.APPROVAL_STATES, 565 'pagename': 'list_requests', 566 } 567 return render_to_response('vouchers/reimbursementrequest_list.html', context, context_instance=RequestContext(http_request), )
Note: See TracChangeset
for help on using the changeset viewer.
![(please configure the [header_logo] section in trac.ini)](/trac/chrome/site/your_project_logo.png)