#!/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

import simple_email_notification

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_OLD = {
    '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',
    'GTWH1000_GTJOHN' : 'GIAN0004',
    'AVRI1000_0000' : 'AVRI0003',
    'SOBE1000_SB0002' :'SOBE0014',
    'SOBE1000_SB0023' : 'SOBE0013',
    'SAMP1000_WELL1' : 'YARI0001',
    'SAMP1000_WELL2' : 'YARI0001',
    'LOND1000_0000' : 'LOND0001',
    'VEND1000_VENDAB' : 'VEND0002',
    'ISLA1000_190132' : 'ISLA0005',
    'ISLA1000_0000' : 'ISLA0005',
    'PURI1000_0000' : 'PURI0002',
    'AVRI1000_AVRIGR' : 'AVRI0001',
    'VANH1000_VANH1000' : 'VANH0001',
}

def main():
    """
    Do it!
    """
    x3_customer_mapping = get_customer_map()
    for edi_filename in X12_DIRECTORY.iterdir():
        if SOURCE_867_FILENAME_RE.match(edi_filename.name):
            process_file(edi_filename, x3_customer_mapping)
            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, 2024-08 dashboard no longer needed


def get_customer_map():
    customer_map = {}
    with yamamotoyama.get_connection() as database:
        result = database.query(
                """
            SELECT 
                [shandex_key]
                ,[x3_customer]
            FROM [staging].[dbo].[shandex_customer_map]
            """,
            ).all()
        for record in result:
            customer_map[record['shandex_key']] = record['x3_customer']
    return customer_map


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</W<7fr"VD~\2&[pZc5')
        smtp.send_message(msg)


def combine_zship867s():
    """
    Collect all ZSHIP867 imports into a single file for easy import.
    """
    archive_directory = IMPORTS_DIRECTORY / "archive"
    archive_directory.mkdir(exist_ok=True)
    with (IMPORTS_DIRECTORY / "ZSHIP867.dat").open(
        "w", encoding="utf-8", newline="\n"
    ) as combined_import_file:
        for individual_import_filename in IMPORTS_DIRECTORY.glob(
            "ZSHIP867_*.dat"
        ):
            with individual_import_filename.open(
                "r", encoding="utf-8", newline="\n"
            ) as individual_import_file:
                for line in individual_import_file:
                    combined_import_file.write(line)
            shutil.move(
                individual_import_filename,
                archive_directory / individual_import_filename.name,
            )


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",
                "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, x3_customer_mapping):
    """
    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:
        result = data_base.query(
        """
        SELECT
            ylicplate
        FROM [staging].[dbo].[shandex_shipments]
        where ylicplate = :order
        """,
        order=warehouse_shipment.header.ylicplate,
        ).all()
        if not result:
            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
                            )
        else:
            simple_email_notification.email_noticication(['bleeson@stashtea.com'],'Shandex Order Error',[f'{warehouse_shipment.header.ylicplate} already exists, is this a reship?'])

@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</W<7fr"VD~\2&[pZc5')
                        smtp.send_message(msg)
                    raise NotImplementedError  # TODO

        result = get()
        self.soplin = result.SOPLIN_0
        self.itmdes = result.ITMDES1_0
        self.sau = result.SAU_0


@dataclasses.dataclass
class WarehouseShipmentHeader:
    """
    Information that goes on a shipment header, taken from ZSHIP867 template.
    """

    salfcy: str = "STC"
    stofcy: str = "WON"
    sdhnum: str = ""
    bpcinv: str = "SHAN0001"
    bpcord: str = "SHAN0001"
    bpaadd: str = "SH001"
    cur: str = "CAD"
    shidat: datetime.date = datetime.date(1753, 1, 1)
    cfmflg: int = 1
    pjt: str = ""
    bptnum: str = ""
    ylicplate: str = "SHANDEX"
    yclippership: str = ""
    invdtaamt_2: decimal.Decimal = decimal.Decimal()
    invdtaamt_3: decimal.Decimal = decimal.Decimal()
    invdtaamt_4: decimal.Decimal = decimal.Decimal()
    invdtaamt_5: decimal.Decimal = decimal.Decimal()
    invdtaamt_6: decimal.Decimal = decimal.Decimal()
    invdtaamt_7: decimal.Decimal = decimal.Decimal()
    invdtaamt_8: decimal.Decimal = decimal.Decimal()
    invdtaamt_9: decimal.Decimal = decimal.Decimal()
    die: str = "" #TODO consider adding dimension codes?
    die_1: str = ""
    die_2: str = ""
    die_3: str = ""
    die_4: str = ""
    die_5: str = ""
    die_6: str = ""
    die_7: str = ""
    die_8: str = ""
    die_9: str = ""
    die_10: str = ""
    die_11: str = ""
    die_12: str = ""
    die_13: str = ""
    die_14: str = ""
    die_15: str = ""
    die_16: str = ""
    die_17: str = ""
    die_18: str = ""
    die_19: str = ""
    cce: str = ""
    cce_1: str = ""
    cce_2: str = ""
    cce_3: str = ""
    cce_4: str = ""
    cce_5: str = ""
    cce_6: str = ""
    cce_7: str = ""
    cce_8: str = ""
    cce_9: str = ""
    cce_10: str = ""
    cce_11: str = ""
    cce_12: str = ""
    cce_13: str = ""
    cce_14: str = ""
    cce_15: str = ""
    cce_16: str = ""
    cce_17: str = ""
    cce_18: str = ""
    cce_19: str = ""
    bpdnam: str = "Shandex Group"
    bpdaddlig: str = ""
    bpdaddlig_1: str = ""
    bpdaddlig_2: str = ""
    bpdposcod: str = ""
    bpdcty: str = ""
    bpdsat: str = ""
    bpdcry: str = "CA"
    bpdcrynam: str = "Canada"
    sdhtyp: str = "SDN"
    growei: decimal.Decimal = decimal.Decimal()#TODO consider gross weight?
    pacnbr: int = 0
    star71: str = ""
    star72: str = ""
    star81: str = ""
    star82: str = ""

    def convert_to_strings(self) -> 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()