Found more code examples from STM. Used claude to generate a summary (CLAUDE.md)

This commit is contained in:
wes
2026-02-11 06:05:20 -05:00
parent cf0edba776
commit c4b970cbde
33 changed files with 6077 additions and 1 deletions
+51
View File
@@ -0,0 +1,51 @@
'''
American Custom Finishing ASN Generate
ACF gets a differently structured document (than HTX/SYF/ATX) but with mostly the same information.
It appears to be pipe ('|') delimited.
Fields IN ORDER:
Constant - 'ST'
Constant - 'ST'
Order/Roll
STYLE1-STYLE2
Pattern Name
???
Roll length ex. 061.250
PO number of some sort
Color number
Customer Name
Ship to Address 1
Ship to Address 2
Ship to City, State, Zip 'SUNBURY PA 17801'
??? Looks like a finish code
**Not Used**
**Not Used**
???
???
Finished Description (?)
**Not Used**
**Not Used**
**Not Used**
**Not Used**
**Not Used**
**Not Used**
???
???
'''
import csv
if __name__ == '__main__':
lines = []
data = []
with open('G:\\FTP\\ACF_SAMPLE.txt') as f:
lines = f.readlines()
for line in lines:
row = line.split('|')
for i,field in enumerate(row):
row[i] = field.strip()
data.append(row)
print row
with open('G:\\FTP\\ACF_SAMPLE_FIXED.txt', 'w') as out:
#derp = csv.writer(out, delimiter ='|',quotechar =',',quoting=csv.QUOTE_NONE,escapechar='*')
derp = csv.writer(out, delimiter ='|',quoting=csv.QUOTE_NONE)
derp.writerows(data)
+250
View File
@@ -0,0 +1,250 @@
'''
Created on September 12, 2016
Interface ACF "Inventory Shipped" transmission.
For now I think I'm going to insert 08 transactions into TXRYBW10; creating rolls in Jomar and putting them
right into the ACF drop shipment warehouse.
@author: Wes
'''
import csv, logging, sys, os, re
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import pyodbc
TEST_MODE = False
# @TODO: Could probably do with making this pattern a little more discerning for the portion preceding the '/'
PROD_ROLL_PAT = ".+[\/]\d+"
#Assign the column indexes to descriptive variables; makes the code below a bit easier to follow.
CUST_ID = 0
ROLL_NUMBER = 1
SHIP_TO_ADDR_NAME = 2
CONTRACT_NUMBER = 3
STYLE_NAME = 4
STYLE_DESC = 5
DATE_RECEIVED = 6
DATE_COMPLETED = 7
DUE_DATE = 8
YARDS_RECEIVED = 9
YARDS_FINISHED = 10
ACF_SHIP_DATE = 11
SHIP_METHOD = 12
PICK_BATCH = 13
TRACKING = 14
UPS_COST = 15
CUST_FINISH_NO = 16
CUST_PO_NO = 17
logger = None
LOG_LEVEL = logging.DEBUG
#engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
engine = create_engine("mssql+pyodbc://jTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
class TXRYBW10(Base):
__tablename__ = 'TXRYBW10'
__table_args__ = {'autoload':True}
class TXUYUV00(Base):
__tablename__ = 'TXUYUV00' #Order Header
__table_args__ = {'autoload':True}
class TXUYUF01(Base):
__tablename__ = 'TXUYUF01' #Order Detail
__table_args__ = {'autoload':True}
class ItemInfo(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
########################################################################
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
#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)
def queryTICKETFL(uNum, ticket, aPatt):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = []
c1.execute("SELECT LINE#, PATNAM, COLOR# FROM STMDATA.TICKETFL WHERE ORDERA = '" + uNum + "' AND TICK# = " + ticket)
for row in c1:
for d in row:
ret.append(d)
c1.close()
#Verify that the pattern name ACF transmitted matches our records and return if so
if(len(ret) > 0 and ret[1].strip() == aPatt):
return ret
else:
return []
def backToSunbury(customerRoll, record):
global logger
# @TODO: Method stub for when we eventually do need to process these.
if(customerRoll):
logger.debug('Ignoring roll that did not drop ship: ' + record[ROLL_NUMBER])
else:
logger.debug('Ignoring non-production roll number: ' + record[ROLL_NUMBER])
def shippedToCust(customerRoll, record):
global logger
# @TODO: Method stub for when we eventually do need to process these.
if(customerRoll):
pass
else:
logger.debug('Ignoring non-production roll number: ' + record[ROLL_NUMBER])
def createInventoryTransaction(jOrder, jLine, jItem, roll, yards):
jLot = None #Optional we don't use lot numbers for rolls, could maybe throw YYYYMMdd in here to tie the jomar rolls to the tranmission.
i = TXRYBW10()
i.RWAPL = '00'
i.RWSPL = '00'
i.RWSLA = 'SO'
i.RWPLT = '00'
i.RWBNR = '001'
i.RWALA = 'SO'
i.RWBEW = '08' #Transaction type. '08' = Production receipt. This creates a new roll
i.RWALN = jOrder
i.RWSLN = jOrder
i.RWAFN = jLine
i.RWSFN = jLine
i.RWANR = jItem
i.RWCHA = jLot
i.RWRNR = roll
i.RWPGLG = yards
i.RWPLG = yards
i.RWPWG = 0 #This is a weight column. What to do about this I wonder?
i.RWTWG = 0 #This is a weight column. This should equal RWPWG
#All rolls process by this script are going into ACF drop ship warehouse for now
i.RWLOC = '11' #Warehouse
i.RWLPL = '' #Bin number. ACF warehouse is not bin tracked so ''
return i
def processACF(inFile):
global logger
os.chdir('D:\FTP')
initializeLogger('ACF_interface')
logger.info('****************************************')
session = loadSession()
prodRollPat = re.compile(PROD_ROLL_PAT)
rawLines = []
logger.info('Loading: ' + inFile)
try:
f = open(inFile, 'r')
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
except IOError:
logger.fatal('Unable to load file. Exiting...')
return
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
# @TODO: Figure out naming convention for these so we can save them in \OLD\ACF
#logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\KF\\')
#os.rename(inFile, 'OLD\\KF\\' + fileName)
sys.exit("Nothing to process.")
#Main processing loop
for line in rawLines:
#There ID for us is 'ST' evidently, we shouldn't process anything that isn't ST. Just in case...
if not line[CUST_ID] == 'ST':
logger.warn('Ignoring non-ST row...')
#Check data in "Roll Number" column to see if it's a production roll
#Production pieces should be in "ORDER#/ROLL#" format
if not(prodRollPat.match(line[ROLL_NUMBER]) == None):
#Process only rolls that drop shipped
#This is kinda hacky. Just looking for 'Sunbury' in the ship-to name...
if not 'sunbury' in line[SHIP_TO_ADDR_NAME].lower():
logger.info('**********')
as400Order, rollNumber = line[ROLL_NUMBER].split('/')
jOrder = None
jLine = None
jItem = None
logger.info('ACF drop shipped roll ' + rollNumber + ' to customer. Transferring it to warehouse 11...')
#Translate AS400 order number to Jomar Order number
logger.debug('SELECT * FROM TXUYUV00 WHERE TXUYUV00.UBNR = \'001\' AND TXUYUV00.UPLT = \'00\' AND TXUYUV00.UBLA = \'SO\' AND (TXUYUV00.URBPO = \'' + as400Order + '\' OR TXUYUV00.URBPO = \'' + as400Order[1:] + '\');')
q = session.query(TXUYUV00).order_by(TXUYUV00.UBLN).filter(TXUYUV00.UBNR == '001', TXUYUV00.UPLT == '00', TXUYUV00.UBLA == 'SO', ((TXUYUV00.URBPO == as400Order) | (TXUYUV00.URBPO == as400Order[1:])))
if(q.count() == 1):
for r in q:
jOrder = r.UBLN
logger.info('AS400 Order: ' + as400Order + ' --> Jomar Order: ' + jOrder)
else:
logger.error('Could not locate Jomar order for: ' + as400Order + ' skipping...')
continue
ticketflInfo = queryTICKETFL(as400Order, rollNumber, line[STYLE_DESC])
if(len(ticketflInfo) == 0):
logger.error('Failed to look up ticket in AS400. Skipping...')
continue
as400Line = ticketflInfo[0]
colorNum = ticketflInfo[2]
#Look up order line in Jomar using Jomar order number + AS400 line number
logger.debug('SELECT VLFN, VANR, XFLX33, XFLX34 FROM TXUYUF01 LEFT JOIN TXMYAT03 ON TXUYUF01.VBNR = TXMYAT03.XBNR AND TXUYUF01.VPLT = TXMYAT03.XPLT AND TXUYUF01.VANR = TXMYAT03.XANR WHERE TXUYUF01.VBNR = \'001\' AND TXUYUF01.VPLT = \'00\' AND TXUYUF01.VBLA = \'SO\' AND TXUYUF01.VBLN = \'' + jOrder + '\' AND TXUYUF01.VPLIN = ' + str(as400Line) + ';')
q = session.query(TXUYUF01,ItemInfo).order_by(TXUYUF01.VBLN).filter(TXUYUF01.VBNR == ItemInfo.XBNR, TXUYUF01.VPLT == ItemInfo.XPLT, TXUYUF01.VANR == ItemInfo.XANR).filter(TXUYUF01.VBNR == '001', TXUYUF01.VPLT == '00', TXUYUF01.VBLA == 'SO', TXUYUF01.VBLN == jOrder, TXUYUF01.VPLIN == as400Line)
if(q.count() == 1):
for r in q:
jLine = r[0].VLFN #Jomar Line Number
jItem = r[0].VANR #Jomar Item Number
logger.info('AS400 Order Line: ' + str(as400Line) + ' --> Jomar Line: ' + str(jLine))
logger.info('Jomar Item Number: ' + jItem)
#Create inventory transaction here
print createInventoryTransaction(jOrder, jLine, jItem, rollNumber, line[YARDS_FINISHED])
else:
backToSunbury(True, line)
else:
if not 'sunbury' in line[SHIP_TO_ADDR_NAME].lower():
#Drop shipped
shippedToCust(False, line)
else:
#Back to Sunbury
backToSunbury(False, line)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'No file name specified!'
else:
processACF(sys.argv[1])
+96
View File
@@ -0,0 +1,96 @@
'''
Melissa asked to be e-mailed the total yardage of our un-shipped, un-invoiced finish goods inventory.
@author: Wes
'''
from alert import send_alert_html
import time, logging, sys, os, StringIO, csv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com', 'user1@example.com']
SUBJECT = 'FGI Report'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
SUM_QUERY = 'select SUM(RTPLG) [total_yards_produced] from TXRYRT00 WHERE (RTSLG = 0.0 AND RTILG = 0.0)'
DETAIL_QUERY = 'select RTALN [order_num], RTRNR [roll_num], RTANR [item_num], xartbz [item_desc], RTLOC [warehouse], RTLPL [bin], RTPLG [yards_produced], RTSLG [yards_shipped], RTILG [yards_invoiced] from TXRYRT00 LEFT JOIN txmyat03 ON RTANR = XANR where (RTSLG = 0.0 AND RTILG = 0.0) ORDER BY RTALN, RTRNR'
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
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.DEBUG)
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)
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def run_FGI_report():
os.chdir('D:\FTP')
initializeLogger('FGI_Report')
logger.debug('Opening connection with DB...')
session = loadSession()
logger.debug('Running SUM query...')
for row in session.execute(SUM_QUERY):
logger.info('Total Finished Goods Inventory: %s yards', "{0:.3f}".format(row.total_yards_produced))
#Open file for writing
logger.debug('Creating Aaron\'s csv file...')
fname = time.strftime("FGI_%Y%m%d.csv")
logger.debug('Creating %s. Using \'excel\' dialect for csv writer.', fname)
f = open(fname, 'wb')
writer = csv.writer(f, dialect='excel')
writer.writerow(['order_num', 'roll_num', 'item_num', 'item_desc', 'warehouse', 'bin', 'yards_produced', 'yards_shipped', 'yards_invoiced'])
logger.debug('Running detail query...')
for row in session.execute(DETAIL_QUERY):
writer.writerow(row)
f.close()
try:
logger.debug('Sending e-mail...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[fname], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
logger.debug('Deleting %s', fname)
os.remove(fname)
if __name__ == '__main__':
run_FGI_report()
+82
View File
@@ -0,0 +1,82 @@
'''
Generate Hitex transmission files
The AS400 essentially transmits copies of the FINMAS and FINDET files, which can be found in STMDATA, directly to
the Hitex/Syn-fin/Applied. As such I used the record formats for those files to buld the MASTER and DETAIL record format strings
you see below.
I strongly doubt Hitex/Syn-fin/Applied require _all_ of the fields in these tables. Who requires what, though, remains
a mystery. I've listed the description of the fields as they appear in the AS400 in the order they need to be passed to
the format() command below.
* denotes fields that are definitely expanding.
FINMAS Fields, IN ORDER:
Address 1
Customer Name
Customer Number (4 digit bill to account)
Ship to Number (2 digit shipto sub-account)
* These 2 fields (6 digits) --> 10 digit account #
Ship to Name
Ship to Address 1
Ship to Address 2
Ship to City
Ship to State
Ship to Zip
Ship to Country
Country Code
Packing List Number
* 5 digit --> 10 digit
Shipping Suffix (?)
Box Number (?)
Stain Repellent Code
* 1 char --> 5 char
Backing Code
* 1 char --> 5 char
Complete Code (?)
Number of Labels Needed (?)
Date yyMMdd
* yyMMdd --> YYYYMMdd (Preferable, not required)
Finish Code
* 4 char --> 5 char
FINDET Fields, IN ORDER:
Packing List Number
* 5 digit --> 10 digit
Shipping Suffix (?)
Box Number (?)
Pattern Name
Style 1
Style 2
Color Number
Order Number
* 6 character --> 10 digit
P/O Number
Roll Number
* 5 digit --> 10 digit
Stain Repellent Code
* 1 char --> 5 char
Backing Code
* 1 char --> 5 char
Complete Code (?)
Cut off Yards
Cut off Eights
Finished Yards
Finished Eights
Finisher Invoice Number
Finisher BOL Number
Finisher Description
Application Code (?)
Finish Code
* 4 char --> 5 char
Customer Pattern
Customer Color
'''
MASTER_RECORD_FORMAT = '{:30s}{:30s}{:4s}{:2s}{:30s}{:30s}{:30s}{:20s}{:2s}{:05d}{:20s}{:10s}{:05d}{:1s}{:02d}{:1s}{:1s}{:1s}{:03d}{:06d}{:4s}'
DETAIL_RECORD_FORMAT = '{:05d}{:1s}{:02d}{:15s}{:>4s}{:06d}{:04d}{:6s}{:15s}{:05d}{:1s}{:1s}{:1s}{:03d}{:01d}{:03d}{:01d}{:7s}{:06d}{:15s}{:2}{:4s}{:15}{:20}'
if __name__ == '__main__':
print MASTER_RECORD_FORMAT.format('PER ROUTING GUIDE','', '', '', 'SUNBURY TEXTILE MILLS, INC.', 'MILLER STREET',
'', 'SUNBURY', 'PA', 17801, '', '', 54065, '', 0, '', '', '', 1, 160719, '')
#print DETAIL_RECORD_FORMAT.format(18342, ' ', 0, 'SYNTAX', 'P', 9953, 2301, 'U51959', '729005S', 25524, '', '', '', 62, 0, 0, 0, '251348', 65, '', '54', '', '', '15310/565/STRAWBERRY')
+8
View File
@@ -0,0 +1,8 @@
#Kravet POs
Get-ChildItem "D:\FTP" -Filter Kravet_to_vendor_*.csv | Foreach-Object { python D:\FTP\kf_850.py $_.Name }
#Momentum POs
Get-ChildItem "D:\FTP" -Filter SBR_850_*.txt | Foreach-Object { python D:\FTP\mg_850.py $_.Name }
#Maharam POs
Get-ChildItem "D:\FTP" -Filter MAH_850_*.txt | Foreach-Object { python D:\FTP\mf_850.py $_.Name }
View File
+108
View File
@@ -0,0 +1,108 @@
'''
Used by other scripts (eventually) to abstract away e-mail alert related code.
'''
import sqlite3, smtplib, os
from email import Encoders
from email.MIMEBase import MIMEBase
from email.MIMEImage import MIMEImage
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
'''
I found the bulk of this function already written on someone's blog for sending mail
via outlook 365. I made a few modifications for our purposes.
'''
def send_mail(send_from, send_to, subject, text, files=None,
data_attachments=None, server="smtp.example.com", port=25,
tls=False, html=False, images=None,
config_file=None, config=None):
if files is None:
files = []
if images is None:
images = []
if data_attachments is None:
data_attachments = []
if config_file is not None:
config = ConfigParser.ConfigParser()
config.read(config_file)
if config is not None:
server = config.get('smtp', 'server')
port = config.get('smtp', 'port')
tls = config.get('smtp', 'tls').lower() in ('true', 'yes', 'y')
username = config.get('smtp', 'username')
password = config.get('smtp', 'password')
msg = MIMEMultipart('related')
msg['From'] = send_from
msg['To'] = send_to if isinstance(send_to, basestring) else COMMASPACE.join(send_to)
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = subject
msg.attach( MIMEText(text, 'html' if html else 'plain') )
for f in files:
part = MIMEBase('application', "octet-stream")
part.set_payload( open(f,"rb").read() )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
msg.attach(part)
for f in data_attachments:
part = MIMEBase('application', "octet-stream")
part.set_payload( f['data'] )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % f['filename'])
msg.attach(part)
for (n, i) in enumerate(images):
fp = open(i, 'rb')
msgImage = MIMEImage(fp.read())
fp.close()
msgImage.add_header('Content-ID', '<image{0}>'.format(str(n+1)))
msg.attach(msgImage)
smtp = smtplib.SMTP(server, int(port))
if tls:
smtp.starttls()
#Load do not reply account credentials from SQLite DB and authenticate
#cursor = sqlite3.connect('D:\\FTP\\ftp.db').execute("SELECT * from accounts where code = 'WMR'")
#auth = cursor.fetchone()
#smtp.login(auth[1], auth[2])
smtp.sendmail(send_from, send_to, msg.as_string())
smtp.close()
'''
Allows use of html in message. Wraps entire message in font tag that forces a fixed width font for now.
I'm not sure that's a great idea or not.
'''
def send_alert_html(to, subject, message='', attachments=None, sendFrom='donotreply@example.com'):
message = '<font face="Courier New, Courier, monospace"><pre>' + message + '</pre></font>'
message = message + '<br><br><br>'
message = message + '<font face="Lucida Sans Unicode"><b>Please do not reply to this message; it was sent from an unmonitored email address.</b></font>'
send_mail(sendFrom, to, subject, message, files=attachments, html=True)
def send_alert(to, subject, message='', attachments=None, sendFrom='donotreply@example.com'):
send_mail(sendFrom, to, subject, message, files=attachments)
if __name__ == '__main__':
#Send a test message if this script is executed
test_data = [['1234567890','9876543210'],
['0101379900','0000055555'],
['1000930000','0000052319']]
msg = 'Designtex has cut into the following pieces:<br><br><table border="1"><tr><th>Order #</th><th>Roll #</th></tr>'
for t in test_data:
msg = msg + '<td>' + t[0] + '</td><td>' + t[1] + '</td></tr>'
msg = msg + '</table>'
send_alert_html('admin@example.com', 'Designtex Test', message=msg)
+325
View File
@@ -0,0 +1,325 @@
'''
Created on Nov 3, 2015
@author: wes
'''
import ftplib
import sys, sqlite3
import logging
import os.path
from glob import glob
#logger = None
LOG_LEVEL = logging.DEBUG
################################################################################
def load_ftp_credentials(code):
vault = sqlite3.connect('D:\\FTP\\ftp.db')
cursor = vault.execute("SELECT * from ftp_auth where code='" + code + "'")
for row in cursor:
return [row[1], row[3], row[4]]
def gettext(ftp, filename, outfile=None):
if outfile is None:
outfile = sys.stdout
# use a lambda to add newlines to the lines read from the server
ftp.retrlines("RETR " + filename, lambda s, w=outfile.write: w(s+"\n"))
def getbinary(ftp, filename, outfile=None):
if outfile is None:
outfile = sys.stdout
ftp.retrbinary("RETR " + filename, outfile.write)
def getFileList(ftp):
log = []
ftp.retrlines('LIST', callback=log.append)
files = (line.rsplit(None, 1)[1] for line in log)
return list(files)
def upload(ftp, f):
ext = os.path.splitext(f)[1]
if ext in (".txt", ".htm", ".html", ".xml"):
ftp.storlines("STOR " + f.split('\\')[-1], open(f))
else:
ftp.storbinary("STOR " + f.split('\\')[-1], open(f, "rb"), 1024)
def initializeLogger(name='log'):
#global logger
#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)
return logger
################################################################################
def kravetOrderDownload():
logger = initializeLogger('kravet_850')
KRAVET = load_ftp_credentials('kf')
logger.info('Connecting to ' + KRAVET[0] + ' as ' + KRAVET[1])
ftp = ftplib.FTP(KRAVET[0])
ftp.login(KRAVET[1], KRAVET[2])
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('kravet_to_vendor')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + KRAVET[0])
ftp.quit()
def maharamOrderDownload():
logger = initializeLogger('Maharam_850')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd OUTGOING')
ftp.cwd('OUTGOING')
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('mah_850_')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def maharamInvoiceUpload():
logger = initializeLogger('Maharam_810')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd Incoming')
ftp.cwd('Incoming')
files = glob('D:\\FTP\\SEND_MF\\*.xml')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MF\\SENT')
os.rename(f, 'SEND_MF\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def maharamASNUpload():
logger = initializeLogger('Maharam_856')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd Incoming')
ftp.cwd('Incoming')
files = glob('D:\\FTP\\SEND_MF\\STM_856_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MF\\SENT')
os.rename(f, 'SEND_MF\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def momentumOrderDownload():
logger = initializeLogger('Momentum_850')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd outbound')
ftp.cwd('outbound')
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('sbr_850_') and f.lower().endswith('.txt')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def momentumInvoiceUpload():
logger = initializeLogger('Momentum_810')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd inbound')
ftp.cwd('inbound')
files = glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
#Create an empty file of the same name with '.done' extension and upload
#They require it for some reason....
doneFile = f.split('.')[0] + '.done'
file(doneFile, 'w').writelines([])
logger.info('Uploading ' + doneFile.split('\\')[-1])
upload(ftp, doneFile)
logger.info('Deleting ' + doneFile)
os.remove(doneFile)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MG\\SENT')
os.rename(f, 'SEND_MG\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def momentumASNUpload():
logger = initializeLogger('Momentum_856')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd inbound')
ftp.cwd('inbound')
files = glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
#Create an empty file of the same name with '.done' extension and upload
#They require it for some reason....
doneFile = f.split('.')[0] + '.done'
file(doneFile, 'w').writelines([])
logger.info('Uploading ' + doneFile.split('\\')[-1])
upload(ftp, doneFile)
logger.info('Deleting ' + doneFile)
os.remove(doneFile)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MG\\SENT')
os.rename(f, 'SEND_MG\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def glenravenASNDownload():
logger = initializeLogger('GR_INCOMING__856')
#They're using our FTP site for this
STM_FTP = load_ftp_credentials('stm')
logger.info('Connecting to %s as %s', STM_FTP[0], STM_FTP[1])
ftp = ftplib.FTP(STM_FTP[0])
ftp.login(STM_FTP[1], STM_FTP[2])
logger.info('cwd glenraven')
ftp.cwd('glenraven')
logger.info('cwd to_STM')
ftp.cwd('to_STM')
for f in getFileList(ftp):
if f in ['.', '..']:
continue
f_local = 'INCOMING\\' + f.replace('sunbury', 'GR_ASN')
if(os.path.isfile(f_local)):
logger.info('Already downloaded: %s', f)
else:
logger.info('Downloading: %s -> %s', f, f_local)
gettext(ftp, f, open(f_local, 'w'))
logger.info('Deleting: %s from FTP site', f)
ftp.delete(f)
logger.info('Closing connection to %s', STM_FTP[0])
ftp.quit()
def UNIFI_846_Download():
logger = initializeLogger('GR_INCOMING__856')
#They're using our FTP site for this
STM_FTP = load_ftp_credentials('stm')
logger.info('Connecting to %s as %s', STM_FTP[0], STM_FTP[1])
ftp = ftplib.FTP(STM_FTP[0])
ftp.login(STM_FTP[1], STM_FTP[2])
logger.info('cwd unifi')
ftp.cwd('unifi')
logger.info('cwd to_STM')
ftp.cwd('to_STM')
for f in getFileList(ftp):
if f in ['.', '..']:
continue
f_local = 'INCOMING\\' + f.replace('ipnet', 'UNIFI_846').replace('.file846', '')
f_local = f_local[:f_local.index('.csv') + 4]
if(os.path.isfile(f_local)):
logger.info('Already downloaded: %s', f)
else:
logger.info('Downloading: %s -> %s', f, f_local)
gettext(ftp, f, open(f_local, 'w'))
logger.info('Deleting: %s from FTP site', f)
ftp.delete(f)
logger.info('Closing connection to %s', STM_FTP[0])
ftp.quit()
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Missing parameter!'
sys.exit(0)
if(len(sys.argv) == 3):
l = sys.argv[2].lower()
if(l.startswith('debug')):
LOG_LEVEL = logging.DEBUG
elif(l.startswith('info')):
LOG_LEVEL = logging.INFO
elif(l.startswith('warning')):
LOG_LEVEL = logging.WARNING
elif(l.startswith('error')):
LOG_LEVEL = logging.ERROR
elif(l.startswith('critical')):
LOG_LEVEL = logging.CRITICAL
s = sys.argv[1].lower()
if(s.startswith('kf850')):
kravetOrderDownload()
if(s.startswith('mf850')):
maharamOrderDownload()
if(s.startswith('mg850')):
momentumOrderDownload()
if(s.startswith('mf810')):
maharamInvoiceUpload()
if(s.startswith('mf856')):
maharamASNUpload()
if(s.startswith('mg810')):
momentumInvoiceUpload()
if(s.startswith('mg856')):
momentumASNUpload()
if(s.startswith('gr856')):
glenravenASNDownload()
if(s.startswith('un846')):
UNIFI_846_Download()
+232
View File
@@ -0,0 +1,232 @@
'''
Create and send e-mail alert listing Designtex pieces that need to be invoiced.
'''
import logging, os, pyodbc, sys
from alert import send_alert_html
from sqlalchemy import create_engine, or_
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com', 'user3@example.com', 'user4@example.com', 'user1@example.com', 'user5@example.com']
#RECIPIENTS = ['admin@example.com']
SUBJECT = 'Designtex Reconciliation Report'
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ROLL_NUM = 1
JOMAR_LINE = 3
ITEM_NUM = 4
AS400_LINE = 5
PATTERN = 6
COLOR = 7
PACKING_LIST = 8
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
#engine = create_engine("mssql+pyodbc://JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class DTXCUTFL(Base): #EDI data from Designtex arrives here
__tablename__ = 'DTXCUTFL'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXUYUV00(Base): #Order header
__tablename__ = 'TXUYUV00'
__table_args__ = {'autoload':True}
class TXUYUF01(Base): #Order detail
__tablename__ = 'TXUYUF01'
__table_args__ = {'autoload':True}
class TXMYAT03(Base): #Sales item detail
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class AS400OrderLookupError(RuntimeError):
''' Raised if we fail to lookup the AS400 order number using the Jomar order number. '''
''' This will eventually go away '''
class JomarOrderLookupError(RuntimeError):
''' Raised if we fail to lookup the Jomar order number using the AS400 order number '''
''' This will eventually go away '''
class AS400LineLookupError(RuntimeError):
''' Raised if AS400 line numbers cannot be retrieved from the Jomar order '''
''' This will eventually go away '''
class RollNotFoundError(RuntimeError):
''' Raised if no record in TXRYRT00 is found for a given ticket number '''
class ItemLookupError(RuntimeError):
''' Raised if no record is found in TXMYAT03 for a given item number '''
''' This should never happen '''
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
#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 - %(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)
def getAS400PackingList(order, line, ticket):
global logger
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
q = "select SHP# from stmdata.XSHPFIL WHERE ORDERA = '"
q = q + order + "' and LINE# = "
q = q + line + " and TICK# = "
q = q + ticket
logger.debug(q)
c1.execute(q)
for row in c1:
ret = row[0]
c1.close()
return str(ret)
def dtxInvoiceAlert():
initializeLogger('dtx_cut_alert')
session = loadSession()
processed_records = []
logger.info('Querying DTXCUTFL for any unprocessed rolls...')
tickets = session.query(DTXCUTFL).filter(or_(DTXCUTFL.PROCESSED != 'Y', DTXCUTFL.PROCESSED == None))
#Load in unprocessed roll/order numers
for row in tickets:
processed_records.append([row.__dict__['STM_ORDER#'], row.__dict__['STM_TICK#']])
if len(processed_records) == 0:
logger.warn('Nothing to process. Exiting...')
return
#Fix/lookup Order Numbers
logger.info('Querying order header table for order numbers...')
for row in processed_records:
#Not sure whether they are transmitting new or old style order numbers, allow for both.
if(row[0].startswith('U')):
row.append(row[0]) #We want U number to be 3rd column
oHeader = session.query(TXUYUV00).filter(TXUYUV00.URBPO == row[0], TXUYUV00.UBLA == 'SO')
if oHeader.count() != 1:
raise JomarOrderLookupError('Failed to lookup Jomar order number. AS400 Order:', row[0])
for r in oHeader:
logger.debug('AS400 Order: %s --> Jomar Order: %s', row[0], r.UBLN)
row[0] = r.UBLN #Overwrite contents of first element with Jomar Order
else:
oHeader = session.query(TXUYUV00).filter(TXUYUV00.UBLN == row[0])
if oHeader.count() != 1:
raise AS400OrderLookupError('Failed to lookup AS400 order number. Jomar Order:', row[0])
for r in oHeader:
logger.debug('Jomar Order: %s --> AS400 Order: %s', row[0], r.URBPO)
row.append(r.URBPO)
logger.debug('*')
logger.info('Querying roll/tag master for order line numbers and SKUs...')
for row in processed_records:
rtMaster = session.query(TXRYRT00).filter(TXRYRT00.RTBNR == '001', TXRYRT00.RTRNR == row[ROLL_NUM])
if rtMaster.count() != 1:
raise RollNotFoundError('Roll does not exist in Jomar!', row[ROLL_NUM])
for r in rtMaster:
logger.debug('Roll: %s --> Line: %s SKU: %s', row[ROLL_NUM], r.RTAFN, r.RTANR)
row.append(str(r.RTAFN))
row.append(r.RTANR)
logger.debug('*')
logger.info('Querying order detail table for AS400 Line numbers...')
for row in processed_records:
oDetail = session.query(TXUYUF01).filter(TXUYUF01.VBLA == 'SO', TXUYUF01.VBLN == row[0], TXUYUF01.VBNR == '001', TXUYUF01.VLFN == row[JOMAR_LINE], TXUYUF01.VPLT == '00')
if oDetail.count() != 1:
raise AS400LineLookupError('Failed to retrieve AS400 line number.', row[0], row[JOMAR_LINE])
for r in oDetail:
logger.debug('Jomar Order: %s Line: %s --> AS400 Line: %s', row[0], row[JOMAR_LINE], r.VPLIN)
row.append(str(r.VPLIN))
logger.debug('*')
logger.info('Querying sales item info table for STM pattern and color...')
for row in processed_records:
itemDetail = session.query(TXMYAT03).filter(TXMYAT03.XANR == row[ITEM_NUM], TXMYAT03.XBNR == '001', TXMYAT03.XPLT == '00')
if itemDetail.count() != 1:
raise ItemLookupError('Sales item lookup failed? I don\'t even know...', row[ITEM_NUM])
for r in itemDetail:
logger.debug('Jomar Item: %s --> %s-%s', row[ITEM_NUM], r.XFLX33, r.XFLX34)
row.append(r.XFLX33)
row.append(r.XFLX34)
logger.debug('*')
logger.info('Querying XSHPFIL on the AS400 to get packing list numbers...')
for row in processed_records:
row.append(getAS400PackingList(row[2], row[AS400_LINE], row[ROLL_NUM]))
logger.debug('*')
#Build e-mail message
msg = 'Designtex has cut into the following pieces:<br><br>'
msg = msg + '<table border="1">'
msg = msg + '<tr>'
msg = msg + '<th>Roll #</th>'
msg = msg + '<th>AS400 Packing List</th>'
msg = msg + '<th>Jomar Order</th>'
msg = msg + '<th>Pattern</th>'
msg = msg + '<th>Color</th>'
msg = msg + '</tr>'
for row in processed_records:
msg = msg + '<tr>'
msg = msg + '<td>' + row[ROLL_NUM] + '</td>'
msg = msg + '<td>' + row[PACKING_LIST] + '</td>'
msg = msg + '<td>' + row[0] + '</td>'
msg = msg + '<td>' + row[PATTERN] + '</td>'
msg = msg + '<td>' + row[COLOR] + '</td>'
msg = msg + '</tr>'
msg = msg + '</table>'
logger.info('Sending e-mails...')
send_alert_html(RECIPIENTS, SUBJECT, message=msg)
p = [row[ROLL_NUM].strip() for row in processed_records]
if(len(p) > 0 and not TEST_MODE):
logger.info('Marking records processed in database...')
flag = 'UPDATE DTXCUTFL SET PROCESSED = \'Y\' where STM_TICK# in ' + str(p).replace('[', '(').replace(']', ')')
session.execute(flag)
session.commit()
if __name__ == '__main__':
#getAS400PackingList('U58475', '1', '52828') #Test packing number retrieval from AS400
dtxInvoiceAlert()
+160
View File
@@ -0,0 +1,160 @@
'''
Created on January 19, 2018
Anderson plant needs an ASN for rolls we weave for them.
This script is pulling data from our current implementation of Jomar (before we are on CF Jomar)
So, GR-Custom Fabrics is set up as customer 8000 in our Jomar; they will be flagged for 856 documents.
This should allow me to use the standard ASN tables I've used for Maharam and Momentum.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import logging
import sys
import StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
# Only 2 patterns in the beginning; might as well just use a dict
NAFTA = {"ADENA PURE" : "Y", "TAILORED INDIGO" : "N"}
CF_ITEM = {"ADENA PURE" : "FF 0000063322", "TAILORED INDIGO" : "FF 0000063311"}
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to GR-Anderson'
MESSAGE = None
TEST_MODE = True
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['8000']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__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 - %(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)
'''
ASN tables
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
def create_GR_Anderson_Ship_Notice():
global MESSAGE
initializeLogger('MAH_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
# Header record if required create here
# Attempt to join xref table in here?
detailQ = session.query(CSPASN03,CSPASN04).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup,
CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust,
CSPASN03.MSTBIL == shipmentNumber,
CSPASN03.BOLNUM == plNumber)
detailCount = detailQ.count()
for detail in detailQ:
print CF_ITEM[detail[1].SB_PATTERNAME], NAFTA[detail[1].SB_PATTERNAME]
# Write lines out to file here; one file per shipment
if(len(shipments_to_flag_processed) > 0 and not TEST_MODE):
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
logger.debug('Comitting changes to database...')
session.commit()
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
else:
pass #add e-mail alert code here
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
create_GR_Anderson_Ship_Notice()
+254
View File
@@ -0,0 +1,254 @@
'''
Created on Apr 27, 2017
Import Glen Raven ASN data into the DB
@author: Wes
'''
from alert import send_alert_html
import datetime, csv, logging, sys, os, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
from itertools import groupby
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Inbound Glen Raven ASN Processed'
MESSAGE = None
TEST_MODE = False
GR_AP_ACCOUNT = '001306'
JOMAR_PO_ORDER_TYPE = 'JB'
VN = 0
PACK_SLIP = 1
YARN_ITEM = 2
COLOR_DESC = 3
PO_DO = 4
LOT_NUMBER = 5
CASE = 6
CASE_UNITS = 7
GRS_LBS = 8
NET_LBS = 9
TRNS_DATE = 9
SHIP_DATE = 10
SHIP_VIA = 11
logger = None
LOG_LEVEL = logging.DEBUG
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
#engine = create_engine("mssql+pyodbc://@JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
#---------------------------------------------------------------------#
class PSPASN00(Base):
__tablename__ = 'PSPASN00' #Inboud ASN table
__table_args__ = {'autoload':True}
class TXEYAT00(Base):
__tablename__ = 'TXEYAT00' #Vendor Item Cross reference table
__table_args__ = {'autoload':True}
class TXUYUF01(Base):
__tablename__ = 'TXUYUF01' #Order Detail Table
__table_args__ = {'autoload':True}
#---------------------------------------------------------------------#
class ItemTranslationError(RuntimeError):
''' Raising this if we fail to lookup STM item using GR item '''
class POError(RuntimeError):
''' Raising this if we fail to lookup required info from relevant PO '''
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)
'''
Query the vendor item xref table to translate GR --> STM
Likely reasons this failed:
* _gr_item_ not entered as a xref for any of our items
* _gr_item_ entered for more than 1 of our items
Returns the matching STM item number if found.
'''
def GRtoSTM(gr_item):
global session
global logger
q = session.query(TXEYAT00).filter(TXEYAT00.UALNR == GR_AP_ACCOUNT, TXEYAT00.UALAN == gr_item)
if(q.count() == 1):
for row in q:
return row.UAANR
else:
logger.debug('GRtoSTM() rows returned: %s', q.count())
return None
'''
Query the order detail table by _po_ and _item_.
Likely reasons this failed:
* _po_ doesn't actually exist
* _item_ not ordered on _po_
* _item_ appears on more than 1 line on _po_
Returns the line number of _item_ on _po_ if found.
'''
def getLineNum(po, item):
global session
global logger
q = session.query(TXUYUF01).filter(TXUYUF01.VBLA == JOMAR_PO_ORDER_TYPE, TXUYUF01.VBLN == po, TXUYUF01.VANR == item)
if(q.count() == 1):
for row in q:
return row.VLFN
else:
return None
def processInboundGRASN(inFile):
global MESSAGE
global session
os.chdir('D:\FTP')
initializeLogger('gr_856_inbound_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: %s', inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving %s to D:\\FTP\\OLD\\GR\\', inFile)
os.rename(inFile, 'OLD\\GR\\' + fileName)
session.close()
return
rawLines.sort(key=itemgetter(PO_DO)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(PO_DO)) #Create group by iterator based on PO number
#For each unique PO appearing in this transmission
for poNumber, lines in purchaseOrders:
logger.info('****')
logger.info('Processing records for purchase order: %s', poNumber)
#For each record of this transmission on poNumber
for line in lines:
if line[VN] not in ['19']:
logger.warn('Encountered record with invalid vendor number!')
logger.warn(line)
continue
#Translate GR item to STM item
stm_item = GRtoSTM(line[YARN_ITEM])
if stm_item:
#Query order detail by PO and STM item to get the correct PO line number
line_num = getLineNum(poNumber, stm_item)
if line_num:
logger.info('Processing Case: %s (%s)', line[CASE], stm_item)
asn = PSPASN00()
asn.BUSINESS_UNIT = '001'
asn.PLANT = '00'
asn.VENDOR = GR_AP_ACCOUNT
asn.PO_NUMBER = poNumber
asn.PO_ITEM = stm_item
asn.PO_LINE = line_num
asn.PACKING_SLIP = line[PACK_SLIP]
asn.CASE_NUMBER = line[CASE]
asn.QTY_GROSS = line[GRS_LBS]
asn.QTY_NET = line[NET_LBS]
asn.VENDOR_LOT = line[LOT_NUMBER]
session.add(asn)
else:
logger.error('Failed to find a line on PO %s for: %s', poNumber, stm_item)
try:
send_alert_html(RECIPIENTS, 'Error Processing GR ASN', attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
raise POError('Failed to retrieve PO info from database', poNumber, stm_item)
else:
logger.error('Failed to lookup STM item number for GR Item: %s', line[YARN_ITEM])
try:
send_alert_html(RECIPIENTS, 'Error Processing GR ASN', attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
raise ItemTranslationError('Failed to lookup STM item number for GR Item', line[YARN_ITEM])
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warn('**TEST MODE ENABLED ROLLING BACK TRANSACTIONS**')
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?')
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\GR\\')
os.rename(inFile, 'OLD\\GR\\' + fileName)
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python gr_856_in.py D:\\FTP\\INCOMING\\GR_ASN_0100334639.csv'
else:
processInboundGRASN(sys.argv[1])
+399
View File
@@ -0,0 +1,399 @@
'''
Created on Sep 18, 2015
Interface a Kravet PO csv file into Jomar EDI tables
@author: Wes
'''
from alert import send_alert_html
import datetime, csv, logging, sys, os, re, StringIO
from itertools import groupby, tee
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Kravet 850 Processed.'
MESSAGE = None
TEST_MODE = False
STM_ITEM_PATTERN = "[\s\S]+[*]\d+$"
KFI_ITEM_PATTERN = "\d+[.]\d+[.]\d+"
STREET = '1500 HIGHWAY 29 SOUTH'
CITY = 'ANDERSON'
PO_NUM = 0
LINE_NUM = 1
SHIPMENT_NUM = 2
VENDOR_NAME = 3
CUSTOMER_NUM = 4
ORDER_DATE = 5
SHIP_TO_NAME = 6
SHIP_1 = 7
SHIP_2 = 8
SHIP_CITY = 9
SHIP_STATE = 10
SHIP_ZIP = 11
SHIP_COUNTRY = 12
BILL_1 = 13
BILL_2 = 14
BILL_CITY = 15
BILL_STATE = 16
BILL_ZIP = 17
STM_ITEM = 18
FINISHES = 19
KRAVET_ITEM = 20
ORDERED_ITEM = 21
QTY = 22
UNITS = 23
UNIT_PRICE = 24
EXT_PRICE = 25
NOTES = 26
PAYMENT_TERMS = 27
FREIGHT_TERMS = 28
SHIP_VIA = 29
CURRENCY = 30
DESCRIPTION = 31
SIDEMARK = 34
logger = None
LOG_LEVEL = logging.DEBUG
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}
#----------------------------------------------------------------------
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)
'''
Most of the data we receive from Kravet needs to be "massaged" into something that's
actually usable. Trying to consolidate these "fixes" into one place...
For reference, the agreed upon format for the STM item field is:
PATTERN*COLOR
Where:
* PATTERN contains only letters, spaces, ', and -
* COLOR contains only digits
Examples:
What they sent:
KIRSTEN 73383*2015
MULBERRY 73151*COL.825
What they should have sent:
KIRSTEN*2015
MULBERRY*0825
'''
def massageSTM(fromKravet):
global logger
og = fromKravet
if not (fromKravet.count('*') == 1):
#logger.error("Unexpected format encountered for vendor item string...")
return fromKravet
fromKravet = re.sub("[^A-Za-z0-9*'\-\s]+", '', fromKravet) #Remove all special characters besides *, ', -, and spaces
#Today's kludge brought to you by: 'KIRSTEN 73383*2015'
i = fromKravet.index('*')
fromKravet = re.sub('[0-9]+', '', fromKravet[:i]).strip() + fromKravet[i:] #Remove any digits to the left of *
#The following bandaid was inspired by: 'MULBERRY 73151*COL.825'
i = fromKravet.index('*')
fromKravet = fromKravet[:i] + re.sub('[A-Za-z]+', '', fromKravet[i:]).strip() #Remove non-digits to the right of *
#Zero pad 3 digits colors while we're at it...
i = fromKravet.index('*') + 1
fromKravet = fromKravet[:i] + fromKravet[i:].zfill(4)
logger.debug('Massaging STM item column: %s --> %s', og, fromKravet)
return fromKravet
'''
'''
def massageKFI(fromKravet):
global logger
og = fromKravet
if(fromKravet.startswith('GWF-')):
fromKravet = fromKravet[4:]
logger.debug('Massaging KFI item column: %s --> %s', og, fromKravet)
return fromKravet
def lookUpSunburyItem(session, k_item, ven):
global logger
k_item = k_item.replace('#','') #Yet another bandaid fix for Kravet...
if(re.compile(STM_ITEM_PATTERN).match(k_item) == None):
logger.error('Kravet transmitted junk in the STM item column: ' + k_item)
return ''
k = k_item.split('*')
k_pat = k[0]
k_col = re.compile(r'[^\d.]+').sub('', k[1]).zfill(4)
q = session.query(ItemXref,ItemInfo).order_by(ItemXref.UACCOL).filter(ItemXref.UACCNR == ven, ItemInfo.XFLX33 == k_pat, ItemInfo.XFLX34 == k_col, ItemXref.UACANR == ItemInfo.XANR)
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:
session.close()
logger.warn('Failed to lookup item # for: ' + k_pat + '-' + k_col + ' is the cross reference entered in Jomar?')
return ''
#raise RuntimeError('Failed to lookup Sunbury item for STM Pattern: ' + k_pat + ' Color: ' + k_col + ' is the cross reference entered in Jomar?')
def processKravetPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('kf_850_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: ' + inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\KF\\')
os.rename(inFile, 'OLD\\KF\\' + fileName)
session.close()
return
#sys.exit("Nothing to process.")
rawLines.sort(key=itemgetter(0)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(0)) #Create group by iterator based on PO number
for poNumber, lines in purchaseOrders:
logger.info('***')
logger.info('Processing purchase order ' + poNumber)
#Create a copy of the lines iterator so we can pull the first line of the PO now to fill in header info
lines, flb = tee(lines)
fl = flb.next()
header = OrderHeader()
#Order header constants
header.UBNR = '001'
header.UPLT = '00'
header.ULOAD = 'N'
header.UBLA = 'SO'
header.FLX01 = '00'
header.URLRV = '0' #An ID column, in case the same PO is transmitted twice both may be added to DB and sorted out in Jomar
header.UDAT = int(datetime.datetime.strptime(filename, "Kravet_to_vendor_%d-%b-%Y.csv").strftime('%Y%m%d')) #Transaction Date -> Order Date
logger.info('PO Date: ' + str(header.UDAT))
header.UIHS = int(datetime.datetime.strptime(fl[ORDER_DATE], "%d/%m/%Y").strftime('%Y%m%d'))
logger.info('Ordered On: ' + str(header.UIHS))
header.FOB = fl[FREIGHT_TERMS]
logger.info('FOB: ' + header.FOB)
header.XTX5 = fl[PAYMENT_TERMS]
logger.info('Payment terms: ' + header.XTX5)
header.UIHR = poNumber
if(fl[CUSTOMER_NUM] == 'KFI' or fl[CUSTOMER_NUM] == '3190'):
logger.info('Column 5 = "KFI" using customer number: 3190')
header.UKDR = '3190'
header.UKDN = '319000'
header.UNA2 = 'KRAVET FABRICS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319003'
elif(fl[CUSTOMER_NUM] == 'LJI' or fl[CUSTOMER_NUM] == '3191'):
logger.info('Column 5 = "LJI" using customer number: 3191')
header.UKDR = '3191'
header.UKDN = '319100'
header.UNA2 = 'LEE JOFA'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319101'
elif(fl[CUSTOMER_NUM] == 'BRF' or fl[CUSTOMER_NUM] == '746'):
logger.info('Column 5 = "BRF" using customer number: 0746')
header.UKDR = '0746'
header.UKDN = '074600'
header.UNA2 = 'BRUNSCHWIG & FILS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '074600'
elif(fl[CUSTOMER_NUM] == 'GIS'):
logger.info('Column 5 = "GIS" using customer number: 3194')
header.UKDR = '3194'
header.UKDN = '319400'
header.UNA2 = 'KRAVET FABRICS/GIS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319401'
elif(fl[CUSTOMER_NUM] == 'GSR'):
logger.info('Column 5 = "GSR" using customer number: 3194')
header.UKDR = '3194'
header.UKDN = '319400'
header.UNA2 = 'KRAVET FABRICS/GIS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319401'
else:
logger.error('Unknown Customer Code: ' + fl[CUSTOMER_NUM])
session.close()
raise ValueError('Unknown Customer Code: ' + fl[CUSTOMER_NUM])
if(len(fl[SHIP_TO_NAME].strip()) > 0):
header.UNA2 = fl[SHIP_TO_NAME]
header.UNA3 = fl[SHIP_1]
header.UNA4 = fl[SHIP_2]
header.UORT = fl[SHIP_CITY]
header.USTA = fl[SHIP_STATE]
header.ULND = fl[SHIP_COUNTRY]
header.UPLZ = fl[SHIP_ZIP]
line = 1
orderLines = []
for l in lines:
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = line
logger.info('Order Line ' + str(line))
oline.VMGS = l[QTY]
logger.info('Quantity Ordered: ' + oline.VMGS + ' ' + l[UNITS] )
oline.VPR2 = l[UNIT_PRICE]
logger.info('Unit Prince: ' + oline.VPR2)
#Attempt to fix various common issues with the xref data sent from Kravet
stmItem = massageSTM(l[STM_ITEM])
kravetItem = massageKFI(l[KRAVET_ITEM])
oline.VANR = lookUpSunburyItem(session, stmItem, header.UKDR)
oline.VKDR = header.UKDR
oline.VKDL = header.UKDN
oline.VIHR = l[PO_NUM]
oline.VTXT1 = l[NOTES][:65]
logger.info('Notes: ' + oline.VTXT1)
#If possible parse Sunbury pattern and color and insert into database
if not(re.compile(STM_ITEM_PATTERN).match(stmItem) == None):
sItem = stmItem.split('*')
oline.VFLX01 = sItem[0]
oline.VFLX02 = sItem[1]
else:
logger.warn('STM item is in an unusual format: ' + stmItem)
#If possible parse Kravet pattern and color and insert into database
if not(re.compile(KFI_ITEM_PATTERN).match(kravetItem) == None):
kItem = kravetItem.split('.')
oline.VFLX03 = kItem[0]
oline.VFLX04 = kItem[1]
else:
logger.warn('Kravet item number is in an unusual format: ' + l[KRAVET_ITEM])
oline.VRLRV = '0' #ID column for adding duplicate POs to EDI tables
line = line + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == header.UKDR, OrderHeader.UIHR == header.UIHR).count()
if(dups > 0):
logger.warn('PO ' + header.UIHR + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
header.URLRV = str(dups)
for line in orderLines:
line.VRLRV = str(dups)
session.add(header)
for line in orderLines:
session.add(line)
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warn('**TEST MODE ENABLED ROLLING BACK TRANSACTIONS**')
session.rollback()
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\\KF\\')
os.rename(inFile, 'OLD\\KF\\' + fileName)
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python kf_850.py D:\\FTP\\Kravet_to_vendor_25-MAY-2016.csv'
else:
processKravetPo(sys.argv[1])
+268
View File
@@ -0,0 +1,268 @@
'''
Created on Dec 8, 2015
Working with David Keenan (dkeenan@maharam.com) at Maharam to send Invoices to them as XML documents.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime, sys, re, logging, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
from sqlalchemy.sql.schema import Table, Column
from sqlalchemy.sql.sqltypes import Integer
import xml.etree.cElementTree as ET
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Invoices Transmitted to Maharam.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3439', '6590']
DESC_PATTERN = "(?P<pName>[a-zA-Z0-9_ \-\']+?)\s(?P<color>\d+)"
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False) #Production
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
########################################################################
#Jomar Invoice (EDI 810) staging tables #
########################################################################
class CSPINV00(Base):
__tablename__ = 'CSPINV00'
__table_args__ = {'autoload':True}
class CSPINV01(Base):
__tablename__ = 'CSPINV01'
__table_args__ = {'autoload':True}
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class XREF(Base): #Customer Xref Table
__tablename__ = 'TXUYAT01'
__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 - %(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)
def createMaharamInvoiceDocuments():
global MESSAGE
initializeLogger('MAH_810_GENERATE')
if TEST_MODE:
logger.warn('**RUNNING IN TEST MODE**')
logger.warn('THIS WILL NOT FLAG INVOICES AS PROCESSED IN CSPINV00!')
else:
logger.info('**RUNNING IN PRODUCTION MODE**')
session = loadSession()
invoices = []
invoices_to_flag_processed = []
#root = ET.Element("MaharamInvoices")
logger.info('Querying EDI table for unprocessed invoices...')
logger.debug('SELECT * FROM CSPINV00 WHERE IVCUSTB IN (\'3439\', \'6590\') ORDER BY IVIDAT;')
q = session.query(CSPINV00).order_by(CSPINV00.IVIDAT).filter(CSPINV00.IVCUSTB.in_(ACC_NUMBERS))
for row in q:
if(row.IVPROCSS == 'Y'):
continue
businessGroup = row.IVBNR
plant = row.IVPLT
invoiceType = row.IVITYP
invoiceNum = row.IVINV
invoices_to_flag_processed.append(invoiceNum)
date = datetime.datetime.strptime(str(row.IVIDAT), "%Y%m%d")
customerBillToAccount = row.IVCUSTB
customerPO = row.IVCPO
orderType = row.IVSTYP
orderNum = row.IVSO
terms = row.IVTERMS
bAddress1 = row.IVBADD1
bAddress2 = row.IVBADD2
bAddress3 = row.IVBADD3
bCity = row.IVBCTY
bState = row.IVBST
bZip = row.IVBZIP
bCountry = row.IVBCTY
sAddress1 = row.IVSADD1
sAddress2 = row.IVSADD2
sAddress3 = row.IVSADD3
sCity = row.IVSCTY
sState = row.IVSST
sZip = row.IVSZIP
sCountry = row.IVSCTY
lineNum = []
itemNum = []
itemDesc = []
cItem = []
cItemDesc = []
qtyInvoiced = []
price = []
invAmtDom = []
invAmtFor = []
invAmtGrossDom = []
invAmtGrossFor = []
cPatterns = []
cColors = []
cSKUs = []
logger.info('Querying EDI table for invoice detail lines for invoice: ' + invoiceNum)
logger.debug('SELECT * FROM CSPINV01 WHERE CSPINV01.I1BNR = \'' + businessGroup + '\' AND CSPINV01.I1PLT = \'' + plant + '\' AND CSPINV01.I1ITYP = \'' + invoiceType + '\' AND CSPINV01.I1INV = \'' + invoiceNum + '\';')
q2 = session.query(CSPINV01).filter(CSPINV01.I1BNR == businessGroup, CSPINV01.I1PLT == plant, CSPINV01.I1ITYP == invoiceType, CSPINV01.I1INV == invoiceNum)
for row2 in q2:
lineNum.append(row2.I1ILINE)
#itemNum.append(row2.I1ITEM) Sending Maharam our PATTERN-COLOR instead of new item number for the time being
itemDesc.append(row2.I1ITEMT)
cItem.append(row2.I1ITEMC)
cItemDesc.append(row2.I1ITEMCT)
qtyInvoiced.append(row2.I1QTY)
price.append(row2.I1PRI)
invAmtDom.append(row2.I1AMTND)
invAmtFor.append(row2.I1AMTNF)
invAmtGrossDom.append(row2.I1AMTGD)
invAmtGrossFor.append(row2.I1AMTGF)
if(row2.I1ITEM == 'INSURANCE' or row2.I1ITEM == 'FREIGHT'):
cPatterns.append('')
cColors.append('')
cSKUs.append('')
itemNum.append(row2.I1ITEM)
continue
blah = re.match(DESC_PATTERN, row2.I1ITEMT, flags=0)
ourPattern = blah.group('pName')
ourColor = blah.group('color')
itemNum.append(ourPattern.strip() + '-' + ourColor.strip()) #Sending Maharam our PATTERN-COLOR instead of new item number for the time being
'''
This queries a view I created to join TXUYAT01 (customer xref) and TXMYAT03 (item info) together.
The join basically adds the Sunbury pattern name, and color broken out into their own columns to the customer xref table.
As far as I know, a customer can only have one 'reference' for a given pattern/color combination so this should be fine.
It also handles instances where a finish/backing was changed during order entry, which results in a new item number that may not
make it into the customer xref table, which broke the old method that relied on jomar item numbers.
'''
logger.info('Querying customer item cross reference table for Sunbury item: ' + ourPattern + '-' + ourColor)
logger.debug('SELECT * FROM item_xref_wes WHERE item_xref_wes.bGroup = \'' + businessGroup + '\' AND item_xref_wes.plant = \'' + plant + '\' AND item_xref_wes.customer = \'' + customerBillToAccount + '\' AND item_xref_wes.sPatt = \'' + ourPattern + '\' AND item_xref_wes.sCol = \'' + ourColor + '\';')
xref_lookup = session.execute('SELECT * FROM item_xref_wes WHERE item_xref_wes.bGroup = :bgroup AND item_xref_wes.plant = :plt AND item_xref_wes.customer = :cust AND item_xref_wes.sPatt = :pat AND item_xref_wes.sCol = :col', {'bgroup' : businessGroup, 'plt' : plant, 'cust' : customerBillToAccount, 'pat' : ourPattern, 'col' : ourColor})
for r in xref_lookup:
cPatterns.append(r.cPatt)
cColors.append(r.cCol)
cSKUs.append(r.cPatt.split('/')[0] + '-' + r.cCol.split('/')[0])
if(len(lineNum) != len(cSKUs)):
logger.warning('Failed to lookup customer cross reference! Skipping ' + invoiceNum)
logger.warning(itemNum)
logger.warning(itemDesc)
logger.warning(cSKUs)
invoices_to_flag_processed.remove(invoiceNum)
send_alert_html(RECIPIENTS, ('Maharam Invoice: ' + invoiceNum + ' not sent!'), message='There is a cross reference issue. Check the log.')
continue
root = ET.Element("MaharamInvoices")
invoice = ET.SubElement(root, "Invoice")
invoiceInfo = ET.SubElement(invoice, "InvoiceInfo")
ET.SubElement(invoiceInfo, "InvoiceDate").text = date.strftime('%m-%d-%Y')
ET.SubElement(invoiceInfo, "InvoiceNumber").text = invoiceNum
billToAddress = ET.SubElement(invoiceInfo, "BillToAddress")
ET.SubElement(billToAddress, "Address").text = bAddress1
if(len(bAddress2) > 0):
ET.SubElement(billToAddress, "Address").text = bAddress2
if(len(bAddress3) > 0):
ET.SubElement(billToAddress, "Address").text = bAddress3
ET.SubElement(invoiceInfo, "BillToCity").text = bCity
ET.SubElement(invoiceInfo, "BillToState").text = bState
ET.SubElement(invoiceInfo, "BillToPostalCode").text = bZip
ET.SubElement(invoiceInfo, "BillToCountry").text = bCountry
ET.SubElement(invoiceInfo, "AttnName").text = ''
ET.SubElement(invoiceInfo, "AttnEmail").text = ''
ET.SubElement(invoiceInfo, "TermsCode").text = ''
ET.SubElement(invoiceInfo, "TermsDesc").text = terms
purchaseOrder = ET.SubElement(invoice, "PurchaseOrder")
ET.SubElement(purchaseOrder, "PONum").text = customerPO
ET.SubElement(purchaseOrder, "ShipToName").text = ''
shipToAddress = ET.SubElement(purchaseOrder, "ShipToAddress")
ET.SubElement(shipToAddress, "Address").text = sAddress1
if(len(bAddress2) > 0):
ET.SubElement(shipToAddress, "Address").text = sAddress2
if(len(bAddress3) > 0):
ET.SubElement(shipToAddress, "Address").text = sAddress3
ET.SubElement(purchaseOrder, "ShipToCity").text = sCity
ET.SubElement(purchaseOrder, "ShipToState").text = sState
ET.SubElement(purchaseOrder, "ShipToPostalCode").text = sZip
ET.SubElement(purchaseOrder, "ShipToCountry").text = sCountry
for x in range(0, len(lineNum)):
line = ET.SubElement(purchaseOrder, "Line")
ET.SubElement(line, "LineNumber").text = '' #str(lineNum[x])
ET.SubElement(line, "SKU").text = cSKUs[x]
ET.SubElement(line, "ResourceSKU").text = str(itemNum[x])
ET.SubElement(line, "QuantityBilled").text = str(qtyInvoiced[x])
ET.SubElement(line, "UM").text = 'YD'
ET.SubElement(line, "Price").text = str(price[x])
ET.SubElement(line, "Curr").text = 'USD'
tree = ET.ElementTree(root)
tree.write("SEND_MF\\STM_INVOICE_" + str(invoiceNum) + ".xml")
if(len(invoices_to_flag_processed) > 0) and not TEST_MODE:
ex = update(CSPINV00.__table__).where(CSPINV00.IVINV.in_(invoices_to_flag_processed)).values(IVPROCSS=u"Y")
session.execute(ex)
session.commit()
if(len(invoices_to_flag_processed) == 0):
logger.warn('Nothing to send!')
else:
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MF\\*.xml'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
createMaharamInvoiceDocuments()
+399
View File
@@ -0,0 +1,399 @@
'''
Created on Dec 2, 2014
There was code in the original RPG/CL programs to e-mail Moe and Malcolm
if the pattern "OPALESCENT" comes through on an order. I think this is probably
best handled within Jomar and not here.
@author: Wes
'''
from alert import send_alert_html
import datetime, logging, sys, os, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Maharam 850 Processed.'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
CR_DATA_LENGTHS = [1, 8, 5]
HR_DATA_LENGTHS = [1, 12, 8, 30, 30, 30, 30, 2, 15, 30, 30, 30, 1]
DR_DATA_LENGTHS = [1, 3, 15, 4, 6, 3, 9, 9, 8, 50, 50, 50, 50]
RR_DATA_LENGTHS = [1, 80]
SR_DATA_LENGTHS = [1, 5]
S_TYPE_CODE = 0 # len 1
S_LINES = 1 # len 5
########################################################################
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}
#----------------------------------------------------------------------
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)
'''
Splits the supplied string into len(lengths) elements.
The length of element i will be equal to lengths[i]
'''
def split_string_by_position(a_string, lengths):
result = []
position = 0
for length in lengths:
result.append(a_string[position:position+length])
position = position+length
return result
def trim_elements(a_list):
result = []
for ele in a_list:
result.append(ele.strip())
return result
def getSunburyCust(hccb, addr, city):
addressList = ['45 RASONS CT', '74 HORSEBLOCK ROAD', '175 MUFFIN LANE']
cityList = ['HAUPPAUGE', 'YAPHANK', 'CUYAHOGA FALLS']
if (len(hccb) > 0):
return 659000
else:
if(addr in addressList[0]) and (city in cityList[0]):
return 343900
elif(addr in addressList[1]) and (city in cityList[1]):
return 3439
elif(addr in addressList[2]) and (city in cityList[2]):
return 343905
else:
#logger.warning('Encountered new bill to address in transmission. Using will-advise ship-to (343999)')
return 343999
def lookUpSunburyItem(session, v_item, v_col, v_num):
if len(str(v_num)) > 4:
ven = str(v_num)[:-2]
else:
ven = v_num
#This query retrieves a list of every item we have for this Pattern + Customer combo
q = session.query(ItemXref).order_by(ItemXref.UACCOL).filter(ItemXref.UACPAT.like(v_item + "%"), ItemXref.UACCNR == ven)
#q = session.query(ItemXref).order_by(ItemXref.UACCOL).filter(ItemXref.UACPAT.like(v_item), ItemXref.UACCNR == ven)
for row in q:
col = int(row.UACCOL.split('/')[0])
#Iterate over results till we find matching color #
if(int(v_col) == col):
logger.info('Maharam Pattern Number: ' + str(v_item) + ' Color: ' + str(v_col) + ' = Sunbury Item: ' + row.UACANR)
return row.UACANR
#If we made it to here the pattern/color combo does not exist in the xref table...
logger.warn('Failed to find a Sunbury item number for: ' + str(v_item) + ' - ' + str(v_col))
return ''
#raise RuntimeError('Failed to find a Sunbury item number for: ' + str(v_item) + ' - ' + str(v_col))
def processMaharamPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('mf_850_interface')
logger.info('****************************************')
#Read downloaded 850 file into list
logger.info('Loading ' + inFile)
f = open(inFile, 'r')
lines = f.readlines()
f.close()
# Last character on each line is '\n'; trim it off
logger.debug('Doctoring up transmitted lines...')
for idx, val in enumerate(lines):
lines[idx] = val[:-1]
# Validate transmission length
logger.debug('Validating transmission length...')
sr = split_string_by_position(lines[len(lines) - 1], SR_DATA_LENGTHS)
if(sr[0] == 'S'):
s_lines = int(sr[1])
if not(s_lines == (len(lines) - 2)):
logger.error('Transmission Corrupt: Number of records actually transmitted does not match what they say they transmitted...')
raise ValueError('Transmission Corrupt: Number of records actually transmitted does not match what they say they transmitted...')
else:
logger.info('They transmitted ' + str((len(lines) - 2)) + ' lines and we received ' + str(s_lines) + ' lines. Good.')
else:
logger.error('Transmission Corrupt: Last record in transmission is not a summary record...')
raise ValueError('Transmission Corrupt: Last record in transmission is not a summary record...')
# Parse Control Record
logger.info('Parsing Control Record:')
cr = split_string_by_position(lines[0], CR_DATA_LENGTHS)
if not(cr[0] == 'T'):
logger.error('Transmission Corrupt: Missing Control Record...')
raise ValueError('Transmission Corrupt: Missing Control Record...')
xmit_date = datetime.datetime.strptime(cr[1], "%m%d%Y")
mah_xmit_seq = int(cr[2])
logger.info('\t Transmission Date: ' + xmit_date.strftime('%m/%d/%Y'))
logger.info('\t Maharam Transmission Sequence #: ' + str(mah_xmit_seq))
# Parse Header Record
logger.info('')
logger.info('Parsing Header Record:')
hr = split_string_by_position(lines[1], HR_DATA_LENGTHS)
if not(hr[0] == 'H'):
logger.error('Transmission Corrupt: Missing Header Record...')
raise ValueError('Transmission Corrupt: Missing Header Record...')
hr = trim_elements(hr) # Trim white space off each data element; we don't need it.
#mah_po_num = int(hr[1])
mah_po_num = hr[1].strip()
po_date = datetime.datetime.strptime(hr[2], "%m%d%Y")
shp_to_name = hr[3]
shp_to_addr1 = hr[4]
shp_to_addr2 = hr[5]
shp_to_city = hr[6]
shp_to_state = hr[7]
shp_to_zip = hr[8]
shp_via = hr[9]
pymt_terms = hr[10]
requester_name = hr[11]
customer_ord_code = hr[12]
logger.info('\t Maharam PO #: ' + str(mah_po_num))
logger.info('\t PO Date: ' + po_date.strftime('%m/%d/%Y'))
logger.info('\t Ship To: ' + shp_to_name)
logger.info('\t Ship to Address 1: ' + shp_to_addr1)
logger.info('\t Ship to Address 2: ' + shp_to_addr2)
logger.info('\t Ship to City: ' + shp_to_city)
logger.info('\t Ship to State: ' + shp_to_state)
logger.info('\t Ship to Zip: ' + str(shp_to_zip))
logger.info('\t Ship Via: ' + shp_via)
logger.info('\t Payment Terms: ' + pymt_terms)
logger.info('\t Requested By: ' + requester_name)
logger.info('\t Custom Order Code: ' + customer_ord_code)
mah_po_line = []
stm_pattern_name = []
stm_color = []
mah_style = []
mah_color = []
yards_ord = []
price = []
req_ship_date = []
line_comment_1 = []
line_comment_2 = []
line_comment_3 = []
line_comment_4 = []
po_comments = []
# Process rest of transmission, may contain any number of D or R record types.
for x in range(2, len(lines) - 1):
rtype = lines[x][:1]
if(rtype == 'D'):
dr = split_string_by_position(lines[x], DR_DATA_LENGTHS)
mah_po_line.append(int(dr[1]))
stm_pattern_name.append(dr[2])
stm_color.append(int(dr[3]))
mah_style.append(int(dr[4]))
mah_color.append(dr[5])
yards_ord.append(float(dr[6]))
price.append(float(dr[7]))
#req_ship_date.append(datetime.datetime.strptime(dr[8], "%m%d%Y"))
line_comment_1.append(dr[9])
line_comment_2.append(dr[10])
line_comment_3.append(dr[11])
line_comment_4.append(dr[12])
elif(rtype == 'R'):
rr = split_string_by_position(lines[x], RR_DATA_LENGTHS)
po_comments.append(rr[1].strip())
else:
logger.error('Transmission Corrupt: Encountered something other than D or R record type in body of transmission...')
raise ValueError('Transmission Corrupt: Encountered something other than D or R record type in body of transmission...')
logger.info('')
logger.info('Detail Records:')
for i in range (0, len(mah_po_line)):
#Recreated from original RPG code (MF850I) modification from 03/03/09. Special price for certain colors of "HIGH FIDELITY"
#They finally ordered some of this and transmitted a price of $18.65; which is higher than the $17.68 and also correct (according to Moe)...
#Going to ignore this going forward.
#if(stm_pattern_name[i] == 'HIGH FIDELITY '):
#if(stm_color[i] == 302) or (stm_color[i] == 6103):
#price[i] = 17.68
logger.info('\t Maharam PO Line #: ' + str(mah_po_line[i]))
logger.info('\t STM Pattern Name: ' + stm_pattern_name[i])
logger.info('\t STM Color #: ' + str(stm_color[i]))
logger.info('\t Maharam Style: ' + str(mah_style[i]))
logger.info('\t Maharam Color: ' + str(mah_color[i]))
logger.info('\t Yards Ordered: ' + str(yards_ord[i]))
logger.info('\t Price/Yard: ' + str(price[i]))
#logger.info('\t Requested Ship Date: ' + req_ship_date[i].strftime('%m/%d/%Y'))
logger.info('\t Detail Line Comment 1: ' + line_comment_1[i])
logger.info('\t Detail Line Comment 2: ' + line_comment_2[i])
logger.info('\t Detail Line Comment 3: ' + line_comment_3[i])
logger.info('\t Detail Line Comment 4: ' + line_comment_4[i])
logger.info('')
logger.info('PO Comments:')
if(len(po_comments) == 0):
logger.info('\t ***No Data***')
else:
for comment in po_comments:
logger.info(comment)
logger.debug('Looking up customer account number...')
sunburyCust = getSunburyCust(customer_ord_code, shp_to_addr1, shp_to_city)
if sunburyCust == 343999:
logger.warning('Encountered new bill to address in transmission. Using will-advise ship-to (343999)')
logger.info('Sunbury Customer Number: ' + str(sunburyCust))
'''
Query server to figure out Jomar internal sequential item number
'''
logger.debug('Querying customer XREF table to get Sunbury item numbers...')
internal_item = []
session = loadSession()
for p, c in zip(mah_style, mah_color):
internal_item.append(lookUpSunburyItem(session, str(p), c, 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(po_date.strftime('%Y%m%d')) #Transaction Date -> Order Date
header.FOB = ' ' #This customer doesn't transmit this information
header.UIHR = mah_po_num
header.UIHS = int(po_date.strftime('%Y%m%d')) #PO date
header.UKDN = sunburyCust #Ship to customer #
header.UKDR = str(sunburyCust)[:4] #Bill to customer #
header.XTX5 = pymt_terms #Payment terms
header.XTX6 = 'N/A' #Jomar "notes" field sticking order message here
header.URLRV = '0' #An ID column of sorts, in case the same PO is transmitted twice
header.FLX01 = '00'
header.UNA2 = shp_to_name
header.UNA3 = shp_to_addr1
header.UNA4 = shp_to_addr2
header.UORT = shp_to_city
header.USTA = shp_to_state
header.ULND = 'US'
header.UPLZ = shp_to_zip
header.UVT1 = '380'
#Create Detail objects
line = 1
orderLines = []
for item, qty, price, sPat, sCol, mPat, mCol in zip(internal_item, yards_ord, price, stm_pattern_name, stm_color, mah_style, mah_color):
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = line
oline.VMGS = qty
oline.VPR2 = price
oline.VANR = item
oline.VKDR = str(sunburyCust)[:4]
oline.VKDL = sunburyCust
oline.VIHR = mah_po_num
oline.VRLRV = '0'
oline.VFLX01 = sPat
oline.VFLX02 = sCol
oline.VFLX03 = mPat
oline.VFLX04 = mCol
oline.VVT1 = '380'
line = line + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
logger.debug('Checking if this PO already exists in Jomar 850 tables...')
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == header.UKDR, OrderHeader.UIHR == header.UIHR).count()
if(dups > 0):
logger.warning('PO ' + str(header.UIHR) + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
header.URLRV = str(dups)
header.XTX6 = '**POSSIBLE DUPLICATE TRANSMISSION** ' + header.XTX6
for line in orderLines:
line.VRLRV = str(dups)
#submit
session.add(header)
for line in orderLines:
session.add(line)
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warning('**RUNNING IN TEST MODE. ROLLING BACK TRANSACTION**')
session.close()
try:
if not TEST_MODE:
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\\MF\\')
os.rename(inFile, 'OLD\\MF\\' + inFile.split('\\')[-1])
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
processMaharamPo(sys.argv[1])
sys.exit()
+215
View File
@@ -0,0 +1,215 @@
'''
Created on May 12, 2015
Generate
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import logging
import sys
import StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to Maharam.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3439', '6590']
DROP_SHIP_WAREHOUSES = ['01', '06', '11', '16']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__table_args__ = {'autoload':True}
class XREF(Base): #Customer Xref Table
__tablename__ = 'TXUYAT01'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__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 - %(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)
def createMaharamShipNotices():
global MESSAGE
initializeLogger('MAH_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
#Build Header Record of transmission
hRec = 'H' + plNumber + shipmentDate.strftime('%m%d%Y') + "\n"
lines.append(hRec)
detailQ = session.query(CSPASN03,CSPASN04).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup,
CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust,
CSPASN03.MSTBIL == shipmentNumber,
CSPASN03.BOLNUM == plNumber)
detailCount = detailQ.count()
for detail in detailQ:
poLine = detail[1].LINE
mahStyle = detail[1].CUST_PATTERNAME.split('/')[0]
mahColor = detail[1].CUST_COLORNAME.split('/')[0]
if(detail[1].SQTY < 10):
ydsShipped = "{0:.5f}".format(detail[1].SQTY)
elif(detail[1].SQTY < 100):
ydsShipped = "{0:.4f}".format(detail[1].SQTY)
else:
ydsShipped = "{0:.3f}".format(detail[1].SQTY)
if(detail[0].WEIGHT < 10):
weight = "{0:.3f}".format(detail[0].WEIGHT)
elif(detail[0].WEIGHT < 100):
weight = "{0:.2f}".format(detail[0].WEIGHT)
else:
weight = "{0:.1f}".format(detail[0].WEIGHT)
pieceNum = detail[1].ROLNUM
ourPattern = detail[1].SB_PATTERNAME.strip()
ourColor = detail[1].SB_COLORNUM.strip()
#@TODO: Need logic to determine proper ship code.
# 01 = Shipped to Maharam
# 02 = Shipped to Customer
# 03 = Shipped to Finisher
# 04 = Drop shipped from finisher
# 99 = UNKNOWN
shipCode = '99'
if(detail[1].WHSE in DROP_SHIP_WAREHOUSES):
shipCode = '04' #Drop shipped from finisher
else:
#if(shipment[1].SNAME in ['MAHARAM FABRICS']):
if(shipment[1].SNAME.upper().find('MAHARAM') > -1):
shipCode = '01'
dRec = 'D'+"{:<12}".format(str(poNumber))+"{:<3}".format(poLine)+"{:<6}".format(mahStyle)+"{:<3}".format(mahColor)+str(ydsShipped)+str(weight)+str(stmOrder)+"{:<10}".format(pieceNum)+"{:<15}".format(ourPattern)+"{:<4}".format(ourColor)+shipCode+"\n"
lines.append(dRec)
logger.debug('D,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s', "{:<12}".format(str(poNumber)),"{:<3}".format(poLine),"{:<6}".format(mahStyle),"{:<3}".format(mahColor),ydsShipped,weight,stmOrder,"{:<10}".format(pieceNum),"{:<15}".format(ourPattern),"{:<4}".format(ourColor), shipCode)
tRec = 'T' + str(detailCount) + "\n"
lines.append(tRec)
sRec = 'S' + str(len(lines)) #+ "\n"
lines.append(sRec)
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MF\\STM_856_' + str(shipmentNumber) + '.txt')
f = open('SEND_MF\\STM_856_' + str(shipmentNumber) + '.txt', 'w')
f.writelines(lines)
f.close()
if(len(shipments_to_flag_processed) > 0 and not TEST_MODE):
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
logger.debug('Comitting changes to database...')
session.commit()
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
else:
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MF\\STM_856_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
'''
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
if __name__ == '__main__':
createMaharamShipNotices()
+214
View File
@@ -0,0 +1,214 @@
'''
Created on Jan 30, 2017
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import os, sys, logging, time, StringIO, datetime
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'ETA Update Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230']
#Yeah.....
#Don't forget to fix account numbers 2215 should be 3398
EXTRACT_QUERY = "select mo.VKTR, mo.VANR, mo.VDAT, mo.VLBLN, mo.VLLFN, SUM(ypp.VYDPERPCE) qty, UKDR, PSFD01, UACPAT, UACCOL from TXUYUF01 mo INNER JOIN TXUYUF01 ypp ON ypp.VBLN = mo.VLBLN AND ypp.VLFN = mo.VLLFN INNER JOIN TXUYUV00 ON mo.VLBLN = UBLN INNER JOIN TXUYAT01 ON UACCNR = UKDR AND UACANR = mo.VANR INNER JOIN TXMYAT03 ON XANR = mo.VANR INNER JOIN TXRYPS00 ON TXMYAT03.XFLX33 = PSPRT AND TXMYAT03.XFLX34 = PSANR AND PSKDN = mo.VKDL and PSKST IN ('COL') WHERE mo.VKDL IN (select KKDN from TXUYKD00 WHERE KKDR IN ('2215', '4318', '5230')) AND mo.VERA NOT LIKE 'E' AND mo.VBLA = 'IB' GROUP BY mo.VKTR, mo.VANR, mo.VDAT, mo.VLBLN, mo.VLLFN, UKDR, PSFD01, UACPAT, UACCOL"
#engine = create_engine("mssql+pyodbc://@jsisbprod", echo=False)
engine = create_engine("mssql+pyodbc://@JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
########################################################################
# SQL Alchemy table declarations. Not sure if I need any for this. #
########################################################################
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__table_args__ = {'autoload':True}
#class CSPINV00(Base):
# __tablename__ = 'CSPINV00'
# __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 - %(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)
'''
@todo: Not sure if I'll actually need this. This is the last document that
the AS400 is sending to them. Just in case...
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
@todo: Not sure if I'll actually need this. This is the last document that
the AS400 is sending to them. Just in case...
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def createMomentumETADocument():
global MESSAGE
initializeLogger('MOM_418_GENERATE')
xmission = [] #List of strings, the individual lines of the transmission file that will be sent
if(TEST_MODE):
logger.info('**RUNNING IN TEST MODE**')
else:
logger.info('**RUNNING IN PRODUCTION MODE**')
logger.debug('Connecting to database...')
session = loadSession()
trans_no = -1
# @todo: create method to grab this from new database
trans_no = getTransNumber()
logger.debug('Using transaction number: %s', trans_no)
'''
Main processing loop goes here
'''
current_po = ''
extract = session.execute(EXTRACT_QUERY)
dCount = 0
for row in extract:
#Level break on PO number
if row.VKTR not in current_po:
#dCount will only be zero for the first pass
#Write t record for previous PO
if dCount > 0:
tRec = 'T~%s\n' % (dCount)
xmission.append(tRec)
logger.debug(tRec[:-1])
dCount = 0
#Write new header record
current_po = row.VKTR
hRec = 'H~%s\n' % (current_po)
xmission.append(hRec)
logger.debug(hRec[:-1])
# @todo: bother figuring out date slip reason code? see doc specs...
dateSlipCode = '0'
promiseDate = datetime.datetime.strptime(str(row.VDAT), "%Y%m%d")
pDate = promiseDate.strftime('%m/%d/%y')
quantity = "{0:.3f}".format(row.qty)
dRec = 'D~%s~%s~%s~%s~%s~%s~YDS\n' % (row.PSFD01, row.UACPAT, row.UACCOL, pDate, dateSlipCode, quantity)
xmission.append(dRec)
dCount += 1 #increment dCount, this is the number that is inserted into the T record for each PO
logger.debug(dRec[:-1])
if dCount > 0:
tRec = 'T~%s\n' % (dCount)
xmission.append(tRec)
logger.debug(tRec[:-1])
# @todo: may need to tune this number later
if len(xmission) > 2:
xmission.append('S~' + str(len(xmission)) + '\n')
logger.debug('S~' + str(len(xmission)))
#delete/comment this later
print ''
for row in xmission:
print row[:-1]
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_418_' + str(trans_no) + '.txt')
f = open('SEND_MG\\SBR_418_' + str(trans_no) + '.txt', 'w')
f.writelines(xmission)
f.close()
try:
logger.info('Sending e-mail alert...')
#send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_418_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
#updateTransNumber(trans_no) #Update transaction number in AS400
else:
logger.warn('Nothing to send!')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
createMomentumETADocument()
+279
View File
@@ -0,0 +1,279 @@
'''
Created on Jul 15, 2015
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import os, sys, logging, time, StringIO
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Invoices Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230', '6600', '4063']
engine = create_engine("mssql+pyodbc://@jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
########################################################################
# Jomar Invoice (EDI 810) staging tables #
########################################################################
class CSPINV00(Base):
__tablename__ = 'CSPINV00'
__table_args__ = {'autoload':True}
class CSPINV01(Base):
__tablename__ = 'CSPINV01'
__table_args__ = {'autoload':True}
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__table_args__ = {'autoload':True}
########################################################################
# Jomar Item description tables. Used to look up Pattern name, color, #
# style, etc. for a given item number #
########################################################################
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
########################################################################
# Jomar Customer master file. Used to look up account names #
########################################################################
class CustomerMaster(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 - %(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)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
'''
Grabs any unprocessed Maharam invoices from the 810 EDI staging tables in Jomar.
Formats data into text document to send to Maharam via FTP.
Marks invoices as processed.
'''
def createMomentumInvoiceDocument():
global MESSAGE
initializeLogger('MOM_810_GENERATE')
xmission = [] #List of strings, the individual lines of the transmission file that will be sent
invoices_to_flag_processed = []
#First line of transmission is T or P. 'Test' or 'Production'
if(TEST_MODE):
logger.info('**RUNNING IN TEST MODE**')
xmission.append('T~\n')
else:
logger.info('**RUNNING IN PRODUCTION MODE**')
xmission.append('P~\n')
logger.debug('Connecting to database...')
session = loadSession()
trans_no = -1
# t = session.query(mf_transaction_number).filter(mf_transaction_number.doc == '810')
# if t.count() == 1:
# for row in t:
# trans_no = row.txnum
# else:
# logger.fatal('Failed to retrieve transaction number from database... Something ain\'t right. %s', t.count())
# sys.exit('Failed to retrieve transaction number from database... Something ain\'t right.')
trans_no = getTransNumber()
#Second line (Control Line) of transmission is essentially a boilerplate
logger.info('Building Control Line')
ctrlLine = 'TP~12~5702863800~12~9498338886~'
ctrlLine += time.strftime('%Y%m%d')
ctrlLine += '~' + str(trans_no)
xmission.append(ctrlLine + "\n")
logger.debug(ctrlLine)
logger.info('Querying EDI table for unprocessed invoices...')
logger.debug('SELECT * FROM CSPINV00 WHERE IVCUSTB IN (\'3398\', \'4318\', \'5230\', \'6600\') ORDER BY IVIDAT;')
q = session.query(CSPINV00,CustomerMaster).order_by(CSPINV00.IVIDAT).filter(CSPINV00.IVCUSTB.in_(ACC_NUMBERS)).filter(CustomerMaster.KKDN == CSPINV00.IVCUSTB)
for row in q:
if(row[0].IVPROCSS == 'Y'):
continue
invoices_to_flag_processed.append(row[0].IVINV)
businessGroup = row[0].IVBNR
plant = row[0].IVPLT
invoiceType = row[0].IVITYP
logger.info('Building header line for invoice: ' + row[0].IVINV)
#Translate Sunbury bill-to account number to Momentum vendor number
momVendor = ''
if(row[0].IVCUSTB == '3398'):
momVendor = '6975'
elif(row[0].IVCUSTB == '4318'):
momVendor = '0036'
elif(row[0].IVCUSTB == '5230'):
momVendor = '7738'
elif(row[0].IVCUSTB == '6600'):
momVendor = '2082'
elif(row[0].IVCUSTB == '4063'):
momVendor = '2082'
logger.debug('Sunbury Account: %s --> Momentum Vendor: %s', row[0].IVCUSTB, momVendor)
momPO = row[0].IVCPO[-5:]
hLine = 'H~'
hLine += (row[0].IVINV + '~')
hLine += (str(row[0].IVIDAT) + '~')
hLine += (momVendor + '~')
hLine += (momPO + '~')
hLine += (row[0].IVCUSTB + '~')
#hLine += (row[0].IVTERMS + '~')
hLine += ('02~') #Changed to constant '02' per Kim Stolmeier @ Momentum
hLine += (row[1].KNA2 + '~')
hLine += (row[0].IVSADD1 + '~')
hLine += (row[0].IVSCTY + '~')
hLine += (row[0].IVSST + '~')
hLine += (row[0].IVSZIP + '~')
hLine += (row[1].KNA2 + '~')
hLine += (row[0].IVBADD1 + '~')
hLine += (row[0].IVBCTY + '~')
hLine += (row[0].IVBST + '~')
hLine += (row[0].IVBZIP + '~')
hLine += (str(row[0].IVFRT) + '~')
xmission.append(hLine + "\n")
logger.debug(hLine)
logger.info('Querying EDI table for invoice detail lines for invoice: ' + row[0].IVINV)
logger.debug('SELECT * FROM CSPINV01 LEFT JOIN TXMYAT03 ON TXMYAT03.XANR = CSPINV01.I1ITEM WHERE CSPINV01.I1BNR = \'' + businessGroup + '\' AND CSPINV01.I1PLT = \'' + plant + '\' AND CSPINV01.I1ITYP = \'' + invoiceType + '\' AND CSPINV01.I1INV = \'' + row[0].IVINV + '\';')
q2 = session.query(CSPINV01).filter(CSPINV01.I1BNR == businessGroup, CSPINV01.I1PLT == plant, CSPINV01.I1ITYP == invoiceType, CSPINV01.I1INV == row[0].IVINV).join(TXMYAT03, TXMYAT03.XANR == CSPINV01.I1ITEM).add_entity(TXMYAT03)
for row2 in q2:
if(row2[0].I1ITEM == 'INSURANCE'):
'''
@todo: Does Momentum even pay for insurance?
'''
logger.warn('Skipping insurance line!')
continue
logger.info('Building detail line for line ' + str(row2[0].I1ILINE))
dLine = 'D~'
dLine += (row2[1].XFLX33 + '~')
dLine += (row2[1].XFLX34 + '~')
dLine += (row2[1].XFLX35 + '~')
dLine += ("{0:.3f}".format(row2[0].I1QTY) + '~')
dLine += ("{0:.2f}".format(row2[0].I1PRI) + '~')
dLine += (momPO + '~')
xmission.append(dLine + "\n")
logger.debug(dLine)
if len(xmission) > 2:
xmission.append('S~' + str(len(xmission)) + '~')
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_810_' + str(trans_no) + '.txt')
f = open('SEND_MG\\SBR_810_' + str(trans_no) + '.txt', 'w')
f.writelines(xmission)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
#Update EDI tables and transaction number #mf_transaction_number
if(len(invoices_to_flag_processed) > 0 and not TEST_MODE):
#Set process flag on these invoices to 'Y'
ex = update(CSPINV00.__table__).where(CSPINV00.IVINV.in_(invoices_to_flag_processed)).values(IVPROCSS=u"Y")
session.execute(ex)
ex = None
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
logger.debug('Comitting changes to database...')
session.commit()
updateTransNumber(trans_no) #Update transaction number in AS400
logger.debug('Closing database connection...')
session.close()
else:
logger.warn('Nothing to send!')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
createMomentumInvoiceDocument()
+476
View File
@@ -0,0 +1,476 @@
'''
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])
+252
View File
@@ -0,0 +1,252 @@
'''
Created on May 12, 2015
NOTE: A _packing list_ triggers a new header record in this transmission not a shipment. There can
and often will be more header records in a transmission file than there were shipments.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime, logging, os, sys, pyodbc, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__table_args__ = {'autoload':True}
class XREF(Base): #Customer Xref Table
__tablename__ = 'TXUYAT01'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class TXRYPS00(Base):
__tablename__ = 'TXRYPS00'
__table_args__ = {'autoload':True}
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__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 - %(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)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def createMomentumASN():
global MESSAGE
initializeLogger('MOM_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
lines = []
trans_no = getTransNumber()
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
#lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
#Build Header Record of transmission
hRec = 'H~%s~%s~%s~%s~%s~%s~%s\n' % (plNumber, '', '', shipmentDate.strftime('%m/%d/%Y'), '', poNumber, '')
lines.append(hRec)
detailQ = session.query(CSPASN03,CSPASN04,TXRYPS00,XREF,TXRYRT00).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup, CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust, CSPASN03.MSTBIL == shipmentNumber, CSPASN03.BOLNUM == plNumber).filter(CSPASN04.ABNR == TXRYPS00.PSBNR,
CSPASN04.APLT == TXRYPS00.PSPLT, CSPASN04.CUSTNO == TXRYPS00.PSKDN, CSPASN04.SB_PATTERNAME == TXRYPS00.PSPRT,
CSPASN04.SB_COLORNUM == TXRYPS00.PSANR).filter(CSPASN04.ABNR == XREF.UACBNR, CSPASN04.APLT == XREF.UACPLT, CSPASN04.CUSTNO == XREF.UACCNR,
CSPASN04.ITEM == XREF.UACANR).filter(CSPASN04.ABNR == TXRYRT00.RTBNR, CSPASN04.APLT == TXRYRT00.RTPLT, CSPASN04.ITEM == TXRYRT00.RTANR,
CSPASN04.ROLNUM == TXRYRT00.RTRNR)
detailCount = detailQ.count()
for detail in detailQ:
poLine = detail[1].LINE
momStyle = detail[3].UACPAT
momColor = detail[3].UACCOL
momProductNum = detail[2].PSFD01[:8] #Momentum product numbers are always 8 digits, it seems like we've left padded them with too many zeros in our DB
ydsShipped = "{0:.3f}".format(detail[1].SQTY)
weight = "{0:.1f}".format(detail[4].RTSWG)
pieceNum = detail[1].ROLNUM
ourPattern = detail[1].SB_PATTERNAME.strip()
ourColor = detail[1].SB_COLORNUM.strip()
dRec = 'D~%s~%s~%s~%s~%s~%s~%s~%s~%s~%s\n' % (momProductNum, momStyle, momColor, '', ydsShipped, 'YD', pieceNum, '', weight, 'LB')
# @todo: There is a bug in the nightmare of a query above that was causing duplicate rows
# to be created in the tranmission for seemingly random rolls. In every case I've found
# the lines that make it into the transmission are identical. So the below 'fix' is good
# enough to get us off the ground for the time being.
#
# 07/11/2016 - I believe this is fixed now by adding "CSPASN03.CTN == CSPASN04.CTN" to the first
# .filter() clause for the 03 <-> 04 join. I discovered this while working on
# Maharam's 856 document. Need to verify...
if not lines[-1].split('~')[7] == pieceNum:
lines.append(dRec)
else:
logger.warn('Query still messed up...')
tRec = 'T~' + str(detailCount) + "\n"
lines.append(tRec)
sRec = 'S~' + str(len(lines)) #+ "\n"
lines.append(sRec)
if(len(shipments_to_flag_processed) > 0):
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_856_' + str(trans_no) + '.txt')
f = open('D:\\FTP\\SEND_MG\\SBR_856_' + str(trans_no) + '.txt', 'w')
f.writelines(lines)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
#Mark shipments as processed
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
logger.debug('Comitting changes to database...')
session.commit()
updateTransNumber(trans_no) #Update transaction number in AS400
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
'''
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
if __name__ == '__main__':
createMomentumASN()
+194
View File
@@ -0,0 +1,194 @@
'''
Created on Dec 13, 2016
To generate daily 997 document for Momentum.
This particular 997 is confirming all POs received by us for the day.
I created a table called mg_850_997 in the database to support this script.
This table is filled by mg_850.py as it processes purchase orders from Momentum.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import sys
import os, re, StringIO
import logging
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com']
SUBJECT = '997 Document Sent to Momentum'
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
########################################################################
#This table lists POs by transaction number and date
#mg_850.py is fills this table as it processes POs
class mg_850_997(Base):
__tablename__ = 'mg_850_997'
__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)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def generateMomentum997(date):
global logger
os.chdir('D:\FTP')
initializeLogger('mg_850_interface')
# @TODO: Properly handle sequence numbers eventually
logger.info('Grabbing next outgoing transaction number from the AS400...')
trans_no = getTransNumber()
logger.info('Opening database connection...')
session = loadSession()
'''
Each element of purchase orders will be structred like this:
[ 15600, ['21495','21540'] ]
In this example 15600 is the incoming transaction number we are confirming.
21495 and 21540 are the purchase orders that were transmitted in that transaction
'''
purchase_orders = []
level = -1
pos = []
q = session.query(mg_850_997).order_by(mg_850_997.trans_no).filter(mg_850_997.proc_date == date)
for row in q:
#First interation special case
if level == -1:
level = row.trans_no
logger.info(' Processing Transaction: %s', level)
#Check if transaction number changed since last record
if not (level == row.trans_no):
logger.info('Purchase orders on this transaction: %s', pos)
purchase_orders.append([level, pos]) #Write this transaction data to list
#Reset loop vars
pos = []
level = row.trans_no
logger.info(' Processing Transaction: %s', level)
pos.append(row.po)
#Write final transcation data to list
logger.info('Purchase orders on this transaction: %s', pos)
purchase_orders.append([level, pos])
logger.debug('Closing database connection...')
session.close()
#Write out to file here
lines_997 = []
lines_997.append('P~\n')
lines_997.append('TP~12~5702863800~12~9498338886~%s~%s~\n' % (date, trans_no))
for t in purchase_orders:
lines_997.append('H~%s~%s~\n' % (t[0], date))
for p in t[1]:
lines_997.append('D~%s~\n' % (p.zfill(5)))
lines_997.append('S~%s~\n' % (len(lines_997)))
#IF testing just dump transmission lines to console
if(TEST_MODE):
for line in lines_997:
print line
if not(TEST_MODE):
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_997_%s.txt', trans_no)
f = open('SEND_MG\\SBR_997_' + str(trans_no) + '.txt', 'w')
f.writelines(lines_997)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_997_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
updateTransNumber(trans_no) #Update transaction number in AS400
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
generateMomentum997(sys.argv[1])
+258
View File
@@ -0,0 +1,258 @@
'''
Created on May 25th, 2016
@author: Wes
'''
import schedule, time, threading, functools, glob, ctypes, sys
sys.dont_write_bytecode = True
from download_transmissions import maharamOrderDownload, maharamInvoiceUpload, maharamASNUpload
from download_transmissions import momentumOrderDownload, momentumInvoiceUpload, momentumASNUpload
from download_transmissions import kravetOrderDownload
from download_transmissions import glenravenASNDownload
from download_transmissions import UNIFI_846_Download
from kf_850 import processKravetPo
from mg_850 import processMomentumPo
from mg_810 import createMomentumInvoiceDocument
from mg_856 import createMomentumASN
from mf_850 import processMaharamPo
from mf_810 import createMaharamInvoiceDocuments
from mf_856 import createMaharamShipNotices
from gr_856_in import processInboundGRASN
from dt_cut import dtxInvoiceAlert
from FGI_report import run_FGI_report
#Function decorator to catch exceptions instead of halting scheduler job.
def catch_exceptions(job_func):
@functools.wraps(job_func)
def wrapper(*args, **kwargs):
try:
job_func(*args, **kwargs)
except:
import traceback
print(traceback.format_exc())
update_title()
return wrapper
@catch_exceptions
def mf_850():
print ('Submitting job: maharamOrderDownload()')
maharamOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\MAH_850_*.txt')) > 0:
print 'Processing Maharam POs...'
for name in reversed(glob.glob('D:\\FTP\\MAH_850_*.txt')):
processMaharamPo(name)
print ''
update_title()
@catch_exceptions
def mf_810():
print 'Submitting job: createMaharamInvoiceDocuments()'
createMaharamInvoiceDocuments()
print ''
if len(glob.glob('D:\\FTP\\SEND_MF\\STM_INVOICE_*.xml')) > 0:
print 'Uploading invoice documents to Maharam FTP server...'
maharamInvoiceUpload()
print ''
update_title()
@catch_exceptions
def mf_856():
print 'Submitting job: createMaharamShipNotices()'
createMaharamShipNotices()
print ''
if len(glob.glob('D:\\FTP\\SEND_MF\\STM_856_*.txt')) > 0:
print 'Uploading shipment notices to Maharam FTP server...'
maharamASNUpload()
print ''
update_title()
@catch_exceptions
def mg_850():
print ('Submitting job: momentumOrderDownload()')
momentumOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\SBR_850_*.txt')) > 0:
print 'Processing Momentum POs...'
for name in reversed(glob.glob('D:\\FTP\\SBR_850_*.txt')):
processMomentumPo(name)
print ''
update_title()
@catch_exceptions
def mg_810():
print 'Submitting job: createMomentumInvoiceDocument()'
createMomentumInvoiceDocument()
print ''
if len(glob.glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt')) > 0:
print 'Uploading invoice documents to Momentum FTP server...'
momentumInvoiceUpload()
print ''
update_title()
@catch_exceptions
def mg_856():
print 'Submitting job: createMomentumASN()'
createMomentumASN()
print ''
if len(glob.glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt')) > 0:
print 'Uploading shipment notices to Momentum FTP server...'
momentumASNUpload()
print ''
update_title()
@catch_exceptions
def kf_850():
print ('Submitting job: karvetOrderDownload()')
kravetOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\Kravet_to_vendor*.csv')) > 0:
print 'Processing Kravet POs...'
for name in reversed(glob.glob('D:\\FTP\\Kravet_to_vendor*.csv')):
processKravetPo(name)
print ''
update_title()
@catch_exceptions
def gr_856_in():
print ('Submitting job: glenravenASNDownload()')
glenravenASNDownload()
print ''
#Add processing bit here later
if len(glob.glob('D:\\FTP\\INCOMING\\GR_ASN_*.csv')) > 0:
print 'Processing Glen Raven Ship Notices...'
for name in reversed(glob.glob('D:\\FTP\\INCOMING\\GR_ASN_*.csv')):
print name
#processInboundGRASN(name)
update_title()
@catch_exceptions
def un_846_in():
print ('Submitting job: UNIFI_846_Download()')
UNIFI_846_Download()
print ''
#Add processing bit here later
update_title()
@catch_exceptions
def dtx_cut_alert():
print ('Submitting job: dtxInvoiceAlert()')
dtxInvoiceAlert()
print ''
update_title()
@catch_exceptions
def FGI_report():
print ('Submitting job: run_FGI_report()')
run_FGI_report()
print ''
update_title()
'''
Hacked this together to so I can add the name of the next job and when it will run to the
console title.
'''
def update_title():
message = 'Scheduler.py - Next Job: ' + min(schedule.jobs).job_func.args.__repr__().split(' ')[1] + ' @ ' + str(schedule.next_run())
ctypes.windll.kernel32.SetConsoleTitleA(message)
#Use to run a job in it's own thread
def run_threaded(job_func):
job_thread = threading.Thread(target=job_func)
job_thread.start()
'''
An attempt to clean up the schedule code below.
Schedules 'func' to run at 'time' Monday-Friday
'''
def weekdays(time, func):
schedule.every().monday.at(time).do(run_threaded, func)
schedule.every().tuesday.at(time).do(run_threaded, func)
schedule.every().wednesday.at(time).do(run_threaded, func)
schedule.every().thursday.at(time).do(run_threaded, func)
schedule.every().friday.at(time).do(run_threaded, func)
if __name__ == '__main__':
#Maharam PO Download
mf_850_runs = ['08:25','09:55','11:25','12:55','14:25','15:55','17:25']
for t in mf_850_runs:
weekdays(t, mf_850)
#Maharam Invoice Upload. This transmission was sent at 16:40 on the AS400
mf_810_runs = ['16:40']
for t in mf_810_runs:
weekdays(t, mf_810)
#Maharam ASN Upload
mf_856_runs = ['08:45','12:45','13:45','14:45','15:45','16:44','17:45']
for t in mf_856_runs:
weekdays(t, mf_856)
#Momentum PO Download
mg_850_runs = ['12:25','19:25']
for t in mg_850_runs:
weekdays(t, mg_850)
#Momentum Invoice Upload Begin. This transmission was sent at 16:45 on the AS400
mg_810_runs = ['16:45']
for t in mg_810_runs:
weekdays(t, mg_810)
#Momentum ASN Upload. This transmission was sent at 12:15 on the AS400
mg_856_runs = ['12:15','17:30']
for t in mg_856_runs:
weekdays(t, mg_856)
#Kravet PO Download
kf_850_runs = ['07:25']
for t in kf_850_runs:
weekdays(t, kf_850)
#Glen Raven 856 Download
gr_856_in_runs = ['08:00', '13:00']
for t in gr_856_in_runs:
weekdays(t, gr_856_in)
#UNIFI 846 Download
un_846_in_runs = ['08:01', '13:01']
for t in un_846_in_runs:
weekdays(t, un_846_in)
'''
** DISABLED 2017-10-02 **
We have ended the consignment program
#Designtex Cut into alert
dt_cut_runs = ['08:02', '10:00']
for t in dt_cut_runs:
weekdays(t, dtx_cut_alert)
'''
#FGI Report
schedule.every().saturday.at('23:59').do(run_threaded, FGI_report)
#schedule.every(10).seconds.do(run_threaded, derp) #threaded example
#Non-threaded examples
#schedule.every(10).minutes.do(job)
#schedule.every().hour.do(job)
#schedule.every().day.at("10:30").do(job)
#schedule.every().monday.do(job)
#schedule.every().wednesday.at("13:15").do(job)
print 'Job started. Do not close this window!'
print 'Ctrl-C to exit.'
update_title()
while True:
schedule.run_pending()
time.sleep(1)
+158
View File
@@ -0,0 +1,158 @@
'''
Test everything!
functions must begin with 'test'
'''
import unittest, logging, kf_850, mf_850
from download_transmissions import load_ftp_credentials
from kf_850 import massageSTM, massageKFI
from mf_850 import getSunburyCust, split_string_by_position, trim_elements
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
class Download_Transmissions_Tests(unittest.TestCase):
'''
Tests relating to download_tranmissions.py
'''
'''
load_ftp_credentials looks up FTP site credentials in a sqlite db that's not committed to the code repo
'''
def test_load_ftp_credentials(self):
codes = ['wf', 'kf', 'mf', 'mg', 'tsg', 'atx', 'htx', 'acf', 'stm']
for code in codes:
test = load_ftp_credentials(code)
self.assertEqual(len(test), 3) # Did we get a list back with _exactly_ 3 elements?
self.assertTrue(all(isinstance(n, basestring) for n in test)) # Are each of those elements strings?
class Kravet_Inbound_850_Tests(unittest.TestCase):
'''
Tests relating to kf_850.py
'''
'''
Kravet seems to hand key cross references in with a number of varying formats which are incorrect.
I've identified the most common and created massageSTM() to fix them
'''
def test_massageSTM(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
test_input = ['KIRSTEN 73383*2015', 'MULBERRY 73151*COL.825']
expected_output = ['KIRSTEN*2015', 'MULBERRY*0825']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(outt, massageSTM(inn))
'''
Right now this simply trims off the first 4 characters of the xref if 'GWF-' is present at the beginning of the string.
'''
def test_massageKFI(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
test_input = ['GWF-3421.516.0']
expected_output = ['3421.516.0']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(outt, massageKFI(inn))
def test_lookUpSunburyItem(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
session = loadSession()
test_input = [ ['BLISS VELVET*2405','3194'], ['DUME*0601','3191'], ['PAISLEY WALK*6103','3190'] ]
expected_output = ['0000041463', '0000006576', '0000032327']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(kf_850.lookUpSunburyItem(session, inn[0], inn[1]), outt)
session.close()
class Maharam_Inbound_850_Tests(unittest.TestCase):
'''
Tests relating to mf_850.py
'''
addressList = ['45 RASONS CT', '74 HORSEBLOCK ROAD', '175 MUFFIN LANE']
cityList = ['HAUPPAUGE', 'YAPHANK', 'CUYAHOGA FALLS']
accountList = [343900, 3439, 343905]
def test_normal_order(self):
for addr, city, acc in zip(self.addressList, self.cityList, self.accountList):
message = "%s, %s should be account %s" % (addr, city, acc)
self.assertEqual(getSunburyCust('', addr, city), acc, msg=message)
'''
If order is custom contract hccb will be something other than an empty string
We should always get account 659000 for contract orders
'''
def test_contract_order(self):
for addr, city in zip(self.addressList, self.cityList):
self.assertEqual(getSunburyCust('Y', addr, city), 659000, msg='hccb = y but function did not return 659000')
self.assertNotEqual(getSunburyCust('', addr, city), 659000, msg='hccb = \'\' but function returned 659000')
'''
Unkown addresses (those not in the lists above) should resolve to 343999
'''
def test_unknown_address(self):
self.assertEqual(getSunburyCust('', 'Castle Black', 'The Wall'), 343999, msg="Castle Black, The Wall should be account 343999")
'''
'''
def test_lookUpSunburyItem(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
mf_850.logger = logger
session = loadSession()
test_input = [ ['464040','004','343904'], ['466166','007','343904'], ['466166','002','3439'] ]
expected_output = ['0000025195', '0000041949', '0000041944']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(mf_850.lookUpSunburyItem(session, inn[0], inn[1], inn[2]), outt)
session.close()
'''
trim_elements takes a list as input and returns a list of the same length with any whitespace characters
removed from the beginning/end of each element.
'''
def test_trim_elements(self):
test_input = [' Pennywise The Dancing Clown ', 'Jon Snow ', 'Paul Atreides\n\r', ' \tBobby B.\t']
expected_output = ['Pennywise The Dancing Clown', 'Jon Snow', 'Paul Atreides', 'Bobby B.']
self.assertEqual(trim_elements(test_input), expected_output)
'''
Maharam is still transmitting fixed length data to us *yuck*
The function takes 1 string and a list of ints
the string is chopped up into len(ints) elements each element being ints[n] in length
'''
def test_split_string_by_position(self):
test_string = 'Pennywise The Dancing ClownJon SnowPaul AtreidesBobby B.'
lens = [27, 8, 13, 8]
expected_output = ['Pennywise The Dancing Clown', 'Jon Snow', 'Paul Atreides', 'Bobby B.']
self.assertEqual(split_string_by_position(test_string, lens), expected_output)
if __name__ == '__main__':
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
'''
Created on Nov 30, 2016
UNIFI e-mails us "Pack and Hold" data daily. Interface this into Jomar
@author: Wes
'''
import datetime, logging, sys, os, StringIO, csv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
from itertools import groupby
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Inbound UNIFI 846 Processed'
MESSAGE = None
TEST_MODE = True
GR_AP_ACCOUNT = '002528'
JOMAR_PO_ORDER_TYPE = 'JB'
logger = None
LOG_LEVEL = logging.DEBUG
UNIFI_PLANT = 0
PO = 1
LOT = 2
STM_COL = 3
WHSE_DATE = 4
QTY = 5
PACKAGES = 6
CASES = 7
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
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)
def process_UNIFI_846(inFile):
global MESSAGE
global session
os.chdir('D:\FTP')
initializeLogger('UNIFI_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: %s', inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving %s to D:\\FTP\\OLD\\UNIFI\\', inFile)
os.rename(inFile, 'OLD\\UNIFI\\' + fileName)
session.close()
return
rawLines.sort(key=itemgetter(PO)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(PO)) #Create group by iterator based on PO number
#For each unique PO appearing in this transmission
for poNumber, lines in purchaseOrders:
print poNumber
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python unifi_interface.py D:\\FTP\\INCOMING\\ipnet_07052017.file846.csv.010690400'
else:
process_UNIFI_846(sys.argv[1])
+483
View File
@@ -0,0 +1,483 @@
'''
Created on Jan 7, 2016
Generate assignment of invoices to be sent to Wells Fargo in csv format.
This is mainly called from the send_wf.py cgi script running on the Apache instance on our test server.
'WF' in comments = Wells Fargo
@todo: Properly truncate invoice amount value to fit WF's decimal(12,2) field size. TOTAL and DETAIL records!
@author: Wes
'''
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)