randomisedbackend2 / app /xero_routes.py
akiko19191's picture
Upload folder using huggingface_hub
aecb7e8 verified
import logging
import traceback
import urllib.parse
import json
from flask import Blueprint, render_template, jsonify, request, flash, redirect, url_for, current_app, session, Response, stream_with_context
from pymongo import UpdateOne
from collections import defaultdict
from bson.binary import Binary
import random
import requests
# This part of the code assumes the Zoho client functions are in a file named `xero_client.py`
from .xero_client import (
zoho_token_required,
is_zoho_token_available,
initialize_zoho_sdk,
clear_zoho_token,
make_zoho_api_request
)
from .extensions import mongo
from utils import jsonify as jsonify_custom
from search_engine import search_and_filter_images, categorise
zoho_bp = Blueprint('zoho', __name__)
logger = logging.getLogger(__name__)
@zoho_bp.context_processor
def inject_zoho_status():
"""Makes the Zoho token status available to all templates."""
return dict(is_zoho_token_available=is_zoho_token_available)
@zoho_bp.route("/WEBUI")
def index():
"""Renders the home page with a welcome message."""
welcome_message = """
Welcome to the Zoho Books Integration Dashboard.
Use the navigation bar above to:
- Sync Inventory: Pull all items from Zoho, find images, and save to our database.
- Edit Inventory: Manually override image URLs for specific products.
Your current Zoho Token Status is: {}
""".format("Available" if is_zoho_token_available() else "Not Available. Please login.")
return render_template(
"code.html",
title="Home",
sub_title="Welcome to the Zoho Integration",
code=welcome_message,
language="text"
)
@zoho_bp.route("/login")
def login():
config = current_app.config
params = {
'scope': 'ZohoBooks.fullaccess.all',
'client_id': config['ZOHO_CLIENT_ID'],
'response_type': 'code',
'access_type': 'offline',
'redirect_uri': config['ZOHO_REDIRECT_URL'],
# Force consent so that Zoho returns a refresh token when possible
'prompt': 'consent'
}
accounts_url = 'https://accounts.zohocloud.ca/oauth/v2/auth'
auth_url = f"{accounts_url}?{urllib.parse.urlencode(params)}"
return redirect(auth_url)
@zoho_bp.route("/callback")
def oauth_callback():
grant_token = request.args.get('code')
accounts_server = request.args.get('accounts-server')
if not grant_token:
flash("Authorization failed: No grant token received from Zoho.", "error")
return redirect(url_for('zoho.index'))
try:
initialize_zoho_sdk(grant_token=grant_token, accounts_server_url=accounts_server)
flash("Successfully authenticated with Zoho Books!", "success")
next_url = session.pop('next_url', url_for('zoho.index'))
return redirect(next_url)
except Exception as e:
logger.error(f"Error during Zoho OAuth callback: {e}", exc_info=True)
flash(f"An error occurred during authentication: {e}", "error")
return redirect(url_for('zoho.index'))
@zoho_bp.route("/logout")
def logout():
clear_zoho_token()
flash("You have been logged out from Zoho.", "info")
return redirect(url_for("zoho.index"))
@zoho_bp.route("/api/inventory")
# @zoho_token_required
def inventory_sync_page():
"""Renders the page that will display the sync progress bar."""
return render_template("inventory_sync.html", title="Inventory Sync")
@zoho_bp.route("/api/inventory/stream")
# @zoho_token_required
def fetch_inventory_stream():
"""
Performs the inventory sync and streams progress updates to the client.
"""
def generate_sync_updates():
try:
yield f"data: {json.dumps({'progress': 0, 'message': 'Starting synchronization...'})}\n\n"
# Step 1: Fetch all items from Zoho
all_zoho_items = []
page = 1
has_more_pages = True
while has_more_pages:
yield f"data: {json.dumps({'progress': 5, 'message': f'Fetching page {page} of items from Zoho...'})}\n\n"
params = {'page': page, 'per_page': 200}
response_data, _ = make_zoho_api_request('GET', '/items', params=params)
if not response_data: break
page_items = response_data.get('items', [])
if not page_items: break
all_zoho_items.extend(item for item in page_items if item.get('status') == 'active')
has_more_pages = response_data.get('page_context', {}).get('has_more_page', False)
page += 1
total_items = len(all_zoho_items)
yield f"data: {json.dumps({'progress': 15, 'message': f'Found {total_items} active items. Grouping by product name...'})}\n\n"
# Step 2: Group items by name
grouped_products = defaultdict(lambda: {
'modes': {}, 'description': '', 'zoho_id_for_image': None, 'name': None, 'category': None
})
for item in all_zoho_items:
name = item.get('name')
unit = item.get('unit', 'piece').lower()
if not grouped_products[name]['description'] and item.get('description'): grouped_products[name]['description'] = item.get('description')
if not grouped_products[name]['category'] and item.get('cf_type'): grouped_products[name]['category'] = item.get('cf_type')
if not grouped_products[name]['zoho_id_for_image'] and item.get('image_document_id'): grouped_products[name]['zoho_id_for_image'] = item.get('item_id')
grouped_products[name]['name'] = name
grouped_products[name]['modes'][unit] = {'price': float(item.get('rate', 0.0)), 'zoho_id': item.get('item_id'), 'sku': item.get('sku')}
# Step 3: Process and sync with MongoDB
db_products_map = {p['name']: p for p in mongo.db.products.find({}, {'name': 1})}
products_to_insert, bulk_update_ops = [], []
total_to_process = len(grouped_products)
for i, (name, fetched_p) in enumerate(grouped_products.items()):
progress = 15 + int(((i + 1) / total_to_process) * 80)
yield f"data: {json.dumps({'progress': progress, 'message': f'Processing: {name}'})}\n\n"
image_data, image_content_type, fallback_image_url = None, None, None
zoho_image_id = fetched_p.get('zoho_id_for_image')
if zoho_image_id:
try:
content, headers = make_zoho_api_request('GET', f'/items/{zoho_image_id}/image')
if content and headers:
image_data = Binary(content)
image_content_type = headers.get('Content-Type', 'image/jpeg')
yield f"data: {json.dumps({'progress': progress, 'message': f'Downloaded existing image for {name} from Zoho.'})}\n\n"
except Exception:
yield f"data: {json.dumps({'progress': progress, 'message': f'Failed to get Zoho image for {name}. Will search online.'})}\n\n"
if not image_data:
yield f"data: {json.dumps({'progress': progress, 'message': f'Searching for new image for: {name}...'})}\n\n"
try:
results = search_and_filter_images(str(name))
if results:
fallback_image_url = str(results[0]["image_url"])
yield f"data: {json.dumps({'progress': progress, 'message': f'Downloading new image for: {name}...'})}\n\n"
response = requests.get(fallback_image_url, timeout=15)
response.raise_for_status()
image_content_from_url, content_type_from_url = response.content, response.headers.get('Content-Type', 'image/jpeg')
image_data, image_content_type = Binary(image_content_from_url), content_type_from_url
for mode_details in fetched_p['modes'].values():
item_id = mode_details.get('zoho_id')
if item_id:
yield f"data: {json.dumps({'progress': progress, 'message': f'Uploading image for {name} to Zoho Item ID {item_id}...'})}\n\n"
files = {'image': (f"{name.replace(' ', '_')}.jpg", image_content_from_url, content_type_from_url)}
make_zoho_api_request('POST', f'/items/{item_id}/image', files=files)
else:
yield f"data: {json.dumps({'progress': progress, 'message': f'No image results found for: {name}'})}\n\n"
except Exception as e:
yield f"data: {json.dumps({'progress': progress, 'message': f'Error fetching new image for {name}: {e}'})}\n\n"
image_data, image_content_type = None, None
update_fields = {
"name": name, "modes": fetched_p["modes"], "description": fetched_p.get('description', ''),
"image_data": image_data, "image_content_type": image_content_type, "image_url": fallback_image_url,
"category": fetched_p.get('category') or str(categorise(name)), "code": random.randint(1,100000),
}
if not db_products_map: products_to_insert.append(update_fields)
else: bulk_update_ops.append(UpdateOne({'name': name}, {'$set': update_fields}, upsert=True))
yield f"data: {json.dumps({'progress': 95, 'message': 'Finalizing database updates...'})}\n\n"
db_names, fetched_names = set(db_products_map.keys()), set(grouped_products.keys())
names_to_delete = list(db_names - fetched_names)
if names_to_delete: mongo.db.products.delete_many({'name': {'$in': names_to_delete}})
if products_to_insert: mongo.db.products.insert_many(products_to_insert)
if bulk_update_ops: mongo.db.products.bulk_write(bulk_update_ops)
summary = f"Sync complete. Inserted/Updated: {len(products_to_insert) + len(bulk_update_ops)}, Deleted: {len(names_to_delete)}"
final_code = jsonify_custom(list(grouped_products.values()))
yield f"data: {json.dumps({'progress': 100, 'message': summary, 'status': 'complete', 'final_code': final_code})}\n\n"
except Exception as e:
logger.error("Inventory sync stream failed: %s", e, exc_info=True)
error_message = f"Error during sync: {e}\n{traceback.format_exc()}"
yield f"data: {json.dumps({'progress': 100, 'message': error_message, 'status': 'error'})}\n\n"
return Response(stream_with_context(generate_sync_updates()), mimetype='text/event-stream')
@zoho_bp.route("/api/edit_inventory", methods=["GET", "POST"])
def edit_inventory():
if request.method == "POST":
product_name, new_image_url = request.form.get("product_name"), request.form.get("image_url")
if not product_name or not new_image_url:
flash("Product name and a new image URL are required.", "error")
else:
result = mongo.db.products.update_one(
{"name": product_name}, {"$set": {"image_url": new_image_url, "image_data": None, "image_content_type": None}}
)
flash(f"Image for '{product_name}' updated successfully!" if result.matched_count else f"Product '{product_name}' not found.", "success" if result.matched_count else "error")
return redirect(url_for("zoho.edit_inventory"))
products = list(mongo.db.products.find({}, {"_id": 0, "name": 1, "image_url": 1}).sort("name", 1))
return render_template("edit_inventory.html", title="Edit Inventory Image", products=products)