2024-03-20 09:42:04 -07:00
|
|
|
#!/usr/bin/env python3
|
|
|
|
"""
|
|
|
|
Consume a 846 file from 3PLs, and translate into a
|
2024-07-05 15:08:22 -07:00
|
|
|
inventory comparison report
|
2024-03-20 09:42:04 -07:00
|
|
|
For Shadex we also need to reply with a 997
|
2024-07-05 15:08:22 -07:00
|
|
|
|
2024-03-20 09:42:04 -07:00
|
|
|
"""
|
|
|
|
import datetime
|
|
|
|
import pathlib
|
|
|
|
import re
|
|
|
|
import shutil
|
|
|
|
import typing
|
|
|
|
import smtplib
|
|
|
|
import pprint
|
|
|
|
|
|
|
|
from email.mime.multipart import MIMEMultipart
|
2024-07-05 15:08:22 -07:00
|
|
|
from email.mime.application import MIMEApplication
|
2024-03-20 09:42:04 -07:00
|
|
|
from email.mime.text import MIMEText
|
|
|
|
|
|
|
|
import records # type: ignore
|
|
|
|
|
|
|
|
import yamamotoyama # type: ignore
|
|
|
|
import yamamotoyama.x3_imports # type: ignore
|
|
|
|
|
|
|
|
THIS_DIRECTORY = pathlib.Path(__file__).parent
|
|
|
|
X12_DIRECTORY = THIS_DIRECTORY / "incoming"
|
|
|
|
IMPORTS_DIRECTORY = THIS_DIRECTORY / "x3_imports"
|
|
|
|
EDI_997_DIRECTORY = THIS_DIRECTORY / "997_processing"
|
2024-07-05 15:08:22 -07:00
|
|
|
EDI_846_ATTACHMENTS = THIS_DIRECTORY / "846_reports"
|
|
|
|
EDI_846_ATTACHMENTS_ARCHIVE = EDI_846_ATTACHMENTS / "archive"
|
2024-03-20 09:42:04 -07:00
|
|
|
|
|
|
|
SHANDEX_846_FILENAME_RE = re.compile(
|
2024-07-05 15:08:22 -07:00
|
|
|
r"\A 846_STASH-YAMAMOTOYAMA_ \S+ [.]edi \Z", re.X | re.M | re.S
|
2024-03-20 09:42:04 -07:00
|
|
|
)
|
|
|
|
|
|
|
|
SHANDEX_STATUS = {
|
|
|
|
'33' : 'A',
|
|
|
|
'20' : 'R',
|
|
|
|
'QH' : 'Q'
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
def main():
|
|
|
|
"""
|
|
|
|
Do it!
|
|
|
|
"""
|
2024-07-05 15:08:22 -07:00
|
|
|
#read in the information from Shandex and store it
|
2024-03-20 09:42:04 -07:00
|
|
|
for edi_filename in X12_DIRECTORY.iterdir():
|
|
|
|
if SHANDEX_846_FILENAME_RE.match(edi_filename.name):
|
2024-12-12 09:41:34 -08:00
|
|
|
pprint.pprint(edi_filename.name)
|
2024-07-05 15:08:22 -07:00
|
|
|
shandex_inventory=process_file(edi_filename)
|
2024-03-20 09:42:04 -07:00
|
|
|
# file moved to 997 processing folder to be sent later
|
2024-08-20 09:43:11 -07:00
|
|
|
shutil.move(edi_filename, EDI_997_DIRECTORY / edi_filename.name)
|
2024-07-05 15:08:22 -07:00
|
|
|
#get stock information about WON and store it
|
|
|
|
#pass date from EDI so we can subtract newer stock movements?
|
|
|
|
x3_won_inventory=get_x3_won_inventory()
|
|
|
|
#write out an excel file with the stock from Shandex with X3 next to it, then include anything missing
|
|
|
|
compare_inventory(shandex_inventory, x3_won_inventory)
|
|
|
|
stock_count_alert()
|
|
|
|
|
|
|
|
|
|
|
|
def compare_inventory(shandex_inventory, x3_inventory):
|
|
|
|
today = datetime.datetime.today()
|
|
|
|
today = today.strftime('%Y-%m-%d')
|
|
|
|
with open(EDI_846_ATTACHMENTS / f'inventory_comparison_{today}.csv', 'w', newline='') as outfile:
|
|
|
|
outfile.write(','.join(['Site','Item','Description','Lot','X3 qty','Shandex qty']))#header
|
|
|
|
outfile.write('\n')
|
|
|
|
for record in x3_inventory:
|
|
|
|
site = record["STOFCY_0"]
|
|
|
|
item = record["ITMREF_0"]
|
|
|
|
des = record["ITMDES1_0"]
|
|
|
|
lot = record["LOT_0"]
|
|
|
|
qty = str(record["QTY"])
|
|
|
|
outfile.write(','.join([site, item, des, lot, qty]))
|
|
|
|
outfile.write(',')
|
|
|
|
if item in shandex_inventory:
|
|
|
|
if lot in shandex_inventory[item]:
|
|
|
|
outfile.write(str(shandex_inventory[item][lot]))
|
|
|
|
# pprint.pprint(shandex_inventory[item])
|
|
|
|
del shandex_inventory[item][lot]
|
|
|
|
# pprint.pprint(shandex_inventory[item])
|
|
|
|
if len(shandex_inventory[item]) == 0:
|
|
|
|
# pprint.pprint('entire del')
|
|
|
|
# pprint.pprint(shandex_inventory[item])
|
|
|
|
del shandex_inventory[item]
|
|
|
|
# pprint.pprint(shandex_inventory[item])
|
|
|
|
else:
|
|
|
|
outfile.write('0')#lot not found
|
|
|
|
else:
|
|
|
|
outfile.write('0')#item not found
|
|
|
|
outfile.write('\n')
|
|
|
|
#write the rest of shandex inventory
|
|
|
|
if len(shandex_inventory) > 0:
|
|
|
|
outfile.write('Shandex only')
|
|
|
|
outfile.write('\n')
|
|
|
|
for item in shandex_inventory:
|
|
|
|
# pprint.pprint(item)
|
|
|
|
for lot in shandex_inventory[item]:
|
|
|
|
# pprint.pprint(lot)
|
|
|
|
qty = str(shandex_inventory[item][lot])
|
|
|
|
outfile.write(','.join([item, lot, qty]))
|
|
|
|
outfile.write('\n')
|
2024-03-20 09:42:04 -07:00
|
|
|
|
|
|
|
|
|
|
|
def tokens_from_edi_file(
|
|
|
|
edi_filename: pathlib.Path,
|
|
|
|
) -> typing.Iterator[typing.List[str]]:
|
|
|
|
"""
|
|
|
|
Read tokens from EDI file
|
|
|
|
"""
|
|
|
|
with edi_filename.open(encoding="utf-8", newline="") as edi_file:
|
|
|
|
for record in edi_file.read().split("~"):
|
|
|
|
fields = record.split("*")
|
|
|
|
if fields[0] in {
|
|
|
|
"ISA",
|
|
|
|
"GS"
|
2024-07-05 15:08:22 -07:00
|
|
|
"ST",
|
|
|
|
"RED",
|
2024-03-20 09:42:04 -07:00
|
|
|
"GE",
|
|
|
|
"IEA"
|
|
|
|
}:
|
|
|
|
continue
|
|
|
|
yield fields
|
|
|
|
|
|
|
|
|
2024-07-05 15:08:22 -07:00
|
|
|
def get_x3_won_inventory():
|
|
|
|
#TODO correct the dates used in stock_issues?
|
|
|
|
with yamamotoyama.get_connection() as db_connection:
|
|
|
|
return db_connection.query(
|
|
|
|
"""
|
|
|
|
with stock_issues as (
|
|
|
|
select
|
|
|
|
STJ.STOFCY_0,
|
|
|
|
STJ.ITMREF_0,
|
|
|
|
STJ.LOT_0,
|
|
|
|
sum(STJ.QTYSTU_0) [QTYSTU_0]
|
|
|
|
from PROD.STOJOU STJ
|
|
|
|
where
|
|
|
|
STJ.STOFCY_0 = 'WON'
|
|
|
|
and STJ.IPTDAT_0 between getdate()-1 and getdate()
|
|
|
|
group by
|
|
|
|
STJ.STOFCY_0,
|
|
|
|
STJ.ITMREF_0,
|
|
|
|
STJ.LOT_0
|
|
|
|
)
|
2024-03-20 09:42:04 -07:00
|
|
|
select
|
2024-07-05 15:08:22 -07:00
|
|
|
SLF.STOFCY_0,
|
|
|
|
SLF.ITMREF_0,
|
|
|
|
ITM.ITMDES1_0,
|
|
|
|
SLF.LOT_0,
|
|
|
|
cast(sum(SLF.AAACUMQTY_0 + SLF.QQQCUMQTY_0 + SLF.RRRCUMQTY_0 - coalesce(stock_issues.QTYSTU_0,0)) as integer) as QTY,
|
|
|
|
SLF.AVC_0
|
|
|
|
from PROD.STOLOTFCY SLF
|
|
|
|
join PROD.ITMMASTER ITM on
|
|
|
|
SLF.ITMREF_0 = ITM.ITMREF_0
|
|
|
|
left join stock_issues
|
|
|
|
on SLF.ITMREF_0 = stock_issues.ITMREF_0
|
|
|
|
and SLF.STOFCY_0 = stock_issues.STOFCY_0
|
|
|
|
and SLF.LOT_0 = stock_issues.LOT_0
|
|
|
|
where
|
|
|
|
SLF.STOFCY_0 = 'WON'
|
|
|
|
group by
|
|
|
|
SLF.STOFCY_0,
|
|
|
|
SLF.ITMREF_0,
|
|
|
|
ITM.ITMDES1_0,
|
|
|
|
SLF.LOT_0,
|
|
|
|
SLF.AVC_0
|
|
|
|
order by 1, 2
|
|
|
|
""",
|
|
|
|
#startdate=edi_date,
|
|
|
|
).all()
|
2024-03-20 09:42:04 -07:00
|
|
|
|
2024-07-05 15:08:22 -07:00
|
|
|
def gtin_lookup(gtin):
|
|
|
|
with yamamotoyama.get_connection() as db_connection:
|
|
|
|
itmref = db_connection.query(
|
|
|
|
"""
|
|
|
|
select
|
|
|
|
[ITM].[ITMREF_0],
|
|
|
|
[ITM].[ITMDES1_0],
|
|
|
|
[ITM].[EANCOD_0],
|
|
|
|
[ITM].[ZCASEUPC_0]
|
|
|
|
from [PROD].[ITMMASTER] [ITM]
|
|
|
|
join [PROD].[ITMFACILIT] [ITF]
|
|
|
|
on [ITM].[ITMREF_0] = [ITF].[ITMREF_0]
|
|
|
|
and [ITF].[STOFCY_0] = 'WON'
|
|
|
|
where
|
|
|
|
replace([ITM].[ZCASEUPC_0],' ','') = :zcaseupc
|
|
|
|
""",
|
|
|
|
zcaseupc=gtin,
|
|
|
|
).first()
|
|
|
|
if itmref is None:
|
|
|
|
itmref = db_connection.query(
|
|
|
|
"""
|
|
|
|
select
|
|
|
|
[ITM].[ITMREF_0],
|
|
|
|
[ITM].[ITMDES1_0],
|
|
|
|
[ITM].[EANCOD_0],
|
|
|
|
[ITM].[ZCASEUPC_0]
|
|
|
|
from [PROD].[ITMMASTER] [ITM]
|
|
|
|
join [PROD].[ITMFACILIT] [ITF]
|
|
|
|
on [ITM].[ITMREF_0] = [ITF].[ITMREF_0]
|
|
|
|
and [ITF].[STOFCY_0] = 'WON'
|
|
|
|
where
|
|
|
|
replace([ITM].[EANCOD_0],' ','') = :zcaseupc
|
|
|
|
""",
|
|
|
|
zcaseupc=gtin,
|
|
|
|
).first()["ITMREF_0"]
|
|
|
|
else:
|
|
|
|
itmref = itmref["ITMREF_0"]
|
|
|
|
return itmref
|
|
|
|
|
|
|
|
|
|
|
|
def stock_count_alert():
|
2024-03-20 09:42:04 -07:00
|
|
|
msg = MIMEMultipart()
|
2024-07-05 15:08:22 -07:00
|
|
|
msg['Subject'] = 'New Stock Count from Shandex'
|
2024-03-20 09:42:04 -07:00
|
|
|
msg['Precedence'] = 'bulk'
|
|
|
|
msg['From'] = 'x3report@stashtea.com'
|
2024-08-20 09:43:11 -07:00
|
|
|
msg['To'] = 'isenn@yamamotoyama.com,vgomez@yamamotoyama.com'
|
|
|
|
msg['Cc'] = 'bleeson@stashtea.com'
|
2024-07-05 15:08:22 -07:00
|
|
|
emailtext = f'Attached.'
|
2024-03-20 09:42:04 -07:00
|
|
|
msg.attach(MIMEText(emailtext, 'plain'))
|
2024-07-05 15:08:22 -07:00
|
|
|
for file in EDI_846_ATTACHMENTS.iterdir():
|
|
|
|
if file.name.endswith('.csv'):
|
|
|
|
part = MIMEApplication(open(file, 'rb').read())
|
|
|
|
part['Content-Disposition'] = f'attachment; filename="{file.name}"'
|
|
|
|
msg.attach(part)
|
|
|
|
shutil.move(file, EDI_846_ATTACHMENTS_ARCHIVE / file.name)
|
2024-03-20 09:42:04 -07:00
|
|
|
with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp:
|
|
|
|
smtp.login(user='x3reportmk2@yamamotoyama.com', password=r'n</W<7fr"VD~\2&[pZc5')
|
|
|
|
smtp.send_message(msg)
|
|
|
|
|
|
|
|
|
|
|
|
def process_file(edi_filename: pathlib.Path):
|
|
|
|
"""
|
|
|
|
Convert a specific EDI file into an import file.
|
|
|
|
"""
|
2024-07-05 15:08:22 -07:00
|
|
|
shandex_inventory = {} #all inventory
|
|
|
|
product = ''
|
|
|
|
lot = ''
|
|
|
|
qty = 0
|
2024-03-20 09:42:04 -07:00
|
|
|
for fields in tokens_from_edi_file(edi_filename):
|
2024-07-05 15:08:22 -07:00
|
|
|
if fields[0] == "BIA":
|
|
|
|
advice_date = fields[4]
|
2024-12-12 09:41:34 -08:00
|
|
|
if fields[0] == 'LIN' and len(fields) > 5:
|
2024-07-05 15:08:22 -07:00
|
|
|
if product != '': #check loop entry
|
|
|
|
if product not in shandex_inventory: #if we haven't seen the product yet add it
|
|
|
|
# pprint.pprint('product was not found')
|
|
|
|
shandex_inventory[product] = {lot : qty}
|
|
|
|
# pprint.pprint(shandex_inventory)
|
|
|
|
else: #we've seen this product, have we seen the lot
|
|
|
|
if lot not in shandex_inventory[product]:#if not, add it
|
|
|
|
shandex_inventory[product][lot] = qty
|
|
|
|
else:
|
|
|
|
shandex_inventory[product][lot] += qty #if we have add to it
|
|
|
|
# pprint.pprint('product: ' + product)
|
|
|
|
# pprint.pprint('lot: ' + lot)
|
|
|
|
# pprint.pprint('qty: ' + str(qty))
|
|
|
|
#LIN**SK*077652972160*LT*31052026A
|
|
|
|
gtin = fields[3]
|
|
|
|
lot = fields[5]
|
2024-03-20 09:42:04 -07:00
|
|
|
product = gtin_lookup(gtin)
|
2024-07-05 15:08:22 -07:00
|
|
|
qty = 0
|
|
|
|
if fields[0] == "QTY":#product should already exist
|
|
|
|
# QTY*33*0
|
|
|
|
# QTY*20*16
|
|
|
|
# QTY*QH*0
|
|
|
|
qty += int(fields[2])
|
|
|
|
if fields[0] == "SE":#end of file
|
|
|
|
# pprint.pprint('final add')
|
|
|
|
# pprint.pprint(shandex_inventory)
|
|
|
|
# pprint.pprint('product: ' + product)
|
|
|
|
# pprint.pprint('lot: ' + lot)
|
|
|
|
# pprint.pprint('qty: ' + str(qty))
|
|
|
|
if product is not None: #check loop entry
|
|
|
|
if product not in shandex_inventory: #if we haven't seen the product yet add it
|
|
|
|
shandex_inventory[product] = {lot : qty}
|
|
|
|
else: #we've seen this product, have we seen the lot
|
|
|
|
if lot not in shandex_inventory[product]:#if not, add it
|
|
|
|
shandex_inventory[product][lot] = qty
|
|
|
|
else:
|
|
|
|
shandex_inventory[product][lot] += qty #if we have add to it
|
|
|
|
return shandex_inventory
|
2024-03-20 09:42:04 -07:00
|
|
|
|
|
|
|
|
|
|
|
if __name__ == "__main__":
|
|
|
|
main()
|