1104 lines
36 KiB
Python
1104 lines
36 KiB
Python
#!/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 = {
|
|
'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',
|
|
}
|
|
|
|
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, 2024-08 dashboard no longer needed
|
|
|
|
|
|
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):
|
|
"""
|
|
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()
|