import json import logging from flask import Blueprint, render_template, redirect, url_for, jsonify, request from pymongo import UpdateOne from xero_python.accounting import AccountingApi from utils import jsonify as jsonify_xero from search_engine import search_and_filter_images, categorise from .xero_client import xero, api_client, store_xero_oauth2_token, obtain_xero_oauth2_token, xero_token_required, get_xero_tenant_id from .extensions import mongo # <-- IMPORT MONGO xero_bp = Blueprint('xero', __name__) logger = logging.getLogger(__name__) @xero_bp.route("/") def index(): xero_access = dict(obtain_xero_oauth2_token() or {}) return render_template( "code.html", title="Home | OAuth Token", code=json.dumps(xero_access, sort_keys=True, indent=4), ) @xero_bp.route("/login") def login(): redirect_url = url_for("xero.oauth_callback", _external=True) return xero.authorize(callback_uri=redirect_url) @xero_bp.route("/callback") def oauth_callback(): try: response = xero.authorized_response() except Exception as e: logger.error("OAuth callback failed: %s", e) return "OAuth callback failed.", 500 if response is None or response.get("access_token") is None: return "Access denied: response=%s" % response store_xero_oauth2_token(response) return redirect(url_for("xero.index", _external=True)) @xero_bp.route("/logout") def logout(): store_xero_oauth2_token(None) return redirect(url_for("xero.index", _external=True)) @xero_bp.route("/api/inventory") @xero_token_required def fetch_inventory(): try: xero_tenant_id = get_xero_tenant_id() accounting_api = AccountingApi(api_client) xero_items = accounting_api.get_items(xero_tenant_id=xero_tenant_id).items fetched_products = [{ "code": item.code, "name": item.name, "price": float(item.sales_details.unit_price) if item.sales_details else 0.0, "unit": item.description, "on_hand": item.quantity_on_hand } for item in xero_items] # Use mongo.db directly db_products = list(mongo.db.products.find({})) db_products_map = {p['code']: p for p in db_products if 'code' in p} fetched_products_map = {p['code']: p for p in fetched_products} products_to_insert = [] bulk_update_ops = [] for code, fetched_p in fetched_products_map.items(): db_p = db_products_map.get(code) if not db_p: new_doc = { "code": fetched_p["code"], "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"], "category": str(categorise(fetched_p["name"])), "image_url": str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"]),} products_to_insert.append(new_doc) elif (fetched_p['name'] != db_p.get('name') or fetched_p['price'] != db_p.get('price') or fetched_p['unit'] != db_p.get('unit')): update_fields = { "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"] } if fetched_p['name'] != db_p.get('name'): update_fields["category"] = str(categorise(fetched_p["name"])) update_fields["image_url"] = str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"]) bulk_update_ops.append(UpdateOne({'code': code}, {'$set': update_fields})) db_codes = set(db_products_map.keys()) fetched_codes = set(fetched_products_map.keys()) codes_to_delete = list(db_codes - fetched_codes) # Use mongo.db directly if codes_to_delete: mongo.db.products.delete_many({'code': {'$in': codes_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) sub_title = (f"Sync complete. Inserted: {len(products_to_insert)}, " f"Updated: {len(bulk_update_ops)}, Deleted: {len(codes_to_delete)}") code_to_display = jsonify_xero(fetched_products) except Exception as e: logger.error("Inventory sync failed: %s", e) sub_title = f"Error during sync: {e}" code_to_display = jsonify({"error": str(e)}) return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)