Spaces:
Running
Running
File size: 13,720 Bytes
72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea 72eef4f 6af31ea |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 |
import logging
import threading
from datetime import datetime
from flask import current_app
from bson.objectid import ObjectId
# Import the new REST API client helper
from .xero_client import make_zoho_api_request
from .extensions import mongo
logger = logging.getLogger(__name__)
def sync_user_approval_from_zoho():
"""
Updates users' 'is_approved' status based on the 'cf_approval_status'
custom dropdown in Zoho Books. A contact is considered approved when:
custom_field.label == 'cf_approval_status' and custom_field.value == 'approved'
This function pages through contacts and inspects custom_fields for the match.
"""
logger.info("Starting Zoho Books 'cf_approval_status' custom field sync.")
try:
page = 1
per_page = 200 # Zoho typically supports up to 200 per page; adjust if necessary
approved_contact_ids = set()
while True:
params = {'page': page, 'per_page': per_page}
response = make_zoho_api_request('GET', '/contacts', params=params)
if not response:
logger.warning(f"No response from Zoho when fetching contacts page {page}. Stopping pagination.")
break
logger.info(response)
contacts = response[0]['contacts'] or []
if not contacts:
logger.info(f"No contacts returned on page {page}. Pagination complete.")
break
for contact in contacts:
# Contact id field might be 'contact_id' or similar - defensive access
contact_id = contact.get('email') or contact.get('contactId') or contact.get('id')
# Ensure we have a custom_fields list to check
custom_fields = contact.get('custom_fields') or []
for cf in custom_fields:
# cf typically has keys like 'label' and 'value' when you use label-based assignment
if cf.get('label') == 'cf_approval_status' and str(cf.get('value')).lower() == 'approved':
if contact_id:
approved_contact_ids.add(str(contact_id))
break # no need to check other custom fields for this contact
# If response contains page context we can use it; otherwise continue until an empty page
page_context = response[0]['page_context']
has_more = page_context.get('has_more_page')
if has_more is None:
# fallback: stop when we received fewer than per_page results
if len(contacts) < per_page:
break
page += 1
else:
if not has_more:
break
page += 1
logger.info(f"Found {len(approved_contact_ids)} approved contacts in Zoho Books (cf_approval_status == 'approved').")
# Set all users to not approved first (only users with a zoho_contact_id are considered)
# mongo.db.users.update_many({'zoho_contact_id': {'$exists': True}}, {'$set': {'is_approved': False}})
# Then, set the ones found in the sync to approved
if approved_contact_ids:
mongo.db.users.update_many(
{'email': {'$in': list(approved_contact_ids)}},
{'$set': {'is_approved': True}}
)
logger.info(f"Zoho approval sync complete. {len(approved_contact_ids)} users are now marked as approved.")
except Exception as e:
logger.error(f"An error occurred during Zoho user approval sync: {e}", exc_info=True)
def create_zoho_contact_async(app_context, registration_data):
"""
Creates a contact in Zoho Books, sets a 'pending' custom field, and adds a note (comment).
Assumes a custom field exists whose label is 'cf_approval_status' (adjust label/index if needed).
"""
with app_context:
user_email = registration_data.get('email')
try:
contact_person_name = registration_data.get('contactPerson', '')
first_name, last_name = (contact_person_name.split(' ', 1) + [''])[:2]
contact_payload = {
'contact_name': registration_data.get('businessName'),
'contact_type': 'customer',
'company_name': registration_data.get('companyName') ,
'contact_persons': [{
'first_name': first_name,
'last_name': last_name,
'email': user_email,
'phone': registration_data.get('phoneNumber'),
# keep this: it's a valid flag for the contact person entry
'is_primary_contact': True
}],
'billing_address': {
'address': registration_data.get('businessAddress', 'N/A')
},
'shipping_address': {
'address': registration_data.get('businessAddress', 'N/A')
},
'website': registration_data.get('companyWebsite'),
# <-- Use label/index + value (not api_name)
'custom_fields': [
{
'label': 'cf_approval_status', # must match the field label in Zoho Books
# 'index': 1, # optional: set if you know the slot (1..10)
'value': 'pending_for_approval' # value must match one of the dropdown option values
}
]
}
response = make_zoho_api_request('POST', '/contacts', json_data=contact_payload)
# defensive logging: log whole response so you can inspect Zoho's error message if any
logger.debug(f"Zoho create contact response: {response}")
# # basic success check — adjust depending on your make_zoho_api_request return structure
# if not response or 'contact' not in response:
# logger.error(f"Zoho Books contact creation failed for {user_email}: {response}")
# return
new_contact_id = response[0]['contact']['contact_id']
logger.info(f"Successfully created Zoho Books contact ({new_contact_id}) for user {user_email}.")
mongo.db.users.update_one({'email': user_email}, {'$set': {'zoho_contact_id': new_contact_id}})
history_details = (
f"--- Client Application Details ---\n"
f"Business Name: {registration_data.get('businessName')}\n"
f"Contact Person: {registration_data.get('contactPerson')}\n"
f"Email: {registration_data.get('email')}\n"
f"Phone: {registration_data.get('phoneNumber')}\n"
f"Company Website: {registration_data.get('companyWebsite')}\n"
f"Business Address: {registration_data.get('businessAddress')}\n"
f"Business Type: {registration_data.get('businessType')}\n"
f"Years Operating: {registration_data.get('yearsOperating')}\n"
f"Number of Locations: {registration_data.get('numLocations')}\n"
f"Estimated Weekly Volume: {registration_data.get('estimatedVolume')}\n\n"
f"--- Logistics Information ---\n"
f"Preferred Delivery Times: {registration_data.get('preferredDeliveryTimes')}\n"
f"Has Loading Dock: {registration_data.get('hasLoadingDock')}\n"
f"Special Delivery Instructions: {registration_data.get('specialDeliveryInstructions')}\n"
f"Minimum Quantity Requirements: {registration_data.get('minQuantityRequirements')}\n\n"
f"--- Service & Billing ---\n"
f"Reason for Switching: {registration_data.get('reasonForSwitch')}\n"
f"Preferred Payment Method: {registration_data.get('paymentMethod')}\n\n"
f"--- Additional Notes ---\n"
f"{registration_data.get('additionalNotes')}"
)
comment_payload = {
'description': history_details,
'show_comment_to_clients': False
}
make_zoho_api_request('POST', f'/contacts/{new_contact_id}/comments', json_data=comment_payload)
logger.info(f"Successfully added registration details as a comment to Zoho contact {new_contact_id}.")
except Exception as e:
logger.error(f"Failed during Zoho Books contact/comment creation for user {user_email}. Error: {e}", exc_info=True)
def trigger_contact_creation(registration_data):
"""Starts a background thread to create a Zoho contact."""
try:
app_context = current_app.app_context()
thread = threading.Thread(target=create_zoho_contact_async, args=(app_context, registration_data))
thread.daemon = True
thread.start()
logger.info(f"Started Zoho contact creation thread for {registration_data.get('email')}")
except Exception as e:
logger.error(f"Failed to start Zoho contact creation thread. Error: {e}")
def get_zoho_contact_by_email(email):
"""Fetches a Zoho contact ID by email."""
try:
# Assumes make_zoho_api_request is a helper function that handles authentication
response = make_zoho_api_request('GET', '/contacts', params={'email': email})
contacts = response[0]['contacts']
if contacts:
return contacts[0]['contact_id']
else:
logger.info(f"No Zoho contact found for email: {email}")
return None
except Exception as e:
logger.error(f"Error fetching Zoho contact for {email}: {e}", exc_info=True)
return None
def create_zoho_invoice_async(app_context, order_details):
"""Creates an Invoice in Zoho Books from order details (CAD currency, order no -> reference_number)."""
with app_context:
user_email = order_details['user_email']
contact_id = get_zoho_contact_by_email(user_email)
if not contact_id:
logger.error(f"Cannot create Invoice. Zoho contact not found for email {user_email}.")
return
line_items = []
for item in order_details['items']:
product = mongo.db.products.find_one({'_id': ObjectId(item['productId'])})
for mode in product.get('modes', []):
if str(mode) == item['mode']:
product['zoho_id'] = product.get('modes')[mode].get('zoho_id')
product['price'] = product.get('modes')[mode].get('price')
break
logger.info(f"Processing item {product.get('zoho_id')} with mode {item['mode']} for invoice creation.")
unit = "lb" if item.get("mode") == "weight" else item.get("mode")
line_items.append({
'item_id': product['zoho_id'],
'quantity': int(item['quantity']),
'rate': float(product.get('price', 0)),
'description': f"{item['quantity']} {unit} of {product.get('name', 'N/A')}"
})
if not line_items:
logger.error("Zoho Invoice failed: No valid line items for order %s", order_details['order_id'])
return
# build invoice payload with CAD and order number
logger.info( order_details.get('additional_info', 'N/A'))
invoice_payload = {
'customer_id': contact_id,
'date': datetime.strptime(order_details.get("order_date", order_details["deliverydate"]), "%Y-%m-%d").strftime("%Y-%m-%d"),
'due_date': datetime.strptime(order_details["deliverydate"], "%Y-%m-%d").strftime("%Y-%m-%d"),
'line_items': line_items,
'notes': order_details.get('additional_info', 'N/A'),
'billing_address': {
'address': order_details.get('delivery_address')
},
# <--- currency & order mapping
'currency_code': 'CAD', # invoice currency = Canadian Dollars
'exchange_rate': 1.0, # set to appropriate rate (1.0 if you treat amounts as CAD already)
'reference_number': order_details['order_id'], # shows your order number on the invoice
# Optionally add custom fields if you have created one and know its customfield_id
# 'custom_fields': [
# {'customfield_id': 123456789012345, 'value': order_details['order_id']}
# ]
}
# send to Zoho
response = make_zoho_api_request('POST', '/invoices', json_data=invoice_payload)
print(response)
invoice_id = response['invoice']['invoice_id']
logger.info(f"Successfully created Zoho Books Invoice {invoice_id} for order ID: {order_details['order_id']}")
def trigger_invoice_creation(order_details):
"""Starts a background thread to create a Zoho Invoice."""
try:
app_context = current_app.app_context()
thread = threading.Thread(target=create_zoho_invoice_async, args=(app_context, order_details))
thread.daemon = True
thread.start()
logger.info(f"Started Zoho Invoice creation thread for order {order_details.get('order_id')}")
except Exception as e:
logger.error(f"Failed to start Zoho Invoice creation thread for order %s. Error: %s", order_details.get('order_id'), e) |