From 0a569a5b51103a760b215789abacd549e7b217fa Mon Sep 17 00:00:00 2001 From: bleeson Date: Tue, 20 Aug 2024 09:43:11 -0700 Subject: [PATCH] Reworked 867 process and minor updates to others. --- edi_846.py | 5 +- edi_867_to_table.py | 1077 +++++++++++++++++++++++++++++++++++ edi_943.py | 24 +- edi_947.py | 75 +-- edi_997_inbound.py | 4 +- master_contoller.py | 27 +- readme.txt | 23 +- update_shandex_dashboard.py | 53 +- 8 files changed, 1166 insertions(+), 122 deletions(-) create mode 100644 edi_867_to_table.py diff --git a/edi_846.py b/edi_846.py index 00eb74e..b5d7955 100644 --- a/edi_846.py +++ b/edi_846.py @@ -49,7 +49,7 @@ def main(): if SHANDEX_846_FILENAME_RE.match(edi_filename.name): shandex_inventory=process_file(edi_filename) # file moved to 997 processing folder to be sent later - #shutil.move(edi_filename, EDI_997_DIRECTORY / edi_filename.name) + shutil.move(edi_filename, EDI_997_DIRECTORY / edi_filename.name) #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() @@ -214,7 +214,8 @@ def stock_count_alert(): msg['Subject'] = 'New Stock Count from Shandex' msg['Precedence'] = 'bulk' msg['From'] = 'x3report@stashtea.com' - msg['To'] = 'bleeson@stashtea.com'#TODO correct receipientscares + msg['To'] = 'isenn@yamamotoyama.com,vgomez@yamamotoyama.com' + msg['Cc'] = 'bleeson@stashtea.com' emailtext = f'Attached.' msg.attach(MIMEText(emailtext, 'plain')) for file in EDI_846_ATTACHMENTS.iterdir(): diff --git a/edi_867_to_table.py b/edi_867_to_table.py new file mode 100644 index 0000000..fad8b5d --- /dev/null +++ b/edi_867_to_table.py @@ -0,0 +1,1077 @@ +#!/usr/bin/env python3 +""" +Consume a 867 file from Shandex, and translate into a Sage X3 +readable file-ZSHIP867. Works with "import_867s.py" + +New changes, need to bring in under ship to customer whenever possible. +Build a mapping file of known customers by matching their address to x3 codes +need to not import a shipment if a customer mapping doesn't exist. + +""" +# pylint: disable=too-many-instance-attributes +import dataclasses +import datetime +import decimal +import functools +import pathlib +import re +import shutil +import typing +import pprint +import smtplib +from email.mime.multipart import MIMEMultipart +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" + +SOURCE_867_FILENAME_RE = re.compile( + r"\A 867_STASH-YAMAMOTOYAMA_ .* [.]edi \Z", re.X | re.M | re.S +) + +INSERT_SHIPMENT = """\ + execute [staging].[dbo].[shandex_insert_shipment] + :PO, + :H, + :salfcy, + :stofcy, + :sdhnum, + :bpcinv, + :bpcord, + :bpaadd, + :cur, + :shidat, + :cfmflg, + :pjt, + :bptnum, + :ylicplate, + :yclippership, + :invdtaamt_2, + :invdtaamt_3, + :invdtaamt_4, + :invdtaamt_5, + :invdtaamt_6, + :invdtaamt_7, + :invdtaamt_8, + :invdtaamt_9, + :die, + :die_1, + :die_2, + :die_3, + :die_4, + :die_5, + :die_6, + :die_7, + :die_8, + :die_9, + :die_10, + :die_11, + :die_12, + :die_13, + :die_14, + :die_15, + :die_16, + :die_17, + :die_18, + :die_19, + :cce, + :cce_1, + :cce_2, + :cce_3, + :cce_4, + :cce_5, + :cce_6, + :cce_7, + :cce_8, + :cce_9, + :cce_10, + :cce_11, + :cce_12, + :cce_13, + :cce_14, + :cce_15, + :cce_16, + :cce_17, + :cce_18, + :cce_19, + :bpdnam, + :bpdaddlig, + :bpdaddlig_1, + :bpdaddlig_2, + :bpdposcod, + :bpdcty, + :bpdsat, + :bpdcry, + :bpdcrynam, + :sdhtyp, + :growei, + :pacnbr, + :star71, + :star72, + :star81, + :star82 + """ + +INSERT_SHIPMENT_DETAIL = """\ + execute [staging].[dbo].[shandex_insert_shipment_line] + :PO, + :L, + :sohnum, + :soplin, + :itmref, + :itmdes, + :sau, + :qty, + :gropri, + :star91, + :star92, + :S, + :sta, + :pcu, + :qtypcu, + :loc, + :lot, + :sernum + """ + +# Not needed, Shandex stores everything how they want so we need to look up in X3 +# UOM_MAPPING = { + # "CA" : "CS", + # "EC" : "EA" +#} +#NAME_ADDRESS_CITY_TERRITORY_POSTAL : X3 Customer Code +X3_CUSTOMER_MAPPING = { + 'AVRI1000_AVRIQC' : 'AVRI0001', + 'BULK1000_BULKAU' : 'BULK0001', + 'COOP2000_190148' : 'FEDE0006', + 'COOP2000_190149' : 'FEDE0005', + 'COOP2000_607S' : 'FEDE0003', + 'COOP2000_CAL' : 'FEDE0007', + 'HORI1000_HORIBC' : 'HORI0001', + 'LOND1000_190005' : 'LOND0001', + 'NATI1000_28' : 'LOBL0002', + 'NATI1000_34' : 'LOBL0006', + 'NATI1000_D022' : 'LOBL0001', + 'ONTA1100_ONTAON' : 'ONTA0002', + 'OVER1000_5111' : 'OVER0002', + 'OVER1000_A24' : 'OVER0004', + 'PARA1100_PARABC' : 'PARA0004', + 'PSCN1000_PSCBC' : 'PSCN0002', + 'PURE1000_PUREON' : 'PURE0004', + 'PURI1000_PURION' : 'PURI0002', + 'SATA1000_SATAQC' : 'SATA0002', + 'UNFI1000_UNFIBC' : 'UNFI0011', + 'UNFI1000_UNFION' : 'UNFI0004', + 'SAMP1000_0000' : 'YARI0001', + 'PURI1000_PURIQC' : 'PURI0005', + 'JIVA1000_JIVABC' : 'JIVA0002', + 'WELL1000_WELL' : 'WELL0002', + 'WELL1000_WELCAL' : 'WELL0003', + 'AMAZ1200_YYC4' : 'AMAZ0210', + 'AMAZ1200_YVR2' : 'AMAZ0014', + 'AMAZ1200_YYZ4' : 'AMAZ0049', + 'AMAZ1200_YXU1' : 'AMAZ0189', + 'AMAZ1200_YOW3' : 'AMAZ0176', + 'PARA1100_0000' : 'PARA0004', + 'AMAZ1200_YVR4' : 'AMAZ0099', + 'AMAZ1200_YHM1' : 'AMAZ0169', + 'AMAZ1200_YYZ7' :'AMAZ0100', + 'PURI1000_PURIBC' : 'PURI0003', + 'PURI1000_PURIAB' : 'PURI0004', + 'AMAZ1200_YEG2' : 'AMAZ0179', + 'HORI1000_0000' : 'HORI0001', + 'NATI1100_NATION' : 'NATI0004', + 'SOBE1000_SB0040' : 'SOBE0009', + 'SOBE1000_SB0050' : 'SOBE0008', + 'SOBE1000_SB0029' : 'SOBE0010', + 'SOBE1000_SB0024' : 'SOBE0011', + 'SOBE1000_SB0092' : 'SOBE0012', +} + +def main(): + """ + Do it! + """ + for edi_filename in X12_DIRECTORY.iterdir(): + if SOURCE_867_FILENAME_RE.match(edi_filename.name): + process_file(edi_filename) + shutil.copy(edi_filename, EDI_997_DIRECTORY / edi_filename.name) + shutil.move(edi_filename, THIS_DIRECTORY / "processed_867s" / edi_filename.name) #They go in here so we can use them in the dashboard script + + +def missing_customer_alert(customer_key): + msg = MIMEMultipart() + msg['Subject'] = 'Shandex 867 - Missing X3 Customer' + msg['Precedence'] = 'bulk' + msg['From'] = 'x3report@stashtea.com' + msg['To'] = 'technical-contact@stashtea.com' + emailtext = f'Missing value: {customer_key}' + msg.attach(MIMEText(emailtext, 'plain')) + with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: + smtp.login(user='x3reportmk2@yamamotoyama.com', password=r'n 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", + "ST", + "BPT", + }: + continue + yield fields + +def get_product_from_gtin(gtin): + #pprint.pprint(gtin) + with yamamotoyama.get_connection() as database: + result = database.query( + """ + select + [ITM].[ITMREF_0], + [ITM].[ITMDES1_0], + [ITM].[EANCOD_0], + [ITM].[ZCASEUPC_0], + [ITM].[STU_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 result is None: + result = database.query( + """ + select + [ITM].[ITMREF_0], + [ITM].[ITMDES1_0], + [ITM].[EANCOD_0], + [ITM].[ZCASEUPC_0], + [ITM].[STU_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() + return result + +def process_file(edi_filename: pathlib.Path): + """ + Convert a specific EDI file into an import file. + """ + shipping_date = '' + previous_picking_number = '' + po_number = '' + cust_po_number = '' + warehouse_shipment = WarehouseShipment() + for fields in tokens_from_edi_file(edi_filename): + if fields[0] == "DTM": + shipping_date = fields[2] + if fields[0] == "PTD" and len(fields) > 2:#There is one PTD in the header that is not used + picking_number = fields[5] + warehouse_shipment.header.ylicplate = f'{previous_picking_number}' + if po_number != '': + warehouse_shipment.header.yclippership = cust_po_number + warehouse_shipment.header.ylicplate = f'{po_number}' + if picking_number != previous_picking_number and previous_picking_number != '': + if warehouse_shipment.header.bpdnam != 'Shandex Group': + warehouse_shipment.header.shidat = datetime.datetime.strptime( + shipping_date, "%Y%m%d") + time_stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + if warehouse_shipment.header.bpcord == '': + missing_customer_alert(customer_key) + import_shipment(warehouse_shipment) + warehouse_shipment = WarehouseShipment() + po_number = '' + warehouse_shipment.header.ylicplate = f'{picking_number}' + previous_picking_number = picking_number + if fields[0] =='REF' and fields[1] == 'PO': + cust_po_number = fields[2] + if fields[0] =='REF' and fields[1] == 'IL': + po_number = fields[2] + if fields[0] == "N1" and fields[1] == 'ST': + ship_to_customer = fields[2] + shandex_code_part1 = fields[4] + warehouse_shipment.header.bpdnam = ship_to_customer + if fields[0] == "N1" and fields[1] == 'BY': + shandex_code_part2 = fields[4] + if fields[0] == "N3": + ship_to_address = fields[1] + warehouse_shipment.header.bpdaddlig = ship_to_address + if fields[0] == "N4": + ship_to_city = fields[1] + ship_to_province = fields[2] + ship_to_zip = fields[3] + warehouse_shipment.header.bpdposcod = ship_to_zip + warehouse_shipment.header.bpdcty = ship_to_city + warehouse_shipment.header.bpdsat = ship_to_province + customer_key = warehouse_shipment.create_customer_key(shandex_code_part2, shandex_code_part1) + if customer_key == 'SAMP1000_0000': #flag sample orders better + warehouse_shipment.header.bpdnam = 'SMP: ' + warehouse_shipment.header.bpdnam + if customer_key not in X3_CUSTOMER_MAPPING.keys(): + pprint.pprint(customer_key + ' not found.') + warehouse_shipment.header.bpcord = '' + else: + warehouse_shipment.header.bpcord = X3_CUSTOMER_MAPPING[customer_key] + if fields[0] == "QTY": + #QTY*39*10*CA + _, _, qty_str, uom = fields[:4] + #warehouse_shipment.sohnum = sohnum + if fields[0] == "LIN": + #LIN**VN*10077652082224*LT*09032026C# + _, _, _, gtin, _, lot = fields[:6] + if fields[0] == "AMT": + #AMT*LP*53.90 + _, _, price = fields[:3] + lookup_values = get_product_from_gtin(gtin) + itmref = lookup_values['ITMREF_0'] + itmdes = lookup_values['ITMDES1_0'] + sau = lookup_values['STU_0'] + subdetail = WarehouseShipmentSubDetail( + qtypcu=-1 * int(qty_str), + lot=lot, + ) + + warehouse_shipment.append( + WarehouseShipmentDetail( + #sohnum=warehouse_shipment.sohnum, + itmref=itmref, + itmdes=itmdes, + qty=int(qty_str), + gropri=price, + sau=sau + ), + subdetail, + ) + #pprint.pprint(warehouse_shipment.header.ylicplate) + warehouse_shipment.header.shidat = datetime.datetime.strptime( + shipping_date, "%Y%m%d") + time_stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") + if warehouse_shipment.header.bpcord == '': + missing_customer_alert(customer_key) + import_shipment(warehouse_shipment) + + +def import_shipment(warehouse_shipment): + """send the shipment information to the shandex staging database""" + with yamamotoyama.get_connection() as data_base: + with data_base.transaction(): + data_base.query( + INSERT_SHIPMENT, + PO=warehouse_shipment.header.yclippership+'_'+warehouse_shipment.header.ylicplate, + H='H', + salfcy=warehouse_shipment.header.salfcy, + stofcy=warehouse_shipment.header.stofcy, + sdhnum=warehouse_shipment.header.sdhnum, + bpcinv=warehouse_shipment.header.bpcinv, + bpcord=warehouse_shipment.header.bpcord, + bpaadd=warehouse_shipment.header.bpaadd, + cur=warehouse_shipment.header.cur, + shidat=warehouse_shipment.header.shidat.strftime("%Y%m%d"), + cfmflg=warehouse_shipment.header.cfmflg, + pjt=warehouse_shipment.header.pjt, + bptnum=warehouse_shipment.header.bptnum, + ylicplate=warehouse_shipment.header.ylicplate, + yclippership=warehouse_shipment.header.yclippership, + invdtaamt_2=warehouse_shipment.header.invdtaamt_2, + invdtaamt_3=warehouse_shipment.header.invdtaamt_3, + invdtaamt_4=warehouse_shipment.header.invdtaamt_4, + invdtaamt_5=warehouse_shipment.header.invdtaamt_5, + invdtaamt_6=warehouse_shipment.header.invdtaamt_6, + invdtaamt_7=warehouse_shipment.header.invdtaamt_7, + invdtaamt_8=warehouse_shipment.header.invdtaamt_8, + invdtaamt_9=warehouse_shipment.header.invdtaamt_9, + die=warehouse_shipment.header.die, + die_1=warehouse_shipment.header.die_1, + die_2=warehouse_shipment.header.die_2, + die_3=warehouse_shipment.header.die_3, + die_4=warehouse_shipment.header.die_4, + die_5=warehouse_shipment.header.die_5, + die_6=warehouse_shipment.header.die_6, + die_7=warehouse_shipment.header.die_7, + die_8=warehouse_shipment.header.die_8, + die_9=warehouse_shipment.header.die_9, + die_10=warehouse_shipment.header.die_10, + die_11=warehouse_shipment.header.die_11, + die_12=warehouse_shipment.header.die_12, + die_13=warehouse_shipment.header.die_13, + die_14=warehouse_shipment.header.die_14, + die_15=warehouse_shipment.header.die_15, + die_16=warehouse_shipment.header.die_16, + die_17=warehouse_shipment.header.die_17, + die_18=warehouse_shipment.header.die_18, + die_19=warehouse_shipment.header.die_19, + cce=warehouse_shipment.header.cce, + cce_1=warehouse_shipment.header.cce_1, + cce_2=warehouse_shipment.header.cce_2, + cce_3=warehouse_shipment.header.cce_3, + cce_4=warehouse_shipment.header.cce_4, + cce_5=warehouse_shipment.header.cce_5, + cce_6=warehouse_shipment.header.cce_6, + cce_7=warehouse_shipment.header.cce_7, + cce_8=warehouse_shipment.header.cce_8, + cce_9=warehouse_shipment.header.cce_9, + cce_10=warehouse_shipment.header.cce_10, + cce_11=warehouse_shipment.header.cce_11, + cce_12=warehouse_shipment.header.cce_12, + cce_13=warehouse_shipment.header.cce_13, + cce_14=warehouse_shipment.header.cce_14, + cce_15=warehouse_shipment.header.cce_15, + cce_16=warehouse_shipment.header.cce_16, + cce_17=warehouse_shipment.header.cce_17, + cce_18=warehouse_shipment.header.cce_18, + cce_19=warehouse_shipment.header.cce_19, + bpdnam=warehouse_shipment.header.bpdnam, + bpdaddlig=warehouse_shipment.header.bpdaddlig, + bpdaddlig_1=warehouse_shipment.header.bpdaddlig_1, + bpdaddlig_2=warehouse_shipment.header.bpdaddlig_2, + bpdposcod=warehouse_shipment.header.bpdposcod, + bpdcty=warehouse_shipment.header.bpdcty, + bpdsat=warehouse_shipment.header.bpdsat, + bpdcry=warehouse_shipment.header.bpdcry, + bpdcrynam=warehouse_shipment.header.bpdcrynam, + sdhtyp=warehouse_shipment.header.sdhtyp, + growei=warehouse_shipment.header.growei, + pacnbr=warehouse_shipment.header.pacnbr, + star71=warehouse_shipment.header.star71, + star72=warehouse_shipment.header.star72, + star81=warehouse_shipment.header.star81, + star82=warehouse_shipment.header.star82 + ) + with data_base.transaction(): + for detail in warehouse_shipment.details: + for subdetail in detail.subdetails: + data_base.query( + INSERT_SHIPMENT_DETAIL, + PO=warehouse_shipment.header.yclippership+'_'+warehouse_shipment.header.ylicplate, + L='L', + sohnum=detail.sohnum, + soplin=str(detail.soplin), + itmref=detail.itmref, + itmdes=detail.itmdes, + sau=detail.sau, + qty=str(detail.qty), + gropri=detail.gropri, + star91=detail.star91, + star92=detail.star92, + S='S', + sta=subdetail.sta, + pcu=subdetail.pcu, + qtypcu=str(subdetail.qtypcu), + loc=subdetail.loc, + lot=subdetail.lot, + sernum=subdetail.sernum + ) + +@dataclasses.dataclass +class WarehouseShipmentSubDetail: + """ + Information that goes onto a shipment sub-detail line, taken from ZSHIP867 template. + """ + + sta: str = "A" + pcu: str = "" + qtypcu: int = 0 + loc: str = "" + lot: str = "" + sernum: str = "" + + def convert_to_strings(self) -> typing.List[str]: + """ + Convert to strings for X3 import writing. + """ + return yamamotoyama.x3_imports.convert_to_strings( + [ + "S", + self.sta, + self.pcu, + self.qtypcu, + self.loc, + self.lot, + self.sernum, + ] + ) + + +@dataclasses.dataclass +class WarehouseShipmentDetail: + """ + Information that goes on a shipment detail line, taken from ZSHIP867 template. + """ + + sohnum: str = "" + soplin: int = 0 + itmref: str = "" + itmdes: str = "" + sau: str = "" + qty: int = 0 + gropri: decimal.Decimal = decimal.Decimal() + star91: str = "" + star92: str = "" + subdetails: typing.List[WarehouseShipmentSubDetail] = dataclasses.field( + default_factory=list + ) + + def append(self, subdetail: WarehouseShipmentSubDetail): + """ + Add subdetail + """ + subdetail.pcu = self.sau + self.subdetails.append(subdetail) + + def check_subdetail_qty(self): + """ + Check for shortages by totaling up subdetail quantities. + """ + total_cases = 0 + for subdetail in self.subdetails: + total_cases += subdetail.qtypcu + return abs(total_cases) + + def convert_to_strings(self) -> typing.List[str]: + """ + Convert to strings for X3 import writing. + """ + self.qty = self.check_subdetail_qty() + return yamamotoyama.x3_imports.convert_to_strings( + [ + "L", + self.sohnum, + self.soplin, + self.itmref, + self.itmdes, + self.sau, + self.qty, + self.gropri, + self.star91, + self.star92, + ] + ) + + def __eq__(self, item: typing.Any) -> bool: + """ + Test for equality + """ + if isinstance(item, str): + return self.itmref == item + if isinstance(item, WarehouseShipmentDetail): + return self.itmref == item.itmref + return False + + def fill(self): + """ + Set soplin & itmdes from itmref & sohnum + """ + + def get() -> records.Record: + with yamamotoyama.get_connection() as database: + how_many = ( + database.query( + """ + select + count(*) as [how_many] + from [PROD].[SORDERP] as [SOP] + where + [SOP].[SOHNUM_0] = :sohnum + and [SOP].[ITMREF_0] = :itmref + """, + sohnum=self.sohnum, + itmref=self.itmref, + ) + .first() + .how_many + ) + if how_many == 1: + return database.query( + """ + select top 1 + [SOP].[SOPLIN_0] + ,[SOP].[ITMDES1_0] + ,[SOP].[SAU_0] + from [PROD].[SORDERP] as [SOP] + where + [SOP].[SOHNUM_0] = :sohnum + and [SOP].[ITMREF_0] = :itmref + order by + [SOP].[SOPLIN_0] + """, + sohnum=self.sohnum, + itmref=self.itmref, + ).first() + else: + emailtext = str(self.sohnum +' '+str(self.itmref)) + msg.attach(MIMEText(emailtext, 'plain')) + with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: + smtp.login(user='x3reportmk2@yamamotoyama.com', password=r'n typing.List[str]: + """ + Convert to X3 import line + """ + return yamamotoyama.x3_imports.convert_to_strings( + [ + "H", + self.salfcy, + self.stofcy, + self.sdhnum, + self.bpcinv, + self.bpcord, + self.bpaadd, + self.cur, + self.shidat.strftime("%Y%m%d"), + self.cfmflg, + self.pjt, + self.bptnum, + self.ylicplate, + self.yclippership, + self.invdtaamt_2, + self.invdtaamt_3, + self.invdtaamt_4, + self.invdtaamt_5, + self.invdtaamt_6, + self.invdtaamt_7, + self.invdtaamt_8, + self.invdtaamt_9, + self.die, + self.die_1, + self.die_2, + self.die_3, + self.die_4, + self.die_5, + self.die_6, + self.die_7, + self.die_8, + self.die_9, + self.die_10, + self.die_11, + self.die_12, + self.die_13, + self.die_14, + self.die_15, + self.die_16, + self.die_17, + self.die_18, + self.die_19, + self.cce, + self.cce_1, + self.cce_2, + self.cce_3, + self.cce_4, + self.cce_5, + self.cce_6, + self.cce_7, + self.cce_8, + self.cce_9, + self.cce_10, + self.cce_11, + self.cce_12, + self.cce_13, + self.cce_14, + self.cce_15, + self.cce_16, + self.cce_17, + self.cce_18, + self.cce_19, + self.bpdnam, + self.bpdaddlig, + self.bpdaddlig_1, + self.bpdaddlig_2, + self.bpdposcod, + self.bpdcty, + self.bpdsat, + self.bpdcry, + self.bpdcrynam, + self.sdhtyp, + self.growei, + self.pacnbr, + self.star71, + self.star72, + self.star81, + self.star82, + ] + ) + + +class WarehouseShipmentDetailList: + """ + List of shipment details + """ + + _details: typing.List[WarehouseShipmentDetail] + _item_set: typing.Set[str] + + def __init__(self): + self._details = [] + self._item_set = set() + + def append( + self, + shipment_detail: WarehouseShipmentDetail, + shipment_subdetail: WarehouseShipmentSubDetail, + ): + """ + Append + """ + itmref = shipment_detail.itmref + # if itmref in self._item_set: + # for detail in self._details: + # if detail == itmref: + # detail.subdetails.append(shipment_subdetail) + # return + self._item_set.add(itmref) + #shipment_detail.fill() + shipment_detail.append(shipment_subdetail) + self._details.append(shipment_detail) + + def __iter__(self): + return iter(self._details) + + +class WarehouseShipment: + """ + Warehosue shipment, both header & details + """ + + header: WarehouseShipmentHeader + details: WarehouseShipmentDetailList + _sohnum: str + + def __init__(self): + self.header = WarehouseShipmentHeader() + self._sohnum = "" + self.details = WarehouseShipmentDetailList() + + def append( + self, + shipment_detail: WarehouseShipmentDetail, + shipment_subdetail: WarehouseShipmentSubDetail, + ): + """ + Add detail information. + """ + self.details.append(shipment_detail, shipment_subdetail) + + @property + def sohnum(self): + """ + Sales order number + """ + return self._sohnum + + @sohnum.setter + def sohnum(self, value: str): + if self._sohnum != value: + self._sohnum = value + if value: + self._fill_info_from_so() + + + def create_customer_key(self, part1, part2): + key = (part1 + '_' + part2).replace(' ', '_') + return key + + + def _get_so_from_x3(self) -> records.Record: + """ + Fetch sales order from X3 database. + """ + with yamamotoyama.get_connection() as db_connection: + return db_connection.query( + """ + select + [SOH].[SALFCY_0] + ,[SOH].[STOFCY_0] + ,[SOH].[BPCORD_0] + ,[SOH].[BPAADD_0] + ,[SOH].[CUR_0] + ,[SOH].[INVDTAAMT_2] + ,[SOH].[INVDTAAMT_3] + ,[SOH].[INVDTAAMT_4] + ,[SOH].[INVDTAAMT_5] + ,[SOH].[INVDTAAMT_6] + ,[SOH].[INVDTAAMT_7] + ,[SOH].[INVDTAAMT_8] + ,[SOH].[INVDTAAMT_9] + ,[SOH].[DIE_0] + ,[SOH].[DIE_1] + ,[SOH].[DIE_2] + ,[SOH].[DIE_3] + ,[SOH].[DIE_4] + ,[SOH].[DIE_5] + ,[SOH].[DIE_6] + ,[SOH].[DIE_7] + ,[SOH].[DIE_8] + ,[SOH].[DIE_9] + ,[SOH].[DIE_10] + ,[SOH].[DIE_11] + ,[SOH].[DIE_12] + ,[SOH].[DIE_13] + ,[SOH].[DIE_14] + ,[SOH].[DIE_15] + ,[SOH].[DIE_16] + ,[SOH].[DIE_17] + ,[SOH].[DIE_18] + ,[SOH].[DIE_19] + ,[SOH].[CCE_0] + ,[SOH].[CCE_1] + ,[SOH].[CCE_2] + ,[SOH].[CCE_3] + ,[SOH].[CCE_4] + ,[SOH].[CCE_5] + ,[SOH].[CCE_6] + ,[SOH].[CCE_7] + ,[SOH].[CCE_8] + ,[SOH].[CCE_9] + ,[SOH].[CCE_10] + ,[SOH].[CCE_11] + ,[SOH].[CCE_12] + ,[SOH].[CCE_13] + ,[SOH].[CCE_14] + ,[SOH].[CCE_15] + ,[SOH].[CCE_16] + ,[SOH].[CCE_17] + ,[SOH].[CCE_18] + ,[SOH].[CCE_19] + ,[SOH].[BPDNAM_0] + ,[SOH].[BPDADDLIG_0] + ,[SOH].[BPDADDLIG_1] + ,[SOH].[BPDADDLIG_2] + ,[SOH].[BPDPOSCOD_0] + ,[SOH].[BPDCTY_0] + ,[SOH].[BPDSAT_0] + ,[SOH].[BPDCRY_0] + ,[SOH].[BPDCRYNAM_0] + from [PROD].[SORDER] as [SOH] + where + [SOH].[SOHNUM_0] = :order + """, + order=self.sohnum, + ).first() + + def _copy_accounting_codes(self, result: records.Record): + """ + Fill in all the accounting codes + """ + self.header.die = result.DIE_0 + self.header.die_1 = result.DIE_1 + self.header.die_2 = result.DIE_2 + self.header.die_3 = result.DIE_3 + self.header.die_4 = result.DIE_4 + self.header.die_5 = result.DIE_5 + self.header.die_6 = result.DIE_6 + self.header.die_7 = result.DIE_7 + self.header.die_8 = result.DIE_8 + self.header.die_9 = result.DIE_9 + self.header.die_10 = result.DIE_10 + self.header.die_11 = result.DIE_11 + self.header.die_12 = result.DIE_12 + self.header.die_13 = result.DIE_13 + self.header.die_14 = result.DIE_14 + self.header.die_15 = result.DIE_15 + self.header.die_16 = result.DIE_16 + self.header.die_17 = result.DIE_17 + self.header.die_18 = result.DIE_18 + self.header.die_19 = result.DIE_19 + self.header.cce = result.CCE_0 + self.header.cce_1 = result.CCE_1 + self.header.cce_2 = result.CCE_2 + self.header.cce_3 = result.CCE_3 + self.header.cce_4 = result.CCE_4 + self.header.cce_5 = result.CCE_5 + self.header.cce_6 = result.CCE_6 + self.header.cce_7 = result.CCE_7 + self.header.cce_8 = result.CCE_8 + self.header.cce_9 = result.CCE_9 + self.header.cce_10 = result.CCE_10 + self.header.cce_11 = result.CCE_11 + self.header.cce_12 = result.CCE_12 + self.header.cce_13 = result.CCE_13 + self.header.cce_14 = result.CCE_14 + self.header.cce_15 = result.CCE_15 + self.header.cce_16 = result.CCE_16 + self.header.cce_17 = result.CCE_17 + self.header.cce_18 = result.CCE_18 + self.header.cce_19 = result.CCE_19 + + def _fill_info_from_so(self): + """ + When we learn the SOHNUM, we can copy information from the sales order. + """ + result = self._get_so_from_x3() + self.header.salfcy = result.SALFCY_0 + self.header.stofcy = result.STOFCY_0 + self.header.bpcord = result.BPCORD_0 + self.header.bpaadd = result.BPAADD_0 + self.header.cur = result.CUR_0 + self.header.invdtaamt_2 = result.INVDTAAMT_2 + self.header.invdtaamt_3 = result.INVDTAAMT_3 + self.header.invdtaamt_4 = result.INVDTAAMT_4 + self.header.invdtaamt_5 = result.INVDTAAMT_5 + self.header.invdtaamt_6 = result.INVDTAAMT_6 + self.header.invdtaamt_7 = result.INVDTAAMT_7 + self.header.invdtaamt_8 = result.INVDTAAMT_8 + self.header.invdtaamt_9 = result.INVDTAAMT_9 + self._copy_accounting_codes(result) + self.header.bpdnam = result.BPDNAM_0 + self.header.bpdaddlig = result.BPDADDLIG_0 + self.header.bpdaddlig_1 = result.BPDADDLIG_1 + self.header.bpdaddlig_2 = result.BPDADDLIG_2 + self.header.bpdposcod = result.BPDPOSCOD_0 + self.header.bpdcty = result.BPDCTY_0 + self.header.bpdsat = result.BPDSAT_0 + self.header.bpdcry = result.BPDCRY_0 + self.header.bpdcrynam = result.BPDCRYNAM_0 + + def output(self, import_file: typing.TextIO): + """ + Output entire order to import_file. + """ + output = functools.partial( + yamamotoyama.x3_imports.output_with_file, import_file + ) + output(self.header.convert_to_strings()) + for detail in self.details: + output(detail.convert_to_strings()) + for subdetail in detail.subdetails: + output(subdetail.convert_to_strings()) + + +if __name__ == "__main__": + main() diff --git a/edi_943.py b/edi_943.py index a4cf740..1e81155 100644 --- a/edi_943.py +++ b/edi_943.py @@ -19,7 +19,7 @@ import yamamotoyama # type:ignore SITE_MAPPING = { 'WNJ' : 'SOURCELOGISTICS', 'WCA' : 'SOURCELOGISTICS', -'WON' : 'SHANDEXTEST ' # TODO CHANGE TO SHANDEX, needs to be 15 characters +'WON' : 'SHANDEX ' #Testing value is SHANDEXTEST with spaces to 15 characters } SHIPPING_CODE_MAPPING = { @@ -35,14 +35,12 @@ SHIPPING_CODE_MAPPING = { THIS_DIRECTORY = pathlib.Path(__file__).parent X12_SHANDEX = THIS_DIRECTORY / "outgoing" -# X12_SOURCELOGISTICS = THIS_DIRECTORY / "edi-testing" #test directories -# X12_SHANDEX = THIS_DIRECTORY / "edi-testing" def main(): """ Do it! """ - with yamamotoyama.get_connection('test') as database: + with yamamotoyama.get_connection() as database: shipments = list(get_shipments(database)) for shipment in shipments: write_943(database, shipment) @@ -127,7 +125,7 @@ def write_943(database: records.Connection, shipment: str): with database.transaction() as _: database.query( """ - update [FY23TEST].[SDELIVERY] + update [PROD].[SDELIVERY] set [XX4S_943RDY_0] = 1 where [SOHNUM_0] = :shipment """, @@ -136,7 +134,7 @@ def write_943(database: records.Connection, shipment: str): order = get_order_for_shipment(database, shipment) database.query( """ - update [FY23TEST].[SORDER] + update [PROD].[SORDER] set [XX4S_UDF2_0] = :sent_message where [SOHNUM_0] = :order """, @@ -153,7 +151,7 @@ def get_shipment_destination(database: records.Connection, shipment: str) -> str """ select [SDH].[BPCORD_0] - from [FY23TEST].[SDELIVERY] as [SDH] + from [PROD].[SDELIVERY] as [SDH] where [SDH].[SDHNUM_0] = :shipment """, @@ -172,7 +170,7 @@ def get_order_for_shipment(database: records.Connection, shipment: str) -> str: """ select [SDH].[SOHNUM_0] - from [FY23TEST].[SDELIVERY] as [SDH] + from [PROD].[SDELIVERY] as [SDH] where [SDH].[SDHNUM_0] = :shipment """, @@ -191,8 +189,8 @@ def get_shipments(database: records.Connection) -> typing.Iterator[str]: """ select [SDH].[SDHNUM_0] - from [FY23TEST].[SDELIVERY] as [SDH] - join [FY23TEST].[SORDER] as [SOH] + from [PROD].[SDELIVERY] as [SDH] + join [PROD].[SORDER] as [SOH] on [SOH].[SOHNUM_0] = [SDH].[SOHNUM_0] where ( @@ -203,7 +201,7 @@ def get_shipments(database: records.Connection) -> typing.Iterator[str]: ) and [SDH].[SHIDAT_0] >= {d'2023-10-09'} and ( - [SOH].[XX4S_UDF2_0] = '' + [SOH].[XX4S_UDF2_0] not like '943%' or [SDH].[XX4S_943RDY_0] = 2 ) and ( @@ -267,7 +265,7 @@ def get_shipment( ,[GROWEI_0] ,[CFMFLG_0] ,[SHIDAT_0] - from [FY23TEST].[zyumiddleware_shipment] as [SDH] + from [PROD].[zyumiddleware_shipment_shandex] as [SDH] where [SDH].[SDHNUM_0] = :shipment """, @@ -549,7 +547,7 @@ class ShipmentDetail(X12): "", "", self.gtin_or_upc_marker, - self.gtin_or_upc_code,#W04-15 + self.gtin_or_upc_code.replace(' ',''),#W04-15 ] ), self.line( diff --git a/edi_947.py b/edi_947.py index 9e78242..7f281a8 100644 --- a/edi_947.py +++ b/edi_947.py @@ -34,10 +34,6 @@ X12_DIRECTORY = THIS_DIRECTORY / "incoming" IMPORTS_DIRECTORY = THIS_DIRECTORY / "x3_imports" EDI_997_DIRECTORY = THIS_DIRECTORY / "997_processing" -SHANDEX_947_FILENAME_RE = re.compile( - r"\A 947_YAMAMOTOYAMA_ \S+ [.]edi \Z", re.X | re.M | re.S -) -#TODO remove this and have a single production name? why is this one different SHANDEX_947_FILENAME_RE = re.compile( r"\A 947_QTY_STASH-YAMAMOTOYAMA_ \S+ [.]edi \Z", re.X | re.M | re.S ) @@ -80,7 +76,7 @@ def main(): if SHANDEX_947_FILENAME_RE.match(edi_filename.name): process_file(edi_filename) # file moved to 997 processing folder to be sent later - #shutil.move(edi_filename, EDI_997_DIRECTORY / edi_filename.name) + shutil.move(edi_filename, EDI_997_DIRECTORY / edi_filename.name) combine_zscs() @@ -154,7 +150,8 @@ def stock_movement_alert(itmref, qty, lot, status): msg['Subject'] = 'New Stock Change from Shandex' msg['Precedence'] = 'bulk' msg['From'] = 'x3report@stashtea.com' - msg['To'] = 'bleeson@stashtea.com'#TODO correct receipientscares + msg['To'] = 'isenn@yamamotoyama.com, vgomez@yamamotoyama.com' + msg['CC'] = 'bleeson@stashtea.com' emailtext = f'Item: {itmref}\nQty: {qty}\nLot: {lot}\nStatus: {DAMAGE_CODE_MAPPING[status]}\nReason: {DAMAGE_CODE_DESCRIPTIONS_MAPPING[status]}' msg.attach(MIMEText(emailtext, 'plain')) with smtplib.SMTP_SSL("smtp.gmail.com", 465) as smtp: @@ -222,37 +219,6 @@ class StockChangeSubDetail: loc: str = "" sta: str = "A" - # def stojous(self, shipment, item) -> typing.List[str]: - # """ - # Convert grouped lot quantities into individual STOJOU records to fit on stockchange - # """ - # with yamamotoyama.get_connection('test') as database: #todo remove 'test' - # details = ( - # database.query( - # """ - # select - # 'S', - # 'A', - # [STJ].[PCU_0], - # cast(cast(-1*[STJ].[QTYSTU_0] as int) as nvarchar), - # [STJ].[LOT_0], - # '', - # '' - # from [FY23TEST].[STOJOU] [STJ] --TODO change to PROD - # where - # [STJ].[VCRNUM_0] = :sdhnum - # and [STJ].[ITMREF_0] = :itmref - # and [STJ].[LOT_0] = :lot - # and [STJ].[TRSTYP_0] = 4 - # """, - # sdhnum=shipment, - # itmref=item, - # lot=self.lot, - # ) - # .all() - # ) - # return details - def convert_to_strings(self) -> typing.List[str]: """ @@ -288,7 +254,7 @@ class StockChangeDetail: itmref: str = "" pcu: str = "" pcustucoe: int = 1 #does this need a lookup? - sta: str = "A" #todo this needs to flip based on the transaction A > R, A > Q, what about Q > A? + sta: str = "A" loctyp: str = "" loc: str = "" lot: str = "" @@ -564,39 +530,6 @@ class StockChange: if value: self._fill_info_from_shipment() - # def _get_shipment_from_x3(self) -> records.Record: - # """ - # Fetch shipment from X3 database. - # """ - # with yamamotoyama.get_connection('test') as db_connection:#todo remove 'test' - # return db_connection.query( - # """ - # select - # [SDH].[STOFCY_0], - # [SDH].[SDHNUM_0], - # [SDH].[SALFCY_0], - # [SDH].[BPCORD_0], - # [SDH].[CUR_0], - # [SDH].[SOHNUM_0] - # from [FY23TEST].[SDELIVERY] [SDH]--TODO change back to [PROD] - # where - # [SDH].[SDHNUM_0] = :shipment - # """, - # shipment=self.sdhnum, - # ).first() - - # def _fill_info_from_shipment(self): - # """ - # When we learn the SOHNUM, we can copy information from the sales order. - # """ - # result = self._get_shipment_from_x3() - # self.header.stofcy = result.STOFCY_0 - # self.header.sdhnum = result.SDHNUM_0 - # self.header.salfcy = result.SALFCY_0 - # self.header.bpcord = result.BPCORD_0 - # self.header.cur = result.CUR_0 - # self.header.sohnum = result.SOHNUM_0 - def output(self, import_file: typing.TextIO): """ diff --git a/edi_997_inbound.py b/edi_997_inbound.py index d872718..27fcc3a 100644 --- a/edi_997_inbound.py +++ b/edi_997_inbound.py @@ -22,7 +22,7 @@ import yamamotoyama.x3_imports # type: ignore THIS_DIRECTORY = pathlib.Path(__file__).parent X12_DIRECTORY = THIS_DIRECTORY / "incoming" -SHANDEX_997_FILENAME_RE = re.compile( #TODO FIX REGEX +SHANDEX_997_FILENAME_RE = re.compile( r"\A 997_YAMAMOTOYAMA_ .* [.]edi \Z", re.X | re.M | re.S ) @@ -46,7 +46,7 @@ def 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 { #TODO see if there are more fields used in vendor EDI + if fields[0] in { "ISA", "GS", "ST", diff --git a/master_contoller.py b/master_contoller.py index 4a1ffff..27359e4 100644 --- a/master_contoller.py +++ b/master_contoller.py @@ -11,10 +11,12 @@ import edi_997_inbound import edi_944 import edi_947 import edi_846 -import edi_867 +import edi_867_to_table import edi_997_outbound import update_shandex_dashboard import edi_943 +import unprocessed_files_report +import import_867s THIS_DIRECTORY = pathlib.Path(__file__).parent X12_SHANDEX_OUTGOING = THIS_DIRECTORY / "outgoing" @@ -28,22 +30,29 @@ def main(): #pick up files from Shandex retrieve_x12_edi_files_shandex() + #report on anything not handled + unprocessed_files_report.main() + #process all EDIs that started with Shandex edi_997_inbound.main() edi_944.main() edi_947.main() - #edi_846.main() - edi_867.main() + edi_846.main() + edi_867_to_table.main() + import_867s.main() # process all EDIs that start with us edi_943.main() edi_997_outbound.main() #send them to Shandex - send_x12_edi_files_shandex()#TODO production changes + send_x12_edi_files_shandex() - #update dashboard - # update_shandex_dashboard.main() + #update dashboard - no longer needed now that it has been moved to the staging database 2024-08-20 + #update_shandex_dashboard.main() + + #report on anything not handled + unprocessed_files_report.main() @@ -69,7 +78,7 @@ def send_x12_edi_files_shandex(): hostname=SHANDEX_SFTP_HOST, username=SHANDEX_SFTP_USERNAME, password=SHANDEX_SFTP_PASSWORD ) with ssh_client.open_sftp() as sftp_connection: - sftp_connection.chdir("/Stash/Test/ToShandex") #TODO change to production folder + sftp_connection.chdir("/Stash/Prod/ToShandex") for filename in X12_SHANDEX_OUTGOING.glob("*.edi"): sftp_connection.put(filename, str(filename.name)) shutil.move(filename, X12_SHANDEX_OUTGOING / "archive" / filename.name) @@ -84,11 +93,11 @@ def retrieve_x12_edi_files_shandex(): hostname=SHANDEX_SFTP_HOST, username=SHANDEX_SFTP_USERNAME, password=SHANDEX_SFTP_PASSWORD ) with ssh_client.open_sftp() as sftp_connection: - sftp_connection.chdir("/Stash/Test/FromShandex") + sftp_connection.chdir("/Stash/Prod/FromShandex") for filename in sftp_connection.listdir(): if filename.endswith(".edi"): sftp_connection.get(filename, X12_SHANDEX_INCOMING / filename) - new_filename = f"/Stash/Test/FromShandex/Archive/{filename}" + new_filename = f"/Stash/Archive/{filename}" sftp_connection.rename(filename, new_filename) diff --git a/readme.txt b/readme.txt index 398b53f..757c13f 100644 --- a/readme.txt +++ b/readme.txt @@ -1,8 +1,5 @@ Process EDI documents between YU and Shandex ------------------------------------------------------------ -EDI types accepted: - 997 Functional acknowledgment Tell Shandex we received an EDI 944 Stock transfer receipt Receive qty and lots from replenishment shipment 947 Inventory advice Tell Stash of Q and R status inventory @@ -10,15 +7,19 @@ EDI types accepted: 867 Resale report Tell Stash qty, price, and lot sold 943 Stock transfer Tell Shandex qty and lots being shipped ------------------------------------------------------------ -General operation: master_controller takes EDI files from the Shandex FTP, processes them, and loads our EDI files. -Sales come in via the 867 script and the shandex_dashboard script stores which files have been received. -The corresponding control numbers can be matched to X3 Sales Deliveries in the YLICPLATE_0 field. +Individual edi_***.py files process specific types of transactions. ------------------------------------------------------------ -Currently runs in the 30 minute X3 recurring task +Sales come in via 867 and the shandex_dashboard script stores which files have been received. +The corresponding control numbers can be matched to X3 Sales Deliveries in the YLICPLATE_0 and YCLIPPERSHIP_0 fields. -To switch between PROD and DEV, database connections need to be passed 'test' -and the schema names in SQL queries need to be correct for the test folder being used. +------------------- +Issues +------------------- +867s will fail to process if a customer has not been mapped for them in the edi_867.py file. +These problems are logged in the staging database in staging.dbo.shandex_shipments. To reprocess, +add the customer code to the table and check it's is_sent field. Make sure to add the right code to +the dictionary in 867 processing as well. + +944s need to be rerun in the edi_944.py script after validation, so that lots appear in the import file. \ No newline at end of file diff --git a/update_shandex_dashboard.py b/update_shandex_dashboard.py index c741b3f..2c7e03b 100644 --- a/update_shandex_dashboard.py +++ b/update_shandex_dashboard.py @@ -7,18 +7,12 @@ To reprocess a file, move it to the correct folder on the FTP import shutil import yamamotoyama import pathlib +import pprint THIS_DIRECTORY = pathlib.Path(__file__).parent RECEIVED_867_DIRECTORY = THIS_DIRECTORY / "processed_867s" ARCHIVE = RECEIVED_867_DIRECTORY / "archive" - -INSERT_867s = """\ - execute [PROD].[insert_shandex_867_data] - :ctrlnum, - :credat; - """ - def main(): """ @@ -41,14 +35,31 @@ def remove_completed_867s(): def import_received_867s(data): with yamamotoyama.get_connection() as data_base: with data_base.transaction(): - data_base.bulk_query(INSERT_867s, data) - + data_base.query( + """ + EXECUTE [PROD].[insert_shandex_867_data] + :ctrlnum, + :ylicplate, + :credat, + :customer_po, + :shandex_so + """, + ctrlnum=data['ctrlnum']+'-'+data['ylicplate'], + ylicplate=data['ylicplate'], + credat=data['credat'], + customer_po=data['customer_po'], + shandex_so=data['shandex_so'], + ) def process_received_867s(): - #pull out the control number on the GS line + #pull out the control number on the GS line and the BOL# on PTD for file in RECEIVED_867_DIRECTORY.iterdir(): control_number = '' transaction_date = '' + picking_number = '' + customer_po = '' + shandex_so = '' + data_set = [] if file.name.endswith('.edi'): with file.open(encoding="utf-8", newline="") as edi_file: for record in edi_file.read().split("~"): @@ -57,11 +68,25 @@ def process_received_867s(): control_number = fields[6] if fields[0] == 'BPT': transaction_date = fields[3] - data = { - "ctrlnum":control_number, - "credat":transaction_date + # REF*PO*3L49287E + if fields[0] == 'REF' and fields[1] == 'PO': + customer_po = fields[2] + # REF*IL*O0215276 + if fields[0] == 'REF' and fields[1] == 'IL': + shandex_so = fields[2] + if fields[0] == "PTD" and len(fields) > 2: + picking_number = fields[5] + data = { + "ctrlnum":control_number, + "ylicplate":picking_number, + "credat":transaction_date, + "customer_po":customer_po, + "shandex_so":shandex_so, } - import_received_867s(data) + if data not in data_set: + data_set.append(data) + import_received_867s(data) + if __name__ == "__main__":