source: remit/vouchers/views.py @ f52f909

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

Allow sorting requests (Trac: #39)

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