source: remit/vouchers/views.py @ 2b31097

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

WIP: gather info needed for RFPs

  • Property mode set to 100644
File size: 17.6 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 Http404, HttpResponseRedirect
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
15from django.db.models import Q
16from django.template import Context, Template
17from django.template.loader import get_template
18
19import decimal
20
21import settings
22
23
24class CommitteesField(ModelChoiceField):
25    def __init__(self, *args, **kargs):
26        base_area = BudgetArea.get_by_path(settings.BASE_COMMITTEE_PATH)
27        self.strip_levels = base_area.depth
28        areas = (base_area.get_descendants()
29            .filter(depth__lte=base_area.depth+settings.COMMITTEE_HIERARCHY_LEVELS)
30            .exclude(name='Holding')
31        )
32        ModelChoiceField.__init__(self, queryset=areas,
33            help_text='Select the appropriate committe or other budget area',
34            *args, **kargs)
35
36    def label_from_instance(self, obj,):
37        return obj.indented_name(strip_levels=self.strip_levels)
38
39class SelectRequestBasicsForm(Form):
40    area = CommitteesField()
41    term = ModelChoiceField(queryset = BudgetTerm.objects.all())
42    recipient_type = ChoiceField(choices=vouchers.models.recipient_type_choices)
43
44class DocUploadForm(ModelForm):
45    def clean_backing_file(self, ):
46        f = self.cleaned_data['backing_file']
47        ext = f.name.rsplit('.')[-1]
48        contenttype = f.content_type
49        if ext != 'pdf':
50            raise django.forms.ValidationError(u"Only PDF files are accepted – you submitted a .%s file" % (ext, ))
51        elif contenttype != 'application/pdf':
52            raise django.forms.ValidationError(u"Only PDF files are accepted – you submitted a %s file" % (contenttype, ))
53        else:
54            return f
55
56    class Meta:
57        model = Documentation
58        fields = (
59            'label',
60            'backing_file',
61        )
62
63
64@user_passes_test(lambda u: u.is_authenticated())
65def select_request_basics(http_request, ):
66    if http_request.method == 'POST': # If the form has been submitted...
67        form = SelectRequestBasicsForm(http_request.POST) # A form bound to the POST data
68        if form.is_valid(): # All validation rules pass
69            term = form.cleaned_data['term'].slug
70            area = form.cleaned_data['area'].id
71            recipient_type = form.cleaned_data['recipient_type']
72            url = reverse(submit_request, args=[term, area, recipient_type],)
73            return HttpResponseRedirect(url) # Redirect after POST
74    else:
75        form = SelectRequestBasicsForm() # An unbound form
76
77    context = {
78        'form':form,
79        'pagename':'request_reimbursement',
80    }
81    return render_to_response('vouchers/select.html', context, context_instance=RequestContext(http_request), )
82
83
84class CommitteeBudgetAreasField(ModelChoiceField):
85    def __init__(self, base_area, *args, **kargs):
86        self.strip_levels = base_area.depth
87        areas = base_area.get_descendants()
88        ModelChoiceField.__init__(self, queryset=areas,
89            help_text='In general, this should be a fully indented budget area, not one with children',
90            *args, **kargs)
91
92    def label_from_instance(self, obj,):
93        return obj.indented_name(strip_levels=self.strip_levels)
94
95class ExpenseAreasField(ModelChoiceField):
96    def __init__(self, *args, **kargs):
97        base_area = vouchers.models.BudgetArea.get_by_path(['Accounts', 'Expenses'])
98        self.strip_levels = base_area.depth
99        areas = base_area.get_descendants()
100        ModelChoiceField.__init__(self, queryset=areas,
101            help_text='In general, this should be a fully indented budget area, not one with children',
102            *args, **kargs)
103
104    def label_from_instance(self, obj,):
105        return obj.indented_name(strip_levels=self.strip_levels)
106
107class RequestForm(ModelForm):
108    expense_area = ExpenseAreasField()
109
110    def __init__(self, *args, **kwargs):
111        super(RequestForm, self).__init__(*args, **kwargs)
112        if self.instance.recipient_type == 'mit':
113            addr_widget = django.forms.HiddenInput()
114            self.fields['check_to_addr'].widget = addr_widget
115        else:
116            rfp = vouchers.models.RFP
117            error_msgs = dict(invalid=rfp.addr_error)
118            addr_widget = self.fields['check_to_addr'].widget
119            addr_field = django.forms.RegexField(regex=rfp.addr_regex, error_messages=error_msgs)
120            addr_field.widget = addr_widget
121            self.fields['check_to_addr'] = addr_field
122
123    class Meta:
124        model = ReimbursementRequest
125        fields = (
126            'name',
127            'description',
128            'incurred_time',
129            'amount',
130            'budget_area',
131            'expense_area',
132            'check_to_first_name',
133            'check_to_last_name',
134            'check_to_email',
135            'check_to_addr',
136        )
137
138@user_passes_test(lambda u: u.is_authenticated())
139def submit_request(http_request, term, committee, recipient_type, ):
140    term_obj = get_object_or_404(BudgetTerm, slug=term)
141    comm_obj = get_object_or_404(BudgetArea, pk=committee)
142
143    new_request = ReimbursementRequest()
144    new_request.submitter = http_request.user.username
145    new_request.budget_term = term_obj
146    new_request.recipient_type = recipient_type
147
148    # Prefill from user information (itself prefilled from LDAP now)
149    initial = {}
150    initial['check_to_first_name'] = http_request.user.first_name
151    initial['check_to_last_name']  = http_request.user.last_name
152    initial['check_to_email']      = http_request.user.email
153
154    if http_request.method == 'POST': # If the form has been submitted...
155        form = RequestForm(http_request.POST, instance=new_request) # A form bound to the POST data
156        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
157
158        if form.is_valid(): # All validation rules pass
159            request_obj = form.save()
160            print "request_obj==new_request:", request_obj == new_request
161
162            # Send email
163            tmpl = get_template('vouchers/emails/request_submit_admin.txt')
164            ctx = Context({
165                'submitter': http_request.user,
166                'request': request_obj,
167            })
168            body = tmpl.render(ctx)
169            recipients = []
170            for name, addr in settings.ADMINS:
171                recipients.append(addr)
172            recipients.append(request_obj.budget_area.owner_address())
173            if settings.CC_SUBMITTER:
174                recipients.append(http_request.user.email)
175            send_mail(
176                '%sRequest submittal: %s requested $%s' % (
177                    settings.EMAIL_SUBJECT_PREFIX,
178                    http_request.user,
179                    request_obj.amount,
180                ),
181                body,
182                settings.SERVER_EMAIL,
183                recipients,
184            )
185
186            return HttpResponseRedirect(reverse(review_request, args=[new_request.pk],) + '?new=true') # Redirect after POST
187    else:
188        form = RequestForm(instance=new_request, initial=initial, ) # An unbound form
189        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
190
191    context = {
192        'term':term_obj,
193        'comm':comm_obj,
194        'request': new_request,
195        'form':form,
196        'pagename':'request_reimbursement',
197    }
198    return render_to_response('vouchers/submit.html', context, context_instance=RequestContext(http_request), )
199
200class VoucherizeForm(Form):
201    name = django.forms.CharField(max_length=100, help_text='Signatory name for voucher',)
202    email = django.forms.EmailField(max_length=100, help_text='Signatory email for voucher')
203
204
205@user_passes_test(lambda u: u.is_authenticated())
206def review_request(http_request, object_id):
207    request_obj = get_object_or_404(ReimbursementRequest, pk=object_id)
208    user = http_request.user
209    pagename = 'request_reimbursement'
210    new = False
211    if 'new' in http_request.REQUEST:
212        if http_request.REQUEST['new'].upper() == 'TRUE':
213            new = True
214        else:
215            new = False
216
217    if (user.has_perm('vouchers.can_list') or
218        user.username == request_obj.submitter or
219        user.email.upper() == request_obj.check_to_email.upper()
220        ):
221        pass
222    else:
223        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, )
224
225    # DOCUMENTATION #
226    if request_obj.documentation:
227        doc_upload_form = None
228    else:
229        new_docs = Documentation()
230        new_docs.submitter = http_request.user.username
231        if http_request.method == 'POST' and 'upload_documentation' in http_request.REQUEST: # If the form has been submitted...
232            doc_upload_form = DocUploadForm(http_request.POST, http_request.FILES, instance=new_docs) # A form bound to the POST data
233
234            if doc_upload_form.is_valid(): # All validation rules pass
235                new_docs = doc_upload_form.save()
236                request_obj.documentation = new_docs
237                request_obj.save()
238
239                return HttpResponseRedirect(reverse(review_request, args=[object_id],)) # Redirect after POST
240        else:
241            doc_upload_form = DocUploadForm(instance=new_docs, ) # An unbound form
242
243    # SEND EMAILS
244    show_email = http_request.user.has_perm('vouchers.can_email')
245    if show_email:
246        email_message = ''
247        if http_request.method == 'POST' and 'send_email' in http_request.REQUEST:
248            mail = vouchers.models.stock_emails[http_request.REQUEST['email_name']]
249            assert mail.context == 'request'
250            mail.send_email_request(request_obj)
251            email_message = 'Sent email "%s".' % (mail.label, )
252        email_options = []
253        for mail in vouchers.models.stock_emails.values():
254            if mail.context == 'request':
255                email_options.append({
256                    'label': mail.label,
257                    'name' : mail.name,
258                })
259
260    # APPROVE VOUCHERS
261    show_approve = (http_request.user.has_perm('vouchers.can_approve')
262        and request_obj.approval_status == vouchers.models.APPROVAL_STATE_PENDING)
263    if show_approve:
264        # Voucherize form
265        # Prefill from certs / config
266        initial = {}
267        initial['name'] = '%s %s' % (http_request.user.first_name, http_request.user.last_name, )
268        if settings.SIGNATORY_EMAIL:
269            initial['email'] = settings.SIGNATORY_EMAIL
270        else:
271            initial['email'] = http_request.user.email
272
273        approve_message = ''
274        if http_request.method == 'POST' and 'approve' in http_request.REQUEST:
275            approve_form = VoucherizeForm(http_request.POST)
276            if approve_form.is_valid():
277                request_obj.approve(
278                    approver=http_request.user,
279                    signatory_name=approve_form.cleaned_data['name'],
280                    signatory_email=approve_form.cleaned_data['email'],
281                )
282                approve_message = 'Created new voucher from request'
283        else:
284            approve_form = VoucherizeForm(initial=initial)
285
286    context = {
287        'rr':request_obj,
288        'pagename':pagename,
289        'new': new,
290        'doc_form': doc_upload_form,
291    }
292    if show_approve:
293        context['approve_form'] = approve_form
294        context['approve_message'] = approve_message
295    if show_email:
296        context['email_options'] = email_options
297        context['email_message'] = email_message
298    return render_to_response('vouchers/ReimbursementRequest_review.html', context, context_instance=RequestContext(http_request), )
299
300@user_passes_test(lambda u: u.has_perm('vouchers.generate_vouchers'))
301def generate_vouchers(http_request, *args):
302    unprocessed = True
303    if 'unprocessed' in http_request.REQUEST:
304        if http_request.REQUEST['unprocessed'].upper() == 'TRUE':
305            unprocessed = True
306        else:
307            unprocessed = False
308    mark = True
309    if 'mark' in http_request.REQUEST:
310        if http_request.REQUEST['mark'].upper() == 'TRUE':
311            mark = True
312        else:
313            mark = False
314
315    lst = vouchers.models.Voucher.objects.all()
316    if unprocessed:
317        lst = lst.filter(processed=False)
318
319    total = decimal.Decimal('0.00')
320    for voucher in lst:
321        total = total + voucher.amount
322
323    context = {
324        'vouchers': lst,
325        'total': total,
326        'MEDIA_ROOT': settings.MEDIA_ROOT,
327    }
328    response = render_to_response(
329        'vouchers/vouchers.tex',
330        context, context_instance=RequestContext(http_request),
331        mimetype=settings.LATEX_MIMETYPE,
332    )
333
334    # Send mail
335    tmpl = get_template('vouchers/emails/vouchers_tex.txt')
336    ctx = Context({
337        'converter': http_request.user,
338        'vouchers': lst,
339        'mark': mark,
340        'unprocessed': unprocessed,
341    })
342    body = tmpl.render(ctx)
343    mail_admins(
344        'Voucher rendering: %d by %s' % (
345            len(lst),
346            http_request.user,
347        ),
348        body,
349    )
350
351    if mark:
352        for voucher in lst:
353            voucher.mark_processed()
354
355    return response
356
357def get_related_requests_qobj(user, ):
358    return Q(submitter=user.username) | Q(check_to_email=user.email)
359
360request_list_orders = (
361#   Name            Label               Columns
362    ('default',     'Default',          ()),
363    ('id',          'ID',               ('id', )),
364    ('state',       'Approval Status',  ('approval_status', )),
365    ('stateamount', 'Approval Status, then amount',  ('approval_status', 'amount', )),
366    ('stateto',     'Approval Status, then recipient',  ('approval_status', 'check_to_first_name', 'check_to_last_name', )),
367    ('statesubmit', 'Approval Status, then submitter',  ('approval_status', 'submitter', )),
368    ('name',        'Request Name',     ('name', )),
369    ('amount',      'Amount',           ('amount', )),
370    ('check_to',    'Check Recipient',  ('check_to_first_name', 'check_to_last_name', )),
371    ('submitter',   'Submitter',        ('submitter', )),
372)
373
374def list_to_keys(lst):
375    dct = {}
376    for key in lst:
377        dct[key] = True
378    return dct
379
380@login_required
381def show_requests(http_request, ):
382    # BULK ACTIONS
383    actions = vouchers.models.BulkRequestAction.filter_can_only(
384        vouchers.models.bulk_request_actions,
385        http_request.user,
386    )
387    apply_action_message = None
388    apply_action_errors = []
389    if 'select' in http_request.REQUEST:
390        selected_rr_ids = [ int(item) for item in http_request.REQUEST.getlist('select') ]
391    else:
392        selected_rr_ids = []
393    if "apply-action" in http_request.POST:
394        action_name = http_request.POST['action']
395        if action_name == 'none':
396            apply_action_message = "No action selected."
397        else:
398            matching_actions = [ action for action in actions if action.name == action_name]
399            if(len(matching_actions) > 0):
400                action = matching_actions[0]
401                rrs = ReimbursementRequest.objects.filter(pk__in=selected_rr_ids)
402                for rr in rrs:
403                    success, msg = action.do(http_request, rr)
404                    if not success:
405                        apply_action_errors.append((rr, msg))
406                apply_action_message = '"%s" applied to %d request(s) (%d errors encountered)' % (action.label, len(rrs), len(apply_action_errors), )
407            else:
408                apply_action_message = "Unknown or forbidden action requested."
409
410    # PERMISSION-BASED REQUEST FILTERING
411    if http_request.user.has_perm('vouchers.can_list'):
412        qs = ReimbursementRequest.objects.all()
413        useronly = False
414    else:
415        qs = ReimbursementRequest.objects.filter(get_related_requests_qobj(http_request.user))
416        useronly = True
417
418    # SORTING
419    if 'order' in http_request.REQUEST:
420        order_row = [row for row in request_list_orders if row[0] == http_request.REQUEST['order']]
421        if order_row:
422            order, label, cols = order_row[0]
423            qs = qs.order_by(*cols)
424        else:
425            raise Http404('Order by constraint not known')
426    else:
427        order = 'default'
428
429    # DISCRETIONARY REQUEST FILTERING
430    if 'approval_status' in http_request.REQUEST:
431        approval_status = http_request.REQUEST['approval_status']
432    else:
433        approval_status = vouchers.models.APPROVAL_STATE_PENDING
434    if approval_status == 'all':
435        pass
436    else:
437        try:
438            approval_status = int(approval_status)
439        except ValueError:
440            raise Http404('approval_status poorly formatted')
441        state_row = [row for row in vouchers.models.APPROVAL_STATES if row[0] == approval_status]
442        if state_row:
443            qs = qs.filter(approval_status=approval_status)
444        else:
445            raise Http404('approval_status not known')
446
447    # GENERATE THE REQUEST
448
449    context = {
450        'object_list' : qs,
451        'actions' : actions,
452        'selected_ids'  : list_to_keys(selected_rr_ids),
453        'action_message': apply_action_message,
454        'action_errors' : apply_action_errors,
455        'useronly': useronly,
456        'order'   : order,
457        'orders'  : request_list_orders,
458        'approval_status' : approval_status,
459        'approval_states':  vouchers.models.APPROVAL_STATES,
460        'pagename': 'list_requests',
461    }
462    return render_to_response('vouchers/reimbursementrequest_list.html', context, context_instance=RequestContext(http_request), )
Note: See TracBrowser for help on using the repository browser.