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
RevLine 
[269db50]1import vouchers.models
[dc17b01]2from vouchers.models import ReimbursementRequest, Documentation
[3e79308]3from finance_core.models import BudgetTerm, BudgetArea
[f7dd5e7]4from util.shortcuts import get_403_response
[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
[6e43384]19from django.views.generic import list_detail
[269db50]20
[71ca9e6]21import decimal
22
[587bb95]23import settings
24
[6b8d891]25class RequestForm(ModelForm):
26    class Meta:
27        model = ReimbursementRequest
28        fields = (
29            'name',
30            'description',
[82211ea]31            'incurred_time',
[6b8d891]32            'amount',
33            'budget_area',
[f6c7295]34            'expense_area',
[248b30b]35            'check_to_first_name',
36            'check_to_last_name',
[6b8d891]37            'check_to_email',
38            'check_to_addr',
39        )
40
[82211ea]41
[70ce03a]42class CommitteesField(ModelChoiceField):
43    def __init__(self, *args, **kargs):
[6f24604]44        base_area = BudgetArea.get_by_path(settings.BASE_COMMITTEE_PATH)
[70ce03a]45        self.strip_levels = base_area.depth
46        areas = (base_area.get_descendants()
[857256d]47            .filter(depth__lte=base_area.depth+settings.COMMITTEE_HIERARCHY_LEVELS)
[70ce03a]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):
[17193ee]58    area = CommitteesField()
[70ce03a]59    term = ModelChoiceField(queryset = BudgetTerm.objects.all())
60
[dc17b01]61class DocUploadForm(ModelForm):
[0a5a003]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
[dc17b01]73    class Meta:
74        model = Documentation
75        fields = (
76            'label',
77            'backing_file',
78        )
79
80
[70ce03a]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,
[3a0c51b]94        'pagename':'request_reimbursement',
[70ce03a]95    }
[fedcbcf]96    return render_to_response('vouchers/select.html', context, context_instance=RequestContext(http_request), )
[70ce03a]97
98class CommitteeBudgetAreasField(ModelChoiceField):
[6b8d891]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
[f6c7295]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
[6b8d891]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
[37c15c4]130    # Prefill from user information (itself prefilled from LDAP now)
[e2f2aa9]131    initial = {}
[37c15c4]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
[e2f2aa9]135
[6b8d891]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
[70ce03a]138        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
[f6c7295]139        form.fields['expense_area'] = ExpenseAreasField()
[dc17b01]140
[6b8d891]141        if form.is_valid(): # All validation rules pass
[0e58ad0]142            request_obj = form.save()
143
144            # Send email
[3bf063c]145            tmpl = get_template('vouchers/emails/request_submit_admin.txt')
[0e58ad0]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())
[ff0ea05]155            if settings.CC_SUBMITTER:
156                recipients.append(http_request.user.email)
[0e58ad0]157            send_mail(
[14adb6b]158                '%sRequest submittal: %s requested $%s' % (
159                    settings.EMAIL_SUBJECT_PREFIX,
[0e58ad0]160                    http_request.user,
161                    request_obj.amount,
162                ),
163                body,
164                settings.SERVER_EMAIL,
165                recipients,
166            )
167
[856aac8]168            return HttpResponseRedirect(reverse(review_request, args=[new_request.pk],) + '?new=true') # Redirect after POST
[6b8d891]169    else:
[e2f2aa9]170        form = RequestForm(instance=new_request, initial=initial, ) # An unbound form
[70ce03a]171        form.fields['budget_area'] = CommitteeBudgetAreasField(comm_obj)
[f6c7295]172        form.fields['expense_area'] = ExpenseAreasField()
[269db50]173
174    context = {
175        'term':term_obj,
176        'comm':comm_obj,
[6b8d891]177        'form':form,
[3a0c51b]178        'pagename':'request_reimbursement',
[269db50]179    }
[fedcbcf]180    return render_to_response('vouchers/submit.html', context, context_instance=RequestContext(http_request), )
[6b8d891]181
[587bb95]182class VoucherizeForm(Form):
[e601d3b]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')
[587bb95]185
186
[6b8d891]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)
[f7dd5e7]190    user = http_request.user
191    pagename = 'request_reimbursement'
[856aac8]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
[587bb95]198
[f7dd5e7]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
[dc17b01]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
[b34d568]213        if http_request.method == 'POST' and 'upload_documentation' in http_request.REQUEST: # If the form has been submitted...
[dc17b01]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()
[a75ed9b]218                request_obj.documentation = new_docs
219                request_obj.save()
[dc17b01]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
[e8550be]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
[dc17b01]242    # APPROVE VOUCHERS
[3e79308]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:
[587bb95]246        # Voucherize form
247        # Prefill from certs / config
248        initial = {}
[37c15c4]249        initial['name'] = '%s %s' % (http_request.user.first_name, http_request.user.last_name, )
[587bb95]250        if settings.SIGNATORY_EMAIL:
251            initial['email'] = settings.SIGNATORY_EMAIL
252        else:
[37c15c4]253            initial['email'] = http_request.user.email
[587bb95]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():
[b1a259c]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'],
[a9d44e0]263                )
[587bb95]264                approve_message = 'Created new voucher from request'
265        else:
266            approve_form = VoucherizeForm(initial=initial)
267
[6b8d891]268    context = {
269        'rr':request_obj,
[f7dd5e7]270        'pagename':pagename,
[856aac8]271        'new': new,
[dc17b01]272        'doc_form': doc_upload_form,
[6b8d891]273    }
[3e79308]274    if show_approve:
[587bb95]275        context['approve_form'] = approve_form
276        context['approve_message'] = approve_message
[e8550be]277    if show_email:
278        context['email_options'] = email_options
279        context['email_message'] = email_message
[fedcbcf]280    return render_to_response('vouchers/ReimbursementRequest_review.html', context, context_instance=RequestContext(http_request), )
[6b8d891]281
[6054f18]282@user_passes_test(lambda u: u.has_perm('vouchers.generate_vouchers'))
[dcaa9c0]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
[71ca9e6]301    total = decimal.Decimal('0.00')
302    for voucher in lst:
303        total = total + voucher.amount
304
[a75ed9b]305    context = {
306        'vouchers': lst,
[71ca9e6]307        'total': total,
[a75ed9b]308        'MEDIA_ROOT': settings.MEDIA_ROOT,
309    }
[70a9bbd]310    response = render_to_response(
311        'vouchers/vouchers.tex',
312        context, context_instance=RequestContext(http_request),
313        mimetype=settings.LATEX_MIMETYPE,
314    )
[dcaa9c0]315
[524b81e]316    # Send mail
[3bf063c]317    tmpl = get_template('vouchers/emails/vouchers_tex.txt')
[524b81e]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
[dcaa9c0]333    if mark:
334        for voucher in lst:
[82211ea]335            voucher.mark_processed()
[dcaa9c0]336
337    return response
[6e43384]338
339def get_related_requests_qobj(user, ):
340    return Q(submitter=user.username) | Q(check_to_email=user.email)
341
[f52f909]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
[3e372da]356def list_to_keys(lst):
357    dct = {}
358    for key in lst:
359        dct[key] = True
360    return dct
361
[6e43384]362@login_required
[bef7191]363def show_requests(http_request, ):
[3e372da]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
[546a1ff]392    # PERMISSION-BASED REQUEST FILTERING
[bef7191]393    if http_request.user.has_perm('vouchers.can_list'):
[6e43384]394        qs = ReimbursementRequest.objects.all()
395        useronly = False
396    else:
[bef7191]397        qs = ReimbursementRequest.objects.filter(get_related_requests_qobj(http_request.user))
[6e43384]398        useronly = True
399
[546a1ff]400    # SORTING
[bef7191]401    if 'order' in http_request.REQUEST:
402        order_row = [row for row in request_list_orders if row[0] == http_request.REQUEST['order']]
[f52f909]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
[546a1ff]411    # DISCRETIONARY REQUEST FILTERING
[bef7191]412    if 'approval_status' in http_request.REQUEST:
413        approval_status = http_request.REQUEST['approval_status']
[5e6ab71]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')
[f52f909]428
[546a1ff]429    # GENERATE THE REQUEST
[6e43384]430    return list_detail.object_list(
[bef7191]431        http_request,
[6e43384]432        queryset=qs,
433        extra_context={
[3e372da]434            'actions' : actions,
435            'selected_ids'  : list_to_keys(selected_rr_ids),
436            'action_message': apply_action_message,
437            'action_errors' : apply_action_errors,
[6e43384]438            'useronly': useronly,
[f52f909]439            'order'   : order,
440            'orders'  : request_list_orders,
[5e6ab71]441            'approval_status' : approval_status,
442            'approval_states':  vouchers.models.APPROVAL_STATES,
[6e43384]443            'pagename': 'list_requests',
444        },
445    )
Note: See TracBrowser for help on using the repository browser.