source: remit/vouchers/models.py

Last change on this file was feed77c, checked in by Alex Dehnert <adehnert@…>, 11 years ago

RFP download/update process (and some tangential changes)

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