Found more code examples from STM. Used claude to generate a summary (CLAUDE.md)
This commit is contained in:
@@ -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)
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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')
|
||||
@@ -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 }
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
@@ -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()
|
||||
@@ -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])
|
||||
@@ -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)
|
||||
Reference in New Issue
Block a user