From 9033d24a824e938a0c196f8c1b0d45b0ca8441c8 Mon Sep 17 00:00:00 2001 From: Kevin Tran Date: Tue, 31 Mar 2026 01:15:04 -0500 Subject: [PATCH] feat: refactor scripts directory + margin edge scripts --- scripts/{ => load_testing}/README.md | 0 scripts/{ => load_testing}/locustfile.py | 0 scripts/{ => load_testing}/requirements.txt | 0 scripts/margin_edge_integration/README.md | 91 ++++++++ .../get_margin_edge_order_details.py | 199 +++++++++++++++++ .../get_prairie_canary_restaurant_orders.py | 207 +++++++++++++++++ .../import_margin_edge_to_culinary_command.py | 210 ++++++++++++++++++ .../margin_edge_integration/requirements.txt | 4 + 8 files changed, 711 insertions(+) rename scripts/{ => load_testing}/README.md (100%) rename scripts/{ => load_testing}/locustfile.py (100%) rename scripts/{ => load_testing}/requirements.txt (100%) create mode 100644 scripts/margin_edge_integration/README.md create mode 100644 scripts/margin_edge_integration/get_margin_edge_order_details.py create mode 100644 scripts/margin_edge_integration/get_prairie_canary_restaurant_orders.py create mode 100644 scripts/margin_edge_integration/import_margin_edge_to_culinary_command.py create mode 100644 scripts/margin_edge_integration/requirements.txt diff --git a/scripts/README.md b/scripts/load_testing/README.md similarity index 100% rename from scripts/README.md rename to scripts/load_testing/README.md diff --git a/scripts/locustfile.py b/scripts/load_testing/locustfile.py similarity index 100% rename from scripts/locustfile.py rename to scripts/load_testing/locustfile.py diff --git a/scripts/requirements.txt b/scripts/load_testing/requirements.txt similarity index 100% rename from scripts/requirements.txt rename to scripts/load_testing/requirements.txt diff --git a/scripts/margin_edge_integration/README.md b/scripts/margin_edge_integration/README.md new file mode 100644 index 0000000..219d62d --- /dev/null +++ b/scripts/margin_edge_integration/README.md @@ -0,0 +1,91 @@ +# MarginEdge integration scripts + +This folder contains small Python utilities for pulling data from the **MarginEdge Public API** and exporting/inspecting it locally. + +## What these scripts do + +Typical workflow: + +1. Create an authenticated HTTP session using the MarginEdge API key. +2. Call MarginEdge endpoints under: + + - Base URL: `https://api.marginedge.com/public` + - Auth header: `X-API-KEY: ` + +3. Pass required query parameters (commonly `restaurantUnitId`, plus optional date filters like `startDate`/`endDate`). +4. Handle pagination when the API returns a `nextPage` cursor. +5. Normalize responses (some endpoints return arrays, others wrap arrays in properties like `orders`, `content`, etc.). +6. Print summaries and/or export results (for example to Excel). + +## Setup + +### 1) Create a virtual environment (recommended) + +```bash +python3 -m venv .venv +source .venv/bin/activate +``` + +### 2) Install dependencies + +If you have a `requirements.txt` in the parent `scripts/` folder, install from there: + +```bash +pip install -r ../requirements.txt +``` + +(If dependencies change in the future, keep `../requirements.txt` as the source of truth.) + +### 3) Configure environment variables + +These scripts expect an API key in your environment (often via a local `.env` file). + +Create a `.env` file **in the repo root** or in this folder (wherever you run the script from) with: + +```ini +ME_API_KEY=YOUR_MARGINEDGE_KEY +# optional +ME_API_BASE_URL=https://api.marginedge.com/public +``` + +- `ME_API_KEY` is required. +- `ME_API_BASE_URL` is optional and defaults to `https://api.marginedge.com/public`. + +## Scripts + +### `get_prairie_canary_restaurant_orders.py` + +Fetches orders for a given `restaurantUnitId` over a recent date range, then exports a simplified list to an Excel file. + +Key behaviors: + +- Uses a shared `requests.Session()` with headers: + - `X-API-KEY` + - `Accept: application/json` +- Calls `GET /orders` with: + - `restaurantUnitId` + - `startDate` / `endDate` (formatted as `YYYY-MM-DD`) +- Handles rate limiting (`HTTP 429`) by respecting `Retry-After`. +- Handles cursor pagination with `nextPage`. +- Exports to `orders__.xlsx`. + +Run it: + +```bash +python3 get_prairie_canary_restaurant_orders.py --restaurant-id +``` + +Optional JSON output: + +```bash +python3 get_prairie_canary_restaurant_orders.py --restaurant-id --json +``` + +## Notes / troubleshooting + +- `restaurantUnitId` is commonly a GUID/UUID. If you pass an invalid value, the API may respond with errors (sometimes even `500`). +- If you want to reproduce calls in Insomnia/Postman: + - Method: `GET` + - URL: `https://api.marginedge.com/public/` + - Header: `X-API-KEY: ` + - Query params: e.g. `restaurantUnitId`, `startDate`, `endDate` diff --git a/scripts/margin_edge_integration/get_margin_edge_order_details.py b/scripts/margin_edge_integration/get_margin_edge_order_details.py new file mode 100644 index 0000000..a85e830 --- /dev/null +++ b/scripts/margin_edge_integration/get_margin_edge_order_details.py @@ -0,0 +1,199 @@ +""" +get_margin_edge_order_details.py + +Reads an Excel file produced by get_prairie_canary_restaurant_orders.py, +fetches the line items for each order from the Margin Edge API, +and writes a new Excel sheet containing the vendorItemName for each line item. + +Usage: + python3 get_margin_edge_order_details.py --input-file .xlsx --restaurant-id + +Environment variables (set in .env): + ME_API_KEY - Margin Edge API key +""" +# AI-ASSISTED + +import argparse +import os +import sys +import time +from datetime import datetime, timezone +from typing import Optional, Union + +import openpyxl +from openpyxl.styles import Alignment, Font, PatternFill +import requests +from dotenv import load_dotenv + +load_dotenv() + +MARGIN_EDGE_API_BASE_URL = os.getenv("ME_API_BASE_URL", "https://api.marginedge.com/public") +MARGIN_EDGE_MIN_REQUEST_INTERVAL = 0.5 +_margin_edge_last_request_time: float = 0.0 + + +# --------------------------------------------------------------------------- +# API helpers +# --------------------------------------------------------------------------- + +def get_margin_edge_session() -> requests.Session: + api_key = os.environ.get("ME_API_KEY") + if not api_key: + print("ERROR: ME_API_KEY environment variable is not set.", file=sys.stderr) + sys.exit(1) + session = requests.Session() + session.headers.update({ + "X-API-KEY": api_key, + "Accept": "application/json", + }) + return session + + +def make_margin_edge_request(session: requests.Session, path: str, params: Optional[dict] = None, retries: int = 3) -> Union[list, dict]: + global _margin_edge_last_request_time + url = f"{MARGIN_EDGE_API_BASE_URL}/{path.lstrip('/')}" + + elapsed = time.monotonic() - _margin_edge_last_request_time + if elapsed < MARGIN_EDGE_MIN_REQUEST_INTERVAL: + time.sleep(MARGIN_EDGE_MIN_REQUEST_INTERVAL - elapsed) + + for attempt in range(1, retries + 1): + _margin_edge_last_request_time = time.monotonic() + response = session.get(url, params=params, timeout=30) + + if response.status_code == 429: + retry_after = float(response.headers.get("Retry-After", 60)) + print(f" Rate limited. Waiting {retry_after:.0f}s (attempt {attempt}/{retries})...") + time.sleep(retry_after) + continue + + response.raise_for_status() + return response.json() + + raise RuntimeError(f"Rate limit exceeded after {retries} retries: {url}") + + +def fetch_order_line_items(session: requests.Session, order_id: str, restaurant_id: str) -> list[dict]: + order_detail = make_margin_edge_request(session, f"orders/{order_id}", params={"restaurantUnitId": restaurant_id}) + if not isinstance(order_detail, dict): + return [] + return order_detail.get("lineItems", []) + + +# --------------------------------------------------------------------------- +# Excel helpers +# --------------------------------------------------------------------------- + +def read_orders_from_excel(input_file: str) -> list[dict]: + workbook = openpyxl.load_workbook(input_file) + worksheet = workbook.active + + headers = [cell.value for cell in worksheet[1]] + orders = [] + for row in worksheet.iter_rows(min_row=2, values_only=True): + row_data = dict(zip(headers, row)) + if row_data.get("Order ID"): + orders.append({ + "orderId": str(row_data.get("Order ID", "")), + "createdDate": str(row_data.get("Created Date", "")), + "vendorName": str(row_data.get("Vendor Name", "")), + }) + return orders + + +def apply_header_style(cell) -> None: + cell.font = Font(bold=True, color="FFFFFF") + cell.fill = PatternFill(fill_type="solid", fgColor="2F5496") + cell.alignment = Alignment(horizontal="center") + + +def apply_group_header_style(cell) -> None: + cell.font = Font(bold=True, color="FFFFFF") + cell.fill = PatternFill(fill_type="solid", fgColor="4472C4") + cell.alignment = Alignment(horizontal="left") + + +def export_line_items_to_excel(order_line_items: list[dict], restaurant_id: str) -> None: + workbook = openpyxl.Workbook() + worksheet = workbook.active + worksheet.title = "Order Line Items" + + column_headers = ["Order ID", "Created Date", "Vendor Name", "Vendor Item Name"] + for col_index, header in enumerate(column_headers, start=1): + cell = worksheet.cell(row=1, column=col_index, value=header) + apply_header_style(cell) + + current_row = 2 + for order_entry in order_line_items: + order_id = order_entry["orderId"] + created_date = order_entry["createdDate"] + vendor_name = order_entry["vendorName"] + line_items = order_entry["lineItems"] + + if not line_items: + worksheet.cell(row=current_row, column=1, value=order_id) + worksheet.cell(row=current_row, column=2, value=created_date) + worksheet.cell(row=current_row, column=3, value=vendor_name) + worksheet.cell(row=current_row, column=4, value="(no line items)") + current_row += 1 + continue + + for line_item in line_items: + vendor_item_name = line_item.get("vendorItemName", "N/A") + worksheet.cell(row=current_row, column=1, value=order_id) + worksheet.cell(row=current_row, column=2, value=created_date) + worksheet.cell(row=current_row, column=3, value=vendor_name) + worksheet.cell(row=current_row, column=4, value=vendor_item_name) + current_row += 1 + + for column in worksheet.columns: + max_length = max(len(str(cell.value or "")) for cell in column) + worksheet.column_dimensions[column[0].column_letter].width = max_length + 4 + + output_filename = f"order_line_items_{restaurant_id}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.xlsx" + workbook.save(output_filename) + print(f"\nExported to {output_filename}") + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser(description="Fetch Margin Edge line items for each order in an Excel file") + parser.add_argument("--input-file", type=str, required=True, help="Path to the Excel file from get_prairie_canary_restaurant_orders.py") + parser.add_argument("--restaurant-id", type=str, required=True, help="Margin Edge restaurantUnitId") + args = parser.parse_args() + + if not os.path.exists(args.input_file): + print(f"ERROR: File not found: {args.input_file}", file=sys.stderr) + sys.exit(1) + + print(f"Reading orders from {args.input_file}...") + orders = read_orders_from_excel(args.input_file) + + if not orders: + print("No orders found in the Excel file.") + sys.exit(0) + + print(f"Found {len(orders)} order(s). Fetching line items...\n") + + margin_edge_session = get_margin_edge_session() + order_line_items = [] + + for index, order in enumerate(orders, start=1): + order_id = order["orderId"] + print(f" [{index}/{len(orders)}] Fetching line items for order {order_id}...") + try: + line_items = fetch_order_line_items(margin_edge_session, order_id, args.restaurant_id) + order_line_items.append({**order, "lineItems": line_items}) + print(f" Found {len(line_items)} line item(s).") + except requests.HTTPError as error: + print(f" Failed to fetch order {order_id}: {error}") + order_line_items.append({**order, "lineItems": []}) + + export_line_items_to_excel(order_line_items, args.restaurant_id) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/margin_edge_integration/get_prairie_canary_restaurant_orders.py b/scripts/margin_edge_integration/get_prairie_canary_restaurant_orders.py new file mode 100644 index 0000000..468972d --- /dev/null +++ b/scripts/margin_edge_integration/get_prairie_canary_restaurant_orders.py @@ -0,0 +1,207 @@ +""" +get_prairie_canary_restaurant_orders.py + +Fetches orders and their line-item details from the Margin Edge API +for a given restaurant unit over the past month. + +Usage: + python3 get_prairie_canary_restaurant_orders.py --restaurant-id + +Environment variables (set in .env): + ME_API_KEY - Margin Edge API key +""" +# AI-ASSISTED + +import argparse +import json +import os +import sys +import time +from datetime import datetime, timedelta, timezone +from typing import Optional, Union + +import openpyxl +from openpyxl.styles import Font, PatternFill, Alignment +import requests +from dotenv import load_dotenv + +load_dotenv() + +MARGIN_EDGE_API_BASE_URL = os.getenv("ME_API_BASE_URL", "https://api.marginedge.com/public") +MARGIN_EDGE_MIN_REQUEST_INTERVAL = 0.5 +_margin_edge_last_request_time: float = 0.0 + + +def get_margin_edge_session() -> requests.Session: + api_key = os.environ.get("ME_API_KEY") + if not api_key: + print("ERROR: ME_API_KEY environment variable is not set.", file=sys.stderr) + sys.exit(1) + session = requests.Session() + session.headers.update({ + "X-API-KEY": api_key, + "Accept": "application/json", + }) + return session + + +def make_margin_edge_request(session: requests.Session, path: str, params: Optional[dict] = None, retries: int = 3) -> Union[list, dict]: + global _margin_edge_last_request_time + url = f"{MARGIN_EDGE_API_BASE_URL}/{path.lstrip('/')}" + + elapsed = time.monotonic() - _margin_edge_last_request_time + if elapsed < MARGIN_EDGE_MIN_REQUEST_INTERVAL: + time.sleep(MARGIN_EDGE_MIN_REQUEST_INTERVAL - elapsed) + + for attempt in range(1, retries + 1): + _margin_edge_last_request_time = time.monotonic() + response = session.get(url, params=params, timeout=30) + + if response.status_code == 429: + retry_after = float(response.headers.get("Retry-After", 60)) + print(f" Rate limited. Waiting {retry_after:.0f}s (attempt {attempt}/{retries})...") + time.sleep(retry_after) + continue + + response.raise_for_status() + return response.json() + + raise RuntimeError(f"Rate limit exceeded after {retries} retries: {url}") + + +def unwrap_margin_edge_response(api_response: Union[list, dict]) -> list[dict]: + if isinstance(api_response, list): + return api_response + for key in ("orders", "content", "data", "items", "results"): + if key in api_response and isinstance(api_response[key], list): + return api_response[key] + return [api_response] if api_response else [] + + +def fetch_orders(session: requests.Session, restaurant_id: str, start_date: str, end_date: str) -> list[dict]: + all_orders = [] + next_page_cursor = None + + while True: + params = { + "restaurantUnitId": restaurant_id, + "startDate": start_date, + "endDate": end_date, + } + if next_page_cursor: + params["nextPage"] = next_page_cursor + + api_response = make_margin_edge_request(session, "orders", params=params) + all_orders.extend(unwrap_margin_edge_response(api_response)) + + next_page_cursor = api_response.get("nextPage") if isinstance(api_response, dict) else None + if not next_page_cursor: + break + + print(f" Fetching next page...") + + return all_orders + + +def fetch_order_detail(session: requests.Session, order_id: str) -> dict: + api_response = make_margin_edge_request(session, f"orders/{order_id}") + return api_response if isinstance(api_response, dict) else {} + + +def print_order_summary(order: dict) -> None: + order_id = order.get("orderId", "N/A") + created_date = order.get("createdDate", "N/A") + vendor_name = order.get("vendorName", "N/A") + print(f" Order {order_id} | {created_date} | {vendor_name}") + + +def print_order_detail(order_detail: dict) -> None: + line_items = order_detail.get("lineItems") or order_detail.get("items") or order_detail.get("lines") or [] + if not line_items: + print(" (no line items found)") + return + print(f" {'Product':<40} {'Qty':>8} {'Unit':<10} {'Unit Price':>12} {'Total':>12}") + print(f" {'-'*40} {'-'*8} {'-'*10} {'-'*12} {'-'*12}") + for line_item in line_items: + product_name = line_item.get("productName") or line_item.get("name", "Unknown") + quantity = line_item.get("quantity") or line_item.get("qty", 0) + unit = line_item.get("unit") or line_item.get("purchaseUnit", "") + unit_price = line_item.get("unitPrice") or line_item.get("price", 0) + line_total = line_item.get("totalPrice") or line_item.get("extendedPrice") or (float(quantity or 0) * float(unit_price or 0)) + print(f" {str(product_name):<40} {str(quantity):>8} {str(unit):<10} ${float(unit_price or 0):>11.2f} ${float(line_total or 0):>11.2f}") + + +def export_orders_to_excel(trimmed_orders: list[dict], restaurant_id: str) -> None: + workbook = openpyxl.Workbook() + worksheet = workbook.active + worksheet.title = "Orders" + + header_font = Font(bold=True, color="FFFFFF") + header_fill = PatternFill(fill_type="solid", fgColor="2F5496") + header_alignment = Alignment(horizontal="center") + + headers = ["Order ID", "Created Date", "Vendor Name"] + for col_index, header in enumerate(headers, start=1): + cell = worksheet.cell(row=1, column=col_index, value=header) + cell.font = header_font + cell.fill = header_fill + cell.alignment = header_alignment + + for row_index, order in enumerate(trimmed_orders, start=2): + worksheet.cell(row=row_index, column=1, value=order.get("orderId")) + worksheet.cell(row=row_index, column=2, value=order.get("createdDate")) + worksheet.cell(row=row_index, column=3, value=order.get("vendorName")) + + for column in worksheet.columns: + max_length = max(len(str(cell.value or "")) for cell in column) + worksheet.column_dimensions[column[0].column_letter].width = max_length + 4 + + output_filename = f"orders_{restaurant_id}_{datetime.now(timezone.utc).strftime('%Y%m%d')}.xlsx" + workbook.save(output_filename) + print(f"\nExported {len(trimmed_orders)} order(s) to {output_filename}") + + +def main() -> None: + parser = argparse.ArgumentParser(description="Fetch Margin Edge order details for a restaurant") + parser.add_argument("--restaurant-id", type=str, required=True, help="Margin Edge restaurantUnitId") + parser.add_argument("--json", action="store_true", help="Output raw JSON instead of formatted table") + + args = parser.parse_args() + + end_date = datetime.now(timezone.utc).strftime("%Y-%m-%d") + start_date = (datetime.now(timezone.utc) - timedelta(days=60)).strftime("%Y-%m-%d") + + margin_edge_session = get_margin_edge_session() + + print(f"Fetching orders for restaurant {args.restaurant_id} ({start_date} → {end_date})...") + + orders = fetch_orders(margin_edge_session, args.restaurant_id, start_date, end_date) + + if not orders: + print("No orders found for this date range.") + sys.exit(0) + + print(f"Found {len(orders)} order(s).\n") + print(f"DEBUG - available fields in first order: {list(orders[0].keys())}\n") + + trimmed_orders = [ + { + "orderId": order.get("orderId", "N/A"), + "createdDate": order.get("createdDate", "N/A"), + "vendorName": order.get("vendorName", "N/A"), + } + for order in orders + ] + + if args.json: + print(json.dumps(trimmed_orders, indent=2)) + return + + for trimmed_order in trimmed_orders: + print_order_summary(trimmed_order) + + export_orders_to_excel(trimmed_orders, args.restaurant_id) + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/margin_edge_integration/import_margin_edge_to_culinary_command.py b/scripts/margin_edge_integration/import_margin_edge_to_culinary_command.py new file mode 100644 index 0000000..4e0ca69 --- /dev/null +++ b/scripts/margin_edge_integration/import_margin_edge_to_culinary_command.py @@ -0,0 +1,210 @@ +""" +import_margin_edge_to_culinary_command.py + +Reads an Excel file produced by get_margin_edge_order_details.py and imports +the vendorItemName entries as Ingredients into the CulinaryCommand RDS MySQL instance +for the matching location. + +Usage: + python3 import_margin_edge_to_culinary_command.py --input-file .xlsx [--dry-run] + +Environment variables (set in .env): + DB_HOST - RDS hostname + DB_PORT - RDS port (default: 3306) + DB_NAME - Database name (default: CulinaryCommandDB) + DB_USER - Database username + DB_PASSWORD - Database password +""" +# AI-ASSISTED + +import argparse +import os +import sys +from datetime import datetime, timezone +from typing import Optional + +import openpyxl +import pymysql +from dotenv import load_dotenv + +load_dotenv() + +PRAIRIE_CANARY_LOCATION_NAME = "Prairie Canary" + + +# --------------------------------------------------------------------------- +# Database helpers +# --------------------------------------------------------------------------- + +def get_db_connection() -> pymysql.Connection: + return pymysql.connect( + host=os.environ["DB_HOST"], + port=int(os.getenv("DB_PORT", "3306")), + database=os.environ["DB_NAME"], + user=os.environ["DB_USER"], + password=os.environ["DB_PASSWORD"], + charset="utf8mb4", + cursorclass=pymysql.cursors.DictCursor, + autocommit=False, + ) + + +def fetch_location_id_by_name(conn: pymysql.Connection, location_name: str) -> Optional[int]: + with conn.cursor() as cursor: + cursor.execute( + "SELECT id FROM Locations WHERE Name LIKE %s LIMIT 1", + (f"%{location_name}%",), + ) + row = cursor.fetchone() + return row["id"] if row else None + + +def fetch_default_unit_id(conn: pymysql.Connection) -> Optional[int]: + with conn.cursor() as cursor: + cursor.execute("SELECT id FROM Units WHERE Abbreviation IN ('ea', 'each', 'unit') LIMIT 1") + row = cursor.fetchone() + return row["id"] if row else None + + +def fetch_vendor_id_by_name(conn: pymysql.Connection, vendor_name: str, location_id: int) -> Optional[int]: + with conn.cursor() as cursor: + cursor.execute( + """SELECT v.id FROM Vendors v + INNER JOIN LocationVendors lv ON lv.VendorId = v.id + WHERE lv.LocationId = %s AND v.Name LIKE %s + LIMIT 1""", + (location_id, f"%{vendor_name}%"), + ) + row = cursor.fetchone() + return row["id"] if row else None + + +def upsert_ingredient( + conn: pymysql.Connection, + ingredient_name: str, + location_id: int, + vendor_id: Optional[int], + default_unit_id: Optional[int], + dry_run: bool, +) -> bool: + """Insert the ingredient if it does not already exist. Returns True if inserted, False if skipped.""" + with conn.cursor() as cursor: + cursor.execute( + "SELECT IngredientId FROM Ingredients WHERE LocationId = %s AND Name = %s LIMIT 1", + (location_id, ingredient_name), + ) + if cursor.fetchone(): + print(f" Skipping (already exists): {ingredient_name}") + return False + + print(f" {'[DRY RUN] ' if dry_run else ''}Inserting: {ingredient_name}") + if not dry_run: + now = datetime.now(timezone.utc) + cursor.execute( + """INSERT INTO Ingredients + (Name, LocationId, VendorId, UnitId, StockQuantity, ReorderLevel, Category, CreatedAt, UpdatedAt) + VALUES (%s, %s, %s, %s, 0, 0, '', %s, %s)""", + (ingredient_name, location_id, vendor_id, default_unit_id, now, now), + ) + return True + + +# --------------------------------------------------------------------------- +# Excel helpers +# --------------------------------------------------------------------------- + +def read_line_items_from_excel(input_file: str) -> list[dict]: + workbook = openpyxl.load_workbook(input_file) + worksheet = workbook.active + + headers = [cell.value for cell in worksheet[1]] + line_items = [] + for row in worksheet.iter_rows(min_row=2, values_only=True): + row_data = dict(zip(headers, row)) + culinary_name = row_data.get("Culinary Command Ingredient", "") + vendor_name = row_data.get("Vendor Name", "") + if culinary_name and culinary_name != "(no line items)": + line_items.append({ + "vendorItemName": str(culinary_name).strip(), + "vendorName": str(vendor_name).strip() if vendor_name else "", + }) + return line_items + + +# --------------------------------------------------------------------------- +# Entry point +# --------------------------------------------------------------------------- + +def main() -> None: + parser = argparse.ArgumentParser(description="Import Margin Edge line items as ingredients into CulinaryCommand") + parser.add_argument("--input-file", type=str, required=True, help="Path to the order_line_items Excel file") + parser.add_argument("--location-id", type=int, default=None, help="Location ID to import into (overrides name-based lookup)") + parser.add_argument("--dry-run", action="store_true", help="Preview inserts without writing to the database") + args = parser.parse_args() + + if not os.path.exists(args.input_file): + print(f"ERROR: File not found: {args.input_file}", file=sys.stderr) + sys.exit(1) + + print(f"Reading line items from {args.input_file}...") + line_items = read_line_items_from_excel(args.input_file) + + if not line_items: + print("No line items found in the Excel file.") + sys.exit(0) + + # Deduplicate by vendorItemName — we only need one row per unique ingredient + seen_ingredient_names: set[str] = set() + unique_line_items = [] + for line_item in line_items: + if line_item["vendorItemName"] not in seen_ingredient_names: + seen_ingredient_names.add(line_item["vendorItemName"]) + unique_line_items.append(line_item) + + print(f"Found {len(line_items)} total row(s), {len(unique_line_items)} unique ingredient(s).\n") + + conn = get_db_connection() + try: + if args.location_id: + location_id = args.location_id + print(f"Using provided location id={location_id}.\n") + else: + location_id = fetch_location_id_by_name(conn, PRAIRIE_CANARY_LOCATION_NAME) + if not location_id: + print(f"ERROR: Could not find location '{PRAIRIE_CANARY_LOCATION_NAME}' in the database.", file=sys.stderr) + sys.exit(1) + print(f"Found location '{PRAIRIE_CANARY_LOCATION_NAME}' (id={location_id}).\n") + + default_unit_id = fetch_default_unit_id(conn) + + inserted_count = 0 + skipped_count = 0 + + for line_item in unique_line_items: + ingredient_name = line_item["vendorItemName"] + vendor_name = line_item["vendorName"] + vendor_id = fetch_vendor_id_by_name(conn, vendor_name, location_id) if vendor_name else None + + was_inserted = upsert_ingredient(conn, ingredient_name, location_id, vendor_id, default_unit_id, args.dry_run) + if was_inserted: + inserted_count += 1 + else: + skipped_count += 1 + + if not args.dry_run: + conn.commit() + + print(f"\nDone. {inserted_count} inserted, {skipped_count} skipped (already existed).") + if args.dry_run: + print("Dry run — no changes were written to the database.") + + except Exception as error: + conn.rollback() + print(f"ERROR: {error}", file=sys.stderr) + raise + finally: + conn.close() + + +if __name__ == "__main__": + main() \ No newline at end of file diff --git a/scripts/margin_edge_integration/requirements.txt b/scripts/margin_edge_integration/requirements.txt new file mode 100644 index 0000000..478a527 --- /dev/null +++ b/scripts/margin_edge_integration/requirements.txt @@ -0,0 +1,4 @@ +python-dotenv +requests +PyMySQL +openpyxl \ No newline at end of file