joycecast commited on
Commit
9c55413
·
verified ·
1 Parent(s): d5a74bd

Update app.py

Browse files
Files changed (1) hide show
  1. app.py +159 -91
app.py CHANGED
@@ -2,13 +2,34 @@ import gradio as gr
2
  import pandas as pd
3
  import re
4
  import unicodedata
5
- import io
6
  import tempfile
7
 
8
- # ---------- Helper Functions ----------
9
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
10
  def clean_text(s: str) -> str:
11
- """Remove unwanted characters and normalize."""
12
  if pd.isna(s):
13
  return ""
14
  s = str(s).replace("ÿ", "")
@@ -16,63 +37,81 @@ def clean_text(s: str) -> str:
16
  s = "".join(ch for ch in s if 32 <= ord(ch) <= 126)
17
  return s.strip()
18
 
19
- def format_zip(zip_code):
20
- """Pad ZIP codes to 5 digits."""
21
  if pd.isna(zip_code):
22
  return ""
23
- z = str(zip_code).strip()
24
- z = re.sub(r"[^\d]", "", z)
25
  if not z:
26
  return ""
27
  return z.zfill(5)[:5]
28
 
29
  def flow_address_lines(lines, maxlen=35, maxlines=3):
30
- """Split long address lines into multiple lines."""
31
  tokens = []
32
  for ln in lines:
33
  txt = clean_text(ln)
34
  if txt:
35
  tokens.extend(txt.split())
36
  out = ["", "", ""]
37
- line_i = 0
38
  for tok in tokens:
39
  while len(tok) > maxlen:
40
  chunk, tok = tok[:maxlen], tok[maxlen:]
41
- if line_i >= maxlines:
42
- return out
43
- if out[line_i]:
44
- line_i += 1
45
- if line_i >= maxlines:
46
- return out
47
- out[line_i] = chunk
48
- line_i += 1
49
- if line_i >= maxlines:
50
- return out
51
- if line_i >= maxlines:
52
- return out
53
- add_len = len(tok) if not out[line_i] else len(tok) + 1
54
- if len(out[line_i]) + add_len <= maxlen:
55
- out[line_i] = (out[line_i] + (" " if out[line_i] else "") + tok).strip()
56
  else:
57
- line_i += 1
58
- if line_i >= maxlines:
59
- return out
60
- out[line_i] = tok
61
- return [ln[:maxlen] for ln in out]
62
-
63
- def convert_dry_ice_kg(x):
64
- """Convert lbs kg and round."""
65
- if pd.isna(x) or str(x).strip() == "":
66
- return ""
67
- try:
68
- kg = round(float(str(x).strip()) / 2.2)
69
- return str(int(kg))
70
- except:
71
- return ""
72
-
73
- # ---------- Main Cleaning Function ----------
74
-
75
- def clean_csv(file):
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
76
  try:
77
  df = pd.read_csv(file.name, encoding="latin1")
78
  except Exception:
@@ -80,58 +119,87 @@ def clean_csv(file):
80
 
81
  df.columns = df.columns.str.strip()
82
 
83
- # ZIP correction
84
- if "ZipCode" in df.columns:
85
- df["ZipCode"] = df["ZipCode"].map(format_zip)
86
-
87
- # Split address lines
88
- addr1, addr2, addr3 = [], [], []
89
  for _, row in df.iterrows():
90
  a1, a2, a3 = flow_address_lines([
91
- row.get("Address1", ""), row.get("Address2", ""), row.get("Address3", "")
92
  ])
93
- addr1.append(a1)
94
- addr2.append(a2)
95
- addr3.append(a3)
96
- df["Address1"] = addr1
97
- df["Address2"] = addr2
98
- df["Address3"] = addr3
99
-
100
- # Clean key fields
101
- for col in ["Company Name", "Contact Name", "City", "State", "Phone Number", "Email"]:
102
- if col in df.columns:
103
- df[col] = df[col].map(clean_text)
104
-
105
- # Convert Dry Ice Weight
106
- if "Dry Ice Weight" in df.columns:
107
- df["Dry Ice Weight (kg)"] = df["Dry Ice Weight"].map(convert_dry_ice_kg)
108
-
109
- # Save to a temp file for download
110
- temp_file = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
111
- df.to_csv(temp_file.name, index=False, encoding="utf-8-sig")
112
- temp_file.close()
113
-
114
- return temp_file.name # Return single file path
115
-
116
- # ---------- Gradio UI ----------
117
-
118
- title = "UPS Shipment CSV Cleaner"
119
- description = """
120
- Upload your **raw shipment CSV file**.
121
- The tool will:
122
- - Remove bad characters (e.g. ÿ)
123
- - Pad ZIP codes to 5 digits
124
- - Split long addresses into 35-character lines
125
- - Convert Dry Ice Weight (lbs kg)
126
- Then download your cleaned CSV ready for UPS Batch import.
127
- """
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
128
 
129
  demo = gr.Interface(
130
- fn=clean_csv,
131
- inputs=gr.File(label="📤 Upload CSV File"),
132
- outputs=gr.File(label="📥 Download Cleaned CSV"),
133
- title=title,
134
- description=description,
135
  allow_flagging="never"
136
  )
137
 
 
2
  import pandas as pd
3
  import re
4
  import unicodedata
 
5
  import tempfile
6
 
7
+ # ---------- UPS TARGET COLUMN ORDER (NO HEADER) ----------
8
+ TARGET_COLUMNS = [
9
+ "Contact Name","Company or Name","Country","Address 1","Address 2","Address 3","City",
10
+ "State/Prov/Other","Postal Code","Telephone","Ext","Residential Ind","Consignee Email",
11
+ "Packaging Type","Customs Value","Weight","Length","Width","Height","Unit of Measure",
12
+ "Description of Goods","Documents of No Commercial Value","GNIFC","Pkg Decl Value",
13
+ "Service","Delivery Confirm","Shipper Release","Ret of Documents","Saturday Deliver",
14
+ "Carbon Neutral","Large Package","Addl handling","Reference 1","Reference 2","Reference 3",
15
+ "QV Notif 1-Addr","QV Notif 1-Ship","QV Notif 1-Excp","QV Notif 1-Delv",
16
+ "QV Notif 2-Addr","QV Notif 2-Ship","QV Notif 2-Excp","QV Notif 2-Delv",
17
+ "QV Notif 3-Addr","QV Notif 3-Ship","QV Notif 3-Excp","QV Notif 3-Delv",
18
+ "QV Notif 4-Addr","QV Notif 4-Ship","QV Notif 4-Excp","QV Notif 4-Delv",
19
+ "QV Notif 5-Addr","QV Notif 5-Ship","QV Notif 5-Excp","QV Notif 5-Delv",
20
+ "QV Notif Msg","QV Failure Addr","UPS Premium Care","ADL Location ID","ADL Media Type",
21
+ "ADL Language","ADL Notification Addr","ADL Failure Addr","ADL COD Value",
22
+ "ADL Deliver to Addressee","ADL Shipper Media Type","ADL Shipper Language",
23
+ "ADL Shipper Notification Addr","ADL Direct Delivery Only",
24
+ "Electronic Package Release Authentication","Lithium Ion Alone","Lithium Ion In Equipment",
25
+ "Lithium Ion With_Equipment","Lithium Metal Alone","Lithium Metal In Equipment",
26
+ "Lithium Metal With Equipment","Weekend Commercial Delivery","Dry Ice Weight",
27
+ "Merchandise Description","UPS Ground Saver Limited Quantity/Lithium Battery"
28
+ ]
29
+
30
+ # ---------- HELPERS ----------
31
  def clean_text(s: str) -> str:
32
+ """Remove 'ÿ', control chars and normalize to printable ASCII."""
33
  if pd.isna(s):
34
  return ""
35
  s = str(s).replace("ÿ", "")
 
37
  s = "".join(ch for ch in s if 32 <= ord(ch) <= 126)
38
  return s.strip()
39
 
40
+ def format_zip(zip_code) -> str:
41
+ """Pad to 5 digits; strip non-digits first."""
42
  if pd.isna(zip_code):
43
  return ""
44
+ z = re.sub(r"[^\d]", "", str(zip_code).strip())
 
45
  if not z:
46
  return ""
47
  return z.zfill(5)[:5]
48
 
49
  def flow_address_lines(lines, maxlen=35, maxlines=3):
50
+ """Word-aware wrap into up to 3 lines, hard-splitting very long tokens."""
51
  tokens = []
52
  for ln in lines:
53
  txt = clean_text(ln)
54
  if txt:
55
  tokens.extend(txt.split())
56
  out = ["", "", ""]
57
+ i = 0
58
  for tok in tokens:
59
  while len(tok) > maxlen:
60
  chunk, tok = tok[:maxlen], tok[maxlen:]
61
+ if i >= maxlines:
62
+ return [s[:maxlen] for s in out]
63
+ if out[i]:
64
+ i += 1
65
+ if i >= maxlines:
66
+ return [s[:maxlen] for s in out]
67
+ out[i] = chunk
68
+ i += 1
69
+ if i >= maxlines:
70
+ return [s[:maxlen] for s in out]
71
+ if i >= maxlines:
72
+ return [s[:maxlen] for s in out]
73
+ add_len = len(tok) if not out[i] else len(tok) + 1
74
+ if len(out[i]) + add_len <= maxlen:
75
+ out[i] = (out[i] + (" " if out[i] else "") + tok).strip()
76
  else:
77
+ i += 1
78
+ if i >= maxlines:
79
+ return [s[:maxlen] for s in out]
80
+ out[i] = tok
81
+ return [s[:maxlen] for s in out]
82
+
83
+ def to_str_series(df, colname):
84
+ """Return a cleaned string Series for an existing column, else blanks."""
85
+ if colname in df.columns:
86
+ return df[colname].apply(lambda x: clean_text(x))
87
+ return pd.Series([""] * len(df))
88
+
89
+ def to_num_str_series(df, colname):
90
+ """Return numeric-looking strings (or blanks) for an existing column."""
91
+ if colname in df.columns:
92
+ return df[colname].apply(lambda x: "" if pd.isna(x) or str(x).strip()=="" else str(x).strip())
93
+ return pd.Series([""] * len(df))
94
+
95
+ def dry_ice_lbs_to_kg_str(df, colname):
96
+ if colname in df.columns:
97
+ def conv(x):
98
+ if pd.isna(x) or str(x).strip()=="":
99
+ return ""
100
+ try:
101
+ return str(int(round(float(str(x).strip())/2.2)))
102
+ except:
103
+ return ""
104
+ return df[colname].apply(conv)
105
+ return pd.Series([""] * len(df))
106
+
107
+ def zip_series(df, colname):
108
+ if colname in df.columns:
109
+ return df[colname].apply(format_zip)
110
+ return pd.Series([""] * len(df))
111
+
112
+ # ---------- CORE PROCESS ----------
113
+ def build_ups_batch_no_header(file):
114
+ # Load CSV with fallback encodings
115
  try:
116
  df = pd.read_csv(file.name, encoding="latin1")
117
  except Exception:
 
119
 
120
  df.columns = df.columns.str.strip()
121
 
122
+ # Address wrap (≤35 chars each)
123
+ a1_list, a2_list, a3_list = [], [], []
 
 
 
 
124
  for _, row in df.iterrows():
125
  a1, a2, a3 = flow_address_lines([
126
+ row.get("Address1",""), row.get("Address2",""), row.get("Address3","")
127
  ])
128
+ a1_list.append(a1); a2_list.append(a2); a3_list.append(a3)
129
+
130
+ # Build output strictly in TARGET_COLUMNS order
131
+ out = pd.DataFrame({c: [""] * len(df) for c in TARGET_COLUMNS})
132
+
133
+ # Required / mapped fields
134
+ out["Contact Name"] = to_str_series(df, "Contact Name")
135
+ out["Company or Name"] = to_str_series(df, "Company Name")
136
+ out["Country"] = "US"
137
+ out["Address 1"] = pd.Series(a1_list)
138
+ out["Address 2"] = pd.Series(a2_list)
139
+ out["Address 3"] = pd.Series(a3_list)
140
+ out["City"] = to_str_series(df, "City")
141
+ out["State/Prov/Other"] = to_str_series(df, "State")
142
+ out["Postal Code"] = zip_series(df, "ZipCode")
143
+ out["Telephone"] = to_str_series(df, "Phone Number")
144
+ out["Consignee Email"] = to_str_series(df, "Email")
145
+
146
+ # Dimensions / weight
147
+ out["Weight"] = to_num_str_series(df, "Weight")
148
+ out["Length"] = to_num_str_series(df, "Length")
149
+ out["Width"] = to_num_str_series(df, "Width")
150
+ out["Height"] = to_num_str_series(df, "Height")
151
+
152
+ # Fixed UPS details per your rules
153
+ out["Packaging Type"] = "2" # not "02"
154
+ out["Service"] = "01" # include leading zero
155
+ out["Delivery Confirm"] = "S"
156
+ out["Description of Goods"] = "Dry Ice Biological Shipment"
157
+ out["Merchandise Description"]= "Dry Ice Biological Shipment"
158
+ out["ADL Language"] = "" # blank
159
+
160
+ # Dry ice conversion (lbs -> kg, rounded)
161
+ out["Dry Ice Weight"] = dry_ice_lbs_to_kg_str(df, "Dry Ice Weight")
162
+
163
+ # References mapping
164
+ out["Reference 1"] = to_num_str_series(df, "PO Number")
165
+ out["Reference 2"] = to_num_str_series(df, "Invoice Number")
166
+ out["Reference 3"] = to_num_str_series(df, "Customer Reference")
167
+
168
+ # QV Notification flags/addresses
169
+ out["QV Notif 1-Addr"] = to_str_series(df, "Email") # recipient email
170
+ out["QV Notif 1-Ship"] = "1"
171
+ out["QV Notif 1-Excp"] = "1"
172
+ out["QV Notif 1-Delv"] = "1"
173
+
174
+ out["QV Notif 2-Addr"] = "shaqdong@apexglobe.com"
175
+ out["QV Notif 2-Ship"] = "1"
176
+ out["QV Notif 2-Excp"] = "1"
177
+ out["QV Notif 2-Delv"] = "1"
178
+
179
+ # All other columns remain blank by default (already created)
180
+
181
+ # Export to a temp file with NO HEADER
182
+ tmp = tempfile.NamedTemporaryFile(delete=False, suffix=".csv")
183
+ out.to_csv(tmp.name, index=False, header=False, encoding="utf-8-sig")
184
+ tmp.close()
185
+ return tmp.name
186
+
187
+ # ---------- GRADIO UI ----------
188
+ TITLE = "UPS Batch CSV Converter (Import-ready, No Header)"
189
+ DESC = (
190
+ "Upload your shipment CSV. The app will clean and convert it to UPS Batch format "
191
+ "(**exact column order** and **no header**), including: ZIP padding, address wrap ≤35 chars, "
192
+ "removing stray characters (e.g. ÿ), converting Dry Ice Weight (lbs→kg, rounded), "
193
+ "Service=01, Packaging Type=2, Delivery Confirm=S, QV Notif flags=1, QV Notif 1-Addr from Email, "
194
+ "QV Notif 2-Addr fixed to shaqdong@apexglobe.com, ADL Language blank."
195
+ )
196
 
197
  demo = gr.Interface(
198
+ fn=build_ups_batch_no_header,
199
+ inputs=gr.File(label="📤 Upload Source CSV"),
200
+ outputs=gr.File(label="📥 Download UPS Import-Ready CSV (No Header)"),
201
+ title=TITLE,
202
+ description=DESC,
203
  allow_flagging="never"
204
  )
205