diff --git a/stm_etl/AS400_Invoice_Interface.py b/stm_etl/AS400_Invoice_Interface.py new file mode 100644 index 0000000..1821b48 --- /dev/null +++ b/stm_etl/AS400_Invoice_Interface.py @@ -0,0 +1,168 @@ +''' +Attempting to automatically close invoices in the AS400 using data from Jomar.. weee... + +Created on: 2017-11-20 + +@author: Wes +''' +import pyodbc, time, logging, sys, os, StringIO, csv, sqlite3 +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from FTP.alert import send_alert_html + +RECIPIENTS = ['admin@example.com', 'user1@example.com'] +SUBJECT = 'AS400 Invoice Inteface Exceptions' +MESSAGE = None + +AS400_LIB = 'STMDATA' +logger = None +LOG_LEVEL = logging.DEBUG + +INVOICE = 0 +INVOICE_LINE = 1 +TICKET = 2 +U_NUMBER = 3 +INV_YARDS = 4 +FIN_YARDS =5 +SHIP_DATE = 6 +INV_DATE = 7 + +#engine = create_engine("mssql+pyodbc://jsisbprod", echo=False) +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 + +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:\\' + 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.DEBUG) + sh.setFormatter(formatter) + logger.addHandler(sh) + +def loadSession(): + global engine + Session = sessionmaker(bind=engine) + session = Session() + return session + +def build_extract_query(d): + q = ("SELECT r1rnr,r1lfn,RIGHT(rtrnr, 5) rtrnr,urbpo,rtilg,rtpglg,lsdat,'***DATE***' as invdte " + "FROM txuyre01 " + "LEFT JOIN invoice_rolls ON rtbnr=r1bnr AND rtplt=r1plt AND rtinv=r1rnr AND r1lfn=rtafn " + "LEFT JOIN TXUYUV00 ON RTALN = UBLN " + "LEFT JOIN TXUYLS00 ON RTDLN = LSNR " + "WHERE r1rnr IN (SELECT RRNR FROM TXUYRE00 WHERE RDATLS = '***DATE***') " + "AND R1ANR NOT LIKE 'FREIGHT' AND R1ART NOT LIKE 'SR'" + "ORDER BY r1lfn,r1rnr") + return q.replace('***DATE***',d) + +def build_insert_statement(r): + q = 'INSERT INTO ' + AS400_LIB + '.JSIINV VALUES (INVNUM,INVLIN,TICK#,\'ORDERA\',INVYDS,FINYDS,SHPDTE,INVDTE)' + q = q.replace('INVNUM',r[INVOICE]) + q = q.replace('INVLIN',str(r[INVOICE_LINE])) + q = q.replace('TICK#',r[TICKET]) + q = q.replace('ORDERA',r[U_NUMBER]) + q = q.replace('INVYDS',str(r[INV_YARDS])) + q = q.replace('FINYDS',str(r[FIN_YARDS])) + q = q.replace('SHPDTE',str(r[SHIP_DATE])) + q = q.replace('INVDTE',str(r[INV_DATE])) + return q + +def AII_main(d): + os.chdir('D:\\') + initializeLogger('AS400_Invoice_Interface') + logger.info('Connecting to JSISBPROD...') + session = loadSession() + + extract = [] + + logger.info('Running extract query for: %s', d) + logger.debug(build_extract_query(d)) + for row in session.execute(build_extract_query(d)): + extract.append(row) + + logger.debug('Disconnecting from JSISBPROD...') + session.close() + + logger.info('Opening connection with AS400...') + 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() + + if 'STMDATA' in AS400_LIB: + try: + logger.info('Backing up rows from STMDATA.JSIINV --> WES.JSIINV_BAK') + c1.execute('INSERT INTO WES.JSIINV_BAK SELECT * FROM STMDATA.JSIINV') + except: + logger.warn('***Failed to backup rows currently in STMDATA.JSIINV***') + + logger.info('Deleting old rows from %s.JSIINV', AS400_LIB) + c1.execute('DELETE FROM ' + AS400_LIB + '.JSIINV') + + exceptions = [] + + logger.info('Inserting rows into %s.JSIINV', AS400_LIB) + for row in extract: + # Ticket will be null for credits + if row[TICKET] and row[U_NUMBER] and row[TICKET].isdigit(): + #print build_insert_statement(row) + c1.execute(build_insert_statement(row)) + else: + exceptions.append(row) + + logger.debug('Disconnecting from AS400...') + c1.close() + + if len(exceptions) > 0: + #Open file for writing + logger.debug('Dumping exceptions to csv file...') + fname = "AII_ERRORS_" + 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(['Invoice #', 'Invoice Line', 'Ticket', 'AS400 Order', 'Invoiced Yards', 'Ship Date', 'Invoice Date']) + + for row in exceptions: + writer.writerow(row) + + f.close() + + try: + logger.debug('Sending e-mail...') + send_alert_html(RECIPIENTS, (SUBJECT + " - " + d), 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__': + AII_main(sys.argv[1]) + #print build_extract_query(sys.argv[1]) diff --git a/stm_etl/CLAUDE.md b/stm_etl/CLAUDE.md new file mode 100644 index 0000000..095e1b4 --- /dev/null +++ b/stm_etl/CLAUDE.md @@ -0,0 +1,80 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Sunbury Textile Mills (STM) enterprise data integration platform. Handles inventory synchronization between AS400 and Jomar ERP, EDI document processing (850/810/856/997) with multiple trading partners, and financial integrations. + +**This is a legacy Python 2 codebase.** Use Python 2 conventions (print statements, `basestring`, `ConfigParser`, etc.). + +## Running Tests + +```bash +python FTP/tests.py +# or +python -m unittest FTP.tests +``` + +Tests cover FTP credential loading, Kravet data processing, and item lookup. + +## Dependencies + +Install manually (no requirements.txt): +```bash +pip install schedule paramiko SQLAlchemy +``` +Also requires: pyodbc (with appropriate ODBC drivers for MS SQL Server and IBM AS400). + +## Production Deployment + +1. Stop `scheduler.py` running under SUNBURYTEXTILE\Administrator +2. Delete all `.pyc` files in `\FTP` +3. `git pull` +4. Restart `scheduler.py` + +Failing to stop the scheduler before pulling means old compiled code continues running. + +## Environment Variables + +The following environment variables must be set for the scripts to connect to backend systems: + +| Variable | Used by | Purpose | +|---|---|---| +| `AS400_UID` | 8 files (AS400_Invoice_Interface, Jomar_SO_Auto_Close, FTP/ACF_process, dt_cut, mg_418, mg_810, mg_856, mg_997) | AS400/iSeries login username | +| `AS400_PWD` | same files | AS400/iSeries login password | +| `JOMAR_DB_UID` | Finished_Goods_Interface.py | SQL Server username for Jomar ERP | +| `JOMAR_DB_PWD` | Finished_Goods_Interface.py | SQL Server password for Jomar ERP | + +Additional credentials (FTP/SFTP, email) are loaded at runtime from the SQLite vault at `D:\FTP\ftp.db`. + +## Architecture + +### Root-level interfaces +- **Finished_Goods_Interface.py** — Processes CSV files from AS400 (D:\INTERFACE) into Jomar inventory transactions. Triggered by PowerShell file watcher (`finishedGoodsStart.ps1`). +- **AS400_Invoice_Interface.py** — Auto-closes invoices in AS400 based on Jomar data. Uses pyodbc (AS400) + SQLAlchemy (Jomar). +- **Jomar_SO_Auto_Close.py** — Weekly sales order closure automation. + +### FTP package (`/FTP`) +Central EDI processing system orchestrated by `scheduler.py` (runs continuously as a scheduled job). + +**Trading partner modules follow a naming convention: `{partner_code}_{document_type}.py`** +- **KF** = Kravet, **MF** = Maharam, **MG** = Momentum, **GR** = Glenraven +- **850** = Purchase Orders (inbound), **810** = Invoices (outbound), **856** = ASN/Ship Notices, **997** = Functional Acknowledgments + +**Shared utilities:** +- `download_transmissions.py` — FTP/SFTP credential management and file transfer operations. Credentials stored in SQLite DB (`D:\FTP\ftp.db`, table `ftp_auth`). +- `alert.py` — HTML email notifications with attachment support. +- `wf_assignments.py` — Wells Fargo invoice assignment via SFTP to spscommerce.net. + +### Database connections +- **Jomar ERP:** `mssql+pyodbc://jsisbprod` (SQLAlchemy) +- **AS400:** iSeries Access ODBC Driver (pyodbc) +- **Credentials vault:** SQLite at `D:\FTP\ftp.db` + +### File system layout (production) +The codebase assumes `D:` as root. Key paths: +- `D:\INTERFACE` (+ `\ERRORS`, `\PROCESSED`) — Finished goods CSV staging +- `D:\FTP\INCOMING` — Inbound EDI documents +- `D:\FTP\SEND_{KF,MF,MG,WF}` (+ `\SENT`) — Outbound EDI staging per partner +- `D:\FTP\OLD\{KF,MF,MG,GR}` — Processed document archive diff --git a/stm_etl/FTP/ACF_gen.py b/stm_etl/FTP/ACF_gen.py new file mode 100644 index 0000000..e226ab0 --- /dev/null +++ b/stm_etl/FTP/ACF_gen.py @@ -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) diff --git a/stm_etl/FTP/ACF_process.py b/stm_etl/FTP/ACF_process.py new file mode 100644 index 0000000..d637686 --- /dev/null +++ b/stm_etl/FTP/ACF_process.py @@ -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]) diff --git a/stm_etl/FTP/FGI_report.py b/stm_etl/FTP/FGI_report.py new file mode 100644 index 0000000..119913e --- /dev/null +++ b/stm_etl/FTP/FGI_report.py @@ -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() diff --git a/stm_etl/FTP/HTX_gen.py b/stm_etl/FTP/HTX_gen.py new file mode 100644 index 0000000..ac9a77d --- /dev/null +++ b/stm_etl/FTP/HTX_gen.py @@ -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') diff --git a/stm_etl/FTP/PROCESS_ALL_850.ps1 b/stm_etl/FTP/PROCESS_ALL_850.ps1 new file mode 100644 index 0000000..7aa876c --- /dev/null +++ b/stm_etl/FTP/PROCESS_ALL_850.ps1 @@ -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 } \ No newline at end of file diff --git a/stm_etl/FTP/__init__.py b/stm_etl/FTP/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/stm_etl/FTP/alert.py b/stm_etl/FTP/alert.py new file mode 100644 index 0000000..f1b4973 --- /dev/null +++ b/stm_etl/FTP/alert.py @@ -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', ''.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 = '
' + message + '
' + message = message + '


' + message = message + 'Please do not reply to this message; it was sent from an unmonitored email address.' + 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:

' + for t in test_data: + msg = msg + '' + + msg = msg + '
Order #Roll #
' + t[0] + '' + t[1] + '
' + + send_alert_html('admin@example.com', 'Designtex Test', message=msg) diff --git a/stm_etl/FTP/download_transmissions.py b/stm_etl/FTP/download_transmissions.py new file mode 100644 index 0000000..d1a23a1 --- /dev/null +++ b/stm_etl/FTP/download_transmissions.py @@ -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() diff --git a/stm_etl/FTP/dt_cut.py b/stm_etl/FTP/dt_cut.py new file mode 100644 index 0000000..ed0bccf --- /dev/null +++ b/stm_etl/FTP/dt_cut.py @@ -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:

' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + + for row in processed_records: + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + msg = msg + '' + + msg = msg + '
Roll #AS400 Packing ListJomar OrderPatternColor
' + row[ROLL_NUM] + '' + row[PACKING_LIST] + '' + row[0] + '' + row[PATTERN] + '' + row[COLOR] + '
' + + 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() diff --git a/stm_etl/FTP/gr_856_anderson.py b/stm_etl/FTP/gr_856_anderson.py new file mode 100644 index 0000000..eb0801d --- /dev/null +++ b/stm_etl/FTP/gr_856_anderson.py @@ -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() diff --git a/stm_etl/FTP/gr_856_in.py b/stm_etl/FTP/gr_856_in.py new file mode 100644 index 0000000..da7517d --- /dev/null +++ b/stm_etl/FTP/gr_856_in.py @@ -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]) diff --git a/stm_etl/FTP/kf_850.py b/stm_etl/FTP/kf_850.py new file mode 100644 index 0000000..5c5560c --- /dev/null +++ b/stm_etl/FTP/kf_850.py @@ -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]) diff --git a/mf_810.py b/stm_etl/FTP/mf_810.py similarity index 99% rename from mf_810.py rename to stm_etl/FTP/mf_810.py index 38ce471..ca51167 100644 --- a/mf_810.py +++ b/stm_etl/FTP/mf_810.py @@ -19,7 +19,7 @@ from sqlalchemy.sql.sqltypes import Integer import xml.etree.cElementTree as ET -RECIPIENTS = ['WRay@glenraven.com'] +RECIPIENTS = ['admin@example.com'] SUBJECT = 'Invoices Transmitted to Maharam.' MESSAGE = None diff --git a/stm_etl/FTP/mf_850.py b/stm_etl/FTP/mf_850.py new file mode 100644 index 0000000..94a69d9 --- /dev/null +++ b/stm_etl/FTP/mf_850.py @@ -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() diff --git a/stm_etl/FTP/mf_856.py b/stm_etl/FTP/mf_856.py new file mode 100644 index 0000000..2f4566c --- /dev/null +++ b/stm_etl/FTP/mf_856.py @@ -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() diff --git a/stm_etl/FTP/mg_418.py b/stm_etl/FTP/mg_418.py new file mode 100644 index 0000000..874b289 --- /dev/null +++ b/stm_etl/FTP/mg_418.py @@ -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() diff --git a/stm_etl/FTP/mg_810.py b/stm_etl/FTP/mg_810.py new file mode 100644 index 0000000..6cc30ec --- /dev/null +++ b/stm_etl/FTP/mg_810.py @@ -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() diff --git a/stm_etl/FTP/mg_850.py b/stm_etl/FTP/mg_850.py new file mode 100644 index 0000000..63ff5df --- /dev/null +++ b/stm_etl/FTP/mg_850.py @@ -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]) diff --git a/stm_etl/FTP/mg_856.py b/stm_etl/FTP/mg_856.py new file mode 100644 index 0000000..ed95efd --- /dev/null +++ b/stm_etl/FTP/mg_856.py @@ -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() diff --git a/stm_etl/FTP/mg_997.py b/stm_etl/FTP/mg_997.py new file mode 100644 index 0000000..a08c8a3 --- /dev/null +++ b/stm_etl/FTP/mg_997.py @@ -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]) diff --git a/stm_etl/FTP/scheduler.py b/stm_etl/FTP/scheduler.py new file mode 100644 index 0000000..93e8822 --- /dev/null +++ b/stm_etl/FTP/scheduler.py @@ -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) diff --git a/stm_etl/FTP/tests.py b/stm_etl/FTP/tests.py new file mode 100644 index 0000000..89956bf --- /dev/null +++ b/stm_etl/FTP/tests.py @@ -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() diff --git a/stm_etl/FTP/unifi_interface.py b/stm_etl/FTP/unifi_interface.py new file mode 100644 index 0000000..5fff485 --- /dev/null +++ b/stm_etl/FTP/unifi_interface.py @@ -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]) diff --git a/stm_etl/FTP/wf_assignments.py b/stm_etl/FTP/wf_assignments.py new file mode 100644 index 0000000..bb86c61 --- /dev/null +++ b/stm_etl/FTP/wf_assignments.py @@ -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\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('%s \t %s \t Amount', '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 'THIS MAY TAKE A MOMENT **DO NOT** REFRESH YOUR BROWSER :)' + 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 log file 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) diff --git a/stm_etl/Finished_Goods_Interface.py b/stm_etl/Finished_Goods_Interface.py new file mode 100644 index 0000000..6426a81 --- /dev/null +++ b/stm_etl/Finished_Goods_Interface.py @@ -0,0 +1,600 @@ +''' +Created on Jun 24, 2015 + +If no arguments supplied script looks for CSV's in the folder specified by PATH. +Can be run for single file by supplying file path as arg. This is the preferred way of doing things +if the filewatcher powershell script is enabled to avoid collisions. + +Creates various inventory transactions needed to keep Jomar inventory up to date with what is entering +shipping dept from the AS400 + +Transaction data is written to TXRYBW10 and processed immediatley. +See 'Inventory Control' -> 'Visibility' -> 'Inventory Errors' for transactions that failed to process + +TEST_MODE, when True: + * Prevents files being moved to the PROCESSED folder. + * Will _NOT_ e-mail Aaron + * Does not commit any transactions to the database + +@author: Wes +''' +import ConfigParser +import csv, os, smtplib +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 +from glob import glob +import logging +from operator import itemgetter +import sys, sqlite3, re + +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker + + +TEST_MODE = False +LOG_LEVEL = logging.DEBUG +PATH = 'D:\\INTERFACE\\' +PULL_ORDERS = ['U17435', 'U32614', 'U37151', 'U38029', 'U39702', 'U38664', 'U30473', 'U61478', 'U64631', 'U74634', 'U89496'] +IN_FILE_FORMAT = "[\S\s]+as400_jsi_(?P[\d][\d][\d][\d][\d]).csv" + +ROLL_ERROR_RECIPIENTS = ['user1@example.com', 'user2@example.com'] +TX_ERROR_RECIPIENTS = ['admin@example.com', 'user1@example.com'] + +ORDER = 0 +LINE = 1 +TICKET = 2 +FINISHED_YARDS = 3 +INVOICED_YARDS = 4 +BIN = 5 +WAREHOUSE = 6 +WEIGHT = 7 +SPAT = 8 +SCOL = 9 + +#This script must be run as SUNBURYTEXTILE\Administrator +#engine = create_engine("mssql+pyodbc://jsisbprod", echo=False) +engine = create_engine("mssql+pyodbc://{}:{}@jsisbprod".format( + os.environ['JOMAR_DB_UID'], os.environ['JOMAR_DB_PWD']), echo=False) +Base = declarative_base(engine) +metadata = Base.metadata + +class TXRYBW10(Base): + __tablename__ = 'TXRYBW10' #Finished good interface table + __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 TXRYRT00(Base): + __tablename__ = 'TXRYRT00' #Roll/tag master + __table_args__ = {'autoload':True} +class ItemInfo(Base): + __tablename__ = 'TXMYAT03' + __table_args__ = {'autoload':True} + +''' +MODIFIED - 01/04/2018 - SB-20 +This table is refreshed from the AS400 every hour. +Need this to determine NAFTA qualification +''' +class HTSCDSFL(Base): + __tablename__ = 'as400_HTSCDSFL' + __table_args__ = {'autoload':True} + +def load_email_credentials(): + vault = sqlite3.connect('D:\\FTP\\ftp.db') + cursor = vault.execute("SELECT * from accounts where code = 'DNR'") + for row in cursor: + return [row[1], row[2]] + +''' +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, + username=None, password=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', ''.format(str(n+1))) + msg.attach(msgImage) + + smtp = smtplib.SMTP(server, int(port)) + if tls: + smtp.starttls() + + #if username is not None: + # smtp.login(username, password) + smtp.sendmail(send_from, send_to, msg.as_string()) + smtp.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 + logger = logging.getLogger(__name__) + logger.setLevel(LOG_LEVEL) + handler = logging.FileHandler('D:\\INTERFACE\\' + 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) + +''' +MODIFIED - 01/04/2018 - SB-20 +This function is the brains of the operation regarding the NAFTA changes. +Simple lookup +''' +def NAFTA(session, pattern, color): + logger.debug('Attempting lookup in HTSCDSFL to determine NAFTA qualification') + logger.debug('SELECT * FROM as400_HTSCDSFL WHERE as400_HTSCDSFL.HTSPAT = \'%s\' AND as400_HTSCDSFL.HTSCLR = \'%s\';', pattern, color) + p = session.query(HTSCDSFL).filter(HTSCDSFL.HTSPAT == pattern, HTSCDSFL.HTSCLR == color) + for r in p: + logger.debug('Record found in HTSCDSFL. Value of HTSNQC is \'%s\'', r.HTSNQC) + if r.HTSNQC == 'X': + return 'Y' + else: + return 'N' + logger.debug('No record found in HTSCDSFL!') + return 'N' + +if __name__ == '__main__': + os.chdir('D:\INTERFACE') + initializeLogger('Finished_Goods_Interface') + logger.info('********************************************************************************') + if(len(sys.argv) < 2): + logger.info('No args supplied, processing all files in folder...') + logger.debug('Obtaining list of CSV files in ' + PATH + '\\') + files = glob(PATH + '\*.csv') + + # Verify file name and check for any previous transactions hung up in folder + elif re.match(IN_FILE_FORMAT, sys.argv[1], re.IGNORECASE): + logger.info('Processing single file: ' + sys.argv[1]) + files = [sys.argv[1]] + + in_file_tx = int(re.match(IN_FILE_FORMAT, sys.argv[1], re.IGNORECASE).group('txnum')) #parse transaction num from file name and cast to int + ls = glob(PATH + '\*.csv') + for f in ls: + test = int(re.match(IN_FILE_FORMAT, f, re.IGNORECASE).group('txnum')) + if test < (in_file_tx - 1): + logger.error('Trying to process: %s but %s is still in folder.', sys.argv[1], f) + + try: + if not TEST_MODE: + logger.info('Alerting relevant people to this situation...') + + msg = 'For whatever reason ' + f + ' has not finished processing before ' + sys.argv[1] + ' was submitted.' + dnr_account = load_email_credentials() + send_mail('donotreply@example.com', TX_ERROR_RECIPIENTS, 'TRANSACTION HUNG UP IN FGI!', msg, + username=dnr_account[0], password=dnr_account[1], html=True) + except: + logger.error('Could not send e-mail. Check internet connection.') + + logger.error('THERE ARE OLDER TRANSACTIONS THAT HAVE NOT PROCESSED!! Exting...') + sys.exit() + + # Invalid file specified. print error msg and exit. + else: + logger.error('File name: %s does not conform to as400_jsi_#####.csv', sys.argv[1]) + sys.exit() + + session = loadSession() + + ''' + Process each CSV file + as400_jsi_00001.csv + ''' + for f in files: + if not('as400_jsi' in f): + logger.debug('Skipping ' + f + ' wrong file name...') + continue + + rows = [] + eRows = [] + jLot = f.split('_')[2].split('.')[0] + ERRORS = False #This can be set True in a number of places, This conditions whether or not an e-mail is sent + + logger.info('****************************************') + logger.info('Loading file ' + f) + + #Read file fully into rows + with open(f, 'rb') as g: + reader = csv.reader(g) + for row in reader: + rows.append(row) + + #rows = sorted(rows, key=itemgetter(0, 1, 2)) #sort rows read by Order,Line,Piece since AS400 seems to supply data in random order + logger.info(str(len(rows)) + ' records read.') + tickets_processed = [] + + ''' + Process each record in file + ''' + for row in reversed(rows): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + if row[TICKET] in tickets_processed: + logger.warn('Encountered ticket: ' + row[TICKET] + ' more than once in single transmission!') + row.append('DUPLICATE TICKET ROW. DOUBLE CHECK YARDAGE/WEIGHT IN JOMAR!!!') + eRows.append(row) + ERRORS = True + continue + + jOrder = 'null' + jLine = -1 + jItem = -1 + + #if(row[ORDER] in PULL_ORDERS): + #logger.warn(row[ORDER] + ' is a pull order. Ignoring for now! Ticket: ' + row[TICKET]) + #row.append('**IGNORING PULL ORDERS FOR NOW**') + #eRows.append(row) + #ERRORS = True + #continue + + #Neil says no pieces in HLD bin should make it this far; just in case... + if(row[BIN] == 'HLD' or (row[WAREHOUSE] == 'SH' and row[BIN] == ' ')): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + logger.warn(row[WAREHOUSE] + '-' + row[BIN] + ' is not a valid bin! Ticket: ' + row[TICKET]) + row.append('INVALID BIN') + eRows.append(row) + ERRORS = True + continue + + if(float(row[FINISHED_YARDS]) <= 0): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + logger.warn('Finished Yards must be > 0. Ticket: ' + row[TICKET]) + row.append('Finished Yards must be > 0') + eRows.append(row) + ERRORS = True + continue + + if(float(row[INVOICED_YARDS]) <= 0): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + logger.warn('Invoiced Yards must be > 0. Ticket: ' + row[TICKET]) + row.append('Invoiced Yards must be > 0') + eRows.append(row) + ERRORS = True + continue + + #Get Jomar order number + logger.info('Querying order header table for Jomar order number...') + logger.debug('SELECT * FROM TXUYUV00 WHERE TXUYUV00.UBNR = \'001\' AND TXUYUV00.UPLT = \'00\' AND TXUYUV00.UBLA = \'SO\' AND (TXUYUV00.URBPO = \'' + row[ORDER] + '\' OR TXUYUV00.URBPO = \'' + row[ORDER][1:] + '\');') + q = session.query(TXUYUV00).order_by(TXUYUV00.UBLN).filter(TXUYUV00.UBNR == '001', TXUYUV00.UPLT == '00', TXUYUV00.UBLA == 'SO', ((TXUYUV00.URBPO == row[ORDER]) | (TXUYUV00.URBPO == row[ORDER][1:]))) + if(q.count() != 1 and row[ORDER]): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + logger.warn('Cannot locate order (' + row[ORDER] + ') in Jomar system. Ticket: ' + row[TICKET]) + row.append('Cannot locate order in Jomar system.') + eRows.append(row) + ERRORS = True + continue + for r in q: + jOrder = r.UBLN + logger.info('Jomar order: ' + jOrder + ' Ticket: ' + row[TICKET]) + #print 'Jomar Order:', jOrder, 'Matches Sunbury Order:', row[ORDER] + + #Get Jomar Line Number + logger.info('Querying order detail table for Jomar 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(int(row[LINE])) + ';') + 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 == int(row[LINE])) + if(q.count() != 1 and row[ORDER]): + #print row[ORDER], row[LINE], row[TICKET], row[FINISHED_YARDS], row[INVOICED_YARDS], row[BIN] + logger.warn('AS400 Line: ' + row[LINE] + ' does not exist on Jomar order: ' + jOrder) + row.append('Jomar Order: ' + jOrder + ' - There are no lines on this order where AS400 Line Number = ' + row[LINE] + ' Ticket: ' + row[TICKET]) + eRows.append(row) + ERRORS = True + continue + skip = False + #Verify pattern and color match b/t AS400 and Jomar + for r in q: + if(r[1].XFLX33.strip().upper() != row[SPAT].strip() and row[ORDER]): + skip = True + ERRORS = True + logger.warn('Pattern Mismatch! Jomar: ' + r[1].XFLX33 + ' AS400: ' + row[SPAT].strip() + ' Ticket: ' + row[TICKET]) + row.append('Pattern Mismatch! Jomar: ' + r[1].XFLX33 + ' AS400: ' + row[SPAT].strip()) + eRows.append(row) + if(int(r[1].XFLX34) != int(row[SCOL].strip()) and row[ORDER]): + skip = True + ERRORS = True + logger.warn('Color Mismatch! Jomar: ' + r[1].XFLX34 + ' AS400: ' + row[SCOL].strip() + ' Ticket: ' + row[TICKET]) + row.append('Color Mismatch! Jomar: ' + r[1].XFLX34 + ' AS400: ' + row[SCOL].strip()) + eRows.append(row) + + if(skip == False): + jLine = r[0].VLFN + jItem = r[0].VANR + logger.info('Jomar line: ' + str(jLine) + ' Ticket: ' + row[TICKET]) + logger.info('Jomar item: ' + str(jItem) + ' Ticket: ' + row[TICKET]) + #print ' Jomar Line:', jLine, 'Matches Sunbury Line:', row[LINE] + + if(skip): + continue + + ''' + Not doing this anymore for obvious reasons... Wish I would've gotten the information + concerning the other possible inventory transactions before 04/20/16...... + ''' + #If a roll with this ticket number exists in the inventory, get rid of it + #session.query(TXRYRT00).filter(TXRYRT00.RTRNR == row[TICKET]).delete() + + tickets_processed.append(row[TICKET]) #Made it past all of the error checks, add this ticket to processed list. + + logger.info('Querying Roll/Tag master to see if this ticket exists in Jomar already. Ticket: ' + row[TICKET]) + logger.debug('SELECT COUNT(*) FROM TXRYRT00 WHERE TXRYRT00.RTRNR = ' + row[TICKET] + ';') + q = session.query(TXRYRT00).filter(TXRYRT00.RTRNR == row[TICKET]).count() + if(q == 0): #Do an 08 transaction... + logger.info('No previous record of roll. Adding production receipt (08) transaction. Ticket: ' + row[TICKET]) + i = TXRYBW10() + i.RWAPL = '00' + i.RWSPL = '00' + i.RWSLA = 'SO' + i.RWPLT = '00' + i.RWBNR = '001' + i.RWALA = 'SO' + i.RWBEW = '08' + ''' + MODIFIED - 01/04/2018 - SB-20 + Set RTGRML to 'US' for all rolls + ''' + i.RWGRML = 'US' + if(row[ORDER] not in PULL_ORDERS): + i.RWALN = jOrder + i.RWSLN = jOrder + i.RWAFN = jLine + i.RWSFN = jLine + else: + i.RWALN = None + i.RWSLN = None + i.RWAFN = None + i.RWSFN = None + i.RWANR = jItem + i.RWCHA = jLot + i.RWRNR = row[TICKET] + i.RWPGLG = row[FINISHED_YARDS] + i.RWPLG = row[INVOICED_YARDS] + if(row[BIN] == 'BAG'): + logger.debug('BAG bin = BG Warehouse. Ticket: ' + row[TICKET]) + i.RWLOC = 'BG' + i.RWLPL = '' + elif(row[BIN].strip() == '08'): + logger.debug('08 bin = B8 Warehouse. Ticket: ' + row[TICKET]) + i.RWLOC = 'B8' + i.RWLPL = '' + else: + if(row[WAREHOUSE] == 'SH'): + i.RWLPL = row[BIN] + else: + i.RWLPL = '' + i.RWLOC = row[WAREHOUSE] + i.RWPWG = int(float(row[WEIGHT])) + #i.RWTWG = int(float(row[WEIGHT])) #This was screwing up proformas. This is a tare weight field and so it should be zero for our purposes + i.RWTWG = 0 + i.RWQAL = '01' + + ''' + MODIFIED - 01/04/2018 - SB-20 + Determine if roll is NAFTA qualified. + ''' + i.RWORCD = NAFTA(session, row[SPAT].strip(), str(int(row[SCOL].strip()))) + + session.add(i) + else: + logger.debug('Roll already exists. Querying roll/tag master again to determine what to do...') + logger.debug('SELECT * FROM TXRYRT00 WHERE TXRYRT00.RTRNR = ' + row[TICKET] + ';') + p = session.query(TXRYRT00).filter(TXRYRT00.RTRNR == row[TICKET]) + for r in p: + ''' + MODIFIED 01/23/17 + Ran across a scenario where rolls already shipped in Jomar came through the interface again. + Now checking for shipped yardage here so the script can kick out an error instead of compounding issue. + ''' + if float(r.RTSLG) > 0: + tickets_processed.remove(row[TICKET]) #Remove this ticket from processed list + logger.warn('Shipped yards greater than zero! Ticket: %s', row[TICKET]) + row.append('Shipped yards > 0') + eRows.append(row) + ERRORS = True + continue + + current_lot = r.RTCHA + current_inv_yards = r.RTPLG + current_fin_yards = r.RTPGLG + current_inv_lbs = r.RTPWG + current_bin = r.RTLPL + current_warehouse = r.RTLOC + dest_bin = row[BIN] + dest_warehouse = row[WAREHOUSE] + + #Adjust bins and warehouses for bagging machine nonsense.... + if(row[BIN].strip() == '08'): + dest_bin = '' + dest_warehouse = 'B8' + if(row[BIN].strip() == 'BAG'): + dest_bin = '' + dest_warehouse = 'BG' + + if((float(current_inv_yards) != float(row[INVOICED_YARDS])) or (int(current_inv_lbs) != int(float(row[WEIGHT]))) or (float(current_fin_yards) != float(row[FINISHED_YARDS]))): + logger.info('Roll exists but length/weight differ. Adjusting via a Production Receipt transaction (08) Ticket: ' + row[TICKET]) + + if (float(current_inv_yards) != float(row[INVOICED_YARDS])): + logger.debug('Current Invoiced Yards = %s Transmitted = %s Adjustment = %s', current_inv_yards, row[INVOICED_YARDS], str(float(row[INVOICED_YARDS]) - float(current_inv_yards))) + if (int(current_inv_lbs) != int(float(row[WEIGHT]))): + logger.debug('Current Weight = %s Transmitted = %s Adjustment = %s', current_inv_lbs, row[WEIGHT], (int(float(row[WEIGHT])) - int(current_inv_lbs))) + if (float(current_fin_yards) != float(row[FINISHED_YARDS])): + logger.debug('Current Finished Yards = %s Transmitted = %s Adjustment = %s', current_fin_yards, row[FINISHED_YARDS], str(float(row[FINISHED_YARDS]) - float(current_fin_yards))) + + i = TXRYBW10() + i.RWAPL = '00' + i.RWSPL = '00' + i.RWSLA = 'SO' + i.RWPLT = '00' + i.RWBNR = '001' + i.RWALA = 'SO' + i.RWBEW = '08' + if(row[ORDER] not in PULL_ORDERS): + i.RWALN = jOrder + i.RWSLN = jOrder + i.RWAFN = jLine + i.RWSFN = jLine + else: + i.RWALN = None + i.RWSLN = None + i.RWAFN = None + i.RWSFN = None + i.RWANR = jItem + i.RWCHA = current_lot + i.RWRNR = row[TICKET] + i.RWPGLG = str(float(row[FINISHED_YARDS]) - float(current_fin_yards)) + i.RWPLG = str(float(row[INVOICED_YARDS]) - float(current_inv_yards)) + i.RWLOC = current_warehouse.strip() + i.RWLPL = current_bin.strip() + i.RWPWG = (int(float(row[WEIGHT])) - int(current_inv_lbs)) + #i.RWTWG = (int(float(row[WEIGHT])) - int(current_inv_lbs)) + i.RWTWG = 0 + i.RWQAL = '01' + session.add(i) + + if((current_bin.strip() != dest_bin.strip()) or (current_warehouse.strip() != dest_warehouse.strip())): + logger.info('Roll exists but bins are different. Adding a movement (06) transaction...' + current_warehouse + '-' + current_bin + ' --> ' + dest_warehouse + '-' + dest_bin + ' Ticket: ' + row[TICKET]) + i = TXRYBW10() + i.RWAPL = '00' + i.RWSPL = '00' + i.RWSLA = 'SO' + i.RWPLT = '00' + i.RWBNR = '001' + i.RWALA = 'SO' + i.RWBEW = '06' + if(row[ORDER] not in PULL_ORDERS): + i.RWALN = jOrder + i.RWSLN = jOrder + i.RWAFN = jLine + i.RWSFN = jLine + else: + i.RWALN = None + i.RWSLN = None + i.RWAFN = None + i.RWSFN = None + i.RWANR = jItem + i.RWCHA = current_lot + i.RWRNR = row[TICKET] + i.RWPGLG = row[FINISHED_YARDS] + i.RWPLG = row[INVOICED_YARDS] + i.RWLOC = dest_warehouse.strip() #Xfer to warehouse + i.RWLPL = dest_bin.strip() #Xfer to bin + i.RWLO2 = current_warehouse.strip() #Xfer from warehouse + i.RWLP2 = current_bin.strip() #Xfer from bin + i.RWPL2 = '00' #Xfer from plant + i.RWPWG = int(float(row[WEIGHT])) + #i.RWTWG = int(float(row[WEIGHT])) + i.RWTWG = 0 + i.RWQAL = '01' + session.add(i) + + if( (float(current_inv_yards) == float(row[INVOICED_YARDS])) and (int(current_inv_lbs) == int(float(row[WEIGHT]))) and + (current_bin.strip() == dest_bin.strip()) and (current_warehouse.strip() == dest_warehouse.strip()) and + (float(current_fin_yards) == float(row[FINISHED_YARDS]))): + logger.info('Nothing appears to be different. Ignoring. Ticket: ' + row[TICKET]) + + ''' + End record processing + ''' + + #If errors were encountered during processing, e-mail the suspect rows to Aaron in a csv + if ERRORS: + #Dump eRows to errors.csv + logger.debug("Dumping error rows to: " + PATH + "ERRORS\\ERRORS_" + jLot + ".csv") + with open(PATH + "ERRORS\\ERRORS_" + jLot + ".csv", "wb") as g: + writer = csv.writer(g) + writer.writerows(eRows) + try: + if not TEST_MODE: + logger.info('E-mailing Aaron list of error rows...') + + dnr_account = load_email_credentials() + send_mail('donotreply@example.com', ROLL_ERROR_RECIPIENTS, 'AS400 -> Jomar Inventory Errors', 'See Attachment.', + username=dnr_account[0], password=dnr_account[1], files=[PATH + "ERRORS\\ERRORS_" + jLot + ".csv"]) + except: + logger.error('Could not send e-mail. Check internet connection.') + + if not TEST_MODE: + logger.debug("Moving " + f + " to " + "PROCESSED\\" + f.split("\\")[2]) + os.rename(f, PATH + "PROCESSED\\" + f.split("\\")[2]) + ''' + End file processing + ''' + + #When all files have been processed commit new records to interface table. + if(TEST_MODE): + logger.warn('**TEST MODE ENABLED. ROLLING BACK DATABASE TRANSACTIONS**') + session.rollback() + else: + logger.debug('Committing database transactions...') + session.commit() + logger.debug('Closing database connection.') + session.close() diff --git a/stm_etl/Jomar_SO_Auto_Close.py b/stm_etl/Jomar_SO_Auto_Close.py new file mode 100644 index 0000000..def4f83 --- /dev/null +++ b/stm_etl/Jomar_SO_Auto_Close.py @@ -0,0 +1,100 @@ +''' +Auto close SOs in Sunbury Jomar based on what went to history in the AS400 last weekend. +To be run every Monday morning. + +Created on: 2018-04-05 + +@author: Wes +''' +import pyodbc, time, logging, sys, os, StringIO, csv, sqlite3 +from sqlalchemy import create_engine +from sqlalchemy.ext.declarative import declarative_base +from sqlalchemy.orm import sessionmaker +from sqlalchemy.sql.expression import update + +EXTRACT_QUERY = 'SELECT ordera FROM wes.sotohist' + +logger = None +LOG_LEVEL = logging.DEBUG + +engine = create_engine("mssql+pyodbc://jsisbprod", echo=False) +Base = declarative_base(engine) +metadata = Base.metadata + +# Jomar Order Header Table +class TXUYUV00(Base): + __tablename__ = 'TXUYUV00' + __table_args__ = {'autoload':True} + +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:\\' + 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.DEBUG) + sh.setFormatter(formatter) + logger.addHandler(sh) + +def loadSession(): + global engine + Session = sessionmaker(bind=engine) + session = Session() + return session + +def so_auto_close_main(): + os.chdir('D:\\') + initializeLogger('Jomar_SO_auto_close') + logger.debug('--------------------------------------------------------------------------------') + + archived_as400_orders = [] + + logger.info('Opening connection with AS400...') + 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() + + logger.info('Fetching list of newly archived orders from AS400...') + for r in c1.execute(EXTRACT_QUERY): + archived_as400_orders.append(r[0]) + + logger.debug(archived_as400_orders) + + logger.debug('Disconnecting from AS400...') + c1.close() + + logger.info('Connecting to JSISBPROD...') + session = loadSession() + + logger.info('Closing Jomar order headers...') + ex = update(TXUYUV00.__table__).where(TXUYUV00.URBPO.in_(archived_as400_orders)).values(UAKT=u"X") + session.execute(ex) + session.commit() + + logger.debug('Disconnecting from JSISBPROD...') + session.close() + +if __name__ == '__main__': + so_auto_close_main() diff --git a/stm_etl/README.md b/stm_etl/README.md new file mode 100644 index 0000000..df04de1 --- /dev/null +++ b/stm_etl/README.md @@ -0,0 +1,64 @@ +# stm-python-interfaces +This code base started out as simply the finished goods interface python script on the D: drive of our test server. + +This location kind of stuck because a network share (INTERFACE) was created for the AS400 side of the finished goods interface and it is non-trivial to change that. From there the FTP package was created to handle the FTP transmissions between us and Maharam/Momentum that needed to be ported over for our shipping/invoicing cutover. At some point after that, it was determined it would be much easier to transmit our invoice data to Wells Fargo via FTP as opposed to true EDI and I took on that project as well which also now resides in the FTP package. + +## IMPORTANT When pulling changes down to production box... + +Before you execute 'git pull' in the production repo; Please be sure to do the following: + +* Terminate scheduler.py job that should be running on the SUNBURYTEXTILE\Administrator account +* Delete all .pyc files in \\FTP (Technically optional. They should be refreshed either way) +* git pull +* Restart scheduler.py + +This will force the recreation of the .pyc files when scheduler.py imports the scripts. + +**Failing to at least terminate the scheduler.py job before pulling changes will result in scheduler continuing to execute old code as the python source will not be recompiled until the scripts are next imported (i.e. restarting scheduler.py)** + +## Getting a test evironment running on your machine + +This code assumes that the root folder is D: just as it is on the test server. The most convenient way I've found to replicate this on my local machine is to: + +* Checkout the source into a directory on our file server. +* Share that folder on the network with myself. +* Mount that network folder as D: on my pc + +From there, you'll need to create a few folders that the scripts will be expecting to be there: + +* D:\\INTERFACE + * \\ERRORS + * \\PROCESSED +* D:\\FTP (_this already should exist since theres code in it_) + * \\INCOMING + * \\logs + * \\OLD + * \\KF + * \\MF + * \\MG + * \\GR + * \\SEND_KF + * \\SENT + * \\SEND_MF + * \\SENT + * \\SEND_MG + * \\SENT + * \\SEND_WF + +With these folders created all of the scripts should be ready to run. + +## Dependencies + +* [Schedule](https://github.com/dbader/schedule): *pip install schedule* +* [Paramiko](https://github.com/paramiko/paramiko): *pip install paramiko* +* [SQL Alchemy](http://www.sqlalchemy.org/): *pip install SQLAlchemy* + +This list is not yet exhaustive. + +## Finished\_Goods\_Interface.py + +This script, when run without any arguments, processes all CSV files located in **\\INTERFACE**. These CSV files are generated by the AS400 when inventory is scanned in shipping. The script created Jomar inventory transactions based on this data that will keep the inventory in Jomar current. + +After the script has processed a CSV it is moved into **\\INTERFACE\\PROCESSED**. If the script encountered any errors during processing the specific rows of the file that caused the errors are written to a separate CSV in **\\INTERFACE\\ERRORS**. These CSV files containing the error rows are e-mailed to Aaron automatically. + +Worth noting: **finishedGoodsStart.ps1** and **finishedGoodsStop.ps1** start and stop a file watcher script that automatically executes Finished\_Goods\_Interface.py as soon as a CSV file is created in **\\INTERFACE**. This should pretty much be running **at all times**. \ No newline at end of file diff --git a/stm_etl/finishedGoodsStart.ps1 b/stm_etl/finishedGoodsStart.ps1 new file mode 100644 index 0000000..f75849e --- /dev/null +++ b/stm_etl/finishedGoodsStart.ps1 @@ -0,0 +1,16 @@ +### SET FOLDER TO WATCH + FILES TO WATCH + SUBFOLDERS YES/NO + $watcher = New-Object System.IO.FileSystemWatcher + $watcher.Path = "D:\INTERFACE" + $watcher.Filter = "*.*" + $watcher.IncludeSubdirectories = $false + $watcher.EnableRaisingEvents = $true + +### DEFINE ACTIONS AFTER A EVENT IS DETECTED + $action = { + Start-Process -FilePath "C:\Python27\python.exe" -ArgumentList "D:\Finished_Goods_Interface.py", $Event.SourceEventArgs.FullPath + } +### DECIDE WHICH EVENTS SHOULD BE WATCHED + SET CHECK FREQUENCY + $created = Register-ObjectEvent $watcher "Created" -Action $action + #$changed = Register-ObjectEvent $watcher "Changed" -Action $action + #$deleted = Register-ObjectEvent $watcher "Deleted" -Action $action + #$renamed = Register-ObjectEvent $watcher "Renamed" -Action $action diff --git a/stm_etl/finishedGoodsStop.ps1 b/stm_etl/finishedGoodsStop.ps1 new file mode 100644 index 0000000..c2842d9 --- /dev/null +++ b/stm_etl/finishedGoodsStop.ps1 @@ -0,0 +1,4 @@ +#Unregister-Event $changed.Id +Unregister-Event $created.Id +#Unregister-Event $deleted.Id +#Unregister-Event $renamed.Id \ No newline at end of file diff --git a/stm_etl/reboot_email.ps1 b/stm_etl/reboot_email.ps1 new file mode 100644 index 0000000..73d3d90 Binary files /dev/null and b/stm_etl/reboot_email.ps1 differ diff --git a/stm_etl/search_errors.py b/stm_etl/search_errors.py new file mode 100644 index 0000000..e1a7077 --- /dev/null +++ b/stm_etl/search_errors.py @@ -0,0 +1,30 @@ +''' +Search through interface CSV files for given ticket number. +''' + +import os, csv, sys +from glob import glob + +TICKET = '32899' +PATH = 'D:\\INTERFACE\\' + +if __name__ == '__main__': + if(len(sys.argv) == 2): + TICKET = sys.argv[1] + + print '****************************************************************************************************' + + files = glob(PATH + 'PROCESSED\*.csv') + for f in files: + rows = [] + with open(f, 'rb') as g: + reader = csv.reader(g) + for row in reader: + rows.append(row) + + for row in rows: + if(row[2] == TICKET): + print 'Ticket', TICKET, 'found in', f + print row + print '' +