joycecast commited on
Commit
5c65f49
·
verified ·
1 Parent(s): 612ad1b

Upload 8 files

Browse files
constants.py ADDED
@@ -0,0 +1,10 @@
 
 
 
 
 
 
 
 
 
 
 
1
+ """
2
+ Constants for timezone mappings and configuration
3
+ """
4
+
5
+ POD_TIMEZONE_MAPPINGS = {
6
+ "JFK": "US/Eastern",
7
+ "LAX": "US/Pacific",
8
+ "ORD": "US/Central",
9
+ "MIA": "US/Eastern"
10
+ }
entry_milestone_exporter.py CHANGED
@@ -8,15 +8,61 @@ import pandas as pd
8
  import sys
9
  from datetime import datetime
10
  from typing import List, Dict, Optional
 
11
 
12
  # Add src directory to path
13
  sys.path.append('src')
14
 
15
  from notion_integration import NotionIntegration
 
16
 
17
  class EntryMilestoneExporter:
18
  """Class to export entry milestone data to Excel format."""
19
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
20
  # Define all Excel columns based on the template
21
  EXCEL_COLUMNS = [
22
  'MAWB', 'Entry', 'POD', 'handover_doc', 'receipt_scan', 'CBP Line',
@@ -62,8 +108,8 @@ class EntryMilestoneExporter:
62
 
63
  return unique_mawbs
64
 
65
- def extract_entry_data(self, entry_record: Dict) -> Dict:
66
- """Extract relevant data from a Notion entry record."""
67
  properties = entry_record.get("properties", {})
68
  data = {}
69
 
@@ -75,7 +121,7 @@ class EntryMilestoneExporter:
75
  if "entry_number" in properties and properties["entry_number"].get("rich_text"):
76
  data["entry_number"] = properties["entry_number"]["rich_text"][0].get("text", {}).get("content", "")
77
 
78
- # Extract dates
79
  date_fields = [
80
  "ams_submit_date", "entry_submit_date", "cbp_hold_date", "cbp_release_date",
81
  "cpsc_review_date", "cpsc_release_date"
@@ -87,7 +133,9 @@ class EntryMilestoneExporter:
87
  if date_str:
88
  try:
89
  dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
90
- data[field] = dt.replace(tzinfo=None)
 
 
91
  except:
92
  data[field] = date_str
93
  else:
@@ -245,7 +293,16 @@ class EntryMilestoneExporter:
245
  # Process data
246
  excel_rows = []
247
  for entry in entries:
248
- entry_data = self.extract_entry_data(entry)
 
 
 
 
 
 
 
 
 
249
  mawb = entry_data.get("mawb", "")
250
 
251
  if mawb in mawb_info:
 
8
  import sys
9
  from datetime import datetime
10
  from typing import List, Dict, Optional
11
+ import pytz
12
 
13
  # Add src directory to path
14
  sys.path.append('src')
15
 
16
  from notion_integration import NotionIntegration
17
+ from constants import POD_TIMEZONE_MAPPINGS
18
 
19
  class EntryMilestoneExporter:
20
  """Class to export entry milestone data to Excel format."""
21
 
22
+ @staticmethod
23
+ def convert_utc_to_pod_timezone(utc_dt: datetime, pod: Optional[str]) -> datetime:
24
+ """
25
+ Convert UTC datetime to POD local timezone without timezone info.
26
+
27
+ Args:
28
+ utc_dt: UTC datetime object
29
+ pod: Port of discharge (JFK, LAX, ORD, MIA)
30
+
31
+ Returns:
32
+ Timezone-naive datetime in POD local time (or UTC if POD not found)
33
+ """
34
+ if not utc_dt or pd.isna(utc_dt):
35
+ return utc_dt
36
+
37
+ if not pod or pod not in POD_TIMEZONE_MAPPINGS:
38
+ # Keep as UTC if POD is missing or not in mappings
39
+ if hasattr(utc_dt, 'tzinfo') and utc_dt.tzinfo is not None:
40
+ return utc_dt.replace(tzinfo=None)
41
+ return utc_dt
42
+
43
+ try:
44
+ # Ensure UTC timezone
45
+ if hasattr(utc_dt, 'tzinfo'):
46
+ if utc_dt.tzinfo is None:
47
+ utc_dt = pytz.utc.localize(utc_dt)
48
+ else:
49
+ utc_dt = utc_dt.astimezone(pytz.utc)
50
+ else:
51
+ utc_dt = pytz.utc.localize(utc_dt)
52
+
53
+ # Convert to POD timezone
54
+ timezone_name = POD_TIMEZONE_MAPPINGS[pod]
55
+ pod_tz = pytz.timezone(timezone_name)
56
+ local_dt = utc_dt.astimezone(pod_tz)
57
+
58
+ # Remove timezone info
59
+ return local_dt.replace(tzinfo=None)
60
+ except:
61
+ # Fallback to UTC if conversion fails
62
+ if hasattr(utc_dt, 'tzinfo') and utc_dt.tzinfo is not None:
63
+ return utc_dt.replace(tzinfo=None)
64
+ return utc_dt
65
+
66
  # Define all Excel columns based on the template
67
  EXCEL_COLUMNS = [
68
  'MAWB', 'Entry', 'POD', 'handover_doc', 'receipt_scan', 'CBP Line',
 
108
 
109
  return unique_mawbs
110
 
111
+ def extract_entry_data(self, entry_record: Dict, pod: Optional[str] = None) -> Dict:
112
+ """Extract relevant data from a Notion entry record with timezone conversion."""
113
  properties = entry_record.get("properties", {})
114
  data = {}
115
 
 
121
  if "entry_number" in properties and properties["entry_number"].get("rich_text"):
122
  data["entry_number"] = properties["entry_number"]["rich_text"][0].get("text", {}).get("content", "")
123
 
124
+ # Extract dates with timezone conversion
125
  date_fields = [
126
  "ams_submit_date", "entry_submit_date", "cbp_hold_date", "cbp_release_date",
127
  "cpsc_review_date", "cpsc_release_date"
 
133
  if date_str:
134
  try:
135
  dt = datetime.fromisoformat(date_str.replace('Z', '+00:00'))
136
+ dt_naive = dt.replace(tzinfo=None)
137
+ # Convert to POD timezone
138
+ data[field] = self.convert_utc_to_pod_timezone(dt_naive, pod)
139
  except:
140
  data[field] = date_str
141
  else:
 
293
  # Process data
294
  excel_rows = []
295
  for entry in entries:
296
+ # Get MAWB to look up POD
297
+ mawb_for_lookup = ""
298
+ if "mawb" in entry.get("properties", {}) and entry["properties"]["mawb"].get("title"):
299
+ mawb_for_lookup = entry["properties"]["mawb"]["title"][0].get("text", {}).get("content", "")
300
+
301
+ # Get POD from mawb_info
302
+ pod = mawb_info.get(mawb_for_lookup, {}).get("pod", None) if mawb_for_lookup else None
303
+
304
+ # Extract entry data with POD timezone conversion
305
+ entry_data = self.extract_entry_data(entry, pod)
306
  mawb = entry_data.get("mawb", "")
307
 
308
  if mawb in mawb_info:
mawb_milestone_exporter.py CHANGED
@@ -8,11 +8,13 @@ import sys
8
  import pandas as pd
9
  from datetime import datetime
10
  from typing import List, Dict, Optional
 
11
 
12
  # Add src directory to path
13
  sys.path.append('src')
14
 
15
  from notion_integration import NotionIntegration
 
16
 
17
 
18
  class MAWBMilestoneExporter:
@@ -22,6 +24,50 @@ class MAWBMilestoneExporter:
22
  """Initialize with Notion integration instance."""
23
  self.notion = notion_integration
24
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
  def parse_mawb_input(self, mawb_input: str) -> List[str]:
26
  """Parse MAWB input string into a list of MAWBs."""
27
  if not mawb_input or not mawb_input.strip():
@@ -260,28 +306,32 @@ class MAWBMilestoneExporter:
260
  if cpsc_release_dt:
261
  cpsc_release_dates_for_applicable.append(cpsc_release_dt)
262
 
263
- # Apply aggregation rules
264
- result["T01 Entry Submission"] = min(entry_submit_dates) if entry_submit_dates else None
265
- result["T01 CBP Exam"] = min(cbp_hold_dates) if cbp_hold_dates else None
266
- result["T01 CPSC/PGA Review"] = min(cpsc_review_dates) if cpsc_review_dates else None
 
 
 
267
 
268
- # Conditional aggregation
269
  if total_entries > 0 and len(cbp_release_dates) == total_entries:
270
- result["T01 CBP Release"] = max(cbp_release_dates)
271
  else:
272
  result["T01 CBP Release"] = None
273
 
274
  if len(cpsc_applicable_entries) > 0 and len(cpsc_release_dates_for_applicable) == len(cpsc_applicable_entries):
275
- result["T01 CPSC/PGA release"] = max(cpsc_release_dates_for_applicable)
 
276
  else:
277
  result["T01 CPSC/PGA release"] = None
278
 
279
  if total_entries > 0 and len(final_release_dates) == total_entries:
280
- result["Customs Release"] = max(final_release_dates)
281
  else:
282
  result["Customs Release"] = None
283
 
284
- # Aggregate milestone record data
285
  warehouse_in_dates = []
286
  cargo_ready_dates = []
287
  warehouse_out_dates = []
@@ -294,9 +344,12 @@ class MAWBMilestoneExporter:
294
  if dt := self.extract_datetime_from_notion_field(milestone_record, "warehouse_out"):
295
  warehouse_out_dates.append(dt)
296
 
297
- result["PDA scan"] = min(warehouse_in_dates) if warehouse_in_dates else None
298
- result["Cargo ready for pickup"] = min(cargo_ready_dates) if cargo_ready_dates else None
299
- result["Ship out completed"] = min(warehouse_out_dates) if warehouse_out_dates else None
 
 
 
300
 
301
  return result
302
 
 
8
  import pandas as pd
9
  from datetime import datetime
10
  from typing import List, Dict, Optional
11
+ import pytz
12
 
13
  # Add src directory to path
14
  sys.path.append('src')
15
 
16
  from notion_integration import NotionIntegration
17
+ from constants import POD_TIMEZONE_MAPPINGS
18
 
19
 
20
  class MAWBMilestoneExporter:
 
24
  """Initialize with Notion integration instance."""
25
  self.notion = notion_integration
26
 
27
+ @staticmethod
28
+ def convert_utc_to_pod_timezone(utc_dt: datetime, pod: Optional[str]) -> datetime:
29
+ """
30
+ Convert UTC datetime to POD local timezone without timezone info.
31
+
32
+ Args:
33
+ utc_dt: UTC datetime object
34
+ pod: Port of discharge (JFK, LAX, ORD, MIA)
35
+
36
+ Returns:
37
+ Timezone-naive datetime in POD local time (or UTC if POD not found)
38
+ """
39
+ if not utc_dt or pd.isna(utc_dt):
40
+ return utc_dt
41
+
42
+ if not pod or pod not in POD_TIMEZONE_MAPPINGS:
43
+ # Keep as UTC if POD is missing or not in mappings
44
+ if hasattr(utc_dt, 'tzinfo') and utc_dt.tzinfo is not None:
45
+ return utc_dt.replace(tzinfo=None)
46
+ return utc_dt
47
+
48
+ try:
49
+ # Ensure UTC timezone
50
+ if hasattr(utc_dt, 'tzinfo'):
51
+ if utc_dt.tzinfo is None:
52
+ utc_dt = pytz.utc.localize(utc_dt)
53
+ else:
54
+ utc_dt = utc_dt.astimezone(pytz.utc)
55
+ else:
56
+ utc_dt = pytz.utc.localize(utc_dt)
57
+
58
+ # Convert to POD timezone
59
+ timezone_name = POD_TIMEZONE_MAPPINGS[pod]
60
+ pod_tz = pytz.timezone(timezone_name)
61
+ local_dt = utc_dt.astimezone(pod_tz)
62
+
63
+ # Remove timezone info
64
+ return local_dt.replace(tzinfo=None)
65
+ except:
66
+ # Fallback to UTC if conversion fails
67
+ if hasattr(utc_dt, 'tzinfo') and utc_dt.tzinfo is not None:
68
+ return utc_dt.replace(tzinfo=None)
69
+ return utc_dt
70
+
71
  def parse_mawb_input(self, mawb_input: str) -> List[str]:
72
  """Parse MAWB input string into a list of MAWBs."""
73
  if not mawb_input or not mawb_input.strip():
 
306
  if cpsc_release_dt:
307
  cpsc_release_dates_for_applicable.append(cpsc_release_dt)
308
 
309
+ # Apply aggregation rules and convert to POD timezone
310
+ result["T01 Entry Submission"] = self.convert_utc_to_pod_timezone(
311
+ min(entry_submit_dates), pod) if entry_submit_dates else None
312
+ result["T01 CBP Exam"] = self.convert_utc_to_pod_timezone(
313
+ min(cbp_hold_dates), pod) if cbp_hold_dates else None
314
+ result["T01 CPSC/PGA Review"] = self.convert_utc_to_pod_timezone(
315
+ min(cpsc_review_dates), pod) if cpsc_review_dates else None
316
 
317
+ # Conditional aggregation with timezone conversion
318
  if total_entries > 0 and len(cbp_release_dates) == total_entries:
319
+ result["T01 CBP Release"] = self.convert_utc_to_pod_timezone(max(cbp_release_dates), pod)
320
  else:
321
  result["T01 CBP Release"] = None
322
 
323
  if len(cpsc_applicable_entries) > 0 and len(cpsc_release_dates_for_applicable) == len(cpsc_applicable_entries):
324
+ result["T01 CPSC/PGA release"] = self.convert_utc_to_pod_timezone(
325
+ max(cpsc_release_dates_for_applicable), pod)
326
  else:
327
  result["T01 CPSC/PGA release"] = None
328
 
329
  if total_entries > 0 and len(final_release_dates) == total_entries:
330
+ result["Customs Release"] = self.convert_utc_to_pod_timezone(max(final_release_dates), pod)
331
  else:
332
  result["Customs Release"] = None
333
 
334
+ # Aggregate milestone record data with timezone conversion
335
  warehouse_in_dates = []
336
  cargo_ready_dates = []
337
  warehouse_out_dates = []
 
344
  if dt := self.extract_datetime_from_notion_field(milestone_record, "warehouse_out"):
345
  warehouse_out_dates.append(dt)
346
 
347
+ result["PDA scan"] = self.convert_utc_to_pod_timezone(
348
+ min(warehouse_in_dates), pod) if warehouse_in_dates else None
349
+ result["Cargo ready for pickup"] = self.convert_utc_to_pod_timezone(
350
+ min(cargo_ready_dates), pod) if cargo_ready_dates else None
351
+ result["Ship out completed"] = self.convert_utc_to_pod_timezone(
352
+ min(warehouse_out_dates), pod) if warehouse_out_dates else None
353
 
354
  return result
355
 
notion_exporter.py CHANGED
@@ -13,11 +13,13 @@ from typing import Dict, List, Optional, Any
13
  import numpy as np
14
  import time
15
  import random
 
16
 
17
  # Add src directory to Python path
18
  sys.path.append('src')
19
 
20
  from notion_integration import NotionIntegration
 
21
 
22
 
23
  def load_notion_config_from_env() -> Dict:
@@ -51,6 +53,50 @@ def load_notion_config_from_env() -> Dict:
51
  return config
52
 
53
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
54
  class NotionExcelExporter:
55
  def __init__(self, slow_mode: bool = False, page_size: int = 50):
56
  """Initialize the exporter with Notion configuration from environment."""
@@ -337,6 +383,10 @@ class NotionExcelExporter:
337
 
338
  for record in records:
339
  props = record.get('properties', {})
 
 
 
 
340
  row = {
341
  'ID': record.get('id', ''),
342
  }
@@ -357,13 +407,13 @@ class NotionExcelExporter:
357
  elif 'select' in prop_data:
358
  row[prop_name] = self._extract_select(prop_data)
359
  elif 'date' in prop_data:
360
- row[prop_name] = self._format_datetime(prop_data)
361
  elif 'checkbox' in prop_data:
362
  row[prop_name] = self._extract_checkbox(prop_data)
363
  elif 'rollup' in prop_data:
364
  row[prop_name] = self._extract_rollup(prop_data)
365
  elif 'formula' in prop_data:
366
- row[prop_name] = self._extract_formula(prop_data)
367
  elif 'relation' in prop_data:
368
  # Extract relation as count
369
  relation_list = prop_data.get('relation', [])
@@ -392,16 +442,16 @@ class NotionExcelExporter:
392
  elif 'last_edited_by' in prop_data:
393
  row[prop_name] = self._extract_last_edited_by(prop_data)
394
  elif 'created_time' in prop_data:
395
- row[prop_name] = self._format_datetime_iso(prop_data.get('created_time', ''))
396
  elif 'last_edited_time' in prop_data:
397
- row[prop_name] = self._format_datetime_iso(prop_data.get('last_edited_time', ''))
398
  else:
399
  # Unknown property type, store as string
400
  row[prop_name] = str(prop_data)
401
 
402
- # Add system metadata fields
403
- row['Created'] = self._format_datetime_iso(record.get('created_time', ''))
404
- row['Last Modified'] = self._format_datetime_iso(record.get('last_edited_time', ''))
405
 
406
  data.append(row)
407
 
@@ -413,22 +463,26 @@ class NotionExcelExporter:
413
 
414
  for record in records:
415
  props = record.get('properties', {})
 
 
 
 
416
  row = {
417
  'ID': record.get('id', ''),
418
  'Entry Number': self._extract_rich_text(props.get('entry_number', {})),
419
  'MAWB': self._extract_title(props.get('mawb', {})),
420
- 'Entry Submit Date': self._format_datetime(props.get('entry_submit_date', {})),
421
- 'CBP Release Date': self._format_datetime(props.get('cbp_release_date', {})),
422
- 'CBP Hold Date': self._format_datetime(props.get('cbp_hold_date', {})),
423
  'CBP Hold Line': self._extract_rich_text(props.get('cbp_hold_line', {})),
424
- 'CPSC Review Date': self._format_datetime(props.get('cpsc_review_date', {})),
425
  'CPSC Review Line': self._extract_rich_text(props.get('cpsc_review_line', {})),
426
- 'CPSC Release Date': self._format_datetime(props.get('cpsc_release_date', {})),
427
- 'Estimated Review Date': self._format_datetime(props.get('estimated_review_date', {})),
428
  'Milestone Task Status': self._extract_rollup(props.get('milestone_task_status', {})),
429
- 'Final Release': self._extract_formula(props.get('final_release', {})),
430
- 'Created': self._format_datetime_iso(record.get('created_time', '')),
431
- 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', ''))
432
  }
433
  data.append(row)
434
 
@@ -440,6 +494,10 @@ class NotionExcelExporter:
440
 
441
  for record in records:
442
  props = record.get('properties', {})
 
 
 
 
443
  row = {
444
  'ID': record.get('id', ''),
445
  'MAWB': self._extract_title(props.get('mawb', {})),
@@ -447,8 +505,8 @@ class NotionExcelExporter:
447
  'Customer': self._extract_rich_text(props.get('customer', {})),
448
  'Entry Type': self._extract_rich_text(props.get('entry_type', {})),
449
  'Done': self._extract_checkbox(props.get('done', {})),
450
- 'Created': self._format_datetime_iso(record.get('created_time', '')),
451
- 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', '')),
452
  'Last Edited By': self._extract_last_edited_by(props.get('Last edited by', {}))
453
  }
454
  data.append(row)
@@ -461,6 +519,10 @@ class NotionExcelExporter:
461
 
462
  for record in records:
463
  props = record.get('properties', {})
 
 
 
 
464
  row = {
465
  'ID': record.get('id', ''),
466
  'MAWB': self._extract_title(props.get('mawb', {})),
@@ -469,15 +531,15 @@ class NotionExcelExporter:
469
  'Pallets': self._extract_number(props.get('pallets', {})),
470
  'GWT': self._extract_number(props.get('gwt', {})),
471
  'CWT': self._extract_number(props.get('cwt', {})),
472
- 'Warehouse In': self._format_datetime(props.get('warehouse_in', {})),
473
- 'Cargo Ready': self._format_datetime(props.get('cargo_ready', {})),
474
- 'Warehouse Out': self._format_datetime(props.get('warehouse_out', {})),
475
- 'ETD': self._format_datetime(props.get('etd', {})),
476
- 'ETA': self._format_datetime(props.get('eta', {})),
477
  'Cartons Left at Airline': self._extract_number(props.get('cartons_left_at_airline', {})),
478
  'Cartons Left at Warehouse': self._extract_number(props.get('cartons_left_at_warehouse', {})),
479
- 'Created': self._format_datetime_iso(record.get('created_time', '')),
480
- 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', ''))
481
  }
482
  data.append(row)
483
 
@@ -550,8 +612,8 @@ class NotionExcelExporter:
550
 
551
  return ''
552
 
553
- def _extract_formula(self, prop: Dict) -> str:
554
- """Extract formula property value - return computed result."""
555
  if not prop or 'formula' not in prop:
556
  return ''
557
  formula_obj = prop['formula']
@@ -568,11 +630,19 @@ class NotionExcelExporter:
568
  elif formula_obj.get('type') == 'date':
569
  date_obj = formula_obj.get('date')
570
  if date_obj and date_obj.get('start'):
571
- return date_obj['start']
 
 
 
 
 
 
 
 
572
  return ''
573
 
574
- def _format_datetime(self, prop: Dict) -> pd.Timestamp:
575
- """Extract date property value and convert to Excel-friendly datetime."""
576
  if not prop or 'date' not in prop:
577
  return pd.NaT
578
  date_obj = prop['date']
@@ -583,12 +653,13 @@ class NotionExcelExporter:
583
  # Remove timezone info for Excel compatibility
584
  if dt.tz is not None:
585
  dt = dt.tz_convert('UTC').tz_localize(None)
586
- return dt
 
587
  except (ValueError, TypeError):
588
  return pd.NaT
589
 
590
- def _format_datetime_iso(self, iso_string: str) -> pd.Timestamp:
591
- """Convert ISO datetime string to Excel-friendly datetime."""
592
  if not iso_string:
593
  return pd.NaT
594
  try:
@@ -596,7 +667,8 @@ class NotionExcelExporter:
596
  # Remove timezone info for Excel compatibility
597
  if dt.tz is not None:
598
  dt = dt.tz_convert('UTC').tz_localize(None)
599
- return dt
 
600
  except (ValueError, TypeError):
601
  return pd.NaT
602
 
 
13
  import numpy as np
14
  import time
15
  import random
16
+ import pytz
17
 
18
  # Add src directory to Python path
19
  sys.path.append('src')
20
 
21
  from notion_integration import NotionIntegration
22
+ from constants import POD_TIMEZONE_MAPPINGS
23
 
24
 
25
  def load_notion_config_from_env() -> Dict:
 
53
  return config
54
 
55
 
56
+ def convert_utc_to_pod_timezone(utc_dt: datetime, pod: Optional[str]) -> datetime:
57
+ """
58
+ Convert UTC datetime to POD local timezone without timezone info.
59
+
60
+ Args:
61
+ utc_dt: UTC datetime object
62
+ pod: Port of discharge (JFK, LAX, ORD, MIA)
63
+
64
+ Returns:
65
+ Timezone-naive datetime in POD local time (or UTC if POD not found)
66
+ """
67
+ if not utc_dt or pd.isna(utc_dt):
68
+ return utc_dt
69
+
70
+ if not pod or pod not in POD_TIMEZONE_MAPPINGS:
71
+ # Keep as UTC if POD is missing or not in mappings
72
+ if hasattr(utc_dt, 'tz') and utc_dt.tz is not None:
73
+ return utc_dt.tz_localize(None)
74
+ return utc_dt
75
+
76
+ try:
77
+ # Ensure UTC timezone
78
+ if hasattr(utc_dt, 'tz'):
79
+ if utc_dt.tz is None:
80
+ utc_dt = pytz.utc.localize(utc_dt)
81
+ else:
82
+ utc_dt = utc_dt.tz_convert(pytz.utc)
83
+ else:
84
+ utc_dt = pytz.utc.localize(utc_dt)
85
+
86
+ # Convert to POD timezone
87
+ timezone_name = POD_TIMEZONE_MAPPINGS[pod]
88
+ pod_tz = pytz.timezone(timezone_name)
89
+ local_dt = utc_dt.astimezone(pod_tz)
90
+
91
+ # Remove timezone info
92
+ return local_dt.replace(tzinfo=None)
93
+ except:
94
+ # Fallback to UTC if conversion fails
95
+ if hasattr(utc_dt, 'tz') and utc_dt.tz is not None:
96
+ return utc_dt.tz_localize(None)
97
+ return utc_dt
98
+
99
+
100
  class NotionExcelExporter:
101
  def __init__(self, slow_mode: bool = False, page_size: int = 50):
102
  """Initialize the exporter with Notion configuration from environment."""
 
383
 
384
  for record in records:
385
  props = record.get('properties', {})
386
+
387
+ # Extract POD for timezone conversion
388
+ pod = self._extract_rich_text(props.get('pod', {}))
389
+
390
  row = {
391
  'ID': record.get('id', ''),
392
  }
 
407
  elif 'select' in prop_data:
408
  row[prop_name] = self._extract_select(prop_data)
409
  elif 'date' in prop_data:
410
+ row[prop_name] = self._format_datetime(prop_data, pod)
411
  elif 'checkbox' in prop_data:
412
  row[prop_name] = self._extract_checkbox(prop_data)
413
  elif 'rollup' in prop_data:
414
  row[prop_name] = self._extract_rollup(prop_data)
415
  elif 'formula' in prop_data:
416
+ row[prop_name] = self._extract_formula(prop_data, pod)
417
  elif 'relation' in prop_data:
418
  # Extract relation as count
419
  relation_list = prop_data.get('relation', [])
 
442
  elif 'last_edited_by' in prop_data:
443
  row[prop_name] = self._extract_last_edited_by(prop_data)
444
  elif 'created_time' in prop_data:
445
+ row[prop_name] = self._format_datetime_iso(prop_data.get('created_time', ''), pod)
446
  elif 'last_edited_time' in prop_data:
447
+ row[prop_name] = self._format_datetime_iso(prop_data.get('last_edited_time', ''), pod)
448
  else:
449
  # Unknown property type, store as string
450
  row[prop_name] = str(prop_data)
451
 
452
+ # Add system metadata fields with timezone conversion
453
+ row['Created'] = self._format_datetime_iso(record.get('created_time', ''), pod)
454
+ row['Last Modified'] = self._format_datetime_iso(record.get('last_edited_time', ''), pod)
455
 
456
  data.append(row)
457
 
 
463
 
464
  for record in records:
465
  props = record.get('properties', {})
466
+
467
+ # Try to extract POD if available in entry record, otherwise None (keep as UTC)
468
+ pod = self._extract_rich_text(props.get('pod', {})) if 'pod' in props else None
469
+
470
  row = {
471
  'ID': record.get('id', ''),
472
  'Entry Number': self._extract_rich_text(props.get('entry_number', {})),
473
  'MAWB': self._extract_title(props.get('mawb', {})),
474
+ 'Entry Submit Date': self._format_datetime(props.get('entry_submit_date', {}), pod),
475
+ 'CBP Release Date': self._format_datetime(props.get('cbp_release_date', {}), pod),
476
+ 'CBP Hold Date': self._format_datetime(props.get('cbp_hold_date', {}), pod),
477
  'CBP Hold Line': self._extract_rich_text(props.get('cbp_hold_line', {})),
478
+ 'CPSC Review Date': self._format_datetime(props.get('cpsc_review_date', {}), pod),
479
  'CPSC Review Line': self._extract_rich_text(props.get('cpsc_review_line', {})),
480
+ 'CPSC Release Date': self._format_datetime(props.get('cpsc_release_date', {}), pod),
481
+ 'Estimated Review Date': self._format_datetime(props.get('estimated_review_date', {}), pod),
482
  'Milestone Task Status': self._extract_rollup(props.get('milestone_task_status', {})),
483
+ 'Final Release': self._extract_formula(props.get('final_release', {}), pod),
484
+ 'Created': self._format_datetime_iso(record.get('created_time', ''), pod),
485
+ 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', ''), pod)
486
  }
487
  data.append(row)
488
 
 
494
 
495
  for record in records:
496
  props = record.get('properties', {})
497
+
498
+ # Try to extract POD if available, otherwise None (keep as UTC)
499
+ pod = self._extract_rich_text(props.get('pod', {})) if 'pod' in props else None
500
+
501
  row = {
502
  'ID': record.get('id', ''),
503
  'MAWB': self._extract_title(props.get('mawb', {})),
 
505
  'Customer': self._extract_rich_text(props.get('customer', {})),
506
  'Entry Type': self._extract_rich_text(props.get('entry_type', {})),
507
  'Done': self._extract_checkbox(props.get('done', {})),
508
+ 'Created': self._format_datetime_iso(record.get('created_time', ''), pod),
509
+ 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', ''), pod),
510
  'Last Edited By': self._extract_last_edited_by(props.get('Last edited by', {}))
511
  }
512
  data.append(row)
 
519
 
520
  for record in records:
521
  props = record.get('properties', {})
522
+
523
+ # Try to extract POD if available, otherwise None (keep as UTC)
524
+ pod = self._extract_rich_text(props.get('pod', {})) if 'pod' in props else None
525
+
526
  row = {
527
  'ID': record.get('id', ''),
528
  'MAWB': self._extract_title(props.get('mawb', {})),
 
531
  'Pallets': self._extract_number(props.get('pallets', {})),
532
  'GWT': self._extract_number(props.get('gwt', {})),
533
  'CWT': self._extract_number(props.get('cwt', {})),
534
+ 'Warehouse In': self._format_datetime(props.get('warehouse_in', {}), pod),
535
+ 'Cargo Ready': self._format_datetime(props.get('cargo_ready', {}), pod),
536
+ 'Warehouse Out': self._format_datetime(props.get('warehouse_out', {}), pod),
537
+ 'ETD': self._format_datetime(props.get('etd', {}), pod),
538
+ 'ETA': self._format_datetime(props.get('eta', {}), pod),
539
  'Cartons Left at Airline': self._extract_number(props.get('cartons_left_at_airline', {})),
540
  'Cartons Left at Warehouse': self._extract_number(props.get('cartons_left_at_warehouse', {})),
541
+ 'Created': self._format_datetime_iso(record.get('created_time', ''), pod),
542
+ 'Last Modified': self._format_datetime_iso(record.get('last_edited_time', ''), pod)
543
  }
544
  data.append(row)
545
 
 
612
 
613
  return ''
614
 
615
+ def _extract_formula(self, prop: Dict, pod: Optional[str] = None):
616
+ """Extract formula property value - return computed result with timezone conversion for dates."""
617
  if not prop or 'formula' not in prop:
618
  return ''
619
  formula_obj = prop['formula']
 
630
  elif formula_obj.get('type') == 'date':
631
  date_obj = formula_obj.get('date')
632
  if date_obj and date_obj.get('start'):
633
+ try:
634
+ dt = pd.to_datetime(date_obj['start'])
635
+ # Remove timezone info for Excel compatibility
636
+ if dt.tz is not None:
637
+ dt = dt.tz_convert('UTC').tz_localize(None)
638
+ # Convert to POD timezone if provided
639
+ return convert_utc_to_pod_timezone(dt, pod)
640
+ except (ValueError, TypeError):
641
+ return pd.NaT
642
  return ''
643
 
644
+ def _format_datetime(self, prop: Dict, pod: Optional[str] = None) -> pd.Timestamp:
645
+ """Extract date property value and convert to Excel-friendly datetime with POD timezone."""
646
  if not prop or 'date' not in prop:
647
  return pd.NaT
648
  date_obj = prop['date']
 
653
  # Remove timezone info for Excel compatibility
654
  if dt.tz is not None:
655
  dt = dt.tz_convert('UTC').tz_localize(None)
656
+ # Convert to POD timezone if provided
657
+ return convert_utc_to_pod_timezone(dt, pod)
658
  except (ValueError, TypeError):
659
  return pd.NaT
660
 
661
+ def _format_datetime_iso(self, iso_string: str, pod: Optional[str] = None) -> pd.Timestamp:
662
+ """Convert ISO datetime string to Excel-friendly datetime with POD timezone."""
663
  if not iso_string:
664
  return pd.NaT
665
  try:
 
667
  # Remove timezone info for Excel compatibility
668
  if dt.tz is not None:
669
  dt = dt.tz_convert('UTC').tz_localize(None)
670
+ # Convert to POD timezone if provided
671
+ return convert_utc_to_pod_timezone(dt, pod)
672
  except (ValueError, TypeError):
673
  return pd.NaT
674