source: remit/vouchers/models.py @ 80d8926

Last change on this file since 80d8926 was 80d8926, checked in by Alex Dehnert <adehnert@…>, 10 years ago

Refactor email-sending part of RR approval

  • Property mode set to 100644
File size: 15.4 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        max_name_len = RFP._meta.get_field_by_name('name')[0].max_length
104        rfp.name = ("%s: %s" % (self.label(), self.name))[:max_name_len]
105        rfp.payee_type = self.recipient_type
106        rfp.payee_name = "%s %s" % (self.check_to_first_name, self.check_to_last_name)
107        rfp.fill_addr(self.check_to_addr)
108        rfp.item_date = self.incurred_time.date()
109        rfp.item_gl = self.expense_area.get_account_number()
110        rfp.item_co = self.budget_area.get_account_number()
111        rfp.item_amount = self.amount
112        rfp.item_desc = self.description
113        rfp.save()
114        self.create_transfers()
115        self.approval_status = APPROVAL_STATE_APPROVED
116        self.approval_time = datetime.datetime.now()
117        self.rfp = rfp
118        self.save()
119
120    def send_approval_email(self, approver, approval_type):
121        tmpl = get_template('vouchers/emails/request_approval_admin.txt')
122        ctx = Context({
123            'approver': approver,
124            'request': self,
125            'approval_type': approval_type,
126        })
127        body = tmpl.render(ctx)
128        mail_admins(
129            'Request approval: %s approved $%s [%s]' % (
130                approver,
131                self.amount,
132                approval_type,
133            ),
134            body,
135        )
136
137    def approve(self, approver, signatory_name, signatory_email=None, ):
138        """Mark a request as approved.
139
140        approver:       user object of the approving user
141        signatory_name: name of signatory
142        signatory_email: email address of signatory (provide None for default)
143        """
144        self.convert_to_voucher(signatory_name, signatory_email,)
145        self.send_approval_email(approver, 'voucher')
146
147    def approve_as_rfp(self, approver, ):
148        self.convert_to_rfp()
149        self.send_approval_email(approver, 'RFP')
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(default=False)
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    # Class constants
205    addr_regex = re.compile(r'^(?P<street>.+)\n(?P<city>.+), (?P<state>\w\w) (?P<zip>\d{5}(-\d{4})?)$')
206    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.'
207
208    # Fields
209    create_time = models.DateTimeField(default=datetime.datetime.now)
210    rfp_submit_time = models.DateTimeField(default=None, null=True, blank=True)
211    rfp_number = models.IntegerField(default=None, null=True, blank=True)
212
213    name = models.CharField(max_length=25, verbose_name='short description', )
214    payee_type = models.CharField(choices=recipient_type_choices, max_length=10, default='other')
215    payee_name = models.CharField(max_length=35, )
216    addr_street = models.CharField(max_length=35, verbose_name='street address', blank=True, )
217    addr_city = models.CharField(max_length=35, verbose_name='city', blank=True, )
218    addr_state = models.CharField(max_length=10, verbose_name='state', blank=True, )
219    addr_zip = models.CharField(max_length=10, verbose_name='zipcode', blank=True, )
220    item_date = models.DateField()
221    item_gl = models.CharField(max_length=10, verbose_name="item's G/L account")
222    item_co = models.CharField(max_length=12, verbose_name="item's cost object")
223    item_amount = models.DecimalField(max_digits=7, decimal_places=2)
224    item_desc = models.TextField()
225    documentation = models.ForeignKey('Documentation', blank=True, null=True, )
226
227    def fill_addr(self, address):
228        match = self.addr_regex.match(address)
229        assert match != None
230        self.addr_street = match.group("street")
231        self.addr_city = match.group("city")
232        self.addr_state = match.group("state")
233        self.addr_zip = match.group("zip")
234
235    def __unicode__(self, ):
236        return "RFP: %s" % (self.create_time.strftime(settings.SHORT_DATETIME_FORMAT_F), )
237
238    class Meta:
239        verbose_name = "RFP"
240
241class Documentation(models.Model):
242    backing_file = models.FileField(upload_to='documentation', verbose_name='File', help_text='PDF files only', )
243    label = models.CharField(max_length=50, default="")
244    submitter = models.CharField(max_length=30, null=True, ) # Username of submitter
245    upload_time = models.DateTimeField(default=datetime.datetime.now)
246
247    def __unicode__(self, ):
248        return "%s; uploaded at %s" % (self.label, self.upload_time, )
249
250
251class StockEmail:
252    def __init__(self, name, label, recipients, template, subject_template, context, ):
253        """
254        Initialize a stock email object.
255       
256        Each argument is required.
257       
258        name:       Short name. Letters, numbers, and hyphens only.
259        label:      User-readable label. Briefly describe what the email says
260        recipients: Who receives the email. List of "recipient" (check recipient), "area" (area owner), "admins" (site admins)
261        template:   Django template filename with the actual text
262        subject_template: Django template string with the subject
263        context:    Type of context the email needs. Must be 'request' currently.
264        """
265
266        self.name       = name
267        self.label      = label
268        self.recipients = recipients
269        self.template   = template
270        self.subject_template = subject_template
271        self.context    = context
272
273    def send_email_request(self, request,):
274        """
275        Send an email that requires context "request".
276        """
277
278        assert self.context == 'request'
279
280        # Generate text
281        from django.template import Context, Template
282        from django.template.loader import get_template
283        ctx = Context({
284            'prefix': settings.EMAIL_SUBJECT_PREFIX,
285            'request': request,
286            'sender': settings.USER_EMAIL_SIGNATURE,
287        })
288        tmpl = get_template(self.template)
289        body = tmpl.render(ctx)
290        subject_tmpl = Template(self.subject_template)
291        subject = subject_tmpl.render(ctx)
292
293        # Generate recipients
294        recipients = []
295        for rt in self.recipients:
296            if rt == 'recipient':
297                recipients.append(request.check_to_email)
298            elif rt == 'area':
299                recipients.append(request.budget_area.owner_address())
300            elif rt == 'admins':
301                pass # you don't *actually* have a choice...
302        for name, addr in settings.ADMINS:
303            recipients.append(addr)
304
305        # Send mail!
306        from django.core.mail import send_mail
307        send_mail(
308            subject,
309            body,
310            settings.SERVER_EMAIL,
311            recipients,
312        )
313
314stock_emails = {
315    'nodoc': StockEmail(
316        name='nodoc',
317        label='No documentation',
318        recipients=['recipient', 'area',],
319        template='vouchers/emails/no_docs_user.txt',
320        subject_template='{{prefix}}Missing documentation for reimbursement',
321        context = 'request',
322    ),
323    'voucher-sao': StockEmail(
324        name='voucher-sao',
325        label='Voucher submitted to SAO',
326        recipients=['recipient', ],
327        template='vouchers/emails/voucher_sao_user.txt',
328        subject_template='{{prefix}}Reimbursement sent to SAO for processing',
329        context = 'request',
330    ),
331}
332
333class BulkRequestAction:
334    def __init__(self, name, label, action, perm_predicate=None, ):
335        self.name = name
336        self.label = label
337        self.action = action
338        if perm_predicate is None:
339            perm_predicate = lambda user: True
340        elif perm_predicate == True:
341            perm_predicate = lambda user: True
342        self.perm_predicate = perm_predicate
343    def can(self, user):
344        return self.perm_predicate(user)
345    def do(self, http_request, rr, ):
346        if self.can(http_request.user):
347            return self.action(http_request, rr, )
348        else:
349            return False, "permission denied"
350    def __str__(self):
351        return self.label
352    @classmethod
353    def filter_can_only(cls, actions, user):
354        return [ action for action in actions if action.can(user) ]
355
356def bulk_action_approve(http_request, rr):
357    approver = http_request.user
358    signatory_name = http_request.user.get_full_name()
359    if rr.voucher:
360        return False, "already approved"
361    else:
362        rr.approve(approver, signatory_name)
363        return True, "request approved"
364
365def bulk_action_approve_as_rfp(http_request, rr):
366    approver = http_request.user
367    if rr.rfp:
368        return False, "already marked as RFPized"
369    else:
370        rr.approve_as_rfp(approver, )
371        return True, "request marked as RFPized"
372
373def bulk_action_email_factory(stock_email_obj):
374    assert stock_email_obj.context == 'request'
375    def inner(http_request, rr):
376        stock_email_obj.send_email_request(rr)
377        return True, "mail sent"
378    return inner
379def perm_checker(perm):
380    def predicate(user):
381        return user.has_perm(perm)
382    return predicate
383
384bulk_request_actions = []
385if settings.SIGNATORY_EMAIL:
386    bulk_request_actions.append(BulkRequestAction(
387        name='approve',
388        label='Approve Requests',
389        action=bulk_action_approve,
390        perm_predicate=perm_checker('vouchers.can_approve'),
391    ))
392bulk_request_actions.append(BulkRequestAction(
393    name='approve_as_rfp',
394    label='Mark Requests as RFPized',
395    action=bulk_action_approve_as_rfp,
396    perm_predicate=perm_checker('vouchers.can_approve'),
397))
398for name, stockemail in stock_emails.items():
399    if stockemail.context == 'request':
400        bulk_request_actions.append(BulkRequestAction(
401            name='email/%s' % name,
402            label='Stock Email: %s' % stockemail.label,
403            action=bulk_action_email_factory(stockemail),
404            perm_predicate=perm_checker('vouchers.can_email'),
405        ))
Note: See TracBrowser for help on using the repository browser.