Files

477 lines
20 KiB
Python

'''
Created on Nov 25, 2014
To process 850 documents from Momentum.
@author: Wes
'''
from alert import send_alert_html
import datetime, time
import sys
import os, re, StringIO
import logging
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import or_
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Momentum 850 Processed.'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
DELIMITER = '~'
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
class OrderHeader(Base):
__tablename__ = 'CSPEDI50H' #EDI 850 Header Table
__table_args__ = {'autoload':True}
class OrderDetail(Base):
__tablename__ = 'CSPEDI50D' #EDI 850 Detail Table
__table_args__ = {'autoload':True}
class ItemXref(Base):
__tablename__ = 'TXUYAT01' #Customer Item -> Sunbury Item reference table
__table_args__ = {'autoload':True}
class ItemInfo(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class mg_850_997(Base):
__tablename__ = 'mg_850_997'
__table_args__ = {'autoload':True}
class TXUYKD00(Base):
__tablename__ = 'TXUYKD00'
__table_args__ = {'autoload':True}
#----------------------------------------------------------------------
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 = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)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.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
This method reproduces the results of the RPG code in MG850I lines 721 thru 751.
Basically creates a 10 digit Sunbury PO based on their PO# and a prefix we determine
based on their Sunbury customer number
Turns out that for whatever reason the decision was made to have Momentum only send the
last 5 digits of their PO and we rebuild it on our end.... why? Eventually I'm going to ask
them to just send their real PO.
'''
def getSunburyPONum(sentPONum, sunburyCust):
if(sunburyCust >= 339800 and sunburyCust <= 339899):
return (4009300000 + sentPONum)
elif(sunburyCust >= 431800 and sunburyCust <= 431899):
return (2009300000 + sentPONum)
elif(sunburyCust >= 523000 and sunburyCust <= 523099):
return (8209300000 + sentPONum)
#We don't want to blow things up if this fails so just insert what they sent to us
return sentPONum
'''
*** DEPRECATED DO NOT USE ***
*** Preserving this in case I feel like laughing at how bad it is in the future ***
This method reproduces the results of the RPG code in MG850I lines 670 thru 719.
Although this would be better if we just queried the server with the address to get
the ship to #.
'''
def getSunburyCustNum(sentVendorId, sentVendorAddr1, sentVendorCity):
global logger
addressList = ['17811 FITCH ST.', '2600 SURRETT DR.', '1450 EAST WALNUT AVE.', '4552 136TH AVE.', '1715 MEYERSIDE DR, UNIT #2', '655 BRIGHAM RD., STE 100']
cityList = ['IRVINE', 'ARCHDALE', 'FULLERTON', 'HOLLAND', 'MISSISSAUGA ON', 'GREENSBORO']
#Set up base account numbers
ret = 0
if(sentVendorId == 6975):
ret = 339899
elif(sentVendorId == 36):
ret = 431899
elif(sentVendorId == 7738):
ret = 523099
elif(sentVendorId == 2082):
ret = 406399
else:
logger.error('They\'ve transmitted a vendor number we haven\'t encountered before: ' + str(sentVendorId))
raise ValueError("They've transmitted a vendor number we haven't encountered before: " + str(sentVendorId))
#Calculate ship to portion of account number based on transmitted bill to address
if(sentVendorAddr1 == addressList[0] and sentVendorCity == cityList[0]):
return (ret - 99)
elif(ret == 339899):
if(sentVendorAddr1 == addressList[1] and sentVendorCity == cityList[1]):
return (ret - 98)
elif(ret == 431899):
if(sentVendorAddr1 == addressList[3] and sentVendorCity == cityList[3]):
return (ret - 98)
elif(ret == 523099):
if(sentVendorAddr1 == addressList[1] and sentVendorCity == cityList[1]):
return (ret - 98)
elif(sentVendorAddr1 == addressList[5] and sentVendorCity == cityList[5]):
return (ret - 98)
elif(sentVendorAddr1 == addressList[2] and sentVendorCity == cityList[2]):
return (ret - 97)
elif(sentVendorAddr1 == addressList[3] and sentVendorCity == cityList[3]):
return (ret - 96)
elif(sentVendorAddr1 == addressList[4] and sentVendorCity == cityList[4]):
return (ret - 95)
return ret #If the address transmitted is not recognized return 99 ship to code for 'WILL ADVISE'
'''
Attempting to rewrite the above so it's not garbage
'''
def getSunburyCustNum2(session, sentVendorId, sentVendorAddr1, sentVendorCity):
global logger
parent = ''
sentVendorAddr1 = sentVendorAddr1.upper()
sentVendorAddr1 = sentVendorAddr1.replace('DR.', 'DRIVE') #Probably the first of many such bandaids...
sentVendorAddr1 = sentVendorAddr1.replace('RD., STE 100', 'RD') #Bandiad number 2
if(sentVendorId == 6975):
parent = '3398'
elif(sentVendorId == 36):
parent = '4318'
elif(sentVendorId == 7738):
parent = '5230'
elif(sentVendorId == 2082):
parent = '4063'
else:
logger.error('They\'ve transmitted a vendor number we haven\'t encountered before: ' + str(sentVendorId))
raise ValueError("They've transmitted a vendor number we haven't encountered before: " + str(sentVendorId))
logger.debug('Attempting to look up ship to account. Parent: %s, Street: %s, City: %s ', parent, sentVendorAddr1, sentVendorCity)
q = session.query(TXUYKD00).filter(TXUYKD00.KKDR == parent, TXUYKD00.KNA3 == sentVendorAddr1, TXUYKD00.KORT == sentVendorCity)
if(q.count() == 1):
for row in q:
logger.debug('Ship to account found: %s', row.KKDN)
return row.KKDN
else:
logger.debug('Ship to account not found. Using: %s', (parent + '00'))
return (parent + '00')
def lookUpSunburyItem(session, patt, colo, ven):
global logger
k_pat = patt
k_col = re.compile(r'[^\d.]+').sub('', colo).zfill(4)
q = session.query(ItemXref,ItemInfo).order_by(ItemXref.UACCOL).filter(ItemXref.UACCNR == str(ven)[:-2], ItemInfo.XFLX33 == k_pat, ItemInfo.XFLX34 == k_col, ItemXref.UACANR == ItemInfo.XANR, or_(ItemXref.UACCOD == None, ItemXref.UACCOD != 'N'))
if(q.count() == 1):
for row in q:
logger.info('STM Pattern: ' + k_pat + ' Color: ' + k_col + ' --> Sunbury Item: ' + row[0].UACANR)
return row[0].UACANR
else:
logger.warn('Failed to lookup item number for: ' + k_pat + '-' + k_col + ' is the cross reference entered in Jomar?')
if(q.count() > 1):
for r in q:
logger.debug(r)
return ''
def processMomentumPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('mg_850_interface')
logger.info('****************************************')
seq = '00000'
#Read transmission file into list
logger.info('Processing: ' + inFile)
f = open(inFile, 'r')
seq = inFile.split('_')[2].split('.')[0]
logger.info('Sequence Number: ' + seq)
lines = f.readlines()
f.close()
#Trim white space chars off the end of lines
for x in range(0, len(lines)):
lines[x] = lines[x].strip()
#Check number of lines parsed against number supposed to be transmitted
rec = lines[len(lines) - 1].split(DELIMITER)
if not(rec[0] in 'S'):
logger.error('Transmission file missing summary record..')
raise ValueError('Transmission file missing summary record..')
elif not((len(lines) - 1) == int(rec[1])):
logger.error('Number of records actually transmitted does not match what they said they transmitted..')
raise ValueError('Number of records actually transmitted does not match what they said they transmitted..')
else:
logger.info('They transmitted ' + rec[1] + ' lines and we received ' + str((len(lines) - 1)) + ' lines. Good.')
#Determine if file is prod, test, or invalid
if(lines[0] in 'P'):
logger.info('Production Transmission File')
elif(lines[0] in 'T'):
logger.info('Test Transmission File')
else:
logger.error('Invalid file: The first line should = "P" or "T"')
raise ValueError('Invalid file: The first line should = "P" or "T"')
#parse control record
logger.info('Parsing Control Record...')
rec = lines[1].split(DELIMITER)
if not(rec[0] in 'TP'):
logger.error('Control Record Corrupt/Missing!')
raise ValueError('Control Record Corrupt/Missing...')
sender_edi_qual = int(rec[1])
sender_edi_id = int(rec[2])
recvr_edi_qual = int(rec[3])
recvr_edi_id = int(rec[4])
transaction_date = datetime.datetime.strptime(rec[5], "%Y%m%d")
transaction_no = int(rec[6])
logger.debug('\t Sender EDI Qualifier: ' + str(sender_edi_qual))
logger.debug('\t Sender EDI ID Number: ' + str(sender_edi_id))
logger.debug('\tReceiver EDI Qualifier: ' + str(recvr_edi_qual))
logger.debug('\tReceiver EDI ID Number: ' + str(recvr_edi_id))
logger.debug('\t Transaction Date: ' + transaction_date.strftime('%m/%d/%Y'))
logger.debug('\t Transastion Control #: ' + str(transaction_no))
#########################BEGIN NEW LOOP#########################
logger.info('Opening database connection...')
session = loadSession()
orderHeaders = []
orderLines = []
po_997 = []
lineNum = 1
po_number = 0
for x in range(2, (len(lines) - 1)):
rec = lines[x].split(DELIMITER)
if not(rec[0] in ['H', 'D']):
logger.error('Encountered a non-header/detail record in body of transmission. Record: ' + str(x))
logger.error(lines[x])
raise ValueError('Record ' + str(x) + ' appears to be corrupt!')
if(rec[0] == 'H'):
#Parse Header Record
logger.info('Parsing Purchase Order Header...')
lineNum = 1
po_type = rec[1].strip()
vendor_id = int(rec[2])
po_number = int(rec[3])
po_997.append([transaction_no, rec[3].zfill(5)])
po_date = datetime.datetime.strptime(rec[4], "%Y%m%d")
bill_to_name = rec[5].strip()
bill_to_addr1 = rec[6].strip()
bill_to_addr2 = rec[7].strip()
bill_to_city = rec[8].strip()
bill_to_state = rec[9].strip()
bill_to_zip = rec[10].strip()
ship_to_name = rec[11].strip()
ship_to_addr1 = rec[12].strip()
ship_to_addr2 = rec[13].strip()
ship_to_city = rec[14].strip()
ship_to_state = rec[15].strip()
ship_to_zip = rec[16].strip()
ship_via = rec[17].strip()
pay_terms = rec[18].strip()
fob = rec[19].strip()
tax_exempt_no = rec[20].strip()
vendor_message = rec[21].strip() #Not used apparently
order_message = rec[22].strip()
express_order = rec[23].strip()
req_ship_date = datetime.datetime.strptime(rec[24], "%Y%m%d")
logger.debug('\t PO Type: ' + po_type)
logger.debug('\t Vendor ID: ' + str(vendor_id))
logger.debug('\t PO Number: ' + str(po_number))
logger.debug('\t PO Date: ' + po_date.strftime('%m/%d/%Y'))
logger.debug('\t Bill To Name: ' + bill_to_name)
logger.debug('\t Bill To Address 1: ' + bill_to_addr1)
logger.debug('\t Bill To Address 2: ' + bill_to_addr2)
logger.debug('\t Bill To City: ' + bill_to_city)
logger.debug('\t Bill To State: ' + bill_to_state)
logger.debug('\t Bill To Zip: ' + bill_to_zip)
logger.debug('\t Ship To Name: ' + ship_to_name)
logger.debug('\t Ship To Address 1: ' + ship_to_addr1)
logger.debug('\t Ship To Address 2: ' + ship_to_addr2)
logger.debug('\t Ship To City: ' + ship_to_city)
logger.debug('\t Ship To State: ' + ship_to_state)
logger.debug('\t Ship To Zip: ' + ship_to_zip)
logger.debug('\t Freight Carrier: ' + ship_via)
logger.debug('\t Payment Terms: ' + pay_terms)
logger.debug('\t FOB Point: ' + fob)
logger.debug('\t Tax Exempt Number: ' + tax_exempt_no)
logger.debug('\t Vendor Message: ' + vendor_message)
logger.debug('\t Order Message: ' + order_message)
logger.debug('\t Express Order Code: ' + express_order)
logger.debug('\t Requested Ship Date: ' + req_ship_date.strftime('%m/%d/%Y'))
#sunburyCust = getSunburyCustNum(vendor_id, ship_to_addr1, ship_to_city)
sunburyCust = getSunburyCustNum2(session, vendor_id, ship_to_addr1, ship_to_city)
#logger.info('Using sold-to account number: ' + str(sunburyCust))
logger.info('Using sold-to account number: ' + sunburyCust[:4])
logger.info('Using ship-to account number: ' + sunburyCust)
logger.info('PO Number: ' + str(getSunburyPONum(po_number, int(sunburyCust))))
#Create Object to be persisted to database
header = OrderHeader()
#Set up constant values in order header record
header.UBNR = '001' #Business Unit
header.UPLT = '00' #Plant #
header.ULOAD = 'N' #Flag to allow order to be processed in Jomar EDI workbench
header.UBLA = 'SO' #Jomar order type "SO" = Sales Order
header.UDAT = int(transaction_date.strftime('%Y%m%d')) #Transaction Date -> Order Date
header.FOB = ' ' #This customer doesn't transmit this information
header.UIHR = getSunburyPONum(po_number, int(sunburyCust)) #Will we continue to calculate the PO numbers in this way?
header.UIHS = int(po_date.strftime('%Y%m%d')) #PO date
#header.UKDN = (sunburyCust + 1) #Ship to customer #
header.UKDN = sunburyCust #Ship to customer #
header.UKDR = str(sunburyCust)[:4] #Bill to customer #
header.XTX5 = pay_terms #Payment terms
header.XTX6 = order_message #Jomar "notes" field; sticking order message here
header.URLRV = '0' #An ID column of sorts, in case the same PO is transmitted twice
header.UNA2 = ship_to_name
header.UNA3 = ship_to_addr1
header.UNA4 = ship_to_addr2
header.UORT = ship_to_city
header.USTA = ship_to_state[:2]
header.ULND = 'US'
header.UPLZ = ship_to_zip
if(po_type == 'ORIGINAL'):
header.FLX01 = '00'
orderHeaders.append(header)
elif(rec[0] == 'D'):
logger.info('Parsing PO Line Record...')
vendor_pattern = rec[1].strip()
vendor_color = rec[2]
vendor_finish = rec[3].strip()
prod_desc = rec[4].strip()
qty_ordered = float(rec[5])
unit_of_measure = rec[6].strip()
price_per_unit = float(rec[7])
comment_1 = rec[8].strip()
comment_2 = rec[9].strip()
comment_3 = rec[10].strip()
comment_4 = rec[11].strip()
comment_5 = rec[12].strip()
logger.debug('\t STM Pattern: ' + vendor_pattern)
logger.debug('\t STM Color: ' + str(vendor_color))
logger.debug('\t STM Finish: ' + vendor_finish)
logger.debug('\t Product Description: ' + prod_desc)
logger.debug('\t Quantity Ordered: ' + str(qty_ordered))
logger.debug('\t Units Priced By: ' + unit_of_measure)
logger.debug('\t Price per Unit: ' + str(price_per_unit))
logger.debug('\t Comment 1: ' + comment_1)
logger.debug('\t Comment 2: ' + comment_2)
logger.debug('\t Comment 3: ' + comment_3)
logger.debug('\t Comment 4: ' + comment_4)
logger.debug('\t Comment 5: ' + comment_5)
#Look up internal item number
i = prod_desc.split('-')[1]
s = lookUpSunburyItem(session, vendor_pattern, str(vendor_color), sunburyCust)
internal_item = s
#logger.info('Sunbury item number for ' + i + ' = ' + str(s))
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = lineNum
oline.VMGS = qty_ordered
oline.VPR2 = price_per_unit
oline.VANR = internal_item
oline.VKDR = str(sunburyCust)[:4]
#oline.VKDL = (sunburyCust + 1)
oline.VKDL = sunburyCust
oline.VIHR = getSunburyPONum(po_number, int(sunburyCust))
oline.VFLX01 = vendor_pattern
oline.VFLX02 = vendor_color
oline.VRLRV = '0'
lineNum = lineNum + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
for h in orderHeaders:
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == h.UKDR, OrderHeader.UIHR == h.UIHR).count()
if(dups > 0):
logger.warning('PO ' + str(h.UIHR) + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
h.URLRV = str(dups)
for line in orderLines:
if(line.VIHR == h.UIHR):
line.VRLRV = str(dups)
#submit
logger.info('Writing records to database...')
for h in orderHeaders:
session.add(h)
for line in orderLines:
session.add(line)
for po in po_997:
c = mg_850_997()
c.trans_no,c.po = po
c.proc_date = time.strftime('%Y%m%d')
session.add(c)
if not(TEST_MODE):
session.commit()
else:
logger.warn('**RUNNING IN TEST MODE ROLLING BACK TRANSACTION**')
session.rollback()
session.close()
if not(TEST_MODE):
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not(TEST_MODE):
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\MG\\')
os.rename(inFile, 'D:\\FTP\\OLD\\MG\\' + inFile.split('\\')[-1])
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
processMomentumPo(sys.argv[1])