source: remit/vouchers/models.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: 13.6 KB
Line 
1from django.db import models
2import settings
3import finance_core
4from finance_core.models import BudgetArea, BudgetTerm
5
6from django.core.mail import send_mail, mail_admins
7from django.core.urlresolvers import reverse
8from django.template import Context, Template
9from django.template.loader import get_template
10
11import datetime
12import re
13
14APPROVAL_STATE_PENDING = 0
15APPROVAL_STATE_APPROVED = 1
16APPROVAL_STATE_REJECTED = -1
17APPROVAL_STATES = (
18    (APPROVAL_STATE_PENDING,  'Pending'),
19    (APPROVAL_STATE_APPROVED, 'Approved'),
20    (APPROVAL_STATE_REJECTED, 'Rejected'),
21)
22
23recipient_type_choices = (
24    ('mit', 'MIT student, faculty, or staff', ),
25    ('other', 'Other, including alumnus or affiliate', ),
26)
27class ReimbursementRequest(models.Model):
28    submitter = models.CharField(max_length=30) # Username of submitter
29    recipient_type = models.CharField(choices=recipient_type_choices, max_length=10, default='other')
30    check_to_first_name = models.CharField(max_length=50, verbose_name="check recipient's first name")
31    check_to_last_name = models.CharField(max_length=50, verbose_name="check recipient's last name")
32    check_to_email = models.EmailField(verbose_name="email address for check pickup")
33    check_to_addr = models.TextField(blank=True, verbose_name="address for check mailing", help_text="For most requests, this should be blank for pickup in SAO (W20-549)")
34    amount = models.DecimalField(max_digits=7, decimal_places=2, help_text='Do not include "$"')
35    budget_area = models.ForeignKey(BudgetArea, related_name='as_budget_area')
36    budget_term = models.ForeignKey(BudgetTerm)
37    expense_area = models.ForeignKey(BudgetArea, related_name='as_expense_area') # ~GL
38    incurred_time = models.DateTimeField(default=datetime.datetime.now, help_text='Time the item or service was purchased')
39    request_time = models.DateTimeField(default=datetime.datetime.now)
40    approval_time = models.DateTimeField(blank=True, null=True,)
41    approval_status = models.IntegerField(default=0, choices=APPROVAL_STATES)
42    name = models.CharField(max_length=50, verbose_name='short description', )
43    description = models.TextField(blank=True, verbose_name='long description', )
44    documentation = models.ForeignKey('Documentation', null=True, blank=True, )
45    voucher       = models.ForeignKey('Voucher',       null=True, )
46    rfp           = models.ForeignKey('RFP',           null=True, blank=True, )
47
48    class Meta:
49        permissions = (
50            ('can_list', 'Can list requests',),
51            ('can_approve', 'Can approve requests',),
52            ('can_email', 'Can send mail about requests',),
53        )
54        ordering = ['id', ]
55
56    def __unicode__(self, ):
57        return "%s: %s %s (%s) (by %s) for $%s" % (
58            self.name,
59            self.check_to_first_name,
60            self.check_to_last_name,
61            self.check_to_email,
62            self.submitter,
63            self.amount,
64        )
65
66    def create_transfers(self, ):
67        finance_core.models.make_transfer(
68            self.name,
69            self.amount,
70            finance_core.models.LAYER_EXPENDITURE,
71            self.budget_term,
72            self.budget_area,
73            self.expense_area,
74            self.description,
75            self.incurred_time,
76        )
77
78    def convert_to_voucher(self, signatory, signatory_email=None):
79        if signatory_email is None:
80            signatory_email = settings.SIGNATORY_EMAIL
81        voucher = Voucher()
82        voucher.group_name = settings.GROUP_NAME
83        voucher.account = self.budget_area.get_account_number()
84        voucher.signatory = signatory
85        voucher.signatory_email = signatory_email
86        voucher.first_name = self.check_to_first_name
87        voucher.last_name = self.check_to_last_name
88        voucher.email_address = self.check_to_email
89        voucher.mailing_address = self.check_to_addr
90        voucher.amount = self.amount
91        voucher.description = self.label() + ': ' + self.name
92        voucher.gl = self.expense_area.get_account_number()
93        voucher.documentation = self.documentation
94        voucher.save()
95        self.create_transfers()
96        self.approval_status = 1
97        self.approval_time = datetime.datetime.now()
98        self.voucher = voucher
99        self.save()
100
101    def convert_to_rfp(self, ):
102        rfp = RFP()
103        rfp.save()
104        self.create_transfers()
105        self.approval_status = APPROVAL_STATE_APPROVED
106        self.approval_time = datetime.datetime.now()
107        self.rfp = rfp
108        self.save()
109
110    def approve(self, approver, signatory_name, signatory_email=None, ):
111        """Mark a request as approved.
112
113        approver:       user object of the approving user
114        signatory_name: name of signatory
115        signatory_email: email address of signatory (provide None for default)
116        """
117        self.convert_to_voucher(signatory_name, signatory_email,)
118        tmpl = get_template('vouchers/emails/request_approval_admin.txt')
119        ctx = Context({
120            'approver': approver,
121            'request': self,
122            'approval_type': 'voucher',
123        })
124        body = tmpl.render(ctx)
125        mail_admins(
126            'Request approval: %s approved $%s [voucher]' % (
127                approver,
128                self.amount,
129            ),
130            body,
131        )
132
133    def approve_as_rfp(self, approver, ):
134        self.convert_to_rfp()
135        tmpl = get_template('vouchers/emails/request_approval_admin.txt')
136        ctx = Context({
137            'approver': approver,
138            'request': self,
139            'approval_type': 'RFP',
140        })
141        body = tmpl.render(ctx)
142        mail_admins(
143            'Request approval: %s approved $%s [RFP]' % (
144                approver,
145                self.amount,
146            ),
147            body,
148        )
149
150
151    def review_link(self, ):
152        path = settings.SITE_URL_BASE + reverse('review_request', kwargs=dict(object_id=self.id,))
153        return path
154
155    def label(self, ):
156        return settings.GROUP_ABBR + unicode(self.pk) + 'RR'
157
158class Voucher(models.Model):
159    group_name = models.CharField(max_length=40)
160    account = models.IntegerField()
161    signatory = models.CharField(max_length=50)
162    signatory_email = models.EmailField()
163    first_name = models.CharField(max_length=20)
164    last_name = models.CharField(max_length=20)
165    email_address = models.EmailField(max_length=50)
166    mailing_address = models.TextField(blank=True, )
167    amount = models.DecimalField(max_digits=7, decimal_places=2,)
168    description = models.TextField()
169    gl = models.IntegerField()
170    processed = models.BooleanField()
171    process_time = models.DateTimeField(blank=True, null=True,)
172    documentation = models.ForeignKey('Documentation', blank=True, null=True, )
173
174    def mailing_addr_lines(self):
175        import re
176        if self.mailing_address:
177            lst = re.split(re.compile('[\n\r]*'), self.mailing_address)
178            lst = filter(lambda elem: len(elem)>0, lst)
179        else:
180            lst = []
181        lst = lst + ['']*(3-len(lst))
182        return lst
183
184    def mark_processed(self, ):
185        self.process_time = datetime.datetime.now()
186        self.processed = True
187        self.save()
188
189    def __unicode__(self, ):
190        return "%s: %s %s (%s) for $%s" % (
191            self.description,
192            self.first_name,
193            self.last_name,
194            self.email_address,
195            self.amount,
196        )
197
198    class Meta:
199        permissions = (
200            ('generate_vouchers', 'Can generate vouchers',),
201        )
202
203class RFP(models.Model):
204    addr_regex = re.compile(r'^(?P<addr>.+)\n(?P<city>.+), (?P<state>\w\w) (?P<zip>\d{5}(-\d{4})?)$')
205    addr_error = 'Address must two lines: a street address, followed by "<city>, <two letter state abbreviation> <zip>". For non-US addresses, please contact a Treasurer for help.'
206
207    create_time = models.DateTimeField(default=datetime.datetime.now)
208
209    def __unicode__(self, ):
210        return "RFP: %s" % (self.create_time.strftime(settings.SHORT_DATETIME_FORMAT_F), )
211
212class Documentation(models.Model):
213    backing_file = models.FileField(upload_to='documentation', verbose_name='File', help_text='PDF files only', )
214    label = models.CharField(max_length=50, default="")
215    submitter = models.CharField(max_length=30, null=True, ) # Username of submitter
216    upload_time = models.DateTimeField(default=datetime.datetime.now)
217
218    def __unicode__(self, ):
219        return "%s; uploaded at %s" % (self.label, self.upload_time, )
220
221
222class StockEmail:
223    def __init__(self, name, label, recipients, template, subject_template, context, ):
224        """
225        Initialize a stock email object.
226       
227        Each argument is required.
228       
229        name:       Short name. Letters, numbers, and hyphens only.
230        label:      User-readable label. Briefly describe what the email says
231        recipients: Who receives the email. List of "recipient" (check recipient), "area" (area owner), "admins" (site admins)
232        template:   Django template filename with the actual text
233        subject_template: Django template string with the subject
234        context:    Type of context the email needs. Must be 'request' currently.
235        """
236
237        self.name       = name
238        self.label      = label
239        self.recipients = recipients
240        self.template   = template
241        self.subject_template = subject_template
242        self.context    = context
243
244    def send_email_request(self, request,):
245        """
246        Send an email that requires context "request".
247        """
248
249        assert self.context == 'request'
250
251        # Generate text
252        from django.template import Context, Template
253        from django.template.loader import get_template
254        ctx = Context({
255            'prefix': settings.EMAIL_SUBJECT_PREFIX,
256            'request': request,
257            'sender': settings.USER_EMAIL_SIGNATURE,
258        })
259        tmpl = get_template(self.template)
260        body = tmpl.render(ctx)
261        subject_tmpl = Template(self.subject_template)
262        subject = subject_tmpl.render(ctx)
263
264        # Generate recipients
265        recipients = []
266        for rt in self.recipients:
267            if rt == 'recipient':
268                recipients.append(request.check_to_email)
269            elif rt == 'area':
270                recipients.append(request.budget_area.owner_address())
271            elif rt == 'admins':
272                pass # you don't *actually* have a choice...
273        for name, addr in settings.ADMINS:
274            recipients.append(addr)
275
276        # Send mail!
277        from django.core.mail import send_mail
278        send_mail(
279            subject,
280            body,
281            settings.SERVER_EMAIL,
282            recipients,
283        )
284
285stock_emails = {
286    'nodoc': StockEmail(
287        name='nodoc',
288        label='No documentation',
289        recipients=['recipient', 'area',],
290        template='vouchers/emails/no_docs_user.txt',
291        subject_template='{{prefix}}Missing documentation for reimbursement',
292        context = 'request',
293    ),
294    'voucher-sao': StockEmail(
295        name='voucher-sao',
296        label='Voucher submitted to SAO',
297        recipients=['recipient', ],
298        template='vouchers/emails/voucher_sao_user.txt',
299        subject_template='{{prefix}}Reimbursement sent to SAO for processing',
300        context = 'request',
301    ),
302}
303
304class BulkRequestAction:
305    def __init__(self, name, label, action, perm_predicate=None, ):
306        self.name = name
307        self.label = label
308        self.action = action
309        if perm_predicate is None:
310            perm_predicate = lambda user: True
311        elif perm_predicate == True:
312            perm_predicate = lambda user: True
313        self.perm_predicate = perm_predicate
314    def can(self, user):
315        return self.perm_predicate(user)
316    def do(self, http_request, rr, ):
317        if self.can(http_request.user):
318            return self.action(http_request, rr, )
319        else:
320            return False, "permission denied"
321    def __str__(self):
322        return self.label
323    @classmethod
324    def filter_can_only(cls, actions, user):
325        return [ action for action in actions if action.can(user) ]
326
327def bulk_action_approve(http_request, rr):
328    approver = http_request.user
329    signatory_name = http_request.user.get_full_name()
330    if rr.voucher:
331        return False, "already approved"
332    else:
333        rr.approve(approver, signatory_name)
334        return True, "request approved"
335
336def bulk_action_approve_as_rfp(http_request, rr):
337    approver = http_request.user
338    if rr.rfp:
339        return False, "already marked as RFPized"
340    else:
341        rr.approve_as_rfp(approver, )
342        return True, "request marked as RFPized"
343
344def bulk_action_email_factory(stock_email_obj):
345    assert stock_email_obj.context == 'request'
346    def inner(http_request, rr):
347        stock_email_obj.send_email_request(rr)
348        return True, "mail sent"
349    return inner
350def perm_checker(perm):
351    def predicate(user):
352        return user.has_perm(perm)
353    return predicate
354
355bulk_request_actions = []
356if settings.SIGNATORY_EMAIL:
357    bulk_request_actions.append(BulkRequestAction(
358        name='approve',
359        label='Approve Requests',
360        action=bulk_action_approve,
361        perm_predicate=perm_checker('vouchers.can_approve'),
362    ))
363bulk_request_actions.append(BulkRequestAction(
364    name='approve_as_rfp',
365    label='Mark Requests as RFPized',
366    action=bulk_action_approve_as_rfp,
367    perm_predicate=perm_checker('vouchers.can_approve'),
368))
369for name, stockemail in stock_emails.items():
370    if stockemail.context == 'request':
371        bulk_request_actions.append(BulkRequestAction(
372            name='email/%s' % name,
373            label='Stock Email: %s' % stockemail.label,
374            action=bulk_action_email_factory(stockemail),
375            perm_predicate=perm_checker('vouchers.can_email'),
376        ))
Note: See TracBrowser for help on using the repository browser.