Merge pull request #19475 from 0Pranav/scheduling-ui-rewrite
feat: Appointment Schedulling
This commit is contained in:
commit
a791170f29
@ -48,6 +48,11 @@ def get_data():
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Appointment",
|
||||
"description" : _("Helps you manage appointments with your leads"),
|
||||
},
|
||||
{
|
||||
"type": "doctype",
|
||||
"name": "Newsletter",
|
||||
"label": _("Newsletter"),
|
||||
}
|
||||
|
0
erpnext/crm/doctype/appointment/__init__.py
Normal file
0
erpnext/crm/doctype/appointment/__init__.py
Normal file
17
erpnext/crm/doctype/appointment/appointment.js
Normal file
17
erpnext/crm/doctype/appointment/appointment.js
Normal file
@ -0,0 +1,17 @@
|
||||
// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
// For license information, please see license.txt
|
||||
|
||||
frappe.ui.form.on('Appointment', {
|
||||
refresh: function(frm) {
|
||||
if(frm.doc.lead){
|
||||
frm.add_custom_button(frm.doc.lead,()=>{
|
||||
frappe.set_route("Form", "Lead", frm.doc.lead);
|
||||
});
|
||||
}
|
||||
if(frm.doc.calendar_event){
|
||||
frm.add_custom_button(__(frm.doc.calendar_event),()=>{
|
||||
frappe.set_route("Form", "Event", frm.doc.calendar_event);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
153
erpnext/crm/doctype/appointment/appointment.json
Normal file
153
erpnext/crm/doctype/appointment/appointment.json
Normal file
@ -0,0 +1,153 @@
|
||||
{
|
||||
"autoname": "format:APMT-{customer_name}-{####}",
|
||||
"creation": "2019-08-27 10:48:27.926283",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"scheduled_time",
|
||||
"status",
|
||||
"customer_details_section",
|
||||
"customer_name",
|
||||
"customer_phone_number",
|
||||
"customer_skype",
|
||||
"customer_email",
|
||||
"col_br_2",
|
||||
"customer_details",
|
||||
"linked_docs_section",
|
||||
"lead",
|
||||
"col_br_3",
|
||||
"calendar_event"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "customer_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Customer Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_name",
|
||||
"fieldtype": "Data",
|
||||
"in_list_view": 1,
|
||||
"label": "Name",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_phone_number",
|
||||
"fieldtype": "Data",
|
||||
"label": "Phone Number"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_skype",
|
||||
"fieldtype": "Data",
|
||||
"label": "Skype ID"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_details",
|
||||
"fieldtype": "Long Text",
|
||||
"label": "Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "scheduled_time",
|
||||
"fieldtype": "Datetime",
|
||||
"in_list_view": 1,
|
||||
"label": "Scheduled Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "status",
|
||||
"fieldtype": "Select",
|
||||
"label": "Status",
|
||||
"options": "Open\nUnverified\nClosed",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "lead",
|
||||
"fieldtype": "Link",
|
||||
"label": "Lead",
|
||||
"options": "Lead"
|
||||
},
|
||||
{
|
||||
"fieldname": "calendar_event",
|
||||
"fieldtype": "Link",
|
||||
"label": "Calendar Event",
|
||||
"options": "Event"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_br_2",
|
||||
"fieldtype": "Column Break"
|
||||
},
|
||||
{
|
||||
"fieldname": "customer_email",
|
||||
"fieldtype": "Data",
|
||||
"label": "Email",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "linked_docs_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Linked Documents"
|
||||
},
|
||||
{
|
||||
"fieldname": "col_br_3",
|
||||
"fieldtype": "Column Break"
|
||||
}
|
||||
],
|
||||
"modified": "2019-10-14 15:23:54.630731",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment",
|
||||
"name_case": "UPPER CASE",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Guest",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"export": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"report": 1,
|
||||
"role": "Sales User",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
223
erpnext/crm/doctype/appointment/appointment.py
Normal file
223
erpnext/crm/doctype/appointment/appointment.py
Normal file
@ -0,0 +1,223 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import urllib
|
||||
from collections import Counter
|
||||
from datetime import timedelta
|
||||
|
||||
import frappe
|
||||
from frappe import _
|
||||
from frappe.model.document import Document
|
||||
from frappe.utils import get_url
|
||||
from frappe.utils.verified_command import verify_request, get_signed_params
|
||||
|
||||
|
||||
class Appointment(Document):
|
||||
|
||||
def find_lead_by_email(self):
|
||||
lead_list = frappe.get_list(
|
||||
'Lead', filters={'email_id': self.customer_email}, ignore_permissions=True)
|
||||
if lead_list:
|
||||
return lead_list[0].name
|
||||
return None
|
||||
|
||||
def before_insert(self):
|
||||
number_of_appointments_in_same_slot = frappe.db.count(
|
||||
'Appointment', filters={'scheduled_time': self.scheduled_time})
|
||||
number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents')
|
||||
if not number_of_agents == 0:
|
||||
if (number_of_appointments_in_same_slot >= number_of_agents):
|
||||
frappe.throw('Time slot is not available')
|
||||
# Link lead
|
||||
if not self.lead:
|
||||
self.lead = self.find_lead_by_email()
|
||||
|
||||
def after_insert(self):
|
||||
if self.lead:
|
||||
# Create Calendar event
|
||||
self.auto_assign()
|
||||
self.create_calendar_event()
|
||||
else:
|
||||
# Set status to unverified
|
||||
self.status = 'Unverified'
|
||||
# Send email to confirm
|
||||
self.send_confirmation_email()
|
||||
|
||||
def send_confirmation_email(self):
|
||||
verify_url = self._get_verify_url()
|
||||
template = 'confirm_appointment'
|
||||
args = {
|
||||
"link":verify_url,
|
||||
"site_url":frappe.utils.get_url(),
|
||||
"full_name":self.customer_name,
|
||||
}
|
||||
frappe.sendmail(recipients=[self.customer_email],
|
||||
template=template,
|
||||
args=args,
|
||||
subject=_('Appointment Confirmation'))
|
||||
if frappe.session.user == "Guest":
|
||||
frappe.msgprint(
|
||||
'Please check your email to confirm the appointment')
|
||||
else :
|
||||
frappe.msgprint(
|
||||
'Appointment was created. But no lead was found. Please check the email to confirm')
|
||||
|
||||
def on_change(self):
|
||||
# Sync Calendar
|
||||
if not self.calendar_event:
|
||||
return
|
||||
cal_event = frappe.get_doc('Event', self.calendar_event)
|
||||
cal_event.starts_on = self.scheduled_time
|
||||
cal_event.save(ignore_permissions=True)
|
||||
|
||||
|
||||
def set_verified(self, email):
|
||||
if not email == self.customer_email:
|
||||
frappe.throw('Email verification failed.')
|
||||
# Create new lead
|
||||
self.create_lead_and_link()
|
||||
# Remove unverified status
|
||||
self.status = 'Open'
|
||||
# Create calender event
|
||||
self.auto_assign()
|
||||
self.create_calendar_event()
|
||||
self.save(ignore_permissions=True)
|
||||
frappe.db.commit()
|
||||
|
||||
def create_lead_and_link(self):
|
||||
# Return if already linked
|
||||
if self.lead:
|
||||
return
|
||||
lead = frappe.get_doc({
|
||||
'doctype': 'Lead',
|
||||
'lead_name': self.customer_name,
|
||||
'email_id': self.customer_email,
|
||||
'notes': self.customer_details,
|
||||
'phone': self.customer_phone_number,
|
||||
})
|
||||
lead.insert(ignore_permissions=True)
|
||||
# Link lead
|
||||
self.lead = lead.name
|
||||
|
||||
def auto_assign(self):
|
||||
from frappe.desk.form.assign_to import add as add_assignemnt
|
||||
existing_assignee = self.get_assignee_from_latest_opportunity()
|
||||
if existing_assignee:
|
||||
# If the latest opportunity is assigned to someone
|
||||
# Assign the appointment to the same
|
||||
add_assignemnt({
|
||||
'doctype': self.doctype,
|
||||
'name': self.name,
|
||||
'assign_to': existing_assignee
|
||||
})
|
||||
return
|
||||
if self._assign:
|
||||
return
|
||||
available_agents = _get_agents_sorted_by_asc_workload(
|
||||
self.scheduled_time.date())
|
||||
for agent in available_agents:
|
||||
if(_check_agent_availability(agent, self.scheduled_time)):
|
||||
agent = agent[0]
|
||||
add_assignemnt({
|
||||
'doctype': self.doctype,
|
||||
'name': self.name,
|
||||
'assign_to': agent
|
||||
})
|
||||
break
|
||||
|
||||
def get_assignee_from_latest_opportunity(self):
|
||||
if not self.lead:
|
||||
return None
|
||||
if not frappe.db.exists('Lead', self.lead):
|
||||
return None
|
||||
opporutnities = frappe.get_list(
|
||||
'Opportunity',
|
||||
filters={
|
||||
'party_name': self.lead,
|
||||
},
|
||||
ignore_permissions=True,
|
||||
order_by='creation desc')
|
||||
if not opporutnities:
|
||||
return None
|
||||
latest_opportunity = frappe.get_doc('Opportunity', opporutnities[0].name )
|
||||
assignee = latest_opportunity._assign
|
||||
if not assignee:
|
||||
return None
|
||||
assignee = frappe.parse_json(assignee)[0]
|
||||
return assignee
|
||||
|
||||
def create_calendar_event(self):
|
||||
if self.calendar_event:
|
||||
return
|
||||
appointment_event = frappe.get_doc({
|
||||
'doctype': 'Event',
|
||||
'subject': ' '.join(['Appointment with', self.customer_name]),
|
||||
'starts_on': self.scheduled_time,
|
||||
'status': 'Open',
|
||||
'type': 'Public',
|
||||
'send_reminder': frappe.db.get_single_value('Appointment Booking Settings', 'email_reminders'),
|
||||
'event_participants': [dict(reference_doctype='Lead', reference_docname=self.lead)]
|
||||
})
|
||||
employee = _get_employee_from_user(self._assign)
|
||||
if employee:
|
||||
appointment_event.append('event_participants', dict(
|
||||
reference_doctype='Employee',
|
||||
reference_docname=employee.name))
|
||||
appointment_event.insert(ignore_permissions=True)
|
||||
self.calendar_event = appointment_event.name
|
||||
self.save(ignore_permissions=True)
|
||||
|
||||
def _get_verify_url(self):
|
||||
verify_route = '/book-appointment/verify'
|
||||
params = {
|
||||
'email': self.customer_email,
|
||||
'appointment': self.name
|
||||
}
|
||||
return get_url(verify_route + '?' + get_signed_params(params))
|
||||
|
||||
|
||||
def _get_agents_sorted_by_asc_workload(date):
|
||||
appointments = frappe.db.get_list('Appointment', fields='*')
|
||||
agent_list = _get_agent_list_as_strings()
|
||||
if not appointments:
|
||||
return agent_list
|
||||
appointment_counter = Counter(agent_list)
|
||||
for appointment in appointments:
|
||||
assigned_to = frappe.parse_json(appointment._assign)
|
||||
if not assigned_to:
|
||||
continue
|
||||
if (assigned_to[0] in agent_list) and appointment.scheduled_time.date() == date:
|
||||
appointment_counter[assigned_to[0]] += 1
|
||||
sorted_agent_list = appointment_counter.most_common()
|
||||
sorted_agent_list.reverse()
|
||||
return sorted_agent_list
|
||||
|
||||
|
||||
def _get_agent_list_as_strings():
|
||||
agent_list_as_strings = []
|
||||
agent_list = frappe.get_doc('Appointment Booking Settings').agent_list
|
||||
for agent in agent_list:
|
||||
agent_list_as_strings.append(agent.user)
|
||||
return agent_list_as_strings
|
||||
|
||||
|
||||
def _check_agent_availability(agent_email, scheduled_time):
|
||||
appointemnts_at_scheduled_time = frappe.get_list(
|
||||
'Appointment', filters={'scheduled_time': scheduled_time})
|
||||
for appointment in appointemnts_at_scheduled_time:
|
||||
if appointment._assign == agent_email:
|
||||
return False
|
||||
return True
|
||||
|
||||
|
||||
def _get_employee_from_user(user):
|
||||
employee_docname = frappe.db.exists(
|
||||
{'doctype': 'Employee', 'user_id': user})
|
||||
if employee_docname:
|
||||
# frappe.db.exists returns a tuple of a tuple
|
||||
return frappe.get_doc('Employee', employee_docname[0][0])
|
||||
return None
|
||||
|
58
erpnext/crm/doctype/appointment/test_appointment.py
Normal file
58
erpnext/crm/doctype/appointment/test_appointment.py
Normal file
@ -0,0 +1,58 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import frappe
|
||||
import unittest
|
||||
import datetime
|
||||
|
||||
|
||||
def create_test_lead():
|
||||
test_lead = frappe.db.exists({'doctype': 'Lead', 'lead_name': 'Test Lead'})
|
||||
if test_lead:
|
||||
return frappe.get_doc('Lead', test_lead[0][0])
|
||||
test_lead = frappe.get_doc({
|
||||
'doctype': 'Lead',
|
||||
'lead_name': 'Test Lead',
|
||||
'email_id': 'test@example.com'
|
||||
})
|
||||
test_lead.insert(ignore_permissions=True)
|
||||
return test_lead
|
||||
|
||||
|
||||
def create_test_appointments():
|
||||
test_appointment = frappe.db.exists(
|
||||
{'doctype': 'Appointment', 'scheduled_time':datetime.datetime.now(),'email':'test@example.com'})
|
||||
if test_appointment:
|
||||
return frappe.get_doc('Appointment', test_appointment[0][0])
|
||||
test_appointment = frappe.get_doc({
|
||||
'doctype': 'Appointment',
|
||||
'email': 'test@example.com',
|
||||
'status': 'Open',
|
||||
'customer_name': 'Test Lead',
|
||||
'customer_phone_number': '666',
|
||||
'customer_skype': 'test',
|
||||
'customer_email': 'test@example.com',
|
||||
'scheduled_time': datetime.datetime.now()
|
||||
})
|
||||
test_appointment.insert()
|
||||
return test_appointment
|
||||
|
||||
|
||||
class TestAppointment(unittest.TestCase):
|
||||
test_appointment = test_lead = None
|
||||
|
||||
def setUp(self):
|
||||
self.test_lead = create_test_lead()
|
||||
self.test_appointment = create_test_appointments()
|
||||
|
||||
def test_calendar_event_created(self):
|
||||
cal_event = frappe.get_doc(
|
||||
'Event', self.test_appointment.calendar_event)
|
||||
self.assertEqual(cal_event.starts_on,
|
||||
self.test_appointment.scheduled_time)
|
||||
|
||||
def test_lead_linked(self):
|
||||
lead = frappe.get_doc('Lead', self.test_lead.name)
|
||||
self.assertIsNotNone(lead)
|
@ -0,0 +1,10 @@
|
||||
frappe.ui.form.on('Appointment Booking Settings', 'validate',check_times);
|
||||
function check_times(frm) {
|
||||
$.each(frm.doc.availability_of_slots || [], function (i, d) {
|
||||
let from_time = Date.parse('01/01/2019 ' + d.from_time);
|
||||
let to_time = Date.parse('01/01/2019 ' + d.to_time);
|
||||
if (from_time > to_time) {
|
||||
frappe.throw(__(`In row ${i + 1} of Appointment Booking Slots : "To Time" must be later than "From Time"`));
|
||||
}
|
||||
});
|
||||
}
|
@ -0,0 +1,151 @@
|
||||
{
|
||||
"creation": "2019-08-27 10:56:48.309824",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"enable_scheduling",
|
||||
"agent_detail_section",
|
||||
"availability_of_slots",
|
||||
"number_of_agents",
|
||||
"agent_list",
|
||||
"holiday_list",
|
||||
"appointment_details_section",
|
||||
"appointment_duration",
|
||||
"email_reminders",
|
||||
"advance_booking_days",
|
||||
"success_details",
|
||||
"success_redirect_url"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "availability_of_slots",
|
||||
"fieldtype": "Table",
|
||||
"label": "Availability Of Slots",
|
||||
"options": "Appointment Booking Slots",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "1",
|
||||
"fieldname": "number_of_agents",
|
||||
"fieldtype": "Int",
|
||||
"hidden": 1,
|
||||
"in_list_view": 1,
|
||||
"label": "Number of Concurrent Appointments",
|
||||
"read_only": 1,
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "holiday_list",
|
||||
"fieldtype": "Link",
|
||||
"in_list_view": 1,
|
||||
"label": "Holiday List",
|
||||
"options": "Holiday List",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "60",
|
||||
"fieldname": "appointment_duration",
|
||||
"fieldtype": "Int",
|
||||
"label": "Appointment Duration (In Minutes)",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"description": "Notify customer and agent via email on the day of the appointment.",
|
||||
"fieldname": "email_reminders",
|
||||
"fieldtype": "Check",
|
||||
"label": "Notify Via Email"
|
||||
},
|
||||
{
|
||||
"default": "7",
|
||||
"fieldname": "advance_booking_days",
|
||||
"fieldtype": "Int",
|
||||
"label": "Number of days appointments can be booked in advance",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "agent_list",
|
||||
"fieldtype": "Table MultiSelect",
|
||||
"label": "Agents",
|
||||
"options": "Assignment Rule User",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"default": "0",
|
||||
"fieldname": "enable_scheduling",
|
||||
"fieldtype": "Check",
|
||||
"label": "Enable Appointment Scheduling",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "agent_detail_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Agent Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "appointment_details_section",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Appointment Details"
|
||||
},
|
||||
{
|
||||
"fieldname": "success_details",
|
||||
"fieldtype": "Section Break",
|
||||
"label": "Success Settings"
|
||||
},
|
||||
{
|
||||
"description": "Leave blank for home.\nThis is relative to site URL, for example \"about\" will redirect to \"https://yoursitename.com/about\"",
|
||||
"fieldname": "success_redirect_url",
|
||||
"fieldtype": "Data",
|
||||
"label": "Success Redirect URL"
|
||||
}
|
||||
],
|
||||
"issingle": 1,
|
||||
"modified": "2019-11-26 12:14:17.669366",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment Booking Settings",
|
||||
"owner": "Administrator",
|
||||
"permissions": [
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "System Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Guest",
|
||||
"share": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "HR Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
},
|
||||
{
|
||||
"create": 1,
|
||||
"delete": 1,
|
||||
"email": 1,
|
||||
"print": 1,
|
||||
"read": 1,
|
||||
"role": "Sales Manager",
|
||||
"share": 1,
|
||||
"write": 1
|
||||
}
|
||||
],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,43 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
import frappe
|
||||
from frappe import _
|
||||
import datetime
|
||||
from frappe.model.document import Document
|
||||
|
||||
|
||||
class AppointmentBookingSettings(Document):
|
||||
agent_list = [] #Hack
|
||||
min_date = '01/01/1970 '
|
||||
format_string = "%d/%m/%Y %H:%M:%S"
|
||||
|
||||
def validate(self):
|
||||
self.validate_availability_of_slots()
|
||||
|
||||
def save(self):
|
||||
self.number_of_agents = len(self.agent_list)
|
||||
super(AppointmentBookingSettings, self).save()
|
||||
|
||||
def validate_availability_of_slots(self):
|
||||
for record in self.availability_of_slots:
|
||||
from_time = datetime.datetime.strptime(
|
||||
self.min_date+record.from_time, self.format_string)
|
||||
to_time = datetime.datetime.strptime(
|
||||
self.min_date+record.to_time, self.format_string)
|
||||
timedelta = to_time-from_time
|
||||
self.validate_from_and_to_time(from_time, to_time)
|
||||
self.duration_is_divisible(from_time, to_time)
|
||||
|
||||
def validate_from_and_to_time(self, from_time, to_time):
|
||||
if from_time > to_time:
|
||||
err_msg = _('<b>From Time</b> cannot be later than <b>To Time</b> for {0}').format(record.day_of_week)
|
||||
frappe.throw(_(err_msg))
|
||||
|
||||
def duration_is_divisible(self, from_time, to_time):
|
||||
timedelta = to_time - from_time
|
||||
if timedelta.total_seconds() % (self.appointment_duration * 60):
|
||||
frappe.throw(
|
||||
_('The difference between from time and To Time must be a multiple of Appointment'))
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and Contributors
|
||||
# See license.txt
|
||||
from __future__ import unicode_literals
|
||||
|
||||
# import frappe
|
||||
import unittest
|
||||
|
||||
class TestAppointmentBookingSettings(unittest.TestCase):
|
||||
pass
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"creation": "2019-11-19 10:49:49.494927",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"day_of_week",
|
||||
"from_time",
|
||||
"to_time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "day_of_week",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Day Of Week",
|
||||
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "From Time ",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "To Time",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-11-19 10:49:49.494927",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Appointment Booking Slots",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class AppointmentBookingSlots(Document):
|
||||
pass
|
@ -0,0 +1,46 @@
|
||||
{
|
||||
"creation": "2019-09-10 15:02:05.779434",
|
||||
"doctype": "DocType",
|
||||
"editable_grid": 1,
|
||||
"engine": "InnoDB",
|
||||
"field_order": [
|
||||
"day_of_week",
|
||||
"from_time",
|
||||
"to_time"
|
||||
],
|
||||
"fields": [
|
||||
{
|
||||
"fieldname": "day_of_week",
|
||||
"fieldtype": "Select",
|
||||
"in_list_view": 1,
|
||||
"label": "Day Of Week",
|
||||
"options": "Sunday\nMonday\nTuesday\nWednesday\nThursday\nFriday\nSaturday",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "from_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "From Time",
|
||||
"reqd": 1
|
||||
},
|
||||
{
|
||||
"fieldname": "to_time",
|
||||
"fieldtype": "Time",
|
||||
"in_list_view": 1,
|
||||
"label": "To Time",
|
||||
"reqd": 1
|
||||
}
|
||||
],
|
||||
"istable": 1,
|
||||
"modified": "2019-09-10 15:05:20.406855",
|
||||
"modified_by": "Administrator",
|
||||
"module": "CRM",
|
||||
"name": "Availability Of Slots",
|
||||
"owner": "Administrator",
|
||||
"permissions": [],
|
||||
"quick_entry": 1,
|
||||
"sort_field": "modified",
|
||||
"sort_order": "DESC",
|
||||
"track_changes": 1
|
||||
}
|
@ -0,0 +1,10 @@
|
||||
# -*- coding: utf-8 -*-
|
||||
# Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
|
||||
# For license information, please see license.txt
|
||||
|
||||
from __future__ import unicode_literals
|
||||
# import frappe
|
||||
from frappe.model.document import Document
|
||||
|
||||
class AvailabilityOfSlots(Document):
|
||||
pass
|
File diff suppressed because it is too large
Load Diff
10
erpnext/templates/emails/confirm_appointment.html
Normal file
10
erpnext/templates/emails/confirm_appointment.html
Normal file
@ -0,0 +1,10 @@
|
||||
<p>{{_("Dear")}} {{ full_name }}{% if last_name %} {{ last_name}}{% endif %},</p>
|
||||
<p>{{_("A new appointment has been created for you with {0}").format(site_url)}}.</p>
|
||||
<p>{{_("Click on the link below to verify your email and confirm the appointment")}}.</p>
|
||||
|
||||
<p style="margin: 30px 0px;">
|
||||
<a href="{{ link }}" rel="nofollow" style="padding: 8px 20px; background-color: #7575ff; color: #fff; border-radius: 4px; text-decoration: none; line-height: 1; border-bottom: 3px solid rgba(0, 0, 0, 0.2); font-size: 14px; font-weight: 200;">{{ _("Verify Email") }}</a>
|
||||
</p>
|
||||
|
||||
<br>
|
||||
<p style="font-size: 85%;">{{_("You can also copy-paste this link in your browser")}} <a href="{{ link }}">{{ link }}</a></p>
|
53
erpnext/www/book-appointment/index.css
Normal file
53
erpnext/www/book-appointment/index.css
Normal file
@ -0,0 +1,53 @@
|
||||
.time-slot {
|
||||
margin-bottom: 2em;
|
||||
margin-left: 0.5em;
|
||||
margin-right: 0.5em;
|
||||
border-radius: 0.4em;
|
||||
cursor: pointer;
|
||||
border: 0.5px solid #cccccc;
|
||||
min-height: 75px;
|
||||
padding: 0.5em 1em;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
#submit-button-area {
|
||||
display: grid;
|
||||
grid-template-areas:
|
||||
"submit"
|
||||
"back";
|
||||
}
|
||||
}
|
||||
#customer-form{
|
||||
border-color: black;
|
||||
}
|
||||
#customer-form ::placeholder{
|
||||
color: #ddd;
|
||||
}
|
||||
#timeslot-container{
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.time-slot:hover {
|
||||
background: #ddd;
|
||||
}
|
||||
|
||||
.time-slot.unavailable {
|
||||
background: #CBD5E0;
|
||||
cursor: not-allowed;
|
||||
color: #718096
|
||||
}
|
||||
|
||||
.time-slot.unavailable .text-muted {
|
||||
color: #718096
|
||||
}
|
||||
|
||||
.time-slot.selected {
|
||||
color: white;
|
||||
background: #5e64ff;
|
||||
}
|
||||
|
||||
.time-slot.selected .text-muted {
|
||||
color: #EDF2F7 !important;
|
||||
}
|
66
erpnext/www/book-appointment/index.html
Normal file
66
erpnext/www/book-appointment/index.html
Normal file
@ -0,0 +1,66 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %}{{ _("Book Appointment") }}{% endblock %}
|
||||
|
||||
{% block script %}
|
||||
<script src="assets/js/moment-bundle.min.js"></script>
|
||||
<script src="book-appointment/index.js"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block page_content %}
|
||||
<div class="container">
|
||||
<!-- title: Book an appointment -->
|
||||
<div id="select-date-time">
|
||||
<div class="text-center mt-5">
|
||||
<h3>Book an appointment</h3>
|
||||
<p class="lead text-muted" id="lead-text">Select the date and your timezone</p>
|
||||
</div>
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col-md-6 align-self-center ">
|
||||
<div class="row">
|
||||
<input type="date" oninput="on_date_or_timezone_select()" name="appointment-date"
|
||||
id="appointment-date" class="form-control mt-3 col-md m-3">
|
||||
<select name="appointment-timezone" oninput="on_date_or_timezone_select()" id="appointment-timezone"
|
||||
class="form-control m-3 col-md">
|
||||
</select>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="mt-3" id="timeslot-container">
|
||||
|
||||
</div>
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col-md-4 mb-3">
|
||||
<button class="btn btn-primary form-control" id="next-button">Next</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<!--Enter Details-->
|
||||
<div id="enter-details" class="mb-5">
|
||||
<div class="text-center mt-5">
|
||||
<h3>Add details</h3>
|
||||
<p class="lead text-muted">Selected date is <span class="date-span"></span> at <span class="time-span">
|
||||
</span></p>
|
||||
</div>
|
||||
<div class="row justify-content-center mt-3">
|
||||
<div class="col-md-4 align-items-center">
|
||||
<form id="customer-form" action='#'>
|
||||
<input class="form-control mt-3" type="text" name="customer_name" id="customer_name" placeholder="Your Name (required)" required>
|
||||
<input class="form-control mt-3" type="tel" name="customer_number" id="customer_number" placeholder="+910000000000">
|
||||
<input class="form-control mt-3" type="text" name="customer_skype" id="customer_skype" placeholder="Skype">
|
||||
<input class="form-control mt-3"type="email" name="customer_email" id="customer_email" placeholder="Email Address (required)" required>
|
||||
|
||||
<textarea class="form-control mt-3" name="customer_notes" id="customer_notes" cols="30" rows="10"
|
||||
placeholder="Notes"></textarea>
|
||||
</form>
|
||||
<div class="row mt-3 " id="submit-button-area">
|
||||
<div class="col-md mt-3" style="grid-area: back;"><button class="btn btn-dark form-control" onclick="initialise_select_date()">Go back</button></div>
|
||||
<div class="col-md mt-3" style="grid-area: submit;"><button class="btn btn-primary form-control " onclick="submit()" id="submit-button">Submit</button></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{% endblock %}
|
236
erpnext/www/book-appointment/index.js
Normal file
236
erpnext/www/book-appointment/index.js
Normal file
@ -0,0 +1,236 @@
|
||||
frappe.ready(async () => {
|
||||
initialise_select_date();
|
||||
})
|
||||
|
||||
window.holiday_list = [];
|
||||
|
||||
async function initialise_select_date() {
|
||||
navigate_to_page(1);
|
||||
await get_global_variables();
|
||||
setup_date_picker();
|
||||
setup_timezone_selector();
|
||||
hide_next_button();
|
||||
}
|
||||
|
||||
async function get_global_variables() {
|
||||
// Using await through this file instead of then.
|
||||
window.appointment_settings = (await frappe.call({
|
||||
method: 'erpnext.www.book-appointment.index.get_appointment_settings'
|
||||
})).message;
|
||||
window.timezones = (await frappe.call({
|
||||
method:'erpnext.www.book-appointment.index.get_timezones'
|
||||
})).message;
|
||||
window.holiday_list = window.appointment_settings.holiday_list;
|
||||
}
|
||||
|
||||
function setup_timezone_selector() {
|
||||
/**
|
||||
* window.timezones is a dictionary with the following structure
|
||||
* { IANA name: Pretty name}
|
||||
* For example : { Asia/Kolkata : "India Time - Asia/Kolkata"}
|
||||
*/
|
||||
let timezones_element = document.getElementById('appointment-timezone');
|
||||
let offset = new Date().getTimezoneOffset();
|
||||
Object.keys(window.timezones).forEach((timezone) => {
|
||||
let opt = document.createElement('option');
|
||||
opt.value = timezone;
|
||||
if (timezone == moment.tz.guess()) {
|
||||
opt.selected = true;
|
||||
}
|
||||
opt.innerHTML = window.timezones[timezone]
|
||||
timezones_element.appendChild(opt)
|
||||
});
|
||||
}
|
||||
|
||||
function setup_date_picker() {
|
||||
let date_picker = document.getElementById('appointment-date');
|
||||
let today = new Date();
|
||||
date_picker.min = today.toISOString().substr(0, 10);
|
||||
today.setDate(today.getDate() + window.appointment_settings.advance_booking_days);
|
||||
date_picker.max = today.toISOString().substr(0, 10);
|
||||
}
|
||||
|
||||
function hide_next_button() {
|
||||
let next_button = document.getElementById('next-button');
|
||||
next_button.disabled = true;
|
||||
next_button.onclick = () => frappe.msgprint("Please select a date and time");
|
||||
}
|
||||
|
||||
function show_next_button() {
|
||||
let next_button = document.getElementById('next-button');
|
||||
next_button.disabled = false;
|
||||
next_button.onclick = setup_details_page;
|
||||
}
|
||||
|
||||
function on_date_or_timezone_select() {
|
||||
let date_picker = document.getElementById('appointment-date');
|
||||
let timezone = document.getElementById('appointment-timezone');
|
||||
if (date_picker.value === '') {
|
||||
clear_time_slots();
|
||||
hide_next_button();
|
||||
frappe.throw('Please select a date');
|
||||
}
|
||||
window.selected_date = date_picker.value;
|
||||
window.selected_timezone = timezone.value;
|
||||
update_time_slots(date_picker.value, timezone.value);
|
||||
let lead_text = document.getElementById('lead-text');
|
||||
lead_text.innerHTML = "Select Time"
|
||||
}
|
||||
|
||||
async function get_time_slots(date, timezone) {
|
||||
let slots = (await frappe.call({
|
||||
method: 'erpnext.www.book-appointment.index.get_appointment_slots',
|
||||
args: {
|
||||
date: date,
|
||||
timezone: timezone
|
||||
}
|
||||
})).message;
|
||||
return slots;
|
||||
}
|
||||
|
||||
async function update_time_slots(selected_date, selected_timezone) {
|
||||
let timeslot_container = document.getElementById('timeslot-container');
|
||||
window.slots = await get_time_slots(selected_date, selected_timezone);
|
||||
clear_time_slots();
|
||||
if (window.slots.length <= 0) {
|
||||
let message_div = document.createElement('p');
|
||||
message_div.innerHTML = "There are no slots available on this date";
|
||||
timeslot_container.appendChild(message_div);
|
||||
return
|
||||
}
|
||||
window.slots.forEach((slot, index) => {
|
||||
// Get and append timeslot div
|
||||
let timeslot_div = get_timeslot_div_layout(slot)
|
||||
timeslot_container.appendChild(timeslot_div);
|
||||
});
|
||||
set_default_timeslot();
|
||||
}
|
||||
|
||||
function get_timeslot_div_layout(timeslot) {
|
||||
let start_time = new Date(timeslot.time)
|
||||
let timeslot_div = document.createElement('div');
|
||||
timeslot_div.classList.add('time-slot');
|
||||
if (!timeslot.availability) {
|
||||
timeslot_div.classList.add('unavailable')
|
||||
}
|
||||
timeslot_div.innerHTML = get_slot_layout(start_time);
|
||||
timeslot_div.id = timeslot.time.substr(11, 20);
|
||||
timeslot_div.addEventListener('click', select_time);
|
||||
return timeslot_div
|
||||
}
|
||||
|
||||
function clear_time_slots() {
|
||||
// Clear any existing divs in timeslot container
|
||||
let timeslot_container = document.getElementById('timeslot-container');
|
||||
while (timeslot_container.firstChild) {
|
||||
timeslot_container.removeChild(timeslot_container.firstChild);
|
||||
}
|
||||
}
|
||||
|
||||
function get_slot_layout(time) {
|
||||
let timezone = document.getElementById("appointment-timezone").value;
|
||||
time = new Date(time);
|
||||
let start_time_string = moment(time).tz(timezone).format("LT");
|
||||
let end_time = moment(time).tz(timezone).add(window.appointment_settings.appointment_duration, 'minutes');
|
||||
let end_time_string = end_time.format("LT");
|
||||
return `<span style="font-size: 1.2em;">${start_time_string}</span><br><span class="text-muted small">to ${end_time_string}</span>`;
|
||||
}
|
||||
|
||||
function select_time() {
|
||||
if (this.classList.contains('unavailable')) {
|
||||
return;
|
||||
}
|
||||
let selected_element = document.getElementsByClassName('selected');
|
||||
if (!(selected_element.length > 0)) {
|
||||
this.classList.add('selected');
|
||||
show_next_button();
|
||||
return;
|
||||
}
|
||||
selected_element = selected_element[0]
|
||||
window.selected_time = this.id;
|
||||
selected_element.classList.remove('selected');
|
||||
this.classList.add('selected');
|
||||
show_next_button();
|
||||
}
|
||||
|
||||
function set_default_timeslot() {
|
||||
let timeslots = document.getElementsByClassName('time-slot')
|
||||
// Can't use a forEach here since, we need to break the loop after a timeslot is selected
|
||||
for (let i = 0; i < timeslots.length; i++) {
|
||||
const timeslot = timeslots[i];
|
||||
if (!timeslot.classList.contains('unavailable')) {
|
||||
timeslot.classList.add('selected');
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function navigate_to_page(page_number) {
|
||||
let page1 = document.getElementById('select-date-time');
|
||||
let page2 = document.getElementById('enter-details');
|
||||
switch (page_number) {
|
||||
case 1:
|
||||
page1.style.display = 'block';
|
||||
page2.style.display = 'none';
|
||||
break;
|
||||
case 2:
|
||||
page1.style.display = 'none';
|
||||
page2.style.display = 'block';
|
||||
break;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
function setup_details_page() {
|
||||
navigate_to_page(2)
|
||||
let date_container = document.getElementsByClassName('date-span')[0];
|
||||
let time_container = document.getElementsByClassName('time-span')[0];
|
||||
date_container.innerHTML = moment(window.selected_date).format("MMM Do YYYY");
|
||||
time_container.innerHTML = moment(window.selected_time, "HH:mm:ss").format("LT");
|
||||
}
|
||||
|
||||
async function submit() {
|
||||
let button = document.getElementById('submit-button');
|
||||
button.disabled = true;
|
||||
let form = document.querySelector('#customer-form');
|
||||
if (!form.checkValidity()) {
|
||||
form.reportValidity();
|
||||
button.disabled = false;
|
||||
return;
|
||||
}
|
||||
let contact = get_form_data();
|
||||
let appointment = frappe.call({
|
||||
method: 'erpnext.www.book-appointment.index.create_appointment',
|
||||
args: {
|
||||
'date': window.selected_date,
|
||||
'time': window.selected_time,
|
||||
'contact': contact,
|
||||
'tz':window.selected_timezone
|
||||
},
|
||||
callback: (response)=>{
|
||||
if (response.message.status == "Unverified") {
|
||||
frappe.show_alert("Please check your email to confirm the appointment")
|
||||
} else {
|
||||
frappe.show_alert("Appointment Created Successfully");
|
||||
}
|
||||
setTimeout(()=>{
|
||||
let redirect_url = "/";
|
||||
if (window.appointment_settings.success_redirect_url){
|
||||
redirect_url += window.appointment_settings.success_redirect_url;
|
||||
}
|
||||
window.location.href = redirect_url;},5000)
|
||||
},
|
||||
error: (err)=>{
|
||||
frappe.show_alert("Something went wrong please try again");
|
||||
button.disabled = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function get_form_data() {
|
||||
contact = {};
|
||||
let inputs = ['name', 'skype', 'number', 'notes', 'email'];
|
||||
inputs.forEach((id) => contact[id] = document.getElementById(`customer_${id}`).value)
|
||||
return contact
|
||||
}
|
159
erpnext/www/book-appointment/index.py
Normal file
159
erpnext/www/book-appointment/index.py
Normal file
@ -0,0 +1,159 @@
|
||||
import frappe
|
||||
import datetime
|
||||
import json
|
||||
import pytz
|
||||
|
||||
|
||||
WEEKDAYS = ["Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday", "Sunday"]
|
||||
|
||||
no_cache = 1
|
||||
|
||||
|
||||
def get_context(context):
|
||||
is_enabled = frappe.db.get_single_value('Appointment Booking Settings', 'enable_scheduling')
|
||||
if is_enabled:
|
||||
return context
|
||||
else:
|
||||
frappe.local.flags.redirect_location = '/404'
|
||||
raise frappe.Redirect
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_appointment_settings():
|
||||
settings = frappe.get_doc('Appointment Booking Settings')
|
||||
settings.holiday_list = frappe.get_doc('Holiday List', settings.holiday_list)
|
||||
return settings
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_timezones():
|
||||
from babel.dates import get_timezone, get_timezone_name, Locale
|
||||
from frappe.utils.momentjs import get_all_timezones
|
||||
|
||||
translated_dict = {}
|
||||
locale = Locale.parse(frappe.local.lang, sep="-")
|
||||
|
||||
for tz in get_all_timezones():
|
||||
timezone_name = get_timezone_name(get_timezone(tz), locale=locale, width='short')
|
||||
if timezone_name:
|
||||
translated_dict[tz] = timezone_name + ' - ' + tz
|
||||
|
||||
return translated_dict
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_appointment_slots(date, timezone):
|
||||
# Convert query to local timezones
|
||||
format_string = '%Y-%m-%d %H:%M:%S'
|
||||
query_start_time = datetime.datetime.strptime(date + ' 00:00:00', format_string)
|
||||
query_end_time = datetime.datetime.strptime(date + ' 23:59:59', format_string)
|
||||
query_start_time = convert_to_system_timezone(timezone, query_start_time)
|
||||
query_end_time = convert_to_system_timezone(timezone, query_end_time)
|
||||
now = convert_to_guest_timezone(timezone, datetime.datetime.now())
|
||||
|
||||
# Database queries
|
||||
settings = frappe.get_doc('Appointment Booking Settings')
|
||||
holiday_list = frappe.get_doc('Holiday List', settings.holiday_list)
|
||||
timeslots = get_available_slots_between(query_start_time, query_end_time, settings)
|
||||
|
||||
# Filter and convert timeslots
|
||||
converted_timeslots = []
|
||||
for timeslot in timeslots:
|
||||
converted_timeslot = convert_to_guest_timezone(timezone, timeslot)
|
||||
# Check if holiday
|
||||
if _is_holiday(converted_timeslot.date(), holiday_list):
|
||||
converted_timeslots.append(dict(time=converted_timeslot, availability=False))
|
||||
continue
|
||||
# Check availability
|
||||
if check_availabilty(timeslot, settings) and converted_timeslot >= now:
|
||||
converted_timeslots.append(dict(time=converted_timeslot, availability=True))
|
||||
else:
|
||||
converted_timeslots.append(dict(time=converted_timeslot, availability=False))
|
||||
date_required = datetime.datetime.strptime(date + ' 00:00:00', format_string).date()
|
||||
converted_timeslots = filter_timeslots(date_required, converted_timeslots)
|
||||
return converted_timeslots
|
||||
|
||||
def get_available_slots_between(query_start_time, query_end_time, settings):
|
||||
records = _get_records(query_start_time, query_end_time, settings)
|
||||
timeslots = []
|
||||
appointment_duration = datetime.timedelta(
|
||||
minutes=settings.appointment_duration)
|
||||
for record in records:
|
||||
if record.day_of_week == WEEKDAYS[query_start_time.weekday()]:
|
||||
current_time = _deltatime_to_datetime(query_start_time, record.from_time)
|
||||
end_time = _deltatime_to_datetime(query_start_time, record.to_time)
|
||||
else:
|
||||
current_time = _deltatime_to_datetime(query_end_time, record.from_time)
|
||||
end_time = _deltatime_to_datetime(query_end_time, record.to_time)
|
||||
while current_time + appointment_duration <= end_time:
|
||||
timeslots.append(current_time)
|
||||
current_time += appointment_duration
|
||||
return timeslots
|
||||
|
||||
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def create_appointment(date, time, tz, contact):
|
||||
format_string = '%Y-%m-%d %H:%M:%S%z'
|
||||
scheduled_time = datetime.datetime.strptime(date + " " + time, format_string)
|
||||
# Strip tzinfo from datetime objects since it's handled by the doctype
|
||||
scheduled_time = scheduled_time.replace(tzinfo = None)
|
||||
scheduled_time = convert_to_system_timezone(tz, scheduled_time)
|
||||
scheduled_time = scheduled_time.replace(tzinfo = None)
|
||||
# Create a appointment document from form
|
||||
appointment = frappe.new_doc('Appointment')
|
||||
appointment.scheduled_time = scheduled_time
|
||||
contact = json.loads(contact)
|
||||
appointment.customer_name = contact.get('name', None)
|
||||
appointment.customer_phone_number = contact.get('number', None)
|
||||
appointment.customer_skype = contact.get('skype', None)
|
||||
appointment.customer_details = contact.get('notes', None)
|
||||
appointment.customer_email = contact.get('email', None)
|
||||
appointment.status = 'Open'
|
||||
appointment.insert()
|
||||
return appointment
|
||||
|
||||
# Helper Functions
|
||||
def filter_timeslots(date, timeslots):
|
||||
filtered_timeslots = []
|
||||
for timeslot in timeslots:
|
||||
if(timeslot['time'].date() == date):
|
||||
filtered_timeslots.append(timeslot)
|
||||
return filtered_timeslots
|
||||
|
||||
def convert_to_guest_timezone(guest_tz, datetimeobject):
|
||||
guest_tz = pytz.timezone(guest_tz)
|
||||
local_timezone = pytz.timezone(frappe.utils.get_time_zone())
|
||||
datetimeobject = local_timezone.localize(datetimeobject)
|
||||
datetimeobject = datetimeobject.astimezone(guest_tz)
|
||||
return datetimeobject
|
||||
|
||||
def convert_to_system_timezone(guest_tz,datetimeobject):
|
||||
guest_tz = pytz.timezone(guest_tz)
|
||||
datetimeobject = guest_tz.localize(datetimeobject)
|
||||
system_tz = pytz.timezone(frappe.utils.get_time_zone())
|
||||
datetimeobject = datetimeobject.astimezone(system_tz)
|
||||
return datetimeobject
|
||||
|
||||
def check_availabilty(timeslot, settings):
|
||||
return frappe.db.count('Appointment', {'scheduled_time': timeslot}) < settings.number_of_agents
|
||||
|
||||
def _is_holiday(date, holiday_list):
|
||||
for holiday in holiday_list.holidays:
|
||||
if holiday.holiday_date == date:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _get_records(start_time, end_time, settings):
|
||||
records = []
|
||||
for record in settings.availability_of_slots:
|
||||
if record.day_of_week == WEEKDAYS[start_time.weekday()] or record.day_of_week == WEEKDAYS[end_time.weekday()]:
|
||||
records.append(record)
|
||||
return records
|
||||
|
||||
|
||||
def _deltatime_to_datetime(date, deltatime):
|
||||
time = (datetime.datetime.min + deltatime).time()
|
||||
return datetime.datetime.combine(date.date(), time)
|
||||
|
||||
|
||||
def _datetime_to_deltatime(date_time):
|
||||
midnight = datetime.datetime.combine(date_time.date(), datetime.time.min)
|
||||
return (date_time-midnight)
|
18
erpnext/www/book-appointment/verify/index.html
Normal file
18
erpnext/www/book-appointment/verify/index.html
Normal file
@ -0,0 +1,18 @@
|
||||
{% extends "templates/web.html" %}
|
||||
|
||||
{% block title %}
|
||||
{{ _("Verify Email") }}
|
||||
{% endblock%}
|
||||
|
||||
{% block page_content %}
|
||||
|
||||
{% if success==True %}
|
||||
<div class="alert alert-success">
|
||||
Your email has been verified and your appointment has been scheduled
|
||||
</div>
|
||||
{% else %}
|
||||
<div class="alert alert-danger">
|
||||
Verification failed please check the link
|
||||
</div>
|
||||
{% endif %}
|
||||
{% endblock%}
|
20
erpnext/www/book-appointment/verify/index.py
Normal file
20
erpnext/www/book-appointment/verify/index.py
Normal file
@ -0,0 +1,20 @@
|
||||
import frappe
|
||||
|
||||
from frappe.utils.verified_command import verify_request
|
||||
@frappe.whitelist(allow_guest=True)
|
||||
def get_context(context):
|
||||
if not verify_request():
|
||||
context.success = False
|
||||
return context
|
||||
|
||||
email = frappe.form_dict['email']
|
||||
appointment_name = frappe.form_dict['appointment']
|
||||
|
||||
if email and appointment_name:
|
||||
appointment = frappe.get_doc('Appointment',appointment_name)
|
||||
appointment.set_verified(email)
|
||||
context.success = True
|
||||
return context
|
||||
else:
|
||||
context.success = False
|
||||
return context
|
Loading…
x
Reference in New Issue
Block a user