Changeset 1019420


Ignore:
Timestamp:
Jul 9, 2014, 12:22:14 AM (12 years ago)
Author:
Alex Dehnert <adehnert@…>
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)
Message:

Merge in some client progress to integrate with the server side

Files:
11 added
1 deleted
23 edited
1 moved

Legend:

Unmodified
Added
Removed
  • .gitignore

    r09b7c26 rc386673  
    33django-treebeard-1.1/
    44django-treebeard-1.1.tar.gz
    5 local_settings.py
    6 local_settings_after.py
     5remit/settings/local.py
     6remit/settings/local_after.py
    77database_settings
    88client/settings.py
  • client/downloader.py

    rf8a5e4b rfeed77c  
    3636
    3737def getLaTeX(br, latex_file, ):
    38     br.open(baseurl + 'vouchers/generate/')
     38    br.open(baseurl + 'vouchers/generate/vouchers.tex')
    3939    if br.viewing_html():
    4040        print br.response().get_data()
  • remit/finance_core/migrations/0004_socket_auth_password.py

    r38843de r75ccc48  
    33from south.db import db
    44from south.v2 import DataMigration
    5 from django.contrib.auth.hashers import UNUSABLE_PASSWORD
     5import django.contrib.auth.hashers
    66from django.db import models
     7from django.db.models import F
    78
    89class Migration(DataMigration):
    … …  
    1112        "Write your forwards methods here."
    1213        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)
    1429
    1530    def backwards(self, orm):
  • remit/finance_core/models.py

    r2fb5a35 r4c6a731  
    207207LAYER_CLOSEOUT    = 40
    208208layers=(
    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
    213220)
    214221def get_layer_by_name(name):
  • remit/finance_core/urls.py

    red4ccc7 rff623c3  
    1 from django.conf.urls.defaults import *
     1from django.conf.urls import patterns, include, url
    22import finance_core.views
    33
  • 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
     2import os
     3import sys
    94
    105if __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 %}
    12<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Transitional//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-transitional.dtd">
    23
    … …  
    1415  <body>
    1516    <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>
    1920        {% 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>
    2122        {% endif %}
    2223        {% 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>
    2425        {% endif %}
    2526    </ul>
    … …  
    3334            <p>Welcome, <span class='username'>{{user.username}}</span>.</p>
    3435            {% endif %}
    35             <p><a href='{% url logout %}'>Logout</a></p>
     36            <p><a href='{% url "logout" %}'>Logout</a></p>
    3637        </div>
    3738        {% endif %}
  • remit/remit_templates/index.html

    r0671644 r695d358  
    11{% extends "base.html" %}
     2{% load url from future %}
    23
    34{% block title %}Welcome to Remit{% endblock %}
    … …  
    1213<ul>
    1314    {% if user.is_anonymous %}
    14     <li><a href='{% url login %}'>Login</a></li>
     15    <li><a href='{% url "login" %}'>Login</a></li>
    1516    {% 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>
    1819    {% if perms.finance_core.use_reporting %}
    19     <li><a href="{% url reporting %}">Reporting</a></li>
     20    <li><a href="{% url "reporting" %}">Reporting</a></li>
    2021    {% endif %}
    2122    {% 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>
    2324    {% endif %}
    2425    {% if user.is_authenticated %}
    25     <li><a href='{% url logout %}'>Logout</a></li>
     26    <li><a href='{% url "logout" %}'>Logout</a></li>
    2627    {% endif %}
    2728</ul>
  • remit/remit_templates/registration/logged_out.html

    r732d9eb r695d358  
    11{% extends "base.html" %}
     2{% load url from future %}
    23
    34{% block title %}Logged out of Remit{% endblock %}
    … …  
    67<h2>Logged out of Remit</h2>
    78
    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>
    910{% endblock %}
  • remit/remit_templates/registration/login.html

    r1ba734f r695d358  
    11{% extends "base.html" %}
     2{% load url from future %}
    23
    34{% block title %}Log in to Remit{% endblock %}
    … …  
    1112{% endif %}
    1213
    13 <form method="post" action="{% url django.contrib.auth.views.login %}">
     14<form method="post" action="{% url "django.contrib.auth.views.login" %}">
    1415{% csrf_token %}
    1516<table>
  • remit/remit_templates/vouchers/ReimbursementRequest_review.html

    ra5ef930 r638b547  
    11{% extends "base.html" %}
     2{% load url from future %}
    23{% load misc %}
    34
    … …  
    9495<tr><th colspan='2'><input type='submit' name='upload_documentation' value='Upload Documentation' /></th></tr>
    9596</table>
     97{% csrf_token %}
    9698</form>
    9799{% endif %}
    … …  
    112114<input type='submit' name='send_email'  value='{{option.label}}'>
    113115<input type='hidden' name='email_name' value='{{option.name}}'>
     116{% csrf_token %}
    114117</form>
    115118{% endfor %}
    … …  
    117120{% endif %}
    118121
    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 }}
    130122
    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 %}
    137138{% endif %}
    138139
  • remit/remit_templates/vouchers/reimbursementrequest_list.html

    rf6f7aff rbc1da44  
    11{% extends "base.html" %}
     2{% load url from future %}
    23{% load misc %}
    34
    … …  
    5859    <td>
    5960        <input type='checkbox' name='select' value='{{object.pk}}'{%ifin object.pk selected_ids %} checked='checked'{%endifin%}></input>
    60         <a href='{% url review_request object.pk %}'>{{ object.pk }}</a>
     61        <a href='{% url "review_request" object.pk %}'>{{ object.pk }}</a>
    6162    </td>
    6263    <td>
    63         <em><a href='{% url review_request object.pk %}'>{{ object.name }}</a></em>
     64        <em><a href='{% url "review_request" object.pk %}'>{{ object.name }}</a></em>
    6465        {% if object.documentation %}<br /><a href='{{object.documentation.backing_file.url}}'>(view docs)</a>{%endif%}
    6566    </td>
    … …  
    139140{%endif%}
    140141{%endif%}
     142
     143{% csrf_token %}
    141144</form>
    142145
  • remit/remit_templates/vouchers/submit.html

    rf301a50 r2b31097  
    1919    <td>{{comm}}</td>
    2020</tr>
     21<tr>
     22    <th>Recipient type</th>
     23    <td>{{request.get_recipient_type_display}}</td>
     24</tr>
    2125
    2226<tr class='sect-head'>
  • remit/settings/__init__.py

    rf5bebab rfeed77c  
    33import sys
    44
    5 SITE_ROOT = os.path.normpath(os.path.dirname(__file__))
     5SITE_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), '..'))
    66SITE_WEB_PATH = ''
    77DEFAULT_DOMAIN = 'mit.edu'
    … …  
    1010TEMPLATE_DEBUG = DEBUG
    1111
     12ATOMIC_REQUESTS = True
     13
    1214ADMINS = (
    1315    ('Remit team', 'remit-default-addr@mit.edu'),
    1416)
    1517SERVER_EMAIL = 'remit-default-addr@mit.edu'
     18DEFAULT_FROM_EMAIL = 'remit-default-addr@mit.edu'
    1619
    1720GROUP_NAME = 'Remit'
    1821GROUP_ABBR = 'RM'
    19 SIGNATORY_EMAIL = None
     22SIGNATORY_EMAIL = None # used only for paper vouchers (which SAO killed)
    2023
    2124CC_SUBMITTER = False
    … …  
    2326MANAGERS = ADMINS
    2427
     28# When changing this, be aware of uses of the budgetarea_pathstr filter
    2529BASE_COMMITTEE_PATH = ['Accounts', 'Assets', ]
    2630COMMITTEE_HIERARCHY_LEVELS = 2
    … …  
    5357USE_I18N = True
    5458
    55 from local_settings import *
     59# Required settings
     60# SITE_URL_BASE: used to construct absolute URLs for use in emails
     61
     62from local import *
    5663
    5764# Absolute path to the directory that holds media.
    … …  
    8592    'django.contrib.sessions.middleware.SessionMiddleware',
    8693    'django.contrib.auth.middleware.AuthenticationMiddleware',
    87     'django.middleware.transaction.TransactionMiddleware',
    8894    'django.middleware.csrf.CsrfViewMiddleware',
    8995    'django.contrib.messages.middleware.MessageMiddleware',
    … …  
    116122    'django.contrib.sessions',
    117123    'django.contrib.sites',
     124    'django.contrib.staticfiles',
    118125    'treebeard',
    119126    'south',
    … …  
    126133USER_EMAIL_SIGNATURE = "%s Treasury" % (GROUP_NAME,)
    127134
    128 from local_settings_after import *
     135from local_after import *
  • remit/urls.py

    rf5bebab r21360c8  
    1 from django.conf.urls.defaults import *
     1from django.conf.urls import patterns, include, url
    22from django.contrib.auth.views import login, logout
    33
    … …  
    66# Necessary views
    77import finance_core.views
     8from util.shortcuts import TemplateViewWithContext
    89
    910# Uncomment the next two lines to enable the admin:
    … …  
    1314urlpatterns = patterns('',
    1415    # 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'),
    1617    (r'^vouchers/', include('vouchers.urls')),
    1718    (r'^finance_core/', include('finance_core.urls')),
  • remit/util/add_accounts

    r8132e8c rf298bd7  
    1 #!/usr/bin/python
     1#!/usr/bin/env python
    22import sys
    33import os
  • remit/util/add_gl_accounts.py

    r0df317c rf298bd7  
    1 #!/usr/bin/python
     1#!/usr/bin/env python
    22
    33import sys
  • remit/util/setup.py

    ra1e4224 rf298bd7  
    1 #!/usr/bin/python
     1#!/usr/bin/env python
    22import sys
    33import os
  • remit/util/shortcuts.py

    rf7dd5e7 r5f82b6c  
    22from django.template import RequestContext, Template
    33from django.template.loader import get_template
     4from django.views.generic.base import TemplateView
     5from django.views.generic.list import ListView
    46
    57def get_403_response(request, errmsg=None, **extra_context):
    … …  
    810    page = tmpl.render(ctx, )
    911    return HttpResponseForbidden(page)
     12
     13
     14class 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/python
    21# Inspired by and partially copied from ESP's backslash stuff
    32
  • remit/vouchers/admin.py

    r9102ac9 rfeed77c  
    2727    list_display_links = ('description', )
    2828
     29class 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
    2933admin.site.register(vouchers.models.ReimbursementRequest, ReimbursementRequestAdmin)
    3034admin.site.register(vouchers.models.Voucher, VoucherAdmin)
     35admin.site.register(vouchers.models.RFP, RFPAdmin)
  • remit/vouchers/models.py

    r42b287c rfeed77c  
    1010
    1111import datetime
     12import re
    1213
    1314APPROVAL_STATE_PENDING = 0
    … …  
    2021)
    2122
     23recipient_type_choices = (
     24    ('mit', 'MIT student, faculty, or staff', ),
     25    ('other', 'Other, including alumnus or affiliate', ),
     26)
    2227class ReimbursementRequest(models.Model):
    2328    submitter = models.CharField(max_length=30) # Username of submitter
     29    recipient_type = models.CharField(choices=recipient_type_choices, max_length=10, default='other')
    2430    check_to_first_name = models.CharField(max_length=50, verbose_name="check recipient's first name")
    2531    check_to_last_name = models.CharField(max_length=50, verbose_name="check recipient's last name")
    … …  
    95101    def convert_to_rfp(self, ):
    96102        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
    97114        rfp.save()
    98115        self.create_transfers()
    … …  
    102119        self.save()
    103120
    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, ):
    105139        """Mark a request as approved.
    106140
    … …  
    110144        """
    111145        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')
    126147
    127148    def approve_as_rfp(self, approver, ):
    128149        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')
    144151
    145152    def review_link(self, ):
    … …  
    162169    description = models.TextField()
    163170    gl = models.IntegerField()
    164     processed = models.BooleanField()
     171    processed = models.BooleanField(default=False)
    165172    process_time = models.DateTimeField(blank=True, null=True,)
    166     documentation = models.ForeignKey('Documentation', blank=True, null=True, )
     173    documentation = models.ForeignKey('Documentation', null=True, )
    167174
    168175    def mailing_addr_lines(self):
    … …  
    196203
    197204class 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
    198210    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")
    199237
    200238    def __unicode__(self, ):
    201239        return "RFP: %s" % (self.create_time.strftime(settings.SHORT_DATETIME_FORMAT_F), )
     240
     241    class Meta:
     242        verbose_name = "RFP"
    202243
    203244class Documentation(models.Model):
    … …  
    316357        return [ action for action in actions if action.can(user) ]
    317358
    318 def bulk_action_approve(http_request, rr):
     359def bulk_action_approve_as_voucher(http_request, rr):
    319360    approver = http_request.user
    320361    signatory_name = http_request.user.get_full_name()
    … …  
    322363        return False, "already approved"
    323364    else:
    324         rr.approve(approver, signatory_name)
     365        rr.approve_as_voucher(approver, signatory_name)
    325366        return True, "request approved"
    326367
    … …  
    347388if settings.SIGNATORY_EMAIL:
    348389    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,
    352393        perm_predicate=perm_checker('vouchers.can_approve'),
    353394    ))
    354395bulk_request_actions.append(BulkRequestAction(
    355396    name='approve_as_rfp',
    356     label='Mark Requests as RFPized',
     397    label='Approve requests (for RFPizing)',
    357398    action=bulk_action_approve_as_rfp,
    358399    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
     1from django.conf.urls import patterns, include, url
    32from django.contrib.auth.decorators import permission_required
    43import vouchers.views
    … …  
    87    url(r'list/', vouchers.views.show_requests, name='list_requests', ),
    98    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', ),
    1110    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', ),
    1313)
  • remit/vouchers/views.py

    r9dee7f6 rfeed77c  
     1# -*- coding: utf-8 -*-
    12import vouchers.models
    23from vouchers.models import ReimbursementRequest, Documentation
    … …  
    78from django.shortcuts import render_to_response, get_object_or_404
    89from django.template import RequestContext
    9 from django.http import Http404, HttpResponseRedirect
     10from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseNotAllowed
    1011import django.forms
    11 from django.forms import Form
    12 from django.forms import ModelForm
    13 from django.forms import ModelChoiceField
     12from django.forms import ChoiceField, Form, ModelForm, ModelChoiceField
    1413from django.core.urlresolvers import reverse
    15 from django.core.mail import send_mail, mail_admins
     14from django.core.mail import send_mail, mail_admins, EmailMessage
    1615from django.db.models import Q
    1716from django.template import Context, Template
    1817from django.template.loader import get_template
    19 from django.views.generic import list_detail
    20 
     18from django.views.decorators.csrf import ensure_csrf_cookie
     19
     20import csv
     21import datetime
    2122import decimal
    2223
    2324import settings
    2425
     26
     27class 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
     42class SelectRequestBasicsForm(Form):
     43    area = CommitteesField()
     44    term = ModelChoiceField(queryset = BudgetTerm.objects.all())
     45    recipient_type = ChoiceField(choices=vouchers.models.recipient_type_choices)
     46
     47class 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())
     68def 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
     87class 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
     98class 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
    25110class 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
    26126    class Meta:
    27127        model = ReimbursementRequest
    … …  
    39139        )
    40140
    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.depth
    46         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_type
    66         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 f
    72 
    73     class Meta:
    74         model = Documentation
    75         fields = (
    76             'label',
    77             'backing_file',
    78         )
    79 
    80 
    81141@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):
     142def submit_request(http_request, term, committee, recipient_type, ):
    123143    term_obj = get_object_or_404(BudgetTerm, slug=term)
    124144    comm_obj = get_object_or_404(BudgetArea, pk=committee)
    … …  
    127147    new_request.submitter = http_request.user.username
    128148    new_request.budget_term = term_obj
     149    new_request.recipient_type = recipient_type
    129150
    130151    # Prefill from user information (itself prefilled from LDAP now)
    … …  
    137158        form = RequestForm(http_request.POST, instance=new_request) # A form bound to the POST data
    138159        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
    139         form.fields['expense_area'] = ExpenseAreasField()
    140160
    141161        if form.is_valid(): # All validation rules pass
    142162            request_obj = form.save()
     163            print "request_obj==new_request:", request_obj == new_request
    143164
    144165            # Send email
    … …  
    170191        form = RequestForm(instance=new_request, initial=initial, ) # An unbound form
    171192        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
    172         form.fields['expense_area'] = ExpenseAreasField()
    173193
    174194    context = {
    175195        'term':term_obj,
    176196        'comm':comm_obj,
     197        'request': new_request,
    177198        'form':form,
    178199        'pagename':'request_reimbursement',
    … …  
    243264    show_approve = (http_request.user.has_perm('vouchers.can_approve')
    244265        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 = ''
    245269    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'
    267280
    268281    context = {
    … …  
    271284        'new': new,
    272285        'doc_form': doc_upload_form,
     286        'show_approve': show_approve,
     287        'approve_message': approve_message,
     288        'approve_level': approve_level,
    273289    }
    274     if show_approve:
    275         context['approve_form'] = approve_form
    276         context['approve_message'] = approve_message
    277290    if show_email:
    278291        context['email_options'] = email_options
    … …  
    337350    return response
    338351
     352# not a view
     353def 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
     392def 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
     411def 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
     452def 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
    339462def get_related_requests_qobj(user, ):
    340463    return Q(submitter=user.username) | Q(check_to_email=user.email)
    … …  
    428551
    429552    # 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.