Spaces:
Running
Running
Upload folder using huggingface_hub
Browse files- __pycache__/search_engine.cpython-311.pyc +0 -0
- app/__init__.py +13 -27
- app/__pycache__/__init__.cpython-311.pyc +0 -0
- app/__pycache__/ai_features.cpython-311.pyc +0 -0
- app/__pycache__/api.cpython-311.pyc +0 -0
- app/__pycache__/config.cpython-311.pyc +0 -0
- app/__pycache__/page_features.cpython-311.pyc +0 -0
- app/__pycache__/xero_client.cpython-311.pyc +0 -0
- app/__pycache__/xero_routes.cpython-311.pyc +0 -0
- app/__pycache__/xero_utils.cpython-311.pyc +0 -0
- app/ai_features.py +47 -20
- app/api.py +222 -72
- app/config.py +40 -23
- app/page_features.py +45 -3
- app/templates/base.html +192 -37
- app/templates/code.html +17 -9
- app/templates/inventory_sync.html +82 -0
- app/xero_client.py +203 -52
- app/xero_routes.py +208 -124
- app/xero_utils.py +230 -305
- auth_token.py +17 -0
- downloaded_image.jpeg +0 -0
- downloaded_image.jpeg;charset=UTF-8 +0 -0
- flask_session/aa71dde20eaf768ca7e5f90a25563ea6 +0 -0
- run.py +1 -1
- search_engine.py +4 -2
- zoho_resources/resources/dmFpYmhhdmFyZHVpbm9odHRwczovL3d3dy56b2hvYXBpcy5pbg==.json +1 -0
- zoho_sdk.log +67 -0
- zoho_tokens.txt +1 -0
__pycache__/search_engine.cpython-311.pyc
CHANGED
Binary files a/__pycache__/search_engine.cpython-311.pyc and b/__pycache__/search_engine.cpython-311.pyc differ
|
|
app/__init__.py
CHANGED
@@ -2,18 +2,12 @@ import logging
|
|
2 |
from flask import Flask, jsonify
|
3 |
|
4 |
from . import config
|
5 |
-
from .extensions import mongo, bcrypt, jwt, cors, session
|
6 |
-
from .xero_client import api_client, xero # <-- IMPORT XERO REMOTE APP
|
7 |
from .ai_features import ai_bp
|
8 |
from .page_features import page_bp
|
9 |
-
# app.register_blueprint(api_bp, url_prefix='/api')
|
10 |
-
def create_app():
|
11 |
|
|
|
12 |
app = Flask(__name__)
|
13 |
-
|
14 |
-
|
15 |
-
app.register_blueprint(ai_bp, url_prefix='/api')
|
16 |
-
app.register_blueprint(page_bp, url_prefix='/api/pages')
|
17 |
app.config.from_object(config)
|
18 |
|
19 |
logging.basicConfig(level=logging.INFO)
|
@@ -24,24 +18,14 @@ def create_app():
|
|
24 |
jwt.init_app(app)
|
25 |
cors.init_app(app, supports_credentials=True)
|
26 |
session.init_app(app)
|
27 |
-
oauth.init_app(app)
|
28 |
|
29 |
-
#
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
xero.client_secret = client_secret
|
37 |
-
|
38 |
-
# 2. Configure the xero-python SDK api_client
|
39 |
-
api_client.configuration.debug = app.config['DEBUG']
|
40 |
-
api_client.configuration.oauth2_token.client_id = client_id
|
41 |
-
api_client.configuration.oauth2_token.client_secret = client_secret
|
42 |
-
else:
|
43 |
-
logging.warning("Xero CLIENT_ID and/or CLIENT_SECRET are not configured.")
|
44 |
-
|
45 |
|
46 |
# Register JWT error handlers
|
47 |
@jwt.unauthorized_loader
|
@@ -58,9 +42,11 @@ def create_app():
|
|
58 |
|
59 |
# Register blueprints to organize routes
|
60 |
from .api import api_bp
|
61 |
-
from .xero_routes import
|
62 |
|
|
|
63 |
app.register_blueprint(api_bp, url_prefix='/api')
|
64 |
-
app.register_blueprint(
|
|
|
65 |
|
66 |
return app
|
|
|
2 |
from flask import Flask, jsonify
|
3 |
|
4 |
from . import config
|
5 |
+
from .extensions import mongo, bcrypt, jwt, cors, session
|
|
|
6 |
from .ai_features import ai_bp
|
7 |
from .page_features import page_bp
|
|
|
|
|
8 |
|
9 |
+
def create_app():
|
10 |
app = Flask(__name__)
|
|
|
|
|
|
|
|
|
11 |
app.config.from_object(config)
|
12 |
|
13 |
logging.basicConfig(level=logging.INFO)
|
|
|
18 |
jwt.init_app(app)
|
19 |
cors.init_app(app, supports_credentials=True)
|
20 |
session.init_app(app)
|
|
|
21 |
|
22 |
+
# --- ZOHO SDK INITIALIZATION ---
|
23 |
+
# We attempt initialization at startup. If the token file exists, the SDK
|
24 |
+
# will be ready. If not, the login flow will trigger initialization later.
|
25 |
+
with app.app_context():
|
26 |
+
from .xero_client import initialize_zoho_sdk
|
27 |
+
initialize_zoho_sdk()
|
28 |
+
# --- END ZOHO SDK INITIALIZATION ---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
29 |
|
30 |
# Register JWT error handlers
|
31 |
@jwt.unauthorized_loader
|
|
|
42 |
|
43 |
# Register blueprints to organize routes
|
44 |
from .api import api_bp
|
45 |
+
from .xero_routes import zoho_bp
|
46 |
|
47 |
+
app.register_blueprint(page_bp, url_prefix='/api/pages')
|
48 |
app.register_blueprint(api_bp, url_prefix='/api')
|
49 |
+
app.register_blueprint(ai_bp, url_prefix='/api')
|
50 |
+
app.register_blueprint(zoho_bp)
|
51 |
|
52 |
return app
|
app/__pycache__/__init__.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/__init__.cpython-311.pyc and b/app/__pycache__/__init__.cpython-311.pyc differ
|
|
app/__pycache__/ai_features.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/ai_features.cpython-311.pyc and b/app/__pycache__/ai_features.cpython-311.pyc differ
|
|
app/__pycache__/api.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/api.cpython-311.pyc and b/app/__pycache__/api.cpython-311.pyc differ
|
|
app/__pycache__/config.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/config.cpython-311.pyc and b/app/__pycache__/config.cpython-311.pyc differ
|
|
app/__pycache__/page_features.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/page_features.cpython-311.pyc and b/app/__pycache__/page_features.cpython-311.pyc differ
|
|
app/__pycache__/xero_client.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/xero_client.cpython-311.pyc and b/app/__pycache__/xero_client.cpython-311.pyc differ
|
|
app/__pycache__/xero_routes.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/xero_routes.cpython-311.pyc and b/app/__pycache__/xero_routes.cpython-311.pyc differ
|
|
app/__pycache__/xero_utils.cpython-311.pyc
CHANGED
Binary files a/app/__pycache__/xero_utils.cpython-311.pyc and b/app/__pycache__/xero_utils.cpython-311.pyc differ
|
|
app/ai_features.py
CHANGED
@@ -14,7 +14,7 @@ from google.genai import types
|
|
14 |
import os
|
15 |
import json
|
16 |
import traceback
|
17 |
-
from .xero_utils import
|
18 |
from .email_utils import send_order_confirmation_email
|
19 |
# +++ START: WHATSAPP FEATURE IMPORTS +++
|
20 |
import requests
|
@@ -309,16 +309,16 @@ cancel_order_function_website = {
|
|
309 |
@ai_bp.route('/chat', methods=['POST'])
|
310 |
@jwt_required()
|
311 |
def handle_ai_chat():
|
|
|
312 |
|
313 |
# ... (Web chat endpoint remains unchanged)
|
314 |
user_email = get_jwt_identity()
|
315 |
-
|
316 |
-
all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
|
317 |
product_context_list = []
|
318 |
for p in all_products:
|
319 |
if 'name' not in p: continue
|
320 |
-
|
321 |
-
available_modes =
|
322 |
if available_modes:
|
323 |
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
324 |
else:
|
@@ -485,13 +485,12 @@ def whatsapp_reply():
|
|
485 |
|
486 |
# --- 2. Setup AI Client and Product Context ---
|
487 |
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
|
488 |
-
|
489 |
-
all_products = list(mongo.db.products.find({}, {'name': 1, 'unit': 1, '_id': 0}))
|
490 |
product_context_list = []
|
491 |
for p in all_products:
|
492 |
if 'name' not in p: continue
|
493 |
-
|
494 |
-
available_modes =
|
495 |
if available_modes:
|
496 |
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
497 |
else:
|
@@ -607,13 +606,14 @@ def whatsapp_reply():
|
|
607 |
validated_items, error_items = [], []
|
608 |
for item in items_from_args:
|
609 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
610 |
-
|
611 |
-
|
|
|
612 |
else:
|
613 |
-
error_items.append(item.get(
|
614 |
|
615 |
if error_items:
|
616 |
-
final_response_text = f"I couldn't place the order for *{user_name}* because these products were not found: {', '.join(error_items)}. Please try again."
|
617 |
else:
|
618 |
next_serial = get_next_order_serial()
|
619 |
order_doc = {'user_email': user_email, 'items': validated_items, 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
|
@@ -631,20 +631,22 @@ def whatsapp_reply():
|
|
631 |
"mode": item.get('mode', 'pieces')
|
632 |
} for item in validated_items]
|
633 |
send_order_confirmation_email(order_doc, user_id)
|
634 |
-
|
635 |
final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
636 |
|
637 |
elif function_call.name == 'add_items_to_cart':
|
638 |
items_to_add, added_messages, db_items = args.get('items', []), [], []
|
639 |
for item in items_to_add:
|
640 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
641 |
-
|
642 |
-
|
643 |
-
|
|
|
644 |
if modes == 'weight':
|
645 |
modes='lb'
|
646 |
added_messages.append(f"{item.get('quantity')} {modes} of {p_doc['name']}")
|
647 |
-
else:
|
|
|
648 |
if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
|
649 |
final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."
|
650 |
|
@@ -661,7 +663,7 @@ def whatsapp_reply():
|
|
661 |
"order_id": str(order_id), "user_email": user_email, "items": cart['items'],
|
662 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
663 |
}
|
664 |
-
|
665 |
order_doc['_id'] = order_id
|
666 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
667 |
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
@@ -699,6 +701,15 @@ def whatsapp_reply():
|
|
699 |
final_response_text ="Here are your recent orders:\n" + details_text
|
700 |
else:
|
701 |
final_response_text = response.text
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
702 |
|
703 |
# --- 6. Save Conversation to Database ---
|
704 |
user_message_to_save = user_message_text if user_message_text else "Audio message"
|
@@ -716,5 +727,21 @@ def whatsapp_reply():
|
|
716 |
current_app.logger.error(f"WhatsApp endpoint error: {e}")
|
717 |
final_response_text = "I'm having a little trouble right now. Please try again in a moment."
|
718 |
|
719 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
720 |
return str(twilio_resp)
|
|
|
14 |
import os
|
15 |
import json
|
16 |
import traceback
|
17 |
+
from .xero_utils import trigger_invoice_creation,trigger_contact_creation
|
18 |
from .email_utils import send_order_confirmation_email
|
19 |
# +++ START: WHATSAPP FEATURE IMPORTS +++
|
20 |
import requests
|
|
|
309 |
@ai_bp.route('/chat', methods=['POST'])
|
310 |
@jwt_required()
|
311 |
def handle_ai_chat():
|
312 |
+
current_app.logger.info("Handling AI chat request.")
|
313 |
|
314 |
# ... (Web chat endpoint remains unchanged)
|
315 |
user_email = get_jwt_identity()
|
316 |
+
all_products = list(mongo.db.products.find({}, {'name': 1, 'modes': 1, '_id': 0}))
|
|
|
317 |
product_context_list = []
|
318 |
for p in all_products:
|
319 |
if 'name' not in p: continue
|
320 |
+
modes_obj = p.get('modes', {})
|
321 |
+
available_modes = list(modes_obj.keys()) if modes_obj else []
|
322 |
if available_modes:
|
323 |
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
324 |
else:
|
|
|
485 |
|
486 |
# --- 2. Setup AI Client and Product Context ---
|
487 |
client = genai.Client(api_key=os.getenv("GEMINI_API_KEY"))
|
488 |
+
all_products = list(mongo.db.products.find({}, {'name': 1, 'modes': 1, '_id': 0}))
|
|
|
489 |
product_context_list = []
|
490 |
for p in all_products:
|
491 |
if 'name' not in p: continue
|
492 |
+
modes_obj = p.get('modes', {})
|
493 |
+
available_modes = list(modes_obj.keys()) if modes_obj else []
|
494 |
if available_modes:
|
495 |
product_context_list.append(f"{p['name']} (available units: {', '.join(available_modes)})")
|
496 |
else:
|
|
|
606 |
validated_items, error_items = [], []
|
607 |
for item in items_from_args:
|
608 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
609 |
+
unit = item.get("unit")
|
610 |
+
if p_doc and unit in p_doc.get("modes", {}):
|
611 |
+
validated_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": unit})
|
612 |
else:
|
613 |
+
error_items.append(f"{item.get('product_name')} (unit: {unit})")
|
614 |
|
615 |
if error_items:
|
616 |
+
final_response_text = f"I couldn't place the order for *{user_name}* because these products/units were not found or are invalid: {', '.join(error_items)}. Please try again."
|
617 |
else:
|
618 |
next_serial = get_next_order_serial()
|
619 |
order_doc = {'user_email': user_email, 'items': validated_items, 'delivery_date': args.get('delivery_date'), 'delivery_address': user.get('businessAddress'), 'mobile_number': user.get('phoneNumber'), 'additional_info': args.get('additional_info', ''), 'status': 'pending', 'created_at': datetime.utcnow(),'serial_no': next_serial}
|
|
|
631 |
"mode": item.get('mode', 'pieces')
|
632 |
} for item in validated_items]
|
633 |
send_order_confirmation_email(order_doc, user_id)
|
634 |
+
trigger_invoice_creation(order_details_for_xero)
|
635 |
final_response_text = f"Thank you! I have directly placed order #{next_serial} for *{user_name}* for delivery on {args.get('delivery_date')}. They can view details at https://matax-express.vercel.app/."
|
636 |
|
637 |
elif function_call.name == 'add_items_to_cart':
|
638 |
items_to_add, added_messages, db_items = args.get('items', []), [], []
|
639 |
for item in items_to_add:
|
640 |
p_doc = mongo.db.products.find_one({'name': {'$regex': f'^{item.get("product_name")}$', '$options': 'i'}})
|
641 |
+
unit = item.get("unit")
|
642 |
+
if p_doc and unit in p_doc.get("modes", {}):
|
643 |
+
db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": unit})
|
644 |
+
modes=unit
|
645 |
if modes == 'weight':
|
646 |
modes='lb'
|
647 |
added_messages.append(f"{item.get('quantity')} {modes} of {p_doc['name']}")
|
648 |
+
else:
|
649 |
+
added_messages.append(f"could not find '{item.get('product_name')}' or unit '{unit}' is invalid")
|
650 |
if db_items: mongo.db.carts.update_one({'user_email': user_email}, {'$push': {'items': {'$each': db_items}}, '$set': {'updated_at': datetime.utcnow()}}, upsert=True)
|
651 |
final_response_text = f"OK, I've updated the cart for *{user_name}*: I added {', '.join(added_messages)}."
|
652 |
|
|
|
663 |
"order_id": str(order_id), "user_email": user_email, "items": cart['items'],
|
664 |
"delivery_address": user.get('businessAddress'), "mobile_number": user.get('phoneNumber'),"deliverydate": args.get('delivery_date')
|
665 |
}
|
666 |
+
trigger_invoice_creation(order_details_for_xero)
|
667 |
order_doc['_id'] = order_id
|
668 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
669 |
products_map = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
|
|
|
701 |
final_response_text ="Here are your recent orders:\n" + details_text
|
702 |
else:
|
703 |
final_response_text = response.text
|
704 |
+
current_app.logger.info(f"AI response: {final_response_text}")
|
705 |
+
try:
|
706 |
+
final_response_text = final_response_text.replace("weight","pound")
|
707 |
+
except Exception as e:
|
708 |
+
pass
|
709 |
+
try:
|
710 |
+
final_response_text = final_response_text.replace("Weight","pound")
|
711 |
+
except Exception as e:
|
712 |
+
pass
|
713 |
|
714 |
# --- 6. Save Conversation to Database ---
|
715 |
user_message_to_save = user_message_text if user_message_text else "Audio message"
|
|
|
727 |
current_app.logger.error(f"WhatsApp endpoint error: {e}")
|
728 |
final_response_text = "I'm having a little trouble right now. Please try again in a moment."
|
729 |
|
730 |
+
if len(final_response_text) > 1600:
|
731 |
+
# Find the last space before the 1600 character limit to avoid splitting a word.
|
732 |
+
split_point = final_response_text[:1600].rfind(' ')
|
733 |
+
|
734 |
+
# If no space is found (very long word/URL), fall back to a hard split at 1600.
|
735 |
+
if split_point == -1:
|
736 |
+
split_point = 1600
|
737 |
+
|
738 |
+
part1 = final_response_text[:split_point]
|
739 |
+
# Use .strip() to remove any leading space from the second part.
|
740 |
+
part2 = final_response_text[split_point:].strip()
|
741 |
+
|
742 |
+
twilio_resp.message(part1)
|
743 |
+
twilio_resp.message("❌TWILLIO ERROR:MESSAGE WAS TOO LONG, SO SOME PARTS OF THE MESSAGE WERE NOT SENT.")
|
744 |
+
else:
|
745 |
+
twilio_resp.message(final_response_text)
|
746 |
+
|
747 |
return str(twilio_resp)
|
app/api.py
CHANGED
@@ -1,26 +1,82 @@
|
|
1 |
-
#
|
2 |
|
3 |
-
from flask import Blueprint, request, jsonify, current_app, redirect
|
4 |
from bson.objectid import ObjectId
|
5 |
from datetime import datetime
|
6 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
7 |
from .extensions import bcrypt, mongo
|
8 |
-
from .
|
|
|
9 |
from .email_utils import send_order_confirmation_email, send_registration_email, send_registration_admin_notification, send_login_notification_email, send_cart_reminder_email
|
10 |
from .general_utils import get_next_order_serial
|
11 |
|
12 |
|
13 |
api_bp = Blueprint('api', __name__)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
14 |
@api_bp.route('/sync_xero_users', methods=['GET'])
|
15 |
def sync_xero_users():
|
16 |
-
|
17 |
return "✅"
|
18 |
|
19 |
-
@api_bp.route('/clear')
|
20 |
-
def clear_all():
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
|
25 |
@api_bp.route('/register', methods=['POST'])
|
26 |
def register():
|
@@ -53,7 +109,6 @@ def register():
|
|
53 |
|
54 |
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
|
55 |
|
56 |
-
# ... (the rest of your api.py file remains unchanged)
|
57 |
@api_bp.route('/login', methods=['POST'])
|
58 |
def login():
|
59 |
data = request.get_json()
|
@@ -89,14 +144,6 @@ def get_user_profile():
|
|
89 |
|
90 |
return jsonify(profile_data), 200
|
91 |
|
92 |
-
@api_bp.route('/products', methods=['GET'])
|
93 |
-
def get_products():
|
94 |
-
products = [{
|
95 |
-
'id': str(p['_id']), 'name': p.get('name'), 'category': p.get('category'),
|
96 |
-
'unit': p.get('unit'), 'image_url': p.get('image_url', ''), 'price': p.get('price', '')
|
97 |
-
} for p in mongo.db.products.find()]
|
98 |
-
return jsonify(products)
|
99 |
-
|
100 |
|
101 |
@api_bp.route('/cart', methods=['GET', 'POST'])
|
102 |
@jwt_required()
|
@@ -112,15 +159,36 @@ def handle_cart():
|
|
112 |
if cart.get('items'):
|
113 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
114 |
if product_ids:
|
115 |
-
|
|
|
|
|
116 |
for item in cart['items']:
|
117 |
details = products.get(item['productId'])
|
118 |
if details:
|
119 |
-
|
120 |
-
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
|
125 |
return jsonify({
|
126 |
'items': populated_items,
|
@@ -135,26 +203,21 @@ def handle_cart():
|
|
135 |
'updated_at': datetime.utcnow()
|
136 |
}
|
137 |
|
138 |
-
# --- MODIFICATION START ---
|
139 |
if 'items' in data:
|
140 |
sanitized_items = []
|
141 |
for item in data['items']:
|
142 |
try:
|
143 |
-
# Ensure required fields exist and are not None
|
144 |
if not all(k in item for k in ['productId', 'quantity', 'mode']) or item['quantity'] is None:
|
145 |
-
continue
|
146 |
|
147 |
mode = item.get('mode')
|
148 |
quantity = item.get('quantity')
|
149 |
|
150 |
if mode == 'weight':
|
151 |
-
# For weight, convert to float. Handles integer and float strings.
|
152 |
numeric_quantity = float(quantity)
|
153 |
else:
|
154 |
-
|
155 |
-
numeric_quantity = int(float(quantity)) # Use float first to handle "1.0"
|
156 |
|
157 |
-
# Ensure quantity is not negative
|
158 |
if numeric_quantity < 0:
|
159 |
continue
|
160 |
|
@@ -165,11 +228,9 @@ def handle_cart():
|
|
165 |
})
|
166 |
|
167 |
except (ValueError, TypeError):
|
168 |
-
# If quantity is not a valid number for its mode, return an error
|
169 |
return jsonify({"msg": f"Invalid quantity format for item."}), 400
|
170 |
|
171 |
update_doc['items'] = sanitized_items
|
172 |
-
# --- MODIFICATION END ---
|
173 |
|
174 |
if 'deliveryDate' in data:
|
175 |
update_doc['deliveryDate'] = data['deliveryDate']
|
@@ -181,6 +242,43 @@ def handle_cart():
|
|
181 |
)
|
182 |
return jsonify({"msg": "Cart updated successfully"})
|
183 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
184 |
@api_bp.route('/orders', methods=['GET', 'POST'])
|
185 |
@jwt_required()
|
186 |
def handle_orders():
|
@@ -208,10 +306,10 @@ def handle_orders():
|
|
208 |
order_doc['_id'] = order_id
|
209 |
|
210 |
order_details_for_xero = {
|
211 |
-
"order_id":
|
212 |
-
"delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"]
|
213 |
}
|
214 |
-
|
215 |
|
216 |
try:
|
217 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
@@ -236,22 +334,58 @@ def handle_orders():
|
|
236 |
if not user_orders: return jsonify([])
|
237 |
|
238 |
all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
|
239 |
-
|
|
|
240 |
|
241 |
for order in user_orders:
|
242 |
-
|
243 |
-
|
244 |
-
|
245 |
-
|
246 |
-
|
247 |
-
|
248 |
-
|
249 |
-
|
250 |
-
|
251 |
-
|
252 |
-
|
253 |
-
|
254 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
255 |
order['_id'] = str(order['_id'])
|
256 |
order['created_at'] = order['created_at'].isoformat()
|
257 |
order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
|
@@ -311,16 +445,43 @@ def cancel_order(order_id):
|
|
311 |
|
312 |
if not order:
|
313 |
return jsonify({"msg": "Order not found or access denied"}), 404
|
314 |
-
|
315 |
-
|
316 |
-
|
317 |
-
|
318 |
-
|
319 |
-
|
320 |
-
|
321 |
-
|
322 |
-
|
323 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
324 |
|
325 |
@api_bp.route('/sendmail', methods=['GET'])
|
326 |
def send_cart_reminders():
|
@@ -382,14 +543,9 @@ def approve_user(user_id):
|
|
382 |
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
|
383 |
return jsonify({"msg": f"User {user_id} approved"})
|
384 |
|
385 |
-
# +++ START: NEW ENDPOINT FOR ITEM REQUESTS +++
|
386 |
@api_bp.route('/request-item', methods=['POST'])
|
387 |
@jwt_required()
|
388 |
def request_item():
|
389 |
-
"""
|
390 |
-
Allows a logged-in user to request an item that is not in the catalog.
|
391 |
-
The request is saved to the database for admin review.
|
392 |
-
"""
|
393 |
user_email = get_jwt_identity()
|
394 |
data = request.get_json()
|
395 |
|
@@ -401,7 +557,6 @@ def request_item():
|
|
401 |
return jsonify({"msg": "Item details cannot be empty."}), 400
|
402 |
|
403 |
try:
|
404 |
-
# Fetch user info for more context in the request
|
405 |
user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
|
406 |
company_name = user.get('company_name', 'N/A') if user else 'N/A'
|
407 |
|
@@ -409,15 +564,10 @@ def request_item():
|
|
409 |
'user_email': user_email,
|
410 |
'company_name': company_name,
|
411 |
'details': details,
|
412 |
-
'status': 'new',
|
413 |
'requested_at': datetime.utcnow()
|
414 |
}
|
415 |
-
|
416 |
-
# The collection 'item_requests' will be created if it doesn't exist
|
417 |
mongo.db.item_requests.insert_one(request_doc)
|
418 |
-
|
419 |
-
# Optional: Here you could add a call to an email utility to notify admins
|
420 |
-
# For example: send_item_request_notification(user_email, company_name, details)
|
421 |
|
422 |
return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
|
423 |
|
|
|
1 |
+
# api.py
|
2 |
|
3 |
+
from flask import Blueprint, request, jsonify, current_app, redirect, Response, url_for
|
4 |
from bson.objectid import ObjectId
|
5 |
from datetime import datetime
|
6 |
from flask_jwt_extended import create_access_token, jwt_required, get_jwt_identity
|
7 |
from .extensions import bcrypt, mongo
|
8 |
+
from .xero_client import make_zoho_api_request
|
9 |
+
from .xero_utils import trigger_invoice_creation, trigger_contact_creation,sync_user_approval_from_zoho
|
10 |
from .email_utils import send_order_confirmation_email, send_registration_email, send_registration_admin_notification, send_login_notification_email, send_cart_reminder_email
|
11 |
from .general_utils import get_next_order_serial
|
12 |
|
13 |
|
14 |
api_bp = Blueprint('api', __name__)
|
15 |
+
|
16 |
+
# --- NEW: Endpoint to serve images stored in MongoDB ---
|
17 |
+
@api_bp.route('/product_image/<product_id>')
|
18 |
+
def get_product_image(product_id):
|
19 |
+
"""
|
20 |
+
Fetches image data stored as binary in the products collection.
|
21 |
+
"""
|
22 |
+
try:
|
23 |
+
product = mongo.db.products.find_one(
|
24 |
+
{'_id': ObjectId(product_id)},
|
25 |
+
{'image_data': 1, 'image_content_type': 1} # Projection to get only needed fields
|
26 |
+
)
|
27 |
+
|
28 |
+
if product and 'image_data' in product and product['image_data'] is not None:
|
29 |
+
# Clean the content_type string to ensure it's a valid MIME type for browsers.
|
30 |
+
content_type = product.get('image_content_type', 'image/jpeg')
|
31 |
+
mime_type = content_type.split(';')[0].strip()
|
32 |
+
|
33 |
+
# Serve the binary data with the correct mimetype
|
34 |
+
return Response(product['image_data'], mimetype=mime_type)
|
35 |
+
else:
|
36 |
+
# Return a 404 Not Found if the product or its image data doesn't exist
|
37 |
+
return jsonify({"msg": "Image not found"}), 404
|
38 |
+
|
39 |
+
except Exception as e:
|
40 |
+
current_app.logger.error(f"Error serving image for product_id {product_id}: {e}")
|
41 |
+
return jsonify({"msg": "Error serving image"}), 500
|
42 |
+
|
43 |
+
|
44 |
+
@api_bp.route('/products', methods=['GET'])
|
45 |
+
def get_products():
|
46 |
+
# --- MODIFIED: Construct the correct image_url pointing to our new endpoint ---
|
47 |
+
products_cursor = mongo.db.products.find()
|
48 |
+
products_list = []
|
49 |
+
for p in products_cursor:
|
50 |
+
image_url = None
|
51 |
+
# If image_data exists, create a URL to our new endpoint
|
52 |
+
if p.get('image_data'):
|
53 |
+
# FIX: Generate an absolute URL to avoid cross-origin issues with the frontend dev server.
|
54 |
+
image_url = url_for('api.get_product_image', product_id=str(p['_id']), _external=True)
|
55 |
+
# Otherwise, use the fallback AI-generated URL if it exists
|
56 |
+
elif p.get('image_url'):
|
57 |
+
image_url = p.get('image_url')
|
58 |
+
|
59 |
+
products_list.append({
|
60 |
+
'id': str(p['_id']),
|
61 |
+
'name': p.get('name'),
|
62 |
+
'category': p.get('category'),
|
63 |
+
'modes': p.get('modes'),
|
64 |
+
'image_url': image_url, # This will be the correct, usable URL
|
65 |
+
'description': p.get('description', '')
|
66 |
+
})
|
67 |
+
return jsonify(products_list)
|
68 |
+
|
69 |
+
# ... (The rest of your api.py file remains unchanged)
|
70 |
@api_bp.route('/sync_xero_users', methods=['GET'])
|
71 |
def sync_xero_users():
|
72 |
+
sync_user_approval_from_zoho()
|
73 |
return "✅"
|
74 |
|
75 |
+
# @api_bp.route('/clear')
|
76 |
+
# def clear_all():
|
77 |
+
# mongo.db.users.delete_many({})
|
78 |
+
# mongo.db.orders.delete_many({})
|
79 |
+
# return "✅"
|
80 |
|
81 |
@api_bp.route('/register', methods=['POST'])
|
82 |
def register():
|
|
|
109 |
|
110 |
return jsonify({"msg": "Registration successful! Your application is being processed."}), 201
|
111 |
|
|
|
112 |
@api_bp.route('/login', methods=['POST'])
|
113 |
def login():
|
114 |
data = request.get_json()
|
|
|
144 |
|
145 |
return jsonify(profile_data), 200
|
146 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
147 |
|
148 |
@api_bp.route('/cart', methods=['GET', 'POST'])
|
149 |
@jwt_required()
|
|
|
159 |
if cart.get('items'):
|
160 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
161 |
if product_ids:
|
162 |
+
products_cursor = mongo.db.products.find({'_id': {'$in': product_ids}})
|
163 |
+
products = {str(p['_id']): p for p in products_cursor}
|
164 |
+
|
165 |
for item in cart['items']:
|
166 |
details = products.get(item['productId'])
|
167 |
if details:
|
168 |
+
mode = item.get('mode', 'piece')
|
169 |
+
mode_details = details.get('modes', {}).get(mode)
|
170 |
+
|
171 |
+
if mode_details:
|
172 |
+
price = mode_details.get('price')
|
173 |
+
|
174 |
+
image_url = None
|
175 |
+
if details.get('image_data'):
|
176 |
+
# FIX: Generate an absolute URL.
|
177 |
+
image_url = url_for('api.get_product_image', product_id=str(details['_id']), _external=True)
|
178 |
+
elif details.get('image_url'):
|
179 |
+
image_url = details.get('image_url')
|
180 |
+
|
181 |
+
populated_items.append({
|
182 |
+
'product': {
|
183 |
+
'id': str(details['_id']),
|
184 |
+
'name': details.get('name'),
|
185 |
+
'modes': details.get('modes'),
|
186 |
+
'image_url': image_url,
|
187 |
+
'price': price
|
188 |
+
},
|
189 |
+
'quantity': item['quantity'],
|
190 |
+
'mode': mode
|
191 |
+
})
|
192 |
|
193 |
return jsonify({
|
194 |
'items': populated_items,
|
|
|
203 |
'updated_at': datetime.utcnow()
|
204 |
}
|
205 |
|
|
|
206 |
if 'items' in data:
|
207 |
sanitized_items = []
|
208 |
for item in data['items']:
|
209 |
try:
|
|
|
210 |
if not all(k in item for k in ['productId', 'quantity', 'mode']) or item['quantity'] is None:
|
211 |
+
continue
|
212 |
|
213 |
mode = item.get('mode')
|
214 |
quantity = item.get('quantity')
|
215 |
|
216 |
if mode == 'weight':
|
|
|
217 |
numeric_quantity = float(quantity)
|
218 |
else:
|
219 |
+
numeric_quantity = int(float(quantity))
|
|
|
220 |
|
|
|
221 |
if numeric_quantity < 0:
|
222 |
continue
|
223 |
|
|
|
228 |
})
|
229 |
|
230 |
except (ValueError, TypeError):
|
|
|
231 |
return jsonify({"msg": f"Invalid quantity format for item."}), 400
|
232 |
|
233 |
update_doc['items'] = sanitized_items
|
|
|
234 |
|
235 |
if 'deliveryDate' in data:
|
236 |
update_doc['deliveryDate'] = data['deliveryDate']
|
|
|
242 |
)
|
243 |
return jsonify({"msg": "Cart updated successfully"})
|
244 |
|
245 |
+
@api_bp.route('/orders/<serial_no>/download_invoice', methods=['GET'])
|
246 |
+
@jwt_required()
|
247 |
+
def download_invoice(serial_no):
|
248 |
+
user_email = get_jwt_identity()
|
249 |
+
|
250 |
+
order = mongo.db.orders.find_one({'serial_no': int(serial_no), 'user_email': user_email})
|
251 |
+
if not order:
|
252 |
+
return jsonify({"msg": "Order not found or access denied"}), 404
|
253 |
+
|
254 |
+
try:
|
255 |
+
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
|
256 |
+
|
257 |
+
if not invoices_response or not invoices_response.get('invoices'):
|
258 |
+
return jsonify({"msg": "Invoice not found in our billing system."}), 404
|
259 |
+
|
260 |
+
invoice_id = invoices_response['invoices'][0].get('invoice_id')
|
261 |
+
if not invoice_id:
|
262 |
+
return jsonify({"msg": "Could not identify the invoice in our billing system."}), 404
|
263 |
+
|
264 |
+
pdf_content, headers = make_zoho_api_request('GET', f'/invoices/{invoice_id}', params={'accept': 'pdf'})
|
265 |
+
|
266 |
+
if not pdf_content:
|
267 |
+
return jsonify({"msg": "Failed to download the invoice PDF from our billing system."}), 500
|
268 |
+
|
269 |
+
return Response(
|
270 |
+
pdf_content,
|
271 |
+
mimetype='application/pdf',
|
272 |
+
headers={
|
273 |
+
"Content-Disposition": f"attachment; filename=invoice-{serial_no}.pdf",
|
274 |
+
"Content-Type": "application/pdf"
|
275 |
+
}
|
276 |
+
)
|
277 |
+
|
278 |
+
except Exception as e:
|
279 |
+
current_app.logger.error(f"Error downloading invoice {serial_no} from Zoho: {e}")
|
280 |
+
return jsonify({"msg": "An internal error occurred while fetching the invoice."}), 500
|
281 |
+
|
282 |
@api_bp.route('/orders', methods=['GET', 'POST'])
|
283 |
@jwt_required()
|
284 |
def handle_orders():
|
|
|
306 |
order_doc['_id'] = order_id
|
307 |
|
308 |
order_details_for_xero = {
|
309 |
+
"order_id": order_doc['serial_no'], "user_email": user_email, "items": cart['items'],
|
310 |
+
"delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"],'additional_info': data.get('additionalInfo')
|
311 |
}
|
312 |
+
trigger_invoice_creation(order_details_for_xero)
|
313 |
|
314 |
try:
|
315 |
product_ids = [ObjectId(item['productId']) for item in cart['items']]
|
|
|
334 |
if not user_orders: return jsonify([])
|
335 |
|
336 |
all_product_ids = {ObjectId(item['productId']) for order in user_orders for item in order.get('items', [])}
|
337 |
+
products_cursor = mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})
|
338 |
+
products = {str(p['_id']): p for p in products_cursor}
|
339 |
|
340 |
for order in user_orders:
|
341 |
+
# Determine status from Zoho
|
342 |
+
live_status = 'pending' # Default status
|
343 |
+
try:
|
344 |
+
serial_no = order.get('serial_no')
|
345 |
+
if serial_no:
|
346 |
+
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
|
347 |
+
if invoices_response and invoices_response.get('invoices'):
|
348 |
+
invoice = invoices_response['invoices'][0]
|
349 |
+
zoho_status = invoice.get('status')
|
350 |
+
if zoho_status == 'draft':
|
351 |
+
live_status = 'pending'
|
352 |
+
elif zoho_status == 'sent':
|
353 |
+
live_status = 'Out for delivery'
|
354 |
+
elif zoho_status == 'paid':
|
355 |
+
live_status = 'Completed'
|
356 |
+
elif zoho_status == 'void':
|
357 |
+
live_status = 'cancelled'
|
358 |
+
except Exception as e:
|
359 |
+
current_app.logger.error(f"Could not fetch Zoho invoice status for order {order.get('serial_no')}: {e}")
|
360 |
+
|
361 |
+
order['status'] = live_status
|
362 |
+
|
363 |
+
# Populate items
|
364 |
+
populated_items = []
|
365 |
+
for item in order.get('items', []):
|
366 |
+
p = products.get(item['productId'])
|
367 |
+
if p:
|
368 |
+
mode = item.get('mode', 'pieces')
|
369 |
+
mode_details = p.get('modes', {}).get(mode, {})
|
370 |
+
|
371 |
+
image_url = None
|
372 |
+
if p.get('image_data'):
|
373 |
+
image_url = url_for('api.get_product_image', product_id=str(p['_id']), _external=True)
|
374 |
+
elif p.get('image_url'):
|
375 |
+
image_url = p.get('image_url')
|
376 |
+
|
377 |
+
populated_items.append({
|
378 |
+
'quantity': item['quantity'],
|
379 |
+
'mode': mode,
|
380 |
+
'price': mode_details.get('price'),
|
381 |
+
'product': {
|
382 |
+
'id': str(p['_id']),
|
383 |
+
'name': p.get('name'),
|
384 |
+
'modes': p.get('modes'),
|
385 |
+
'image_url': image_url
|
386 |
+
}
|
387 |
+
})
|
388 |
+
order['items'] = populated_items
|
389 |
order['_id'] = str(order['_id'])
|
390 |
order['created_at'] = order['created_at'].isoformat()
|
391 |
order['delivery_date'] = order['delivery_date'] if isinstance(order['delivery_date'], str) else order['delivery_date'].isoformat()
|
|
|
445 |
|
446 |
if not order:
|
447 |
return jsonify({"msg": "Order not found or access denied"}), 404
|
448 |
+
|
449 |
+
serial_no = order.get('serial_no')
|
450 |
+
if not serial_no:
|
451 |
+
return jsonify({"msg": "Cannot cancel order without a billing reference."}), 400
|
452 |
+
|
453 |
+
try:
|
454 |
+
# Find the corresponding invoice in Zoho
|
455 |
+
invoices_response, _ = make_zoho_api_request('GET', '/invoices', params={'reference_number': serial_no})
|
456 |
+
|
457 |
+
if not invoices_response or not invoices_response.get('invoices'):
|
458 |
+
return jsonify({"msg": "Invoice not found in our billing system. Cannot cancel."}), 404
|
459 |
+
|
460 |
+
invoice = invoices_response['invoices'][0]
|
461 |
+
invoice_id = invoice.get('invoice_id')
|
462 |
+
zoho_status = invoice.get('status')
|
463 |
+
|
464 |
+
# The order can only be cancelled if the invoice is a draft
|
465 |
+
if zoho_status != 'draft':
|
466 |
+
return jsonify({"msg": "This order cannot be cancelled as it is already being processed."}), 400
|
467 |
+
|
468 |
+
# Proceed to void the invoice in Zoho
|
469 |
+
void_response, _ = make_zoho_api_request('POST', f'/invoices/{invoice_id}/status/void')
|
470 |
+
|
471 |
+
if not void_response:
|
472 |
+
return jsonify({"msg": "Failed to cancel the order in the billing system."}), 500
|
473 |
+
|
474 |
+
# If Zoho void was successful, update our local DB status
|
475 |
+
mongo.db.orders.update_one(
|
476 |
+
{'_id': ObjectId(order_id)},
|
477 |
+
{'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
|
478 |
+
)
|
479 |
+
|
480 |
+
return jsonify({"msg": "Order has been cancelled."}), 200
|
481 |
+
|
482 |
+
except Exception as e:
|
483 |
+
current_app.logger.error(f"Error cancelling order {order_id} and voiding Zoho invoice: {e}")
|
484 |
+
return jsonify({"msg": "An internal error occurred while cancelling the order."}), 500
|
485 |
|
486 |
@api_bp.route('/sendmail', methods=['GET'])
|
487 |
def send_cart_reminders():
|
|
|
543 |
mongo.db.users.update_one({'_id': ObjectId(user_id)}, {'$set': {'is_approved': True}})
|
544 |
return jsonify({"msg": f"User {user_id} approved"})
|
545 |
|
|
|
546 |
@api_bp.route('/request-item', methods=['POST'])
|
547 |
@jwt_required()
|
548 |
def request_item():
|
|
|
|
|
|
|
|
|
549 |
user_email = get_jwt_identity()
|
550 |
data = request.get_json()
|
551 |
|
|
|
557 |
return jsonify({"msg": "Item details cannot be empty."}), 400
|
558 |
|
559 |
try:
|
|
|
560 |
user = mongo.db.users.find_one({'email': user_email}, {'company_name': 1})
|
561 |
company_name = user.get('company_name', 'N/A') if user else 'N/A'
|
562 |
|
|
|
564 |
'user_email': user_email,
|
565 |
'company_name': company_name,
|
566 |
'details': details,
|
567 |
+
'status': 'new',
|
568 |
'requested_at': datetime.utcnow()
|
569 |
}
|
|
|
|
|
570 |
mongo.db.item_requests.insert_one(request_doc)
|
|
|
|
|
|
|
571 |
|
572 |
return jsonify({"msg": "Your item request has been submitted. We will look into it!"}), 201
|
573 |
|
app/config.py
CHANGED
@@ -1,23 +1,40 @@
|
|
1 |
-
# from dotenv import load_dotenv
|
2 |
-
# load_dotenv(r'C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env')
|
3 |
-
import os
|
4 |
-
from datetime import timedelta
|
5 |
-
|
6 |
-
# It is recommended to load sensitive data from environment variables
|
7 |
-
SECRET_KEY = os.environ.get("SECRET_KEY")
|
8 |
-
SESSION_TYPE = 'filesystem'
|
9 |
-
DEBUG = True
|
10 |
-
|
11 |
-
# MongoDB
|
12 |
-
MONGO_URI = os.environ.get(
|
13 |
-
"MONGO_URI")
|
14 |
-
# JWT
|
15 |
-
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
16 |
-
JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=7)
|
17 |
-
|
18 |
-
# Xero OAuth Credentials
|
19 |
-
CLIENT_ID = os.environ.get("CLIENT_ID")
|
20 |
-
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
|
21 |
-
BREVO_API_KEY=os.environ.get("BREVO_API_KEY")
|
22 |
-
CLIENT_ADMIN_EMAIL="
|
23 |
-
SENDER_EMAIL="vaibhavarduino@gmail.com"
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
# from dotenv import load_dotenv
|
2 |
+
# load_dotenv(r'C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\AIAPPS\.env')
|
3 |
+
import os
|
4 |
+
from datetime import timedelta
|
5 |
+
|
6 |
+
# It is recommended to load sensitive data from environment variables
|
7 |
+
SECRET_KEY = os.environ.get("SECRET_KEY")
|
8 |
+
SESSION_TYPE = 'filesystem'
|
9 |
+
DEBUG = True
|
10 |
+
|
11 |
+
# MongoDB
|
12 |
+
MONGO_URI = os.environ.get(
|
13 |
+
"MONGO_URI")
|
14 |
+
# JWT
|
15 |
+
JWT_SECRET_KEY = os.environ.get("JWT_SECRET_KEY")
|
16 |
+
JWT_ACCESS_TOKEN_EXPIRES = timedelta(days=7)
|
17 |
+
|
18 |
+
# Xero OAuth Credentials
|
19 |
+
CLIENT_ID = os.environ.get("CLIENT_ID")
|
20 |
+
CLIENT_SECRET = os.environ.get("CLIENT_SECRET")
|
21 |
+
BREVO_API_KEY=os.environ.get("BREVO_API_KEY")
|
22 |
+
CLIENT_ADMIN_EMAIL="namita0105@gmail.com"
|
23 |
+
SENDER_EMAIL="vaibhavarduino@gmail.com"
|
24 |
+
# ZOHO_AUTHORIZATION_URL="https://accounts.zoho.com/oauth/v2/auth"
|
25 |
+
# DEFAULT_ZOHO_API_BASE = "https://www.zohoapis.com/"
|
26 |
+
# DEFAULT_ZOHO_AUTH_URL = "https://accounts.zoho.com/oauth/v2/auth"
|
27 |
+
# DEFAULT_ZOHO_TOKEN_URL = "https://accounts.zoho.com/oauth/v2/token"
|
28 |
+
ZOHO_CLIENT_ID = "1000.OFJMGGUTUZSQAQTHP2Z48FRKA5U6ZJ"
|
29 |
+
ZOHO_CLIENT_SECRET = "21db80fc9d9c447aafd964a9972ddf39332d177fb5"
|
30 |
+
ZOHO_REDIRECT_URL = "http://localhost:7860/callback" # e.g., http://localhost:5000/callback
|
31 |
+
ZOHO_CURRENT_USER_EMAIL = "vaibhavarduino@yahoo.com"
|
32 |
+
|
33 |
+
# NEW: Define the scopes your application needs.
|
34 |
+
ZOHO_SCOPES = "ZohoCRM.modules.ALL,ZohoCRM.settings.ALL,ZohoCRM.users.ALL"
|
35 |
+
|
36 |
+
# Paths for Zoho SDK to store data. Make sure these directories are writable.
|
37 |
+
ZOHO_RESOURCE_PATH = r"C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\backend\zoho_resources"
|
38 |
+
ZOHO_TOKEN_PERSISTENCE_PATH = r"C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\backend\zoho_tokens.txt"
|
39 |
+
ZOHO_LOG_FILE_PATH = r"C:\Users\Vaibhav Arora\Documents\MyExperimentsandCodes\APPS_WEBSITES\CANADA_WHOLESALE_PROJECT\GITHUB_REPOS\mvp-vue\wholesale-grocery-app\backend\zoho_sdk.log"
|
40 |
+
ZOHO_ORGANIZATION_ID ="110001770386"
|
app/page_features.py
CHANGED
@@ -180,6 +180,9 @@ def edit_homepage():
|
|
180 |
# into JSON strings, so the backend logic can remain the same.
|
181 |
why_us_json_str = request.form.get('why_us_features')
|
182 |
faqs_json_str = request.form.get('faqs')
|
|
|
|
|
|
|
183 |
|
184 |
try:
|
185 |
# Validate and parse the submitted JSON
|
@@ -197,9 +200,11 @@ def edit_homepage():
|
|
197 |
success1 = set_vercel_env_var("REACT_APP_WHY_US_FEATURES", why_us_data, target_environments, logs)
|
198 |
logs.append("\n")
|
199 |
success2 = set_vercel_env_var("REACT_APP_FAQS", faqs_data, target_environments, logs)
|
|
|
|
|
200 |
|
201 |
-
# If variable updates were successful, trigger deployment
|
202 |
-
if success1 and success2:
|
203 |
trigger_vercel_deployment(logs)
|
204 |
else:
|
205 |
logs.append("\nSkipping deployment due to errors in updating environment variables.")
|
@@ -232,12 +237,24 @@ def edit_homepage():
|
|
232 |
faqs_data = DEFAULT_FAQS_DATA
|
233 |
else:
|
234 |
faqs_data = DEFAULT_FAQS_DATA
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
235 |
|
236 |
# Pass the Python objects directly to the template for the interactive UI
|
237 |
return render_template_string(
|
238 |
EDITOR_UI_TEMPLATE,
|
239 |
why_us_data=why_us_data,
|
240 |
-
faqs_data=faqs_data
|
|
|
241 |
)
|
242 |
|
243 |
# --- HTML Templates for the new endpoint ---
|
@@ -320,6 +337,17 @@ EDITOR_UI_TEMPLATE = """
|
|
320 |
|
321 |
.deploy-button { background-color: #28a745; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1.2em; font-weight: bold; display: block; width: 100%; margin-top: 2em; transition: background-color 0.2s; }
|
322 |
.deploy-button:hover { background-color: #218838; }
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
323 |
</style>
|
324 |
</head>
|
325 |
<body>
|
@@ -327,6 +355,20 @@ EDITOR_UI_TEMPLATE = """
|
|
327 |
<h1>Edit Homepage Content & Deploy</h1>
|
328 |
<form id="main-form" action="/api/pages/edit_homepage" method="post">
|
329 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
330 |
<div class="editor-section">
|
331 |
<h2>'Why Us' Features</h2>
|
332 |
<div id="why-us-editor">
|
|
|
180 |
# into JSON strings, so the backend logic can remain the same.
|
181 |
why_us_json_str = request.form.get('why_us_features')
|
182 |
faqs_json_str = request.form.get('faqs')
|
183 |
+
|
184 |
+
# Get the price slider value. request.form.get will be 'on' if checked, None if not.
|
185 |
+
price_enabled_bool = True if request.form.get('price_enabled') == 'on' else False
|
186 |
|
187 |
try:
|
188 |
# Validate and parse the submitted JSON
|
|
|
200 |
success1 = set_vercel_env_var("REACT_APP_WHY_US_FEATURES", why_us_data, target_environments, logs)
|
201 |
logs.append("\n")
|
202 |
success2 = set_vercel_env_var("REACT_APP_FAQS", faqs_data, target_environments, logs)
|
203 |
+
logs.append("\n")
|
204 |
+
success3 = set_vercel_env_var("REACT_APP_PRICE", price_enabled_bool, target_environments, logs)
|
205 |
|
206 |
+
# If ALL variable updates were successful, trigger deployment
|
207 |
+
if success1 and success2 and success3:
|
208 |
trigger_vercel_deployment(logs)
|
209 |
else:
|
210 |
logs.append("\nSkipping deployment due to errors in updating environment variables.")
|
|
|
237 |
faqs_data = DEFAULT_FAQS_DATA
|
238 |
else:
|
239 |
faqs_data = DEFAULT_FAQS_DATA
|
240 |
+
|
241 |
+
# Fetch Price Enabled status
|
242 |
+
price_var = get_existing_env_var("REACT_APP_PRICE", headers, params, [])
|
243 |
+
price_enabled = False # Default to false
|
244 |
+
if price_var and 'value' in price_var:
|
245 |
+
try:
|
246 |
+
# The value from Vercel is a JSON string, e.g., 'true' or 'false'
|
247 |
+
price_enabled = json.loads(price_var['value'])
|
248 |
+
except (json.JSONDecodeError, TypeError):
|
249 |
+
# Fallback for older non-JSON values if they exist
|
250 |
+
price_enabled = price_var['value'].lower() == 'true'
|
251 |
|
252 |
# Pass the Python objects directly to the template for the interactive UI
|
253 |
return render_template_string(
|
254 |
EDITOR_UI_TEMPLATE,
|
255 |
why_us_data=why_us_data,
|
256 |
+
faqs_data=faqs_data,
|
257 |
+
price_enabled=price_enabled
|
258 |
)
|
259 |
|
260 |
# --- HTML Templates for the new endpoint ---
|
|
|
337 |
|
338 |
.deploy-button { background-color: #28a745; color: white; padding: 0.8em 1.5em; border: none; border-radius: 4px; cursor: pointer; font-size: 1.2em; font-weight: bold; display: block; width: 100%; margin-top: 2em; transition: background-color 0.2s; }
|
339 |
.deploy-button:hover { background-color: #218838; }
|
340 |
+
|
341 |
+
/* --- Slider switch styles --- */
|
342 |
+
.setting-group { display: flex; align-items: center; justify-content: space-between; padding: 0 0.5em; }
|
343 |
+
.setting-group > label { margin-bottom: 0; font-size: 1em; font-weight: normal; color: #212529; }
|
344 |
+
.switch { position: relative; display: inline-block; width: 60px; height: 34px; }
|
345 |
+
.switch input { opacity: 0; width: 0; height: 0; }
|
346 |
+
.slider { position: absolute; cursor: pointer; top: 0; left: 0; right: 0; bottom: 0; background-color: #ccc; -webkit-transition: .4s; transition: .4s; border-radius: 34px; }
|
347 |
+
.slider:before { position: absolute; content: ""; height: 26px; width: 26px; left: 4px; bottom: 4px; background-color: white; -webkit-transition: .4s; transition: .4s; border-radius: 50%; }
|
348 |
+
input:checked + .slider { background-color: #28a745; }
|
349 |
+
input:focus + .slider { box-shadow: 0 0 1px #28a745; }
|
350 |
+
input:checked + .slider:before { -webkit-transform: translateX(26px); -ms-transform: translateX(26px); transform: translateX(26px); }
|
351 |
</style>
|
352 |
</head>
|
353 |
<body>
|
|
|
355 |
<h1>Edit Homepage Content & Deploy</h1>
|
356 |
<form id="main-form" action="/api/pages/edit_homepage" method="post">
|
357 |
|
358 |
+
<!-- --- Price Enable/Disable Slider --- -->
|
359 |
+
<div class="editor-section">
|
360 |
+
<h2>General Settings</h2>
|
361 |
+
<div class="editor-item">
|
362 |
+
<div class="setting-group">
|
363 |
+
<label for="price-toggle">Enable Price Display</label>
|
364 |
+
<label class="switch">
|
365 |
+
<input type="checkbox" id="price-toggle" name="price_enabled" {% if price_enabled %}checked{% endif %}>
|
366 |
+
<span class="slider"></span>
|
367 |
+
</label>
|
368 |
+
</div>
|
369 |
+
</div>
|
370 |
+
</div>
|
371 |
+
|
372 |
<div class="editor-section">
|
373 |
<h2>'Why Us' Features</h2>
|
374 |
<div id="why-us-editor">
|
app/templates/base.html
CHANGED
@@ -3,94 +3,249 @@
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
-
<title>{% if title %}{{ title }}{% else %}
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
|
|
|
|
|
|
8 |
<style>
|
9 |
body {
|
10 |
font-family: 'Inter', sans-serif;
|
|
|
11 |
}
|
12 |
.link-card {
|
13 |
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
|
|
|
|
|
|
|
|
14 |
}
|
15 |
.link-card:hover {
|
16 |
-
transform: translateY(-
|
17 |
-
box-shadow: 0
|
18 |
}
|
19 |
.code-block {
|
20 |
-
background: #
|
21 |
-
border: 1px solid #
|
22 |
border-radius: 0.75rem;
|
23 |
-
padding: 1rem;
|
24 |
font-family: 'Fira Code', monospace;
|
25 |
font-size: 0.9rem;
|
26 |
-
color: #
|
27 |
overflow-x: auto;
|
28 |
-
box-shadow: 0 4px 12px rgba(0,0,0,0.
|
29 |
}
|
30 |
.code-block pre {
|
31 |
margin: 0;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
32 |
}
|
33 |
</style>
|
34 |
</head>
|
35 |
-
<body class="bg-
|
36 |
|
37 |
<!-- Navbar -->
|
38 |
-
<nav class="bg-white shadow-sm
|
39 |
-
<div class="max-w-
|
40 |
<h1 class="text-2xl font-bold text-gray-900">Matax Express Admin Panel</h1>
|
41 |
<div class="space-x-6 text-gray-600">
|
42 |
-
<a href="{{ url_for('
|
43 |
-
<a href="{{ url_for('xero.login') }}" class="hover:text-blue-600 transition">Login</a>
|
44 |
-
<a href="{{ url_for('xero.logout') }}" class="hover:text-blue-600 transition">Logout</a>
|
45 |
</div>
|
46 |
</div>
|
47 |
</nav>
|
48 |
|
49 |
<!-- Content -->
|
50 |
-
<div class="max-w-
|
51 |
<h2 class="text-3xl font-bold mb-8 text-gray-900">Quick Actions</h2>
|
52 |
-
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-
|
53 |
|
54 |
-
|
55 |
-
|
56 |
-
<
|
57 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
58 |
|
59 |
-
<a href="/api/edit_inventory" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
|
60 |
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Edit Product Images</h3>
|
61 |
-
<p class="text-gray-500 text-sm">Manage and update product catalog images effortlessly.</p>
|
62 |
-
</a>
|
63 |
|
64 |
-
<a href="/api/sync_xero_users" class="link-card
|
65 |
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sync
|
66 |
-
<p class="text-gray-500 text-sm">Keep your approved
|
67 |
</a>
|
68 |
|
69 |
-
<a href="/api/sendmail" class="link-card
|
70 |
<h3 class="text-lg font-semibold text-gray-900 mb-2">Send Reminder Emails</h3>
|
71 |
<p class="text-gray-500 text-sm">Notify users with pending items in their cart.</p>
|
72 |
</a>
|
73 |
|
74 |
-
<a href="/api/pages/update_ui" class="link-card
|
75 |
-
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update
|
76 |
<p class="text-gray-500 text-sm">Revamp the look of the Contact and About Us pages.</p>
|
77 |
</a>
|
78 |
|
79 |
-
<a href="/api/pages/edit_homepage" class="link-card
|
80 |
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update Homepage UI</h3>
|
81 |
<p class="text-gray-500 text-sm">Edit the Faq and Why choose us page.This may take some time to take effect</p>
|
82 |
</a>
|
83 |
</div>
|
84 |
|
85 |
-
<!--
|
86 |
-
<div class="mt-12">
|
87 |
-
<h3 class="text-xl font-bold text-gray-800 mb-4">
|
88 |
<div class="code-block">
|
89 |
-
<pre>{% block content %}{% endblock %}</pre>
|
90 |
</div>
|
91 |
</div>
|
92 |
-
|
93 |
</div>
|
94 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
95 |
</body>
|
96 |
-
</html>
|
|
|
3 |
<head>
|
4 |
<meta charset="UTF-8">
|
5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
6 |
+
<title>{% if title %}{{ title }}{% else %}Matax Express Admin Panel{% endif %}</title>
|
7 |
<script src="https://cdn.tailwindcss.com"></script>
|
8 |
+
<link rel="preconnect" href="https://fonts.googleapis.com">
|
9 |
+
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
10 |
+
<link href="https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600;700&family=Fira+Code&display=swap" rel="stylesheet">
|
11 |
<style>
|
12 |
body {
|
13 |
font-family: 'Inter', sans-serif;
|
14 |
+
background-color: #f8fafc; /* Lighter gray background */
|
15 |
}
|
16 |
.link-card {
|
17 |
transition: transform 0.3s ease, box-shadow 0.3s ease;
|
18 |
+
background-color: white;
|
19 |
+
border: 1px solid #e2e8f0;
|
20 |
+
border-radius: 1rem; /* Slightly larger radius */
|
21 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.05);
|
22 |
}
|
23 |
.link-card:hover {
|
24 |
+
transform: translateY(-5px);
|
25 |
+
box-shadow: 0 10px 25px rgba(0,0,0,0.08);
|
26 |
}
|
27 |
.code-block {
|
28 |
+
background: #1e293b; /* Dark slate background */
|
29 |
+
border: 1px solid #334155;
|
30 |
border-radius: 0.75rem;
|
31 |
+
padding: 1rem 1.5rem;
|
32 |
font-family: 'Fira Code', monospace;
|
33 |
font-size: 0.9rem;
|
34 |
+
color: #cbd5e1;
|
35 |
overflow-x: auto;
|
36 |
+
box-shadow: 0 4px 12px rgba(0,0,0,0.1);
|
37 |
}
|
38 |
.code-block pre {
|
39 |
margin: 0;
|
40 |
+
white-space: pre-wrap; /* Allow wrapping */
|
41 |
+
word-break: break-all;
|
42 |
+
}
|
43 |
+
.log-output {
|
44 |
+
background: #0f172a; /* Even darker for logs */
|
45 |
+
color: #94a3b8;
|
46 |
+
font-family: 'Fira Code', monospace;
|
47 |
+
font-size: 0.8rem;
|
48 |
+
height: 200px;
|
49 |
+
overflow-y: auto;
|
50 |
+
border-radius: 0.5rem;
|
51 |
+
padding: 0.75rem;
|
52 |
+
border: 1px solid #334155;
|
53 |
+
}
|
54 |
+
.progress-bar-fill {
|
55 |
+
transition: width 0.4s ease-in-out;
|
56 |
}
|
57 |
</style>
|
58 |
</head>
|
59 |
+
<body class="bg-gray-50 text-gray-800">
|
60 |
|
61 |
<!-- Navbar -->
|
62 |
+
<nav class="bg-white shadow-sm sticky top-0 left-0 w-full z-10 border-b border-gray-200">
|
63 |
+
<div class="max-w-7xl mx-auto px-6 py-4 flex justify-between items-center">
|
64 |
<h1 class="text-2xl font-bold text-gray-900">Matax Express Admin Panel</h1>
|
65 |
<div class="space-x-6 text-gray-600">
|
66 |
+
<a href="{{ url_for('zoho.index') }}" class="hover:text-blue-600 transition">Index</a>
|
|
|
|
|
67 |
</div>
|
68 |
</div>
|
69 |
</nav>
|
70 |
|
71 |
<!-- Content -->
|
72 |
+
<div class="max-w-7xl mx-auto px-6 pt-12 pb-10">
|
73 |
<h2 class="text-3xl font-bold mb-8 text-gray-900">Quick Actions</h2>
|
74 |
+
<div class="grid md:grid-cols-2 lg:grid-cols-3 gap-8">
|
75 |
|
76 |
+
<!-- MODIFIED: Inventory Sync Card -->
|
77 |
+
<div class="link-card p-6 flex flex-col justify-between space-y-4 col-span-1 md:col-span-2">
|
78 |
+
<div>
|
79 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Inventory from Zoho</h3>
|
80 |
+
<p class="text-gray-500 text-sm">Update your website inventory directly from Zoho. This may take a few minutes.</p>
|
81 |
+
</div>
|
82 |
+
|
83 |
+
<!-- Sync Progress UI (Initially hidden) -->
|
84 |
+
<div id="sync-progress-container" class="hidden space-y-3 pt-2">
|
85 |
+
<!-- Progress Bar -->
|
86 |
+
<div class="w-full bg-gray-200 rounded-full h-2.5">
|
87 |
+
<div id="progress-bar" class="bg-blue-600 h-2.5 rounded-full progress-bar-fill" style="width: 0%"></div>
|
88 |
+
</div>
|
89 |
+
<!-- Status Message -->
|
90 |
+
<p id="sync-status-message" class="text-center text-sm font-medium text-gray-600">Initializing...</p>
|
91 |
+
<!-- Live Log -->
|
92 |
+
<div id="sync-log-output" class="log-output"></div>
|
93 |
+
</div>
|
94 |
+
|
95 |
+
<!-- Sync Button -->
|
96 |
+
<button id="start-sync-button" class="w-full mt-auto bg-blue-600 text-white font-semibold py-2.5 px-4 rounded-lg hover:bg-blue-700 focus:outline-none focus:ring-4 focus:ring-blue-300 transition-all duration-300 flex items-center justify-center space-x-2 disabled:bg-gray-400 disabled:cursor-not-allowed">
|
97 |
+
<svg xmlns="http://www.w3.org/2000/svg" class="h-5 w-5" viewBox="0 0 20 20" fill="currentColor"><path fill-rule="evenodd" d="M10 18a8 8 0 100-16 8 8 0 000 16zm3.707-8.707l-3-3a1 1 0 00-1.414 0l-3 3a1 1 0 001.414 1.414L9 9.414V13a1 1 0 102 0V9.414l1.293 1.293a1 1 0 001.414-1.414z" clip-rule="evenodd" /></svg>
|
98 |
+
<span>Sync Inventory</span>
|
99 |
+
</button>
|
100 |
+
</div>
|
101 |
|
|
|
|
|
|
|
|
|
102 |
|
103 |
+
<a href="/api/sync_xero_users" class="link-card p-6">
|
104 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Zoho Users</h3>
|
105 |
+
<p class="text-gray-500 text-sm">Keep your approved Zoho users up to date in the database.</p>
|
106 |
</a>
|
107 |
|
108 |
+
<a href="/api/sendmail" class="link-card p-6">
|
109 |
<h3 class="text-lg font-semibold text-gray-900 mb-2">Send Reminder Emails</h3>
|
110 |
<p class="text-gray-500 text-sm">Notify users with pending items in their cart.</p>
|
111 |
</a>
|
112 |
|
113 |
+
<a href="/api/pages/update_ui" class="link-card p-6">
|
114 |
+
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update About UI</h3>
|
115 |
<p class="text-gray-500 text-sm">Revamp the look of the Contact and About Us pages.</p>
|
116 |
</a>
|
117 |
|
118 |
+
<a href="/api/pages/edit_homepage" class="link-card p-6">
|
119 |
<h3 class="text-lg font-semibold text-gray-900 mb-2">Update Homepage UI</h3>
|
120 |
<p class="text-gray-500 text-sm">Edit the Faq and Why choose us page.This may take some time to take effect</p>
|
121 |
</a>
|
122 |
</div>
|
123 |
|
124 |
+
<!-- Output Block for final results -->
|
125 |
+
<div id="output-wrapper" class="mt-12 hidden">
|
126 |
+
<h3 class="text-xl font-bold text-gray-800 mb-4">Sync Result Summary</h3>
|
127 |
<div class="code-block">
|
128 |
+
<pre id="final-output-pre">{% block content %}{% endblock %}</pre>
|
129 |
</div>
|
130 |
</div>
|
|
|
131 |
</div>
|
132 |
|
133 |
+
<script>
|
134 |
+
document.addEventListener('DOMContentLoaded', function() {
|
135 |
+
const startButton = document.getElementById('start-sync-button');
|
136 |
+
const progressContainer = document.getElementById('sync-progress-container');
|
137 |
+
const progressBar = document.getElementById('progress-bar');
|
138 |
+
const statusMessage = document.getElementById('sync-status-message');
|
139 |
+
const logOutput = document.getElementById('sync-log-output');
|
140 |
+
const outputWrapper = document.getElementById('output-wrapper');
|
141 |
+
const finalOutputPre = document.getElementById('final-output-pre');
|
142 |
+
|
143 |
+
let eventSource;
|
144 |
+
|
145 |
+
startButton.addEventListener('click', function() {
|
146 |
+
// --- 1. Reset UI State ---
|
147 |
+
startButton.disabled = true;
|
148 |
+
startButton.querySelector('span').textContent = 'Syncing...';
|
149 |
+
progressContainer.classList.remove('hidden');
|
150 |
+
progressBar.style.width = '0%';
|
151 |
+
progressBar.classList.remove('bg-green-500', 'bg-red-500');
|
152 |
+
progressBar.classList.add('bg-blue-600');
|
153 |
+
logOutput.innerHTML = '';
|
154 |
+
statusMessage.textContent = 'Connecting to server...';
|
155 |
+
outputWrapper.classList.add('hidden');
|
156 |
+
finalOutputPre.textContent = '';
|
157 |
+
addLogEntry('Initiating synchronization...', 'info');
|
158 |
+
|
159 |
+
// --- 2. Start Server-Sent Events Connection ---
|
160 |
+
// IMPORTANT: Ensure this URL points to your actual streaming endpoint.
|
161 |
+
const eventSourceURL = "{{ url_for('zoho.fetch_inventory_stream') }}";
|
162 |
+
eventSource = new EventSource(eventSourceURL);
|
163 |
+
|
164 |
+
eventSource.onopen = function() {
|
165 |
+
addLogEntry('Connection established. Starting sync process.', 'info');
|
166 |
+
};
|
167 |
+
|
168 |
+
eventSource.onmessage = function(event) {
|
169 |
+
const data = JSON.parse(event.data);
|
170 |
+
|
171 |
+
// Update Progress Bar
|
172 |
+
progressBar.style.width = data.progress + '%';
|
173 |
+
|
174 |
+
// Update Status Message (show first line only for brevity)
|
175 |
+
statusMessage.textContent = data.message.split('\n')[0];
|
176 |
+
|
177 |
+
// Add full message to log
|
178 |
+
addLogEntry(data.message, 'message');
|
179 |
+
|
180 |
+
// Handle final states
|
181 |
+
if (data.status === 'complete' || data.status === 'error') {
|
182 |
+
eventSource.close();
|
183 |
+
if (data.status === 'complete') {
|
184 |
+
handleSyncComplete(data);
|
185 |
+
} else {
|
186 |
+
handleSyncError(data);
|
187 |
+
}
|
188 |
+
}
|
189 |
+
};
|
190 |
+
|
191 |
+
eventSource.onerror = function(err) {
|
192 |
+
console.error("EventSource failed:", err);
|
193 |
+
eventSource.close();
|
194 |
+
const errorData = { message: 'Connection to the server was lost. Please check the console and try again.' };
|
195 |
+
handleSyncError(errorData);
|
196 |
+
};
|
197 |
+
});
|
198 |
+
|
199 |
+
function handleSyncComplete(data) {
|
200 |
+
statusMessage.textContent = "Synchronization completed successfully!";
|
201 |
+
progressBar.classList.remove('bg-blue-600');
|
202 |
+
progressBar.classList.add('bg-green-500');
|
203 |
+
|
204 |
+
addLogEntry('Sync complete!', 'success');
|
205 |
+
|
206 |
+
if (data.final_code) {
|
207 |
+
finalOutputPre.textContent = data.final_code;
|
208 |
+
outputWrapper.classList.remove('hidden');
|
209 |
+
}
|
210 |
+
|
211 |
+
resetButtonAfterDelay();
|
212 |
+
}
|
213 |
+
|
214 |
+
function handleSyncError(data) {
|
215 |
+
statusMessage.textContent = "An error occurred during synchronization.";
|
216 |
+
progressBar.classList.remove('bg-blue-600');
|
217 |
+
progressBar.classList.add('bg-red-500');
|
218 |
+
|
219 |
+
addLogEntry(data.message, 'error');
|
220 |
+
|
221 |
+
resetButtonAfterDelay();
|
222 |
+
}
|
223 |
+
|
224 |
+
function addLogEntry(message, type) {
|
225 |
+
const timestamp = new Date().toLocaleTimeString();
|
226 |
+
const entry = document.createElement('div');
|
227 |
+
entry.classList.add('flex', 'items-start', 'space-x-2');
|
228 |
+
|
229 |
+
let colorClass = 'text-gray-400';
|
230 |
+
if (type === 'success') colorClass = 'text-green-400';
|
231 |
+
if (type === 'error') colorClass = 'text-red-400';
|
232 |
+
|
233 |
+
entry.innerHTML = `
|
234 |
+
<span class="font-mono text-gray-500">[${timestamp}]</span>
|
235 |
+
<span class="flex-1 ${colorClass}">${message.replace(/\n/g, '<br>')}</span>
|
236 |
+
`;
|
237 |
+
|
238 |
+
logOutput.appendChild(entry);
|
239 |
+
logOutput.scrollTop = logOutput.scrollHeight; // Auto-scroll to bottom
|
240 |
+
}
|
241 |
+
|
242 |
+
function resetButtonAfterDelay() {
|
243 |
+
setTimeout(() => {
|
244 |
+
startButton.disabled = false;
|
245 |
+
startButton.querySelector('span').textContent = 'Sync Inventory';
|
246 |
+
}, 2000);
|
247 |
+
}
|
248 |
+
});
|
249 |
+
</script>
|
250 |
</body>
|
251 |
+
</html>
|
app/templates/code.html
CHANGED
@@ -1,16 +1,24 @@
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
{% block content %}
|
4 |
-
|
5 |
-
|
6 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
{% if result_list %}
|
8 |
-
|
9 |
-
|
10 |
-
|
11 |
-
|
12 |
-
|
13 |
{% endif %}
|
14 |
-
|
|
|
15 |
</div>
|
|
|
16 |
{% endblock %}
|
|
|
1 |
{% extends "base.html" %}
|
2 |
|
3 |
{% block content %}
|
4 |
+
<div class="card">
|
5 |
+
<div class="card-header">
|
6 |
+
<h2 class="card-title mb-0">{{ title }}</h2>
|
7 |
+
</div>
|
8 |
+
<div class="card-body">
|
9 |
+
{% if sub_title %}
|
10 |
+
<h5 class="card-subtitle mb-3 text-muted">{{ sub_title }}</h5>
|
11 |
+
{% endif %}
|
12 |
+
|
13 |
{% if result_list %}
|
14 |
+
<ul class="list-group mb-3">
|
15 |
+
{% for message in result_list %}
|
16 |
+
<li class="list-group-item">{{ message }}</li>
|
17 |
+
{% endfor %}
|
18 |
+
</ul>
|
19 |
{% endif %}
|
20 |
+
|
21 |
+
<pre class="prettyprint"><code class="language-{{ language | default('javascript') }}">{{ code }}</code></pre>
|
22 |
</div>
|
23 |
+
</div>
|
24 |
{% endblock %}
|
app/templates/inventory_sync.html
ADDED
@@ -0,0 +1,82 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
{% extends "base.html" %}
|
2 |
+
|
3 |
+
{% block content %}
|
4 |
+
<div class="card mb-4">
|
5 |
+
<div class="card-header">
|
6 |
+
<h2 class="card-title mb-0">{{ title }}</h2>
|
7 |
+
</div>
|
8 |
+
<div class="card-body">
|
9 |
+
<p>The inventory synchronization process has started. Please keep this page open until it completes. This may take a few minutes.</p>
|
10 |
+
|
11 |
+
<div id="progress-container">
|
12 |
+
<div class="progress" role="progressbar" aria-label="Sync Progress" aria-valuenow="0" aria-valuemin="0" aria-valuemax="100" style="height: 25px;">
|
13 |
+
<div class="progress-bar progress-bar-striped progress-bar-animated" id="progressBar" style="width: 0%">0%</div>
|
14 |
+
</div>
|
15 |
+
<p class="mt-2 text-center" id="progress-message">Initializing...</p>
|
16 |
+
</div>
|
17 |
+
|
18 |
+
<h4 class="mt-4">Live Log:</h4>
|
19 |
+
<pre id="log-output" class="prettyprint" style="height: 300px; overflow-y: scroll; background-color: #282c34;"><code class="language-bash"></code></pre>
|
20 |
+
|
21 |
+
<div id="results-container" class="d-none">
|
22 |
+
<h3>Synchronization Complete</h3>
|
23 |
+
<h4 id="final-subtitle" class="text-muted"></h4>
|
24 |
+
<pre class="prettyprint"><code id="final-code" class="language-javascript"></code></pre>
|
25 |
+
</div>
|
26 |
+
</div>
|
27 |
+
</div>
|
28 |
+
|
29 |
+
<script>
|
30 |
+
document.addEventListener('DOMContentLoaded', function() {
|
31 |
+
const progressBar = document.getElementById('progressBar');
|
32 |
+
const progressMessage = document.getElementById('progress-message');
|
33 |
+
const logOutput = document.querySelector('#log-output code');
|
34 |
+
const progressContainer = document.getElementById('progress-container');
|
35 |
+
const resultsContainer = document.getElementById('results-container');
|
36 |
+
const finalSubtitle = document.getElementById('final-subtitle');
|
37 |
+
const finalCode = document.getElementById('final-code');
|
38 |
+
|
39 |
+
const eventSource = new EventSource("{{ url_for('zoho.fetch_inventory_stream') }}");
|
40 |
+
let logContent = '';
|
41 |
+
|
42 |
+
eventSource.onmessage = function(event) {
|
43 |
+
const data = JSON.parse(event.data);
|
44 |
+
|
45 |
+
progressBar.style.width = data.progress + '%';
|
46 |
+
progressBar.textContent = data.progress + '%';
|
47 |
+
progressBar.setAttribute('aria-valuenow', data.progress);
|
48 |
+
progressMessage.textContent = data.message.split('\n')[0]; // Show only first line of message
|
49 |
+
|
50 |
+
const timestamp = new Date().toLocaleTimeString();
|
51 |
+
logContent += `[${timestamp}] ${data.message}\n`;
|
52 |
+
logOutput.textContent = logContent;
|
53 |
+
logOutput.parentElement.scrollTop = logOutput.parentElement.scrollHeight;
|
54 |
+
|
55 |
+
if (data.status === 'complete' || data.status === 'error') {
|
56 |
+
eventSource.close();
|
57 |
+
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
|
58 |
+
|
59 |
+
if(data.status === 'complete') {
|
60 |
+
progressBar.classList.add('bg-success');
|
61 |
+
finalSubtitle.textContent = data.message;
|
62 |
+
finalCode.textContent = data.final_code;
|
63 |
+
Prism.highlightElement(finalCode);
|
64 |
+
progressContainer.classList.add('d-none');
|
65 |
+
resultsContainer.classList.remove('d-none');
|
66 |
+
} else {
|
67 |
+
progressBar.classList.add('bg-danger');
|
68 |
+
progressMessage.textContent = "An error occurred. Check the log for details.";
|
69 |
+
}
|
70 |
+
}
|
71 |
+
};
|
72 |
+
|
73 |
+
eventSource.onerror = function(err) {
|
74 |
+
console.error("EventSource failed:", err);
|
75 |
+
progressMessage.textContent = 'Error: Connection to the server was lost.';
|
76 |
+
progressBar.classList.add('bg-danger');
|
77 |
+
progressBar.classList.remove('progress-bar-animated', 'progress-bar-striped');
|
78 |
+
eventSource.close();
|
79 |
+
};
|
80 |
+
});
|
81 |
+
</script>
|
82 |
+
{% endblock %}
|
app/xero_client.py
CHANGED
@@ -1,66 +1,217 @@
|
|
1 |
import logging
|
|
|
|
|
|
|
|
|
2 |
from functools import wraps
|
3 |
-
from flask import redirect, url_for
|
4 |
-
from xero_python.api_client import ApiClient, Configuration
|
5 |
-
from xero_python.api_client.oauth2 import OAuth2Token # <-- IMPORT THIS
|
6 |
-
from xero_python.identity import IdentityApi
|
7 |
-
from .extensions import oauth, mongo
|
8 |
|
9 |
logger = logging.getLogger(__name__)
|
10 |
|
11 |
-
|
12 |
-
|
13 |
-
|
14 |
-
|
15 |
-
|
16 |
-
|
17 |
-
|
18 |
-
|
19 |
-
|
20 |
-
|
21 |
-
|
22 |
-
|
23 |
-
|
24 |
-
|
25 |
-
|
26 |
-
|
27 |
-
|
28 |
-
|
29 |
-
)
|
30 |
-
|
31 |
-
|
32 |
-
|
33 |
-
|
34 |
-
|
35 |
-
|
36 |
-
|
37 |
-
|
38 |
-
|
39 |
-
|
40 |
-
|
41 |
-
|
42 |
-
|
43 |
-
|
44 |
-
|
45 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
46 |
else:
|
47 |
-
|
48 |
-
logger.info("Xero token removed from database.")
|
49 |
|
50 |
-
def
|
51 |
-
token
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
52 |
if not token:
|
53 |
return None
|
54 |
|
55 |
-
|
56 |
-
|
57 |
-
if
|
58 |
-
return
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
59 |
|
60 |
-
def
|
|
|
|
|
|
|
61 |
@wraps(function)
|
62 |
def decorator(*args, **kwargs):
|
63 |
-
if not
|
64 |
-
|
|
|
65 |
return function(*args, **kwargs)
|
66 |
-
return decorator
|
|
|
|
1 |
import logging
|
2 |
+
import os
|
3 |
+
import json
|
4 |
+
import time
|
5 |
+
import requests
|
6 |
from functools import wraps
|
7 |
+
from flask import current_app, redirect, url_for, session, request
|
|
|
|
|
|
|
|
|
8 |
|
9 |
logger = logging.getLogger(__name__)
|
10 |
|
11 |
+
|
12 |
+
def make_zoho_api_request(method, url_path, params=None, json_data=None, files=None):
|
13 |
+
"""
|
14 |
+
Makes a request to the Zoho Books API, handling authentication and token refresh.
|
15 |
+
It now supports multipart/form-data for file uploads via the 'files' parameter.
|
16 |
+
|
17 |
+
For JSON responses, it returns (data, None).
|
18 |
+
For file/image downloads, it returns (raw_bytes, headers).
|
19 |
+
"""
|
20 |
+
access_token = get_access_token()
|
21 |
+
if not access_token:
|
22 |
+
# This will be caught by the calling function's try-except block
|
23 |
+
raise Exception("Zoho authentication failed: Could not retrieve a valid access token.")
|
24 |
+
|
25 |
+
# Dynamically determine the base URL from the stored token information
|
26 |
+
stored_token = _get_stored_token()
|
27 |
+
if not stored_token or 'api_domain' not in stored_token:
|
28 |
+
logger.error("Zoho 'api_domain' not found in stored token. The token is outdated or invalid. Please re-authenticate.")
|
29 |
+
clear_zoho_token() # Force re-authentication
|
30 |
+
raise Exception("Zoho configuration error: API domain missing. Please log in again.")
|
31 |
+
|
32 |
+
api_domain = stored_token['api_domain']
|
33 |
+
base_url = f"{api_domain}/books/v3"
|
34 |
+
|
35 |
+
headers = {
|
36 |
+
"Authorization": f"Zoho-oauthtoken {access_token}",
|
37 |
+
}
|
38 |
+
# For multipart/form-data (when 'files' is used), 'requests' sets the Content-Type automatically.
|
39 |
+
# For JSON, we set it explicitly.
|
40 |
+
if json_data:
|
41 |
+
headers["Content-Type"] = "application/json"
|
42 |
+
|
43 |
+
full_url = f"{base_url}{url_path}"
|
44 |
+
|
45 |
+
logger.debug(f"Making Zoho API request: {method} {full_url}")
|
46 |
+
# Pass the 'files' parameter to the requests call for handling file uploads
|
47 |
+
response = requests.request(method, full_url, headers=headers, params=params, json=json_data, files=files)
|
48 |
+
|
49 |
+
# Check for success (200 OK, 201 Created)
|
50 |
+
if response.status_code not in [200, 201]:
|
51 |
+
error_message = f"Zoho API Error ({response.status_code}): {response.text}"
|
52 |
+
logger.error(error_message)
|
53 |
+
response.raise_for_status()
|
54 |
+
|
55 |
+
content_type = response.headers.get('content-type', '').lower()
|
56 |
+
# The print statement below was already present, I've left it as is.
|
57 |
+
print(f"Response Content-Type: {content_type}")
|
58 |
+
|
59 |
+
# If the response is not JSON, treat it as a file/image download.
|
60 |
+
if 'application/json' not in content_type:
|
61 |
+
logger.info(f"Received non-JSON content-type '{content_type}', returning raw content and headers.")
|
62 |
+
return response.content, response.headers
|
63 |
+
|
64 |
+
# Handle JSON responses, returning a tuple for consistency.
|
65 |
+
if response.text:
|
66 |
+
return response.json(), None
|
67 |
+
|
68 |
+
# Handle empty success responses
|
69 |
+
return None, None
|
70 |
+
|
71 |
+
|
72 |
+
def initialize_zoho_sdk(grant_token=None, accounts_server_url=None):
|
73 |
+
"""
|
74 |
+
Initializes Zoho authentication. If a grant_token is provided, it exchanges
|
75 |
+
it for access/refresh tokens using the correct data center.
|
76 |
+
"""
|
77 |
+
if grant_token:
|
78 |
+
logger.info("Initializing Zoho Auth with new GRANT token.")
|
79 |
+
try:
|
80 |
+
config = current_app.config
|
81 |
+
|
82 |
+
# Use the dynamically provided accounts_server_url or default to .in
|
83 |
+
base_accounts_url = accounts_server_url if accounts_server_url else 'https://accounts.zoho.in'
|
84 |
+
token_url = f'{base_accounts_url}/oauth/v2/token'
|
85 |
+
logger.info(f"Using token exchange URL: {token_url}")
|
86 |
+
|
87 |
+
payload = {
|
88 |
+
'code': grant_token,
|
89 |
+
'client_id': config['ZOHO_CLIENT_ID'],
|
90 |
+
'client_secret': config['ZOHO_CLIENT_SECRET'],
|
91 |
+
'redirect_uri': config['ZOHO_REDIRECT_URL'],
|
92 |
+
'grant_type': 'authorization_code'
|
93 |
+
}
|
94 |
+
response = requests.post(token_url, data=payload)
|
95 |
+
response.raise_for_status()
|
96 |
+
token_data = response.json()
|
97 |
+
|
98 |
+
# --- EDIT: Print the received datacenter URL and store it ---
|
99 |
+
api_domain = token_data.get("api_domain")
|
100 |
+
if api_domain:
|
101 |
+
logger.info(f"Received Zoho API Domain: {api_domain}")
|
102 |
+
print(f"--- Successfully received Zoho API Domain: {api_domain} ---")
|
103 |
+
else:
|
104 |
+
logger.warning("Could not find 'api_domain' in the token response. API requests might fail.")
|
105 |
+
print("--- WARNING: 'api_domain' not found in Zoho's response. ---")
|
106 |
+
|
107 |
+
# Store the accounts server URL for refreshing the token later
|
108 |
+
token_data['accounts_server'] = base_accounts_url
|
109 |
+
|
110 |
+
logger.info(f"Token data received: {token_data}")
|
111 |
+
token_data['expires_at'] = int(time.time()) + token_data['expires_in']
|
112 |
+
_save_token(token_data)
|
113 |
+
logger.info("Successfully exchanged grant token and stored tokens.")
|
114 |
+
except requests.exceptions.RequestException as e:
|
115 |
+
error_text = e.response.text if e.response else "No response from server"
|
116 |
+
logger.critical(f"Failed to exchange grant token: {error_text}", exc_info=True)
|
117 |
+
raise Exception(f"Failed to get token from Zoho: {error_text}") from e
|
118 |
else:
|
119 |
+
logger.debug("initialize_zoho_sdk called without grant token. No action taken.")
|
|
|
120 |
|
121 |
+
def _get_stored_token():
|
122 |
+
"""Reads token data from the persistence file."""
|
123 |
+
token_path = current_app.config['ZOHO_TOKEN_PERSISTENCE_PATH']
|
124 |
+
if not os.path.exists(token_path):
|
125 |
+
return None
|
126 |
+
try:
|
127 |
+
with open(token_path, 'r') as f:
|
128 |
+
return json.load(f)
|
129 |
+
except (json.JSONDecodeError, FileNotFoundError):
|
130 |
+
return None
|
131 |
+
|
132 |
+
def _save_token(token_data):
|
133 |
+
"""Saves token data to the persistence file."""
|
134 |
+
token_path = current_app.config['ZOHO_TOKEN_PERSISTENCE_PATH']
|
135 |
+
with open(token_path, 'w') as f:
|
136 |
+
json.dump(token_data, f)
|
137 |
+
|
138 |
+
def _refresh_access_token():
|
139 |
+
"""Uses the refresh token to get a new access token from the correct data center."""
|
140 |
+
stored_token = _get_stored_token()
|
141 |
+
if not stored_token or 'refresh_token' not in stored_token:
|
142 |
+
logger.error("Refresh token not found. Cannot refresh.")
|
143 |
+
return None
|
144 |
+
|
145 |
+
logger.info("Refreshing Zoho access token.")
|
146 |
+
try:
|
147 |
+
config = current_app.config
|
148 |
+
|
149 |
+
# Use the stored accounts_server or default for backward compatibility
|
150 |
+
base_accounts_url = stored_token.get('accounts_server', 'https://accounts.zoho.in')
|
151 |
+
token_url = f'{base_accounts_url}/oauth/v2/token'
|
152 |
+
logger.info(f"Using token refresh URL: {token_url}")
|
153 |
+
|
154 |
+
payload = {
|
155 |
+
'refresh_token': stored_token['refresh_token'],
|
156 |
+
'client_id': config['ZOHO_CLIENT_ID'],
|
157 |
+
'client_secret': config['ZOHO_CLIENT_SECRET'],
|
158 |
+
'grant_type': 'refresh_token'
|
159 |
+
}
|
160 |
+
response = requests.post(token_url, data=payload)
|
161 |
+
response.raise_for_status()
|
162 |
+
new_token_data = response.json()
|
163 |
+
|
164 |
+
# Preserve crucial details not always present in the refresh response
|
165 |
+
new_token_data['refresh_token'] = new_token_data.get('refresh_token', stored_token['refresh_token'])
|
166 |
+
new_token_data['accounts_server'] = new_token_data.get('accounts_server', base_accounts_url)
|
167 |
+
new_token_data['expires_at'] = int(time.time()) + new_token_data['expires_in']
|
168 |
+
|
169 |
+
_save_token(new_token_data)
|
170 |
+
logger.info("Successfully refreshed and saved new access token.")
|
171 |
+
return new_token_data
|
172 |
+
except requests.exceptions.RequestException as e:
|
173 |
+
error_text = e.response.text if e.response else "No response from server"
|
174 |
+
logger.critical(f"Failed to refresh Zoho access token: {error_text}", exc_info=True)
|
175 |
+
clear_zoho_token()
|
176 |
+
return None
|
177 |
+
|
178 |
+
def get_access_token():
|
179 |
+
"""Returns a valid access token, refreshing it if necessary."""
|
180 |
+
token = _get_stored_token()
|
181 |
if not token:
|
182 |
return None
|
183 |
|
184 |
+
if token.get('expires_at', 0) < time.time() + 60:
|
185 |
+
token = _refresh_access_token()
|
186 |
+
if not token:
|
187 |
+
return None
|
188 |
+
|
189 |
+
return token.get('access_token')
|
190 |
+
|
191 |
+
def is_zoho_token_available():
|
192 |
+
"""Checks if a refresh token has been persisted in the file store."""
|
193 |
+
token = _get_stored_token()
|
194 |
+
return token is not None and 'refresh_token' in token
|
195 |
+
|
196 |
+
def clear_zoho_token():
|
197 |
+
"""Removes the persisted token file to effectively log out."""
|
198 |
+
token_path = current_app.config['ZOHO_TOKEN_PERSISTENCE_PATH']
|
199 |
+
if os.path.exists(token_path):
|
200 |
+
try:
|
201 |
+
os.remove(token_path)
|
202 |
+
logger.info("Zoho token file removed successfully.")
|
203 |
+
except OSError as e:
|
204 |
+
logger.error(f"Error removing Zoho token file: {e}")
|
205 |
|
206 |
+
def zoho_token_required(function):
|
207 |
+
"""
|
208 |
+
Decorator to ensure a valid Zoho token exists. If not, redirects to the login page.
|
209 |
+
"""
|
210 |
@wraps(function)
|
211 |
def decorator(*args, **kwargs):
|
212 |
+
if not is_zoho_token_available():
|
213 |
+
session['next_url'] = request.url
|
214 |
+
return redirect(url_for("zoho.login"))
|
215 |
return function(*args, **kwargs)
|
216 |
+
return decorator
|
217 |
+
|
app/xero_routes.py
CHANGED
@@ -1,151 +1,235 @@
|
|
1 |
-
import json
|
2 |
import logging
|
3 |
-
|
|
|
|
|
|
|
4 |
from pymongo import UpdateOne
|
5 |
-
from
|
6 |
-
from
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
7 |
from search_engine import search_and_filter_images, categorise
|
8 |
-
|
9 |
-
|
10 |
-
import traceback
|
11 |
-
xero_bp = Blueprint('xero', __name__)
|
12 |
logger = logging.getLogger(__name__)
|
13 |
|
14 |
|
15 |
-
@
|
16 |
-
def
|
17 |
-
|
18 |
-
return
|
19 |
-
|
20 |
-
title="Home | OAuth Token",
|
21 |
-
code=json.dumps(xero_access, sort_keys=True, indent=4),
|
22 |
-
)
|
23 |
|
24 |
-
@
|
25 |
def index():
|
26 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
27 |
return render_template(
|
28 |
"code.html",
|
29 |
title="Home",
|
30 |
-
|
|
|
|
|
31 |
)
|
32 |
|
33 |
-
@xero_bp.route("/login")
|
34 |
-
def login():
|
35 |
-
redirect_url = url_for("xero.oauth_callback", _external=True)
|
36 |
-
return xero.authorize(callback_uri=redirect_url)
|
37 |
|
38 |
-
@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
39 |
def oauth_callback():
|
|
|
|
|
|
|
|
|
|
|
|
|
40 |
try:
|
41 |
-
|
|
|
|
|
|
|
42 |
except Exception as e:
|
43 |
-
logger.error("OAuth callback
|
44 |
-
|
45 |
-
|
46 |
-
if response is None or response.get("access_token") is None:
|
47 |
-
return "Access denied: response=%s" % response
|
48 |
-
|
49 |
-
store_xero_oauth2_token(response)
|
50 |
-
return redirect(url_for("xero.index", _external=True))
|
51 |
|
52 |
-
|
|
|
53 |
def logout():
|
54 |
-
|
55 |
-
|
|
|
56 |
|
57 |
-
@xero_bp.route("/api/inventory")
|
58 |
-
@xero_token_required
|
59 |
-
def fetch_inventory():
|
60 |
-
try:
|
61 |
-
xero_tenant_id = get_xero_tenant_id()
|
62 |
-
accounting_api = AccountingApi(api_client)
|
63 |
-
xero_items = accounting_api.get_items(xero_tenant_id=xero_tenant_id).items
|
64 |
-
fetched_products = [{
|
65 |
-
"code": item.code, "name": item.name,
|
66 |
-
"price": float(item.sales_details.unit_price) if item.sales_details.unit_price else 0.0,
|
67 |
-
"unit": item.description, "on_hand": item.quantity_on_hand if item.quantity_on_hand else 1,
|
68 |
-
} for item in xero_items]
|
69 |
-
print(fetched_products)
|
70 |
-
|
71 |
-
# Use mongo.db directly
|
72 |
-
db_products = list(mongo.db.products.find({}))
|
73 |
-
db_products_map = {p['code']: p for p in db_products if 'code' in p}
|
74 |
-
fetched_products_map = {p['code']: p for p in fetched_products}
|
75 |
-
|
76 |
-
products_to_insert = []
|
77 |
-
bulk_update_ops = []
|
78 |
-
|
79 |
-
for code, fetched_p in fetched_products_map.items():
|
80 |
-
db_p = db_products_map.get(code)
|
81 |
-
if not db_p:
|
82 |
-
new_doc = { "code": fetched_p["code"], "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"],
|
83 |
-
"category": str(categorise(fetched_p["name"])),
|
84 |
-
"image_url": str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"]),}
|
85 |
-
products_to_insert.append(new_doc)
|
86 |
-
elif (fetched_p['name'] != db_p.get('name') or fetched_p['price'] != db_p.get('price') or fetched_p['unit'] != db_p.get('unit')):
|
87 |
-
update_fields = { "name": fetched_p["name"], "price": fetched_p["price"], "unit": fetched_p["unit"] }
|
88 |
-
if fetched_p['name'] != db_p.get('name'):
|
89 |
-
update_fields["category"] = str(categorise(fetched_p["name"]))
|
90 |
-
update_fields["image_url"] = str(search_and_filter_images(str(fetched_p["name"]))[0]["image_url"])
|
91 |
-
bulk_update_ops.append(UpdateOne({'code': code}, {'$set': update_fields}))
|
92 |
-
|
93 |
-
db_codes = set(db_products_map.keys())
|
94 |
-
fetched_codes = set(fetched_products_map.keys())
|
95 |
-
codes_to_delete = list(db_codes - fetched_codes)
|
96 |
-
|
97 |
-
# Use mongo.db directly
|
98 |
-
if codes_to_delete: mongo.db.products.delete_many({'code': {'$in': codes_to_delete}})
|
99 |
-
if products_to_insert: mongo.db.products.insert_many(products_to_insert)
|
100 |
-
if bulk_update_ops: mongo.db.products.bulk_write(bulk_update_ops)
|
101 |
-
|
102 |
-
sub_title = (f"Sync complete. Inserted: {len(products_to_insert)}, "
|
103 |
-
f"Updated: {len(bulk_update_ops)}, Deleted: {len(codes_to_delete)}")
|
104 |
-
code_to_display = jsonify_xero(fetched_products)
|
105 |
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
|
112 |
-
return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)
|
113 |
|
114 |
-
@
|
115 |
-
|
|
|
116 |
"""
|
117 |
-
|
118 |
-
On GET, it displays a form with all products.
|
119 |
-
On POST, it updates the product's image URL in the database.
|
120 |
"""
|
121 |
-
|
122 |
-
|
123 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
124 |
if request.method == "POST":
|
125 |
-
|
126 |
-
|
127 |
-
|
128 |
-
if not product_code or not new_image_url:
|
129 |
-
flash("Product and a new image URL are required.", "error")
|
130 |
-
return redirect(url_for("xero.edit_inventory"))
|
131 |
-
|
132 |
-
# Update the document in MongoDB
|
133 |
-
# Use mongo.db directly
|
134 |
-
result = mongo.db.products.update_one(
|
135 |
-
{"code": product_code},
|
136 |
-
{"$set": {"image_url": new_image_url}}
|
137 |
-
)
|
138 |
-
|
139 |
-
if result.matched_count:
|
140 |
-
flash(f"Image for product '{product_code}' updated successfully!", "success")
|
141 |
else:
|
142 |
-
|
143 |
-
|
144 |
-
|
145 |
-
|
146 |
-
|
147 |
-
|
148 |
-
products = list(mongo.db.products.find({}, {"_id": 0, "
|
149 |
-
return render_template("edit_inventory.html", title="Edit Inventory Image", products=products)
|
150 |
-
|
151 |
-
# --- NEW FEATURE CODE ENDS HERE ---
|
|
|
|
|
1 |
import logging
|
2 |
+
import traceback
|
3 |
+
import urllib.parse
|
4 |
+
import json
|
5 |
+
from flask import Blueprint, render_template, jsonify, request, flash, redirect, url_for, current_app, session, Response, stream_with_context
|
6 |
from pymongo import UpdateOne
|
7 |
+
from collections import defaultdict
|
8 |
+
from bson.binary import Binary
|
9 |
+
import random
|
10 |
+
import requests
|
11 |
+
|
12 |
+
# This part of the code assumes the Zoho client functions are in a file named `xero_client.py`
|
13 |
+
from .xero_client import (
|
14 |
+
zoho_token_required,
|
15 |
+
is_zoho_token_available,
|
16 |
+
initialize_zoho_sdk,
|
17 |
+
clear_zoho_token,
|
18 |
+
make_zoho_api_request
|
19 |
+
)
|
20 |
+
from .extensions import mongo
|
21 |
+
from utils import jsonify as jsonify_custom
|
22 |
from search_engine import search_and_filter_images, categorise
|
23 |
+
|
24 |
+
zoho_bp = Blueprint('zoho', __name__)
|
|
|
|
|
25 |
logger = logging.getLogger(__name__)
|
26 |
|
27 |
|
28 |
+
@zoho_bp.context_processor
|
29 |
+
def inject_zoho_status():
|
30 |
+
"""Makes the Zoho token status available to all templates."""
|
31 |
+
return dict(is_zoho_token_available=is_zoho_token_available)
|
32 |
+
|
|
|
|
|
|
|
33 |
|
34 |
+
@zoho_bp.route("/WEBUI")
|
35 |
def index():
|
36 |
+
"""Renders the home page with a welcome message."""
|
37 |
+
welcome_message = """
|
38 |
+
Welcome to the Zoho Books Integration Dashboard.
|
39 |
+
|
40 |
+
Use the navigation bar above to:
|
41 |
+
- Sync Inventory: Pull all items from Zoho, find images, and save to our database.
|
42 |
+
- Edit Inventory: Manually override image URLs for specific products.
|
43 |
+
|
44 |
+
Your current Zoho Token Status is: {}
|
45 |
+
""".format("Available" if is_zoho_token_available() else "Not Available. Please login.")
|
46 |
+
|
47 |
return render_template(
|
48 |
"code.html",
|
49 |
title="Home",
|
50 |
+
sub_title="Welcome to the Zoho Integration",
|
51 |
+
code=welcome_message,
|
52 |
+
language="text"
|
53 |
)
|
54 |
|
|
|
|
|
|
|
|
|
55 |
|
56 |
+
@zoho_bp.route("/login")
|
57 |
+
def login():
|
58 |
+
config = current_app.config
|
59 |
+
params = {
|
60 |
+
'scope': 'ZohoBooks.fullaccess.all',
|
61 |
+
'client_id': config['ZOHO_CLIENT_ID'],
|
62 |
+
'response_type': 'code',
|
63 |
+
'access_type': 'offline',
|
64 |
+
'redirect_uri': config['ZOHO_REDIRECT_URL']
|
65 |
+
}
|
66 |
+
accounts_url = 'https://accounts.zoho.in/oauth/v2/auth'
|
67 |
+
auth_url = f"{accounts_url}?{urllib.parse.urlencode(params)}"
|
68 |
+
return redirect(auth_url)
|
69 |
+
|
70 |
+
|
71 |
+
@zoho_bp.route("/callback")
|
72 |
def oauth_callback():
|
73 |
+
grant_token = request.args.get('code')
|
74 |
+
accounts_server = request.args.get('accounts-server')
|
75 |
+
|
76 |
+
if not grant_token:
|
77 |
+
flash("Authorization failed: No grant token received from Zoho.", "error")
|
78 |
+
return redirect(url_for('zoho.index'))
|
79 |
try:
|
80 |
+
initialize_zoho_sdk(grant_token=grant_token, accounts_server_url=accounts_server)
|
81 |
+
flash("Successfully authenticated with Zoho Books!", "success")
|
82 |
+
next_url = session.pop('next_url', url_for('zoho.index'))
|
83 |
+
return redirect(next_url)
|
84 |
except Exception as e:
|
85 |
+
logger.error(f"Error during Zoho OAuth callback: {e}", exc_info=True)
|
86 |
+
flash(f"An error occurred during authentication: {e}", "error")
|
87 |
+
return redirect(url_for('zoho.index'))
|
|
|
|
|
|
|
|
|
|
|
88 |
|
89 |
+
|
90 |
+
@zoho_bp.route("/logout")
|
91 |
def logout():
|
92 |
+
clear_zoho_token()
|
93 |
+
flash("You have been logged out from Zoho.", "info")
|
94 |
+
return redirect(url_for("zoho.index"))
|
95 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
96 |
|
97 |
+
@zoho_bp.route("/api/inventory")
|
98 |
+
@zoho_token_required
|
99 |
+
def inventory_sync_page():
|
100 |
+
"""Renders the page that will display the sync progress bar."""
|
101 |
+
return render_template("inventory_sync.html", title="Inventory Sync")
|
102 |
|
|
|
103 |
|
104 |
+
@zoho_bp.route("/api/inventory/stream")
|
105 |
+
@zoho_token_required
|
106 |
+
def fetch_inventory_stream():
|
107 |
"""
|
108 |
+
Performs the inventory sync and streams progress updates to the client.
|
|
|
|
|
109 |
"""
|
110 |
+
def generate_sync_updates():
|
111 |
+
try:
|
112 |
+
yield f"data: {json.dumps({'progress': 0, 'message': 'Starting synchronization...'})}\n\n"
|
113 |
+
|
114 |
+
# Step 1: Fetch all items from Zoho
|
115 |
+
all_zoho_items = []
|
116 |
+
page = 1
|
117 |
+
has_more_pages = True
|
118 |
+
while has_more_pages:
|
119 |
+
yield f"data: {json.dumps({'progress': 5, 'message': f'Fetching page {page} of items from Zoho...'})}\n\n"
|
120 |
+
params = {'page': page, 'per_page': 200}
|
121 |
+
response_data, _ = make_zoho_api_request('GET', '/items', params=params)
|
122 |
+
|
123 |
+
if not response_data: break
|
124 |
+
page_items = response_data.get('items', [])
|
125 |
+
if not page_items: break
|
126 |
+
|
127 |
+
all_zoho_items.extend(item for item in page_items if item.get('status') == 'active')
|
128 |
+
has_more_pages = response_data.get('page_context', {}).get('has_more_page', False)
|
129 |
+
page += 1
|
130 |
+
|
131 |
+
total_items = len(all_zoho_items)
|
132 |
+
yield f"data: {json.dumps({'progress': 15, 'message': f'Found {total_items} active items. Grouping by product name...'})}\n\n"
|
133 |
+
|
134 |
+
# Step 2: Group items by name
|
135 |
+
grouped_products = defaultdict(lambda: {
|
136 |
+
'modes': {}, 'description': '', 'zoho_id_for_image': None, 'name': None, 'category': None
|
137 |
+
})
|
138 |
+
for item in all_zoho_items:
|
139 |
+
name = item.get('name')
|
140 |
+
unit = item.get('unit', 'piece').lower()
|
141 |
+
if not grouped_products[name]['description'] and item.get('description'): grouped_products[name]['description'] = item.get('description')
|
142 |
+
if not grouped_products[name]['category'] and item.get('cf_type'): grouped_products[name]['category'] = item.get('cf_type')
|
143 |
+
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')
|
144 |
+
grouped_products[name]['name'] = name
|
145 |
+
grouped_products[name]['modes'][unit] = {'price': float(item.get('rate', 0.0)), 'zoho_id': item.get('item_id'), 'sku': item.get('sku')}
|
146 |
+
|
147 |
+
# Step 3: Process and sync with MongoDB
|
148 |
+
db_products_map = {p['name']: p for p in mongo.db.products.find({}, {'name': 1})}
|
149 |
+
products_to_insert, bulk_update_ops = [], []
|
150 |
+
total_to_process = len(grouped_products)
|
151 |
+
|
152 |
+
for i, (name, fetched_p) in enumerate(grouped_products.items()):
|
153 |
+
progress = 15 + int(((i + 1) / total_to_process) * 80)
|
154 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Processing: {name}'})}\n\n"
|
155 |
+
|
156 |
+
image_data, image_content_type, fallback_image_url = None, None, None
|
157 |
+
zoho_image_id = fetched_p.get('zoho_id_for_image')
|
158 |
+
|
159 |
+
if zoho_image_id:
|
160 |
+
try:
|
161 |
+
content, headers = make_zoho_api_request('GET', f'/items/{zoho_image_id}/image')
|
162 |
+
if content and headers:
|
163 |
+
image_data = Binary(content)
|
164 |
+
image_content_type = headers.get('Content-Type', 'image/jpeg')
|
165 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Downloaded existing image for {name} from Zoho.'})}\n\n"
|
166 |
+
except Exception:
|
167 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Failed to get Zoho image for {name}. Will search online.'})}\n\n"
|
168 |
+
|
169 |
+
if not image_data:
|
170 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Searching for new image for: {name}...'})}\n\n"
|
171 |
+
try:
|
172 |
+
results = search_and_filter_images(str(name))
|
173 |
+
if results:
|
174 |
+
fallback_image_url = str(results[0]["image_url"])
|
175 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Downloading new image for: {name}...'})}\n\n"
|
176 |
+
response = requests.get(fallback_image_url, timeout=15)
|
177 |
+
response.raise_for_status()
|
178 |
+
image_content_from_url, content_type_from_url = response.content, response.headers.get('Content-Type', 'image/jpeg')
|
179 |
+
image_data, image_content_type = Binary(image_content_from_url), content_type_from_url
|
180 |
+
|
181 |
+
for mode_details in fetched_p['modes'].values():
|
182 |
+
item_id = mode_details.get('zoho_id')
|
183 |
+
if item_id:
|
184 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Uploading image for {name} to Zoho Item ID {item_id}...'})}\n\n"
|
185 |
+
files = {'image': (f"{name.replace(' ', '_')}.jpg", image_content_from_url, content_type_from_url)}
|
186 |
+
make_zoho_api_request('POST', f'/items/{item_id}/image', files=files)
|
187 |
+
else:
|
188 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'No image results found for: {name}'})}\n\n"
|
189 |
+
except Exception as e:
|
190 |
+
yield f"data: {json.dumps({'progress': progress, 'message': f'Error fetching new image for {name}: {e}'})}\n\n"
|
191 |
+
image_data, image_content_type = None, None
|
192 |
+
|
193 |
+
update_fields = {
|
194 |
+
"name": name, "modes": fetched_p["modes"], "description": fetched_p.get('description', ''),
|
195 |
+
"image_data": image_data, "image_content_type": image_content_type, "image_url": fallback_image_url,
|
196 |
+
"category": fetched_p.get('category') or str(categorise(name)), "code": random.randint(1,100000),
|
197 |
+
}
|
198 |
+
|
199 |
+
if not db_products_map: products_to_insert.append(update_fields)
|
200 |
+
else: bulk_update_ops.append(UpdateOne({'name': name}, {'$set': update_fields}, upsert=True))
|
201 |
+
|
202 |
+
yield f"data: {json.dumps({'progress': 95, 'message': 'Finalizing database updates...'})}\n\n"
|
203 |
+
db_names, fetched_names = set(db_products_map.keys()), set(grouped_products.keys())
|
204 |
+
names_to_delete = list(db_names - fetched_names)
|
205 |
+
if names_to_delete: mongo.db.products.delete_many({'name': {'$in': names_to_delete}})
|
206 |
+
if products_to_insert: mongo.db.products.insert_many(products_to_insert)
|
207 |
+
if bulk_update_ops: mongo.db.products.bulk_write(bulk_update_ops)
|
208 |
+
|
209 |
+
summary = f"Sync complete. Inserted/Updated: {len(products_to_insert) + len(bulk_update_ops)}, Deleted: {len(names_to_delete)}"
|
210 |
+
final_code = jsonify_custom(list(grouped_products.values()))
|
211 |
+
yield f"data: {json.dumps({'progress': 100, 'message': summary, 'status': 'complete', 'final_code': final_code})}\n\n"
|
212 |
+
|
213 |
+
except Exception as e:
|
214 |
+
logger.error("Inventory sync stream failed: %s", e, exc_info=True)
|
215 |
+
error_message = f"Error during sync: {e}\n{traceback.format_exc()}"
|
216 |
+
yield f"data: {json.dumps({'progress': 100, 'message': error_message, 'status': 'error'})}\n\n"
|
217 |
+
|
218 |
+
return Response(stream_with_context(generate_sync_updates()), mimetype='text/event-stream')
|
219 |
+
|
220 |
+
|
221 |
+
@zoho_bp.route("/api/edit_inventory", methods=["GET", "POST"])
|
222 |
+
def edit_inventory():
|
223 |
if request.method == "POST":
|
224 |
+
product_name, new_image_url = request.form.get("product_name"), request.form.get("image_url")
|
225 |
+
if not product_name or not new_image_url:
|
226 |
+
flash("Product name and a new image URL are required.", "error")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
227 |
else:
|
228 |
+
result = mongo.db.products.update_one(
|
229 |
+
{"name": product_name}, {"$set": {"image_url": new_image_url, "image_data": None, "image_content_type": None}}
|
230 |
+
)
|
231 |
+
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")
|
232 |
+
return redirect(url_for("zoho.edit_inventory"))
|
233 |
+
|
234 |
+
products = list(mongo.db.products.find({}, {"_id": 0, "name": 1, "image_url": 1}).sort("name", 1))
|
235 |
+
return render_template("edit_inventory.html", title="Edit Inventory Image", products=products)
|
|
|
|
app/xero_utils.py
CHANGED
@@ -3,347 +3,272 @@ import threading
|
|
3 |
from datetime import datetime
|
4 |
from flask import current_app
|
5 |
from bson.objectid import ObjectId
|
6 |
-
from xero_python.accounting import (
|
7 |
-
AccountingApi, Contact, Contacts, ContactPerson,ContactGroup,ContactGroups, PurchaseOrder,
|
8 |
-
PurchaseOrders, LineItem, Address, Phone, HistoryRecord, HistoryRecords
|
9 |
-
)
|
10 |
|
11 |
-
|
12 |
-
from .
|
|
|
13 |
|
14 |
logger = logging.getLogger(__name__)
|
15 |
-
|
|
|
16 |
"""
|
17 |
-
|
18 |
-
|
19 |
-
|
|
|
20 |
"""
|
21 |
-
logger.info("Starting
|
22 |
try:
|
23 |
-
|
24 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
25 |
|
26 |
-
|
27 |
-
all_groups = accounting_api.get_contact_groups(xero_tenant_id).contact_groups
|
28 |
-
approved_group_info = next((g for g in all_groups if g.name == "approved_user"), None)
|
29 |
|
30 |
-
|
31 |
-
|
32 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
33 |
|
34 |
-
|
35 |
-
approved_group_full = accounting_api.get_contact_group(
|
36 |
-
xero_tenant_id, approved_group_info.contact_group_id
|
37 |
-
).contact_groups[0]
|
38 |
-
|
39 |
-
# Create a set of approved contact IDs for efficient lookup
|
40 |
-
approved_contact_ids = {str(contact.contact_id) for contact in approved_group_full.contacts}
|
41 |
-
logger.info(f"Found {len(approved_contact_ids)} approved contacts in Xero group 'approve_user'.")
|
42 |
-
|
43 |
-
# --- Step 3: Iterate through all users in MongoDB ---
|
44 |
-
users_in_db = mongo.db.users.find({})
|
45 |
-
update_count = 0
|
46 |
-
|
47 |
-
for user in users_in_db:
|
48 |
-
user_xero_id = user.get('xero_contact_id')
|
49 |
-
current_approval_status = user.get('is_approved', False)
|
50 |
-
|
51 |
-
if not user_xero_id:
|
52 |
-
# Skip users who are not yet synced to Xero
|
53 |
-
continue
|
54 |
-
|
55 |
-
# --- Step 4: Check membership and update MongoDB if necessary ---
|
56 |
-
is_now_approved = str(user_xero_id) in approved_contact_ids
|
57 |
-
|
58 |
-
if is_now_approved != current_approval_status:
|
59 |
-
# Status has changed, update the user in MongoDB
|
60 |
-
mongo.db.users.update_one(
|
61 |
-
{'_id': user['_id']},
|
62 |
-
{'$set': {'is_approved': is_now_approved}}
|
63 |
-
)
|
64 |
-
logger.info(f"Updated approval status for user {user['email']} to {is_now_approved}.")
|
65 |
-
update_count += 1
|
66 |
-
|
67 |
-
logger.info(f"Xero approval sync complete. Updated {update_count} users.")
|
68 |
|
69 |
except Exception as e:
|
70 |
-
logger.error(f"An error occurred during
|
|
|
71 |
|
72 |
-
|
73 |
-
def create_xero_contact_async(app_context, registration_data):
|
74 |
"""
|
75 |
-
Creates a contact in
|
76 |
-
|
77 |
"""
|
78 |
with app_context:
|
79 |
user_email = registration_data.get('email')
|
80 |
try:
|
81 |
-
xero_tenant_id = get_xero_tenant_id()
|
82 |
-
accounting_api = AccountingApi(api_client)
|
83 |
-
|
84 |
-
# --- Step 1: Map and Create the Core Contact ---
|
85 |
-
|
86 |
contact_person_name = registration_data.get('contactPerson', '')
|
87 |
first_name, last_name = (contact_person_name.split(' ', 1) + [''])[:2]
|
88 |
|
89 |
-
|
90 |
-
|
91 |
-
|
92 |
-
|
93 |
-
|
94 |
-
|
95 |
-
|
96 |
-
|
97 |
-
|
98 |
-
|
99 |
-
|
100 |
-
|
101 |
-
|
102 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
103 |
|
104 |
-
|
105 |
-
|
106 |
-
|
107 |
-
|
108 |
-
|
109 |
-
|
110 |
-
|
111 |
-
|
112 |
-
|
113 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
114 |
)
|
|
|
|
|
|
|
|
|
115 |
|
116 |
-
|
117 |
-
|
118 |
-
|
119 |
-
|
|
|
120 |
|
121 |
-
# --- Step 2: Add Registration Details as a History Note ---
|
122 |
-
|
123 |
-
if not created_contact_response.contacts:
|
124 |
-
logger.error(f"Xero contact creation failed for {user_email}. No contact returned.")
|
125 |
-
return
|
126 |
-
|
127 |
-
# Get the ID of the contact we just created
|
128 |
-
new_contact_id = created_contact_response.contacts[0].contact_id
|
129 |
-
logger.info(f"Successfully created base Xero contact ({new_contact_id}) for user {user_email}. Now adding history.")
|
130 |
-
contact_groups = accounting_api.get_contact_groups(xero_tenant_id).contact_groups
|
131 |
-
pending_group = next((group for group in contact_groups if group.name == "pending_for_approval"), None)
|
132 |
-
|
133 |
-
if pending_group:
|
134 |
-
logger.info(f"Adding contact {new_contact_id} to pending_for_approval group {pending_group.contact_group_id}.")
|
135 |
-
# Prepare the group update payload with the new contact added
|
136 |
-
cg = ContactGroup(
|
137 |
-
contact_group_id=pending_group.contact_group_id,
|
138 |
-
contacts=[Contact(contact_id=new_contact_id)]
|
139 |
-
)
|
140 |
-
response = accounting_api.create_contact_group_contacts(
|
141 |
-
xero_tenant_id=xero_tenant_id,
|
142 |
-
contact_group_id=pending_group.contact_group_id,
|
143 |
-
contacts=Contacts(contacts=[Contact(contact_id=new_contact_id)])
|
144 |
-
)
|
145 |
-
logger.info(response)
|
146 |
-
logger.info(f"Contact {new_contact_id} added to contact group 'pending_for_approval'.")
|
147 |
-
else:
|
148 |
-
logger.warning("Contact group 'pending_for_approval' not found—skipping group update.")
|
149 |
-
# First, update our own database with the new ID
|
150 |
-
mongo.db.users.update_one(
|
151 |
-
{'email': user_email},
|
152 |
-
{'$set': {'xero_contact_id': new_contact_id}}
|
153 |
-
)
|
154 |
|
155 |
-
# Prepare the detailed notes for the history record
|
156 |
-
history_details = (
|
157 |
-
f"--- Client Application Details ---\n"
|
158 |
-
f"Business Name: {registration_data.get('businessName')}\n"
|
159 |
-
f"Contact Person: {registration_data.get('contactPerson')}\n"
|
160 |
-
f"Email: {registration_data.get('email')}\n"
|
161 |
-
f"Phone: {registration_data.get('phoneNumber')}\n"
|
162 |
-
f"Company Website: {registration_data.get('companyWebsite')}\n"
|
163 |
-
f"Business Address: {registration_data.get('businessAddress')}\n"
|
164 |
-
f"Business Type: {registration_data.get('businessType')}\n"
|
165 |
-
f"Years Operating: {registration_data.get('yearsOperating')}\n"
|
166 |
-
f"Number of Locations: {registration_data.get('numLocations')}\n"
|
167 |
-
f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n"
|
168 |
-
|
169 |
-
f"--- Logistics Information ---\n"
|
170 |
-
f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n"
|
171 |
-
f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n"
|
172 |
-
f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n"
|
173 |
-
f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n"
|
174 |
-
|
175 |
-
f"--- Service & Billing ---\n"
|
176 |
-
f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n"
|
177 |
-
f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n"
|
178 |
-
|
179 |
-
f"--- Additional Notes ---\n"
|
180 |
-
f"{registration_data.get('additionalNotes')}"
|
181 |
-
)
|
182 |
-
#f"Supplier Priorities: {registration_data.get('priorities')}\n"
|
183 |
-
|
184 |
-
|
185 |
-
# Create the HistoryRecord payload
|
186 |
-
history_record = HistoryRecord(details=history_details)
|
187 |
-
history_payload = HistoryRecords(history_records=[history_record])
|
188 |
-
|
189 |
-
# Make the second API call to add the note
|
190 |
-
accounting_api.create_contact_history(
|
191 |
-
xero_tenant_id=xero_tenant_id,
|
192 |
-
contact_id=new_contact_id,
|
193 |
-
history_records=history_payload
|
194 |
-
)
|
195 |
-
|
196 |
-
logger.info(f"Successfully added registration history note to Xero contact {new_contact_id}.")
|
197 |
|
198 |
-
except Exception as e:
|
199 |
-
# This will catch errors from either the contact creation or history creation
|
200 |
-
logger.error(f"Failed during Xero contact/history creation for user {user_email}. Error: {e}")
|
201 |
-
# --- NEW FUNCTION: The trigger to run the contact creation in the background ---
|
202 |
def trigger_contact_creation(registration_data):
|
203 |
-
"""Starts a background thread to create a
|
204 |
try:
|
205 |
app_context = current_app.app_context()
|
206 |
-
thread = threading.Thread(
|
207 |
-
target=create_xero_contact_async,
|
208 |
-
args=(app_context, registration_data)
|
209 |
-
)
|
210 |
thread.daemon = True
|
211 |
thread.start()
|
212 |
-
logger.info(f"Started
|
213 |
except Exception as e:
|
214 |
-
logger.error(f"Failed to start
|
215 |
-
|
216 |
-
|
217 |
-
|
218 |
-
#
|
219 |
-
|
220 |
-
|
221 |
-
|
222 |
-
|
223 |
-
|
224 |
-
|
225 |
-
|
226 |
-
|
227 |
-
|
228 |
-
|
229 |
-
|
230 |
-
|
231 |
-
|
232 |
-
|
233 |
-
# # ... (rest of your PO logic is mostly the same)
|
234 |
-
# all_product_ids = [ObjectId(item['productId']) for item in order_details['items']]
|
235 |
-
# products_cursor = mongo.db.products.find({'_id': {'$in': all_product_ids}})
|
236 |
-
# product_map = {str(p['_id']): p for p in products_cursor}
|
237 |
-
|
238 |
-
# xero_items = accounting_api.get_items(xero_tenant_id).items
|
239 |
-
# xero_item_map = {item.name: item.code for item in xero_items}
|
240 |
-
|
241 |
-
# line_items = []
|
242 |
-
# for item in order_details['items']:
|
243 |
-
# product = product_map.get(item['productId'])
|
244 |
-
# if product:
|
245 |
-
# product_name = product.get('name', 'N/A')
|
246 |
-
# item_code = xero_item_map.get(product_name, "")
|
247 |
-
# line_items.append(
|
248 |
-
# LineItem(
|
249 |
-
# item_code=item_code,
|
250 |
-
# description=product_name+" ("+str(item["mode"]) + ")",
|
251 |
-
# quantity=item['quantity'],
|
252 |
-
# unit_amount=float(product.get('price', 0))
|
253 |
-
# )
|
254 |
-
# )
|
255 |
-
|
256 |
-
# if not line_items:
|
257 |
-
# logger.error("Xero PO failed: No valid line items for order %s", order_details['order_id'])
|
258 |
-
# return
|
259 |
-
|
260 |
-
# purchase_order = PurchaseOrder(
|
261 |
-
# contact=Contact(contact_id=contact_id), # <-- Use the specific contact_id
|
262 |
-
# date=datetime.now().date(),
|
263 |
-
# delivery_date=datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").date(),
|
264 |
-
# delivery_address=order_details['delivery_address'],
|
265 |
-
# telephone=order_details['mobile_number'],
|
266 |
-
# reference=str(order_details['user_email']),
|
267 |
-
# line_items=line_items,
|
268 |
-
# status="AUTHORISED"
|
269 |
-
# )
|
270 |
-
|
271 |
-
# result = accounting_api.create_purchase_orders(
|
272 |
-
# xero_tenant_id, purchase_orders=PurchaseOrders(purchase_orders=[purchase_order])
|
273 |
-
# )
|
274 |
-
# logger.info("Created Xero PO for order ID: %s", order_details['order_id'])
|
275 |
-
|
276 |
-
# # except Exception as e:
|
277 |
-
# # logger.error("Failed to create Xero PO for order %s. Error: %s", order_details['order_id'], e)
|
278 |
-
|
279 |
-
def create_xero_purchase_order_async(app_context, xero_tenant_id, order_details):
|
280 |
with app_context:
|
281 |
-
|
282 |
-
|
283 |
-
|
284 |
-
all_product_ids = [ObjectId(item['productId']) for item in order_details['items']]
|
285 |
-
# Use mongo.db directly
|
286 |
-
products_cursor = mongo.db.products.find({'_id': {'$in': all_product_ids}})
|
287 |
-
product_map = {str(p['_id']): p for p in products_cursor}
|
288 |
-
|
289 |
-
xero_items = accounting_api.get_items(xero_tenant_id).items
|
290 |
-
xero_item_map = {item.name: item.code for item in xero_items}
|
291 |
-
|
292 |
-
line_items = []
|
293 |
-
for item in order_details['items']:
|
294 |
-
product = product_map.get(item['productId'])
|
295 |
-
if product:
|
296 |
-
product_name = product.get('name', 'N/A')
|
297 |
-
item_code = xero_item_map.get(product_name, "")
|
298 |
-
unitss=item["mode"]
|
299 |
-
if unitss == "weight":
|
300 |
-
unitss = "lb"
|
301 |
-
line_items.append(
|
302 |
-
LineItem(
|
303 |
-
item_code=item_code,
|
304 |
-
description=str(item['quantity']) + " " + unitss + " of " + product_name,
|
305 |
-
quantity=item['quantity'],
|
306 |
-
unit_amount=float(product.get('price', 0))
|
307 |
-
)
|
308 |
-
)
|
309 |
-
|
310 |
-
if not line_items:
|
311 |
-
logger.error("Xero PO failed: No valid line items for order %s", order_details['order_id'])
|
312 |
-
return
|
313 |
-
email_id=order_details['user_email']
|
314 |
-
|
315 |
-
contact_id = accounting_api.get_contacts(xero_tenant_id,where=f'EmailAddress=="{email_id}"').contacts[0].contact_id
|
316 |
-
print(order_details["deliverydate"])
|
317 |
-
purchase_order = PurchaseOrder(
|
318 |
-
contact=Contact(contact_id=contact_id),
|
319 |
-
date=datetime.now().date(),
|
320 |
-
delivery_date=datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").date(),
|
321 |
-
delivery_address=order_details['delivery_address'],
|
322 |
-
telephone=order_details['mobile_number'],
|
323 |
-
reference=str(order_details['user_email']),
|
324 |
-
line_items=line_items,
|
325 |
-
status="SUBMITTED"
|
326 |
-
)
|
327 |
|
328 |
-
|
329 |
-
|
330 |
-
|
331 |
-
logger.info("Created Xero PO for order ID: %s", order_details['order_id'])
|
332 |
|
333 |
-
# except Exception as e:
|
334 |
-
# logger.error("Failed to create Xero PO for order %s. Error: %s", order_details['order_id'], e)
|
335 |
|
336 |
-
|
337 |
-
|
338 |
-
|
339 |
-
|
340 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
341 |
|
342 |
-
|
343 |
-
|
344 |
-
|
345 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
346 |
thread.daemon = True
|
347 |
thread.start()
|
348 |
-
|
349 |
-
|
|
|
|
3 |
from datetime import datetime
|
4 |
from flask import current_app
|
5 |
from bson.objectid import ObjectId
|
|
|
|
|
|
|
|
|
6 |
|
7 |
+
# Import the new REST API client helper
|
8 |
+
from .xero_client import make_zoho_api_request
|
9 |
+
from .extensions import mongo
|
10 |
|
11 |
logger = logging.getLogger(__name__)
|
12 |
+
|
13 |
+
def sync_user_approval_from_zoho():
|
14 |
"""
|
15 |
+
Updates users' 'is_approved' status based on the 'cf_approval_status'
|
16 |
+
custom dropdown in Zoho Books. A contact is considered approved when:
|
17 |
+
custom_field.label == 'cf_approval_status' and custom_field.value == 'approved'
|
18 |
+
This function pages through contacts and inspects custom_fields for the match.
|
19 |
"""
|
20 |
+
logger.info("Starting Zoho Books 'cf_approval_status' custom field sync.")
|
21 |
try:
|
22 |
+
page = 1
|
23 |
+
per_page = 200 # Zoho typically supports up to 200 per page; adjust if necessary
|
24 |
+
approved_contact_ids = set()
|
25 |
+
|
26 |
+
while True:
|
27 |
+
params = {'page': page, 'per_page': per_page}
|
28 |
+
response = make_zoho_api_request('GET', '/contacts', params=params)
|
29 |
+
|
30 |
+
if not response:
|
31 |
+
logger.warning(f"No response from Zoho when fetching contacts page {page}. Stopping pagination.")
|
32 |
+
break
|
33 |
+
logger.info(response)
|
34 |
+
|
35 |
+
contacts = response[0]['contacts'] or []
|
36 |
+
if not contacts:
|
37 |
+
logger.info(f"No contacts returned on page {page}. Pagination complete.")
|
38 |
+
break
|
39 |
+
|
40 |
+
for contact in contacts:
|
41 |
+
# Contact id field might be 'contact_id' or similar - defensive access
|
42 |
+
contact_id = contact.get('email') or contact.get('contactId') or contact.get('id')
|
43 |
+
# Ensure we have a custom_fields list to check
|
44 |
+
custom_fields = contact.get('custom_fields') or []
|
45 |
+
for cf in custom_fields:
|
46 |
+
# cf typically has keys like 'label' and 'value' when you use label-based assignment
|
47 |
+
if cf.get('label') == 'cf_approval_status' and str(cf.get('value')).lower() == 'approved':
|
48 |
+
if contact_id:
|
49 |
+
approved_contact_ids.add(str(contact_id))
|
50 |
+
break # no need to check other custom fields for this contact
|
51 |
+
|
52 |
+
# If response contains page context we can use it; otherwise continue until an empty page
|
53 |
+
page_context = response[0]['page_context']
|
54 |
+
has_more = page_context.get('has_more_page')
|
55 |
+
if has_more is None:
|
56 |
+
# fallback: stop when we received fewer than per_page results
|
57 |
+
if len(contacts) < per_page:
|
58 |
+
break
|
59 |
+
page += 1
|
60 |
+
else:
|
61 |
+
if not has_more:
|
62 |
+
break
|
63 |
+
page += 1
|
64 |
|
65 |
+
logger.info(f"Found {len(approved_contact_ids)} approved contacts in Zoho Books (cf_approval_status == 'approved').")
|
|
|
|
|
66 |
|
67 |
+
# Set all users to not approved first (only users with a zoho_contact_id are considered)
|
68 |
+
# mongo.db.users.update_many({'zoho_contact_id': {'$exists': True}}, {'$set': {'is_approved': False}})
|
69 |
+
|
70 |
+
# Then, set the ones found in the sync to approved
|
71 |
+
if approved_contact_ids:
|
72 |
+
mongo.db.users.update_many(
|
73 |
+
{'email': {'$in': list(approved_contact_ids)}},
|
74 |
+
{'$set': {'is_approved': True}}
|
75 |
+
)
|
76 |
|
77 |
+
logger.info(f"Zoho approval sync complete. {len(approved_contact_ids)} users are now marked as approved.")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
78 |
|
79 |
except Exception as e:
|
80 |
+
logger.error(f"An error occurred during Zoho user approval sync: {e}", exc_info=True)
|
81 |
+
|
82 |
|
83 |
+
def create_zoho_contact_async(app_context, registration_data):
|
|
|
84 |
"""
|
85 |
+
Creates a contact in Zoho Books, sets a 'pending' custom field, and adds a note (comment).
|
86 |
+
Assumes a custom field exists whose label is 'cf_approval_status' (adjust label/index if needed).
|
87 |
"""
|
88 |
with app_context:
|
89 |
user_email = registration_data.get('email')
|
90 |
try:
|
|
|
|
|
|
|
|
|
|
|
91 |
contact_person_name = registration_data.get('contactPerson', '')
|
92 |
first_name, last_name = (contact_person_name.split(' ', 1) + [''])[:2]
|
93 |
|
94 |
+
contact_payload = {
|
95 |
+
'contact_name': registration_data.get('businessName'),
|
96 |
+
'contact_type': 'customer',
|
97 |
+
'company_name': registration_data.get('companyName') ,
|
98 |
+
'contact_persons': [{
|
99 |
+
'first_name': first_name,
|
100 |
+
'last_name': last_name,
|
101 |
+
'email': user_email,
|
102 |
+
'phone': registration_data.get('phoneNumber'),
|
103 |
+
# keep this: it's a valid flag for the contact person entry
|
104 |
+
'is_primary_contact': True
|
105 |
+
}],
|
106 |
+
'billing_address': {
|
107 |
+
'address': registration_data.get('businessAddress', 'N/A')
|
108 |
+
},
|
109 |
+
'shipping_address': {
|
110 |
+
'address': registration_data.get('businessAddress', 'N/A')
|
111 |
+
},
|
112 |
+
'website': registration_data.get('companyWebsite'),
|
113 |
+
# <-- Use label/index + value (not api_name)
|
114 |
+
'custom_fields': [
|
115 |
+
{
|
116 |
+
'label': 'cf_approval_status', # must match the field label in Zoho Books
|
117 |
+
# 'index': 1, # optional: set if you know the slot (1..10)
|
118 |
+
'value': 'pending_for_approval' # value must match one of the dropdown option values
|
119 |
+
}
|
120 |
+
]
|
121 |
+
}
|
122 |
+
|
123 |
+
response = make_zoho_api_request('POST', '/contacts', json_data=contact_payload)
|
124 |
+
|
125 |
+
# defensive logging: log whole response so you can inspect Zoho's error message if any
|
126 |
+
logger.debug(f"Zoho create contact response: {response}")
|
127 |
+
|
128 |
+
# # basic success check — adjust depending on your make_zoho_api_request return structure
|
129 |
+
# if not response or 'contact' not in response:
|
130 |
+
# logger.error(f"Zoho Books contact creation failed for {user_email}: {response}")
|
131 |
+
# return
|
132 |
+
|
133 |
+
new_contact_id = response[0]['contact']['contact_id']
|
134 |
+
logger.info(f"Successfully created Zoho Books contact ({new_contact_id}) for user {user_email}.")
|
135 |
+
|
136 |
+
mongo.db.users.update_one({'email': user_email}, {'$set': {'zoho_contact_id': new_contact_id}})
|
137 |
|
138 |
+
history_details = (
|
139 |
+
f"--- Client Application Details ---\n"
|
140 |
+
f"Business Name: {registration_data.get('businessName')}\n"
|
141 |
+
f"Contact Person: {registration_data.get('contactPerson')}\n"
|
142 |
+
f"Email: {registration_data.get('email')}\n"
|
143 |
+
f"Phone: {registration_data.get('phoneNumber')}\n"
|
144 |
+
f"Company Website: {registration_data.get('companyWebsite')}\n"
|
145 |
+
f"Business Address: {registration_data.get('businessAddress')}\n"
|
146 |
+
f"Business Type: {registration_data.get('businessType')}\n"
|
147 |
+
f"Years Operating: {registration_data.get('yearsOperating')}\n"
|
148 |
+
f"Number of Locations: {registration_data.get('numLocations')}\n"
|
149 |
+
f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n"
|
150 |
+
|
151 |
+
f"--- Logistics Information ---\n"
|
152 |
+
f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n"
|
153 |
+
f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n"
|
154 |
+
f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n"
|
155 |
+
f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n"
|
156 |
+
|
157 |
+
f"--- Service & Billing ---\n"
|
158 |
+
f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n"
|
159 |
+
f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n"
|
160 |
+
|
161 |
+
f"--- Additional Notes ---\n"
|
162 |
+
f"{registration_data.get('additionalNotes')}"
|
163 |
)
|
164 |
+
comment_payload = {
|
165 |
+
'description': history_details,
|
166 |
+
'show_comment_to_clients': False
|
167 |
+
}
|
168 |
|
169 |
+
make_zoho_api_request('POST', f'/contacts/{new_contact_id}/comments', json_data=comment_payload)
|
170 |
+
logger.info(f"Successfully added registration details as a comment to Zoho contact {new_contact_id}.")
|
171 |
+
|
172 |
+
except Exception as e:
|
173 |
+
logger.error(f"Failed during Zoho Books contact/comment creation for user {user_email}. Error: {e}", exc_info=True)
|
174 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
175 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
176 |
|
|
|
|
|
|
|
|
|
177 |
def trigger_contact_creation(registration_data):
|
178 |
+
"""Starts a background thread to create a Zoho contact."""
|
179 |
try:
|
180 |
app_context = current_app.app_context()
|
181 |
+
thread = threading.Thread(target=create_zoho_contact_async, args=(app_context, registration_data))
|
|
|
|
|
|
|
182 |
thread.daemon = True
|
183 |
thread.start()
|
184 |
+
logger.info(f"Started Zoho contact creation thread for {registration_data.get('email')}")
|
185 |
except Exception as e:
|
186 |
+
logger.error(f"Failed to start Zoho contact creation thread. Error: {e}")
|
187 |
+
def get_zoho_contact_by_email(email):
|
188 |
+
"""Fetches a Zoho contact ID by email."""
|
189 |
+
try:
|
190 |
+
# Assumes make_zoho_api_request is a helper function that handles authentication
|
191 |
+
response = make_zoho_api_request('GET', '/contacts', params={'email': email})
|
192 |
+
contacts = response[0]['contacts']
|
193 |
+
if contacts:
|
194 |
+
return contacts[0]['contact_id']
|
195 |
+
else:
|
196 |
+
logger.info(f"No Zoho contact found for email: {email}")
|
197 |
+
return None
|
198 |
+
except Exception as e:
|
199 |
+
logger.error(f"Error fetching Zoho contact for {email}: {e}", exc_info=True)
|
200 |
+
return None
|
201 |
+
|
202 |
+
def create_zoho_invoice_async(app_context, order_details):
|
203 |
+
"""Creates an Invoice in Zoho Books from order details (CAD currency, order no -> reference_number)."""
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
204 |
with app_context:
|
205 |
+
user_email = order_details['user_email']
|
206 |
+
contact_id = get_zoho_contact_by_email(user_email)
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
207 |
|
208 |
+
if not contact_id:
|
209 |
+
logger.error(f"Cannot create Invoice. Zoho contact not found for email {user_email}.")
|
210 |
+
return
|
|
|
211 |
|
|
|
|
|
212 |
|
213 |
+
line_items = []
|
214 |
+
for item in order_details['items']:
|
215 |
+
product = mongo.db.products.find_one({'_id': ObjectId(item['productId'])})
|
216 |
+
for mode in product.get('modes', []):
|
217 |
+
if str(mode) == item['mode']:
|
218 |
+
product['zoho_id'] = product.get('modes')[mode].get('zoho_id')
|
219 |
+
product['price'] = product.get('modes')[mode].get('price')
|
220 |
+
break
|
221 |
+
|
222 |
+
logger.info(f"Processing item {product.get('zoho_id')} with mode {item['mode']} for invoice creation.")
|
223 |
+
unit = "lb" if item.get("mode") == "weight" else item.get("mode")
|
224 |
+
line_items.append({
|
225 |
+
'item_id': product['zoho_id'],
|
226 |
+
'quantity': int(item['quantity']),
|
227 |
+
'rate': float(product.get('price', 0)),
|
228 |
+
'description': f"{item['quantity']} {unit} of {product.get('name', 'N/A')}"
|
229 |
+
})
|
230 |
+
|
231 |
+
if not line_items:
|
232 |
+
logger.error("Zoho Invoice failed: No valid line items for order %s", order_details['order_id'])
|
233 |
+
return
|
234 |
|
235 |
+
# build invoice payload with CAD and order number
|
236 |
+
logger.info( order_details.get('additional_info', 'N/A'))
|
237 |
+
invoice_payload = {
|
238 |
+
'customer_id': contact_id,
|
239 |
+
'date': datetime.strptime(order_details.get("order_date", order_details["deliverydate"]), "%Y-%m-%d").strftime("%Y-%m-%d"),
|
240 |
+
'due_date': datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").strftime("%Y-%m-%d"),
|
241 |
+
'line_items': line_items,
|
242 |
+
'notes': order_details.get('additional_info', 'N/A'),
|
243 |
+
'billing_address': {
|
244 |
+
'address': order_details.get('delivery_address')
|
245 |
+
},
|
246 |
+
|
247 |
+
# <--- currency & order mapping
|
248 |
+
'currency_code': 'CAD', # invoice currency = Canadian Dollars
|
249 |
+
'exchange_rate': 1.0, # set to appropriate rate (1.0 if you treat amounts as CAD already)
|
250 |
+
'reference_number': order_details['order_id'], # shows your order number on the invoice
|
251 |
+
|
252 |
+
# Optionally add custom fields if you have created one and know its customfield_id
|
253 |
+
# 'custom_fields': [
|
254 |
+
# {'customfield_id': 123456789012345, 'value': order_details['order_id']}
|
255 |
+
# ]
|
256 |
+
}
|
257 |
+
|
258 |
+
# send to Zoho
|
259 |
+
response = make_zoho_api_request('POST', '/invoices', json_data=invoice_payload)
|
260 |
+
print(response)
|
261 |
+
invoice_id = response['invoice']['invoice_id']
|
262 |
+
logger.info(f"Successfully created Zoho Books Invoice {invoice_id} for order ID: {order_details['order_id']}")
|
263 |
+
|
264 |
+
|
265 |
+
def trigger_invoice_creation(order_details):
|
266 |
+
"""Starts a background thread to create a Zoho Invoice."""
|
267 |
+
try:
|
268 |
+
app_context = current_app.app_context()
|
269 |
+
thread = threading.Thread(target=create_zoho_invoice_async, args=(app_context, order_details))
|
270 |
thread.daemon = True
|
271 |
thread.start()
|
272 |
+
logger.info(f"Started Zoho Invoice creation thread for order {order_details.get('order_id')}")
|
273 |
+
except Exception as e:
|
274 |
+
logger.error(f"Failed to start Zoho Invoice creation thread for order %s. Error: %s", order_details.get('order_id'), e)
|
auth_token.py
ADDED
@@ -0,0 +1,17 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
import requests
|
2 |
+
ZOHO_CLIENT_ID = "1000.4MJJE7NC4Y6KHZ9FFDXS90OHERFP0M"
|
3 |
+
ZOHO_CLIENT_SECRET = "5abfd2f10d995b81c02d42463511930529c746491a"
|
4 |
+
ZOHO_REFRESH_TOKEN = "1000.8e784c3d79574f813d44e0d72201dc26.d493569b713a155ea0d4ac0816e65f3d" # Generate this from the API console
|
5 |
+
ZOHO_CURRENT_USER_EMAIL = "vaibhavarduino@gmail.com"
|
6 |
+
ZOHO_REDIRECT_URL = "http://localhost:7860/callback" # e.g., http://localhost:5000/callback
|
7 |
+
|
8 |
+
data = {
|
9 |
+
'client_id': ZOHO_CLIENT_ID,
|
10 |
+
'client_secret':ZOHO_CLIENT_SECRET,
|
11 |
+
'grant_type': 'authorization_code',
|
12 |
+
'code': "1000.ec6e9f0e35d5ed30123122dd6ec7efbf.e4e539a890f2b17a8ad8d5775d30deea" ,
|
13 |
+
'redirect_uri': ZOHO_REDIRECT_URL,
|
14 |
+
}
|
15 |
+
|
16 |
+
response = requests.post('https://accounts.zoho.com/oauth/v2/token', data=data)
|
17 |
+
print(response.text)
|
downloaded_image.jpeg
ADDED
![]() |
downloaded_image.jpeg;charset=UTF-8
ADDED
Binary file (48.9 kB). View file
|
|
flask_session/aa71dde20eaf768ca7e5f90a25563ea6
CHANGED
Binary files a/flask_session/aa71dde20eaf768ca7e5f90a25563ea6 and b/flask_session/aa71dde20eaf768ca7e5f90a25563ea6 differ
|
|
run.py
CHANGED
@@ -4,4 +4,4 @@ os.environ['OAUTHLIB_INSECURE_TRANSPORT'] = '1'
|
|
4 |
app = create_app()
|
5 |
|
6 |
if __name__ == '__main__':
|
7 |
-
app.run(host="0.0.0.0",debug=
|
|
|
4 |
app = create_app()
|
5 |
|
6 |
if __name__ == '__main__':
|
7 |
+
app.run(host="0.0.0.0",debug=True, port=7860)
|
search_engine.py
CHANGED
@@ -13,6 +13,8 @@ class Categorise(BaseModel):
|
|
13 |
client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))
|
14 |
|
15 |
def categorise(product):
|
|
|
|
|
16 |
try:
|
17 |
|
18 |
|
@@ -44,10 +46,10 @@ def search_images(query: str, api_key: str, cse_id: str,no) -> dict | None:
|
|
44 |
"""
|
45 |
print(f"Searching for images with query: '{query}'...")
|
46 |
try:
|
47 |
-
service = build("customsearch", "v1", developerKey=
|
48 |
result = service.cse().list(
|
49 |
q=query,
|
50 |
-
cx=
|
51 |
searchType='image',
|
52 |
num=no
|
53 |
).execute()
|
|
|
13 |
client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))
|
14 |
|
15 |
def categorise(product):
|
16 |
+
client = genai.Client(api_key=random.choice(json.loads(os.getenv("GEMINI_KEY_LIST"))))
|
17 |
+
|
18 |
try:
|
19 |
|
20 |
|
|
|
46 |
"""
|
47 |
print(f"Searching for images with query: '{query}'...")
|
48 |
try:
|
49 |
+
service = build("customsearch", "v1", developerKey="AIzaSyBntcCqrtL5tdpM3iIXzPvKydCvZx1KdqQ")
|
50 |
result = service.cse().list(
|
51 |
q=query,
|
52 |
+
cx="a2982aa5c06f54e66",
|
53 |
searchType='image',
|
54 |
num=no
|
55 |
).execute()
|
zoho_resources/resources/dmFpYmhhdmFyZHVpbm9odHRwczovL3d3dy56b2hvYXBpcy5pbg==.json
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"FIELDS-LAST-MODIFIED-TIME": 1754830442729.9026, "leads": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Company": {"type": "String", "name": "Company"}, "First_Name": {"type": "String", "name": "First_Name"}, "Last_Name": {"required": true, "type": "String", "name": "Last_Name"}, "Full_Name": {"type": "String", "name": "Full_Name"}, "Designation": {"type": "String", "name": "Designation"}, "Email": {"type": "String", "name": "Email"}, "Phone": {"type": "String", "name": "Phone"}, "Fax": {"type": "String", "name": "Fax"}, "Mobile": {"type": "String", "name": "Mobile"}, "Website": {"type": "String", "name": "Website"}, "Lead_Source": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Advertisement", "Cold Call", "Employee Referral", "External Referral", "OnlineStore", "Partner", "Public Relations", "Sales Mail Alias", "Seminar Partner", "Seminar-Internal", "Trade Show", "Web Download", "Web Research", "Chat", "X (Twitter)", "Facebook"], "name": "Lead_Source"}, "Lead_Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Attempted to Contact", "Contact in Future", "Contacted", "Junk Lead", "Lost Lead", "Not Contacted", "Pre-Qualified", "Not Qualified"], "name": "Lead_Status"}, "Industry": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "ASP (Application Service Provider)", "Data/Telecom OEM", "ERP (Enterprise Resource Planning)", "Government/Military", "Large Enterprise", "ManagementISV", "MSP (Management Service Provider)", "Network Equipment (Enterprise)", "Non-management ISV", "Optical Networking", "Service Provider", "Small/Medium Enterprise", "Storage Equipment", "Storage Service Provider", "Systems Integrator", "Wireless Industry"], "name": "Industry"}, "No_of_Employees": {"type": "Integer", "name": "No_of_Employees"}, "Annual_Revenue": {"type": "Float", "name": "Annual_Revenue"}, "Rating": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Acquired", "Active", "Market Failed", "Project Cancelled", "ShutDown"], "name": "Rating"}, "Email_Opt_Out": {"type": "Boolean", "name": "Email_Opt_Out"}, "Skype_ID": {"type": "String", "name": "Skype_ID"}, "Salutation": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Mr.", "Mrs.", "Ms.", "Dr.", "Prof."], "name": "Salutation"}, "Secondary_Email": {"type": "String", "name": "Secondary_Email"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Twitter": {"type": "String", "name": "Twitter"}, "Street": {"type": "String", "name": "Street"}, "City": {"type": "String", "name": "City"}, "State": {"type": "String", "name": "State"}, "Zip_Code": {"type": "String", "name": "Zip_Code"}, "Country": {"type": "String", "name": "Country"}, "Description": {"type": "String", "name": "Description"}, "Record_Image": {"type": "String", "name": "Record_Image"}, "Unsubscribed_Mode": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Consent form", "Manual", "Unsubscribe link", "Zoho campaigns"], "name": "Unsubscribed_Mode"}, "Unsubscribed_Time": {"type": "DateTime", "name": "Unsubscribed_Time"}, "Change_Log_Time__s": {"type": "DateTime", "name": "Change_Log_Time__s"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Enriched_Time__s": {"type": "DateTime", "name": "Last_Enriched_Time__s"}, "Enrich_Status__s": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Available", "Enriched", "Data not found"], "name": "Enrich_Status__s"}}, "contacts": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Lead_Source": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Advertisement", "Cold Call", "Employee Referral", "External Referral", "OnlineStore", "Partner", "Public Relations", "Sales Mail Alias", "Seminar Partner", "Seminar-Internal", "Trade Show", "Web Download", "Web Research", "Web Cases", "Web Mail", "Chat", "X (Twitter)", "Facebook"], "name": "Lead_Source"}, "First_Name": {"type": "String", "name": "First_Name"}, "Last_Name": {"required": true, "type": "String", "name": "Last_Name"}, "Full_Name": {"type": "String", "name": "Full_Name"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Vendor_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Vendors", "name": "Vendor_Name"}, "Email": {"type": "String", "name": "Email"}, "Title": {"type": "String", "name": "Title"}, "Department": {"type": "String", "name": "Department"}, "Phone": {"type": "String", "name": "Phone"}, "Home_Phone": {"type": "String", "name": "Home_Phone"}, "Other_Phone": {"type": "String", "name": "Other_Phone"}, "Fax": {"type": "String", "name": "Fax"}, "Mobile": {"type": "String", "name": "Mobile"}, "Date_of_Birth": {"type": "Date", "name": "Date_of_Birth"}, "Assistant": {"type": "String", "name": "Assistant"}, "Asst_Phone": {"type": "String", "name": "Asst_Phone"}, "Email_Opt_Out": {"type": "Boolean", "name": "Email_Opt_Out"}, "Skype_ID": {"type": "String", "name": "Skype_ID"}, "Salutation": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Mr.", "Mrs.", "Ms.", "Dr.", "Prof."], "name": "Salutation"}, "Secondary_Email": {"type": "String", "name": "Secondary_Email"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Twitter": {"type": "String", "name": "Twitter"}, "Mailing_Street": {"type": "String", "name": "Mailing_Street"}, "Other_Street": {"type": "String", "name": "Other_Street"}, "Mailing_City": {"type": "String", "name": "Mailing_City"}, "Other_City": {"type": "String", "name": "Other_City"}, "Mailing_State": {"type": "String", "name": "Mailing_State"}, "Other_State": {"type": "String", "name": "Other_State"}, "Mailing_Zip": {"type": "String", "name": "Mailing_Zip"}, "Other_Zip": {"type": "String", "name": "Other_Zip"}, "Mailing_Country": {"type": "String", "name": "Mailing_Country"}, "Other_Country": {"type": "String", "name": "Other_Country"}, "Description": {"type": "String", "name": "Description"}, "Record_Image": {"type": "String", "name": "Record_Image"}, "Reporting_To": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Reporting_To"}, "Unsubscribed_Mode": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Consent form", "Manual", "Unsubscribe link", "Zoho campaigns"], "name": "Unsubscribed_Mode"}, "Unsubscribed_Time": {"type": "DateTime", "name": "Unsubscribed_Time"}, "Change_Log_Time__s": {"type": "DateTime", "name": "Change_Log_Time__s"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Enriched_Time__s": {"type": "DateTime", "name": "Last_Enriched_Time__s"}, "Enrich_Status__s": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Available", "Enriched", "Data not found"], "name": "Enrich_Status__s"}}, "accounts": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Rating": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Acquired", "Active", "Market Failed", "Project Cancelled", "ShutDown"], "name": "Rating"}, "Account_Name": {"required": true, "type": "String", "name": "Account_Name"}, "Phone": {"type": "String", "name": "Phone"}, "Fax": {"type": "String", "name": "Fax"}, "Parent_Account": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Parent_Account"}, "Website": {"type": "String", "name": "Website"}, "Ticker_Symbol": {"type": "String", "name": "Ticker_Symbol"}, "Account_Type": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Analyst", "Competitor", "Customer", "Distributor", "Integrator", "Investor", "Other", "Partner", "Press", "Prospect", "Reseller", "Supplier", "Vendor"], "name": "Account_Type"}, "Ownership": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Other", "Private", "Public", "Subsidiary"], "name": "Ownership"}, "Industry": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "ASP (Application Service Provider)", "Data/Telecom OEM", "ERP (Enterprise Resource Planning)", "Government/Military", "Large Enterprise", "ManagementISV", "MSP (Management Service Provider)", "Network Equipment (Enterprise)", "Non-management ISV", "Optical Networking", "Service Provider", "Small/Medium Enterprise", "Storage Equipment", "Storage Service Provider", "Systems Integrator", "Wireless Industry"], "name": "Industry"}, "Employees": {"type": "Integer", "name": "Employees"}, "Annual_Revenue": {"type": "Float", "name": "Annual_Revenue"}, "SIC_Code": {"type": "Integer", "name": "SIC_Code"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Account_Number": {"type": "String", "name": "Account_Number"}, "Account_Site": {"type": "String", "name": "Account_Site"}, "Billing_Street": {"type": "String", "name": "Billing_Street"}, "Shipping_Street": {"type": "String", "name": "Shipping_Street"}, "Billing_City": {"type": "String", "name": "Billing_City"}, "Shipping_City": {"type": "String", "name": "Shipping_City"}, "Billing_State": {"type": "String", "name": "Billing_State"}, "Shipping_State": {"type": "String", "name": "Shipping_State"}, "Billing_Code": {"type": "String", "name": "Billing_Code"}, "Shipping_Code": {"type": "String", "name": "Shipping_Code"}, "Billing_Country": {"type": "String", "name": "Billing_Country"}, "Shipping_Country": {"type": "String", "name": "Shipping_Country"}, "Description": {"type": "String", "name": "Description"}, "Record_Image": {"type": "String", "name": "Record_Image"}, "Change_Log_Time__s": {"type": "DateTime", "name": "Change_Log_Time__s"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Enriched_Time__s": {"type": "DateTime", "name": "Last_Enriched_Time__s"}, "Enrich_Status__s": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Available", "Enriched", "Data not found"], "name": "Enrich_Status__s"}}, "vendors": {"Vendor_Name": {"required": true, "type": "String", "name": "Vendor_Name"}, "Phone": {"type": "String", "name": "Phone"}, "Email": {"type": "String", "name": "Email"}, "Website": {"type": "String", "name": "Website"}, "GL_Account": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Sales-Software", "Sales-Hardware", "Rental-Income", "Interest-Income", "Sales-Software-Support", "Sales Other", "Interest Sales", "Service-Hardware Labor"], "name": "GL_Account"}, "Category": {"type": "String", "name": "Category"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Street": {"type": "String", "name": "Street"}, "City": {"type": "String", "name": "City"}, "State": {"type": "String", "name": "State"}, "Zip_Code": {"type": "String", "name": "Zip_Code"}, "Country": {"type": "String", "name": "Country"}, "Description": {"type": "String", "name": "Description"}, "Record_Image": {"type": "String", "name": "Record_Image"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Email_Opt_Out": {"type": "Boolean", "name": "Email_Opt_Out"}, "Unsubscribed_Mode": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Consent form", "Manual", "Unsubscribe link", "Zoho campaigns"], "name": "Unsubscribed_Mode"}, "Unsubscribed_Time": {"type": "DateTime", "name": "Unsubscribed_Time"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "deals": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Amount": {"type": "Float", "name": "Amount"}, "Deal_Name": {"required": true, "type": "String", "name": "Deal_Name"}, "Closing_Date": {"type": "Date", "name": "Closing_Date"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Stage": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Qualification", "Needs Analysis", "Value Proposition", "Id. Decision Makers", "Proposal/Price Quote", "Negotiation/Review", "Closed Won", "Closed Lost", "Closed Lost to Competition"], "name": "Stage"}, "Type": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Existing Business", "New Business"], "name": "Type"}, "Probability": {"type": "Integer", "name": "Probability"}, "Expected_Revenue": {"type": "Float", "name": "Expected_Revenue"}, "Next_Step": {"type": "String", "name": "Next_Step"}, "Lead_Source": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Advertisement", "Cold Call", "Employee Referral", "External Referral", "OnlineStore", "Partner", "Public Relations", "Sales Mail Alias", "Seminar Partner", "Seminar-Internal", "Trade Show", "Web Download", "Web Research", "Chat"], "name": "Lead_Source"}, "Campaign_Source": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Campaigns", "name": "Campaign_Source"}, "Contact_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Contact_Name"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Lead_Conversion_Time": {"type": "Integer", "name": "Lead_Conversion_Time"}, "Sales_Cycle_Duration": {"type": "Integer", "name": "Sales_Cycle_Duration"}, "Overall_Sales_Duration": {"type": "Integer", "name": "Overall_Sales_Duration"}, "Description": {"type": "String", "name": "Description"}, "Change_Log_Time__s": {"type": "DateTime", "name": "Change_Log_Time__s"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Reason_For_Loss__s": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Expectation Mismatch", "Price", "Unqualified Customer", "Lack of response", "Missed Follow Ups", "Wrong Target", "Competition", "Future Interest", "Other"], "name": "Reason_For_Loss__s"}}, "campaigns": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Type": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Conference", "Webinar", "Trade Show", "Public Relations", "Partners", "Referral Program", "Advertisement", "Banner Ads", "Direct mail", "Email", "Telemarketing", "Others"], "name": "Type"}, "Campaign_Name": {"required": true, "type": "String", "name": "Campaign_Name"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Planning", "Active", "Inactive", "Complete"], "name": "Status"}, "Start_Date": {"type": "Date", "name": "Start_Date"}, "End_Date": {"type": "Date", "name": "End_Date"}, "Expected_Revenue": {"type": "Float", "name": "Expected_Revenue"}, "Budgeted_Cost": {"type": "Float", "name": "Budgeted_Cost"}, "Actual_Cost": {"type": "Float", "name": "Actual_Cost"}, "Expected_Response": {"type": "String", "name": "Expected_Response"}, "Num_sent": {"type": "String", "name": "Num_sent"}, "Description": {"type": "String", "name": "Description"}, "Parent_Campaign": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Campaigns", "name": "Parent_Campaign"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "activities": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Activity_Type": {"type": "String", "name": "Activity_Type"}, "Who_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Who_Id"}, "What_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "What_Id"}, "Description": {"type": "String", "name": "Description"}, "Due_Date": {"type": "Date", "name": "Due_Date"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Not Started", "Deferred", "In Progress", "Completed", "Waiting on someone else"], "name": "Status"}, "Priority": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["High", "Highest", "Low", "Lowest", "Normal"], "name": "Priority"}, "Closed_Time": {"type": "DateTime", "name": "Closed_Time"}, "Recurring_Activity": {"type": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "name": "Recurring_Activity"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Venue": {"type": "String", "name": "Venue"}, "All_day": {"type": "Boolean", "name": "All_day"}, "Start_DateTime": {"required": true, "type": "DateTime", "name": "Start_DateTime"}, "End_DateTime": {"required": true, "type": "DateTime", "name": "End_DateTime"}, "Participants": {"name": "Participants", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Participants", "skip_mandatory": true}, "Check_In_Time": {"type": "DateTime", "name": "Check_In_Time"}, "Check_In_By": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Check_In_By"}, "Check_In_Comment": {"type": "String", "name": "Check_In_Comment"}, "Check_In_Sub_Locality": {"type": "String", "name": "Check_In_Sub_Locality"}, "Check_In_City": {"type": "String", "name": "Check_In_City"}, "Check_In_State": {"type": "String", "name": "Check_In_State"}, "Check_In_Country": {"type": "String", "name": "Check_In_Country"}, "Latitude": {"type": "Float", "name": "Latitude"}, "Longitude": {"type": "Float", "name": "Longitude"}, "ZIP_Code": {"type": "String", "name": "ZIP_Code"}, "Check_In_Address": {"type": "String", "name": "Check_In_Address"}, "Check_In_Status": {"type": "String", "name": "Check_In_Status"}, "Call_Type": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Outbound", "Inbound", "Missed"], "name": "Call_Type"}, "Call_Purpose": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Prospecting", "Administrative", "Negotiation", "Demo", "Project", "Support"], "name": "Call_Purpose"}, "Call_Start_Time": {"required": true, "type": "DateTime", "name": "Call_Start_Time"}, "Call_Duration": {"required": true, "type": "String", "name": "Call_Duration"}, "Call_Duration_in_seconds": {"type": "Integer", "name": "Call_Duration_in_seconds"}, "Call_Result": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Interested", "Not interested", "No response/Busy", "Requested more info", "Requested call back", "Invalid number"], "name": "Call_Result"}, "CTI_Entry": {"type": "Boolean", "name": "CTI_Entry"}, "Reminder": {"type": "String", "name": "Reminder"}, "Call_Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "name": "Call_Status"}, "Call_Agenda": {"type": "String", "name": "Call_Agenda"}, "Caller_ID": {"type": "String", "name": "Caller_ID"}, "Dialled_Number": {"type": "String", "name": "Dialled_Number"}, "Voice_Recording__s": {"type": "String", "name": "Voice_Recording__s"}}, "tasks": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Due_Date": {"type": "Date", "name": "Due_Date"}, "Who_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Who_Id"}, "What_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "What_Id"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Not Started", "Deferred", "In Progress", "Completed", "Waiting on someone else"], "name": "Status"}, "Priority": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["High", "Highest", "Low", "Lowest", "Normal"], "name": "Priority"}, "Closed_Time": {"type": "DateTime", "name": "Closed_Time"}, "Send_Notification_Email": {"type": "Boolean", "name": "Send_Notification_Email"}, "Recurring_Activity": {"type": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "name": "Recurring_Activity"}, "Remind_At": {"type": "zcrmsdk.src.com.zoho.crm.api.record.RemindAt", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.RemindAt", "name": "Remind_At"}, "Description": {"type": "String", "name": "Description"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "events": {"Event_Title": {"required": true, "type": "String", "name": "Event_Title"}, "Venue": {"type": "String", "name": "Venue"}, "All_day": {"type": "Boolean", "name": "All_day"}, "Start_DateTime": {"required": true, "type": "DateTime", "name": "Start_DateTime"}, "End_DateTime": {"required": true, "type": "DateTime", "name": "End_DateTime"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Who_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Who_Id"}, "What_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "What_Id"}, "Recurring_Activity": {"type": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.RecurringActivity", "name": "Recurring_Activity"}, "Remind_At": {"type": "DateTime", "name": "Remind_At"}, "Participants": {"name": "Participants", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Participants", "skip_mandatory": true}, "Description": {"type": "String", "name": "Description"}, "Check_In_Time": {"type": "DateTime", "name": "Check_In_Time"}, "Check_In_By": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Check_In_By"}, "Check_In_Comment": {"type": "String", "name": "Check_In_Comment"}, "Check_In_Sub_Locality": {"type": "String", "name": "Check_In_Sub_Locality"}, "Check_In_City": {"type": "String", "name": "Check_In_City"}, "Check_In_State": {"type": "String", "name": "Check_In_State"}, "Check_In_Country": {"type": "String", "name": "Check_In_Country"}, "Latitude": {"type": "Float", "name": "Latitude"}, "Longitude": {"type": "Float", "name": "Longitude"}, "ZIP_Code": {"type": "String", "name": "ZIP_Code"}, "Check_In_Address": {"type": "String", "name": "Check_In_Address"}, "Check_In_Status": {"type": "String", "name": "Check_In_Status"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "calls": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Subject": {"type": "String", "name": "Subject"}, "Call_Type": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Outbound", "Inbound", "Missed"], "name": "Call_Type"}, "Call_Purpose": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Prospecting", "Administrative", "Negotiation", "Demo", "Project", "Support"], "name": "Call_Purpose"}, "Who_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Who_Id"}, "What_Id": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "What_Id"}, "Call_Start_Time": {"required": true, "type": "DateTime", "name": "Call_Start_Time"}, "Call_Duration": {"type": "String", "name": "Call_Duration"}, "Call_Duration_in_seconds": {"type": "Integer", "name": "Call_Duration_in_seconds"}, "Description": {"type": "String", "name": "Description"}, "Call_Result": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Interested", "Not interested", "No response/Busy", "Requested more info", "Requested call back", "Invalid number"], "name": "Call_Result"}, "CTI_Entry": {"type": "Boolean", "name": "CTI_Entry"}, "Reminder": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["None", "5 minutes before", "10 minutes before", "15 minutes before", "30 minutes before", "1 hour before", "2 hours before", "1 day before", "2 days before"], "name": "Reminder"}, "Call_Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "name": "Call_Status"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "Call_Agenda": {"type": "String", "name": "Call_Agenda"}, "Caller_ID": {"type": "String", "name": "Caller_ID"}, "Dialled_Number": {"type": "String", "name": "Dialled_Number"}}, "products": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Product_Name": {"required": true, "type": "String", "name": "Product_Name"}, "Product_Code": {"type": "String", "name": "Product_Code"}, "Vendor_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Vendors", "name": "Vendor_Name"}, "Product_Active": {"type": "Boolean", "name": "Product_Active"}, "Manufacturer": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "AltvetPet Inc.", "LexPon Inc.", "MetBeat Corp."], "name": "Manufacturer"}, "Product_Category": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Hardware", "Software", "CRM Applications"], "name": "Product_Category"}, "Sales_Start_Date": {"type": "Date", "name": "Sales_Start_Date"}, "Sales_End_Date": {"type": "Date", "name": "Sales_End_Date"}, "Support_Start_Date": {"type": "Date", "name": "Support_Start_Date"}, "Support_Expiry_Date": {"type": "Date", "name": "Support_Expiry_Date"}, "Unit_Price": {"type": "Float", "name": "Unit_Price"}, "Commission_Rate": {"type": "Float", "name": "Commission_Rate"}, "Tax": {"type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "name": "Tax"}, "Taxable": {"type": "Boolean", "name": "Taxable"}, "Usage_Unit": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Box", "Carton", "Dozen", "Each", "Hours", "Impressions", "Lb", "M", "Pack", "Pages", "Pieces", "Quantity", "Reams", "Sheet", "Spiral Binder", "Sq Ft"], "name": "Usage_Unit"}, "Qty_Ordered": {"type": "Float", "name": "Qty_Ordered"}, "Qty_in_Stock": {"type": "Float", "name": "Qty_in_Stock"}, "Reorder_Level": {"type": "Float", "name": "Reorder_Level"}, "Handler": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Handler"}, "Qty_in_Demand": {"type": "Float", "name": "Qty_in_Demand"}, "Description": {"type": "String", "name": "Description"}, "Record_Image": {"type": "String", "name": "Record_Image"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "quotes": {"Team": {"type": "String", "name": "Team"}, "Quote_Number": {"type": "String", "name": "Quote_Number"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Deal_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Deals", "name": "Deal_Name"}, "Quote_Stage": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Draft", "Negotiation", "Delivered", "On Hold", "Confirmed", "Closed Won", "Closed Lost"], "name": "Quote_Stage"}, "Valid_Till": {"type": "Date", "name": "Valid_Till"}, "Contact_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Contact_Name"}, "Carrier": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["FedEX", "UPS", "USPS", "DHL", "BlueDart"], "name": "Carrier"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Billing_Street": {"type": "String", "name": "Billing_Street"}, "Shipping_Street": {"type": "String", "name": "Shipping_Street"}, "Billing_City": {"type": "String", "name": "Billing_City"}, "Shipping_City": {"type": "String", "name": "Shipping_City"}, "Billing_State": {"type": "String", "name": "Billing_State"}, "Shipping_State": {"type": "String", "name": "Shipping_State"}, "Billing_Code": {"type": "String", "name": "Billing_Code"}, "Shipping_Code": {"type": "String", "name": "Shipping_Code"}, "Billing_Country": {"type": "String", "name": "Billing_Country"}, "Shipping_Country": {"type": "String", "name": "Shipping_Country"}, "Product_Details": {"required": true, "name": "Product_Details", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.InventoryLineItems", "skip_mandatory": true}, "Sub_Total": {"type": "Float", "name": "Sub_Total"}, "Discount": {"type": "Float", "name": "Discount"}, "Tax": {"type": "Float", "name": "Tax"}, "Adjustment": {"type": "Float", "name": "Adjustment"}, "Grand_Total": {"type": "Float", "name": "Grand_Total"}, "Terms_and_Conditions": {"type": "String", "name": "Terms_and_Conditions"}, "Description": {"type": "String", "name": "Description"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "$line_tax": {"name": "$line_tax", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.LineTax"}}, "sales_orders": {"Customer_No": {"type": "String", "name": "Customer_No"}, "SO_Number": {"type": "String", "name": "SO_Number"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Deal_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Deals", "name": "Deal_Name"}, "Purchase_Order": {"type": "String", "name": "Purchase_Order"}, "Quote_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Quotes", "name": "Quote_Name"}, "Due_Date": {"type": "Date", "name": "Due_Date"}, "Pending": {"type": "String", "name": "Pending"}, "Contact_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Contact_Name"}, "Carrier": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["FedEX", "UPS", "USPS", "DHL", "BlueDart"], "name": "Carrier"}, "Excise_Duty": {"type": "Float", "name": "Excise_Duty"}, "Sales_Commission": {"type": "Float", "name": "Sales_Commission"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Created", "Approved", "Delivered", "Cancelled"], "name": "Status"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Billing_Street": {"type": "String", "name": "Billing_Street"}, "Shipping_Street": {"type": "String", "name": "Shipping_Street"}, "Billing_City": {"type": "String", "name": "Billing_City"}, "Shipping_City": {"type": "String", "name": "Shipping_City"}, "Billing_State": {"type": "String", "name": "Billing_State"}, "Shipping_State": {"type": "String", "name": "Shipping_State"}, "Billing_Code": {"type": "String", "name": "Billing_Code"}, "Shipping_Code": {"type": "String", "name": "Shipping_Code"}, "Billing_Country": {"type": "String", "name": "Billing_Country"}, "Shipping_Country": {"type": "String", "name": "Shipping_Country"}, "Product_Details": {"required": true, "name": "Product_Details", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.InventoryLineItems", "skip_mandatory": true}, "Sub_Total": {"type": "Float", "name": "Sub_Total"}, "Discount": {"type": "Float", "name": "Discount"}, "Tax": {"type": "Float", "name": "Tax"}, "Adjustment": {"type": "Float", "name": "Adjustment"}, "Grand_Total": {"type": "Float", "name": "Grand_Total"}, "Terms_and_Conditions": {"type": "String", "name": "Terms_and_Conditions"}, "Description": {"type": "String", "name": "Description"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "$line_tax": {"name": "$line_tax", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.LineTax"}}, "purchase_orders": {"Requisition_No": {"type": "String", "name": "Requisition_No"}, "Tracking_Number": {"type": "String", "name": "Tracking_Number"}, "PO_Number": {"type": "String", "name": "PO_Number"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Vendor_Name": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Vendors", "name": "Vendor_Name"}, "Contact_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Contact_Name"}, "PO_Date": {"type": "Date", "name": "PO_Date"}, "Due_Date": {"type": "Date", "name": "Due_Date"}, "Carrier": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["FedEX", "UPS", "USPS", "DHL", "BlueDart"], "name": "Carrier"}, "Excise_Duty": {"type": "Float", "name": "Excise_Duty"}, "Sales_Commission": {"type": "Float", "name": "Sales_Commission"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Created", "Approved", "Delivered", "Cancelled"], "name": "Status"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Billing_Street": {"type": "String", "name": "Billing_Street"}, "Shipping_Street": {"type": "String", "name": "Shipping_Street"}, "Billing_City": {"type": "String", "name": "Billing_City"}, "Shipping_City": {"type": "String", "name": "Shipping_City"}, "Billing_State": {"type": "String", "name": "Billing_State"}, "Shipping_State": {"type": "String", "name": "Shipping_State"}, "Billing_Code": {"type": "String", "name": "Billing_Code"}, "Shipping_Code": {"type": "String", "name": "Shipping_Code"}, "Billing_Country": {"type": "String", "name": "Billing_Country"}, "Shipping_Country": {"type": "String", "name": "Shipping_Country"}, "Product_Details": {"required": true, "name": "Product_Details", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.InventoryLineItems", "skip_mandatory": true}, "Sub_Total": {"type": "Float", "name": "Sub_Total"}, "Discount": {"type": "Float", "name": "Discount"}, "Tax": {"type": "Float", "name": "Tax"}, "Adjustment": {"type": "Float", "name": "Adjustment"}, "Grand_Total": {"type": "Float", "name": "Grand_Total"}, "Terms_and_Conditions": {"type": "String", "name": "Terms_and_Conditions"}, "Description": {"type": "String", "name": "Description"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "$line_tax": {"name": "$line_tax", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.LineTax"}}, "invoices": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Invoice_Number": {"type": "String", "name": "Invoice_Number"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Sales_Order": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Sales_Orders", "name": "Sales_Order"}, "Invoice_Date": {"type": "Date", "name": "Invoice_Date"}, "Purchase_Order": {"type": "String", "name": "Purchase_Order"}, "Due_Date": {"type": "Date", "name": "Due_Date"}, "Excise_Duty": {"type": "Float", "name": "Excise_Duty"}, "Sales_Commission": {"type": "Float", "name": "Sales_Commission"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Created", "Approved", "Delivered", "Cancelled"], "name": "Status"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Contact_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Contact_Name"}, "Billing_Street": {"type": "String", "name": "Billing_Street"}, "Shipping_Street": {"type": "String", "name": "Shipping_Street"}, "Billing_City": {"type": "String", "name": "Billing_City"}, "Shipping_City": {"type": "String", "name": "Shipping_City"}, "Billing_State": {"type": "String", "name": "Billing_State"}, "Shipping_State": {"type": "String", "name": "Shipping_State"}, "Billing_Code": {"type": "String", "name": "Billing_Code"}, "Shipping_Code": {"type": "String", "name": "Shipping_Code"}, "Billing_Country": {"type": "String", "name": "Billing_Country"}, "Shipping_Country": {"type": "String", "name": "Shipping_Country"}, "Product_Details": {"required": true, "name": "Product_Details", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.InventoryLineItems", "skip_mandatory": true}, "Sub_Total": {"type": "Float", "name": "Sub_Total"}, "Discount": {"type": "Float", "name": "Discount"}, "Tax": {"type": "Float", "name": "Tax"}, "Adjustment": {"type": "Float", "name": "Adjustment"}, "Grand_Total": {"type": "Float", "name": "Grand_Total"}, "Terms_and_Conditions": {"type": "String", "name": "Terms_and_Conditions"}, "Description": {"type": "String", "name": "Description"}, "Deal_Name__s": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Deals", "name": "Deal_Name__s"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}, "$line_tax": {"name": "$line_tax", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.LineTax"}}, "price_books": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Price_Book_Name": {"required": true, "type": "String", "name": "Price_Book_Name"}, "Active": {"type": "Boolean", "name": "Active"}, "Pricing_Model": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Flat", "Differential"], "name": "Pricing_Model"}, "Pricing_Details": {"name": "Pricing_Details", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.PricingDetails", "skip_mandatory": true}, "Description": {"type": "String", "name": "Description"}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "cases": {"Case_Reason": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "User did not attend any training", "Complex functionality", "Existing problem", "Instructions not clear", "New problem"], "name": "Case_Reason"}, "Case_Number": {"type": "String", "name": "Case_Number"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Status": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["New", "Escalated", "On Hold", "Closed"], "name": "Status"}, "Product_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Products", "name": "Product_Name"}, "Priority": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "High", "Medium", "Low"], "name": "Priority"}, "Type": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Problem", "Feature Request", "Question"], "name": "Type"}, "Case_Origin": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["-None-", "Email", "Phone", "Web"], "name": "Case_Origin"}, "Subject": {"required": true, "type": "String", "name": "Subject"}, "Related_To": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Contacts", "name": "Related_To"}, "Account_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Accounts", "skip_mandatory": true, "name": "Account_Name"}, "Deal_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Deals", "name": "Deal_Name"}, "No_of_comments": {"type": "Integer", "name": "No_of_comments"}, "Reported_By": {"type": "String", "name": "Reported_By"}, "Email": {"type": "String", "name": "Email"}, "Phone": {"type": "String", "name": "Phone"}, "Description": {"type": "String", "name": "Description"}, "Internal_Comments": {"type": "String", "name": "Internal_Comments"}, "Solution": {"type": "String", "name": "Solution"}, "Add_Comment": {"type": "String", "name": "Add_Comment"}, "Comments": {"name": "Comments", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Comment", "lookup": true}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "solutions": {"Solution_Number": {"type": "String", "name": "Solution_Number"}, "Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Solution_Title": {"required": true, "type": "String", "name": "Solution_Title"}, "Published": {"type": "Boolean", "name": "Published"}, "Status": {"type": "zcrmsdk.src.com.zoho.crm.api.util.Choice", "picklist": true, "values": ["Draft", "Reviewed", "Duplicate"], "name": "Status"}, "Product_Name": {"type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "module": "Products", "name": "Product_Name"}, "No_of_comments": {"type": "Integer", "name": "No_of_comments"}, "Question": {"type": "String", "name": "Question"}, "Answer": {"type": "String", "name": "Answer"}, "Add_Comment": {"type": "String", "name": "Add_Comment"}, "Comments": {"name": "Comments", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Comment", "lookup": true}, "Locked__s": {"type": "Boolean", "name": "Locked__s"}, "Last_Activity_Time": {"type": "DateTime", "name": "Last_Activity_Time"}}, "visits": {}, "notes": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "Note_Title": {"type": "String", "name": "Note_Title"}, "Note_Content": {"type": "String", "name": "Note_Content"}, "Parent_Id": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "Parent_Id"}, "$attachments": {"name": "$attachments", "type": "List", "structure_name": "zcrmsdk.src.com.zoho.crm.api.attachments.Attachment"}}, "attachments": {"Owner": {"type": "zcrmsdk.src.com.zoho.crm.api.users.User", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.users.User", "name": "Owner"}, "File_Name": {"required": true, "type": "String", "name": "File_Name"}, "Size": {"required": true, "type": "String", "name": "Size"}, "Parent_Id": {"required": true, "type": "zcrmsdk.src.com.zoho.crm.api.record.Record", "lookup": true, "structure_name": "zcrmsdk.src.com.zoho.crm.api.record.Record", "name": "Parent_Id"}}, "actions_performed": {}}
|
zoho_sdk.log
ADDED
@@ -0,0 +1,67 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
1 |
+
2025-08-10 17:06:09,388 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
2 |
+
2025-08-10 17:06:44,070 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
3 |
+
2025-08-10 17:06:58,661 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
4 |
+
2025-08-10 17:07:15,577 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
5 |
+
2025-08-10 17:07:21,916 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: Exception in saving tokens - Expecting value: line 1 column 1 (char 0)
|
6 |
+
2025-08-10 17:07:21,916 - SDKLogger - INFO - utility - utility.py - get_fields_info - 179 - ExceptionCaused By: Exception in saving tokens - Expecting value: line 1 column 1 (char 0)
|
7 |
+
2025-08-10 17:18:53,099 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
8 |
+
2025-08-10 17:19:26,517 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
9 |
+
2025-08-10 17:19:26,517 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
10 |
+
2025-08-10 17:19:27,539 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
11 |
+
2025-08-10 17:19:27,539 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
12 |
+
2025-08-10 17:20:12,591 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
13 |
+
2025-08-10 17:20:12,591 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
14 |
+
2025-08-10 17:20:12,591 - SDKLogger - INFO - utility - utility.py - get_fields_info - 179 - ExceptionCaused By: INVALID CLIENT ERROR - invalid_code
|
15 |
+
2025-08-10 17:20:12,591 - SDKLogger - INFO - utility - utility.py - get_fields_info - 179 - ExceptionCaused By: INVALID CLIENT ERROR - invalid_code
|
16 |
+
2025-08-10 17:23:28,849 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
17 |
+
2025-08-10 17:23:54,644 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
18 |
+
2025-08-10 17:24:32,517 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
19 |
+
2025-08-10 17:25:51,842 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
20 |
+
2025-08-10 17:26:20,454 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
21 |
+
2025-08-10 17:26:20,454 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
22 |
+
2025-08-10 17:26:23,491 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
23 |
+
2025-08-10 17:26:23,491 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
24 |
+
2025-08-10 17:29:29,573 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
25 |
+
2025-08-10 17:29:47,595 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
26 |
+
2025-08-10 17:29:47,595 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
27 |
+
2025-08-10 17:29:48,478 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
28 |
+
2025-08-10 17:29:48,478 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
29 |
+
2025-08-10 17:39:28,404 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
30 |
+
2025-08-10 17:39:44,288 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
31 |
+
2025-08-10 17:39:44,288 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
32 |
+
2025-08-10 17:39:45,676 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
33 |
+
2025-08-10 17:39:45,676 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
34 |
+
2025-08-10 17:50:25,398 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
35 |
+
2025-08-10 17:51:12,807 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
36 |
+
2025-08-10 17:51:20,619 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
37 |
+
2025-08-10 17:51:20,619 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
38 |
+
2025-08-10 18:10:32,191 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
39 |
+
2025-08-10 18:10:33,065 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
40 |
+
2025-08-10 18:18:43,308 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.com.
|
41 |
+
2025-08-10 18:18:44,154 - SDKLogger - INFO - common_api_handler - common_api_handler.py - api_call - 290 - Exception in authenticating current request : Caused By: INVALID CLIENT ERROR - invalid_code
|
42 |
+
2025-08-10 18:23:43,276 - SDKLogger - INFO - initializer - initializer.py - initialize - 130 - Initialization successful for Email Id : vaibhavarduino@gmail.com in Environment : https://www.zohoapis.in.
|
43 |
+
2025-08-10 18:23:43,465 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/modules , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {}.
|
44 |
+
2025-08-10 18:24:02,334 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/modules , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {}.
|
45 |
+
2025-08-10 18:24:02,733 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Leads"}.
|
46 |
+
2025-08-10 18:24:03,062 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Contacts"}.
|
47 |
+
2025-08-10 18:24:03,443 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Accounts"}.
|
48 |
+
2025-08-10 18:24:03,794 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Vendors"}.
|
49 |
+
2025-08-10 18:24:04,093 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Deals"}.
|
50 |
+
2025-08-10 18:24:04,392 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Campaigns"}.
|
51 |
+
2025-08-10 18:24:04,830 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Activities"}.
|
52 |
+
2025-08-10 18:24:05,490 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Tasks"}.
|
53 |
+
2025-08-10 18:24:05,738 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Events"}.
|
54 |
+
2025-08-10 18:24:05,996 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Calls"}.
|
55 |
+
2025-08-10 18:24:06,170 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Products"}.
|
56 |
+
2025-08-10 18:24:06,435 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Quotes"}.
|
57 |
+
2025-08-10 18:24:06,724 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Sales_Orders"}.
|
58 |
+
2025-08-10 18:24:06,938 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Purchase_Orders"}.
|
59 |
+
2025-08-10 18:24:07,230 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Invoices"}.
|
60 |
+
2025-08-10 18:24:07,428 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Price_Books"}.
|
61 |
+
2025-08-10 18:24:07,597 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Cases"}.
|
62 |
+
2025-08-10 18:24:07,779 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Solutions"}.
|
63 |
+
2025-08-10 18:24:07,969 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Visits"}.
|
64 |
+
2025-08-10 18:24:08,212 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Notes"}.
|
65 |
+
2025-08-10 18:24:08,435 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Attachments"}.
|
66 |
+
2025-08-10 18:24:08,600 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/settings/fields , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {"module": "Actions_Performed"}.
|
67 |
+
2025-08-10 18:24:08,742 - SDKLogger - INFO - api_http_connector - api_http_connector.py - fire_request - 100 - GET - URL = https://www.zohoapis.in/crm/v2/Products , HEADERS = {"Authorization": " ## can't disclose ## ", "X-ZOHO-SDK": "Windows/10 python/3.11.0:3.1.0"} , PARAMS = {}.
|
zoho_tokens.txt
ADDED
@@ -0,0 +1 @@
|
|
|
|
|
1 |
+
{"access_token": "1000.5c923b9c2bdabbc11d1b4c9c31aa7d7d.9b93bc4f9443e4950f16441831fb4e0a", "scope": "ZohoBooks.fullaccess.all", "api_domain": "https://www.zohoapis.ca", "token_type": "Bearer", "expires_in": 3600, "refresh_token": "1000.f770638242667f6ffb6c7972e79ce11b.ccb0062411880dd2ade5a52ad6951975", "accounts_server": "https://accounts.zohocloud.ca", "expires_at": 1755349005}
|