#!/usr/bin/env python3 """ Consume a 850 file from Shandex, and translate into a Sage X3 readable file - import template ZPOH. For Shandex we also need to reply with a 997 """ # 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 os import base64 import google.auth import pickle # Gmail API utils from googleapiclient.discovery import build from google_auth_oauthlib.flow import InstalledAppFlow from google.auth.transport.requests import Request import records # type: ignore import yamamotoyama # type: ignore import yamamotoyama.x3_imports # type: ignore SCOPES = ['https://mail.google.com/'] THIS_DIRECTORY = pathlib.Path(__file__).parent X12_DIRECTORY = THIS_DIRECTORY / "incoming" IMPORTS_DIRECTORY = THIS_DIRECTORY / "x3_imports" EDI_997_DIRECTORY = THIS_DIRECTORY / "997_processing" SHANDEX_850_FILENAME_RE = re.compile( r"\A 850_STASH-YAMAMOTOYAMA_ \S+ [.]edi \Z", re.X | re.M | re.S ) def main(): """ Do it! """ for edi_filename in X12_DIRECTORY.iterdir(): if SHANDEX_850_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) combine_zpohs() def gmail_authenticate(): creds = None # the file token.pickle stores the user's access and refresh tokens, and is # created automatically when the authorization flow completes for the first time if os.path.exists("token.pickle"): with open("token.pickle", "rb") as token: creds = pickle.load(token) # if there are no (valid) credentials availablle, let the user log in. if not creds or not creds.valid: if creds and creds.expired and creds.refresh_token: creds.refresh(Request()) else: flow = InstalledAppFlow.from_client_secrets_file('credentials.json', SCOPES) creds = flow.run_local_server(port=0) # save the credentials for the next run with open("token.pickle", "wb") as token: pickle.dump(creds, token) return build('gmail', 'v1', credentials=creds) def gmail_send_message(service, payload): create_message = {"raw": payload} # pylint: disable=E1101 send_message = ( service.users() .messages() .send(userId="me", body=create_message) .execute() ) return send_message def new_850_alert(ordref, orddat): msg = MIMEMultipart() msg['Subject'] = 'New PO from Shandex' msg['Precedence'] = 'bulk' msg['From'] = 'x3report@stashtea.com' msg['To'] = 'icortes@yamamotoyama.com,alai@yamamotoyama.com,sbravo@yamamotoyama.com,scplanning@yamamotoyama.com' msg['CC'] = 'bleeson@stashtea.com' emailtext = f'Ref: {ordref}\nDate: {orddat}' msg.attach(MIMEText(emailtext, 'plain')) service = gmail_authenticate() encoded_message = base64.urlsafe_b64encode(msg.as_bytes()).decode() gmail_send_message(service, encoded_message) # 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", "ST", "N2", "N3", "N4", }: continue yield fields def process_file(edi_filename: pathlib.Path): """ Convert a specific EDI file into an import file. """ with yamamotoyama.get_connection() as database: purchase_order = PO() pohnum = '' for fields in tokens_from_edi_file(edi_filename): #BEG*00*SA*PO0040865**20241209 if fields[0] == "BEG": _, _, _, ordref, _, orddat = fields[:6] purchase_order.header.ordref = ordref purchase_order.header.orddat = datetime.datetime.strptime( orddat, "%Y%m%d" ).date() # 20230922 purchase_order.header.pohfcy='WON' purchase_order.header.bpsnum='PMW' purchase_order.header.cur='USD' #DTM*010*20250109 if fields[0] == "DTM" and fields[1] == '010': expected_ship_date = fields[2] #PO1*1*2688*CA*15.02**VP*C08225*IN*10077652082255 if fields[0] == "PO1": _, lineno, qty_str, uom, pricestr, _, _, itmref, gtin = fields[:9] detail = PODetail( itmref=itmref, itmdes=get_itmdes(itmref, database), prhfcy='WON', uom=uom, qtyuom=int(qty_str), extrcpdat=expected_ship_date, gropri=pricestr, ) purchase_order.append(detail) time_stamp = datetime.datetime.now().strftime("%Y%m%d_%H%M%S") new_850_alert(ordref, purchase_order.header.orddat)#TODO new alert type, after it hits X3? with yamamotoyama.x3_imports.open_import_file( IMPORTS_DIRECTORY / f"ZPOHB_{purchase_order.header.ordref}_{time_stamp}.dat" ) as import_file: purchase_order.output(import_file) def get_itmdes(itmref, database): result = database.query( """ select ITMDES1_0 from PROD.ITMMASTER where ITMREF_0 = :itmref """, itmref=itmref, ).first()['ITMDES1_0'] return result @dataclasses.dataclass class PODetail: """ Information that goes on a PO detail line, taken from ZPOH template. """ itmref: str = "" itmdes: str = "" prhfcy: str = "" uom: str = "" qtyuom: int = 0 extrcpdat: datetime.date = datetime.date(1753, 1, 1) gropri: decimal.Decimal = 0 discrgval1: decimal.Decimal = 0 discrgval2: decimal.Decimal = 0 discrgval3: decimal.Decimal = 0 pjt: str = "" vat: str = '' star91: str = "" star92: str = "" def convert_to_strings(self) -> typing.List[str]: """ Convert to strings for X3 import writing. """ def fix_uom(uom): x3_uom = '' if uom == 'CA': x3_uom = 'CS' else: x3_uom = uom return x3_uom return yamamotoyama.x3_imports.convert_to_strings( [ "L", self.itmref, self.itmdes, self.prhfcy, fix_uom(self.uom), self.qtyuom, self.extrcpdat, self.gropri, self.discrgval1, self.discrgval2, self.discrgval3, self.pjt, self.vat, self.star91, self.star92, ] ) @dataclasses.dataclass class POHeader: """ Information that goes on a po header, taken from ZPOHB template. """ pohfcy: str = "" pohnum: str = "" orddat: datetime.date = datetime.date(1753, 1, 1) bpsnum: str = "" buy: str = "SC001" ordref: str = "" bpsnum: str = "" cur: str = "USD" star71 = "" star72 = "" star81 = "" star82 = "" def convert_to_strings(self) -> typing.List[str]: """ Convert to X3 import line """ return yamamotoyama.x3_imports.convert_to_strings( [ "E", self.pohfcy, '',#self.pohnum, self.orddat.strftime("%Y%m%d"), self.bpsnum, self.buy, self.ordref, self.cur, self.star71, self.star72, self.star81, self.star82, ] ) class PODetailList: """ List of PO details """ _details: typing.List[PODetail] _item_set: typing.Set[str] def __init__(self): self._details = [] self._item_set = set() def append( self, po_detail: PODetail ): """ Append """ self._details.append(po_detail) def __iter__(self): return iter(self._details) class PO: """ Warehouse po, both header & details """ header: POHeader details: PODetailList def __init__(self): self.header = POHeader() self.details = PODetailList() def append( self, po_detail: PODetail, ): """ Add detail information. """ self.details.append(po_detail) def output(self, import_file: typing.TextIO): """ Output entire po 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()) if __name__ == "__main__": main()