Upload app.py
Browse files
app.py
CHANGED
|
@@ -67,8 +67,7 @@ MANUAL_FEE_TYPES = [
|
|
| 67 |
|
| 68 |
# 归档时需要迁移的属性(按 Notion 字段类型分组)
|
| 69 |
ARCHIVE_RICH_TEXT_FIELDS = [
|
| 70 |
-
"Customer", "POD", "
|
| 71 |
-
"LM Carrier", "AR Completed Date", "AP Completed Date"
|
| 72 |
]
|
| 73 |
ARCHIVE_NUMBER_FIELDS = [
|
| 74 |
"Parcels", "HTS Count", "Entry Count", "CWT",
|
|
@@ -78,7 +77,15 @@ ARCHIVE_NUMBER_FIELDS = [
|
|
| 78 |
"WT Fee", "CES Transfer Fee"
|
| 79 |
]
|
| 80 |
ARCHIVE_CHECKBOX_FIELDS = ["AR", "AP"]
|
| 81 |
-
ARCHIVE_DATE_FIELDS = ["WH-OUT Date"]
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 82 |
|
| 83 |
# ================== 会话与重试配置 ==================
|
| 84 |
session = requests.Session()
|
|
@@ -306,14 +313,17 @@ def extract_mawb_with_id_whout(page):
|
|
| 306 |
|
| 307 |
|
| 308 |
def extract_mawb_with_id_ar_ap(page):
|
| 309 |
-
"""提取 MAWB 及页面 ID、AR/AP
|
| 310 |
title = page["properties"]["MAWB"]["title"]
|
| 311 |
if title:
|
| 312 |
mawb = title[0]["text"]["content"]
|
|
|
|
|
|
|
| 313 |
return mawb, {
|
| 314 |
"id": page["id"],
|
| 315 |
"ar_checked": page["properties"]["AR"]["checkbox"],
|
| 316 |
"ap_checked": page["properties"]["AP"]["checkbox"],
|
|
|
|
| 317 |
}
|
| 318 |
return None, None
|
| 319 |
|
|
@@ -385,14 +395,17 @@ def create_mawb_eta(file, logger=None):
|
|
| 385 |
skipped += 1
|
| 386 |
continue
|
| 387 |
|
| 388 |
-
# -------- ETA
|
| 389 |
-
|
|
|
|
|
|
|
| 390 |
if pd.notna(row["Estimated Time Arrival"]):
|
| 391 |
try:
|
| 392 |
eta_dt = pd.to_datetime(row["Estimated Time Arrival"])
|
| 393 |
-
|
|
|
|
| 394 |
except Exception:
|
| 395 |
-
|
| 396 |
|
| 397 |
service_level = str(row["Service Level"]).strip()
|
| 398 |
cbm = row["CBM"]
|
|
@@ -407,7 +420,7 @@ def create_mawb_eta(file, logger=None):
|
|
| 407 |
"rich_text": [{"text": {"content": str(row["Customer"]).strip()}}]
|
| 408 |
},
|
| 409 |
"POD": {
|
| 410 |
-
"rich_text": [{"text": {"content":
|
| 411 |
},
|
| 412 |
"Service Level": {
|
| 413 |
"rich_text": [{"text": {"content": service_level}}]
|
|
@@ -422,10 +435,8 @@ def create_mawb_eta(file, logger=None):
|
|
| 422 |
"AP": {"checkbox": False}
|
| 423 |
}
|
| 424 |
|
| 425 |
-
if
|
| 426 |
-
properties["ETA"] =
|
| 427 |
-
"rich_text": [{"text": {"content": eta_text}}]
|
| 428 |
-
}
|
| 429 |
|
| 430 |
# -------- HTS Count + Entry Count(仅 T01 服务等级)--------
|
| 431 |
if service_level == "T01" and pd.notna(cbm):
|
|
@@ -486,77 +497,94 @@ def update_ar(file, logger=None):
|
|
| 486 |
# 解析 Excel
|
| 487 |
result = parse_ar_excel(file, logger)
|
| 488 |
if isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], str):
|
| 489 |
-
# 返回错误信息
|
| 490 |
return result[1]
|
| 491 |
|
| 492 |
df, (mawb_col, hts_fee_col, total_col) = result
|
| 493 |
|
| 494 |
-
#
|
| 495 |
-
logger.log("Querying
|
| 496 |
-
|
| 497 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 498 |
|
| 499 |
-
#
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 500 |
updated, skipped, not_found = [], [], []
|
|
|
|
| 501 |
total = len(df)
|
| 502 |
|
| 503 |
for current, (_, row) in enumerate(df.iterrows(), 1):
|
| 504 |
mawb = str(row[mawb_col]).strip()
|
| 505 |
-
logger.log(f"
|
| 506 |
|
| 507 |
if not mawb or mawb.lower() == 'nan':
|
| 508 |
logger.log("Empty MAWB, skipped")
|
| 509 |
continue
|
| 510 |
|
| 511 |
-
if mawb
|
| 512 |
-
|
| 513 |
-
|
| 514 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 515 |
|
| 516 |
-
|
| 517 |
-
|
| 518 |
-
logger.log("AR already checked, skip")
|
| 519 |
skipped.append(mawb)
|
| 520 |
-
|
| 521 |
-
|
| 522 |
-
|
| 523 |
-
properties = {}
|
| 524 |
-
properties["AR"] = {"checkbox": True}
|
| 525 |
-
|
| 526 |
-
# 获取当前时间(洛杉矶时区)
|
| 527 |
-
la_tz = ZoneInfo("America/Los_Angeles")
|
| 528 |
-
completed_time = datetime.now(la_tz).strftime("%Y/%m/%d %H:%M")
|
| 529 |
-
properties["AR Completed Date"] = {"rich_text": [{"text": {"content": completed_time}}]}
|
| 530 |
|
| 531 |
-
|
| 532 |
-
if hts_fee_col and pd.notna(row.get(hts_fee_col)):
|
| 533 |
-
try:
|
| 534 |
-
customs_fee = float(row[hts_fee_col])
|
| 535 |
-
properties["Customs Clearance Fee"] = {"number": round(customs_fee, 2)}
|
| 536 |
-
except (ValueError, TypeError):
|
| 537 |
-
logger.log(f"Warning: Invalid HTS Processing Fee value, skipped")
|
| 538 |
|
| 539 |
-
|
| 540 |
-
|
| 541 |
-
try:
|
| 542 |
-
ar_amount = float(row[total_col])
|
| 543 |
-
properties["AR amount"] = {"number": round(ar_amount, 2)}
|
| 544 |
-
except (ValueError, TypeError):
|
| 545 |
-
logger.log(f"Warning: Invalid Total value, skipped")
|
| 546 |
|
| 547 |
-
|
| 548 |
-
|
| 549 |
-
|
| 550 |
-
if "Customs Clearance Fee" in properties:
|
| 551 |
-
log_parts.append(f"Customs Clearance Fee: {properties['Customs Clearance Fee']['number']:.2f}")
|
| 552 |
-
if "AR amount" in properties:
|
| 553 |
-
log_parts.append(f"AR amount: {properties['AR amount']['number']:.2f}")
|
| 554 |
-
logger.log(" | ".join(log_parts))
|
| 555 |
-
updated.append(mawb)
|
| 556 |
-
else:
|
| 557 |
-
logger.log(f"Update failed: {r.text}")
|
| 558 |
|
| 559 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 560 |
|
| 561 |
logger.log("\n================ Process Completed ================")
|
| 562 |
logger.log(f"AR checked: {len(updated)}")
|
|
@@ -666,17 +694,25 @@ def aggregate_ap_fees(df, columns, logger):
|
|
| 666 |
return mawb_dict, extra_fee_records, jfk_mawbs
|
| 667 |
|
| 668 |
|
| 669 |
-
def update_ap_in_notion(mawb_dict, extra_fee_records, existing_pages, isc_summary, jfk_mawbs, logger):
|
| 670 |
"""更新 AP 到 Notion"""
|
| 671 |
-
updated_count = 0
|
| 672 |
skipped_ap_mawbs = []
|
| 673 |
not_found_mawbs = []
|
| 674 |
total = len(mawb_dict)
|
| 675 |
|
| 676 |
extra_fee_mawbs = {x.split(" - ")[0] for x in extra_fee_records}
|
| 677 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 678 |
for current, (mawb, fees) in enumerate(mawb_dict.items(), 1):
|
| 679 |
-
logger.log(f"
|
| 680 |
|
| 681 |
# 跳过含有额外费用的 MAWB
|
| 682 |
if mawb in extra_fee_mawbs:
|
|
@@ -684,20 +720,18 @@ def update_ap_in_notion(mawb_dict, extra_fee_records, existing_pages, isc_summar
|
|
| 684 |
skipped_ap_mawbs.append(f"{mawb} - Extra fee present")
|
| 685 |
continue
|
| 686 |
|
| 687 |
-
#
|
| 688 |
if mawb not in existing_pages:
|
| 689 |
-
|
| 690 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 691 |
continue
|
| 692 |
|
| 693 |
page_info = existing_pages[mawb]
|
| 694 |
|
| 695 |
-
# 跳过已勾选 AP 的记录
|
| 696 |
-
if page_info["ap_checked"]:
|
| 697 |
-
logger.log("AP already checked, skip")
|
| 698 |
-
skipped_ap_mawbs.append(f"{mawb} - AP already checked")
|
| 699 |
-
continue
|
| 700 |
-
|
| 701 |
# ===== CWT 前置强校验 =====
|
| 702 |
cwt_value = page_info.get("CWT")
|
| 703 |
|
|
@@ -743,11 +777,9 @@ def update_ap_in_notion(mawb_dict, extra_fee_records, existing_pages, isc_summar
|
|
| 743 |
for col_name in ["ISC Fee", "BreakDown Fee", "Bypass Fee", "CES Inspection Fee", "CES Transfer Fee"]:
|
| 744 |
properties[col_name] = {"number": round(fees.get(col_name, 0.0), 2)}
|
| 745 |
|
| 746 |
-
# 写入 WT Fee
|
| 747 |
wt_fee_value = round(fees.get("WT Fee", 0.0), 2)
|
| 748 |
properties["WT Fee"] = {"number": wt_fee_value}
|
| 749 |
|
| 750 |
-
# 计算 AP amount(所有费用总和)
|
| 751 |
ap_amount = (
|
| 752 |
fees.get("ISC Fee", 0.0) +
|
| 753 |
fees.get("BreakDown Fee", 0.0) +
|
|
@@ -757,21 +789,34 @@ def update_ap_in_notion(mawb_dict, extra_fee_records, existing_pages, isc_summar
|
|
| 757 |
wt_fee_value
|
| 758 |
)
|
| 759 |
properties["AP amount"] = {"number": round(ap_amount, 2)}
|
| 760 |
-
|
| 761 |
properties["AP"] = {"checkbox": True}
|
| 762 |
-
|
| 763 |
-
la_tz = ZoneInfo("America/Los_Angeles")
|
| 764 |
-
completed_time = datetime.now(la_tz).strftime("%Y/%m/%d %H:%M")
|
| 765 |
-
properties["AP Completed Date"] = {"rich_text": [{"text": {"content": completed_time}}]}
|
| 766 |
|
| 767 |
-
|
| 768 |
-
if r.status_code == 200:
|
| 769 |
-
logger.log(f"Update successful, AP checked, AP amount: {ap_amount:.2f}, AP Completed Date: {completed_time}")
|
| 770 |
-
updated_count += 1
|
| 771 |
-
else:
|
| 772 |
-
logger.log(f"Update failed: {r.text}")
|
| 773 |
|
| 774 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 775 |
|
| 776 |
return updated_count, not_found_mawbs, skipped_ap_mawbs
|
| 777 |
|
|
@@ -798,14 +843,20 @@ def update_ap(file, isc_file, logger=None):
|
|
| 798 |
else:
|
| 799 |
logger.log("No JFK invoices found, ISC fee validation not required")
|
| 800 |
|
| 801 |
-
#
|
| 802 |
-
logger.log("Querying Notion
|
| 803 |
-
|
| 804 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 805 |
|
| 806 |
# 更新 Notion
|
| 807 |
updated_count, not_found_mawbs, skipped_ap_mawbs = update_ap_in_notion(
|
| 808 |
-
mawb_dict, extra_fee_records, existing_pages, isc_summary, jfk_mawbs, logger
|
| 809 |
)
|
| 810 |
|
| 811 |
logger.log("\n================ Process Completed ================")
|
|
@@ -886,11 +937,12 @@ def update_cwt(file, logger=None):
|
|
| 886 |
log_parts.append(f"CWT={weight:.2f}")
|
| 887 |
|
| 888 |
# ATA 仅在 Excel 中有值时才写入(不受 AR/AP 影响)
|
| 889 |
-
ata_str = None
|
| 890 |
if pd.notna(ata):
|
| 891 |
-
|
| 892 |
-
|
| 893 |
-
|
|
|
|
|
|
|
| 894 |
|
| 895 |
# 没有任何属性需要更新,跳过
|
| 896 |
if not properties:
|
|
@@ -993,28 +1045,23 @@ def archive_old_records(cutoff_date_str=None, logger=None):
|
|
| 993 |
logger.log(f"Progress: {idx}/{total} | Processing MAWB: {mawb}")
|
| 994 |
|
| 995 |
# 解析 ETA,判断是否超过 3 个月
|
| 996 |
-
|
| 997 |
-
if not
|
| 998 |
logger.log("ETA is empty, skipped")
|
| 999 |
skipped_eta.append(f"{mawb} - ETA empty")
|
| 1000 |
continue
|
| 1001 |
|
| 1002 |
-
|
| 1003 |
-
|
| 1004 |
-
|
| 1005 |
-
|
| 1006 |
-
|
| 1007 |
-
|
| 1008 |
-
except ValueError:
|
| 1009 |
-
continue
|
| 1010 |
-
if eta_date is None:
|
| 1011 |
-
logger.log(f"ETA parse failed: '{eta_text}', skipped")
|
| 1012 |
-
skipped_eta.append(f"{mawb} - ETA parse failed: {eta_text}")
|
| 1013 |
continue
|
| 1014 |
|
| 1015 |
if eta_date > cutoff_date:
|
| 1016 |
-
logger.log(f"ETA {
|
| 1017 |
-
skipped_eta.append(f"{mawb} - ETA {
|
| 1018 |
continue
|
| 1019 |
|
| 1020 |
# 检查 WH-OUT Date 是否为空
|
|
@@ -1036,7 +1083,7 @@ def archive_old_records(cutoff_date_str=None, logger=None):
|
|
| 1036 |
|
| 1037 |
r_archive = notion_archive_page(page["id"])
|
| 1038 |
if r_archive.status_code == 200:
|
| 1039 |
-
logger.log(f"Archived successfully (ETA: {
|
| 1040 |
archived_count += 1
|
| 1041 |
else:
|
| 1042 |
logger.log(f"Delete from main DB failed: {r_archive.text}")
|
|
|
|
| 67 |
|
| 68 |
# 归档时需要迁移的属性(按 Notion 字段类型分组)
|
| 69 |
ARCHIVE_RICH_TEXT_FIELDS = [
|
| 70 |
+
"Customer", "POD", "Service Level", "LM Carrier"
|
|
|
|
| 71 |
]
|
| 72 |
ARCHIVE_NUMBER_FIELDS = [
|
| 73 |
"Parcels", "HTS Count", "Entry Count", "CWT",
|
|
|
|
| 77 |
"WT Fee", "CES Transfer Fee"
|
| 78 |
]
|
| 79 |
ARCHIVE_CHECKBOX_FIELDS = ["AR", "AP"]
|
| 80 |
+
ARCHIVE_DATE_FIELDS = ["WH-OUT Date", "ETA", "ATA", "AR Completed Date", "AP Completed Date"]
|
| 81 |
+
|
| 82 |
+
# POD → IANA timezone mapping (for localizing Excel dates when writing ETA/ATA)
|
| 83 |
+
POD_TIMEZONE_MAP = {
|
| 84 |
+
"JFK": "America/New_York",
|
| 85 |
+
"ORD": "America/Chicago",
|
| 86 |
+
"LAX": "America/Los_Angeles",
|
| 87 |
+
"MIA": "America/New_York",
|
| 88 |
+
}
|
| 89 |
|
| 90 |
# ================== 会话与重试配置 ==================
|
| 91 |
session = requests.Session()
|
|
|
|
| 313 |
|
| 314 |
|
| 315 |
def extract_mawb_with_id_ar_ap(page):
|
| 316 |
+
"""提取 MAWB 及页面 ID、AR/AP 状态、POD"""
|
| 317 |
title = page["properties"]["MAWB"]["title"]
|
| 318 |
if title:
|
| 319 |
mawb = title[0]["text"]["content"]
|
| 320 |
+
pod_rt = page["properties"].get("POD", {}).get("rich_text", [])
|
| 321 |
+
pod = pod_rt[0]["text"]["content"] if pod_rt else ""
|
| 322 |
return mawb, {
|
| 323 |
"id": page["id"],
|
| 324 |
"ar_checked": page["properties"]["AR"]["checkbox"],
|
| 325 |
"ap_checked": page["properties"]["AP"]["checkbox"],
|
| 326 |
+
"pod": pod,
|
| 327 |
}
|
| 328 |
return None, None
|
| 329 |
|
|
|
|
| 395 |
skipped += 1
|
| 396 |
continue
|
| 397 |
|
| 398 |
+
# -------- ETA 处理(日期写入,含目的地时区)--------
|
| 399 |
+
pod = str(row["Destination"]).strip()
|
| 400 |
+
tz_name = POD_TIMEZONE_MAP.get(pod.upper(), "America/Los_Angeles")
|
| 401 |
+
eta_date_prop = None
|
| 402 |
if pd.notna(row["Estimated Time Arrival"]):
|
| 403 |
try:
|
| 404 |
eta_dt = pd.to_datetime(row["Estimated Time Arrival"])
|
| 405 |
+
eta_start = eta_dt.strftime("%Y-%m-%dT%H:%M:%S")
|
| 406 |
+
eta_date_prop = {"date": {"start": eta_start, "time_zone": tz_name}}
|
| 407 |
except Exception:
|
| 408 |
+
eta_date_prop = None
|
| 409 |
|
| 410 |
service_level = str(row["Service Level"]).strip()
|
| 411 |
cbm = row["CBM"]
|
|
|
|
| 420 |
"rich_text": [{"text": {"content": str(row["Customer"]).strip()}}]
|
| 421 |
},
|
| 422 |
"POD": {
|
| 423 |
+
"rich_text": [{"text": {"content": pod}}]
|
| 424 |
},
|
| 425 |
"Service Level": {
|
| 426 |
"rich_text": [{"text": {"content": service_level}}]
|
|
|
|
| 435 |
"AP": {"checkbox": False}
|
| 436 |
}
|
| 437 |
|
| 438 |
+
if eta_date_prop:
|
| 439 |
+
properties["ETA"] = eta_date_prop
|
|
|
|
|
|
|
| 440 |
|
| 441 |
# -------- HTS Count + Entry Count(仅 T01 服务等级)--------
|
| 442 |
if service_level == "T01" and pd.notna(cbm):
|
|
|
|
| 497 |
# 解析 Excel
|
| 498 |
result = parse_ar_excel(file, logger)
|
| 499 |
if isinstance(result, tuple) and len(result) == 2 and isinstance(result[1], str):
|
|
|
|
| 500 |
return result[1]
|
| 501 |
|
| 502 |
df, (mawb_col, hts_fee_col, total_col) = result
|
| 503 |
|
| 504 |
+
# -------- 服务端过滤:只查 AR=false 的记录 --------
|
| 505 |
+
logger.log("Querying Notion (filter: AR=false)...")
|
| 506 |
+
ar_false_filter = {"property": "AR", "checkbox": {"equals": False}}
|
| 507 |
+
pages_to_update = query_all_pages(extract_mawb_with_ar, filter_body=ar_false_filter)
|
| 508 |
+
logger.log(f"Records with AR unchecked: {len(pages_to_update)}")
|
| 509 |
+
|
| 510 |
+
# 轻量查询:全部 MAWB,用于区分「未找到」与「已勾选 AR」
|
| 511 |
+
logger.log("Querying all MAWBs for existence check...")
|
| 512 |
+
all_mawbs = set(query_all_pages(extract_mawb_only).keys())
|
| 513 |
+
logger.log(f"Total MAWBs in Notion: {len(all_mawbs)}")
|
| 514 |
|
| 515 |
+
# 统一时间戳(洛杉矶时区,同批次一致)
|
| 516 |
+
la_tz = ZoneInfo("America/Los_Angeles")
|
| 517 |
+
_now_la = datetime.now(la_tz)
|
| 518 |
+
completed_time = _now_la.strftime("%Y/%m/%d %H:%M") # 用于日志显示
|
| 519 |
+
completed_time_iso = _now_la.strftime("%Y-%m-%dT%H:%M:%S") # 用于 Notion 日期字段
|
| 520 |
+
|
| 521 |
+
# -------- Phase 1:预处理,构建更新任务列表 --------
|
| 522 |
updated, skipped, not_found = [], [], []
|
| 523 |
+
tasks = [] # (mawb, page_id, properties, log_suffix)
|
| 524 |
total = len(df)
|
| 525 |
|
| 526 |
for current, (_, row) in enumerate(df.iterrows(), 1):
|
| 527 |
mawb = str(row[mawb_col]).strip()
|
| 528 |
+
logger.log(f"Preparing: {current}/{total} | MAWB: {mawb}")
|
| 529 |
|
| 530 |
if not mawb or mawb.lower() == 'nan':
|
| 531 |
logger.log("Empty MAWB, skipped")
|
| 532 |
continue
|
| 533 |
|
| 534 |
+
if mawb in pages_to_update:
|
| 535 |
+
properties = {
|
| 536 |
+
"AR": {"checkbox": True},
|
| 537 |
+
"AR Completed Date": {"date": {"start": completed_time_iso, "time_zone": "America/Los_Angeles"}}
|
| 538 |
+
}
|
| 539 |
+
log_parts = [f"AR checked, AR Completed Date: {completed_time}"]
|
| 540 |
+
|
| 541 |
+
if hts_fee_col and pd.notna(row.get(hts_fee_col)):
|
| 542 |
+
try:
|
| 543 |
+
customs_fee = float(row[hts_fee_col])
|
| 544 |
+
properties["Customs Clearance Fee"] = {"number": round(customs_fee, 2)}
|
| 545 |
+
log_parts.append(f"Customs Clearance Fee: {customs_fee:.2f}")
|
| 546 |
+
except (ValueError, TypeError):
|
| 547 |
+
logger.log("Warning: Invalid HTS Processing Fee value, skipped")
|
| 548 |
+
|
| 549 |
+
if total_col and pd.notna(row.get(total_col)):
|
| 550 |
+
try:
|
| 551 |
+
ar_amount = float(row[total_col])
|
| 552 |
+
properties["AR amount"] = {"number": round(ar_amount, 2)}
|
| 553 |
+
log_parts.append(f"AR amount: {ar_amount:.2f}")
|
| 554 |
+
except (ValueError, TypeError):
|
| 555 |
+
logger.log("Warning: Invalid Total value, skipped")
|
| 556 |
+
|
| 557 |
+
tasks.append((mawb, pages_to_update[mawb]["id"], properties, " | ".join(log_parts)))
|
| 558 |
|
| 559 |
+
elif mawb in all_mawbs:
|
| 560 |
+
logger.log("AR already checked, skipped")
|
|
|
|
| 561 |
skipped.append(mawb)
|
| 562 |
+
else:
|
| 563 |
+
logger.log("Not found in Notion")
|
| 564 |
+
not_found.append(mawb)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 565 |
|
| 566 |
+
logger.log(f"\nPrepared {len(tasks)} update tasks, starting concurrent PATCH (workers={MAX_CONCURRENT_WORKERS})...")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 567 |
|
| 568 |
+
# -------- Phase 2:并发 PATCH --------
|
| 569 |
+
lock = threading.Lock()
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 570 |
|
| 571 |
+
def _patch_task(mawb, page_id, properties, log_suffix):
|
| 572 |
+
r = notion_patch(page_id, properties)
|
| 573 |
+
return mawb, r.status_code, r.text, log_suffix
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 574 |
|
| 575 |
+
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_WORKERS) as executor:
|
| 576 |
+
futures = {
|
| 577 |
+
executor.submit(_patch_task, mawb, page_id, props, log_sfx): mawb
|
| 578 |
+
for mawb, page_id, props, log_sfx in tasks
|
| 579 |
+
}
|
| 580 |
+
for future in as_completed(futures):
|
| 581 |
+
mawb, status, text, log_sfx = future.result()
|
| 582 |
+
with lock:
|
| 583 |
+
if status == 200:
|
| 584 |
+
logger.log(f"MAWB {mawb} | {log_sfx}")
|
| 585 |
+
updated.append(mawb)
|
| 586 |
+
else:
|
| 587 |
+
logger.log(f"MAWB {mawb} update failed: {text}")
|
| 588 |
|
| 589 |
logger.log("\n================ Process Completed ================")
|
| 590 |
logger.log(f"AR checked: {len(updated)}")
|
|
|
|
| 694 |
return mawb_dict, extra_fee_records, jfk_mawbs
|
| 695 |
|
| 696 |
|
| 697 |
+
def update_ap_in_notion(mawb_dict, extra_fee_records, existing_pages, isc_summary, jfk_mawbs, all_mawbs, logger):
|
| 698 |
"""更新 AP 到 Notion"""
|
|
|
|
| 699 |
skipped_ap_mawbs = []
|
| 700 |
not_found_mawbs = []
|
| 701 |
total = len(mawb_dict)
|
| 702 |
|
| 703 |
extra_fee_mawbs = {x.split(" - ")[0] for x in extra_fee_records}
|
| 704 |
|
| 705 |
+
# 统一时间戳(洛杉矶时区,同批次一致)
|
| 706 |
+
la_tz = ZoneInfo("America/Los_Angeles")
|
| 707 |
+
_now_la = datetime.now(la_tz)
|
| 708 |
+
completed_time = _now_la.strftime("%Y/%m/%d %H:%M") # 用于日志显示
|
| 709 |
+
completed_time_iso = _now_la.strftime("%Y-%m-%dT%H:%M:%S") # 用于 Notion 日期字段
|
| 710 |
+
|
| 711 |
+
# -------- Phase 1:预处理,构建更新任务列表 --------
|
| 712 |
+
tasks = [] # (mawb, page_id, properties, ap_amount)
|
| 713 |
+
|
| 714 |
for current, (mawb, fees) in enumerate(mawb_dict.items(), 1):
|
| 715 |
+
logger.log(f"Preparing: {current}/{total} | MAWB: {mawb}")
|
| 716 |
|
| 717 |
# 跳过含有额外费用的 MAWB
|
| 718 |
if mawb in extra_fee_mawbs:
|
|
|
|
| 720 |
skipped_ap_mawbs.append(f"{mawb} - Extra fee present")
|
| 721 |
continue
|
| 722 |
|
| 723 |
+
# existing_pages 仅含 AP=false 记录;不在其中说明未找到或已勾选
|
| 724 |
if mawb not in existing_pages:
|
| 725 |
+
if mawb in all_mawbs:
|
| 726 |
+
logger.log("AP already checked, skipped")
|
| 727 |
+
skipped_ap_mawbs.append(f"{mawb} - AP already checked")
|
| 728 |
+
else:
|
| 729 |
+
logger.log("MAWB not found in Notion, skip")
|
| 730 |
+
not_found_mawbs.append(mawb)
|
| 731 |
continue
|
| 732 |
|
| 733 |
page_info = existing_pages[mawb]
|
| 734 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 735 |
# ===== CWT 前置强校验 =====
|
| 736 |
cwt_value = page_info.get("CWT")
|
| 737 |
|
|
|
|
| 777 |
for col_name in ["ISC Fee", "BreakDown Fee", "Bypass Fee", "CES Inspection Fee", "CES Transfer Fee"]:
|
| 778 |
properties[col_name] = {"number": round(fees.get(col_name, 0.0), 2)}
|
| 779 |
|
|
|
|
| 780 |
wt_fee_value = round(fees.get("WT Fee", 0.0), 2)
|
| 781 |
properties["WT Fee"] = {"number": wt_fee_value}
|
| 782 |
|
|
|
|
| 783 |
ap_amount = (
|
| 784 |
fees.get("ISC Fee", 0.0) +
|
| 785 |
fees.get("BreakDown Fee", 0.0) +
|
|
|
|
| 789 |
wt_fee_value
|
| 790 |
)
|
| 791 |
properties["AP amount"] = {"number": round(ap_amount, 2)}
|
|
|
|
| 792 |
properties["AP"] = {"checkbox": True}
|
| 793 |
+
properties["AP Completed Date"] = {"date": {"start": completed_time_iso, "time_zone": "America/Los_Angeles"}}
|
|
|
|
|
|
|
|
|
|
| 794 |
|
| 795 |
+
tasks.append((mawb, page_info["id"], properties, ap_amount))
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 796 |
|
| 797 |
+
logger.log(f"\nPrepared {len(tasks)} update tasks, starting concurrent PATCH (workers={MAX_CONCURRENT_WORKERS})...")
|
| 798 |
+
|
| 799 |
+
# -------- Phase 2:并发 PATCH --------
|
| 800 |
+
updated_count = 0
|
| 801 |
+
lock = threading.Lock()
|
| 802 |
+
|
| 803 |
+
def _patch_task(mawb, page_id, properties, ap_amount):
|
| 804 |
+
r = notion_patch(page_id, properties)
|
| 805 |
+
return mawb, r.status_code, r.text, ap_amount
|
| 806 |
+
|
| 807 |
+
with ThreadPoolExecutor(max_workers=MAX_CONCURRENT_WORKERS) as executor:
|
| 808 |
+
futures = {
|
| 809 |
+
executor.submit(_patch_task, mawb, page_id, props, ap_amt): mawb
|
| 810 |
+
for mawb, page_id, props, ap_amt in tasks
|
| 811 |
+
}
|
| 812 |
+
for future in as_completed(futures):
|
| 813 |
+
mawb, status, text, ap_amt = future.result()
|
| 814 |
+
with lock:
|
| 815 |
+
if status == 200:
|
| 816 |
+
logger.log(f"MAWB {mawb} updated | AP checked, AP amount: {ap_amt:.2f}, AP Completed Date: {completed_time}")
|
| 817 |
+
updated_count += 1
|
| 818 |
+
else:
|
| 819 |
+
logger.log(f"MAWB {mawb} update failed: {text}")
|
| 820 |
|
| 821 |
return updated_count, not_found_mawbs, skipped_ap_mawbs
|
| 822 |
|
|
|
|
| 843 |
else:
|
| 844 |
logger.log("No JFK invoices found, ISC fee validation not required")
|
| 845 |
|
| 846 |
+
# -------- 服务端过滤:只查 AP=false 的记录 --------
|
| 847 |
+
logger.log("Querying Notion (filter: AP=false)...")
|
| 848 |
+
ap_false_filter = {"property": "AP", "checkbox": {"equals": False}}
|
| 849 |
+
existing_pages = query_all_pages(extract_mawb_with_ap_cwt, filter_body=ap_false_filter)
|
| 850 |
+
logger.log(f"Records with AP unchecked: {len(existing_pages)}")
|
| 851 |
+
|
| 852 |
+
# 轻量查询:全部 MAWB,用于区分「未找到」与「已勾选 AP」
|
| 853 |
+
logger.log("Querying all MAWBs for existence check...")
|
| 854 |
+
all_mawbs = set(query_all_pages(extract_mawb_only).keys())
|
| 855 |
+
logger.log(f"Total MAWBs in Notion: {len(all_mawbs)}")
|
| 856 |
|
| 857 |
# 更新 Notion
|
| 858 |
updated_count, not_found_mawbs, skipped_ap_mawbs = update_ap_in_notion(
|
| 859 |
+
mawb_dict, extra_fee_records, existing_pages, isc_summary, jfk_mawbs, all_mawbs, logger
|
| 860 |
)
|
| 861 |
|
| 862 |
logger.log("\n================ Process Completed ================")
|
|
|
|
| 937 |
log_parts.append(f"CWT={weight:.2f}")
|
| 938 |
|
| 939 |
# ATA 仅在 Excel 中有值时才写入(不受 AR/AP 影响)
|
|
|
|
| 940 |
if pd.notna(ata):
|
| 941 |
+
pod = page_info.get("pod", "")
|
| 942 |
+
tz_name = POD_TIMEZONE_MAP.get(pod.upper(), "America/Los_Angeles")
|
| 943 |
+
ata_start = ata.strftime("%Y-%m-%dT%H:%M:%S")
|
| 944 |
+
properties["ATA"] = {"date": {"start": ata_start, "time_zone": tz_name}}
|
| 945 |
+
log_parts.append(f"ATA={ata_start} ({tz_name})")
|
| 946 |
|
| 947 |
# 没有任何属性需要更新,跳过
|
| 948 |
if not properties:
|
|
|
|
| 1045 |
logger.log(f"Progress: {idx}/{total} | Processing MAWB: {mawb}")
|
| 1046 |
|
| 1047 |
# 解析 ETA,判断是否超过 3 个月
|
| 1048 |
+
eta_date_val = page["properties"].get("ETA", {}).get("date")
|
| 1049 |
+
if not eta_date_val or not eta_date_val.get("start"):
|
| 1050 |
logger.log("ETA is empty, skipped")
|
| 1051 |
skipped_eta.append(f"{mawb} - ETA empty")
|
| 1052 |
continue
|
| 1053 |
|
| 1054 |
+
eta_start = eta_date_val["start"]
|
| 1055 |
+
try:
|
| 1056 |
+
eta_date = datetime.strptime(eta_start[:10], "%Y-%m-%d")
|
| 1057 |
+
except ValueError:
|
| 1058 |
+
logger.log(f"ETA parse failed: '{eta_start}', skipped")
|
| 1059 |
+
skipped_eta.append(f"{mawb} - ETA parse failed: {eta_start}")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1060 |
continue
|
| 1061 |
|
| 1062 |
if eta_date > cutoff_date:
|
| 1063 |
+
logger.log(f"ETA {eta_start[:10]} is after cutoff date, skipped")
|
| 1064 |
+
skipped_eta.append(f"{mawb} - ETA {eta_start[:10]} after cutoff date")
|
| 1065 |
continue
|
| 1066 |
|
| 1067 |
# 检查 WH-OUT Date 是否为空
|
|
|
|
| 1083 |
|
| 1084 |
r_archive = notion_archive_page(page["id"])
|
| 1085 |
if r_archive.status_code == 200:
|
| 1086 |
+
logger.log(f"Archived successfully (ETA: {eta_start[:10]})")
|
| 1087 |
archived_count += 1
|
| 1088 |
else:
|
| 1089 |
logger.log(f"Delete from main DB failed: {r_archive.text}")
|