source: remit/vouchers/views.py @ bc1da44

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

Django 1.6: Switch to class-based ListView?

Dango 1.5(?) removed function-based generic views, so we need to switch to the
class-based ones.

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