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

This commit is contained in:
wes
2026-02-11 06:05:20 -05:00
parent cf0edba776
commit c4b970cbde
33 changed files with 6077 additions and 1 deletions
+168
View File
@@ -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])
+80
View File
@@ -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
+51
View File
@@ -0,0 +1,51 @@
'''
American Custom Finishing ASN Generate
ACF gets a differently structured document (than HTX/SYF/ATX) but with mostly the same information.
It appears to be pipe ('|') delimited.
Fields IN ORDER:
Constant - 'ST'
Constant - 'ST'
Order/Roll
STYLE1-STYLE2
Pattern Name
???
Roll length ex. 061.250
PO number of some sort
Color number
Customer Name
Ship to Address 1
Ship to Address 2
Ship to City, State, Zip 'SUNBURY PA 17801'
??? Looks like a finish code
**Not Used**
**Not Used**
???
???
Finished Description (?)
**Not Used**
**Not Used**
**Not Used**
**Not Used**
**Not Used**
**Not Used**
???
???
'''
import csv
if __name__ == '__main__':
lines = []
data = []
with open('G:\\FTP\\ACF_SAMPLE.txt') as f:
lines = f.readlines()
for line in lines:
row = line.split('|')
for i,field in enumerate(row):
row[i] = field.strip()
data.append(row)
print row
with open('G:\\FTP\\ACF_SAMPLE_FIXED.txt', 'w') as out:
#derp = csv.writer(out, delimiter ='|',quotechar =',',quoting=csv.QUOTE_NONE,escapechar='*')
derp = csv.writer(out, delimiter ='|',quoting=csv.QUOTE_NONE)
derp.writerows(data)
+250
View File
@@ -0,0 +1,250 @@
'''
Created on September 12, 2016
Interface ACF "Inventory Shipped" transmission.
For now I think I'm going to insert 08 transactions into TXRYBW10; creating rolls in Jomar and putting them
right into the ACF drop shipment warehouse.
@author: Wes
'''
import csv, logging, sys, os, re
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
import pyodbc
TEST_MODE = False
# @TODO: Could probably do with making this pattern a little more discerning for the portion preceding the '/'
PROD_ROLL_PAT = ".+[\/]\d+"
#Assign the column indexes to descriptive variables; makes the code below a bit easier to follow.
CUST_ID = 0
ROLL_NUMBER = 1
SHIP_TO_ADDR_NAME = 2
CONTRACT_NUMBER = 3
STYLE_NAME = 4
STYLE_DESC = 5
DATE_RECEIVED = 6
DATE_COMPLETED = 7
DUE_DATE = 8
YARDS_RECEIVED = 9
YARDS_FINISHED = 10
ACF_SHIP_DATE = 11
SHIP_METHOD = 12
PICK_BATCH = 13
TRACKING = 14
UPS_COST = 15
CUST_FINISH_NO = 16
CUST_PO_NO = 17
logger = None
LOG_LEVEL = logging.DEBUG
#engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
engine = create_engine("mssql+pyodbc://jTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
class TXRYBW10(Base):
__tablename__ = 'TXRYBW10'
__table_args__ = {'autoload':True}
class TXUYUV00(Base):
__tablename__ = 'TXUYUV00' #Order Header
__table_args__ = {'autoload':True}
class TXUYUF01(Base):
__tablename__ = 'TXUYUF01' #Order Detail
__table_args__ = {'autoload':True}
class ItemInfo(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
########################################################################
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
def queryTICKETFL(uNum, ticket, aPatt):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = []
c1.execute("SELECT LINE#, PATNAM, COLOR# FROM STMDATA.TICKETFL WHERE ORDERA = '" + uNum + "' AND TICK# = " + ticket)
for row in c1:
for d in row:
ret.append(d)
c1.close()
#Verify that the pattern name ACF transmitted matches our records and return if so
if(len(ret) > 0 and ret[1].strip() == aPatt):
return ret
else:
return []
def backToSunbury(customerRoll, record):
global logger
# @TODO: Method stub for when we eventually do need to process these.
if(customerRoll):
logger.debug('Ignoring roll that did not drop ship: ' + record[ROLL_NUMBER])
else:
logger.debug('Ignoring non-production roll number: ' + record[ROLL_NUMBER])
def shippedToCust(customerRoll, record):
global logger
# @TODO: Method stub for when we eventually do need to process these.
if(customerRoll):
pass
else:
logger.debug('Ignoring non-production roll number: ' + record[ROLL_NUMBER])
def createInventoryTransaction(jOrder, jLine, jItem, roll, yards):
jLot = None #Optional we don't use lot numbers for rolls, could maybe throw YYYYMMdd in here to tie the jomar rolls to the tranmission.
i = TXRYBW10()
i.RWAPL = '00'
i.RWSPL = '00'
i.RWSLA = 'SO'
i.RWPLT = '00'
i.RWBNR = '001'
i.RWALA = 'SO'
i.RWBEW = '08' #Transaction type. '08' = Production receipt. This creates a new roll
i.RWALN = jOrder
i.RWSLN = jOrder
i.RWAFN = jLine
i.RWSFN = jLine
i.RWANR = jItem
i.RWCHA = jLot
i.RWRNR = roll
i.RWPGLG = yards
i.RWPLG = yards
i.RWPWG = 0 #This is a weight column. What to do about this I wonder?
i.RWTWG = 0 #This is a weight column. This should equal RWPWG
#All rolls process by this script are going into ACF drop ship warehouse for now
i.RWLOC = '11' #Warehouse
i.RWLPL = '' #Bin number. ACF warehouse is not bin tracked so ''
return i
def processACF(inFile):
global logger
os.chdir('D:\FTP')
initializeLogger('ACF_interface')
logger.info('****************************************')
session = loadSession()
prodRollPat = re.compile(PROD_ROLL_PAT)
rawLines = []
logger.info('Loading: ' + inFile)
try:
f = open(inFile, 'r')
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
except IOError:
logger.fatal('Unable to load file. Exiting...')
return
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
# @TODO: Figure out naming convention for these so we can save them in \OLD\ACF
#logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\KF\\')
#os.rename(inFile, 'OLD\\KF\\' + fileName)
sys.exit("Nothing to process.")
#Main processing loop
for line in rawLines:
#There ID for us is 'ST' evidently, we shouldn't process anything that isn't ST. Just in case...
if not line[CUST_ID] == 'ST':
logger.warn('Ignoring non-ST row...')
#Check data in "Roll Number" column to see if it's a production roll
#Production pieces should be in "ORDER#/ROLL#" format
if not(prodRollPat.match(line[ROLL_NUMBER]) == None):
#Process only rolls that drop shipped
#This is kinda hacky. Just looking for 'Sunbury' in the ship-to name...
if not 'sunbury' in line[SHIP_TO_ADDR_NAME].lower():
logger.info('**********')
as400Order, rollNumber = line[ROLL_NUMBER].split('/')
jOrder = None
jLine = None
jItem = None
logger.info('ACF drop shipped roll ' + rollNumber + ' to customer. Transferring it to warehouse 11...')
#Translate AS400 order number to Jomar Order number
logger.debug('SELECT * FROM TXUYUV00 WHERE TXUYUV00.UBNR = \'001\' AND TXUYUV00.UPLT = \'00\' AND TXUYUV00.UBLA = \'SO\' AND (TXUYUV00.URBPO = \'' + as400Order + '\' OR TXUYUV00.URBPO = \'' + as400Order[1:] + '\');')
q = session.query(TXUYUV00).order_by(TXUYUV00.UBLN).filter(TXUYUV00.UBNR == '001', TXUYUV00.UPLT == '00', TXUYUV00.UBLA == 'SO', ((TXUYUV00.URBPO == as400Order) | (TXUYUV00.URBPO == as400Order[1:])))
if(q.count() == 1):
for r in q:
jOrder = r.UBLN
logger.info('AS400 Order: ' + as400Order + ' --> Jomar Order: ' + jOrder)
else:
logger.error('Could not locate Jomar order for: ' + as400Order + ' skipping...')
continue
ticketflInfo = queryTICKETFL(as400Order, rollNumber, line[STYLE_DESC])
if(len(ticketflInfo) == 0):
logger.error('Failed to look up ticket in AS400. Skipping...')
continue
as400Line = ticketflInfo[0]
colorNum = ticketflInfo[2]
#Look up order line in Jomar using Jomar order number + AS400 line number
logger.debug('SELECT VLFN, VANR, XFLX33, XFLX34 FROM TXUYUF01 LEFT JOIN TXMYAT03 ON TXUYUF01.VBNR = TXMYAT03.XBNR AND TXUYUF01.VPLT = TXMYAT03.XPLT AND TXUYUF01.VANR = TXMYAT03.XANR WHERE TXUYUF01.VBNR = \'001\' AND TXUYUF01.VPLT = \'00\' AND TXUYUF01.VBLA = \'SO\' AND TXUYUF01.VBLN = \'' + jOrder + '\' AND TXUYUF01.VPLIN = ' + str(as400Line) + ';')
q = session.query(TXUYUF01,ItemInfo).order_by(TXUYUF01.VBLN).filter(TXUYUF01.VBNR == ItemInfo.XBNR, TXUYUF01.VPLT == ItemInfo.XPLT, TXUYUF01.VANR == ItemInfo.XANR).filter(TXUYUF01.VBNR == '001', TXUYUF01.VPLT == '00', TXUYUF01.VBLA == 'SO', TXUYUF01.VBLN == jOrder, TXUYUF01.VPLIN == as400Line)
if(q.count() == 1):
for r in q:
jLine = r[0].VLFN #Jomar Line Number
jItem = r[0].VANR #Jomar Item Number
logger.info('AS400 Order Line: ' + str(as400Line) + ' --> Jomar Line: ' + str(jLine))
logger.info('Jomar Item Number: ' + jItem)
#Create inventory transaction here
print createInventoryTransaction(jOrder, jLine, jItem, rollNumber, line[YARDS_FINISHED])
else:
backToSunbury(True, line)
else:
if not 'sunbury' in line[SHIP_TO_ADDR_NAME].lower():
#Drop shipped
shippedToCust(False, line)
else:
#Back to Sunbury
backToSunbury(False, line)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'No file name specified!'
else:
processACF(sys.argv[1])
+96
View File
@@ -0,0 +1,96 @@
'''
Melissa asked to be e-mailed the total yardage of our un-shipped, un-invoiced finish goods inventory.
@author: Wes
'''
from alert import send_alert_html
import time, logging, sys, os, StringIO, csv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com', 'user1@example.com']
SUBJECT = 'FGI Report'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
SUM_QUERY = 'select SUM(RTPLG) [total_yards_produced] from TXRYRT00 WHERE (RTSLG = 0.0 AND RTILG = 0.0)'
DETAIL_QUERY = 'select RTALN [order_num], RTRNR [roll_num], RTANR [item_num], xartbz [item_desc], RTLOC [warehouse], RTLPL [bin], RTPLG [yards_produced], RTSLG [yards_shipped], RTILG [yards_invoiced] from TXRYRT00 LEFT JOIN txmyat03 ON RTANR = XANR where (RTSLG = 0.0 AND RTILG = 0.0) ORDER BY RTALN, RTRNR'
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.DEBUG)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def run_FGI_report():
os.chdir('D:\FTP')
initializeLogger('FGI_Report')
logger.debug('Opening connection with DB...')
session = loadSession()
logger.debug('Running SUM query...')
for row in session.execute(SUM_QUERY):
logger.info('Total Finished Goods Inventory: %s yards', "{0:.3f}".format(row.total_yards_produced))
#Open file for writing
logger.debug('Creating Aaron\'s csv file...')
fname = time.strftime("FGI_%Y%m%d.csv")
logger.debug('Creating %s. Using \'excel\' dialect for csv writer.', fname)
f = open(fname, 'wb')
writer = csv.writer(f, dialect='excel')
writer.writerow(['order_num', 'roll_num', 'item_num', 'item_desc', 'warehouse', 'bin', 'yards_produced', 'yards_shipped', 'yards_invoiced'])
logger.debug('Running detail query...')
for row in session.execute(DETAIL_QUERY):
writer.writerow(row)
f.close()
try:
logger.debug('Sending e-mail...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[fname], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
logger.debug('Deleting %s', fname)
os.remove(fname)
if __name__ == '__main__':
run_FGI_report()
+82
View File
@@ -0,0 +1,82 @@
'''
Generate Hitex transmission files
The AS400 essentially transmits copies of the FINMAS and FINDET files, which can be found in STMDATA, directly to
the Hitex/Syn-fin/Applied. As such I used the record formats for those files to buld the MASTER and DETAIL record format strings
you see below.
I strongly doubt Hitex/Syn-fin/Applied require _all_ of the fields in these tables. Who requires what, though, remains
a mystery. I've listed the description of the fields as they appear in the AS400 in the order they need to be passed to
the format() command below.
* denotes fields that are definitely expanding.
FINMAS Fields, IN ORDER:
Address 1
Customer Name
Customer Number (4 digit bill to account)
Ship to Number (2 digit shipto sub-account)
* These 2 fields (6 digits) --> 10 digit account #
Ship to Name
Ship to Address 1
Ship to Address 2
Ship to City
Ship to State
Ship to Zip
Ship to Country
Country Code
Packing List Number
* 5 digit --> 10 digit
Shipping Suffix (?)
Box Number (?)
Stain Repellent Code
* 1 char --> 5 char
Backing Code
* 1 char --> 5 char
Complete Code (?)
Number of Labels Needed (?)
Date yyMMdd
* yyMMdd --> YYYYMMdd (Preferable, not required)
Finish Code
* 4 char --> 5 char
FINDET Fields, IN ORDER:
Packing List Number
* 5 digit --> 10 digit
Shipping Suffix (?)
Box Number (?)
Pattern Name
Style 1
Style 2
Color Number
Order Number
* 6 character --> 10 digit
P/O Number
Roll Number
* 5 digit --> 10 digit
Stain Repellent Code
* 1 char --> 5 char
Backing Code
* 1 char --> 5 char
Complete Code (?)
Cut off Yards
Cut off Eights
Finished Yards
Finished Eights
Finisher Invoice Number
Finisher BOL Number
Finisher Description
Application Code (?)
Finish Code
* 4 char --> 5 char
Customer Pattern
Customer Color
'''
MASTER_RECORD_FORMAT = '{:30s}{:30s}{:4s}{:2s}{:30s}{:30s}{:30s}{:20s}{:2s}{:05d}{:20s}{:10s}{:05d}{:1s}{:02d}{:1s}{:1s}{:1s}{:03d}{:06d}{:4s}'
DETAIL_RECORD_FORMAT = '{:05d}{:1s}{:02d}{:15s}{:>4s}{:06d}{:04d}{:6s}{:15s}{:05d}{:1s}{:1s}{:1s}{:03d}{:01d}{:03d}{:01d}{:7s}{:06d}{:15s}{:2}{:4s}{:15}{:20}'
if __name__ == '__main__':
print MASTER_RECORD_FORMAT.format('PER ROUTING GUIDE','', '', '', 'SUNBURY TEXTILE MILLS, INC.', 'MILLER STREET',
'', 'SUNBURY', 'PA', 17801, '', '', 54065, '', 0, '', '', '', 1, 160719, '')
#print DETAIL_RECORD_FORMAT.format(18342, ' ', 0, 'SYNTAX', 'P', 9953, 2301, 'U51959', '729005S', 25524, '', '', '', 62, 0, 0, 0, '251348', 65, '', '54', '', '', '15310/565/STRAWBERRY')
+8
View File
@@ -0,0 +1,8 @@
#Kravet POs
Get-ChildItem "D:\FTP" -Filter Kravet_to_vendor_*.csv | Foreach-Object { python D:\FTP\kf_850.py $_.Name }
#Momentum POs
Get-ChildItem "D:\FTP" -Filter SBR_850_*.txt | Foreach-Object { python D:\FTP\mg_850.py $_.Name }
#Maharam POs
Get-ChildItem "D:\FTP" -Filter MAH_850_*.txt | Foreach-Object { python D:\FTP\mf_850.py $_.Name }
View File
+108
View File
@@ -0,0 +1,108 @@
'''
Used by other scripts (eventually) to abstract away e-mail alert related code.
'''
import sqlite3, smtplib, os
from email import Encoders
from email.MIMEBase import MIMEBase
from email.MIMEImage import MIMEImage
from email.MIMEMultipart import MIMEMultipart
from email.MIMEText import MIMEText
from email.Utils import COMMASPACE, formatdate
'''
I found the bulk of this function already written on someone's blog for sending mail
via outlook 365. I made a few modifications for our purposes.
'''
def send_mail(send_from, send_to, subject, text, files=None,
data_attachments=None, server="smtp.example.com", port=25,
tls=False, html=False, images=None,
config_file=None, config=None):
if files is None:
files = []
if images is None:
images = []
if data_attachments is None:
data_attachments = []
if config_file is not None:
config = ConfigParser.ConfigParser()
config.read(config_file)
if config is not None:
server = config.get('smtp', 'server')
port = config.get('smtp', 'port')
tls = config.get('smtp', 'tls').lower() in ('true', 'yes', 'y')
username = config.get('smtp', 'username')
password = config.get('smtp', 'password')
msg = MIMEMultipart('related')
msg['From'] = send_from
msg['To'] = send_to if isinstance(send_to, basestring) else COMMASPACE.join(send_to)
msg['Date'] = formatdate(localtime=True)
msg['Subject'] = subject
msg.attach( MIMEText(text, 'html' if html else 'plain') )
for f in files:
part = MIMEBase('application', "octet-stream")
part.set_payload( open(f,"rb").read() )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % os.path.basename(f))
msg.attach(part)
for f in data_attachments:
part = MIMEBase('application', "octet-stream")
part.set_payload( f['data'] )
Encoders.encode_base64(part)
part.add_header('Content-Disposition', 'attachment; filename="%s"' % f['filename'])
msg.attach(part)
for (n, i) in enumerate(images):
fp = open(i, 'rb')
msgImage = MIMEImage(fp.read())
fp.close()
msgImage.add_header('Content-ID', '<image{0}>'.format(str(n+1)))
msg.attach(msgImage)
smtp = smtplib.SMTP(server, int(port))
if tls:
smtp.starttls()
#Load do not reply account credentials from SQLite DB and authenticate
#cursor = sqlite3.connect('D:\\FTP\\ftp.db').execute("SELECT * from accounts where code = 'WMR'")
#auth = cursor.fetchone()
#smtp.login(auth[1], auth[2])
smtp.sendmail(send_from, send_to, msg.as_string())
smtp.close()
'''
Allows use of html in message. Wraps entire message in font tag that forces a fixed width font for now.
I'm not sure that's a great idea or not.
'''
def send_alert_html(to, subject, message='', attachments=None, sendFrom='donotreply@example.com'):
message = '<font face="Courier New, Courier, monospace"><pre>' + message + '</pre></font>'
message = message + '<br><br><br>'
message = message + '<font face="Lucida Sans Unicode"><b>Please do not reply to this message; it was sent from an unmonitored email address.</b></font>'
send_mail(sendFrom, to, subject, message, files=attachments, html=True)
def send_alert(to, subject, message='', attachments=None, sendFrom='donotreply@example.com'):
send_mail(sendFrom, to, subject, message, files=attachments)
if __name__ == '__main__':
#Send a test message if this script is executed
test_data = [['1234567890','9876543210'],
['0101379900','0000055555'],
['1000930000','0000052319']]
msg = 'Designtex has cut into the following pieces:<br><br><table border="1"><tr><th>Order #</th><th>Roll #</th></tr>'
for t in test_data:
msg = msg + '<td>' + t[0] + '</td><td>' + t[1] + '</td></tr>'
msg = msg + '</table>'
send_alert_html('admin@example.com', 'Designtex Test', message=msg)
+325
View File
@@ -0,0 +1,325 @@
'''
Created on Nov 3, 2015
@author: wes
'''
import ftplib
import sys, sqlite3
import logging
import os.path
from glob import glob
#logger = None
LOG_LEVEL = logging.DEBUG
################################################################################
def load_ftp_credentials(code):
vault = sqlite3.connect('D:\\FTP\\ftp.db')
cursor = vault.execute("SELECT * from ftp_auth where code='" + code + "'")
for row in cursor:
return [row[1], row[3], row[4]]
def gettext(ftp, filename, outfile=None):
if outfile is None:
outfile = sys.stdout
# use a lambda to add newlines to the lines read from the server
ftp.retrlines("RETR " + filename, lambda s, w=outfile.write: w(s+"\n"))
def getbinary(ftp, filename, outfile=None):
if outfile is None:
outfile = sys.stdout
ftp.retrbinary("RETR " + filename, outfile.write)
def getFileList(ftp):
log = []
ftp.retrlines('LIST', callback=log.append)
files = (line.rsplit(None, 1)[1] for line in log)
return list(files)
def upload(ftp, f):
ext = os.path.splitext(f)[1]
if ext in (".txt", ".htm", ".html", ".xml"):
ftp.storlines("STOR " + f.split('\\')[-1], open(f))
else:
ftp.storbinary("STOR " + f.split('\\')[-1], open(f, "rb"), 1024)
def initializeLogger(name='log'):
#global logger
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
return logger
################################################################################
def kravetOrderDownload():
logger = initializeLogger('kravet_850')
KRAVET = load_ftp_credentials('kf')
logger.info('Connecting to ' + KRAVET[0] + ' as ' + KRAVET[1])
ftp = ftplib.FTP(KRAVET[0])
ftp.login(KRAVET[1], KRAVET[2])
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('kravet_to_vendor')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + KRAVET[0])
ftp.quit()
def maharamOrderDownload():
logger = initializeLogger('Maharam_850')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd OUTGOING')
ftp.cwd('OUTGOING')
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('mah_850_')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def maharamInvoiceUpload():
logger = initializeLogger('Maharam_810')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd Incoming')
ftp.cwd('Incoming')
files = glob('D:\\FTP\\SEND_MF\\*.xml')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MF\\SENT')
os.rename(f, 'SEND_MF\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def maharamASNUpload():
logger = initializeLogger('Maharam_856')
MAHARAM = load_ftp_credentials('mf')
logger.info('Connecting to ' + MAHARAM[0] + ' as ' + MAHARAM[1])
ftp = ftplib.FTP(MAHARAM[0])
ftp.login(MAHARAM[1], MAHARAM[2])
logger.info('cwd Incoming')
ftp.cwd('Incoming')
files = glob('D:\\FTP\\SEND_MF\\STM_856_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MF\\SENT')
os.rename(f, 'SEND_MF\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MAHARAM[0])
ftp.quit()
def momentumOrderDownload():
logger = initializeLogger('Momentum_850')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd outbound')
ftp.cwd('outbound')
for f in getFileList(ftp):
#logger.debug(f)
if(f.lower().startswith('sbr_850_') and f.lower().endswith('.txt')):
if(os.path.isfile(f)):
logger.info('Already downloaded:' + f)
else:
logger.info('Downloading: ' + f)
gettext(ftp, f, open(f, 'w'))
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def momentumInvoiceUpload():
logger = initializeLogger('Momentum_810')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd inbound')
ftp.cwd('inbound')
files = glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
#Create an empty file of the same name with '.done' extension and upload
#They require it for some reason....
doneFile = f.split('.')[0] + '.done'
file(doneFile, 'w').writelines([])
logger.info('Uploading ' + doneFile.split('\\')[-1])
upload(ftp, doneFile)
logger.info('Deleting ' + doneFile)
os.remove(doneFile)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MG\\SENT')
os.rename(f, 'SEND_MG\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def momentumASNUpload():
logger = initializeLogger('Momentum_856')
MOMENTUM = load_ftp_credentials('mg')
logger.info('Connecting to ' + MOMENTUM[0] + ' as ' + MOMENTUM[1])
ftp = ftplib.FTP(MOMENTUM[0])
ftp.login(MOMENTUM[1], MOMENTUM[2])
logger.info('cwd inbound')
ftp.cwd('inbound')
files = glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt')
for f in files:
logger.info('Uploading ' + f.split('\\')[-1])
upload(ftp, f)
#Create an empty file of the same name with '.done' extension and upload
#They require it for some reason....
doneFile = f.split('.')[0] + '.done'
file(doneFile, 'w').writelines([])
logger.info('Uploading ' + doneFile.split('\\')[-1])
upload(ftp, doneFile)
logger.info('Deleting ' + doneFile)
os.remove(doneFile)
logger.info('Moving ' + f + ' to D:\\FTP\\SEND_MG\\SENT')
os.rename(f, 'SEND_MG\\SENT\\' + f.split('\\')[-1])
logger.info('Closing connection to ' + MOMENTUM[0])
ftp.quit()
def glenravenASNDownload():
logger = initializeLogger('GR_INCOMING__856')
#They're using our FTP site for this
STM_FTP = load_ftp_credentials('stm')
logger.info('Connecting to %s as %s', STM_FTP[0], STM_FTP[1])
ftp = ftplib.FTP(STM_FTP[0])
ftp.login(STM_FTP[1], STM_FTP[2])
logger.info('cwd glenraven')
ftp.cwd('glenraven')
logger.info('cwd to_STM')
ftp.cwd('to_STM')
for f in getFileList(ftp):
if f in ['.', '..']:
continue
f_local = 'INCOMING\\' + f.replace('sunbury', 'GR_ASN')
if(os.path.isfile(f_local)):
logger.info('Already downloaded: %s', f)
else:
logger.info('Downloading: %s -> %s', f, f_local)
gettext(ftp, f, open(f_local, 'w'))
logger.info('Deleting: %s from FTP site', f)
ftp.delete(f)
logger.info('Closing connection to %s', STM_FTP[0])
ftp.quit()
def UNIFI_846_Download():
logger = initializeLogger('GR_INCOMING__856')
#They're using our FTP site for this
STM_FTP = load_ftp_credentials('stm')
logger.info('Connecting to %s as %s', STM_FTP[0], STM_FTP[1])
ftp = ftplib.FTP(STM_FTP[0])
ftp.login(STM_FTP[1], STM_FTP[2])
logger.info('cwd unifi')
ftp.cwd('unifi')
logger.info('cwd to_STM')
ftp.cwd('to_STM')
for f in getFileList(ftp):
if f in ['.', '..']:
continue
f_local = 'INCOMING\\' + f.replace('ipnet', 'UNIFI_846').replace('.file846', '')
f_local = f_local[:f_local.index('.csv') + 4]
if(os.path.isfile(f_local)):
logger.info('Already downloaded: %s', f)
else:
logger.info('Downloading: %s -> %s', f, f_local)
gettext(ftp, f, open(f_local, 'w'))
logger.info('Deleting: %s from FTP site', f)
ftp.delete(f)
logger.info('Closing connection to %s', STM_FTP[0])
ftp.quit()
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Missing parameter!'
sys.exit(0)
if(len(sys.argv) == 3):
l = sys.argv[2].lower()
if(l.startswith('debug')):
LOG_LEVEL = logging.DEBUG
elif(l.startswith('info')):
LOG_LEVEL = logging.INFO
elif(l.startswith('warning')):
LOG_LEVEL = logging.WARNING
elif(l.startswith('error')):
LOG_LEVEL = logging.ERROR
elif(l.startswith('critical')):
LOG_LEVEL = logging.CRITICAL
s = sys.argv[1].lower()
if(s.startswith('kf850')):
kravetOrderDownload()
if(s.startswith('mf850')):
maharamOrderDownload()
if(s.startswith('mg850')):
momentumOrderDownload()
if(s.startswith('mf810')):
maharamInvoiceUpload()
if(s.startswith('mf856')):
maharamASNUpload()
if(s.startswith('mg810')):
momentumInvoiceUpload()
if(s.startswith('mg856')):
momentumASNUpload()
if(s.startswith('gr856')):
glenravenASNDownload()
if(s.startswith('un846')):
UNIFI_846_Download()
+232
View File
@@ -0,0 +1,232 @@
'''
Create and send e-mail alert listing Designtex pieces that need to be invoiced.
'''
import logging, os, pyodbc, sys
from alert import send_alert_html
from sqlalchemy import create_engine, or_
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com', 'user3@example.com', 'user4@example.com', 'user1@example.com', 'user5@example.com']
#RECIPIENTS = ['admin@example.com']
SUBJECT = 'Designtex Reconciliation Report'
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ROLL_NUM = 1
JOMAR_LINE = 3
ITEM_NUM = 4
AS400_LINE = 5
PATTERN = 6
COLOR = 7
PACKING_LIST = 8
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
#engine = create_engine("mssql+pyodbc://JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class DTXCUTFL(Base): #EDI data from Designtex arrives here
__tablename__ = 'DTXCUTFL'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXUYUV00(Base): #Order header
__tablename__ = 'TXUYUV00'
__table_args__ = {'autoload':True}
class TXUYUF01(Base): #Order detail
__tablename__ = 'TXUYUF01'
__table_args__ = {'autoload':True}
class TXMYAT03(Base): #Sales item detail
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class AS400OrderLookupError(RuntimeError):
''' Raised if we fail to lookup the AS400 order number using the Jomar order number. '''
''' This will eventually go away '''
class JomarOrderLookupError(RuntimeError):
''' Raised if we fail to lookup the Jomar order number using the AS400 order number '''
''' This will eventually go away '''
class AS400LineLookupError(RuntimeError):
''' Raised if AS400 line numbers cannot be retrieved from the Jomar order '''
''' This will eventually go away '''
class RollNotFoundError(RuntimeError):
''' Raised if no record in TXRYRT00 is found for a given ticket number '''
class ItemLookupError(RuntimeError):
''' Raised if no record is found in TXMYAT03 for a given item number '''
''' This should never happen '''
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
def getAS400PackingList(order, line, ticket):
global logger
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
q = "select SHP# from stmdata.XSHPFIL WHERE ORDERA = '"
q = q + order + "' and LINE# = "
q = q + line + " and TICK# = "
q = q + ticket
logger.debug(q)
c1.execute(q)
for row in c1:
ret = row[0]
c1.close()
return str(ret)
def dtxInvoiceAlert():
initializeLogger('dtx_cut_alert')
session = loadSession()
processed_records = []
logger.info('Querying DTXCUTFL for any unprocessed rolls...')
tickets = session.query(DTXCUTFL).filter(or_(DTXCUTFL.PROCESSED != 'Y', DTXCUTFL.PROCESSED == None))
#Load in unprocessed roll/order numers
for row in tickets:
processed_records.append([row.__dict__['STM_ORDER#'], row.__dict__['STM_TICK#']])
if len(processed_records) == 0:
logger.warn('Nothing to process. Exiting...')
return
#Fix/lookup Order Numbers
logger.info('Querying order header table for order numbers...')
for row in processed_records:
#Not sure whether they are transmitting new or old style order numbers, allow for both.
if(row[0].startswith('U')):
row.append(row[0]) #We want U number to be 3rd column
oHeader = session.query(TXUYUV00).filter(TXUYUV00.URBPO == row[0], TXUYUV00.UBLA == 'SO')
if oHeader.count() != 1:
raise JomarOrderLookupError('Failed to lookup Jomar order number. AS400 Order:', row[0])
for r in oHeader:
logger.debug('AS400 Order: %s --> Jomar Order: %s', row[0], r.UBLN)
row[0] = r.UBLN #Overwrite contents of first element with Jomar Order
else:
oHeader = session.query(TXUYUV00).filter(TXUYUV00.UBLN == row[0])
if oHeader.count() != 1:
raise AS400OrderLookupError('Failed to lookup AS400 order number. Jomar Order:', row[0])
for r in oHeader:
logger.debug('Jomar Order: %s --> AS400 Order: %s', row[0], r.URBPO)
row.append(r.URBPO)
logger.debug('*')
logger.info('Querying roll/tag master for order line numbers and SKUs...')
for row in processed_records:
rtMaster = session.query(TXRYRT00).filter(TXRYRT00.RTBNR == '001', TXRYRT00.RTRNR == row[ROLL_NUM])
if rtMaster.count() != 1:
raise RollNotFoundError('Roll does not exist in Jomar!', row[ROLL_NUM])
for r in rtMaster:
logger.debug('Roll: %s --> Line: %s SKU: %s', row[ROLL_NUM], r.RTAFN, r.RTANR)
row.append(str(r.RTAFN))
row.append(r.RTANR)
logger.debug('*')
logger.info('Querying order detail table for AS400 Line numbers...')
for row in processed_records:
oDetail = session.query(TXUYUF01).filter(TXUYUF01.VBLA == 'SO', TXUYUF01.VBLN == row[0], TXUYUF01.VBNR == '001', TXUYUF01.VLFN == row[JOMAR_LINE], TXUYUF01.VPLT == '00')
if oDetail.count() != 1:
raise AS400LineLookupError('Failed to retrieve AS400 line number.', row[0], row[JOMAR_LINE])
for r in oDetail:
logger.debug('Jomar Order: %s Line: %s --> AS400 Line: %s', row[0], row[JOMAR_LINE], r.VPLIN)
row.append(str(r.VPLIN))
logger.debug('*')
logger.info('Querying sales item info table for STM pattern and color...')
for row in processed_records:
itemDetail = session.query(TXMYAT03).filter(TXMYAT03.XANR == row[ITEM_NUM], TXMYAT03.XBNR == '001', TXMYAT03.XPLT == '00')
if itemDetail.count() != 1:
raise ItemLookupError('Sales item lookup failed? I don\'t even know...', row[ITEM_NUM])
for r in itemDetail:
logger.debug('Jomar Item: %s --> %s-%s', row[ITEM_NUM], r.XFLX33, r.XFLX34)
row.append(r.XFLX33)
row.append(r.XFLX34)
logger.debug('*')
logger.info('Querying XSHPFIL on the AS400 to get packing list numbers...')
for row in processed_records:
row.append(getAS400PackingList(row[2], row[AS400_LINE], row[ROLL_NUM]))
logger.debug('*')
#Build e-mail message
msg = 'Designtex has cut into the following pieces:<br><br>'
msg = msg + '<table border="1">'
msg = msg + '<tr>'
msg = msg + '<th>Roll #</th>'
msg = msg + '<th>AS400 Packing List</th>'
msg = msg + '<th>Jomar Order</th>'
msg = msg + '<th>Pattern</th>'
msg = msg + '<th>Color</th>'
msg = msg + '</tr>'
for row in processed_records:
msg = msg + '<tr>'
msg = msg + '<td>' + row[ROLL_NUM] + '</td>'
msg = msg + '<td>' + row[PACKING_LIST] + '</td>'
msg = msg + '<td>' + row[0] + '</td>'
msg = msg + '<td>' + row[PATTERN] + '</td>'
msg = msg + '<td>' + row[COLOR] + '</td>'
msg = msg + '</tr>'
msg = msg + '</table>'
logger.info('Sending e-mails...')
send_alert_html(RECIPIENTS, SUBJECT, message=msg)
p = [row[ROLL_NUM].strip() for row in processed_records]
if(len(p) > 0 and not TEST_MODE):
logger.info('Marking records processed in database...')
flag = 'UPDATE DTXCUTFL SET PROCESSED = \'Y\' where STM_TICK# in ' + str(p).replace('[', '(').replace(']', ')')
session.execute(flag)
session.commit()
if __name__ == '__main__':
#getAS400PackingList('U58475', '1', '52828') #Test packing number retrieval from AS400
dtxInvoiceAlert()
+160
View File
@@ -0,0 +1,160 @@
'''
Created on January 19, 2018
Anderson plant needs an ASN for rolls we weave for them.
This script is pulling data from our current implementation of Jomar (before we are on CF Jomar)
So, GR-Custom Fabrics is set up as customer 8000 in our Jomar; they will be flagged for 856 documents.
This should allow me to use the standard ASN tables I've used for Maharam and Momentum.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import logging
import sys
import StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
# Only 2 patterns in the beginning; might as well just use a dict
NAFTA = {"ADENA PURE" : "Y", "TAILORED INDIGO" : "N"}
CF_ITEM = {"ADENA PURE" : "FF 0000063322", "TAILORED INDIGO" : "FF 0000063311"}
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to GR-Anderson'
MESSAGE = None
TEST_MODE = True
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['8000']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__table_args__ = {'autoload':True}
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
ASN tables
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
def create_GR_Anderson_Ship_Notice():
global MESSAGE
initializeLogger('MAH_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
# Header record if required create here
# Attempt to join xref table in here?
detailQ = session.query(CSPASN03,CSPASN04).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup,
CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust,
CSPASN03.MSTBIL == shipmentNumber,
CSPASN03.BOLNUM == plNumber)
detailCount = detailQ.count()
for detail in detailQ:
print CF_ITEM[detail[1].SB_PATTERNAME], NAFTA[detail[1].SB_PATTERNAME]
# Write lines out to file here; one file per shipment
if(len(shipments_to_flag_processed) > 0 and not TEST_MODE):
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
logger.debug('Comitting changes to database...')
session.commit()
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
else:
pass #add e-mail alert code here
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
create_GR_Anderson_Ship_Notice()
+254
View File
@@ -0,0 +1,254 @@
'''
Created on Apr 27, 2017
Import Glen Raven ASN data into the DB
@author: Wes
'''
from alert import send_alert_html
import datetime, csv, logging, sys, os, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
from itertools import groupby
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Inbound Glen Raven ASN Processed'
MESSAGE = None
TEST_MODE = False
GR_AP_ACCOUNT = '001306'
JOMAR_PO_ORDER_TYPE = 'JB'
VN = 0
PACK_SLIP = 1
YARN_ITEM = 2
COLOR_DESC = 3
PO_DO = 4
LOT_NUMBER = 5
CASE = 6
CASE_UNITS = 7
GRS_LBS = 8
NET_LBS = 9
TRNS_DATE = 9
SHIP_DATE = 10
SHIP_VIA = 11
logger = None
LOG_LEVEL = logging.DEBUG
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
#engine = create_engine("mssql+pyodbc://@JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
#---------------------------------------------------------------------#
class PSPASN00(Base):
__tablename__ = 'PSPASN00' #Inboud ASN table
__table_args__ = {'autoload':True}
class TXEYAT00(Base):
__tablename__ = 'TXEYAT00' #Vendor Item Cross reference table
__table_args__ = {'autoload':True}
class TXUYUF01(Base):
__tablename__ = 'TXUYUF01' #Order Detail Table
__table_args__ = {'autoload':True}
#---------------------------------------------------------------------#
class ItemTranslationError(RuntimeError):
''' Raising this if we fail to lookup STM item using GR item '''
class POError(RuntimeError):
''' Raising this if we fail to lookup required info from relevant PO '''
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Query the vendor item xref table to translate GR --> STM
Likely reasons this failed:
* _gr_item_ not entered as a xref for any of our items
* _gr_item_ entered for more than 1 of our items
Returns the matching STM item number if found.
'''
def GRtoSTM(gr_item):
global session
global logger
q = session.query(TXEYAT00).filter(TXEYAT00.UALNR == GR_AP_ACCOUNT, TXEYAT00.UALAN == gr_item)
if(q.count() == 1):
for row in q:
return row.UAANR
else:
logger.debug('GRtoSTM() rows returned: %s', q.count())
return None
'''
Query the order detail table by _po_ and _item_.
Likely reasons this failed:
* _po_ doesn't actually exist
* _item_ not ordered on _po_
* _item_ appears on more than 1 line on _po_
Returns the line number of _item_ on _po_ if found.
'''
def getLineNum(po, item):
global session
global logger
q = session.query(TXUYUF01).filter(TXUYUF01.VBLA == JOMAR_PO_ORDER_TYPE, TXUYUF01.VBLN == po, TXUYUF01.VANR == item)
if(q.count() == 1):
for row in q:
return row.VLFN
else:
return None
def processInboundGRASN(inFile):
global MESSAGE
global session
os.chdir('D:\FTP')
initializeLogger('gr_856_inbound_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: %s', inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving %s to D:\\FTP\\OLD\\GR\\', inFile)
os.rename(inFile, 'OLD\\GR\\' + fileName)
session.close()
return
rawLines.sort(key=itemgetter(PO_DO)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(PO_DO)) #Create group by iterator based on PO number
#For each unique PO appearing in this transmission
for poNumber, lines in purchaseOrders:
logger.info('****')
logger.info('Processing records for purchase order: %s', poNumber)
#For each record of this transmission on poNumber
for line in lines:
if line[VN] not in ['19']:
logger.warn('Encountered record with invalid vendor number!')
logger.warn(line)
continue
#Translate GR item to STM item
stm_item = GRtoSTM(line[YARN_ITEM])
if stm_item:
#Query order detail by PO and STM item to get the correct PO line number
line_num = getLineNum(poNumber, stm_item)
if line_num:
logger.info('Processing Case: %s (%s)', line[CASE], stm_item)
asn = PSPASN00()
asn.BUSINESS_UNIT = '001'
asn.PLANT = '00'
asn.VENDOR = GR_AP_ACCOUNT
asn.PO_NUMBER = poNumber
asn.PO_ITEM = stm_item
asn.PO_LINE = line_num
asn.PACKING_SLIP = line[PACK_SLIP]
asn.CASE_NUMBER = line[CASE]
asn.QTY_GROSS = line[GRS_LBS]
asn.QTY_NET = line[NET_LBS]
asn.VENDOR_LOT = line[LOT_NUMBER]
session.add(asn)
else:
logger.error('Failed to find a line on PO %s for: %s', poNumber, stm_item)
try:
send_alert_html(RECIPIENTS, 'Error Processing GR ASN', attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
raise POError('Failed to retrieve PO info from database', poNumber, stm_item)
else:
logger.error('Failed to lookup STM item number for GR Item: %s', line[YARN_ITEM])
try:
send_alert_html(RECIPIENTS, 'Error Processing GR ASN', attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
raise ItemTranslationError('Failed to lookup STM item number for GR Item', line[YARN_ITEM])
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warn('**TEST MODE ENABLED ROLLING BACK TRANSACTIONS**')
session.rollback()
session.close()
if not TEST_MODE:
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\GR\\')
os.rename(inFile, 'OLD\\GR\\' + fileName)
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python gr_856_in.py D:\\FTP\\INCOMING\\GR_ASN_0100334639.csv'
else:
processInboundGRASN(sys.argv[1])
+399
View File
@@ -0,0 +1,399 @@
'''
Created on Sep 18, 2015
Interface a Kravet PO csv file into Jomar EDI tables
@author: Wes
'''
from alert import send_alert_html
import datetime, csv, logging, sys, os, re, StringIO
from itertools import groupby, tee
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Kravet 850 Processed.'
MESSAGE = None
TEST_MODE = False
STM_ITEM_PATTERN = "[\s\S]+[*]\d+$"
KFI_ITEM_PATTERN = "\d+[.]\d+[.]\d+"
STREET = '1500 HIGHWAY 29 SOUTH'
CITY = 'ANDERSON'
PO_NUM = 0
LINE_NUM = 1
SHIPMENT_NUM = 2
VENDOR_NAME = 3
CUSTOMER_NUM = 4
ORDER_DATE = 5
SHIP_TO_NAME = 6
SHIP_1 = 7
SHIP_2 = 8
SHIP_CITY = 9
SHIP_STATE = 10
SHIP_ZIP = 11
SHIP_COUNTRY = 12
BILL_1 = 13
BILL_2 = 14
BILL_CITY = 15
BILL_STATE = 16
BILL_ZIP = 17
STM_ITEM = 18
FINISHES = 19
KRAVET_ITEM = 20
ORDERED_ITEM = 21
QTY = 22
UNITS = 23
UNIT_PRICE = 24
EXT_PRICE = 25
NOTES = 26
PAYMENT_TERMS = 27
FREIGHT_TERMS = 28
SHIP_VIA = 29
CURRENCY = 30
DESCRIPTION = 31
SIDEMARK = 34
logger = None
LOG_LEVEL = logging.DEBUG
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
class OrderHeader(Base):
__tablename__ = 'CSPEDI50H' #EDI 850 Header Table
__table_args__ = {'autoload':True}
class OrderDetail(Base):
__tablename__ = 'CSPEDI50D' #EDI 850 Detail Table
__table_args__ = {'autoload':True}
class ItemXref(Base):
__tablename__ = 'TXUYAT01' #Customer Item -> Sunbury Item reference table
__table_args__ = {'autoload':True}
class ItemInfo(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
#----------------------------------------------------------------------
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Most of the data we receive from Kravet needs to be "massaged" into something that's
actually usable. Trying to consolidate these "fixes" into one place...
For reference, the agreed upon format for the STM item field is:
PATTERN*COLOR
Where:
* PATTERN contains only letters, spaces, ', and -
* COLOR contains only digits
Examples:
What they sent:
KIRSTEN 73383*2015
MULBERRY 73151*COL.825
What they should have sent:
KIRSTEN*2015
MULBERRY*0825
'''
def massageSTM(fromKravet):
global logger
og = fromKravet
if not (fromKravet.count('*') == 1):
#logger.error("Unexpected format encountered for vendor item string...")
return fromKravet
fromKravet = re.sub("[^A-Za-z0-9*'\-\s]+", '', fromKravet) #Remove all special characters besides *, ', -, and spaces
#Today's kludge brought to you by: 'KIRSTEN 73383*2015'
i = fromKravet.index('*')
fromKravet = re.sub('[0-9]+', '', fromKravet[:i]).strip() + fromKravet[i:] #Remove any digits to the left of *
#The following bandaid was inspired by: 'MULBERRY 73151*COL.825'
i = fromKravet.index('*')
fromKravet = fromKravet[:i] + re.sub('[A-Za-z]+', '', fromKravet[i:]).strip() #Remove non-digits to the right of *
#Zero pad 3 digits colors while we're at it...
i = fromKravet.index('*') + 1
fromKravet = fromKravet[:i] + fromKravet[i:].zfill(4)
logger.debug('Massaging STM item column: %s --> %s', og, fromKravet)
return fromKravet
'''
'''
def massageKFI(fromKravet):
global logger
og = fromKravet
if(fromKravet.startswith('GWF-')):
fromKravet = fromKravet[4:]
logger.debug('Massaging KFI item column: %s --> %s', og, fromKravet)
return fromKravet
def lookUpSunburyItem(session, k_item, ven):
global logger
k_item = k_item.replace('#','') #Yet another bandaid fix for Kravet...
if(re.compile(STM_ITEM_PATTERN).match(k_item) == None):
logger.error('Kravet transmitted junk in the STM item column: ' + k_item)
return ''
k = k_item.split('*')
k_pat = k[0]
k_col = re.compile(r'[^\d.]+').sub('', k[1]).zfill(4)
q = session.query(ItemXref,ItemInfo).order_by(ItemXref.UACCOL).filter(ItemXref.UACCNR == ven, ItemInfo.XFLX33 == k_pat, ItemInfo.XFLX34 == k_col, ItemXref.UACANR == ItemInfo.XANR)
if(q.count() == 1):
for row in q:
logger.info('STM Pattern: ' + k_pat + ' Color: ' + k_col + ' --> Sunbury Item: ' + row[0].UACANR)
return row[0].UACANR
else:
session.close()
logger.warn('Failed to lookup item # for: ' + k_pat + '-' + k_col + ' is the cross reference entered in Jomar?')
return ''
#raise RuntimeError('Failed to lookup Sunbury item for STM Pattern: ' + k_pat + ' Color: ' + k_col + ' is the cross reference entered in Jomar?')
def processKravetPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('kf_850_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: ' + inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\KF\\')
os.rename(inFile, 'OLD\\KF\\' + fileName)
session.close()
return
#sys.exit("Nothing to process.")
rawLines.sort(key=itemgetter(0)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(0)) #Create group by iterator based on PO number
for poNumber, lines in purchaseOrders:
logger.info('***')
logger.info('Processing purchase order ' + poNumber)
#Create a copy of the lines iterator so we can pull the first line of the PO now to fill in header info
lines, flb = tee(lines)
fl = flb.next()
header = OrderHeader()
#Order header constants
header.UBNR = '001'
header.UPLT = '00'
header.ULOAD = 'N'
header.UBLA = 'SO'
header.FLX01 = '00'
header.URLRV = '0' #An ID column, in case the same PO is transmitted twice both may be added to DB and sorted out in Jomar
header.UDAT = int(datetime.datetime.strptime(filename, "Kravet_to_vendor_%d-%b-%Y.csv").strftime('%Y%m%d')) #Transaction Date -> Order Date
logger.info('PO Date: ' + str(header.UDAT))
header.UIHS = int(datetime.datetime.strptime(fl[ORDER_DATE], "%d/%m/%Y").strftime('%Y%m%d'))
logger.info('Ordered On: ' + str(header.UIHS))
header.FOB = fl[FREIGHT_TERMS]
logger.info('FOB: ' + header.FOB)
header.XTX5 = fl[PAYMENT_TERMS]
logger.info('Payment terms: ' + header.XTX5)
header.UIHR = poNumber
if(fl[CUSTOMER_NUM] == 'KFI' or fl[CUSTOMER_NUM] == '3190'):
logger.info('Column 5 = "KFI" using customer number: 3190')
header.UKDR = '3190'
header.UKDN = '319000'
header.UNA2 = 'KRAVET FABRICS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319003'
elif(fl[CUSTOMER_NUM] == 'LJI' or fl[CUSTOMER_NUM] == '3191'):
logger.info('Column 5 = "LJI" using customer number: 3191')
header.UKDR = '3191'
header.UKDN = '319100'
header.UNA2 = 'LEE JOFA'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319101'
elif(fl[CUSTOMER_NUM] == 'BRF' or fl[CUSTOMER_NUM] == '746'):
logger.info('Column 5 = "BRF" using customer number: 0746')
header.UKDR = '0746'
header.UKDN = '074600'
header.UNA2 = 'BRUNSCHWIG & FILS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '074600'
elif(fl[CUSTOMER_NUM] == 'GIS'):
logger.info('Column 5 = "GIS" using customer number: 3194')
header.UKDR = '3194'
header.UKDN = '319400'
header.UNA2 = 'KRAVET FABRICS/GIS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319401'
elif(fl[CUSTOMER_NUM] == 'GSR'):
logger.info('Column 5 = "GSR" using customer number: 3194')
header.UKDR = '3194'
header.UKDN = '319400'
header.UNA2 = 'KRAVET FABRICS/GIS'
if(fl[SHIP_1].startswith(STREET) and fl[SHIP_CITY].startswith(CITY)):
header.UKDN = '319401'
else:
logger.error('Unknown Customer Code: ' + fl[CUSTOMER_NUM])
session.close()
raise ValueError('Unknown Customer Code: ' + fl[CUSTOMER_NUM])
if(len(fl[SHIP_TO_NAME].strip()) > 0):
header.UNA2 = fl[SHIP_TO_NAME]
header.UNA3 = fl[SHIP_1]
header.UNA4 = fl[SHIP_2]
header.UORT = fl[SHIP_CITY]
header.USTA = fl[SHIP_STATE]
header.ULND = fl[SHIP_COUNTRY]
header.UPLZ = fl[SHIP_ZIP]
line = 1
orderLines = []
for l in lines:
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = line
logger.info('Order Line ' + str(line))
oline.VMGS = l[QTY]
logger.info('Quantity Ordered: ' + oline.VMGS + ' ' + l[UNITS] )
oline.VPR2 = l[UNIT_PRICE]
logger.info('Unit Prince: ' + oline.VPR2)
#Attempt to fix various common issues with the xref data sent from Kravet
stmItem = massageSTM(l[STM_ITEM])
kravetItem = massageKFI(l[KRAVET_ITEM])
oline.VANR = lookUpSunburyItem(session, stmItem, header.UKDR)
oline.VKDR = header.UKDR
oline.VKDL = header.UKDN
oline.VIHR = l[PO_NUM]
oline.VTXT1 = l[NOTES][:65]
logger.info('Notes: ' + oline.VTXT1)
#If possible parse Sunbury pattern and color and insert into database
if not(re.compile(STM_ITEM_PATTERN).match(stmItem) == None):
sItem = stmItem.split('*')
oline.VFLX01 = sItem[0]
oline.VFLX02 = sItem[1]
else:
logger.warn('STM item is in an unusual format: ' + stmItem)
#If possible parse Kravet pattern and color and insert into database
if not(re.compile(KFI_ITEM_PATTERN).match(kravetItem) == None):
kItem = kravetItem.split('.')
oline.VFLX03 = kItem[0]
oline.VFLX04 = kItem[1]
else:
logger.warn('Kravet item number is in an unusual format: ' + l[KRAVET_ITEM])
oline.VRLRV = '0' #ID column for adding duplicate POs to EDI tables
line = line + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == header.UKDR, OrderHeader.UIHR == header.UIHR).count()
if(dups > 0):
logger.warn('PO ' + header.UIHR + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
header.URLRV = str(dups)
for line in orderLines:
line.VRLRV = str(dups)
session.add(header)
for line in orderLines:
session.add(line)
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warn('**TEST MODE ENABLED ROLLING BACK TRANSACTIONS**')
session.rollback()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\KF\\')
os.rename(inFile, 'OLD\\KF\\' + fileName)
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python kf_850.py D:\\FTP\\Kravet_to_vendor_25-MAY-2016.csv'
else:
processKravetPo(sys.argv[1])
+1 -1
View File
@@ -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
+399
View File
@@ -0,0 +1,399 @@
'''
Created on Dec 2, 2014
There was code in the original RPG/CL programs to e-mail Moe and Malcolm
if the pattern "OPALESCENT" comes through on an order. I think this is probably
best handled within Jomar and not here.
@author: Wes
'''
from alert import send_alert_html
import datetime, logging, sys, os, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Maharam 850 Processed.'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
CR_DATA_LENGTHS = [1, 8, 5]
HR_DATA_LENGTHS = [1, 12, 8, 30, 30, 30, 30, 2, 15, 30, 30, 30, 1]
DR_DATA_LENGTHS = [1, 3, 15, 4, 6, 3, 9, 9, 8, 50, 50, 50, 50]
RR_DATA_LENGTHS = [1, 80]
SR_DATA_LENGTHS = [1, 5]
S_TYPE_CODE = 0 # len 1
S_LINES = 1 # len 5
########################################################################
class OrderHeader(Base):
__tablename__ = 'CSPEDI50H' #EDI 850 Header Table
__table_args__ = {'autoload':True}
class OrderDetail(Base):
__tablename__ = 'CSPEDI50D' #EDI 850 Detail Table
__table_args__ = {'autoload':True}
class ItemXref(Base):
__tablename__ = 'TXUYAT01' #Customer Item -> Sunbury Item reference table
__table_args__ = {'autoload':True}
#----------------------------------------------------------------------
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Splits the supplied string into len(lengths) elements.
The length of element i will be equal to lengths[i]
'''
def split_string_by_position(a_string, lengths):
result = []
position = 0
for length in lengths:
result.append(a_string[position:position+length])
position = position+length
return result
def trim_elements(a_list):
result = []
for ele in a_list:
result.append(ele.strip())
return result
def getSunburyCust(hccb, addr, city):
addressList = ['45 RASONS CT', '74 HORSEBLOCK ROAD', '175 MUFFIN LANE']
cityList = ['HAUPPAUGE', 'YAPHANK', 'CUYAHOGA FALLS']
if (len(hccb) > 0):
return 659000
else:
if(addr in addressList[0]) and (city in cityList[0]):
return 343900
elif(addr in addressList[1]) and (city in cityList[1]):
return 3439
elif(addr in addressList[2]) and (city in cityList[2]):
return 343905
else:
#logger.warning('Encountered new bill to address in transmission. Using will-advise ship-to (343999)')
return 343999
def lookUpSunburyItem(session, v_item, v_col, v_num):
if len(str(v_num)) > 4:
ven = str(v_num)[:-2]
else:
ven = v_num
#This query retrieves a list of every item we have for this Pattern + Customer combo
q = session.query(ItemXref).order_by(ItemXref.UACCOL).filter(ItemXref.UACPAT.like(v_item + "%"), ItemXref.UACCNR == ven)
#q = session.query(ItemXref).order_by(ItemXref.UACCOL).filter(ItemXref.UACPAT.like(v_item), ItemXref.UACCNR == ven)
for row in q:
col = int(row.UACCOL.split('/')[0])
#Iterate over results till we find matching color #
if(int(v_col) == col):
logger.info('Maharam Pattern Number: ' + str(v_item) + ' Color: ' + str(v_col) + ' = Sunbury Item: ' + row.UACANR)
return row.UACANR
#If we made it to here the pattern/color combo does not exist in the xref table...
logger.warn('Failed to find a Sunbury item number for: ' + str(v_item) + ' - ' + str(v_col))
return ''
#raise RuntimeError('Failed to find a Sunbury item number for: ' + str(v_item) + ' - ' + str(v_col))
def processMaharamPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('mf_850_interface')
logger.info('****************************************')
#Read downloaded 850 file into list
logger.info('Loading ' + inFile)
f = open(inFile, 'r')
lines = f.readlines()
f.close()
# Last character on each line is '\n'; trim it off
logger.debug('Doctoring up transmitted lines...')
for idx, val in enumerate(lines):
lines[idx] = val[:-1]
# Validate transmission length
logger.debug('Validating transmission length...')
sr = split_string_by_position(lines[len(lines) - 1], SR_DATA_LENGTHS)
if(sr[0] == 'S'):
s_lines = int(sr[1])
if not(s_lines == (len(lines) - 2)):
logger.error('Transmission Corrupt: Number of records actually transmitted does not match what they say they transmitted...')
raise ValueError('Transmission Corrupt: Number of records actually transmitted does not match what they say they transmitted...')
else:
logger.info('They transmitted ' + str((len(lines) - 2)) + ' lines and we received ' + str(s_lines) + ' lines. Good.')
else:
logger.error('Transmission Corrupt: Last record in transmission is not a summary record...')
raise ValueError('Transmission Corrupt: Last record in transmission is not a summary record...')
# Parse Control Record
logger.info('Parsing Control Record:')
cr = split_string_by_position(lines[0], CR_DATA_LENGTHS)
if not(cr[0] == 'T'):
logger.error('Transmission Corrupt: Missing Control Record...')
raise ValueError('Transmission Corrupt: Missing Control Record...')
xmit_date = datetime.datetime.strptime(cr[1], "%m%d%Y")
mah_xmit_seq = int(cr[2])
logger.info('\t Transmission Date: ' + xmit_date.strftime('%m/%d/%Y'))
logger.info('\t Maharam Transmission Sequence #: ' + str(mah_xmit_seq))
# Parse Header Record
logger.info('')
logger.info('Parsing Header Record:')
hr = split_string_by_position(lines[1], HR_DATA_LENGTHS)
if not(hr[0] == 'H'):
logger.error('Transmission Corrupt: Missing Header Record...')
raise ValueError('Transmission Corrupt: Missing Header Record...')
hr = trim_elements(hr) # Trim white space off each data element; we don't need it.
#mah_po_num = int(hr[1])
mah_po_num = hr[1].strip()
po_date = datetime.datetime.strptime(hr[2], "%m%d%Y")
shp_to_name = hr[3]
shp_to_addr1 = hr[4]
shp_to_addr2 = hr[5]
shp_to_city = hr[6]
shp_to_state = hr[7]
shp_to_zip = hr[8]
shp_via = hr[9]
pymt_terms = hr[10]
requester_name = hr[11]
customer_ord_code = hr[12]
logger.info('\t Maharam PO #: ' + str(mah_po_num))
logger.info('\t PO Date: ' + po_date.strftime('%m/%d/%Y'))
logger.info('\t Ship To: ' + shp_to_name)
logger.info('\t Ship to Address 1: ' + shp_to_addr1)
logger.info('\t Ship to Address 2: ' + shp_to_addr2)
logger.info('\t Ship to City: ' + shp_to_city)
logger.info('\t Ship to State: ' + shp_to_state)
logger.info('\t Ship to Zip: ' + str(shp_to_zip))
logger.info('\t Ship Via: ' + shp_via)
logger.info('\t Payment Terms: ' + pymt_terms)
logger.info('\t Requested By: ' + requester_name)
logger.info('\t Custom Order Code: ' + customer_ord_code)
mah_po_line = []
stm_pattern_name = []
stm_color = []
mah_style = []
mah_color = []
yards_ord = []
price = []
req_ship_date = []
line_comment_1 = []
line_comment_2 = []
line_comment_3 = []
line_comment_4 = []
po_comments = []
# Process rest of transmission, may contain any number of D or R record types.
for x in range(2, len(lines) - 1):
rtype = lines[x][:1]
if(rtype == 'D'):
dr = split_string_by_position(lines[x], DR_DATA_LENGTHS)
mah_po_line.append(int(dr[1]))
stm_pattern_name.append(dr[2])
stm_color.append(int(dr[3]))
mah_style.append(int(dr[4]))
mah_color.append(dr[5])
yards_ord.append(float(dr[6]))
price.append(float(dr[7]))
#req_ship_date.append(datetime.datetime.strptime(dr[8], "%m%d%Y"))
line_comment_1.append(dr[9])
line_comment_2.append(dr[10])
line_comment_3.append(dr[11])
line_comment_4.append(dr[12])
elif(rtype == 'R'):
rr = split_string_by_position(lines[x], RR_DATA_LENGTHS)
po_comments.append(rr[1].strip())
else:
logger.error('Transmission Corrupt: Encountered something other than D or R record type in body of transmission...')
raise ValueError('Transmission Corrupt: Encountered something other than D or R record type in body of transmission...')
logger.info('')
logger.info('Detail Records:')
for i in range (0, len(mah_po_line)):
#Recreated from original RPG code (MF850I) modification from 03/03/09. Special price for certain colors of "HIGH FIDELITY"
#They finally ordered some of this and transmitted a price of $18.65; which is higher than the $17.68 and also correct (according to Moe)...
#Going to ignore this going forward.
#if(stm_pattern_name[i] == 'HIGH FIDELITY '):
#if(stm_color[i] == 302) or (stm_color[i] == 6103):
#price[i] = 17.68
logger.info('\t Maharam PO Line #: ' + str(mah_po_line[i]))
logger.info('\t STM Pattern Name: ' + stm_pattern_name[i])
logger.info('\t STM Color #: ' + str(stm_color[i]))
logger.info('\t Maharam Style: ' + str(mah_style[i]))
logger.info('\t Maharam Color: ' + str(mah_color[i]))
logger.info('\t Yards Ordered: ' + str(yards_ord[i]))
logger.info('\t Price/Yard: ' + str(price[i]))
#logger.info('\t Requested Ship Date: ' + req_ship_date[i].strftime('%m/%d/%Y'))
logger.info('\t Detail Line Comment 1: ' + line_comment_1[i])
logger.info('\t Detail Line Comment 2: ' + line_comment_2[i])
logger.info('\t Detail Line Comment 3: ' + line_comment_3[i])
logger.info('\t Detail Line Comment 4: ' + line_comment_4[i])
logger.info('')
logger.info('PO Comments:')
if(len(po_comments) == 0):
logger.info('\t ***No Data***')
else:
for comment in po_comments:
logger.info(comment)
logger.debug('Looking up customer account number...')
sunburyCust = getSunburyCust(customer_ord_code, shp_to_addr1, shp_to_city)
if sunburyCust == 343999:
logger.warning('Encountered new bill to address in transmission. Using will-advise ship-to (343999)')
logger.info('Sunbury Customer Number: ' + str(sunburyCust))
'''
Query server to figure out Jomar internal sequential item number
'''
logger.debug('Querying customer XREF table to get Sunbury item numbers...')
internal_item = []
session = loadSession()
for p, c in zip(mah_style, mah_color):
internal_item.append(lookUpSunburyItem(session, str(p), c, sunburyCust))
#Create Object to be persisted to database
header = OrderHeader()
#Set up constant values in order header record
header.UBNR = '001' #Business Unit
header.UPLT = '00' #Plant #
header.ULOAD = 'N' #Flag to allow order to be processed in Jomar EDI workbench
header.UBLA = 'SO' #Jomar order type "SO" = Sales Order
header.UDAT = int(po_date.strftime('%Y%m%d')) #Transaction Date -> Order Date
header.FOB = ' ' #This customer doesn't transmit this information
header.UIHR = mah_po_num
header.UIHS = int(po_date.strftime('%Y%m%d')) #PO date
header.UKDN = sunburyCust #Ship to customer #
header.UKDR = str(sunburyCust)[:4] #Bill to customer #
header.XTX5 = pymt_terms #Payment terms
header.XTX6 = 'N/A' #Jomar "notes" field sticking order message here
header.URLRV = '0' #An ID column of sorts, in case the same PO is transmitted twice
header.FLX01 = '00'
header.UNA2 = shp_to_name
header.UNA3 = shp_to_addr1
header.UNA4 = shp_to_addr2
header.UORT = shp_to_city
header.USTA = shp_to_state
header.ULND = 'US'
header.UPLZ = shp_to_zip
header.UVT1 = '380'
#Create Detail objects
line = 1
orderLines = []
for item, qty, price, sPat, sCol, mPat, mCol in zip(internal_item, yards_ord, price, stm_pattern_name, stm_color, mah_style, mah_color):
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = line
oline.VMGS = qty
oline.VPR2 = price
oline.VANR = item
oline.VKDR = str(sunburyCust)[:4]
oline.VKDL = sunburyCust
oline.VIHR = mah_po_num
oline.VRLRV = '0'
oline.VFLX01 = sPat
oline.VFLX02 = sCol
oline.VFLX03 = mPat
oline.VFLX04 = mCol
oline.VVT1 = '380'
line = line + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
logger.debug('Checking if this PO already exists in Jomar 850 tables...')
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == header.UKDR, OrderHeader.UIHR == header.UIHR).count()
if(dups > 0):
logger.warning('PO ' + str(header.UIHR) + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
header.URLRV = str(dups)
header.XTX6 = '**POSSIBLE DUPLICATE TRANSMISSION** ' + header.XTX6
for line in orderLines:
line.VRLRV = str(dups)
#submit
session.add(header)
for line in orderLines:
session.add(line)
if not TEST_MODE:
logger.info('Writing records to database...')
session.commit()
else:
logger.warning('**RUNNING IN TEST MODE. ROLLING BACK TRANSACTION**')
session.close()
try:
if not TEST_MODE:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\MF\\')
os.rename(inFile, 'OLD\\MF\\' + inFile.split('\\')[-1])
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
processMaharamPo(sys.argv[1])
sys.exit()
+215
View File
@@ -0,0 +1,215 @@
'''
Created on May 12, 2015
Generate
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import logging
import sys
import StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to Maharam.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3439', '6590']
DROP_SHIP_WAREHOUSES = ['01', '06', '11', '16']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__table_args__ = {'autoload':True}
class XREF(Base): #Customer Xref Table
__tablename__ = 'TXUYAT01'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
def createMaharamShipNotices():
global MESSAGE
initializeLogger('MAH_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
#Build Header Record of transmission
hRec = 'H' + plNumber + shipmentDate.strftime('%m%d%Y') + "\n"
lines.append(hRec)
detailQ = session.query(CSPASN03,CSPASN04).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup,
CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust,
CSPASN03.MSTBIL == shipmentNumber,
CSPASN03.BOLNUM == plNumber)
detailCount = detailQ.count()
for detail in detailQ:
poLine = detail[1].LINE
mahStyle = detail[1].CUST_PATTERNAME.split('/')[0]
mahColor = detail[1].CUST_COLORNAME.split('/')[0]
if(detail[1].SQTY < 10):
ydsShipped = "{0:.5f}".format(detail[1].SQTY)
elif(detail[1].SQTY < 100):
ydsShipped = "{0:.4f}".format(detail[1].SQTY)
else:
ydsShipped = "{0:.3f}".format(detail[1].SQTY)
if(detail[0].WEIGHT < 10):
weight = "{0:.3f}".format(detail[0].WEIGHT)
elif(detail[0].WEIGHT < 100):
weight = "{0:.2f}".format(detail[0].WEIGHT)
else:
weight = "{0:.1f}".format(detail[0].WEIGHT)
pieceNum = detail[1].ROLNUM
ourPattern = detail[1].SB_PATTERNAME.strip()
ourColor = detail[1].SB_COLORNUM.strip()
#@TODO: Need logic to determine proper ship code.
# 01 = Shipped to Maharam
# 02 = Shipped to Customer
# 03 = Shipped to Finisher
# 04 = Drop shipped from finisher
# 99 = UNKNOWN
shipCode = '99'
if(detail[1].WHSE in DROP_SHIP_WAREHOUSES):
shipCode = '04' #Drop shipped from finisher
else:
#if(shipment[1].SNAME in ['MAHARAM FABRICS']):
if(shipment[1].SNAME.upper().find('MAHARAM') > -1):
shipCode = '01'
dRec = 'D'+"{:<12}".format(str(poNumber))+"{:<3}".format(poLine)+"{:<6}".format(mahStyle)+"{:<3}".format(mahColor)+str(ydsShipped)+str(weight)+str(stmOrder)+"{:<10}".format(pieceNum)+"{:<15}".format(ourPattern)+"{:<4}".format(ourColor)+shipCode+"\n"
lines.append(dRec)
logger.debug('D,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s,%s', "{:<12}".format(str(poNumber)),"{:<3}".format(poLine),"{:<6}".format(mahStyle),"{:<3}".format(mahColor),ydsShipped,weight,stmOrder,"{:<10}".format(pieceNum),"{:<15}".format(ourPattern),"{:<4}".format(ourColor), shipCode)
tRec = 'T' + str(detailCount) + "\n"
lines.append(tRec)
sRec = 'S' + str(len(lines)) #+ "\n"
lines.append(sRec)
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MF\\STM_856_' + str(shipmentNumber) + '.txt')
f = open('SEND_MF\\STM_856_' + str(shipmentNumber) + '.txt', 'w')
f.writelines(lines)
f.close()
if(len(shipments_to_flag_processed) > 0 and not TEST_MODE):
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
logger.debug('Comitting changes to database...')
session.commit()
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
else:
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MF\\STM_856_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
'''
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
if __name__ == '__main__':
createMaharamShipNotices()
+214
View File
@@ -0,0 +1,214 @@
'''
Created on Jan 30, 2017
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import os, sys, logging, time, StringIO, datetime
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'ETA Update Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230']
#Yeah.....
#Don't forget to fix account numbers 2215 should be 3398
EXTRACT_QUERY = "select mo.VKTR, mo.VANR, mo.VDAT, mo.VLBLN, mo.VLLFN, SUM(ypp.VYDPERPCE) qty, UKDR, PSFD01, UACPAT, UACCOL from TXUYUF01 mo INNER JOIN TXUYUF01 ypp ON ypp.VBLN = mo.VLBLN AND ypp.VLFN = mo.VLLFN INNER JOIN TXUYUV00 ON mo.VLBLN = UBLN INNER JOIN TXUYAT01 ON UACCNR = UKDR AND UACANR = mo.VANR INNER JOIN TXMYAT03 ON XANR = mo.VANR INNER JOIN TXRYPS00 ON TXMYAT03.XFLX33 = PSPRT AND TXMYAT03.XFLX34 = PSANR AND PSKDN = mo.VKDL and PSKST IN ('COL') WHERE mo.VKDL IN (select KKDN from TXUYKD00 WHERE KKDR IN ('2215', '4318', '5230')) AND mo.VERA NOT LIKE 'E' AND mo.VBLA = 'IB' GROUP BY mo.VKTR, mo.VANR, mo.VDAT, mo.VLBLN, mo.VLLFN, UKDR, PSFD01, UACPAT, UACCOL"
#engine = create_engine("mssql+pyodbc://@jsisbprod", echo=False)
engine = create_engine("mssql+pyodbc://@JTest", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
########################################################################
# SQL Alchemy table declarations. Not sure if I need any for this. #
########################################################################
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__table_args__ = {'autoload':True}
#class CSPINV00(Base):
# __tablename__ = 'CSPINV00'
# __table_args__ = {'autoload':True}
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
@todo: Not sure if I'll actually need this. This is the last document that
the AS400 is sending to them. Just in case...
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
@todo: Not sure if I'll actually need this. This is the last document that
the AS400 is sending to them. Just in case...
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def createMomentumETADocument():
global MESSAGE
initializeLogger('MOM_418_GENERATE')
xmission = [] #List of strings, the individual lines of the transmission file that will be sent
if(TEST_MODE):
logger.info('**RUNNING IN TEST MODE**')
else:
logger.info('**RUNNING IN PRODUCTION MODE**')
logger.debug('Connecting to database...')
session = loadSession()
trans_no = -1
# @todo: create method to grab this from new database
trans_no = getTransNumber()
logger.debug('Using transaction number: %s', trans_no)
'''
Main processing loop goes here
'''
current_po = ''
extract = session.execute(EXTRACT_QUERY)
dCount = 0
for row in extract:
#Level break on PO number
if row.VKTR not in current_po:
#dCount will only be zero for the first pass
#Write t record for previous PO
if dCount > 0:
tRec = 'T~%s\n' % (dCount)
xmission.append(tRec)
logger.debug(tRec[:-1])
dCount = 0
#Write new header record
current_po = row.VKTR
hRec = 'H~%s\n' % (current_po)
xmission.append(hRec)
logger.debug(hRec[:-1])
# @todo: bother figuring out date slip reason code? see doc specs...
dateSlipCode = '0'
promiseDate = datetime.datetime.strptime(str(row.VDAT), "%Y%m%d")
pDate = promiseDate.strftime('%m/%d/%y')
quantity = "{0:.3f}".format(row.qty)
dRec = 'D~%s~%s~%s~%s~%s~%s~YDS\n' % (row.PSFD01, row.UACPAT, row.UACCOL, pDate, dateSlipCode, quantity)
xmission.append(dRec)
dCount += 1 #increment dCount, this is the number that is inserted into the T record for each PO
logger.debug(dRec[:-1])
if dCount > 0:
tRec = 'T~%s\n' % (dCount)
xmission.append(tRec)
logger.debug(tRec[:-1])
# @todo: may need to tune this number later
if len(xmission) > 2:
xmission.append('S~' + str(len(xmission)) + '\n')
logger.debug('S~' + str(len(xmission)))
#delete/comment this later
print ''
for row in xmission:
print row[:-1]
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_418_' + str(trans_no) + '.txt')
f = open('SEND_MG\\SBR_418_' + str(trans_no) + '.txt', 'w')
f.writelines(xmission)
f.close()
try:
logger.info('Sending e-mail alert...')
#send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_418_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
#updateTransNumber(trans_no) #Update transaction number in AS400
else:
logger.warn('Nothing to send!')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
createMomentumETADocument()
+279
View File
@@ -0,0 +1,279 @@
'''
Created on Jul 15, 2015
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import os, sys, logging, time, StringIO
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Invoices Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230', '6600', '4063']
engine = create_engine("mssql+pyodbc://@jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
########################################################################
# Jomar Invoice (EDI 810) staging tables #
########################################################################
class CSPINV00(Base):
__tablename__ = 'CSPINV00'
__table_args__ = {'autoload':True}
class CSPINV01(Base):
__tablename__ = 'CSPINV01'
__table_args__ = {'autoload':True}
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__table_args__ = {'autoload':True}
########################################################################
# Jomar Item description tables. Used to look up Pattern name, color, #
# style, etc. for a given item number #
########################################################################
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
########################################################################
# Jomar Customer master file. Used to look up account names #
########################################################################
class CustomerMaster(Base):
__tablename__ = 'TXUYKD00'
__table_args__ = {'autoload':True}
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
'''
Grabs any unprocessed Maharam invoices from the 810 EDI staging tables in Jomar.
Formats data into text document to send to Maharam via FTP.
Marks invoices as processed.
'''
def createMomentumInvoiceDocument():
global MESSAGE
initializeLogger('MOM_810_GENERATE')
xmission = [] #List of strings, the individual lines of the transmission file that will be sent
invoices_to_flag_processed = []
#First line of transmission is T or P. 'Test' or 'Production'
if(TEST_MODE):
logger.info('**RUNNING IN TEST MODE**')
xmission.append('T~\n')
else:
logger.info('**RUNNING IN PRODUCTION MODE**')
xmission.append('P~\n')
logger.debug('Connecting to database...')
session = loadSession()
trans_no = -1
# t = session.query(mf_transaction_number).filter(mf_transaction_number.doc == '810')
# if t.count() == 1:
# for row in t:
# trans_no = row.txnum
# else:
# logger.fatal('Failed to retrieve transaction number from database... Something ain\'t right. %s', t.count())
# sys.exit('Failed to retrieve transaction number from database... Something ain\'t right.')
trans_no = getTransNumber()
#Second line (Control Line) of transmission is essentially a boilerplate
logger.info('Building Control Line')
ctrlLine = 'TP~12~5702863800~12~9498338886~'
ctrlLine += time.strftime('%Y%m%d')
ctrlLine += '~' + str(trans_no)
xmission.append(ctrlLine + "\n")
logger.debug(ctrlLine)
logger.info('Querying EDI table for unprocessed invoices...')
logger.debug('SELECT * FROM CSPINV00 WHERE IVCUSTB IN (\'3398\', \'4318\', \'5230\', \'6600\') ORDER BY IVIDAT;')
q = session.query(CSPINV00,CustomerMaster).order_by(CSPINV00.IVIDAT).filter(CSPINV00.IVCUSTB.in_(ACC_NUMBERS)).filter(CustomerMaster.KKDN == CSPINV00.IVCUSTB)
for row in q:
if(row[0].IVPROCSS == 'Y'):
continue
invoices_to_flag_processed.append(row[0].IVINV)
businessGroup = row[0].IVBNR
plant = row[0].IVPLT
invoiceType = row[0].IVITYP
logger.info('Building header line for invoice: ' + row[0].IVINV)
#Translate Sunbury bill-to account number to Momentum vendor number
momVendor = ''
if(row[0].IVCUSTB == '3398'):
momVendor = '6975'
elif(row[0].IVCUSTB == '4318'):
momVendor = '0036'
elif(row[0].IVCUSTB == '5230'):
momVendor = '7738'
elif(row[0].IVCUSTB == '6600'):
momVendor = '2082'
elif(row[0].IVCUSTB == '4063'):
momVendor = '2082'
logger.debug('Sunbury Account: %s --> Momentum Vendor: %s', row[0].IVCUSTB, momVendor)
momPO = row[0].IVCPO[-5:]
hLine = 'H~'
hLine += (row[0].IVINV + '~')
hLine += (str(row[0].IVIDAT) + '~')
hLine += (momVendor + '~')
hLine += (momPO + '~')
hLine += (row[0].IVCUSTB + '~')
#hLine += (row[0].IVTERMS + '~')
hLine += ('02~') #Changed to constant '02' per Kim Stolmeier @ Momentum
hLine += (row[1].KNA2 + '~')
hLine += (row[0].IVSADD1 + '~')
hLine += (row[0].IVSCTY + '~')
hLine += (row[0].IVSST + '~')
hLine += (row[0].IVSZIP + '~')
hLine += (row[1].KNA2 + '~')
hLine += (row[0].IVBADD1 + '~')
hLine += (row[0].IVBCTY + '~')
hLine += (row[0].IVBST + '~')
hLine += (row[0].IVBZIP + '~')
hLine += (str(row[0].IVFRT) + '~')
xmission.append(hLine + "\n")
logger.debug(hLine)
logger.info('Querying EDI table for invoice detail lines for invoice: ' + row[0].IVINV)
logger.debug('SELECT * FROM CSPINV01 LEFT JOIN TXMYAT03 ON TXMYAT03.XANR = CSPINV01.I1ITEM WHERE CSPINV01.I1BNR = \'' + businessGroup + '\' AND CSPINV01.I1PLT = \'' + plant + '\' AND CSPINV01.I1ITYP = \'' + invoiceType + '\' AND CSPINV01.I1INV = \'' + row[0].IVINV + '\';')
q2 = session.query(CSPINV01).filter(CSPINV01.I1BNR == businessGroup, CSPINV01.I1PLT == plant, CSPINV01.I1ITYP == invoiceType, CSPINV01.I1INV == row[0].IVINV).join(TXMYAT03, TXMYAT03.XANR == CSPINV01.I1ITEM).add_entity(TXMYAT03)
for row2 in q2:
if(row2[0].I1ITEM == 'INSURANCE'):
'''
@todo: Does Momentum even pay for insurance?
'''
logger.warn('Skipping insurance line!')
continue
logger.info('Building detail line for line ' + str(row2[0].I1ILINE))
dLine = 'D~'
dLine += (row2[1].XFLX33 + '~')
dLine += (row2[1].XFLX34 + '~')
dLine += (row2[1].XFLX35 + '~')
dLine += ("{0:.3f}".format(row2[0].I1QTY) + '~')
dLine += ("{0:.2f}".format(row2[0].I1PRI) + '~')
dLine += (momPO + '~')
xmission.append(dLine + "\n")
logger.debug(dLine)
if len(xmission) > 2:
xmission.append('S~' + str(len(xmission)) + '~')
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_810_' + str(trans_no) + '.txt')
f = open('SEND_MG\\SBR_810_' + str(trans_no) + '.txt', 'w')
f.writelines(xmission)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
#Update EDI tables and transaction number #mf_transaction_number
if(len(invoices_to_flag_processed) > 0 and not TEST_MODE):
#Set process flag on these invoices to 'Y'
ex = update(CSPINV00.__table__).where(CSPINV00.IVINV.in_(invoices_to_flag_processed)).values(IVPROCSS=u"Y")
session.execute(ex)
ex = None
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
logger.debug('Comitting changes to database...')
session.commit()
updateTransNumber(trans_no) #Update transaction number in AS400
logger.debug('Closing database connection...')
session.close()
else:
logger.warn('Nothing to send!')
session.close()
MESSAGE.truncate(0)
if __name__ == '__main__':
createMomentumInvoiceDocument()
+476
View File
@@ -0,0 +1,476 @@
'''
Created on Nov 25, 2014
To process 850 documents from Momentum.
@author: Wes
'''
from alert import send_alert_html
import datetime, time
import sys
import os, re, StringIO
import logging
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy import or_
RECIPIENTS = ['admin@example.com']
SUBJECT = 'New Momentum 850 Processed.'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
DELIMITER = '~'
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
class OrderHeader(Base):
__tablename__ = 'CSPEDI50H' #EDI 850 Header Table
__table_args__ = {'autoload':True}
class OrderDetail(Base):
__tablename__ = 'CSPEDI50D' #EDI 850 Detail Table
__table_args__ = {'autoload':True}
class ItemXref(Base):
__tablename__ = 'TXUYAT01' #Customer Item -> Sunbury Item reference table
__table_args__ = {'autoload':True}
class ItemInfo(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class mg_850_997(Base):
__tablename__ = 'mg_850_997'
__table_args__ = {'autoload':True}
class TXUYKD00(Base):
__tablename__ = 'TXUYKD00'
__table_args__ = {'autoload':True}
#----------------------------------------------------------------------
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
This method reproduces the results of the RPG code in MG850I lines 721 thru 751.
Basically creates a 10 digit Sunbury PO based on their PO# and a prefix we determine
based on their Sunbury customer number
Turns out that for whatever reason the decision was made to have Momentum only send the
last 5 digits of their PO and we rebuild it on our end.... why? Eventually I'm going to ask
them to just send their real PO.
'''
def getSunburyPONum(sentPONum, sunburyCust):
if(sunburyCust >= 339800 and sunburyCust <= 339899):
return (4009300000 + sentPONum)
elif(sunburyCust >= 431800 and sunburyCust <= 431899):
return (2009300000 + sentPONum)
elif(sunburyCust >= 523000 and sunburyCust <= 523099):
return (8209300000 + sentPONum)
#We don't want to blow things up if this fails so just insert what they sent to us
return sentPONum
'''
*** DEPRECATED DO NOT USE ***
*** Preserving this in case I feel like laughing at how bad it is in the future ***
This method reproduces the results of the RPG code in MG850I lines 670 thru 719.
Although this would be better if we just queried the server with the address to get
the ship to #.
'''
def getSunburyCustNum(sentVendorId, sentVendorAddr1, sentVendorCity):
global logger
addressList = ['17811 FITCH ST.', '2600 SURRETT DR.', '1450 EAST WALNUT AVE.', '4552 136TH AVE.', '1715 MEYERSIDE DR, UNIT #2', '655 BRIGHAM RD., STE 100']
cityList = ['IRVINE', 'ARCHDALE', 'FULLERTON', 'HOLLAND', 'MISSISSAUGA ON', 'GREENSBORO']
#Set up base account numbers
ret = 0
if(sentVendorId == 6975):
ret = 339899
elif(sentVendorId == 36):
ret = 431899
elif(sentVendorId == 7738):
ret = 523099
elif(sentVendorId == 2082):
ret = 406399
else:
logger.error('They\'ve transmitted a vendor number we haven\'t encountered before: ' + str(sentVendorId))
raise ValueError("They've transmitted a vendor number we haven't encountered before: " + str(sentVendorId))
#Calculate ship to portion of account number based on transmitted bill to address
if(sentVendorAddr1 == addressList[0] and sentVendorCity == cityList[0]):
return (ret - 99)
elif(ret == 339899):
if(sentVendorAddr1 == addressList[1] and sentVendorCity == cityList[1]):
return (ret - 98)
elif(ret == 431899):
if(sentVendorAddr1 == addressList[3] and sentVendorCity == cityList[3]):
return (ret - 98)
elif(ret == 523099):
if(sentVendorAddr1 == addressList[1] and sentVendorCity == cityList[1]):
return (ret - 98)
elif(sentVendorAddr1 == addressList[5] and sentVendorCity == cityList[5]):
return (ret - 98)
elif(sentVendorAddr1 == addressList[2] and sentVendorCity == cityList[2]):
return (ret - 97)
elif(sentVendorAddr1 == addressList[3] and sentVendorCity == cityList[3]):
return (ret - 96)
elif(sentVendorAddr1 == addressList[4] and sentVendorCity == cityList[4]):
return (ret - 95)
return ret #If the address transmitted is not recognized return 99 ship to code for 'WILL ADVISE'
'''
Attempting to rewrite the above so it's not garbage
'''
def getSunburyCustNum2(session, sentVendorId, sentVendorAddr1, sentVendorCity):
global logger
parent = ''
sentVendorAddr1 = sentVendorAddr1.upper()
sentVendorAddr1 = sentVendorAddr1.replace('DR.', 'DRIVE') #Probably the first of many such bandaids...
sentVendorAddr1 = sentVendorAddr1.replace('RD., STE 100', 'RD') #Bandiad number 2
if(sentVendorId == 6975):
parent = '3398'
elif(sentVendorId == 36):
parent = '4318'
elif(sentVendorId == 7738):
parent = '5230'
elif(sentVendorId == 2082):
parent = '4063'
else:
logger.error('They\'ve transmitted a vendor number we haven\'t encountered before: ' + str(sentVendorId))
raise ValueError("They've transmitted a vendor number we haven't encountered before: " + str(sentVendorId))
logger.debug('Attempting to look up ship to account. Parent: %s, Street: %s, City: %s ', parent, sentVendorAddr1, sentVendorCity)
q = session.query(TXUYKD00).filter(TXUYKD00.KKDR == parent, TXUYKD00.KNA3 == sentVendorAddr1, TXUYKD00.KORT == sentVendorCity)
if(q.count() == 1):
for row in q:
logger.debug('Ship to account found: %s', row.KKDN)
return row.KKDN
else:
logger.debug('Ship to account not found. Using: %s', (parent + '00'))
return (parent + '00')
def lookUpSunburyItem(session, patt, colo, ven):
global logger
k_pat = patt
k_col = re.compile(r'[^\d.]+').sub('', colo).zfill(4)
q = session.query(ItemXref,ItemInfo).order_by(ItemXref.UACCOL).filter(ItemXref.UACCNR == str(ven)[:-2], ItemInfo.XFLX33 == k_pat, ItemInfo.XFLX34 == k_col, ItemXref.UACANR == ItemInfo.XANR, or_(ItemXref.UACCOD == None, ItemXref.UACCOD != 'N'))
if(q.count() == 1):
for row in q:
logger.info('STM Pattern: ' + k_pat + ' Color: ' + k_col + ' --> Sunbury Item: ' + row[0].UACANR)
return row[0].UACANR
else:
logger.warn('Failed to lookup item number for: ' + k_pat + '-' + k_col + ' is the cross reference entered in Jomar?')
if(q.count() > 1):
for r in q:
logger.debug(r)
return ''
def processMomentumPo(inFile):
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('mg_850_interface')
logger.info('****************************************')
seq = '00000'
#Read transmission file into list
logger.info('Processing: ' + inFile)
f = open(inFile, 'r')
seq = inFile.split('_')[2].split('.')[0]
logger.info('Sequence Number: ' + seq)
lines = f.readlines()
f.close()
#Trim white space chars off the end of lines
for x in range(0, len(lines)):
lines[x] = lines[x].strip()
#Check number of lines parsed against number supposed to be transmitted
rec = lines[len(lines) - 1].split(DELIMITER)
if not(rec[0] in 'S'):
logger.error('Transmission file missing summary record..')
raise ValueError('Transmission file missing summary record..')
elif not((len(lines) - 1) == int(rec[1])):
logger.error('Number of records actually transmitted does not match what they said they transmitted..')
raise ValueError('Number of records actually transmitted does not match what they said they transmitted..')
else:
logger.info('They transmitted ' + rec[1] + ' lines and we received ' + str((len(lines) - 1)) + ' lines. Good.')
#Determine if file is prod, test, or invalid
if(lines[0] in 'P'):
logger.info('Production Transmission File')
elif(lines[0] in 'T'):
logger.info('Test Transmission File')
else:
logger.error('Invalid file: The first line should = "P" or "T"')
raise ValueError('Invalid file: The first line should = "P" or "T"')
#parse control record
logger.info('Parsing Control Record...')
rec = lines[1].split(DELIMITER)
if not(rec[0] in 'TP'):
logger.error('Control Record Corrupt/Missing!')
raise ValueError('Control Record Corrupt/Missing...')
sender_edi_qual = int(rec[1])
sender_edi_id = int(rec[2])
recvr_edi_qual = int(rec[3])
recvr_edi_id = int(rec[4])
transaction_date = datetime.datetime.strptime(rec[5], "%Y%m%d")
transaction_no = int(rec[6])
logger.debug('\t Sender EDI Qualifier: ' + str(sender_edi_qual))
logger.debug('\t Sender EDI ID Number: ' + str(sender_edi_id))
logger.debug('\tReceiver EDI Qualifier: ' + str(recvr_edi_qual))
logger.debug('\tReceiver EDI ID Number: ' + str(recvr_edi_id))
logger.debug('\t Transaction Date: ' + transaction_date.strftime('%m/%d/%Y'))
logger.debug('\t Transastion Control #: ' + str(transaction_no))
#########################BEGIN NEW LOOP#########################
logger.info('Opening database connection...')
session = loadSession()
orderHeaders = []
orderLines = []
po_997 = []
lineNum = 1
po_number = 0
for x in range(2, (len(lines) - 1)):
rec = lines[x].split(DELIMITER)
if not(rec[0] in ['H', 'D']):
logger.error('Encountered a non-header/detail record in body of transmission. Record: ' + str(x))
logger.error(lines[x])
raise ValueError('Record ' + str(x) + ' appears to be corrupt!')
if(rec[0] == 'H'):
#Parse Header Record
logger.info('Parsing Purchase Order Header...')
lineNum = 1
po_type = rec[1].strip()
vendor_id = int(rec[2])
po_number = int(rec[3])
po_997.append([transaction_no, rec[3].zfill(5)])
po_date = datetime.datetime.strptime(rec[4], "%Y%m%d")
bill_to_name = rec[5].strip()
bill_to_addr1 = rec[6].strip()
bill_to_addr2 = rec[7].strip()
bill_to_city = rec[8].strip()
bill_to_state = rec[9].strip()
bill_to_zip = rec[10].strip()
ship_to_name = rec[11].strip()
ship_to_addr1 = rec[12].strip()
ship_to_addr2 = rec[13].strip()
ship_to_city = rec[14].strip()
ship_to_state = rec[15].strip()
ship_to_zip = rec[16].strip()
ship_via = rec[17].strip()
pay_terms = rec[18].strip()
fob = rec[19].strip()
tax_exempt_no = rec[20].strip()
vendor_message = rec[21].strip() #Not used apparently
order_message = rec[22].strip()
express_order = rec[23].strip()
req_ship_date = datetime.datetime.strptime(rec[24], "%Y%m%d")
logger.debug('\t PO Type: ' + po_type)
logger.debug('\t Vendor ID: ' + str(vendor_id))
logger.debug('\t PO Number: ' + str(po_number))
logger.debug('\t PO Date: ' + po_date.strftime('%m/%d/%Y'))
logger.debug('\t Bill To Name: ' + bill_to_name)
logger.debug('\t Bill To Address 1: ' + bill_to_addr1)
logger.debug('\t Bill To Address 2: ' + bill_to_addr2)
logger.debug('\t Bill To City: ' + bill_to_city)
logger.debug('\t Bill To State: ' + bill_to_state)
logger.debug('\t Bill To Zip: ' + bill_to_zip)
logger.debug('\t Ship To Name: ' + ship_to_name)
logger.debug('\t Ship To Address 1: ' + ship_to_addr1)
logger.debug('\t Ship To Address 2: ' + ship_to_addr2)
logger.debug('\t Ship To City: ' + ship_to_city)
logger.debug('\t Ship To State: ' + ship_to_state)
logger.debug('\t Ship To Zip: ' + ship_to_zip)
logger.debug('\t Freight Carrier: ' + ship_via)
logger.debug('\t Payment Terms: ' + pay_terms)
logger.debug('\t FOB Point: ' + fob)
logger.debug('\t Tax Exempt Number: ' + tax_exempt_no)
logger.debug('\t Vendor Message: ' + vendor_message)
logger.debug('\t Order Message: ' + order_message)
logger.debug('\t Express Order Code: ' + express_order)
logger.debug('\t Requested Ship Date: ' + req_ship_date.strftime('%m/%d/%Y'))
#sunburyCust = getSunburyCustNum(vendor_id, ship_to_addr1, ship_to_city)
sunburyCust = getSunburyCustNum2(session, vendor_id, ship_to_addr1, ship_to_city)
#logger.info('Using sold-to account number: ' + str(sunburyCust))
logger.info('Using sold-to account number: ' + sunburyCust[:4])
logger.info('Using ship-to account number: ' + sunburyCust)
logger.info('PO Number: ' + str(getSunburyPONum(po_number, int(sunburyCust))))
#Create Object to be persisted to database
header = OrderHeader()
#Set up constant values in order header record
header.UBNR = '001' #Business Unit
header.UPLT = '00' #Plant #
header.ULOAD = 'N' #Flag to allow order to be processed in Jomar EDI workbench
header.UBLA = 'SO' #Jomar order type "SO" = Sales Order
header.UDAT = int(transaction_date.strftime('%Y%m%d')) #Transaction Date -> Order Date
header.FOB = ' ' #This customer doesn't transmit this information
header.UIHR = getSunburyPONum(po_number, int(sunburyCust)) #Will we continue to calculate the PO numbers in this way?
header.UIHS = int(po_date.strftime('%Y%m%d')) #PO date
#header.UKDN = (sunburyCust + 1) #Ship to customer #
header.UKDN = sunburyCust #Ship to customer #
header.UKDR = str(sunburyCust)[:4] #Bill to customer #
header.XTX5 = pay_terms #Payment terms
header.XTX6 = order_message #Jomar "notes" field; sticking order message here
header.URLRV = '0' #An ID column of sorts, in case the same PO is transmitted twice
header.UNA2 = ship_to_name
header.UNA3 = ship_to_addr1
header.UNA4 = ship_to_addr2
header.UORT = ship_to_city
header.USTA = ship_to_state[:2]
header.ULND = 'US'
header.UPLZ = ship_to_zip
if(po_type == 'ORIGINAL'):
header.FLX01 = '00'
orderHeaders.append(header)
elif(rec[0] == 'D'):
logger.info('Parsing PO Line Record...')
vendor_pattern = rec[1].strip()
vendor_color = rec[2]
vendor_finish = rec[3].strip()
prod_desc = rec[4].strip()
qty_ordered = float(rec[5])
unit_of_measure = rec[6].strip()
price_per_unit = float(rec[7])
comment_1 = rec[8].strip()
comment_2 = rec[9].strip()
comment_3 = rec[10].strip()
comment_4 = rec[11].strip()
comment_5 = rec[12].strip()
logger.debug('\t STM Pattern: ' + vendor_pattern)
logger.debug('\t STM Color: ' + str(vendor_color))
logger.debug('\t STM Finish: ' + vendor_finish)
logger.debug('\t Product Description: ' + prod_desc)
logger.debug('\t Quantity Ordered: ' + str(qty_ordered))
logger.debug('\t Units Priced By: ' + unit_of_measure)
logger.debug('\t Price per Unit: ' + str(price_per_unit))
logger.debug('\t Comment 1: ' + comment_1)
logger.debug('\t Comment 2: ' + comment_2)
logger.debug('\t Comment 3: ' + comment_3)
logger.debug('\t Comment 4: ' + comment_4)
logger.debug('\t Comment 5: ' + comment_5)
#Look up internal item number
i = prod_desc.split('-')[1]
s = lookUpSunburyItem(session, vendor_pattern, str(vendor_color), sunburyCust)
internal_item = s
#logger.info('Sunbury item number for ' + i + ' = ' + str(s))
oline = OrderDetail()
oline.VBNR = '001'
oline.VPLT = '00'
oline.VQUALT = '01'
oline.VUOM = 'YDS'
oline.VBLA = 'SO'
oline.VLFN = lineNum
oline.VMGS = qty_ordered
oline.VPR2 = price_per_unit
oline.VANR = internal_item
oline.VKDR = str(sunburyCust)[:4]
#oline.VKDL = (sunburyCust + 1)
oline.VKDL = sunburyCust
oline.VIHR = getSunburyPONum(po_number, int(sunburyCust))
oline.VFLX01 = vendor_pattern
oline.VFLX02 = vendor_color
oline.VRLRV = '0'
lineNum = lineNum + 1
orderLines.append(oline)
#Check for duplicate PO(s) already in database; increment URLRV/VRLRV accordingly if so
for h in orderHeaders:
dups = session.query(OrderHeader).filter(OrderHeader.UBNR == '001', OrderHeader.UPLT == '00', OrderHeader.UKDR == h.UKDR, OrderHeader.UIHR == h.UIHR).count()
if(dups > 0):
logger.warning('PO ' + str(h.UIHR) + ' already in 850 tables. Adding this anyway with sequence number ' + str(dups) + ' it may be deleted in Jomar if not needed.')
h.URLRV = str(dups)
for line in orderLines:
if(line.VIHR == h.UIHR):
line.VRLRV = str(dups)
#submit
logger.info('Writing records to database...')
for h in orderHeaders:
session.add(h)
for line in orderLines:
session.add(line)
for po in po_997:
c = mg_850_997()
c.trans_no,c.po = po
c.proc_date = time.strftime('%Y%m%d')
session.add(c)
if not(TEST_MODE):
session.commit()
else:
logger.warn('**RUNNING IN TEST MODE ROLLING BACK TRANSACTION**')
session.rollback()
session.close()
if not(TEST_MODE):
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=[inFile], message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not(TEST_MODE):
logger.info('Moving ' + inFile + ' to D:\\FTP\\OLD\\MG\\')
os.rename(inFile, 'D:\\FTP\\OLD\\MG\\' + inFile.split('\\')[-1])
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
processMomentumPo(sys.argv[1])
+252
View File
@@ -0,0 +1,252 @@
'''
Created on May 12, 2015
NOTE: A _packing list_ triggers a new header record in this transmission not a shipment. There can
and often will be more header records in a transmission file than there were shipments.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime, logging, os, sys, pyodbc, StringIO
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Shipment Notices Transmitted to Momentum.'
MESSAGE = None
TEST_MODE = False
LOG_LEVEL = logging.DEBUG
ACC_NUMBERS = ['3398', '4318', '5230']
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
class CSPASN00(Base):
__tablename__ = 'CSPASN00'
__table_args__ = {'autoload':True}
class CSPASN01(Base):
__tablename__ = 'CSPASN01'
__table_args__ = {'autoload':True}
class CSPASN02(Base):
__tablename__ = 'CSPASN02'
__table_args__ = {'autoload':True}
class CSPASN03(Base):
__tablename__ = 'CSPASN03'
__table_args__ = {'autoload':True}
class CSPASN04(Base):
__tablename__ = 'CSPASN04'
__table_args__ = {'autoload':True}
class XREF(Base): #Customer Xref Table
__tablename__ = 'TXUYAT01'
__table_args__ = {'autoload':True}
class TXRYRT00(Base): #Roll/Tag Master (ticketfl)
__tablename__ = 'TXRYRT00'
__table_args__ = {'autoload':True}
class TXMYAT03(Base):
__tablename__ = 'TXMYAT03'
__table_args__ = {'autoload':True}
class TXRYPS00(Base):
__tablename__ = 'TXRYPS00'
__table_args__ = {'autoload':True}
class TransactionTable(Base):
__tablename__ = 'ftp_trans_numbers'
__table_args__ = {'autoload':True}
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def createMomentumASN():
global MESSAGE
initializeLogger('MOM_856_GENERATE')
session = loadSession()
shipments_to_flag_processed = []
lines = []
trans_no = getTransNumber()
logger.info('Querying CSPASN0[0,1] for unprocessed shipments...')
shipmentQ = session.query(CSPASN00,CSPASN01).order_by(CSPASN00.MSTBIL).filter(CSPASN00.CUSTNO.in_(ACC_NUMBERS)).filter(CSPASN00.ABNR == CSPASN01.ABNR, CSPASN00.APLT == CSPASN01.APLT, CSPASN00.CUSTNO == CSPASN01.CUSTNO, CSPASN00.MSTBIL == CSPASN01.MSTBIL)
for shipment in shipmentQ:
if(shipment[0].PROCSS == 'Y'):
continue
#lines = []
bGroup = shipment[0].ABNR
plant = shipment[0].APLT
cust = shipment[0].CUSTNO
shipmentNumber = shipment[0].MSTBIL
shipmentDate = datetime.datetime.strptime(str(shipment[1].SHPDTE), "%Y%m%d")
logger.info('Processing shipment number: ' + shipmentNumber)
logger.info(' Shipped on: ' + shipmentDate.strftime('%m/%d/%Y'))
shipments_to_flag_processed.append(shipmentNumber)
logger.info('Querying CSPASN02 for packing lists on this shipment...')
packingListQ = session.query(CSPASN02).order_by(CSPASN02.BOLNUM).filter(CSPASN02.ABNR == bGroup, CSPASN02.APLT == plant, CSPASN02.CUSTNO == cust, CSPASN02.MSTBIL == shipmentNumber)
for packingList in packingListQ:
plNumber = packingList.BOLNUM
poNumber = packingList.PONUM
stmOrder = packingList.ORDNUM
#Build Header Record of transmission
hRec = 'H~%s~%s~%s~%s~%s~%s~%s\n' % (plNumber, '', '', shipmentDate.strftime('%m/%d/%Y'), '', poNumber, '')
lines.append(hRec)
detailQ = session.query(CSPASN03,CSPASN04,TXRYPS00,XREF,TXRYRT00).order_by(CSPASN04.LINE).filter(CSPASN03.ABNR == CSPASN04.ABNR, CSPASN03.APLT == CSPASN04.APLT, CSPASN03.CUSTNO == CSPASN04.CUSTNO,
CSPASN03.MSTBIL == CSPASN04.MSTBIL, CSPASN03.BOLNUM == CSPASN04.BOLNUM, CSPASN03.CTN == CSPASN04.CTN).filter(CSPASN03.ABNR == bGroup, CSPASN03.APLT == plant,
CSPASN03.CUSTNO == cust, CSPASN03.MSTBIL == shipmentNumber, CSPASN03.BOLNUM == plNumber).filter(CSPASN04.ABNR == TXRYPS00.PSBNR,
CSPASN04.APLT == TXRYPS00.PSPLT, CSPASN04.CUSTNO == TXRYPS00.PSKDN, CSPASN04.SB_PATTERNAME == TXRYPS00.PSPRT,
CSPASN04.SB_COLORNUM == TXRYPS00.PSANR).filter(CSPASN04.ABNR == XREF.UACBNR, CSPASN04.APLT == XREF.UACPLT, CSPASN04.CUSTNO == XREF.UACCNR,
CSPASN04.ITEM == XREF.UACANR).filter(CSPASN04.ABNR == TXRYRT00.RTBNR, CSPASN04.APLT == TXRYRT00.RTPLT, CSPASN04.ITEM == TXRYRT00.RTANR,
CSPASN04.ROLNUM == TXRYRT00.RTRNR)
detailCount = detailQ.count()
for detail in detailQ:
poLine = detail[1].LINE
momStyle = detail[3].UACPAT
momColor = detail[3].UACCOL
momProductNum = detail[2].PSFD01[:8] #Momentum product numbers are always 8 digits, it seems like we've left padded them with too many zeros in our DB
ydsShipped = "{0:.3f}".format(detail[1].SQTY)
weight = "{0:.1f}".format(detail[4].RTSWG)
pieceNum = detail[1].ROLNUM
ourPattern = detail[1].SB_PATTERNAME.strip()
ourColor = detail[1].SB_COLORNUM.strip()
dRec = 'D~%s~%s~%s~%s~%s~%s~%s~%s~%s~%s\n' % (momProductNum, momStyle, momColor, '', ydsShipped, 'YD', pieceNum, '', weight, 'LB')
# @todo: There is a bug in the nightmare of a query above that was causing duplicate rows
# to be created in the tranmission for seemingly random rolls. In every case I've found
# the lines that make it into the transmission are identical. So the below 'fix' is good
# enough to get us off the ground for the time being.
#
# 07/11/2016 - I believe this is fixed now by adding "CSPASN03.CTN == CSPASN04.CTN" to the first
# .filter() clause for the 03 <-> 04 join. I discovered this while working on
# Maharam's 856 document. Need to verify...
if not lines[-1].split('~')[7] == pieceNum:
lines.append(dRec)
else:
logger.warn('Query still messed up...')
tRec = 'T~' + str(detailCount) + "\n"
lines.append(tRec)
sRec = 'S~' + str(len(lines)) #+ "\n"
lines.append(sRec)
if(len(shipments_to_flag_processed) > 0):
#Write transmission data out to file here
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_856_' + str(trans_no) + '.txt')
f = open('D:\\FTP\\SEND_MG\\SBR_856_' + str(trans_no) + '.txt', 'w')
f.writelines(lines)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
if not TEST_MODE:
#Mark shipments as processed
ex = update(CSPASN00.__table__).where(CSPASN00.MSTBIL.in_(shipments_to_flag_processed)).values(PROCSS=u"Y")
session.execute(ex)
ex = None
#Might as well keep the table in the new database up to date
ex2 = update(TransactionTable.__table__).where(TransactionTable.client.in_(['MOMTEX'])).values(xaction=(trans_no))
session.execute(ex2)
ex2 = None
logger.debug('Comitting changes to database...')
session.commit()
updateTransNumber(trans_no) #Update transaction number in AS400
if(len(shipments_to_flag_processed) == 0):
logger.info('Nothing to send!')
logger.debug('Closing database connection...')
session.close()
MESSAGE.truncate(0)
'''
00 - List of shipments
01 - Contains shipment date for shipments in 01
02 - List of packing slips on each shipment
'''
if __name__ == '__main__':
createMomentumASN()
+194
View File
@@ -0,0 +1,194 @@
'''
Created on Dec 13, 2016
To generate daily 997 document for Momentum.
This particular 997 is confirming all POs received by us for the day.
I created a table called mg_850_997 in the database to support this script.
This table is filled by mg_850.py as it processes purchase orders from Momentum.
@author: Wes
'''
from alert import send_alert_html
from glob import glob
import datetime
import sys
import os, re, StringIO
import logging
import pyodbc
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
RECIPIENTS = ['admin@example.com']
SUBJECT = '997 Document Sent to Momentum'
MESSAGE = None
TEST_MODE = False
logger = None
LOG_LEVEL = logging.DEBUG
DELIMITER = '~'
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
########################################################################
#This table lists POs by transaction number and date
#mg_850.py is fills this table as it processes POs
class mg_850_997(Base):
__tablename__ = 'mg_850_997'
__table_args__ = {'autoload':True}
########################################################################
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
Gotta get transaction number from AS400 until ALL transmissions
are handled on the new system... whoopee
'''
def getTransNumber():
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'])
c1 = connection.cursor()
ret = -1
c1.execute('select * from stmdata.MGTX#FL')
for row in c1:
ret = int(row[1]) + 1
c1.close()
return ret
'''
Update the Momentum transaction number in the AS400 database
'''
def updateTransNumber(txnum):
connection = pyodbc.connect(
driver='{iSeries Access ODBC Driver}',
system='SUNBURY',
uid=os.environ['AS400_UID'],
pwd=os.environ['AS400_PWD'],
autocommit=True)
c1 = connection.cursor()
c1.execute('UPDATE STMDATA.MGTX#FL SET MGSEQ# = ' + str(txnum))
c1.close()
def generateMomentum997(date):
global logger
os.chdir('D:\FTP')
initializeLogger('mg_850_interface')
# @TODO: Properly handle sequence numbers eventually
logger.info('Grabbing next outgoing transaction number from the AS400...')
trans_no = getTransNumber()
logger.info('Opening database connection...')
session = loadSession()
'''
Each element of purchase orders will be structred like this:
[ 15600, ['21495','21540'] ]
In this example 15600 is the incoming transaction number we are confirming.
21495 and 21540 are the purchase orders that were transmitted in that transaction
'''
purchase_orders = []
level = -1
pos = []
q = session.query(mg_850_997).order_by(mg_850_997.trans_no).filter(mg_850_997.proc_date == date)
for row in q:
#First interation special case
if level == -1:
level = row.trans_no
logger.info(' Processing Transaction: %s', level)
#Check if transaction number changed since last record
if not (level == row.trans_no):
logger.info('Purchase orders on this transaction: %s', pos)
purchase_orders.append([level, pos]) #Write this transaction data to list
#Reset loop vars
pos = []
level = row.trans_no
logger.info(' Processing Transaction: %s', level)
pos.append(row.po)
#Write final transcation data to list
logger.info('Purchase orders on this transaction: %s', pos)
purchase_orders.append([level, pos])
logger.debug('Closing database connection...')
session.close()
#Write out to file here
lines_997 = []
lines_997.append('P~\n')
lines_997.append('TP~12~5702863800~12~9498338886~%s~%s~\n' % (date, trans_no))
for t in purchase_orders:
lines_997.append('H~%s~%s~\n' % (t[0], date))
for p in t[1]:
lines_997.append('D~%s~\n' % (p.zfill(5)))
lines_997.append('S~%s~\n' % (len(lines_997)))
#IF testing just dump transmission lines to console
if(TEST_MODE):
for line in lines_997:
print line
if not(TEST_MODE):
logger.info('Creating D:\\FTP\\SEND_MG\\SBR_997_%s.txt', trans_no)
f = open('SEND_MG\\SBR_997_' + str(trans_no) + '.txt', 'w')
f.writelines(lines_997)
f.close()
try:
logger.info('Sending e-mail alert...')
send_alert_html(RECIPIENTS, SUBJECT, attachments=glob('D:\\FTP\\SEND_MG\\SBR_997_*.txt'), message=MESSAGE.getvalue())
except:
logger.error('Unable to send e-mail. Is the internet out?')
updateTransNumber(trans_no) #Update transaction number in AS400
if __name__ == '__main__':
if(len(sys.argv) < 2):
pass
else:
generateMomentum997(sys.argv[1])
+258
View File
@@ -0,0 +1,258 @@
'''
Created on May 25th, 2016
@author: Wes
'''
import schedule, time, threading, functools, glob, ctypes, sys
sys.dont_write_bytecode = True
from download_transmissions import maharamOrderDownload, maharamInvoiceUpload, maharamASNUpload
from download_transmissions import momentumOrderDownload, momentumInvoiceUpload, momentumASNUpload
from download_transmissions import kravetOrderDownload
from download_transmissions import glenravenASNDownload
from download_transmissions import UNIFI_846_Download
from kf_850 import processKravetPo
from mg_850 import processMomentumPo
from mg_810 import createMomentumInvoiceDocument
from mg_856 import createMomentumASN
from mf_850 import processMaharamPo
from mf_810 import createMaharamInvoiceDocuments
from mf_856 import createMaharamShipNotices
from gr_856_in import processInboundGRASN
from dt_cut import dtxInvoiceAlert
from FGI_report import run_FGI_report
#Function decorator to catch exceptions instead of halting scheduler job.
def catch_exceptions(job_func):
@functools.wraps(job_func)
def wrapper(*args, **kwargs):
try:
job_func(*args, **kwargs)
except:
import traceback
print(traceback.format_exc())
update_title()
return wrapper
@catch_exceptions
def mf_850():
print ('Submitting job: maharamOrderDownload()')
maharamOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\MAH_850_*.txt')) > 0:
print 'Processing Maharam POs...'
for name in reversed(glob.glob('D:\\FTP\\MAH_850_*.txt')):
processMaharamPo(name)
print ''
update_title()
@catch_exceptions
def mf_810():
print 'Submitting job: createMaharamInvoiceDocuments()'
createMaharamInvoiceDocuments()
print ''
if len(glob.glob('D:\\FTP\\SEND_MF\\STM_INVOICE_*.xml')) > 0:
print 'Uploading invoice documents to Maharam FTP server...'
maharamInvoiceUpload()
print ''
update_title()
@catch_exceptions
def mf_856():
print 'Submitting job: createMaharamShipNotices()'
createMaharamShipNotices()
print ''
if len(glob.glob('D:\\FTP\\SEND_MF\\STM_856_*.txt')) > 0:
print 'Uploading shipment notices to Maharam FTP server...'
maharamASNUpload()
print ''
update_title()
@catch_exceptions
def mg_850():
print ('Submitting job: momentumOrderDownload()')
momentumOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\SBR_850_*.txt')) > 0:
print 'Processing Momentum POs...'
for name in reversed(glob.glob('D:\\FTP\\SBR_850_*.txt')):
processMomentumPo(name)
print ''
update_title()
@catch_exceptions
def mg_810():
print 'Submitting job: createMomentumInvoiceDocument()'
createMomentumInvoiceDocument()
print ''
if len(glob.glob('D:\\FTP\\SEND_MG\\SBR_810_*.txt')) > 0:
print 'Uploading invoice documents to Momentum FTP server...'
momentumInvoiceUpload()
print ''
update_title()
@catch_exceptions
def mg_856():
print 'Submitting job: createMomentumASN()'
createMomentumASN()
print ''
if len(glob.glob('D:\\FTP\\SEND_MG\\SBR_856_*.txt')) > 0:
print 'Uploading shipment notices to Momentum FTP server...'
momentumASNUpload()
print ''
update_title()
@catch_exceptions
def kf_850():
print ('Submitting job: karvetOrderDownload()')
kravetOrderDownload()
print ''
if len(glob.glob('D:\\FTP\\Kravet_to_vendor*.csv')) > 0:
print 'Processing Kravet POs...'
for name in reversed(glob.glob('D:\\FTP\\Kravet_to_vendor*.csv')):
processKravetPo(name)
print ''
update_title()
@catch_exceptions
def gr_856_in():
print ('Submitting job: glenravenASNDownload()')
glenravenASNDownload()
print ''
#Add processing bit here later
if len(glob.glob('D:\\FTP\\INCOMING\\GR_ASN_*.csv')) > 0:
print 'Processing Glen Raven Ship Notices...'
for name in reversed(glob.glob('D:\\FTP\\INCOMING\\GR_ASN_*.csv')):
print name
#processInboundGRASN(name)
update_title()
@catch_exceptions
def un_846_in():
print ('Submitting job: UNIFI_846_Download()')
UNIFI_846_Download()
print ''
#Add processing bit here later
update_title()
@catch_exceptions
def dtx_cut_alert():
print ('Submitting job: dtxInvoiceAlert()')
dtxInvoiceAlert()
print ''
update_title()
@catch_exceptions
def FGI_report():
print ('Submitting job: run_FGI_report()')
run_FGI_report()
print ''
update_title()
'''
Hacked this together to so I can add the name of the next job and when it will run to the
console title.
'''
def update_title():
message = 'Scheduler.py - Next Job: ' + min(schedule.jobs).job_func.args.__repr__().split(' ')[1] + ' @ ' + str(schedule.next_run())
ctypes.windll.kernel32.SetConsoleTitleA(message)
#Use to run a job in it's own thread
def run_threaded(job_func):
job_thread = threading.Thread(target=job_func)
job_thread.start()
'''
An attempt to clean up the schedule code below.
Schedules 'func' to run at 'time' Monday-Friday
'''
def weekdays(time, func):
schedule.every().monday.at(time).do(run_threaded, func)
schedule.every().tuesday.at(time).do(run_threaded, func)
schedule.every().wednesday.at(time).do(run_threaded, func)
schedule.every().thursday.at(time).do(run_threaded, func)
schedule.every().friday.at(time).do(run_threaded, func)
if __name__ == '__main__':
#Maharam PO Download
mf_850_runs = ['08:25','09:55','11:25','12:55','14:25','15:55','17:25']
for t in mf_850_runs:
weekdays(t, mf_850)
#Maharam Invoice Upload. This transmission was sent at 16:40 on the AS400
mf_810_runs = ['16:40']
for t in mf_810_runs:
weekdays(t, mf_810)
#Maharam ASN Upload
mf_856_runs = ['08:45','12:45','13:45','14:45','15:45','16:44','17:45']
for t in mf_856_runs:
weekdays(t, mf_856)
#Momentum PO Download
mg_850_runs = ['12:25','19:25']
for t in mg_850_runs:
weekdays(t, mg_850)
#Momentum Invoice Upload Begin. This transmission was sent at 16:45 on the AS400
mg_810_runs = ['16:45']
for t in mg_810_runs:
weekdays(t, mg_810)
#Momentum ASN Upload. This transmission was sent at 12:15 on the AS400
mg_856_runs = ['12:15','17:30']
for t in mg_856_runs:
weekdays(t, mg_856)
#Kravet PO Download
kf_850_runs = ['07:25']
for t in kf_850_runs:
weekdays(t, kf_850)
#Glen Raven 856 Download
gr_856_in_runs = ['08:00', '13:00']
for t in gr_856_in_runs:
weekdays(t, gr_856_in)
#UNIFI 846 Download
un_846_in_runs = ['08:01', '13:01']
for t in un_846_in_runs:
weekdays(t, un_846_in)
'''
** DISABLED 2017-10-02 **
We have ended the consignment program
#Designtex Cut into alert
dt_cut_runs = ['08:02', '10:00']
for t in dt_cut_runs:
weekdays(t, dtx_cut_alert)
'''
#FGI Report
schedule.every().saturday.at('23:59').do(run_threaded, FGI_report)
#schedule.every(10).seconds.do(run_threaded, derp) #threaded example
#Non-threaded examples
#schedule.every(10).minutes.do(job)
#schedule.every().hour.do(job)
#schedule.every().day.at("10:30").do(job)
#schedule.every().monday.do(job)
#schedule.every().wednesday.at("13:15").do(job)
print 'Job started. Do not close this window!'
print 'Ctrl-C to exit.'
update_title()
while True:
schedule.run_pending()
time.sleep(1)
+158
View File
@@ -0,0 +1,158 @@
'''
Test everything!
functions must begin with 'test'
'''
import unittest, logging, kf_850, mf_850
from download_transmissions import load_ftp_credentials
from kf_850 import massageSTM, massageKFI
from mf_850 import getSunburyCust, split_string_by_position, trim_elements
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
class Download_Transmissions_Tests(unittest.TestCase):
'''
Tests relating to download_tranmissions.py
'''
'''
load_ftp_credentials looks up FTP site credentials in a sqlite db that's not committed to the code repo
'''
def test_load_ftp_credentials(self):
codes = ['wf', 'kf', 'mf', 'mg', 'tsg', 'atx', 'htx', 'acf', 'stm']
for code in codes:
test = load_ftp_credentials(code)
self.assertEqual(len(test), 3) # Did we get a list back with _exactly_ 3 elements?
self.assertTrue(all(isinstance(n, basestring) for n in test)) # Are each of those elements strings?
class Kravet_Inbound_850_Tests(unittest.TestCase):
'''
Tests relating to kf_850.py
'''
'''
Kravet seems to hand key cross references in with a number of varying formats which are incorrect.
I've identified the most common and created massageSTM() to fix them
'''
def test_massageSTM(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
test_input = ['KIRSTEN 73383*2015', 'MULBERRY 73151*COL.825']
expected_output = ['KIRSTEN*2015', 'MULBERRY*0825']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(outt, massageSTM(inn))
'''
Right now this simply trims off the first 4 characters of the xref if 'GWF-' is present at the beginning of the string.
'''
def test_massageKFI(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
test_input = ['GWF-3421.516.0']
expected_output = ['3421.516.0']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(outt, massageKFI(inn))
def test_lookUpSunburyItem(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
kf_850.logger = logger
session = loadSession()
test_input = [ ['BLISS VELVET*2405','3194'], ['DUME*0601','3191'], ['PAISLEY WALK*6103','3190'] ]
expected_output = ['0000041463', '0000006576', '0000032327']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(kf_850.lookUpSunburyItem(session, inn[0], inn[1]), outt)
session.close()
class Maharam_Inbound_850_Tests(unittest.TestCase):
'''
Tests relating to mf_850.py
'''
addressList = ['45 RASONS CT', '74 HORSEBLOCK ROAD', '175 MUFFIN LANE']
cityList = ['HAUPPAUGE', 'YAPHANK', 'CUYAHOGA FALLS']
accountList = [343900, 3439, 343905]
def test_normal_order(self):
for addr, city, acc in zip(self.addressList, self.cityList, self.accountList):
message = "%s, %s should be account %s" % (addr, city, acc)
self.assertEqual(getSunburyCust('', addr, city), acc, msg=message)
'''
If order is custom contract hccb will be something other than an empty string
We should always get account 659000 for contract orders
'''
def test_contract_order(self):
for addr, city in zip(self.addressList, self.cityList):
self.assertEqual(getSunburyCust('Y', addr, city), 659000, msg='hccb = y but function did not return 659000')
self.assertNotEqual(getSunburyCust('', addr, city), 659000, msg='hccb = \'\' but function returned 659000')
'''
Unkown addresses (those not in the lists above) should resolve to 343999
'''
def test_unknown_address(self):
self.assertEqual(getSunburyCust('', 'Castle Black', 'The Wall'), 343999, msg="Castle Black, The Wall should be account 343999")
'''
'''
def test_lookUpSunburyItem(self):
#create dummy logger
logger = logging.getLogger('Unit_Testing')
logger.disabled = True
mf_850.logger = logger
session = loadSession()
test_input = [ ['464040','004','343904'], ['466166','007','343904'], ['466166','002','3439'] ]
expected_output = ['0000025195', '0000041949', '0000041944']
for inn, outt in zip(test_input, expected_output):
self.assertEqual(mf_850.lookUpSunburyItem(session, inn[0], inn[1], inn[2]), outt)
session.close()
'''
trim_elements takes a list as input and returns a list of the same length with any whitespace characters
removed from the beginning/end of each element.
'''
def test_trim_elements(self):
test_input = [' Pennywise The Dancing Clown ', 'Jon Snow ', 'Paul Atreides\n\r', ' \tBobby B.\t']
expected_output = ['Pennywise The Dancing Clown', 'Jon Snow', 'Paul Atreides', 'Bobby B.']
self.assertEqual(trim_elements(test_input), expected_output)
'''
Maharam is still transmitting fixed length data to us *yuck*
The function takes 1 string and a list of ints
the string is chopped up into len(ints) elements each element being ints[n] in length
'''
def test_split_string_by_position(self):
test_string = 'Pennywise The Dancing ClownJon SnowPaul AtreidesBobby B.'
lens = [27, 8, 13, 8]
expected_output = ['Pennywise The Dancing Clown', 'Jon Snow', 'Paul Atreides', 'Bobby B.']
self.assertEqual(split_string_by_position(test_string, lens), expected_output)
if __name__ == '__main__':
unittest.main()
+121
View File
@@ -0,0 +1,121 @@
'''
Created on Nov 30, 2016
UNIFI e-mails us "Pack and Hold" data daily. Interface this into Jomar
@author: Wes
'''
import datetime, logging, sys, os, StringIO, csv
from sqlalchemy import create_engine
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from operator import itemgetter
from itertools import groupby
RECIPIENTS = ['admin@example.com']
SUBJECT = 'Inbound UNIFI 846 Processed'
MESSAGE = None
TEST_MODE = True
GR_AP_ACCOUNT = '002528'
JOMAR_PO_ORDER_TYPE = 'JB'
logger = None
LOG_LEVEL = logging.DEBUG
UNIFI_PLANT = 0
PO = 1
LOT = 2
STM_COL = 3
WHSE_DATE = 4
QTY = 5
PACKAGES = 6
CASES = 7
engine = create_engine("mssql+pyodbc://jsisbprod", echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
#logger = logging.getLogger(__name__)
logger = logging.getLogger(name)
if len(logger.handlers) > 0:
return logger
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('D:\\FTP\\logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(name)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.DEBUG)
sh.setFormatter(formatter)
logger.addHandler(sh)
def process_UNIFI_846(inFile):
global MESSAGE
global session
os.chdir('D:\FTP')
initializeLogger('UNIFI_interface')
logger.info('****************************************')
fileName = inFile.split('\\')[-1]
rawLines = []
session = loadSession()
logger.info('Loading: %s', inFile)
try:
f = open(inFile, 'r')
except IOError:
logger.fatal('Unable to load file. Did you specify the full path? Exiting...')
return
filename = inFile.split('\\')[-1]
try:
reader = csv.reader(f)
for row in reader:
rawLines.append(row)
finally:
f.close()
rawLines.pop(0) #Remove first line as it is just column headers
if(len(rawLines) == 0):
logger.warning('They sent us an empty file...')
logger.info('Moving %s to D:\\FTP\\OLD\\UNIFI\\', inFile)
os.rename(inFile, 'OLD\\UNIFI\\' + fileName)
session.close()
return
rawLines.sort(key=itemgetter(PO)) #Ensure raw data is sorted by PO number
purchaseOrders = groupby(rawLines, itemgetter(PO)) #Create group by iterator based on PO number
#For each unique PO appearing in this transmission
for poNumber, lines in purchaseOrders:
print poNumber
MESSAGE.truncate(0)
if __name__ == '__main__':
if(len(sys.argv) < 2):
print 'Must supply fully qualified path to csv file ex:'
print 'python unifi_interface.py D:\\FTP\\INCOMING\\ipnet_07052017.file846.csv.010690400'
else:
process_UNIFI_846(sys.argv[1])
+483
View File
@@ -0,0 +1,483 @@
'''
Created on Jan 7, 2016
Generate assignment of invoices to be sent to Wells Fargo in csv format.
This is mainly called from the send_wf.py cgi script running on the Apache instance on our test server.
'WF' in comments = Wells Fargo
@todo: Properly truncate invoice amount value to fit WF's decimal(12,2) field size. TOTAL and DETAIL records!
@author: Wes
'''
import paramiko, csv, sys, logging, re, time, os, smtplib, sqlite3, StringIO, subprocess
from datetime import datetime
from sqlalchemy import create_engine, func, Table, text, Column
from sqlalchemy.types import VARCHAR
from sqlalchemy.ext.declarative import declarative_base
from sqlalchemy.orm import sessionmaker
from sqlalchemy.sql.expression import update
from alert import send_alert_html
TEST_MODE = False #When 'True' files will be uploaded to the 'testin' directory on the remote server
''' CLIENT_CODE = 5867 #Old Sunbury Textile Mills client code. Last Used: 09/29/2017 '''
CLIENT_CODE = 4143 #New code assigned to us by WF. Effective: 10/02/2017
REMOTE_HOST = 'sftp.spscommerce.net'
REMOTE_PORT = 10022
MESSAGE = None
VALID_TRADE_STYLES = ['888888', '88888800', '88888803', '88888805', '88888809']
'''
The only valid trade style numbers at the time this pattern was created are:
888888
88888800
88888803
88888805
88888809
'''
TRADE_STYLE_PATTERN = "888888(?P<tradeStyle>\d{0}$|0[0359]$)"
''' EXACTLY 10 digits '''
PHONE_NUM_PATTERN = "^\d{10}$"
#Moved connection string into sqlite db
for row in sqlite3.connect('D:\\FTP\\ftp.db').execute("SELECT * FROM connection_strings WHERE code = 'wfa'"):
engine = create_engine(row[1], echo=False)
Base = declarative_base(engine)
metadata = Base.metadata
logger = None
LOG_LEVEL = logging.DEBUG
TERMS_CODES = { '00': 'No Terms' }
assignmentNumber = 0
fileNames = []
backouts = ['9999999999']
#Customer Master
class CustomerMaster(Base):
__tablename__ = 'TXUYKD00'
__table_args__ = {'autoload':True}
#Tables and Codes
class TXMYTX00(Base):
__tablename__ = 'TXMYTX00'
__table_args__ = {'autoload':True}
#Invoice header table
class InvoiceHeader(Base):
__tablename__ = 'TXUYRE00'
__table_args__ = {'autoload':True}
#Invoice detail table
class InvoiceDetail(Base):
__tablename__ = 'TXUYRE01'
__table_args__ = {'autoload':True}
#Order Header
class OrderHeader(Base):
__tablename__ = 'TXUYUV00'
__table_args__ = {'autoload':True}
#Backouts
class Invoice_Backouts(Base):
__table__ = Table('Invoice_Backouts', Base.metadata,
Column('b_Invoice', VARCHAR, primary_key=True),
autoload=True, autoload_with=engine
)
'''
Storing FTP site information in a sqlite db now to keep it out of source code
'''
def load_ftp_credentials():
vault = sqlite3.connect('D:\\FTP\\ftp.db')
cursor = vault.execute("SELECT * from ftp_auth where code='wf'")
for row in cursor:
return row
'''
Upload files to WF's ftp site for processing.
Global var TEST_MODE dictates which remote dir the files are uploaded into.
files should contain a list of fully qualified file paths to be sent.
'''
def sendFiles(files):
return
# global logger
# global TEST_MODE
# if(TEST_MODE):
# remoteDir = '/testin/'
# else:
# remoteDir = '/in/'
#
# creds = load_ftp_credentials()
#
# logger.info('Opening SFTP connection to %s:%s',REMOTE_HOST,REMOTE_PORT)
# transport = paramiko.Transport((REMOTE_HOST, REMOTE_PORT))
# transport.connect(username = creds[3], password = creds[4])
# sftp = paramiko.SFTPClient.from_transport(transport)
#
# for f in files:
# path = remoteDir + f
# logger.info('sftp.put(\'%s\', \'%s\')', ('D:\\FTP\\SEND_WF\\' + f), path)
# sftp.put(('D:\\FTP\\SEND_WF\\' + f), path)
#
# logger.debug('DIR %s = %s', remoteDir, sftp.listdir(remoteDir[:-1]))
#
# logger.info('Closing SFTP connection.')
# sftp.close()
# transport.close()
'''
Opens connection to database returning a session object.
'''
def loadSession():
global engine
Session = sessionmaker(bind=engine)
session = Session()
return session
def initializeLogger(name='log'):
global logger
global MESSAGE
logger = logging.getLogger(__name__)
logger.setLevel(LOG_LEVEL)
handler = logging.FileHandler('logs\\' + name + '.log')
handler.setLevel(LOG_LEVEL)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
handler.setFormatter(formatter)
logger.addHandler(handler)
#Also send logs > DEBUG to stdout
ch = logging.StreamHandler(sys.stdout)
ch.setLevel(logging.INFO)
formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
ch.setFormatter(formatter)
logger.addHandler(ch)
#Capture log messages in a string for e-mail alert
MESSAGE = StringIO.StringIO()
sh = logging.StreamHandler(MESSAGE)
sh.setLevel(logging.INFO)
sh.setFormatter(formatter)
logger.addHandler(sh)
'''
The result of this function is a single assignment file.
An assignment containing all of the invoices (or credits) for a single tradestyle on a given date.
Invoices and Credits (assuming all is done through the ERP method in jomar) are stored in the same
table.
An 'Invoice' for the purposes of transmitting to wf is defined as any invoice in the table with a
_positive_ dollar amount.
A 'Credit' is similarly defined as any invoice with a _negative_ dolar amount.
There is a flag cloumn in the table that designates what type of invoice it is (SO, SR, etc.) however
Caleb has expressed that the +/- methodology works well.
Valid Tradestyles: 00, 03, 05, 09
These are mapped to these factor accounts in Jomar: 888888, 88888803, 88888805, 88888809
d - the target date in YYYmmdd format
tStyle - The associated factor account for the target tradestyle ex. 888888 for 00
creditMemo - True to process credits, False to process invoices
ex: createAssignment(20160520, '888888', False) # Generate 00 invoice assignment for 05/20/16
This method returns a list containing the invoice/credit numbers that were processed. This is
used by the main method to build a complete list of invoices to flag as processed if the CSV files
are sent to Wells Fargo. This prevents any invoices processed by this script from being "backed out"
after the fact.
'''
def createAssignment(d, tStyle, creditMemo):
global assignmentNumber
global fileNames
global backouts
invoices = []
#Check for invalid trade styles, The script ignores invalid trade styles and proceeds if it can.
test = re.match(TRADE_STYLE_PATTERN, tStyle, flags=0)
if(test == None):
if not(tStyle == ''):
logger.warning('Ignoring Invalid Trade Style: \'%s\'', tStyle)
return invoices
elif(test.group('tradeStyle') == ''):
tradeStyleCode = '01'
logger.debug('Jomar trade style = %s --> Wells Fargo trade style = %s', tStyle, tradeStyleCode)
else:
tradeStyleCode = test.group('tradeStyle')
logger.debug('Jomar trade style = %s --> Wells Fargo trade style = %s', tStyle, tradeStyleCode)
#Get number of invoices for this trade style
#logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\'', d, tStyle)
### GR PATCHES ###
if(creditMemo):
logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF < 0;', d, tStyle)
invoiceCount = session.query(InvoiceHeader.RRNR).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts)).count()
else:
logger.debug('SELECT COUNT(RRNR) FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF > 0;', d, tStyle)
invoiceCount = session.query(InvoiceHeader.RRNR).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts)).count()
### GR PATCHES ###
if(invoiceCount == 0):
return invoices
if(creditMemo):
logger.info('**************************************************')
logger.info('Total number of %s CREDITS: %s', tradeStyleCode, invoiceCount)
else:
logger.info('**************************************************')
logger.info('Total number of %s INVOICES: %s', tradeStyleCode, invoiceCount)
#Trade style is valid, increment assignment
assignmentNumber += 1
#Open file for writing
fname = str(CLIENT_CODE) + tradeStyleCode + '_INV_' + str(d) + '_' + str(assignmentNumber) + '.csv'
logger.debug('Creating %s. Using \'excel\' dialect for csv writer.', fname)
f = open('SEND_WF\\' + fname, 'wb')
writer = csv.writer(f, dialect='excel')
fileNames.append(fname)
#Get total dollar amount of all invoices for this trade style
invoiceTotal = 0
#logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\'', d, tStyle)
### GR PATCHES ###
if(creditMemo):
logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF < 0;', d, tStyle)
for row in session.query(func.sum(InvoiceHeader.RWTS).label("sum")).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts)):
invoiceTotal = row.sum
else:
logger.debug('SELECT SUM(RWTS) sum FROM TXUYRE00 WHERE RDATLS = \'%s\' AND RFCT LIKE \'%s\' AND RWTF > 0;', d, tStyle)
for row in session.query(func.sum(InvoiceHeader.RWTS).label("sum")).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts)):
invoiceTotal = row.sum
### GR PATCHES ###
if(creditMemo):
logger.info('Total dollar amount for %s CREDITS: %s', tradeStyleCode, '${:,.2f}'.format(invoiceTotal))
logger.info('**************************************************')
else:
logger.info('Total dollar amount for %s INVOICES: %s', tradeStyleCode, '${:,.2f}'.format(invoiceTotal))
logger.info('**************************************************')
if(creditMemo):
aType = 'C'
else:
aType = 'I'
writer.writerow(['TRL', CLIENT_CODE, tradeStyleCode, assignmentNumber, d, invoiceCount, aType, invoiceTotal])
#Loop over each invoice header record..
logger.info('<b>%s</b> \t <b>%s</b> \t <b>Amount</b>', 'Invoice'.ljust(10), 'Customer'.ljust(40))
### GR PATCHES ###
if(creditMemo):
q = session.query(InvoiceHeader).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF < 0, ~InvoiceHeader.RRNR.in_(backouts))
else:
q = session.query(InvoiceHeader).filter(InvoiceHeader.RDATLS == d, InvoiceHeader.RFCT.in_(VALID_TRADE_STYLES), InvoiceHeader.RWTF > 0, ~InvoiceHeader.RRNR.in_(backouts))
### GR PATCHES ###
for row in q:
logger.debug('Processing invoice %s', row.RRNR)
line = ['DTL', CLIENT_CODE, tradeStyleCode, assignmentNumber, d]
line.append(row.RRNR) #Invoice Number; DB length = 10, WF will accept 22
invoices.append(row.RRNR)
line.append(row.RDATRE)
line.append(row.RWTF) #Invoice amount; DB length = (15,5), WF will accept (12,2)
line.append('C') #Terms type, constant field determined based on docs from WF
line.append(row.RTXZ[2:]) #Our Terms Code; DB length = 5, WF will accept 15
if(len(TERMS_CODES[row.RTXZ[2:]]) > 30):
logger.warning('Terms description \'%s\' exceeds Wells Fargo character limit of 30! Sending \'%s\' instead.', TERMS_CODES[row.RTXZ[2:]], TERMS_CODES[row.RTXZ[2:]][:30])
line.append(TERMS_CODES[row.RTXZ[2:]][:30]) #Terms Description; DB length = 65, WF will accept 30
line.append(row.RKDR) #Bill to account number; DB length = 10, WF will accept 20
''' Grab customer contact info '''
logger.debug('SELECT * FROM TXUYKD00 WHERE TXUYKD00.KKDN = \'%s\'', row.RKDR)
q2 = session.query(CustomerMaster).filter(CustomerMaster.KKDN == row.RKDR)
if(q2.count() != 1):
logger.fatal('Failed to retrieve customer information from customer master table. Customer Number: %s Rows Returned: %s', row.RKDR, q2.count())
logger.fatal('**HALTING SCRIPT**')
logger.fatal('**NOTHING HAS BEEN SENT TO WELLS FARGO FOR %s**', d)
logger.debug('<--------------------------------------------------SCRIPT STOPPED---------------------------------------->')
sys.exit('Failed to retrieve customer information from customer master table.')
else:
for row2 in q2:
logger.info('%s \t %s \t %s', row.RRNR, row2.KNA2[:40].ljust(40), "{0:.2f}".format(row.RWTF).rjust(9))
if(len(row2.KNA2.replace(',',' ')) > 40):
logger.warning('Bill to account name \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA2.replace(',',' '), row2.KNA2.replace(',',' ')[:40])
line.append(row2.KNA2.replace(',',' ')[:40]) #Bill to account name; DB length = 65, WF will accept 40
if(len(row2.KNA3.replace(',',' ')) > 40):
logger.warning('Bill to address line \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA3.replace(',',' '), row2.KNA3.replace(',',' ')[:40])
line.append(row2.KNA3.replace(',',' ')[:40]) #Bill to addr1; DB length = 65, WF will accept 40
if(len(row2.KNA4.replace(',',' ')) > 40):
logger.warning('Bill to address line \'%s\' exceeds Wells Fargo character limit of 40! Sending \'%s\' instead.', row2.KNA4.replace(',',' '), row2.KNA4.replace(',',' ')[:40])
line.append(row2.KNA4.replace(',',' ')[:40]) #Bill to addr2; DB length = 65, WF will accept 40
line.append(row2.KORT.replace(',',' ')) #Bill to city; DB length = 28, WF will accept 40
line.append(row2.KSTA.replace(',',' ')) #Bill to state; DB length = 3, WF will accept 3
line.append(row2.KLND.replace(',',' ')) #Bill to country; DB length = 3, WF will accept 3
if(row2.KPLZ == '0'):
line.append('')
else:
line.append(row2.KPLZ.replace(',',' ')) #Bill to zip, DB length = 10, WF will accept 10
#Give it our best shot by stripping all non-digit characters first
p = re.sub('[\D\s]', '', row2.KTEL)
#Regex pattern tests if we have EXACTLY 10 DIGITS as required by Wells Fargo
test = re.match(PHONE_NUM_PATTERN, p, flags=0)
if(test == None):
line.append('') #Match failed, field is optional. Append empty string and move on
else:
line.append(p) #Bill to phone [optional]
#if(int(row.RVAL) == 0):
#line.append('') #Wells Fargo requires 8 characters if anything is to be sent at all. So we'll catch the 0 from the DB here and just insert an empty string
#else:
#line.append(row.RVAL) #As of date, dates in Jomar are already stored in the proper yyyymmdd format
line.append('') #Replaced
line.append('') #Intentionally left blank, "Store Number" column is N/A
''' Customer PO is optional as far as Wells-Fargo is concerned. We'll attempt to look it up '''
logger.debug('SELECT * FROM TXUYUV00 WHERE TXUYUV00.UBLN = \'%s\'', row.RBLN)
q2 = session.query(OrderHeader).filter(OrderHeader.UBLN == row.RBLN)
if(q2.count() != 1):
logger.error('Failed to retrieve customer PO number from order header table. Order Number on Invoice: %s Rows Returned: %s', row.RBLN, q2.count())
else:
for row2 in q2:
line.append(row2.UIHR.replace(',',' ')) #Customer PO; DB length = 15, WF will accept 15
writer.writerow(line)
logger.info('')
f.close()
return invoices
'''
For the purposes of running this via a web cgi interface I broke out the main logic of this script
into a 'main' function. See send_wf.py
iMode - a boolean, if True will prompt user for relevant info when running from a terminal
d - YYYYmmdd formatted date
sendNow - if True, the generated csv files will be sent immediatley to WF
email - if True the generated csv files are e-mailed to ar@example.com
'''
def main(iMode, d, sendNow, email=False):
global session
global backouts
global MESSAGE
os.chdir('D:\FTP')
initializeLogger('wf_assignment_build')
logger.debug('<--------------------------------------------------SCRIPT STARTED---------------------------------------->')
logger.info('Running for date: %s', d)
session = loadSession()
#DL terms table to local dict for speedy lookup
logger.debug('Downloading terms code list.')
logger.debug('SELECT * FROM TXMYTX00 WHERE TXMYTX00.AXANR LIKE \'#Z%\'')
q = session.query(TXMYTX00).filter(TXMYTX00.AXANR.like('#Z%'))
for row in q:
if(row.AXANR == '#Z'):
continue
else:
TERMS_CODES[row.AXANR[2:]] = row.AXTXT[:65].strip().replace(',',' ')
#Figure out which trade styles need assignments for given date
tradeStyles = []
### GR PATCHES ###
'''
logger.debug('SELECT DISTINCT RFCT FROM TXUYRE00 WHERE TXUYRE00.RDATLS = \'%s\'', d)
for value in session.query(InvoiceHeader.RFCT).filter(InvoiceHeader.RDATLS == d).distinct():
tradeStyles.append(value.RFCT)
'''
tradeStyles.append('888888')
### GR PATCHES ###
logger.info('Invoices/Credits where created on %s for these trade styles: %s', d, tradeStyles)
#Get list of backed out invoices to ignore
logger.debug('Fetching list of backed out invoices for %s', d)
filter_string = 'b_create_date = ' + str(d)
logger.debug('SELECT * FROM Invoice_Backouts WHERE %s;', filter_string)
q = session.query(Invoice_Backouts).filter(Invoice_Backouts.b_create_date == d)
for row in q:
if row.o_create_date == d: #Check that create date of original invoice is also today
logger.warn('Invoice %s was created and backed out with invoice %s on the same day. These will not be transmitted!', row.o_Invoice, row.b_Invoice)
backouts.append(row.o_Invoice)
backouts.append(row.b_Invoice)
#Create assignment for each trade style
processed_invoices = []
for t in tradeStyles:
processed_invoices += createAssignment(d, t, False)
processed_invoices += createAssignment(d, t, True)
if(iMode):
if(raw_input('Send assignments to Wells Fargo? (n): ').lower() == 'y'):
sendFiles(fileNames)
if not TEST_MODE:
logger.info('Marking %s Invoices/Credits as processed...', len(processed_invoices))
ex = update(InvoiceHeader.__table__).where(InvoiceHeader.RRNR.in_(processed_invoices)).values(RFIL=u"X")
session.execute(ex)
session.commit()
sts = subprocess.call("python D:\\AS400_Invoice_Interface.py " + d, shell=True)
else:
if(sendNow):
sendFiles(fileNames)
if not TEST_MODE:
logger.info('Marking %s Invoices/Credits as processed...', len(processed_invoices))
ex = update(InvoiceHeader.__table__).where(InvoiceHeader.RRNR.in_(processed_invoices)).values(RFIL=u"X")
session.execute(ex)
session.commit()
if(email):
try:
logger.info('E-mailing generated CSVs to ar@example.com.')
print '<b>THIS MAY TAKE A MOMENT **DO NOT** REFRESH YOUR BROWSER</b> :)'
attachment = []
for f in fileNames:
attachment.append('D:\\FTP\\SEND_WF\\' + f)
send_alert_html('ar@example.com', 'Wells Fargo Assignments', message=MESSAGE.getvalue(), attachments=attachment)
except:
logger.warn('Unable to e-mail CSV files. Is the internet down?')
logger.warn('The CSVs can be found in D:\\FTP\\SEND_WF on the test server')
logger.debug(sys.exc_info()[0])
print 'See the <a href="http://stmtest:81/cgi-bin/logs.py?logname=wf">log file</a> for extra details...'
session.close()
logger.debug('<--------------------------------------------------SCRIPT STOPPED---------------------------------------->')
MESSAGE.truncate(0)
if(sendNow):
sts = subprocess.call("python D:\\AS400_Invoice_Interface.py " + d, shell=True)
if __name__ == '__main__':
#main(False, 20161108, False, email=True)
#sys.exit()
a = len(sys.argv)
interactive_mode = False
if(a == 2):
if(len(sys.argv[1]) == 8): #grab date from sys.argv[1] and run
DATE = int(sys.argv[1])
else:
sys.exit('Invalid date supplied ' + sys.argv[1] + '. Must be in yyyymmdd format.')
else:
interactive_mode = True
prompt = time.strftime("Date (%Y%m%d): ")
DATE = raw_input(prompt)
if(len(DATE) == 0):
DATE = int(time.strftime("%Y%m%d"))
elif(len(DATE) == 8):
d = time.strptime(DATE, "%Y%m%d")
prompt = datetime.fromtimestamp(time.mktime(d)).strftime("Generate assignments for %A, %B %d %Y? (n): ")
confirm = raw_input(prompt)
if(confirm == 'y' or confirm == 'Y'):
DATE = int(DATE)
else:
sys.exit('User cancelled script.')
else:
sys.exit('Date format doesn\'t look right... Exiting.')
main(interactive_mode, DATE, True)
+600
View File
@@ -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<txnum>[\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', '<image{0}>'.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 <b>' + f + '</b> has not finished processing before <b>' + sys.argv[1] + '</b> 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()
+100
View File
@@ -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()
+64
View File
@@ -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**.
+16
View File
@@ -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
+4
View File
@@ -0,0 +1,4 @@
#Unregister-Event $changed.Id
Unregister-Event $created.Id
#Unregister-Event $deleted.Id
#Unregister-Event $renamed.Id
Binary file not shown.
+30
View File
@@ -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 ''