source: remit/vouchers/views.py @ feed77c

Last change on this file since feed77c was feed77c, checked in by Alex Dehnert <adehnert@…>, 10 years ago

RFP download/update process (and some tangential changes)

  • Property mode set to 100644
File size: 21.2 KB
Line 
1# -*- coding: utf-8 -*-
2import vouchers.models
3from vouchers.models import ReimbursementRequest, Documentation
4from finance_core.models import BudgetTerm, BudgetArea
5from util.shortcuts import get_403_response
6
7from django.contrib.auth.decorators import user_passes_test, login_required
8from django.shortcuts import render_to_response, get_object_or_404
9from django.template import RequestContext
10from django.http import HttpResponse, Http404, HttpResponseRedirect, HttpResponseNotAllowed
11import django.forms
12from django.forms import ChoiceField, Form, ModelForm, ModelChoiceField
13from django.core.urlresolvers import reverse
14from django.core.mail import send_mail, mail_admins, EmailMessage
15from django.db.models import Q
16from django.template import Context, Template
17from django.template.loader import get_template
18from django.views.decorators.csrf import ensure_csrf_cookie
19
20import csv
21import datetime
22import decimal
23
24import settings
25
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
110class 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
126    class Meta:
127        model = ReimbursementRequest
128        fields = (
129            'name',
130            'description',
131            'incurred_time',
132            'amount',
133            'budget_area',
134            'expense_area',
135            'check_to_first_name',
136            'check_to_last_name',
137            'check_to_email',
138            'check_to_addr',
139        )
140
141@user_passes_test(lambda u: u.is_authenticated())
142def submit_request(http_request, term, committee, recipient_type, ):
143    term_obj = get_object_or_404(BudgetTerm, slug=term)
144    comm_obj = get_object_or_404(BudgetArea, pk=committee)
145
146    new_request = ReimbursementRequest()
147    new_request.submitter = http_request.user.username
148    new_request.budget_term = term_obj
149    new_request.recipient_type = recipient_type
150
151    # Prefill from user information (itself prefilled from LDAP now)
152    initial = {}
153    initial['check_to_first_name'] = http_request.user.first_name
154    initial['check_to_last_name']  = http_request.user.last_name
155    initial['check_to_email']      = http_request.user.email
156
157    if http_request.method == 'POST': # If the form has been submitted...
158        form = RequestForm(http_request.POST, instance=new_request) # A form bound to the POST data
159        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
160
161        if form.is_valid(): # All validation rules pass
162            request_obj = form.save()
163            print "request_obj==new_request:", request_obj == new_request
164
165            # Send email
166            tmpl = get_template('vouchers/emails/request_submit_admin.txt')
167            ctx = Context({
168                'submitter': http_request.user,
169                'request': request_obj,
170            })
171            body = tmpl.render(ctx)
172            recipients = []
173            for name, addr in settings.ADMINS:
174                recipients.append(addr)
175            recipients.append(request_obj.budget_area.owner_address())
176            if settings.CC_SUBMITTER:
177                recipients.append(http_request.user.email)
178            send_mail(
179                '%sRequest submittal: %s requested $%s' % (
180                    settings.EMAIL_SUBJECT_PREFIX,
181                    http_request.user,
182                    request_obj.amount,
183                ),
184                body,
185                settings.SERVER_EMAIL,
186                recipients,
187            )
188
189            return HttpResponseRedirect(reverse(review_request, args=[new_request.pk],) + '?new=true') # Redirect after POST
190    else:
191        form = RequestForm(instance=new_request, initial=initial, ) # An unbound form
192        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
193
194    context = {
195        'term':term_obj,
196        'comm':comm_obj,
197        'request': new_request,
198        'form':form,
199        'pagename':'request_reimbursement',
200    }
201    return render_to_response('vouchers/submit.html', context, context_instance=RequestContext(http_request), )
202
203class VoucherizeForm(Form):
204    name = django.forms.CharField(max_length=100, help_text='Signatory name for voucher',)
205    email = django.forms.EmailField(max_length=100, help_text='Signatory email for voucher')
206
207
208@user_passes_test(lambda u: u.is_authenticated())
209def review_request(http_request, object_id):
210    request_obj = get_object_or_404(ReimbursementRequest, pk=object_id)
211    user = http_request.user
212    pagename = 'request_reimbursement'
213    new = False
214    if 'new' in http_request.REQUEST:
215        if http_request.REQUEST['new'].upper() == 'TRUE':
216            new = True
217        else:
218            new = False
219
220    if (user.has_perm('vouchers.can_list') or
221        user.username == request_obj.submitter or
222        user.email.upper() == request_obj.check_to_email.upper()
223        ):
224        pass
225    else:
226        return get_403_response(http_request, errmsg="You do not have permission to access this reimbursement request. You can only view requests you submitted or are the recipient for, unless you have general viewing permissions.", pagename=pagename, )
227
228    # DOCUMENTATION #
229    if request_obj.documentation:
230        doc_upload_form = None
231    else:
232        new_docs = Documentation()
233        new_docs.submitter = http_request.user.username
234        if http_request.method == 'POST' and 'upload_documentation' in http_request.REQUEST: # If the form has been submitted...
235            doc_upload_form = DocUploadForm(http_request.POST, http_request.FILES, instance=new_docs) # A form bound to the POST data
236
237            if doc_upload_form.is_valid(): # All validation rules pass
238                new_docs = doc_upload_form.save()
239                request_obj.documentation = new_docs
240                request_obj.save()
241
242                return HttpResponseRedirect(reverse(review_request, args=[object_id],)) # Redirect after POST
243        else:
244            doc_upload_form = DocUploadForm(instance=new_docs, ) # An unbound form
245
246    # SEND EMAILS
247    show_email = http_request.user.has_perm('vouchers.can_email')
248    if show_email:
249        email_message = ''
250        if http_request.method == 'POST' and 'send_email' in http_request.REQUEST:
251            mail = vouchers.models.stock_emails[http_request.REQUEST['email_name']]
252            assert mail.context == 'request'
253            mail.send_email_request(request_obj)
254            email_message = 'Sent email "%s".' % (mail.label, )
255        email_options = []
256        for mail in vouchers.models.stock_emails.values():
257            if mail.context == 'request':
258                email_options.append({
259                    'label': mail.label,
260                    'name' : mail.name,
261                })
262
263    # APPROVE VOUCHERS
264    show_approve = (http_request.user.has_perm('vouchers.can_approve')
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 = ''
269    if show_approve:
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'
280
281    context = {
282        'rr':request_obj,
283        'pagename':pagename,
284        'new': new,
285        'doc_form': doc_upload_form,
286        'show_approve': show_approve,
287        'approve_message': approve_message,
288        'approve_level': approve_level,
289    }
290    if show_email:
291        context['email_options'] = email_options
292        context['email_message'] = email_message
293    return render_to_response('vouchers/ReimbursementRequest_review.html', context, context_instance=RequestContext(http_request), )
294
295@user_passes_test(lambda u: u.has_perm('vouchers.generate_vouchers'))
296def generate_vouchers(http_request, *args):
297    unprocessed = True
298    if 'unprocessed' in http_request.REQUEST:
299        if http_request.REQUEST['unprocessed'].upper() == 'TRUE':
300            unprocessed = True
301        else:
302            unprocessed = False
303    mark = True
304    if 'mark' in http_request.REQUEST:
305        if http_request.REQUEST['mark'].upper() == 'TRUE':
306            mark = True
307        else:
308            mark = False
309
310    lst = vouchers.models.Voucher.objects.all()
311    if unprocessed:
312        lst = lst.filter(processed=False)
313
314    total = decimal.Decimal('0.00')
315    for voucher in lst:
316        total = total + voucher.amount
317
318    context = {
319        'vouchers': lst,
320        'total': total,
321        'MEDIA_ROOT': settings.MEDIA_ROOT,
322    }
323    response = render_to_response(
324        'vouchers/vouchers.tex',
325        context, context_instance=RequestContext(http_request),
326        mimetype=settings.LATEX_MIMETYPE,
327    )
328
329    # Send mail
330    tmpl = get_template('vouchers/emails/vouchers_tex.txt')
331    ctx = Context({
332        'converter': http_request.user,
333        'vouchers': lst,
334        'mark': mark,
335        'unprocessed': unprocessed,
336    })
337    body = tmpl.render(ctx)
338    mail_admins(
339        'Voucher rendering: %d by %s' % (
340            len(lst),
341            http_request.user,
342        ),
343        body,
344    )
345
346    if mark:
347        for voucher in lst:
348            voucher.mark_processed()
349
350    return response
351
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
462def get_related_requests_qobj(user, ):
463    return Q(submitter=user.username) | Q(check_to_email=user.email)
464
465request_list_orders = (
466#   Name            Label               Columns
467    ('default',     'Default',          ()),
468    ('id',          'ID',               ('id', )),
469    ('state',       'Approval Status',  ('approval_status', )),
470    ('stateamount', 'Approval Status, then amount',  ('approval_status', 'amount', )),
471    ('stateto',     'Approval Status, then recipient',  ('approval_status', 'check_to_first_name', 'check_to_last_name', )),
472    ('statesubmit', 'Approval Status, then submitter',  ('approval_status', 'submitter', )),
473    ('name',        'Request Name',     ('name', )),
474    ('amount',      'Amount',           ('amount', )),
475    ('check_to',    'Check Recipient',  ('check_to_first_name', 'check_to_last_name', )),
476    ('submitter',   'Submitter',        ('submitter', )),
477)
478
479def list_to_keys(lst):
480    dct = {}
481    for key in lst:
482        dct[key] = True
483    return dct
484
485@login_required
486def show_requests(http_request, ):
487    # BULK ACTIONS
488    actions = vouchers.models.BulkRequestAction.filter_can_only(
489        vouchers.models.bulk_request_actions,
490        http_request.user,
491    )
492    apply_action_message = None
493    apply_action_errors = []
494    if 'select' in http_request.REQUEST:
495        selected_rr_ids = [ int(item) for item in http_request.REQUEST.getlist('select') ]
496    else:
497        selected_rr_ids = []
498    if "apply-action" in http_request.POST:
499        action_name = http_request.POST['action']
500        if action_name == 'none':
501            apply_action_message = "No action selected."
502        else:
503            matching_actions = [ action for action in actions if action.name == action_name]
504            if(len(matching_actions) > 0):
505                action = matching_actions[0]
506                rrs = ReimbursementRequest.objects.filter(pk__in=selected_rr_ids)
507                for rr in rrs:
508                    success, msg = action.do(http_request, rr)
509                    if not success:
510                        apply_action_errors.append((rr, msg))
511                apply_action_message = '"%s" applied to %d request(s) (%d errors encountered)' % (action.label, len(rrs), len(apply_action_errors), )
512            else:
513                apply_action_message = "Unknown or forbidden action requested."
514
515    # PERMISSION-BASED REQUEST FILTERING
516    if http_request.user.has_perm('vouchers.can_list'):
517        qs = ReimbursementRequest.objects.all()
518        useronly = False
519    else:
520        qs = ReimbursementRequest.objects.filter(get_related_requests_qobj(http_request.user))
521        useronly = True
522
523    # SORTING
524    if 'order' in http_request.REQUEST:
525        order_row = [row for row in request_list_orders if row[0] == http_request.REQUEST['order']]
526        if order_row:
527            order, label, cols = order_row[0]
528            qs = qs.order_by(*cols)
529        else:
530            raise Http404('Order by constraint not known')
531    else:
532        order = 'default'
533
534    # DISCRETIONARY REQUEST FILTERING
535    if 'approval_status' in http_request.REQUEST:
536        approval_status = http_request.REQUEST['approval_status']
537    else:
538        approval_status = vouchers.models.APPROVAL_STATE_PENDING
539    if approval_status == 'all':
540        pass
541    else:
542        try:
543            approval_status = int(approval_status)
544        except ValueError:
545            raise Http404('approval_status poorly formatted')
546        state_row = [row for row in vouchers.models.APPROVAL_STATES if row[0] == approval_status]
547        if state_row:
548            qs = qs.filter(approval_status=approval_status)
549        else:
550            raise Http404('approval_status not known')
551
552    # GENERATE THE REQUEST
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 TracBrowser for help on using the repository browser.