update
This commit is contained in:
@@ -3,5 +3,4 @@
|
||||
from . import portal_main
|
||||
from . import portal_assessment
|
||||
from . import pdf_editor
|
||||
from . import portal_schedule
|
||||
from . import portal_page11_sign
|
||||
@@ -348,13 +348,13 @@ class AssessmentPortal(CustomerPortal):
|
||||
vals = {
|
||||
'signature_page_11': signature_data,
|
||||
'signature_page_11_name': signer_name,
|
||||
'signature_page_11_date': datetime.now(),
|
||||
'signature_page_11_date': fields.Datetime.now(),
|
||||
}
|
||||
elif signature_type == 'page_12':
|
||||
vals = {
|
||||
'signature_page_12': signature_data,
|
||||
'signature_page_12_name': signer_name,
|
||||
'signature_page_12_date': datetime.now(),
|
||||
'signature_page_12_date': fields.Datetime.now(),
|
||||
}
|
||||
else:
|
||||
return {'success': False, 'error': 'Invalid signature type'}
|
||||
@@ -1018,227 +1018,6 @@ class AssessmentPortal(CustomerPortal):
|
||||
('Nunavut', 'Nunavut'),
|
||||
]
|
||||
|
||||
# =========================================================================
|
||||
# LOANER PORTAL ROUTES
|
||||
# =========================================================================
|
||||
|
||||
@http.route('/my/loaner/categories', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_categories(self, **kw):
|
||||
"""Return loaner product categories."""
|
||||
parent = request.env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False)
|
||||
if not parent:
|
||||
return []
|
||||
categories = request.env['product.category'].sudo().search([
|
||||
('parent_id', '=', parent.id),
|
||||
], order='name')
|
||||
return [{'id': c.id, 'name': c.name} for c in categories]
|
||||
|
||||
@http.route('/my/loaner/products', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_products(self, **kw):
|
||||
"""Return available loaner products and their serial numbers."""
|
||||
domain = [('x_fc_can_be_loaned', '=', True)]
|
||||
category_id = kw.get('category_id')
|
||||
if category_id:
|
||||
domain.append(('categ_id', '=', int(category_id)))
|
||||
|
||||
products = request.env['product.product'].sudo().search(domain)
|
||||
loaner_location = request.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False)
|
||||
|
||||
result = []
|
||||
for p in products:
|
||||
lots = []
|
||||
if loaner_location:
|
||||
quants = request.env['stock.quant'].sudo().search([
|
||||
('product_id', '=', p.id),
|
||||
('location_id', '=', loaner_location.id),
|
||||
('quantity', '>', 0),
|
||||
])
|
||||
for q in quants:
|
||||
if q.lot_id:
|
||||
lots.append({'id': q.lot_id.id, 'name': q.lot_id.name})
|
||||
result.append({
|
||||
'id': p.id,
|
||||
'name': p.name,
|
||||
'category_id': p.categ_id.id,
|
||||
'period_days': p.product_tmpl_id.x_fc_loaner_period_days or 7,
|
||||
'lots': lots,
|
||||
})
|
||||
return result
|
||||
|
||||
@http.route('/my/loaner/locations', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_locations(self, **kw):
|
||||
"""Return internal stock locations for return."""
|
||||
locations = request.env['stock.location'].sudo().search([
|
||||
('usage', '=', 'internal'),
|
||||
('company_id', '=', request.env.company.id),
|
||||
])
|
||||
return [{'id': loc.id, 'name': loc.complete_name} for loc in locations]
|
||||
|
||||
@http.route('/my/loaner/checkout', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_checkout(self, **kw):
|
||||
"""Checkout a loaner from the portal."""
|
||||
partner = request.env.user.partner_id
|
||||
if not partner.is_sales_rep_portal and not partner.is_authorizer:
|
||||
return {'error': 'Unauthorized'}
|
||||
|
||||
product_id = int(kw.get('product_id', 0))
|
||||
lot_id = int(kw.get('lot_id', 0)) if kw.get('lot_id') else False
|
||||
sale_order_id = int(kw.get('sale_order_id', 0)) if kw.get('sale_order_id') else False
|
||||
client_id = int(kw.get('client_id', 0)) if kw.get('client_id') else False
|
||||
loaner_period = int(kw.get('loaner_period_days', 7))
|
||||
condition = kw.get('checkout_condition', 'good')
|
||||
notes = kw.get('checkout_notes', '')
|
||||
|
||||
if not product_id:
|
||||
return {'error': 'Product is required'}
|
||||
|
||||
vals = {
|
||||
'product_id': product_id,
|
||||
'loaner_period_days': loaner_period,
|
||||
'checkout_condition': condition,
|
||||
'checkout_notes': notes,
|
||||
'sales_rep_id': request.env.user.id,
|
||||
}
|
||||
if lot_id:
|
||||
vals['lot_id'] = lot_id
|
||||
if sale_order_id:
|
||||
so = request.env['sale.order'].sudo().browse(sale_order_id)
|
||||
if so.exists():
|
||||
vals['sale_order_id'] = so.id
|
||||
vals['partner_id'] = so.partner_id.id
|
||||
vals['authorizer_id'] = so.x_fc_authorizer_id.id if so.x_fc_authorizer_id else False
|
||||
vals['delivery_address'] = so.partner_shipping_id.contact_address if so.partner_shipping_id else ''
|
||||
if client_id and not vals.get('partner_id'):
|
||||
vals['partner_id'] = client_id
|
||||
|
||||
if not vals.get('partner_id'):
|
||||
return {'error': 'Client is required'}
|
||||
|
||||
try:
|
||||
checkout = request.env['fusion.loaner.checkout'].sudo().create(vals)
|
||||
checkout.action_checkout()
|
||||
return {
|
||||
'success': True,
|
||||
'checkout_id': checkout.id,
|
||||
'name': checkout.name,
|
||||
'message': f'Loaner {checkout.name} checked out successfully',
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error(f"Loaner checkout error: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
@http.route('/my/loaner/create-product', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_create_product(self, **kw):
|
||||
"""Quick-create a loaner product with serial number from the portal."""
|
||||
partner = request.env.user.partner_id
|
||||
if not partner.is_sales_rep_portal and not partner.is_authorizer:
|
||||
return {'error': 'Unauthorized'}
|
||||
|
||||
product_name = kw.get('product_name', '').strip()
|
||||
serial_number = kw.get('serial_number', '').strip()
|
||||
|
||||
if not product_name:
|
||||
return {'error': 'Product name is required'}
|
||||
if not serial_number:
|
||||
return {'error': 'Serial number is required'}
|
||||
|
||||
try:
|
||||
# Use provided category or default to Loaner Equipment
|
||||
category_id = kw.get('category_id')
|
||||
if category_id:
|
||||
category = request.env['product.category'].sudo().browse(int(category_id))
|
||||
if not category.exists():
|
||||
category = None
|
||||
else:
|
||||
category = None
|
||||
|
||||
if not category:
|
||||
category = request.env.ref('fusion_claims.product_category_loaner', raise_if_not_found=False)
|
||||
if not category:
|
||||
category = request.env['product.category'].sudo().search([
|
||||
('name', '=', 'Loaner Equipment'),
|
||||
], limit=1)
|
||||
if not category:
|
||||
category = request.env['product.category'].sudo().create({
|
||||
'name': 'Loaner Equipment',
|
||||
})
|
||||
|
||||
# Create product template
|
||||
product_tmpl = request.env['product.template'].sudo().create({
|
||||
'name': product_name,
|
||||
'type': 'consu',
|
||||
'tracking': 'serial',
|
||||
'categ_id': category.id,
|
||||
'x_fc_can_be_loaned': True,
|
||||
'x_fc_loaner_period_days': 7,
|
||||
'sale_ok': False,
|
||||
'purchase_ok': False,
|
||||
})
|
||||
product = product_tmpl.product_variant_id
|
||||
|
||||
# Create serial number (lot)
|
||||
lot = request.env['stock.lot'].sudo().create({
|
||||
'name': serial_number,
|
||||
'product_id': product.id,
|
||||
'company_id': request.env.company.id,
|
||||
})
|
||||
|
||||
# Add stock in loaner location
|
||||
loaner_location = request.env.ref('fusion_claims.stock_location_loaner', raise_if_not_found=False)
|
||||
if loaner_location:
|
||||
request.env['stock.quant'].sudo().create({
|
||||
'product_id': product.id,
|
||||
'location_id': loaner_location.id,
|
||||
'lot_id': lot.id,
|
||||
'quantity': 1,
|
||||
})
|
||||
|
||||
return {
|
||||
'success': True,
|
||||
'product_id': product.id,
|
||||
'product_name': product.name,
|
||||
'lot_id': lot.id,
|
||||
'lot_name': lot.name,
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error(f"Loaner product creation error: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
@http.route('/my/loaner/return', type='jsonrpc', auth='user', website=True)
|
||||
def portal_loaner_return(self, **kw):
|
||||
"""Return/pickup a loaner from the portal."""
|
||||
partner = request.env.user.partner_id
|
||||
if not partner.is_sales_rep_portal and not partner.is_authorizer:
|
||||
return {'error': 'Unauthorized'}
|
||||
|
||||
checkout_id = int(kw.get('checkout_id', 0))
|
||||
return_condition = kw.get('return_condition', 'good')
|
||||
return_notes = kw.get('return_notes', '')
|
||||
return_location_id = int(kw.get('return_location_id', 0)) if kw.get('return_location_id') else None
|
||||
|
||||
if not checkout_id:
|
||||
return {'error': 'Checkout ID is required'}
|
||||
|
||||
try:
|
||||
checkout = request.env['fusion.loaner.checkout'].sudo().browse(checkout_id)
|
||||
if not checkout.exists():
|
||||
return {'error': 'Checkout not found'}
|
||||
if checkout.state not in ('checked_out', 'overdue', 'rental_pending'):
|
||||
return {'error': 'This loaner is not currently checked out'}
|
||||
|
||||
checkout.action_process_return(
|
||||
return_condition=return_condition,
|
||||
return_notes=return_notes,
|
||||
return_location_id=return_location_id,
|
||||
)
|
||||
return {
|
||||
'success': True,
|
||||
'message': f'Loaner {checkout.name} returned successfully',
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.error(f"Loaner return error: {e}")
|
||||
return {'error': str(e)}
|
||||
|
||||
# ==========================================================================
|
||||
# PUBLIC ASSESSMENT BOOKING
|
||||
# ==========================================================================
|
||||
|
||||
@@ -14,6 +14,45 @@ _logger = logging.getLogger(__name__)
|
||||
class AuthorizerPortal(CustomerPortal):
|
||||
"""Portal controller for Authorizers (OTs/Therapists)"""
|
||||
|
||||
def _get_user_tz(self):
|
||||
"""Return a pytz timezone from the best available source.
|
||||
Priority: user tz > browser cookie > company calendar > UTC."""
|
||||
candidates = [
|
||||
request.env.user.tz,
|
||||
]
|
||||
try:
|
||||
candidates.append(request.httprequest.cookies.get('tz'))
|
||||
except Exception:
|
||||
pass
|
||||
try:
|
||||
cal = request.env.company.resource_calendar_id
|
||||
if cal:
|
||||
candidates.append(cal.tz)
|
||||
except Exception:
|
||||
pass
|
||||
for tz_name in candidates:
|
||||
if tz_name:
|
||||
try:
|
||||
return pytz.timezone(tz_name)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
continue
|
||||
return pytz.UTC
|
||||
|
||||
@http.route('/my/timezone/detect', type='jsonrpc', auth='user', website=True)
|
||||
def timezone_auto_detect(self, timezone=None, **kw):
|
||||
"""Auto-save browser-detected timezone to the user profile if not already set."""
|
||||
if not timezone:
|
||||
return {'status': 'ignored'}
|
||||
user = request.env.user
|
||||
if user.tz:
|
||||
return {'status': 'already_set', 'tz': user.tz}
|
||||
try:
|
||||
pytz.timezone(timezone)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
return {'status': 'invalid'}
|
||||
user.sudo().write({'tz': timezone})
|
||||
return {'status': 'saved', 'tz': timezone}
|
||||
|
||||
@http.route(['/my', '/my/home'], type='http', auth='user', website=True)
|
||||
def home(self, **kw):
|
||||
"""Override home to add ADP posting info for Fusion users"""
|
||||
@@ -111,15 +150,8 @@ class AuthorizerPortal(CustomerPortal):
|
||||
except (ValueError, TypeError):
|
||||
base_date = date(2026, 1, 23)
|
||||
|
||||
# Get user's timezone for accurate date display
|
||||
user_tz = request.env.user.tz or 'UTC'
|
||||
try:
|
||||
tz = pytz.timezone(user_tz)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
tz = pytz.UTC
|
||||
|
||||
# Get today's date in user's timezone
|
||||
from datetime import datetime
|
||||
tz = self._get_user_tz()
|
||||
now_utc = datetime.now(pytz.UTC)
|
||||
now_local = now_utc.astimezone(tz)
|
||||
today = now_local.date()
|
||||
@@ -1116,14 +1148,27 @@ class AuthorizerPortal(CustomerPortal):
|
||||
('check_out', '=', False),
|
||||
], limit=1)
|
||||
if att:
|
||||
check_in_time = att.check_in.isoformat() if att.check_in else ''
|
||||
check_in_time = (att.check_in.isoformat() + 'Z') if att.check_in else ''
|
||||
location_name = att.x_fclk_location_id.name if att.x_fclk_location_id else ''
|
||||
|
||||
from datetime import datetime
|
||||
tz = self._get_user_tz()
|
||||
now_local = pytz.utc.localize(fields.Datetime.now()).astimezone(tz)
|
||||
today_local = now_local.date()
|
||||
today_start = tz.localize(datetime.combine(today_local, datetime.min.time())).astimezone(pytz.utc).replace(tzinfo=None)
|
||||
today_atts = request.env['hr.attendance'].sudo().search([
|
||||
('employee_id', '=', employee.id),
|
||||
('check_in', '>=', today_start),
|
||||
('check_out', '!=', False),
|
||||
])
|
||||
today_hours = sum(a.x_fclk_net_hours or 0 for a in today_atts)
|
||||
|
||||
return {
|
||||
'clock_enabled': True,
|
||||
'clock_checked_in': is_checked_in,
|
||||
'clock_check_in_time': check_in_time,
|
||||
'clock_location_name': location_name,
|
||||
'clock_today_hours': round(today_hours, 1),
|
||||
}
|
||||
except Exception as e:
|
||||
_logger.warning("Clock status check failed: %s", e)
|
||||
@@ -1310,10 +1355,18 @@ class AuthorizerPortal(CustomerPortal):
|
||||
('id', '!=', task.id),
|
||||
], order='time_start', limit=1)
|
||||
|
||||
# Get order lines if linked to a sale order
|
||||
# Get order lines from linked sale order or purchase order
|
||||
order_lines = []
|
||||
linked_order = False
|
||||
linked_order_type = ''
|
||||
if task.sale_order_id:
|
||||
linked_order = task.sale_order_id
|
||||
linked_order_type = 'sale'
|
||||
order_lines = task.sale_order_id.order_line.filtered(lambda l: not l.display_type)
|
||||
elif task.purchase_order_id:
|
||||
linked_order = task.purchase_order_id
|
||||
linked_order_type = 'purchase'
|
||||
order_lines = task.purchase_order_id.order_line
|
||||
|
||||
# Get VAPID public key for push notifications
|
||||
vapid_public = request.env['ir.config_parameter'].sudo().get_param(
|
||||
@@ -1323,6 +1376,8 @@ class AuthorizerPortal(CustomerPortal):
|
||||
values = {
|
||||
'task': task,
|
||||
'order_lines': order_lines,
|
||||
'linked_order': linked_order,
|
||||
'linked_order_type': linked_order_type,
|
||||
'vapid_public_key': vapid_public,
|
||||
'page_name': 'technician_task_detail',
|
||||
'earlier_incomplete': earlier_incomplete,
|
||||
@@ -1384,7 +1439,9 @@ class AuthorizerPortal(CustomerPortal):
|
||||
safe_notes = str(escape(notes or ''))
|
||||
formatted_notes = re.sub(r'\n', '<br/>', safe_notes)
|
||||
|
||||
timestamp = fields.Datetime.now().strftime("%b %d, %Y %I:%M %p")
|
||||
tz = self._get_user_tz()
|
||||
local_now = pytz.utc.localize(fields.Datetime.now()).astimezone(tz)
|
||||
timestamp = local_now.strftime("%b %d, %Y %I:%M %p")
|
||||
safe_user = str(escape(user.name))
|
||||
safe_task = str(escape(task.name))
|
||||
|
||||
@@ -2077,7 +2134,7 @@ class AuthorizerPortal(CustomerPortal):
|
||||
'x_fc_pod_client_name': client_name.strip(),
|
||||
'x_fc_pod_signature_date': sig_date,
|
||||
'x_fc_pod_signed_by_user_id': user.id,
|
||||
'x_fc_pod_signed_datetime': datetime.now(),
|
||||
'x_fc_pod_signed_datetime': fields.Datetime.now(),
|
||||
})
|
||||
|
||||
# Generate the signed POD PDF
|
||||
|
||||
@@ -1,327 +0,0 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
from odoo import http, _, fields
|
||||
from odoo.http import request
|
||||
from odoo.addons.portal.controllers.portal import CustomerPortal
|
||||
from odoo.exceptions import AccessError, ValidationError
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import logging
|
||||
import pytz
|
||||
|
||||
_logger = logging.getLogger(__name__)
|
||||
|
||||
|
||||
class PortalSchedule(CustomerPortal):
|
||||
"""Portal controller for appointment scheduling and calendar management."""
|
||||
|
||||
def _get_schedule_values(self):
|
||||
"""Common values for schedule pages."""
|
||||
ICP = request.env['ir.config_parameter'].sudo()
|
||||
g_start = ICP.get_param('fusion_claims.portal_gradient_start', '#5ba848')
|
||||
g_mid = ICP.get_param('fusion_claims.portal_gradient_mid', '#3a8fb7')
|
||||
g_end = ICP.get_param('fusion_claims.portal_gradient_end', '#2e7aad')
|
||||
gradient = 'linear-gradient(135deg, %s 0%%, %s 60%%, %s 100%%)' % (g_start, g_mid, g_end)
|
||||
google_maps_api_key = ICP.get_param('fusion_claims.google_maps_api_key', '')
|
||||
|
||||
return {
|
||||
'portal_gradient': gradient,
|
||||
'google_maps_api_key': google_maps_api_key,
|
||||
}
|
||||
|
||||
def _get_user_timezone(self):
|
||||
tz_name = request.env.user.tz or 'America/Toronto'
|
||||
try:
|
||||
return pytz.timezone(tz_name)
|
||||
except pytz.exceptions.UnknownTimeZoneError:
|
||||
return pytz.timezone('America/Toronto')
|
||||
|
||||
def _get_appointment_types(self):
|
||||
"""Get appointment types available to the current user."""
|
||||
return request.env['appointment.type'].sudo().search([
|
||||
('staff_user_ids', 'in', [request.env.user.id]),
|
||||
])
|
||||
|
||||
@http.route(['/my/schedule'], type='http', auth='user', website=True)
|
||||
def schedule_page(self, **kw):
|
||||
"""Schedule overview: upcoming appointments and shareable link."""
|
||||
partner = request.env.user.partner_id
|
||||
user = request.env.user
|
||||
now = fields.Datetime.now()
|
||||
|
||||
upcoming_events = request.env['calendar.event'].sudo().search([
|
||||
('partner_ids', 'in', [partner.id]),
|
||||
('start', '>=', now),
|
||||
], order='start asc', limit=20)
|
||||
|
||||
today_events = request.env['calendar.event'].sudo().search([
|
||||
('partner_ids', 'in', [partner.id]),
|
||||
('start', '>=', now.replace(hour=0, minute=0, second=0)),
|
||||
('start', '<', (now + timedelta(days=1)).replace(hour=0, minute=0, second=0)),
|
||||
], order='start asc')
|
||||
|
||||
invite = request.env['appointment.invite'].sudo().search([
|
||||
('staff_user_ids', 'in', [user.id]),
|
||||
], limit=1)
|
||||
share_url = invite.book_url if invite else ''
|
||||
|
||||
appointment_types = self._get_appointment_types()
|
||||
tz = self._get_user_timezone()
|
||||
|
||||
values = self._get_schedule_values()
|
||||
values.update({
|
||||
'page_name': 'schedule',
|
||||
'upcoming_events': upcoming_events,
|
||||
'today_events': today_events,
|
||||
'share_url': share_url,
|
||||
'appointment_types': appointment_types,
|
||||
'user_tz': tz,
|
||||
'now': now,
|
||||
})
|
||||
return request.render('fusion_authorizer_portal.portal_schedule_page', values)
|
||||
|
||||
@http.route(['/my/schedule/book'], type='http', auth='user', website=True)
|
||||
def schedule_book(self, appointment_type_id=None, **kw):
|
||||
"""Booking form for a new appointment."""
|
||||
appointment_types = self._get_appointment_types()
|
||||
if not appointment_types:
|
||||
return request.redirect('/my/schedule')
|
||||
|
||||
if appointment_type_id:
|
||||
selected_type = request.env['appointment.type'].sudo().browse(int(appointment_type_id))
|
||||
if not selected_type.exists():
|
||||
selected_type = appointment_types[0]
|
||||
else:
|
||||
selected_type = appointment_types[0]
|
||||
|
||||
values = self._get_schedule_values()
|
||||
values.update({
|
||||
'page_name': 'schedule_book',
|
||||
'appointment_types': appointment_types,
|
||||
'selected_type': selected_type,
|
||||
'now': fields.Datetime.now(),
|
||||
'error': kw.get('error'),
|
||||
'success': kw.get('success'),
|
||||
})
|
||||
return request.render('fusion_authorizer_portal.portal_schedule_book', values)
|
||||
|
||||
@http.route('/my/schedule/available-slots', type='json', auth='user', website=True)
|
||||
def schedule_available_slots(self, appointment_type_id, selected_date=None, **kw):
|
||||
"""JSON-RPC endpoint: return available time slots for a date."""
|
||||
appointment_type = request.env['appointment.type'].sudo().browse(int(appointment_type_id))
|
||||
if not appointment_type.exists():
|
||||
return {'error': 'Appointment type not found', 'slots': []}
|
||||
|
||||
user = request.env.user
|
||||
tz_name = user.tz or 'America/Toronto'
|
||||
tz = self._get_user_timezone()
|
||||
|
||||
ref_date = fields.Datetime.now()
|
||||
slot_data = appointment_type._get_appointment_slots(
|
||||
timezone=tz_name,
|
||||
filter_users=request.env['res.users'].sudo().browse(user.id),
|
||||
asked_capacity=1,
|
||||
reference_date=ref_date,
|
||||
)
|
||||
|
||||
filtered_slots = []
|
||||
target_date = None
|
||||
if selected_date:
|
||||
try:
|
||||
target_date = datetime.strptime(selected_date, '%Y-%m-%d').date()
|
||||
except ValueError:
|
||||
return {'error': 'Invalid date format', 'slots': []}
|
||||
|
||||
for month_data in slot_data:
|
||||
for week in month_data.get('weeks', []):
|
||||
for day_info in week:
|
||||
if not day_info:
|
||||
continue
|
||||
day = day_info.get('day')
|
||||
if target_date and day != target_date:
|
||||
continue
|
||||
for slot in day_info.get('slots', []):
|
||||
slot_dt_str = slot.get('datetime')
|
||||
if not slot_dt_str:
|
||||
continue
|
||||
filtered_slots.append({
|
||||
'datetime': slot_dt_str,
|
||||
'start_hour': slot.get('start_hour', ''),
|
||||
'end_hour': slot.get('end_hour', ''),
|
||||
'duration': slot.get('slot_duration', str(appointment_type.appointment_duration)),
|
||||
'staff_user_id': slot.get('staff_user_id', user.id),
|
||||
})
|
||||
|
||||
available_dates = []
|
||||
if not target_date:
|
||||
seen = set()
|
||||
for month_data in slot_data:
|
||||
for week in month_data.get('weeks', []):
|
||||
for day_info in week:
|
||||
if not day_info:
|
||||
continue
|
||||
day = day_info.get('day')
|
||||
if day and day_info.get('slots') and str(day) not in seen:
|
||||
seen.add(str(day))
|
||||
available_dates.append(str(day))
|
||||
|
||||
return {
|
||||
'slots': filtered_slots,
|
||||
'available_dates': sorted(available_dates),
|
||||
'duration': appointment_type.appointment_duration,
|
||||
'timezone': tz_name,
|
||||
}
|
||||
|
||||
@http.route('/my/schedule/week-events', type='json', auth='user', website=True)
|
||||
def schedule_week_events(self, selected_date, **kw):
|
||||
"""Return the user's calendar events for the Mon-Sun week containing selected_date."""
|
||||
try:
|
||||
target = datetime.strptime(selected_date, '%Y-%m-%d').date()
|
||||
except (ValueError, TypeError):
|
||||
return {'error': 'Invalid date format', 'events': [], 'week_days': []}
|
||||
|
||||
monday = target - timedelta(days=target.weekday())
|
||||
sunday = monday + timedelta(days=6)
|
||||
|
||||
partner = request.env.user.partner_id
|
||||
tz = self._get_user_timezone()
|
||||
|
||||
monday_start_local = tz.localize(datetime.combine(monday, datetime.min.time()))
|
||||
sunday_end_local = tz.localize(datetime.combine(sunday, datetime.max.time()))
|
||||
monday_start_utc = monday_start_local.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
sunday_end_utc = sunday_end_local.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
|
||||
events = request.env['calendar.event'].sudo().search([
|
||||
('partner_ids', 'in', [partner.id]),
|
||||
('start', '>=', monday_start_utc),
|
||||
('start', '<=', sunday_end_utc),
|
||||
], order='start asc')
|
||||
|
||||
event_list = []
|
||||
for ev in events:
|
||||
start_utc = ev.start
|
||||
stop_utc = ev.stop
|
||||
start_local = pytz.utc.localize(start_utc).astimezone(tz)
|
||||
stop_local = pytz.utc.localize(stop_utc).astimezone(tz)
|
||||
event_list.append({
|
||||
'name': ev.name or '',
|
||||
'start': start_local.strftime('%Y-%m-%d %H:%M'),
|
||||
'end': stop_local.strftime('%Y-%m-%d %H:%M'),
|
||||
'start_time': start_local.strftime('%I:%M %p'),
|
||||
'end_time': stop_local.strftime('%I:%M %p'),
|
||||
'day_of_week': start_local.weekday(),
|
||||
'date': start_local.strftime('%Y-%m-%d'),
|
||||
'location': ev.location or '',
|
||||
'duration': ev.duration,
|
||||
})
|
||||
|
||||
day_labels = ['Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat', 'Sun']
|
||||
week_days = []
|
||||
for i in range(7):
|
||||
day = monday + timedelta(days=i)
|
||||
week_days.append({
|
||||
'label': day_labels[i],
|
||||
'date': day.strftime('%Y-%m-%d'),
|
||||
'day_num': day.day,
|
||||
'is_selected': day == target,
|
||||
})
|
||||
|
||||
return {
|
||||
'events': event_list,
|
||||
'week_days': week_days,
|
||||
'selected_date': selected_date,
|
||||
}
|
||||
|
||||
@http.route('/my/schedule/book/submit', type='http', auth='user', website=True, methods=['POST'])
|
||||
def schedule_book_submit(self, **post):
|
||||
"""Process the booking form submission."""
|
||||
appointment_type_id = int(post.get('appointment_type_id', 0))
|
||||
appointment_type = request.env['appointment.type'].sudo().browse(appointment_type_id)
|
||||
if not appointment_type.exists():
|
||||
return request.redirect('/my/schedule/book?error=Invalid+appointment+type')
|
||||
|
||||
client_name = (post.get('client_name') or '').strip()
|
||||
client_street = (post.get('client_street') or '').strip()
|
||||
client_city = (post.get('client_city') or '').strip()
|
||||
client_province = (post.get('client_province') or '').strip()
|
||||
client_postal = (post.get('client_postal') or '').strip()
|
||||
notes = (post.get('notes') or '').strip()
|
||||
slot_datetime = (post.get('slot_datetime') or '').strip()
|
||||
slot_duration = post.get('slot_duration', str(appointment_type.appointment_duration))
|
||||
|
||||
if not client_name or not slot_datetime:
|
||||
return request.redirect('/my/schedule/book?error=Client+name+and+time+slot+are+required')
|
||||
|
||||
user = request.env.user
|
||||
tz = self._get_user_timezone()
|
||||
|
||||
try:
|
||||
start_dt_naive = datetime.strptime(slot_datetime, '%Y-%m-%d %H:%M:%S')
|
||||
start_dt_local = tz.localize(start_dt_naive)
|
||||
start_dt_utc = start_dt_local.astimezone(pytz.utc).replace(tzinfo=None)
|
||||
except (ValueError, Exception) as e:
|
||||
_logger.error("Failed to parse slot datetime %s: %s", slot_datetime, e)
|
||||
return request.redirect('/my/schedule/book?error=Invalid+time+slot')
|
||||
|
||||
duration = float(slot_duration)
|
||||
stop_dt_utc = start_dt_utc + timedelta(hours=duration)
|
||||
|
||||
is_valid = appointment_type._check_appointment_is_valid_slot(
|
||||
staff_user=user,
|
||||
resources=request.env['appointment.resource'],
|
||||
asked_capacity=1,
|
||||
timezone=str(tz),
|
||||
start_dt=start_dt_utc,
|
||||
duration=duration,
|
||||
allday=False,
|
||||
)
|
||||
if not is_valid:
|
||||
return request.redirect('/my/schedule/book?error=This+slot+is+no+longer+available.+Please+choose+another+time.')
|
||||
|
||||
address_parts = [p for p in [client_street, client_city, client_province, client_postal] if p]
|
||||
location = ', '.join(address_parts)
|
||||
|
||||
description_lines = []
|
||||
if client_name:
|
||||
description_lines.append(f"Client: {client_name}")
|
||||
if location:
|
||||
description_lines.append(f"Address: {location}")
|
||||
if notes:
|
||||
description_lines.append(f"Notes: {notes}")
|
||||
description = '\n'.join(description_lines)
|
||||
|
||||
event_name = f"{client_name} - {appointment_type.name}"
|
||||
|
||||
booking_line_values = [{
|
||||
'appointment_user_id': user.id,
|
||||
'capacity_reserved': 1,
|
||||
'capacity_used': 1,
|
||||
}]
|
||||
|
||||
try:
|
||||
event_vals = appointment_type._prepare_calendar_event_values(
|
||||
asked_capacity=1,
|
||||
booking_line_values=booking_line_values,
|
||||
description=description,
|
||||
duration=duration,
|
||||
allday=False,
|
||||
appointment_invite=request.env['appointment.invite'],
|
||||
guests=request.env['res.partner'],
|
||||
name=event_name,
|
||||
customer=user.partner_id,
|
||||
staff_user=user,
|
||||
start=start_dt_utc,
|
||||
stop=stop_dt_utc,
|
||||
)
|
||||
event_vals['location'] = location
|
||||
event = request.env['calendar.event'].sudo().create(event_vals)
|
||||
|
||||
_logger.info(
|
||||
"Appointment booked: %s at %s (event ID: %s)",
|
||||
event_name, start_dt_utc, event.id,
|
||||
)
|
||||
except Exception as e:
|
||||
_logger.error("Failed to create appointment: %s", e)
|
||||
return request.redirect('/my/schedule/book?error=Failed+to+create+appointment.+Please+try+again.')
|
||||
|
||||
return request.redirect('/my/schedule?success=Appointment+booked+successfully')
|
||||
Reference in New Issue
Block a user