389 lines
14 KiB
Python
389 lines
14 KiB
Python
import io
|
|
import logging
|
|
import os
|
|
import re
|
|
from django.utils import timezone
|
|
|
|
from googleapiclient.discovery import build
|
|
from googleapiclient.http import MediaIoBaseDownload, MediaIoBaseUpload
|
|
from google.oauth2 import service_account
|
|
from google.auth.exceptions import GoogleAuthError
|
|
from googleapiclient.errors import HttpError
|
|
|
|
from .models import Punchlist
|
|
|
|
logger = logging.getLogger(__name__)
|
|
|
|
# Configuration from environment
|
|
CALENDAR_ID = os.getenv('GOOGLE_CALENDAR_ID', 'primary')
|
|
IMPERSONATOR_EMAIL = os.getenv('GOOGLE_IMPERSONATOR_EMAIL')
|
|
TIME_ZONE = os.getenv('TIME_ZONE', 'America/Detroit')
|
|
|
|
# Punchlist Google Drive configuration from environment
|
|
PUNCHLIST_TEMPLATE = os.getenv('PUNCHLIST_TEMPLATE_ID', '')
|
|
PUNCHLIST_FOLDER = os.getenv('PUNCHLIST_FOLDER_ID', '')
|
|
|
|
|
|
def create_event(key, data):
|
|
"""
|
|
Create an event on the project calendar using the Google API
|
|
Args:
|
|
key (dict): Service account credentials dictionary
|
|
data (dict): Event data with the following structure:
|
|
- summary (str): Event title
|
|
- description (str): Event description
|
|
- start (str): ISO format start datetime
|
|
- end (str): ISO format end datetime
|
|
- location (str): Event location
|
|
- attendees (list): List of dicts with 'email' keys
|
|
- reminders (list, optional): List of dicts with 'method' and 'minutes' keys
|
|
Returns:
|
|
dict: Created event data or error information
|
|
"""
|
|
service_account_key = key
|
|
# Set up calendar details from environment
|
|
calendar_id = CALENDAR_ID
|
|
impersonator = IMPERSONATOR_EMAIL
|
|
# Validate required fields
|
|
required_fields = ['summary', 'start', 'end']
|
|
missing_fields = [field for field in required_fields if field not in data]
|
|
if missing_fields:
|
|
error_msg = f"Missing required fields: {', '.join(missing_fields)}"
|
|
logger.error(error_msg)
|
|
return {"error": error_msg}
|
|
# Construct event data
|
|
event = {
|
|
'summary': data.get('summary'),
|
|
'description': data.get('description', ''),
|
|
'start': {
|
|
'dateTime': data.get('start'),
|
|
'timeZone': TIME_ZONE,
|
|
},
|
|
'end': {
|
|
'dateTime': data.get('end'),
|
|
'timeZone': TIME_ZONE,
|
|
},
|
|
'location': data.get('location', ''),
|
|
'attendees': data.get('attendees', []),
|
|
'reminders': {
|
|
'useDefault': False if data.get('reminders') else True,
|
|
'overrides': data.get('reminders', []) if data.get('reminders') else [],
|
|
}
|
|
}
|
|
try:
|
|
# Set up authentication
|
|
scopes = ['https://www.googleapis.com/auth/calendar']
|
|
credentials = service_account.Credentials.from_service_account_info(
|
|
service_account_key, scopes=scopes, subject=impersonator
|
|
)
|
|
# Build and call the service
|
|
service = build('calendar', 'v3', credentials=credentials)
|
|
created_event = service.events().insert(calendarId=calendar_id, body=event).execute()
|
|
logger.info(f"Event created: {created_event.get('htmlLink')}")
|
|
return created_event
|
|
|
|
except GoogleAuthError as e:
|
|
logger.error(f"Authentication error: {e}")
|
|
return {"error": "Authentication failed", "details": str(e)}
|
|
except HttpError as e:
|
|
logger.error(f"Google API error: {e}")
|
|
return {"error": "Google Calendar API error", "details": str(e)}
|
|
except Exception as e:
|
|
logger.error(f"Unexpected error creating event: {e}")
|
|
return {"error": "Unexpected error creating event", "details": str(e)}
|
|
|
|
|
|
# Field mapping for punchlist export to Google Sheets
|
|
# Maps model field names to spreadsheet cell references
|
|
# Customize this mapping to match your Google Sheets template
|
|
PUNCHLIST_FIELD_MAPPING = {
|
|
'account_name': 'B4',
|
|
'date': 'D4',
|
|
# Front area
|
|
'front_ceiling': 'D8',
|
|
'front_vents': 'D9',
|
|
'front_fixtures': 'D10',
|
|
'front_counter': 'D11',
|
|
# Main work area
|
|
'main_equipment': 'D15',
|
|
'main_equipment_disassemble': 'D16',
|
|
'main_equipment_reassemble': 'D17',
|
|
'main_equipment_test': 'D18',
|
|
'main_equipment_exterior': 'D19',
|
|
'main_walls': 'D20',
|
|
'main_fixtures': 'D21',
|
|
'main_ceiling': 'D22',
|
|
'main_vents': 'D23',
|
|
'main_floors': 'D24',
|
|
# Equipment stations
|
|
'equip_station_1': 'D28',
|
|
'equip_station_2': 'D29',
|
|
'equip_station_3': 'D30',
|
|
'equip_station_4': 'D31',
|
|
'equip_station_5': 'D32',
|
|
'equip_station_6': 'D33',
|
|
'equip_station_7': 'D34',
|
|
'equip_sinks': 'D35',
|
|
'equip_dispensers': 'D36',
|
|
'equip_other': 'D37',
|
|
# Back area
|
|
'back_ceiling': 'D41',
|
|
'back_vents': 'D42',
|
|
# End of visit
|
|
'end_trash': 'D46',
|
|
'end_clean': 'D47',
|
|
'end_secure': 'D48',
|
|
# Notes
|
|
'notes': 'A52'
|
|
}
|
|
|
|
|
|
def get_google_service(key):
|
|
"""Create and return Google API service"""
|
|
credentials = service_account.Credentials.from_service_account_info(
|
|
key,
|
|
scopes=['https://www.googleapis.com/auth/drive', 'https://www.googleapis.com/auth/spreadsheets']
|
|
)
|
|
|
|
drive_service = build('drive', 'v3', credentials=credentials)
|
|
sheets_service = build('sheets', 'v4', credentials=credentials)
|
|
|
|
return drive_service, sheets_service
|
|
|
|
|
|
def export_punchlist_to_sheet(key, punchlist_id, template_id=None):
|
|
"""
|
|
Export a punchlist to Google Sheets and generate a PDF
|
|
|
|
Args:
|
|
key: Service account credentials dictionary
|
|
punchlist_id: The ID of the punchlist to export
|
|
template_id: Optional template ID, uses default if not provided
|
|
|
|
Returns:
|
|
dict: URLs to the created sheet and PDF
|
|
"""
|
|
# Get the punchlist data
|
|
try:
|
|
punchlist = Punchlist.objects.get(id=punchlist_id)
|
|
|
|
if punchlist.sheet_url and punchlist.pdf_url:
|
|
# Try to verify files still exist in Google Drive
|
|
try:
|
|
drive_service, _ = get_google_service(key)
|
|
|
|
# Extract file IDs from URLs
|
|
sheet_id = extract_file_id_from_url(punchlist.sheet_url)
|
|
pdf_id = extract_file_id_from_url(punchlist.pdf_url)
|
|
|
|
# Check if files exist
|
|
sheet_exists = check_file_exists(drive_service, sheet_id)
|
|
pdf_exists = check_file_exists(drive_service, pdf_id)
|
|
|
|
if sheet_exists and pdf_exists:
|
|
return {
|
|
'success': True,
|
|
'already_exported': True,
|
|
'sheetUrl': punchlist.sheet_url,
|
|
'pdfUrl': punchlist.pdf_url,
|
|
'message': 'Files already exist'
|
|
}
|
|
except Exception:
|
|
# If there's an error checking, proceed with creating new files
|
|
pass
|
|
except Punchlist.DoesNotExist:
|
|
return {
|
|
'success': False,
|
|
'message': f'Punchlist with ID {punchlist_id} not found'
|
|
}
|
|
|
|
# Get account and project info
|
|
account_name = punchlist.account.name if punchlist.account else 'Unknown Account'
|
|
service_date = punchlist.project.date if punchlist.project else punchlist.date
|
|
|
|
# Get Google services
|
|
drive_service, sheets_service = get_google_service(key)
|
|
|
|
# Use provided template ID or default
|
|
template_id = template_id or PUNCHLIST_TEMPLATE
|
|
|
|
if not template_id:
|
|
return {
|
|
'success': False,
|
|
'message': 'No punchlist template ID configured'
|
|
}
|
|
|
|
try:
|
|
# 1. Copy the template
|
|
copied_file = drive_service.files().copy(
|
|
fileId=template_id,
|
|
body={
|
|
'name': f'Punchlist - {account_name} - {service_date}',
|
|
'parents': [PUNCHLIST_FOLDER] if PUNCHLIST_FOLDER else []
|
|
}
|
|
).execute()
|
|
|
|
new_sheet_id = copied_file['id']
|
|
|
|
# 2. Prepare data for the sheet
|
|
value_range_body = {
|
|
'valueInputOption': 'USER_ENTERED',
|
|
'data': []
|
|
}
|
|
|
|
# Map punchlist data to sheet cells
|
|
data_mapping = {
|
|
'account_name': account_name,
|
|
'date': service_date.strftime('%m/%d/%Y') if service_date else '',
|
|
'front_ceiling': 'X' if punchlist.front_ceiling else '',
|
|
'front_vents': 'X' if punchlist.front_vents else '',
|
|
'front_fixtures': 'X' if punchlist.front_fixtures else '',
|
|
'front_counter': 'X' if punchlist.front_counter else '',
|
|
'main_equipment': punchlist.main_equipment.upper() if punchlist.main_equipment else '',
|
|
'main_equipment_disassemble': 'X' if punchlist.main_equipment_disassemble else '',
|
|
'main_equipment_reassemble': 'X' if punchlist.main_equipment_reassemble else '',
|
|
'main_equipment_test': 'X' if punchlist.main_equipment_test else '',
|
|
'main_equipment_exterior': 'X' if punchlist.main_equipment_exterior else '',
|
|
'main_walls': 'X' if punchlist.main_walls else '',
|
|
'main_fixtures': 'X' if punchlist.main_fixtures else '',
|
|
'main_ceiling': 'X' if punchlist.main_ceiling else '',
|
|
'main_vents': 'X' if punchlist.main_vents else '',
|
|
'main_floors': 'X' if punchlist.main_floors else '',
|
|
'equip_station_1': 'X' if punchlist.equip_station_1 else '',
|
|
'equip_station_2': 'X' if punchlist.equip_station_2 else '',
|
|
'equip_station_3': 'X' if punchlist.equip_station_3 else '',
|
|
'equip_station_4': 'X' if punchlist.equip_station_4 else '',
|
|
'equip_station_5': 'X' if punchlist.equip_station_5 else '',
|
|
'equip_station_6': 'X' if punchlist.equip_station_6 else '',
|
|
'equip_station_7': 'X' if punchlist.equip_station_7 else '',
|
|
'equip_sinks': 'X' if punchlist.equip_sinks else '',
|
|
'equip_dispensers': 'X' if punchlist.equip_dispensers else '',
|
|
'equip_other': 'X' if punchlist.equip_other else '',
|
|
'back_ceiling': 'X' if punchlist.back_ceiling else '',
|
|
'back_vents': 'X' if punchlist.back_vents else '',
|
|
'end_trash': 'X' if punchlist.end_trash else '',
|
|
'end_clean': 'X' if punchlist.end_clean else '',
|
|
'end_secure': 'X' if punchlist.end_secure else '',
|
|
'notes': punchlist.notes or ''
|
|
}
|
|
|
|
# Create value range objects for each cell
|
|
for field, cell in PUNCHLIST_FIELD_MAPPING.items():
|
|
if field in data_mapping:
|
|
value = data_mapping[field]
|
|
|
|
# Extract sheet name and cell reference
|
|
if '!' in cell:
|
|
sheet_name, cell_ref = cell.split('!')
|
|
else:
|
|
sheet_name = 'Sheet1' # Default sheet name
|
|
cell_ref = cell
|
|
|
|
value_range_body['data'].append({
|
|
'range': f"'{sheet_name}'!{cell_ref}",
|
|
'values': [[value]]
|
|
})
|
|
|
|
# 3. Update the sheet with data
|
|
sheets_service.spreadsheets().values().batchUpdate(
|
|
spreadsheetId=new_sheet_id,
|
|
body=value_range_body
|
|
).execute()
|
|
|
|
# 4. Export as PDF
|
|
request = drive_service.files().export_media(
|
|
fileId=new_sheet_id,
|
|
mimeType='application/pdf'
|
|
)
|
|
|
|
# Create a BytesIO object to store the PDF
|
|
pdf_file = io.BytesIO()
|
|
downloader = MediaIoBaseDownload(pdf_file, request)
|
|
|
|
# Download the PDF
|
|
done = False
|
|
while not done:
|
|
status, done = downloader.next_chunk()
|
|
|
|
# 5. Upload the PDF to Drive
|
|
pdf_metadata = {
|
|
'name': f'Punchlist - {account_name} - {service_date}.pdf',
|
|
'mimeType': 'application/pdf',
|
|
}
|
|
if PUNCHLIST_FOLDER:
|
|
pdf_metadata['parents'] = [PUNCHLIST_FOLDER]
|
|
|
|
pdf_file.seek(0)
|
|
pdf_media = MediaIoBaseUpload(pdf_file, mimetype='application/pdf')
|
|
|
|
uploaded_pdf = drive_service.files().create(
|
|
body=pdf_metadata,
|
|
media_body=pdf_media,
|
|
fields='id,webViewLink'
|
|
).execute()
|
|
|
|
# 6. Make both files accessible via link
|
|
for file_id in [new_sheet_id, uploaded_pdf['id']]:
|
|
drive_service.permissions().create(
|
|
fileId=file_id,
|
|
body={
|
|
'type': 'anyone',
|
|
'role': 'reader'
|
|
}
|
|
).execute()
|
|
|
|
# Get the web view links
|
|
sheet_file = drive_service.files().get(
|
|
fileId=new_sheet_id,
|
|
fields='webViewLink'
|
|
).execute()
|
|
|
|
punchlist.sheet_url = sheet_file['webViewLink']
|
|
punchlist.pdf_url = uploaded_pdf['webViewLink']
|
|
punchlist.exported_at = timezone.now()
|
|
punchlist.save()
|
|
|
|
return {
|
|
'success': True,
|
|
'sheetUrl': sheet_file['webViewLink'],
|
|
'pdfUrl': uploaded_pdf['webViewLink']
|
|
}
|
|
|
|
except Exception as e:
|
|
return {
|
|
'success': False,
|
|
'message': f'Error exporting punchlist: {str(e)}'
|
|
}
|
|
|
|
|
|
def extract_file_id_from_url(url):
|
|
"""Extract Google Drive file ID from a sharing URL"""
|
|
if not url:
|
|
return None
|
|
|
|
# Google Drive URLs typically have the format:
|
|
# https://drive.google.com/file/d/{FILE_ID}/view or
|
|
# https://docs.google.com/spreadsheets/d/{FILE_ID}/edit
|
|
match = re.search(r'\/d\/([a-zA-Z0-9_-]+)', url)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
# For spreadsheet URLs
|
|
match = re.search(r'\/spreadsheets\/d\/([a-zA-Z0-9_-]+)', url)
|
|
if match:
|
|
return match.group(1)
|
|
|
|
return None
|
|
|
|
|
|
def check_file_exists(drive_service, file_id):
|
|
"""Check if a file exists in Google Drive"""
|
|
if not file_id:
|
|
return False
|
|
|
|
try:
|
|
drive_service.files().get(fileId=file_id).execute()
|
|
return True
|
|
except Exception:
|
|
return False
|