source: remit/vouchers/views.py @ f332ade

client
Last change on this file since f332ade was 3e372da, checked in by Alex Dehnert <adehnert@…>, 15 years ago

Add bulk actions to request list (Trac: #4)

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