akiko19191 commited on
Commit
6af31ea
·
verified ·
1 Parent(s): 8ff98f8

Upload folder using huggingface_hub

Browse files
__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, oauth
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
- # Manually configure client credentials after the app config is loaded
30
- client_id = app.config.get('CLIENT_ID')
31
- client_secret = app.config.get('CLIENT_SECRET')
32
-
33
- if client_id and client_secret:
34
- # 1. Configure the flask-oauthlib remote app
35
- xero.client_id = client_id
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 xero_bp
62
 
 
63
  app.register_blueprint(api_bp, url_prefix='/api')
64
- app.register_blueprint(xero_bp)
 
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 trigger_po_creation,trigger_contact_creation
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
- MODES = ['case', 'bag', 'piece', 'tray', 'weight']
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
- unit_string = p.get('unit', '').lower()
321
- available_modes = [mode for mode in MODES if mode in unit_string]
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
- MODES = ['case', 'bag', 'piece', 'tray', 'weight']
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
- unit_string = p.get('unit', '').lower()
494
- available_modes = [mode for mode in MODES if mode in unit_string]
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
- if p_doc:
611
- validated_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
 
612
  else:
613
- error_items.append(item.get("product_name"))
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
- trigger_po_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
- if p_doc:
642
- db_items.append({"productId": str(p_doc['_id']), "quantity": item.get('quantity'), "mode": item.get('unit')})
643
- modes=item.get('unit')
 
644
  if modes == 'weight':
645
  modes='lb'
646
  added_messages.append(f"{item.get('quantity')} {modes} of {p_doc['name']}")
647
- else: added_messages.append(f"could not find '{item.get('product_name')}'")
 
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
- trigger_po_creation(order_details_for_xero)
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
- twilio_resp.message(final_response_text)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- # your_app/api.py
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 .xero_utils import trigger_po_creation, trigger_contact_creation,sync_user_approval_from_xero
 
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
- sync_user_approval_from_xero()
17
  return "✅"
18
 
19
- @api_bp.route('/clear')
20
- def clear_all():
21
- mongo.db.users.delete_many({})
22
- mongo.db.orders.delete_many({})
23
- return "✅"
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
- products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': product_ids}})}
 
 
116
  for item in cart['items']:
117
  details = products.get(item['productId'])
118
  if details:
119
- populated_items.append({
120
- 'product': {'id': str(details['_id']), 'name': details.get('name'), 'unit': details.get('unit'), 'image_url': details.get('image_url'), 'price': details.get('price')},
121
- 'quantity': item['quantity'],
122
- 'mode': item.get('mode', 'pieces')
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 # Skip malformed items
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
- # For all other modes, convert to integer.
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": str(order_id), "user_email": user_email, "items": cart['items'],
212
- "delivery_address": data['deliveryAddress'], "mobile_number": data['mobileNumber'],"deliverydate":data["deliveryDate"]
213
  }
214
- trigger_po_creation(order_details_for_xero)
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
- products = {str(p['_id']): p for p in mongo.db.products.find({'_id': {'$in': list(all_product_ids)}})}
 
240
 
241
  for order in user_orders:
242
- order['items'] = [
243
- {
244
- 'quantity': item['quantity'],
245
- 'mode': item.get('mode', 'pieces'),
246
- 'product': {
247
- 'id': str(p['_id']),
248
- 'name': p.get('name'),
249
- 'unit': p.get('unit'),
250
- 'image_url': p.get('image_url')
251
- }
252
- }
253
- for item in order.get('items', []) if (p := products.get(item['productId']))
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
- if order.get('status') in ['delivered', 'cancelled']:
316
- return jsonify({"msg": "This order can no longer be cancelled."}), 400
317
-
318
- mongo.db.orders.update_one(
319
- {'_id': ObjectId(order_id)},
320
- {'$set': {'status': 'cancelled', 'updated_at': datetime.utcnow()}}
321
- )
322
-
323
- return jsonify({"msg": "Order has been cancelled."}), 200
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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', # Possible statuses: 'new', 'reviewed', 'sourced', 'rejected'
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="moshe.garty@gmail.com"
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 %}Welcome to Xero Python OAuth Starter{% endif %}</title>
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(-4px);
17
- box-shadow: 0 8px 20px rgba(0,0,0,0.08);
18
  }
19
  .code-block {
20
- background: #f8fafc;
21
- border: 1px solid #e2e8f0;
22
  border-radius: 0.75rem;
23
- padding: 1rem;
24
  font-family: 'Fira Code', monospace;
25
  font-size: 0.9rem;
26
- color: #334155;
27
  overflow-x: auto;
28
- box-shadow: 0 4px 12px rgba(0,0,0,0.05);
29
  }
30
  .code-block pre {
31
  margin: 0;
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
32
  }
33
  </style>
34
  </head>
35
- <body class="bg-white text-gray-800">
36
 
37
  <!-- Navbar -->
38
- <nav class="bg-white shadow-sm fixed top-0 left-0 w-full z-10">
39
- <div class="max-w-6xl mx-auto px-6 py-4 flex justify-between items-center">
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('xero.logs') }}" class="hover:text-blue-600 transition">Index</a>
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-5xl mx-auto px-6 pt-28 pb-10">
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-6">
53
 
54
- <a href="{{ url_for('xero.fetch_inventory') }}" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
55
- <h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Xero Inventory</h3>
56
- <p class="text-gray-500 text-sm">Update your website inventory directly from Xero in one click.</p>
57
- </a>
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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 bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
65
- <h3 class="text-lg font-semibold text-gray-900 mb-2">Sync Xero Users</h3>
66
- <p class="text-gray-500 text-sm">Keep your approved Xero users up to date in the database.</p>
67
  </a>
68
 
69
- <a href="/api/sendmail" class="link-card bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
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 bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
75
- <h3 class="text-lg font-semibold text-gray-900 mb-2">Update Pages UI</h3>
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 bg-white p-6 rounded-2xl shadow-sm border border-gray-100 hover:border-blue-200">
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
- <!-- Beautiful Code Block for Block Content -->
86
- <div class="mt-12">
87
- <h3 class="text-xl font-bold text-gray-800 mb-4">Output</h3>
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
- <div>
5
- <h2>{{ title }}</h2>
6
- {% if sub_title %}<h3>{{ sub_title }}</h3>{% endif %}
 
 
 
 
 
 
7
  {% if result_list %}
8
- <ul>
9
- {% for message in result_list %}
10
- <li>{{ message }}</li>
11
- {% endfor %}
12
- </ul>
13
  {% endif %}
14
- <pre class="prettyprint"><code class="language-javascript">{{ code }}</code></pre>
 
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
- # Pass an empty OAuth2Token object during initialization
12
- api_client = ApiClient(
13
- Configuration(oauth2_token=OAuth2Token()),
14
- pool_threads=1
15
- )
16
-
17
- xero = oauth.remote_app(
18
- name="xero",
19
- version="2",
20
- client_id=None, # This will be set in create_app
21
- client_secret=None, # This will be set in create_app
22
- endpoint_url="https://api.xero.com/",
23
- authorization_url="https://login.xero.com/identity/connect/authorize",
24
- access_token_url="https://identity.xero.com/connect/token",
25
- refresh_token_url="https://identity.xero.com/connect/token",
26
- scope="offline_access openid profile email accounting.transactions "
27
- "accounting.journals.read accounting.transactions payroll.payruns accounting.reports.read "
28
- "files accounting.settings.read accounting.settings accounting.attachments payroll.payslip payroll.settings files.read openid assets.read profile payroll.employees projects.read email accounting.contacts.read accounting.attachments.read projects assets accounting.contacts payroll.timesheets accounting.budgets.read",
29
- )
30
- @xero.tokengetter
31
- @api_client.oauth2_token_getter
32
- def obtain_xero_oauth2_token():
33
- token_doc = mongo.db.xero_tokens.find_one({'name': 'xero_app_token'})
34
- return token_doc.get('token') if token_doc else None
35
-
36
- @xero.tokensaver
37
- @api_client.oauth2_token_saver
38
- def store_xero_oauth2_token(token):
39
- if token:
40
- mongo.db.xero_tokens.update_one(
41
- {'name': 'xero_app_token'},
42
- {'$set': {'token': token}},
43
- upsert=True
44
- )
45
- logger.info("Xero token stored/updated in database.")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
46
  else:
47
- if mongo.db.xero_tokens.delete_one({'name': 'xero_app_token'}).deleted_count > 0:
48
- logger.info("Xero token removed from database.")
49
 
50
- def get_xero_tenant_id():
51
- token = obtain_xero_oauth2_token()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
52
  if not token:
53
  return None
54
 
55
- identity_api = IdentityApi(api_client)
56
- for connection in identity_api.get_connections():
57
- if connection.tenant_type == "ORGANISATION":
58
- return connection.tenant_id
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
59
 
60
- def xero_token_required(function):
 
 
 
61
  @wraps(function)
62
  def decorator(*args, **kwargs):
63
- if not obtain_xero_oauth2_token():
64
- return redirect(url_for("xero.login", _external=True))
 
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
- from flask import Blueprint, render_template, redirect, url_for, jsonify, request,flash
 
 
 
4
  from pymongo import UpdateOne
5
- from xero_python.accounting import AccountingApi
6
- from utils import jsonify as jsonify_xero
 
 
 
 
 
 
 
 
 
 
 
 
 
7
  from search_engine import search_and_filter_images, categorise
8
- from .xero_client import xero, api_client, store_xero_oauth2_token, obtain_xero_oauth2_token, xero_token_required, get_xero_tenant_id
9
- from .extensions import mongo # <-- IMPORT MONGO
10
- import traceback
11
- xero_bp = Blueprint('xero', __name__)
12
  logger = logging.getLogger(__name__)
13
 
14
 
15
- @xero_bp.route("/logs")
16
- def logs():
17
- xero_access = dict(obtain_xero_oauth2_token() or {})
18
- return render_template(
19
- "code.html",
20
- title="Home | OAuth Token",
21
- code=json.dumps(xero_access, sort_keys=True, indent=4),
22
- )
23
 
24
- @xero_bp.route("/")
25
  def index():
26
- xero_access = dict(obtain_xero_oauth2_token() or {})
 
 
 
 
 
 
 
 
 
 
27
  return render_template(
28
  "code.html",
29
  title="Home",
30
- code="",
 
 
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
- @xero_bp.route("/callback")
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
39
  def oauth_callback():
 
 
 
 
 
 
40
  try:
41
- response = xero.authorized_response()
 
 
 
42
  except Exception as e:
43
- logger.error("OAuth callback failed: %s", e)
44
- return "OAuth callback failed.", 500
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
- @xero_bp.route("/logout")
 
53
  def logout():
54
- store_xero_oauth2_token(None)
55
- return redirect(url_for("xero.index", _external=True))
 
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
- except Exception as e:
107
- print(traceback.format_exc())
108
- logger.error("Inventory sync failed: %s", e)
109
- sub_title = f"Error during sync: {e}"
110
- code_to_display = jsonify({"error": str(e)})
111
 
112
- return render_template("code.html", title="Inventory Sync", sub_title=sub_title, code=code_to_display)
113
 
114
- @xero_bp.route("/api/edit_inventory", methods=["GET", "POST"])
115
- def edit_inventory():
 
116
  """
117
- Provides a UI to edit the image_url for inventory items.
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
- # Note: For the flash() functionality to work, you must have a SECRET_KEY configured for your Flask app.
122
- # For example: app.config['SECRET_KEY'] = 'a-secret-key'
123
-
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
124
  if request.method == "POST":
125
- product_code = request.form.get("product_code")
126
- new_image_url = request.form.get("image_url")
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
- flash(f"Product with code '{product_code}' not found.", "error")
143
-
144
- return redirect(url_for("xero.edit_inventory"))
145
-
146
- # For a GET request, fetch all products and render the edit page
147
- # Fetch only the necessary fields to populate the form and sort by name
148
- products = list(mongo.db.products.find({}, {"_id": 0, "code": 1, "name": 1, "image_url": 1}).sort("name", 1))
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
- from .xero_client import api_client,get_xero_tenant_id
12
- from .extensions import mongo # <-- IMPORT MONGO
 
13
 
14
  logger = logging.getLogger(__name__)
15
- def sync_user_approval_from_xero():
 
16
  """
17
- Iterates through all users in MongoDB and updates their 'is_approved' status
18
- based on their membership in the 'approve_user' contact group in Xero.
19
- This function is intended to be run periodically (e.g., by a scheduler).
 
20
  """
21
- logger.info("Starting Xero 'approve_user' contact group sync.")
22
  try:
23
- xero_tenant_id = get_xero_tenant_id()
24
- accounting_api = AccountingApi(api_client)
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
25
 
26
- # --- Step 1: Find the 'approve_user' contact group in Xero ---
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
- if not approved_group_info:
31
- logger.warning("Xero contact group 'approve_user' not found. Skipping approval sync.")
32
- return
 
 
 
 
 
 
33
 
34
- # --- Step 2: Get the full list of contacts from that group ---
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 Xero user approval sync: {e}")
 
71
 
72
- # --- NEW FUNCTION: The core logic to create a Xero Contact ---
73
- def create_xero_contact_async(app_context, registration_data):
74
  """
75
- Creates a contact in Xero, then adds the detailed registration info
76
- as a history note. Runs in a background thread.
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
- contact_person = ContactPerson(
90
- first_name=first_name,
91
- last_name=last_name,
92
- email_address=user_email,
93
- include_in_emails=True,
94
- )
95
- business_address = Address(
96
- address_type='STREET',
97
- address_line1=registration_data.get('businessAddress', 'N/A')
98
- )
99
- phone = Phone(
100
- phone_type='DEFAULT',
101
- phone_number=registration_data.get('phoneNumber')
102
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
103
 
104
- # Create the main Contact object WITHOUT the notes field
105
- contact_to_create = Contact(
106
- name=registration_data.get('businessName'),
107
- email_address=user_email,
108
- contact_persons=[contact_person],
109
- addresses=[business_address],
110
- phones=[phone],
111
- website=registration_data.get('companyWebsite'),
112
- is_customer=True
113
- # The 'notes' field is intentionally left out here
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
114
  )
 
 
 
 
115
 
116
- contacts_payload = Contacts(contacts=[contact_to_create])
117
- created_contact_response = accounting_api.create_contacts(
118
- xero_tenant_id, contacts=contacts_payload
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 Xero contact."""
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 Xero contact creation thread for {registration_data.get('email')}")
213
  except Exception as e:
214
- logger.error(f"Failed to start Xero contact creation thread. Error: {e}")
215
-
216
-
217
- # # --- Your existing Purchase Order functions below ---
218
- # def create_xero_purchase_order_async(app_context, xero_tenant_id, order_details):
219
- # with app_context:
220
- # # try:
221
- # accounting_api = AccountingApi(api_client)
222
-
223
- # # --- MODIFICATION: Use the stored xero_contact_id ---
224
- # user = mongo.db.users.find_one({"email": order_details['user_email']})
225
- # contact_id = user.get('xero_contact_id')
226
-
227
- # if not contact_id:
228
- # logger.error(f"Cannot create PO. User {order_details['user_email']} does not have a Xero Contact ID.")
229
- # # As a fallback, you could get the first contact, but it's better to be specific.
230
- # # contact_id = accounting_api.get_contacts(xero_tenant_id).contacts[0].contact_id
231
- # return
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
- # try:
282
- accounting_api = AccountingApi(api_client)
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
- result = accounting_api.create_purchase_orders(
329
- xero_tenant_id, purchase_orders=PurchaseOrders(purchase_orders=[purchase_order])
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
- def trigger_po_creation(order_details):
337
- # try:
338
- from .xero_client import get_xero_tenant_id # Import locally to prevent cycles
339
- xero_tenant_id = get_xero_tenant_id()
340
- app_context = current_app.app_context()
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
341
 
342
- thread = threading.Thread(
343
- target=create_xero_purchase_order_async,
344
- args=(app_context, xero_tenant_id, order_details)
345
- )
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
346
  thread.daemon = True
347
  thread.start()
348
- # except Exception as e:
349
- # logger.error("Failed to start Xero PO creation thread for order %s. Error: %s", order_details.get('order_id'), e)
 
 
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=False, port=7860)
 
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=api_key)
48
  result = service.cse().list(
49
  q=query,
50
- cx=cse_id,
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}