Changeset 45a52c6


Ignore:
Timestamp:
Jul 14, 2014, 1:21:16 AM (10 years ago)
Author:
Alex Dehnert <adehnert@…>
Branches:
master
Parents:
4439c52 (diff), 3b7f125 (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/14/14 01:21:16)
git-committer:
Alex Dehnert <adehnert@…> (07/14/14 01:21:16)
Message:

Add support for RFPs

Merge branch 'rfp'

Files:
8 added
10 edited

Legend:

Unmodified
Added
Removed
  • INSTALL.rst

    rcabf7ac r4f2c77a  
    3939    - ``ADMINS``
    4040    - Database configuration
    41     - ``SERVER_EMAIL``
     41    - ``SERVER_EMAIL`` and ``DEFAULT_FROM_EMAIL``
    4242    - ``SITE_URL_BASE`` -- full URL (including protocol and hostname; used in emails)
    4343    - Any other settings you want
     
    53537.  Use the admin interface to set up appropriate BudgetTerm(s)
    54548.  Make sure that the remit/media/ directory gets served to the web as media
     55
     56Post-setup notes
     57----------------
     58
     59- By default, the "downloader" user is inactive and has no password. To use it, you will need to make it active, set a password, and configure your client (see ``client/README`` for details).
  • client/downloader.py

    rf8a5e4b r952d9fb  
    1 #!/usr/bin/python
     1#!/usr/bin/env python
    22
    33import os
     
    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/remit_templates/vouchers/ReimbursementRequest_review.html

    r0445831 r03aae73  
    8686
    8787{% if doc_form %}
    88 <h3>(Optional) Upload Documentation</h3>
     88<h3>Upload Documentation</h3>
    8989
    90 <p>If you ordered online, you may wish to upload documentation instead of providing a physical copy.</p>
     90<p>SAO requires that all RFPs be submitted with electronic documentation. If you ordered online, you can just upload a PDF of your purchase details. For in-person purchases, you should upload a scan of the receipt. Be sure documentation includes both itemization and proof of payment.</p>
    9191
    9292<form enctype="multipart/form-data" method="post" action="">
     
    120120{% endif %}
    121121
    122 {% if approve_form %}
    123 <h3>Approve this reimbursement request:</h3>
    124 {% if approve_message %}
    125 <p>{{ approve_message }}</p>
    126 {% else %}
    127 {% if perms.vouchers.change_reimbursementrequest %}
    128 <p>Optionally, <a href='{% url "admin:vouchers_reimbursementrequest_change" rr.pk %}'>change this request first</a>.</p>
    129 {% endif %}
    130 <form method="post" action="">
    131 <table class='pretty-table'>
    132 {{ approve_form.as_table }}
    133122
    134 <tr>
    135     <th colspan='2'><input type="submit" name="approve" value="Approve" /></td>
    136 </tr>
    137 </table>
    138 {% csrf_token %}
    139 </form>
    140 {% 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 %}
    141138{% endif %}
    142139
  • 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

    r7a292ed rfeed77c  
    1616)
    1717SERVER_EMAIL = 'remit-default-addr@mit.edu'
     18DEFAULT_FROM_EMAIL = 'remit-default-addr@mit.edu'
    1819
    1920GROUP_NAME = 'Remit'
    2021GROUP_ABBR = 'RM'
    21 SIGNATORY_EMAIL = None
     22SIGNATORY_EMAIL = None # used only for paper vouchers (which SAO killed)
    2223
    2324CC_SUBMITTER = False
  • 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

    rff623c3 rfeed77c  
    77    url(r'list/', vouchers.views.show_requests, name='list_requests', ),
    88    url(r'reimbursement/', 'vouchers.views.select_request_basics', name='request_reimbursement', ),
    9     (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', ),
    1010    url(r'review/(?P<object_id>\d+)/', 'vouchers.views.review_request', name='review_request', ),
    11     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', ),
    1213)
  • remit/vouchers/views.py

    r4439c52 rdb5be5d  
    88from django.shortcuts import render_to_response, get_object_or_404
    99from django.template import RequestContext
    10 from django.http import Http404, HttpResponseRedirect
     10from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseNotAllowed
    1111import django.forms
    12 from django.forms import Form
    13 from django.forms import ModelForm
    14 from django.forms import ModelChoiceField
     12from django.forms import ChoiceField, Form, ModelForm, ModelChoiceField
    1513from django.core.urlresolvers import reverse
    16 from django.core.mail import send_mail, mail_admins
     14from django.core.mail import send_mail, mail_admins, EmailMessage
    1715from django.db.models import Q
    1816from django.template import Context, Template
    1917from django.template.loader import get_template
    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@login_required
     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(u"Only PDF files are accepted – you submitted a .%s file" % (ext, ))
    68         elif contenttype != 'application/pdf':
    69             raise django.forms.ValidationError(u"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 
    81 @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):
     141@login_required
     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',
     
    185206
    186207
    187 @user_passes_test(lambda u: u.is_authenticated())
     208@login_required
    188209def review_request(http_request, object_id):
    189210    request_obj = get_object_or_404(ReimbursementRequest, pk=object_id)
     
    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.amount', '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, mimetype='text/plain')
     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)
  • setup.py

    rd604f81 r9a12b18  
    66    packages = ["remit"],
    77    install_requires = [
    8         "pysapweb",
     8        # Server
    99        "django",
    1010        "django-treebeard",
     
    1313        # on Ubuntu, try apt-get build-dep python-ldap
    1414        "python-ldap"
     15
     16        # Client
     17        "pysapweb",     # SAPWeb
     18        "mechanize",    # site->client downloader (for SAPWeb or vouchers)
    1519    ],
    1620
Note: See TracChangeset for help on using the changeset viewer.