Files
Python_Examples/stm_etl/FTP/wf_assignments.py
T

542 lines
25 KiB
Python

'''
Wells Fargo Invoice Assignment Generator
Created on Jan 7, 2016
@author: Wes
PURPOSE:
Generates CSV assignment files containing invoice and credit memo data for
transmission to Wells Fargo (WF) via SFTP. WF acts as the factoring company —
invoices are "assigned" to them so they can collect payment from customers on
our behalf.
Primarily invoked from the send_wf.py CGI script on the test server's Apache
instance, but can also be run standalone from the command line.
USAGE:
python wf_assignments.py <yyyymmdd> # non-interactive, generates + sends
python wf_assignments.py # interactive, prompts for date and confirmation
KEY CONCEPTS:
- Trade Style: A Wells Fargo grouping code. Valid styles are 00, 03, 05, 09,
mapped to Jomar factor account numbers (888888, 88888803, 88888805, 88888809).
Each trade style gets its own assignment file.
- Assignment: A single CSV file containing all invoices OR all credits for one
trade style on a given date. Invoices have positive dollar amounts; credits
have negative dollar amounts.
- Backout: An invoice that was created and reversed on the same day. Both the
original and the reversing invoice are excluded from transmission.
- Client Code: Our identifier with WF (4143, changed from 5867 on 10/02/2017).
PROCESSING LOGIC:
1. Load terms codes from Jomar (TXMYTX00) into a lookup dictionary.
2. Query Invoice_Backouts table for the target date. If an invoice was created
and backed out on the same day, both the original and backout invoice numbers
are excluded from all subsequent queries.
3. For each valid trade style, generate two assignments:
a. Invoices (RWTF > 0)
b. Credits (RWTF < 0)
4. Each assignment CSV contains:
- TRL (trailer/summary) row: client code, trade style, assignment number,
date, invoice count, type (I/C), and total dollar amount.
- DTL (detail) rows, one per invoice: invoice number, date, amount, terms,
bill-to customer info (name, address, city, state, country, zip, phone),
and customer PO number.
Customer data is looked up from the Customer Master table (TXUYKD00).
Field lengths are truncated to WF's maximum widths (e.g. name max 40 chars).
Phone numbers must be exactly 10 digits or are omitted.
5. If sendNow=True:
- CSV files are uploaded via SFTP to sftp.spscommerce.net (port 10022).
TEST_MODE uploads to /testin/ instead of /in/.
- Transmitted invoices are flagged as processed in Jomar (RFIL = 'X').
- AS400_Invoice_Interface.py is called as a subprocess to sync the
same invoices to the AS400.
6. If email=True, the generated CSVs and log output are emailed to AR.
OUTPUT:
- CSV files in D:\FTP\SEND_WF\ named {clientCode}{tradeStyle}_INV_{date}_{seq}.csv
- Log file at D:\FTP\logs\wf_assignment_build.log
- Email alert with CSVs attached (optional)
NOTE:
The sendFiles() function is currently stubbed out (returns immediately).
SFTP upload logic is preserved in comments within the function body.
'''
import paramiko, csv, sys, logging, re, time, os, smtplib, sqlite3, StringIO, subprocess
from datetime import datetime
from sqlalchemy import create_engine, func, Table, text, Column
from sqlalchemy.types import VARCHAR
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
from alert import send_alert_html
TEST_MODE = False #When 'True' files will be uploaded to the 'testin' directory on the remote server
''' CLIENT_CODE = 5867 #Old Sunbury Textile Mills client code. Last Used: 09/29/2017 '''
CLIENT_CODE = 4143 #New code assigned to us by WF. Effective: 10/02/2017
REMOTE_HOST = 'sftp.spscommerce.net'
REMOTE_PORT = 10022
MESSAGE = None
VALID_TRADE_STYLES = ['888888', '88888800', '88888803', '88888805', '88888809']
'''
The only valid trade style numbers at the time this pattern was created are:
888888
88888800
88888803
88888805
88888809
'''
TRADE_STYLE_PATTERN = "888888(?P<tradeStyle>\d{0}$|0[0359]$)"
''' EXACTLY 10 digits '''
PHONE_NUM_PATTERN = "^\d{10}$"
#Moved connection string into sqlite db
for row in sqlite3.connect('D:\\FTP\\ftp.db').execute("SELECT * FROM connection_strings WHERE code = 'wfa'"):
engine = create_engine(row[1], echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
LOG_LEVEL = logging.DEBUG
TERMS_CODES = { '00': 'No Terms' }
assignmentNumber = 0
fileNames = []
backouts = ['9999999999']
#Customer Master
class CustomerMaster(Base):
__tablename__ = 'TXUYKD00'
__table_args__ = {'autoload':True}
#Tables and Codes
class TXMYTX00(Base):
__tablename__ = 'TXMYTX00'
__table_args__ = {'autoload':True}
#Invoice header table
class InvoiceHeader(Base):
__tablename__ = 'TXUYRE00'
__table_args__ = {'autoload':True}
#Invoice detail table
class InvoiceDetail(Base):
__tablename__ = 'TXUYRE01'
__table_args__ = {'autoload':True}
#Order Header
class OrderHeader(Base):
__tablename__ = 'TXUYUV00'
__table_args__ = {'autoload':True}
#Backouts
class Invoice_Backouts(Base):
__table__ = Table('Invoice_Backouts', Base.metadata,
Column('b_Invoice', VARCHAR, primary_key=True),
autoload=True, autoload_with=engine
)
'''
Storing FTP site information in a sqlite db now to keep it out of source code
'''
def load_ftp_credentials():
vault = sqlite3.connect('D:\\FTP\\ftp.db')
cursor = vault.execute("SELECT * from ftp_auth where code='wf'")
for row in cursor:
return row
'''
Upload files to WF's ftp site for processing.
Global var TEST_MODE dictates which remote dir the files are uploaded into.
files should contain a list of fully qualified file paths to be sent.
'''
def sendFiles(files):
return
# global logger
# global TEST_MODE
# if(TEST_MODE):
# remoteDir = '/testin/'
# else:
# remoteDir = '/in/'
#
# creds = load_ftp_credentials()
#
# logger.info('Opening SFTP connection to %s:%s',REMOTE_HOST,REMOTE_PORT)
# transport = paramiko.Transport((REMOTE_HOST, REMOTE_PORT))
# transport.connect(username = creds[3], password = creds[4])
# sftp = paramiko.SFTPClient.from_transport(transport)
#
# for f in files:
# path = remoteDir + f
# logger.info('sftp.put(\'%s\', \'%s\')', ('D:\\FTP\\SEND_WF\\' + f), path)
# sftp.put(('D:\\FTP\\SEND_WF\\' + f), path)
#
# logger.debug('DIR %s = %s', remoteDir, sftp.listdir(remoteDir[:-1]))
#
# logger.info('Closing SFTP connection.')
# sftp.close()
# transport.close()
'''
Opens connection to database returning a session object.
'''
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
The result of this function is a single assignment file.
An assignment containing all of the invoices (or credits) for a single tradestyle on a given date.
Invoices and Credits (assuming all is done through the ERP method in jomar) are stored in the same
table.
An 'Invoice' for the purposes of transmitting to wf is defined as any invoice in the table with a
_positive_ dollar amount.
A 'Credit' is similarly defined as any invoice with a _negative_ dolar amount.
There is a flag cloumn in the table that designates what type of invoice it is (SO, SR, etc.) however
Caleb has expressed that the +/- methodology works well.
Valid Tradestyles: 00, 03, 05, 09
These are mapped to these factor accounts in Jomar: 888888, 88888803, 88888805, 88888809
d - the target date in YYYmmdd format
tStyle - The associated factor account for the target tradestyle ex. 888888 for 00
creditMemo - True to process credits, False to process invoices
ex: createAssignment(20160520, '888888', False) # Generate 00 invoice assignment for 05/20/16
This method returns a list containing the invoice/credit numbers that were processed. This is
used by the main method to build a complete list of invoices to flag as processed if the CSV files
are sent to Wells Fargo. This prevents any invoices processed by this script from being "backed out"
after the fact.
'''
def createAssignment(d, tStyle, creditMemo):
global assignmentNumber
global fileNames
global backouts
invoices = []
#Check for invalid trade styles, The script ignores invalid trade styles and proceeds if it can.
test = re.match(TRADE_STYLE_PATTERN, tStyle, flags=0)
if(test == None):
if not(tStyle == ''):
logger.warning('Ignoring Invalid Trade Style: \'%s\'', tStyle)
return invoices
elif(test.group('tradeStyle') == ''):
tradeStyleCode = '01'
logger.debug('Jomar trade style = %s --> Wells Fargo trade style = %s', tStyle, tradeStyleCode)
else:
tradeStyleCode = test.group('tradeStyle')
logger.debug('Jomar trade style = %s --> Wells Fargo trade style = %s', tStyle, tradeStyleCode)
#Get number of invoices for this trade style
#logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\'', d, tStyle)
### GR PATCHES ###
if(creditMemo):
logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF < 0;', d, tStyle)
invoiceCount = session.query(InvoiceHeader.RRNR).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts)).count()
else:
logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF > 0;', d, tStyle)
invoiceCount = session.query(InvoiceHeader.RRNR).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts)).count()
### GR PATCHES ###
if(invoiceCount == 0):
return invoices
if(creditMemo):
logger.info('**************************************************')
logger.info('Total number of %s CREDITS: %s', tradeStyleCode, invoiceCount)
else:
logger.info('**************************************************')
logger.info('Total number of %s INVOICES: %s', tradeStyleCode, invoiceCount)
#Trade style is valid, increment assignment
assignmentNumber += 1
#Open file for writing
fname = str(CLIENT_CODE) + tradeStyleCode + '_INV_' + str(d) + '_' + str(assignmentNumber) + '.csv'
logger.debug('Creating %s. Using \'excel\' dialect for csv writer.', fname)
f = open('SEND_WF\\' + fname, 'wb')
writer = csv.writer(f, dialect='excel')
fileNames.append(fname)
#Get total dollar amount of all invoices for this trade style
invoiceTotal = 0
#logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\'', d, tStyle)
### GR PATCHES ###
if(creditMemo):
logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF < 0;', d, tStyle)
for row in session.query(func.sum(InvoiceHeader.RWTS).label("sum")).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts)):
invoiceTotal = row.sum
else:
logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF > 0;', d, tStyle)
for row in session.query(func.sum(InvoiceHeader.RWTS).label("sum")).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts)):
invoiceTotal = row.sum
### GR PATCHES ###
if(creditMemo):
logger.info('Total dollar amount for %s CREDITS: %s', tradeStyleCode, '${:,.2f}'.format(invoiceTotal))
logger.info('**************************************************')
else:
logger.info('Total dollar amount for %s INVOICES: %s', tradeStyleCode, '${:,.2f}'.format(invoiceTotal))
logger.info('**************************************************')
if(creditMemo):
aType = 'C'
else:
aType = 'I'
writer.writerow(['TRL', CLIENT_CODE, tradeStyleCode, assignmentNumber, d, invoiceCount, aType, invoiceTotal])
#Loop over each invoice header record..
logger.info('<b>%s</b> \t <b>%s</b> \t <b>Amount</b>', 'Invoice'.ljust(10), 'Customer'.ljust(40))
### GR PATCHES ###
if(creditMemo):
q = session.query(InvoiceHeader).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts))
else:
q = session.query(InvoiceHeader).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts))
### GR PATCHES ###
for row in q:
logger.debug('Processing invoice %s', row.RRNR)
line = ['DTL', CLIENT_CODE, tradeStyleCode, assignmentNumber, d]
line.append(row.RRNR) #Invoice Number; DB length = 10, WF will accept 22
invoices.append(row.RRNR)
line.append(row.RDATRE)
line.append(row.RWTF) #Invoice amount; DB length = (15,5), WF will accept (12,2)
line.append('C') #Terms type, constant field determined based on docs from WF
line.append(row.RTXZ[2:]) #Our Terms Code; DB length = 5, WF will accept 15
if(len(TERMS_CODES[row.RTXZ[2:]]) > 30):
logger.warning('Terms description \'%s\' exceeds Wells Fargo character limit of 30! Sending \'%s\' instead.', TERMS_CODES[row.RTXZ[2:]], TERMS_CODES[row.RTXZ[2:]][:30])
line.append(TERMS_CODES[row.RTXZ[2:]][:30]) #Terms Description; DB length = 65, WF will accept 30
line.append(row.RKDR) #Bill to account number; DB length = 10, WF will accept 20
''' Grab customer contact info '''
logger.debug('SELECT * FROM TXUYKD00 WHERE TXUYKD00.KKDN = \'%s\'', row.RKDR)
q2 = session.query(CustomerMaster).filter(CustomerMaster.KKDN == row.RKDR)
if(q2.count() != 1):
logger.fatal('Failed to retrieve customer information from customer master table. Customer Number: %s Rows Returned: %s', row.RKDR, q2.count())
logger.fatal('**HALTING SCRIPT**')
logger.fatal('**NOTHING HAS BEEN SENT TO WELLS FARGO FOR %s**', d)
logger.debug('<--------------------------------------------------SCRIPT STOPPED---------------------------------------->')
sys.exit('Failed to retrieve customer information from customer master table.')
else:
for row2 in q2:
logger.info('%s \t %s \t %s', row.RRNR, row2.KNA2[:40].ljust(40), "{0:.2f}".format(row.RWTF).rjust(9))
if(len(row2.KNA2.replace(',',' ')) > 40):
logger.warning('Bill to account name \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA2.replace(',',' '), row2.KNA2.replace(',',' ')[:40])
line.append(row2.KNA2.replace(',',' ')[:40]) #Bill to account name; DB length = 65, WF will accept 40
if(len(row2.KNA3.replace(',',' ')) > 40):
logger.warning('Bill to address line \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA3.replace(',',' '), row2.KNA3.replace(',',' ')[:40])
line.append(row2.KNA3.replace(',',' ')[:40]) #Bill to addr1; DB length = 65, WF will accept 40
if(len(row2.KNA4.replace(',',' ')) > 40):
logger.warning('Bill to address line \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA4.replace(',',' '), row2.KNA4.replace(',',' ')[:40])
line.append(row2.KNA4.replace(',',' ')[:40]) #Bill to addr2; DB length = 65, WF will accept 40
line.append(row2.KORT.replace(',',' ')) #Bill to city; DB length = 28, WF will accept 40
line.append(row2.KSTA.replace(',',' ')) #Bill to state; DB length = 3, WF will accept 3
line.append(row2.KLND.replace(',',' ')) #Bill to country; DB length = 3, WF will accept 3
if(row2.KPLZ == '0'):
line.append('')
else:
line.append(row2.KPLZ.replace(',',' ')) #Bill to zip, DB length = 10, WF will accept 10
#Give it our best shot by stripping all non-digit characters first
p = re.sub('[\D\s]', '', row2.KTEL)
#Regex pattern tests if we have EXACTLY 10 DIGITS as required by Wells Fargo
test = re.match(PHONE_NUM_PATTERN, p, flags=0)
if(test == None):
line.append('') #Match failed, field is optional. Append empty string and move on
else:
line.append(p) #Bill to phone [optional]
#if(int(row.RVAL) == 0):
#line.append('') #Wells Fargo requires 8 characters if anything is to be sent at all. So we'll catch the 0 from the DB here and just insert an empty string
#else:
#line.append(row.RVAL) #As of date, dates in Jomar are already stored in the proper yyyymmdd format
line.append('') #Replaced
line.append('') #Intentionally left blank, "Store Number" column is N/A
''' Customer PO is optional as far as Wells-Fargo is concerned. We'll attempt to look it up '''
logger.debug('SELECT * FROM TXUYUV00 WHERE TXUYUV00.UBLN = \'%s\'', row.RBLN)
q2 = session.query(OrderHeader).filter(OrderHeader.UBLN == row.RBLN)
if(q2.count() != 1):
logger.error('Failed to retrieve customer PO number from order header table. Order Number on Invoice: %s Rows Returned: %s', row.RBLN, q2.count())
else:
for row2 in q2:
line.append(row2.UIHR.replace(',',' ')) #Customer PO; DB length = 15, WF will accept 15
writer.writerow(line)
logger.info('')
f.close()
return invoices
'''
For the purposes of running this via a web cgi interface I broke out the main logic of this script
into a 'main' function. See send_wf.py
iMode - a boolean, if True will prompt user for relevant info when running from a terminal
d - YYYYmmdd formatted date
sendNow - if True, the generated csv files will be sent immediatley to WF
email - if True the generated csv files are e-mailed to ar@example.com
'''
def main(iMode, d, sendNow, email=False):
global session
global backouts
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('wf_assignment_build')
logger.debug('<--------------------------------------------------SCRIPT STARTED---------------------------------------->')
logger.info('Running for date: %s', d)
session = loadSession()
#DL terms table to local dict for speedy lookup
logger.debug('Downloading terms code list.')
logger.debug('SELECT * FROM TXMYTX00 WHERE TXMYTX00.AXANR LIKE \'#Z%\'')
q = session.query(TXMYTX00).filter(TXMYTX00.AXANR.like('#Z%'))
for row in q:
if(row.AXANR == '#Z'):
continue
else:
TERMS_CODES[row.AXANR[2:]] = row.AXTXT[:65].strip().replace(',',' ')
#Figure out which trade styles need assignments for given date
tradeStyles = []
### GR PATCHES ###
'''
logger.debug('SELECT DISTINCT RFCT FROM TXUYRE00 WHERE TXUYRE00.RDATLS = \'%s\'', d)
for value in session.query(InvoiceHeader.RFCT).filter(InvoiceHeader.RDATLS == d).distinct():
tradeStyles.append(value.RFCT)
'''
tradeStyles.append('888888')
### GR PATCHES ###
logger.info('Invoices/Credits where created on %s for these trade styles: %s', d, tradeStyles)
#Get list of backed out invoices to ignore
logger.debug('Fetching list of backed out invoices for %s', d)
filter_string = 'b_create_date = ' + str(d)
logger.debug('SELECT * FROM Invoice_Backouts WHERE %s;', filter_string)
q = session.query(Invoice_Backouts).filter(Invoice_Backouts.b_create_date == d)
for row in q:
if row.o_create_date == d: #Check that create date of original invoice is also today
logger.warn('Invoice %s was created and backed out with invoice %s on the same day. These will not be transmitted!', row.o_Invoice, row.b_Invoice)
backouts.append(row.o_Invoice)
backouts.append(row.b_Invoice)
#Create assignment for each trade style
processed_invoices = []
for t in tradeStyles:
processed_invoices += createAssignment(d, t, False)
processed_invoices += createAssignment(d, t, True)
if(iMode):
if(raw_input('Send assignments to Wells Fargo? (n): ').lower() == 'y'):
sendFiles(fileNames)
if not TEST_MODE:
logger.info('Marking %s Invoices/Credits as processed...', len(processed_invoices))
ex = update(InvoiceHeader.__table__).where(InvoiceHeader.RRNR.in_(processed_invoices)).values(RFIL=u"X")
session.execute(ex)
session.commit()
sts = subprocess.call("python D:\\AS400_Invoice_Interface.py " + d, shell=True)
else:
if(sendNow):
sendFiles(fileNames)
if not TEST_MODE:
logger.info('Marking %s Invoices/Credits as processed...', len(processed_invoices))
ex = update(InvoiceHeader.__table__).where(InvoiceHeader.RRNR.in_(processed_invoices)).values(RFIL=u"X")
session.execute(ex)
session.commit()
if(email):
try:
logger.info('E-mailing generated CSVs to ar@example.com.')
print '<b>THIS MAY TAKE A MOMENT **DO NOT** REFRESH YOUR BROWSER</b> :)'
attachment = []
for f in fileNames:
attachment.append('D:\\FTP\\SEND_WF\\' + f)
send_alert_html('ar@example.com', 'Wells Fargo Assignments', message=MESSAGE.getvalue(), attachments=attachment)
except:
logger.warn('Unable to e-mail CSV files. Is the internet down?')
logger.warn('The CSVs can be found in D:\\FTP\\SEND_WF on the test server')
logger.debug(sys.exc_info()[0])
print 'See the <a href="http://stmtest:81/cgi-bin/logs.py?logname=wf">log file</a> for extra details...'
session.close()
logger.debug('<--------------------------------------------------SCRIPT STOPPED---------------------------------------->')
MESSAGE.truncate(0)
if(sendNow):
sts = subprocess.call("python D:\\AS400_Invoice_Interface.py " + d, shell=True)
if __name__ == '__main__':
#main(False, 20161108, False, email=True)
#sys.exit()
a = len(sys.argv)
interactive_mode = False
if(a == 2):
if(len(sys.argv[1]) == 8): #grab date from sys.argv[1] and run
DATE = int(sys.argv[1])
else:
sys.exit('Invalid date supplied ' + sys.argv[1] + '. Must be in yyyymmdd format.')
else:
interactive_mode = True
prompt = time.strftime("Date (%Y%m%d): ")
DATE = raw_input(prompt)
if(len(DATE) == 0):
DATE = int(time.strftime("%Y%m%d"))
elif(len(DATE) == 8):
d = time.strptime(DATE, "%Y%m%d")
prompt = datetime.fromtimestamp(time.mktime(d)).strftime("Generate assignments for %A, %B %d %Y? (n): ")
confirm = raw_input(prompt)
if(confirm == 'y' or confirm == 'Y'):
DATE = int(DATE)
else:
sys.exit('User cancelled script.')
else:
sys.exit('Date format doesn\'t look right... Exiting.')
main(interactive_mode, DATE, True)