diff --git a/TuitionAutomation/TuitionAutomation.py b/TuitionAutomation/TuitionAutomation.py index 0720b2a..052bfe2 100644 --- a/TuitionAutomation/TuitionAutomation.py +++ b/TuitionAutomation/TuitionAutomation.py @@ -3,8 +3,8 @@ from datetime import timedelta import urllib -paymentOrgId = 51 -minTransactionId = 17000 +# model.TestEmail = True +model.Transactional = True orgs = [ { @@ -12,7 +12,7 @@ 'name': "Kindergarten", 'base': 837 }, - + { 'id': 212, 'name': "Pre-K", @@ -23,7 +23,7 @@ 'name': "Pre-K", 'base': 837 }, - + { 'id': 210, 'name': "3-Year-Old", @@ -34,7 +34,7 @@ 'name': "3-Year-Old", 'base': 497 }, - + { 'id': 207, 'name': "2-Year-Old", @@ -48,14 +48,14 @@ { 'id': 215, 'name': "Afternoon - Mon", - 'base': 40, + 'base': 45, 'payPer': "meeting", 'discounts': False }, { 'id': 253, 'name': "Afternoon - Thu", - 'base': 40, + 'base': 45, 'payPer': "meeting", 'discounts': False }, @@ -66,20 +66,20 @@ # } ] -# ################# Basic configuration ends here. ######################### +paymentOrgId = 206 +minTransactionId = 1 #28100 +retroactive = True +now = datetime.now() - timedelta(days=9) +# ################# Basic configuration ends here. ######################### print "Assess Next Due Amount
" print "Generate Email Previews
" print "View Balances
" print "Program Totals
" -# model.TestEmail = True -model.Transactional = True -isDeposit = False - model.CurrentOrgId = paymentOrgId - + # this is a container for collecting the "first child" of a given family, used to determine when a second child comes along. firstChildren = {} familyData = {} @@ -91,8 +91,8 @@ model.Title = "Preschool Tuition Automation Tools" -monthToUse = datetime.now() + timedelta(days=28) -monthToUseR = datetime.now() +monthToUse = now + timedelta(days=28) +monthToUseR = now monthName = monthToUse.strftime('%B') monthNameR = monthToUseR.strftime('%B') @@ -101,204 +101,261 @@ for cls in orgs: meetingCntSql = """SELECT COUNT(*) as mtgCnt FROM Meetings WHERE OrganizationId = {} AND YEAR(MeetingDate) = {} AND MONTH(MeetingDate) = {}""".format(cls['id'], monthToUse.year, monthToUse.month) meetingCnt = q.QuerySqlTop1(meetingCntSql).mtgCnt - + if showTuitionAssessment: print "

{}

".format(cls['name']) - + if cls.has_key('payPer') and cls['payPer'] == "meeting": print "

Tuition per Meeting - {} meetings in {}

".format(meetingCnt, monthName) else: print "

Tuition per Month

" - + for student in q.QueryList("MemberTypeCodes( Org={} ) = 220[Member]".format(cls['id'])): - + enrollmentDateSql = "SELECT EnrollmentDate FROM OrganizationMembers WHERE OrganizationId = {} AND PeopleId = {}".format(cls['id'], student.PeopleId) enrollmentDate = q.QuerySqlTop1(enrollmentDateSql).EnrollmentDate - + lastChargeSql = """SELECT MAX(TransactionDate) as date FROM [Transaction] t LEFT JOIN [TransactionPeople] tp ON t.OriginalId = tp.Id WHERE t.OrgId = {} AND t.AdjustFee = 1 AND tp.PeopleId = {} AND Message LIKE '{}%'""".format(paymentOrgId, student.PeopleId, cls['name']) lastChargeDate = q.QuerySqlTop1(lastChargeSql) lastChargeDate = lastChargeDate if lastChargeDate is None else lastChargeDate.date - + isFirstChild = True if firstChildren.has_key("{}".format(student.FamilyId)): isFirstChild = (firstChildren["{}".format(student.FamilyId)].PeopleId == student.PeopleId) - + else: firstChildren["{}".format(student.FamilyId)] = student - - tuitionOverrideSql = """SELECT ome.IntValue as Tuition FROM OrgMemberExtra ome + + tuitionOverrideSql = """SELECT ome.IntValue as Tuition FROM OrgMemberExtra ome WHERE UPPER(ome.Field) = 'Tuition' AND ome.OrganizationId = {} AND ome.PeopleId = {}""".format(cls['id'], student.PeopleId) - + tuitionOverride = q.QuerySqlTop1(tuitionOverrideSql) - + meetingCntRSql = """SELECT COUNT(*) as mtgCnt FROM Meetings WHERE OrganizationId = {} AND YEAR(MeetingDate) = {} AND MONTH(MeetingDate) = {} AND MeetingDate > '{}'""".format(cls['id'], monthToUseR.year, monthToUseR.month, enrollmentDate) meetingCntR = q.QuerySqlTop1(meetingCntRSql).mtgCnt - + tuitionLine = cls['name'] depositLine = cls['name'] + " Deposit" - + if tuitionOverride is None: - + tuitionCharge = 1.0 * cls['base'] depositCharge = 1.0 * cls['base'] tuitionChargeR = 1.0 * cls['base'] * (meetingCntR > 0) - + if cls.has_key('payPer') and cls['payPer'] == "meeting": tuitionCharge *= meetingCnt tuitionChargeR *= meetingCntR - + if cls.has_key('discounts') and cls['discounts'] == False: pass # no discounts else: - if not isDeposit and q.QueryCount("FamHasPrimAdultChurchMemb = 1[True] AND PeopleId = {}".format(student.PeopleId)) > 0: + if q.QueryCount("FamHasPrimAdultChurchMemb = 1[True] AND PeopleId = {}".format(student.PeopleId)) > 0: tuitionCharge = tuitionCharge * (5.0/6.0) tuitionChargeR = tuitionChargeR * (5.0/6.0) tuitionLine = tuitionLine + " (Tenth Member Rate)" - depositLine = depositLine + " (Tenth Member Rate)" - - elif not isDeposit and not isFirstChild: + # depositLine = depositLine + " (Tenth Member Rate)" Do not apply discounts to Deposits. + + elif not isFirstChild: tuitionCharge = tuitionCharge * (8.0/9.0) tuitionChargeR = tuitionChargeR * (8.0/9.0) tuitionLine = tuitionLine + " (Sibling Rate)" - depositLine = depositLine + " (Sibling Rate)" - - else: + # depositLine = depositLine + " (Sibling Rate)" Do not apply discounts to Deposits. + + else: tuitionCharge = tuitionOverride.Tuition * 1.0 tuitionChargeR = tuitionOverride.Tuition * 1.0 depositCharge = tuitionOverride.Tuition * 1.0 tuitionLine = tuitionLine + " (Special Rate)" depositLine = depositLine + " (Special Rate)" - + if cls.has_key('payPer') and cls['payPer'] == "meeting": tuitionCharge *= meetingCnt tuitionChargeR *= meetingCntR - + tuitionLineR = tuitionLine + " - " + monthNameR tuitionLine += " - " + monthName - - # Rounding, because the preschool wants that. + + # Rounding, because the preschool wants that. tuitionCharge = round(tuitionCharge, 0) tuitionChargeR = round(tuitionChargeR, 0) depositCharge = round(depositCharge, 0) - - if enrollmentDate < lastChargeDate: + + if enrollmentDate < lastChargeDate: # and not cls.has_key('payPer'): tuitionChargeR = 0.0 - + if showTuitionAssessment: - if isDeposit: - print "

{} {} ${:,.2f} {}

".format(student.PreferredName, student.LastName, depositCharge, depositLine) - else: - if tuitionChargeR > 0: - print "

{} {} ${:,.2f} {}

".format(student.PreferredName, student.LastName, tuitionChargeR, tuitionLineR) - print "

{} {} ${:,.2f} {}

".format(student.PreferredName, student.LastName, tuitionCharge, tuitionLine) - - + # print "

{} {} ${:,.2f} {}

".format(student.PreferredName, student.LastName, depositCharge, depositLine) + if tuitionChargeR > 0 and retroactive: + print "

{} {} ${:,.2f} {} (retro)

".format(student.PreferredName, student.LastName, tuitionChargeR, tuitionLineR) + print "

{} {} ${:,.2f} {}

".format(student.PreferredName, student.LastName, tuitionCharge, tuitionLine) + + # Add to org, and add deposit if new to the org to account if not model.InOrg(student.PeopleId, paymentOrgId): model.JoinOrg(paymentOrgId, student.PeopleId) + + # Add deposit if needed if monthToUse.month > 6: model.AdjustFee(student.PeopleId, paymentOrgId, -depositCharge, depositLine) - - + + # TODO: Automatically add fee adjustments. - # TODO: Calculate proration of tuition. - if applyTuitionAssessment and not isDeposit: - if tuitionChargeR > 0: + # TODO: Calculate proration of tuition. + if applyTuitionAssessment: + if tuitionChargeR > 0 and tuitionLineR != tuitionLine: model.AdjustFee(student.PeopleId, paymentOrgId, -tuitionChargeR, tuitionLineR) model.AdjustFee(student.PeopleId, paymentOrgId, -tuitionCharge, tuitionLine) - - + + # Initialize FamilyData dict for emails if not familyData.has_key("{}".format(student.FamilyId)): fd = { 'recipients': [], 'participants': [], + 'participantPids': [], + 'paylinks': [], + 'totalDue': 0.0, 'print': '', - 'hasBalanceDue': False + 'table': '', + 'hasBalanceDue': False, + 'otherParent': None } else: fd = familyData["{}".format(student.FamilyId)] - + needsToAppend = True for part in fd['participants']: if part.PeopleId == student.PeopleId: needsToAppend = False break - + if not needsToAppend: continue - + fd['participants'].append(student) - - fd['print'] += "

Tuition for {}

".format(student.PreferredName) - + fd['participantPids'].append(str(student.PeopleId)) + + otherParent = model.ExtraValueInt(student.PeopleId, 'Parent') + if otherParent != "": + fd['otherParent'] = model.GetPerson(otherParent) + + transactionSql = """ - SELECT + SELECT FORMAT(t.TransactionDate, 'MMM dd, yyyy') as Date, FORMAT(t.Amt * -1, 'C') as Amount, - REPLACE(t.Message, 'APPROVED', 'Online Transaction') as Description - FROM [Transaction] t - LEFT JOIN [TransactionPeople] tp ON t.OriginalId = tp.Id - WHERE t.OrgId = {0} AND tp.PeopleId = {1} AND t.Approved = 1 AND t.id > {2} - """.format(paymentOrgId, student.PeopleId, minTransactionId) - - totalSql = """ - SELECT SUM(t.Amt) * -100 as Amount - FROM [Transaction] t - LEFT JOIN [TransactionPeople] tp ON t.OriginalId = tp.Id - WHERE t.OrgId = {} AND tp.PeopleId = {} AND t.Approved = 1 AND t.id > {} - """.format(paymentOrgId, student.PeopleId, minTransactionId) - - amtOwed = q.QuerySqlInt(totalSql) - - table = "" + t.Amt as AmtNum, + t.Name, + t.Description + + FROM ( + + -- Deposit + SELECT + t3.TransactionDate, + tp3.Amt * 1.0 as Amt, + COALESCE(REPLACE(t3.Message, 'APPROVED', 'Deposit'), 'Deposit') as Description, + p3.Name + FROM [TransactionPeople] tp3 + LEFT JOIN People p3 ON tp3.PeopleId = p3.PeopleId + LEFT JOIN [Transaction] t3 ON tp3.Id = t3.Id + WHERE tp3.Id IN (SELECT DISTINCT TranId FROM OrganizationMembers WHERE OrganizationId = {0} AND PeopleId IN ({1})) + + UNION + + -- Original Payment + SELECT + t2.TransactionDate, + (tp2.Amt - t2.amtdue) * -1.0 as Amt, + COALESCE(REPLACE(t2.Message, 'APPROVED', 'Online Transaction'), 'Discount Code') as Description, + p2.Name + FROM [TransactionPeople] tp2 + LEFT JOIN People p2 ON tp2.PeopleId = p2.PeopleId + LEFT JOIN [Transaction] t2 ON tp2.Id = t2.Id + WHERE t2.coupon is null AND tp2.Id IN (SELECT DISTINCT TranId FROM OrganizationMembers WHERE OrganizationId = {0} AND PeopleId IN ({1})) + + UNION + + -- Original Payment + SELECT + t4.TransactionDate, + t4.Amt * -1.0 as Amt, + COALESCE(REPLACE(t4.Message, 'APPROVED', 'Online Transaction'), 'Discount Code') as Description, + p4.Name + FROM [TransactionPeople] tp4 + LEFT JOIN People p4 ON tp4.PeopleId = p4.PeopleId + LEFT JOIN [Transaction] t4 ON tp4.Id = t4.Id + WHERE t4.coupon = 1 AND tp4.Id IN (SELECT DISTINCT TranId FROM OrganizationMembers WHERE OrganizationId = {0} AND PeopleId IN ({1})) + + UNION + + -- Adjustments + SELECT + t1.TransactionDate, + t1.amt * -1.0 as Amt, + REPLACE(t1.Message, 'APPROVED', 'Online Transaction') as Description, + t1.Name + FROM [Transaction] t1 + WHERE t1.Id <> t1.OriginalId AND t1.OriginalId IN (SELECT DISTINCT TranId FROM OrganizationMembers WHERE OrganizationId = {0} AND PeopleId IN ({1})) + + ) as t + + WHERE t.Amt <> 0 + + ORDER BY t.TransactionDate, t.Description, t.Name + """.format(paymentOrgId, ", ".join(fd['participantPids'])) + + amtOwed = 0.0 + + table = "
DateAmountDescription
" for row in q.QuerySql(transactionSql): - table = table + "".format(row.Date, row.Amount, row.Description) + table = table + "".format(row.Date, row.Amount, row.Name, row.Description) + amtOwed += row.AmtNum or 0 table += "
DateAmountPersonDescription
{}{}{}
{}{}{}{}
" - - table += "

Balance: ${:,.2f}


".format(amtOwed * 0.01) - - fd['print'] += table + + table += "

Balance: ${:,.2f}


".format(amtOwed) + + fd['table'] = table + amtOwed = amtOwed * 100 + if amtOwed > 0: fd['hasBalanceDue'] = True - + paylink = model.GetPayLink(student.PeopleId, paymentOrgId) if paylink is not None: paylink = paylink.replace(model.CmsHost, "") paylink = model.CmsHost + "/Logon?" + urllib.urlencode({"ReturnUrl": paylink}) - - if amtOwed < 0: - fd['print'] += "

This credit of ${:,.2f} will be applied to future monthly billing cycles. You do not need to do anything now.

".format(amtOwed * -0.01) - elif amtOwed > 0: - if paylink is None: - fd['print'] += "

Missing paylink for pid {}

".format(student.PeopleId) - fd['print'] += "

Please make a payment of ${:,.2f} for {} by sending in a check, or paying online here.

".format(amtOwed * 0.01, student.PreferredName, paylink) - else: - fd['print'] += "

Since there is no outstanding balance, you don't need to do anything.

".format(student.PreferredName) - - fd['print'] += "
" - + paylink = "paying online here".format(paylink) + + if paylink not in fd['paylinks']: + fd['paylinks'].append(paylink) + + fd['totalDue'] = amtOwed + familyData["{}".format(student.FamilyId)] = fd - + emailData = [] emailPids = [] - + for fi in familyData: fam = familyData[fi] - + p1 = fam['participants'][0].Family.HeadOfHousehold or None p2 = fam['participants'][0].Family.HeadOfHouseholdSpouse or None - + p3 = fam['otherParent'] + if showEmails: - + if not sendEmails: print "

{} Family

".format(familyData[fi]['participants'][0].LastName) - + p1Name = None p2Name = None + p3Name = None pPids = [] if p1 is not None: p1Name = p1.Name @@ -306,21 +363,35 @@ if p2 is not None: p2Name = p2.Name pPids.append(p2.PeopleId) - + if p3 is not None: + p3Name = p3.Name + pPids.append(p3.PeopleId) + pPids = map(str, pPids) - + if not sendEmails: if familyData[fi]['hasBalanceDue']: - print "

Sends to: {} {} Send Individually

".format(p1Name, p2Name, ",".join(pPids)) + print "

Sends to: {} {} {} Send Individually

".format(p1Name, p2Name, p3Name, ",".join(pPids)) else: - print "

Does NOT Send to: {} {} Send Individually

".format(p1Name, p2Name, ",".join(pPids)) - - if len(familyData[fi]['participants']) > 1: - familyData[fi]['print'] = "

Each child's balance and payments are listed below. Please note that each child's account must be payed separately if paying online.


" + familyData[fi]['print'] - + print "

Does NOT Send to: {} {} {} Send Individually

".format(p1Name, p2Name, p3Name, ",".join(pPids)) + + # if len(familyData[fi]['participants']) > 1: + familyData[fi]['print'] = familyData[fi]['table'] + + amtOwed = familyData[fi]['totalDue'] + if amtOwed < 0: + familyData[fi]['print'] += "

This credit of ${:,.2f} will be applied to future monthly billing cycles. You do not need to do anything now.

".format(amtOwed * -0.01) + elif amtOwed > 0: + if paylink is None: + familyData[fi]['print'] += "

Missing paylink for pid {}

".format(student.PeopleId) + familyData[fi]['print'] += "

Please make a payment of ${:,.2f} by sending in a check, or {}.

".format(amtOwed * 0.01, " AND ".join(familyData[fi]['paylinks'])) + else: + familyData[fi]['print'] += "

Since there is no outstanding balance, you don't need to do anything.

".format(student.PreferredName) + + if not sendEmails: print familyData[fi]['print'] - + if sendEmails and familyData[fi]['hasBalanceDue']: if p1 is not None: emailPids.append(p1.PeopleId) @@ -328,14 +399,21 @@ recipientData.PeopleId = p1.PeopleId recipientData.Summary = familyData[fi]['print'] emailData.append(recipientData) - + if p2 is not None: emailPids.append(p2.PeopleId) recipientData = model.DynamicData() recipientData.PeopleId = p2.PeopleId recipientData.Summary = familyData[fi]['print'] emailData.append(recipientData) - + + if p3 is not None: + emailPids.append(p3.PeopleId) + recipientData = model.DynamicData() + recipientData.PeopleId = p3.PeopleId + recipientData.Summary = familyData[fi]['print'] + emailData.append(recipientData) + if sendEmails: if Data.to == "all": @@ -343,8 +421,8 @@ emailPids = ",".join(emailPids) else: emailPids = Data.to - + print "

Emails have been sent to:

" - + print "PeopleIds = '{}'".format(emailPids) - model.EmailContentWithPythonData("PeopleIds = '{}'".format(emailPids), 27796, "preschooltuition@tenth.org", "Tenth Preschool", "Preschool Tuition", emailData) \ No newline at end of file + model.EmailContentWithPythonData("PeopleIds = '{}'".format(emailPids), 27796, "preschooltuition@tenth.org", "Tenth Preschool", "Preschool Tuition", emailData)