Merge branch 'develop' into develop
diff --git a/erpnext/crm/doctype/appointment/__init__.py b/erpnext/crm/doctype/appointment/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/__init__.py
diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js
new file mode 100644
index 0000000..975abfc
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.js
@@ -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)
+ })
+ }
+ }
+});
diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json
new file mode 100644
index 0000000..22df5c6
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.json
@@ -0,0 +1,132 @@
+{
+ "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",
+ "reqd": 1
+ },
+ {
+ "fieldname": "customer_skype",
+ "fieldtype": "Data",
+ "label": "Skype ID",
+ "reqd": 1
+ },
+ {
+ "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 Docs"
+ },
+ {
+ "fieldname": "col_br_3",
+ "fieldtype": "Column Break"
+ }
+ ],
+ "modified": "2019-09-23 10:57:04.876506",
+ "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
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py
new file mode 100644
index 0000000..2f14098
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/appointment.py
@@ -0,0 +1,167 @@
+# -*- 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.desk.form.assign_to import add as add_assignemnt
+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})
+ settings = frappe.get_doc('Appointment Booking Settings')
+ if(number_of_appointments_in_same_slot >= settings.number_of_agents):
+ frappe.throw('Time slot is not available')
+ # Link lead
+ self.lead = self.find_lead_by_email()
+
+ def after_insert(self):
+ # Auto assign
+ self.auto_assign()
+ # Check if lead was found
+ if(self.lead):
+ # Create Calendar event
+ self.create_calendar_event()
+ else:
+ # Set status to unverified
+ self.status = 'Unverified'
+ # Send email to confirm
+ verify_url = self.get_verify_url()
+ message = ''.join(['Please click the following link to confirm your appointment:',verify_url])
+ frappe.sendmail(recipients=[self.customer_email],
+ message=message,
+ subject=_('Appointment Confirmation'))
+ frappe.msgprint('Please check your email to confirm the appointment')
+
+ 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 on_update(self):
+ # Sync Calednar
+ 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()
+ # Remove unverified status
+ self.status = 'Open'
+ # Create calender event
+ self.create_calendar_event()
+ self.save(ignore_permissions=True)
+ frappe.db.commit()
+
+ def create_lead(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):
+ 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 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_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:
+ return frappe.get_doc('Employee', employee_docname[0][0]) # frappe.db.exists returns a tuple of a tuple
+ return None
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py
new file mode 100644
index 0000000..d529d37
--- /dev/null
+++ b/erpnext/crm/doctype/appointment/test_appointment.py
@@ -0,0 +1,52 @@
+# -*- 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():
+ if frappe.db.exists('Lead',filters={'lead_name':'Test Lead'}):
+ return
+ 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():
+ if frappe.db.exists('Appointment',filters={'email':'test@example.com'}):
+ return
+ 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):
+ test_lead = create_test_lead()
+ test_appointment = test_create_test_appointments()
+
+ def tearDown(self):
+ pass
+
+ def test_calendar_event_created(self):
+ cal_event = frappe.get_doc('Event',test_appointment.calendar_event)
+ self.assertEqual(cal_event.starts_on ,test_appointment.scheduled_time)
+
+ def test_lead_linked(self):
+ lead = frappe.get_doc('Lead',self.lead)
+ self.assertIsNotNone(lead)
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment_booking_settings/__init__.py b/erpnext/crm/doctype/appointment_booking_settings/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/__init__.py
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
new file mode 100644
index 0000000..2642e6e
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js
@@ -0,0 +1,13 @@
+// frappe.ui.form.on('Availability Of Slots', 'from_time', check_time)
+// frappe.ui.form.on('Availability Of Slots', 'to_time', check_time)
+
+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 Availability Of Slots : "To Time" must be later than "From Time"`));
+ }
+ });
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
new file mode 100644
index 0000000..d72f577
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json
@@ -0,0 +1,105 @@
+{
+ "creation": "2019-08-27 10:56:48.309824",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "availability_of_slots",
+ "number_of_agents",
+ "holiday_list",
+ "appointment_duration",
+ "email_reminders",
+ "advance_booking_days",
+ "agent_list"
+ ],
+ "fields": [
+ {
+ "fieldname": "availability_of_slots",
+ "fieldtype": "Table",
+ "label": "Availability Of Slots",
+ "options": "Availability Of Slots",
+ "reqd": 1
+ },
+ {
+ "default": "1",
+ "fieldname": "number_of_agents",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "No. Of Agents",
+ "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",
+ "fieldname": "email_reminders",
+ "fieldtype": "Check",
+ "label": "Email Reminders"
+ },
+ {
+ "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
+ }
+ ],
+ "issingle": 1,
+ "modified": "2019-09-19 12:36:34.011724",
+ "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
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
new file mode 100644
index 0000000..da181ae
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py
@@ -0,0 +1,35 @@
+# -*- 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):
+ def validate(self):
+ # Day of week should not be repeated
+ list_of_days = []
+ date = '01/01/1970 '
+ format_string = "%d/%m/%Y %H:%M:%S"
+
+ for record in self.availability_of_slots:
+ list_of_days.append(record.day_of_week)
+ # Difference between from_time and to_time is multiple of appointment_duration
+ from_time = datetime.datetime.strptime(date+record.from_time, format_string)
+ to_time = datetime.datetime.strptime(date+record.to_time, format_string)
+ timedelta = to_time-from_time
+
+ if(from_time > to_time):
+ frappe.throw('From Time cannot be later than To Time for '+record.day_of_week)
+
+ if timedelta.total_seconds() % (self.appointment_duration * 60):
+ frappe.throw('The difference between from time and To Time must be a multiple of Appointment ')
+
+ set_of_days = set(list_of_days)
+
+ if len(list_of_days) > len(set_of_days):
+ frappe.throw(_('Days of week must be unique'))
+
diff --git a/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
new file mode 100644
index 0000000..3dc3c39
--- /dev/null
+++ b/erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py
@@ -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
diff --git a/erpnext/crm/doctype/availability_of_slots/__init__.py b/erpnext/crm/doctype/availability_of_slots/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/__init__.py
diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json
new file mode 100644
index 0000000..b54af8d
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json
@@ -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
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py
new file mode 100644
index 0000000..8258471
--- /dev/null
+++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py
@@ -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
diff --git a/erpnext/crm/doctype/availabilty_of_slots/__init__.py b/erpnext/crm/doctype/availabilty_of_slots/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/__init__.py
diff --git a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json
new file mode 100644
index 0000000..d26f7ce
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json
@@ -0,0 +1,46 @@
+{
+ "creation": "2019-08-27 10:52:54.204677",
+ "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-08-27 10:52:54.204677",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Availabilty Of Slots",
+ "owner": "Administrator",
+ "permissions": [],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py b/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py
new file mode 100644
index 0000000..bd76480
--- /dev/null
+++ b/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py
@@ -0,0 +1,11 @@
+# -*- 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 AvailabiltyOfSlots(Document):
+ pass
diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json
index 3c22dc7..eb68c67 100644
--- a/erpnext/crm/doctype/lead/lead.json
+++ b/erpnext/crm/doctype/lead/lead.json
@@ -1,1436 +1,372 @@
{
- "allow_copy": 0,
"allow_events_in_timeline": 1,
- "allow_guest_to_view": 0,
"allow_import": 1,
- "allow_rename": 0,
"autoname": "naming_series:",
- "beta": 0,
"creation": "2013-04-10 11:45:37",
- "custom": 0,
- "docstatus": 0,
"doctype": "DocType",
"document_type": "Document",
- "editable_grid": 0,
+ "engine": "InnoDB",
+ "field_order": [
+ "organization_lead",
+ "lead_details",
+ "naming_series",
+ "lead_name",
+ "company_name",
+ "email_id",
+ "col_break123",
+ "lead_owner",
+ "status",
+ "gender",
+ "source",
+ "customer",
+ "campaign_name",
+ "image",
+ "section_break_12",
+ "contact_by",
+ "column_break_14",
+ "contact_date",
+ "ends_on",
+ "notes_section",
+ "notes",
+ "contact_info",
+ "address_desc",
+ "address_html",
+ "column_break2",
+ "contact_html",
+ "phone",
+ "salutation",
+ "mobile_no",
+ "fax",
+ "website",
+ "territory",
+ "more_info",
+ "type",
+ "market_segment",
+ "industry",
+ "request_type",
+ "column_break3",
+ "company",
+ "unsubscribed",
+ "blog_subscriber"
+ ],
"fields": [
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "organization_lead",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Lead is an Organization",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "lead_details",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "",
- "length": 0,
- "no_copy": 0,
- "options": "fa fa-user",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "fa fa-user"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "default": "",
- "fetch_if_empty": 0,
"fieldname": "naming_series",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Series",
- "length": 0,
"no_copy": 1,
"oldfieldname": "naming_series",
"oldfieldtype": "Select",
"options": "CRM-LEAD-.YYYY.-",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 1,
- "translatable": 0,
- "unique": 0
+ "set_only_once": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "lead_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
"in_global_search": 1,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Person Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "lead_name",
"oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "company_name",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Organization Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "company_name",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "oldfieldtype": "Data"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "email_id",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Email Address",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "email_id",
"oldfieldtype": "Data",
"options": "Email",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "col_break123",
"fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "50%"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "__user",
- "fetch_if_empty": 0,
"fieldname": "lead_owner",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
- "in_standard_filter": 0,
"label": "Lead Owner",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "lead_owner",
"oldfieldtype": "Link",
"options": "User",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"default": "Lead",
- "fetch_if_empty": 0,
"fieldname": "status",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
"in_list_view": 1,
"in_standard_filter": 1,
"label": "Status",
- "length": 0,
"no_copy": 1,
"oldfieldname": "status",
"oldfieldtype": "Select",
"options": "Lead\nOpen\nReplied\nOpportunity\nQuotation\nLost Quotation\nInterested\nConverted\nDo Not Contact",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
"reqd": 1,
- "search_index": 1,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "search_index": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "gender",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Gender",
- "length": 0,
- "no_copy": 0,
- "options": "Gender",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Gender"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "depends_on": "",
- "fetch_if_empty": 0,
"fieldname": "source",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Source",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "source",
"oldfieldtype": "Select",
- "options": "Lead Source",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Lead Source"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.source == 'Existing Customer'",
- "fetch_if_empty": 0,
"fieldname": "customer",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "From Customer",
- "length": 0,
"no_copy": 1,
"oldfieldname": "customer",
"oldfieldtype": "Link",
- "options": "Customer",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Customer"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval: doc.source==\"Campaign\"",
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "campaign_name",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Campaign Name",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "campaign_name",
"oldfieldtype": "Link",
- "options": "Campaign",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Campaign"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "image",
"fieldtype": "Attach Image",
"hidden": 1,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Image",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "print_hide": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "section_break_12",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Follow Up",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Follow Up"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "contact_by",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Next Contact By",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "contact_by",
"oldfieldtype": "Link",
"options": "User",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "100px"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break_14",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
"bold": 1,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "contact_date",
"fieldtype": "Datetime",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Next Contact Date",
- "length": 0,
"no_copy": 1,
"oldfieldname": "contact_date",
"oldfieldtype": "Date",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "100px"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
"bold": 1,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "ends_on",
"fieldtype": "Datetime",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Ends On",
- "length": 0,
- "no_copy": 1,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "no_copy": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
"collapsible": 1,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "notes_section",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notes",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Notes"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "notes",
"fieldtype": "Text Editor",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Notes",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Notes"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
"collapsible": 1,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "contact_info",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Address & Contact",
- "length": 0,
- "no_copy": 0,
"oldfieldtype": "Column Break",
- "options": "fa fa-map-marker",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "fa fa-map-marker"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.__islocal",
- "fetch_if_empty": 0,
"fieldname": "address_desc",
"fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Address Desc",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "print_hide": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "address_html",
"fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Address HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break2",
- "fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "fieldtype": "Column Break"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "contact_html",
"fieldtype": "HTML",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Contact HTML",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 1,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "read_only": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "phone",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Phone",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "contact_no",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "oldfieldtype": "Data"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "salutation",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Salutation",
- "length": 0,
- "no_copy": 0,
- "options": "Salutation",
- "permlevel": 0,
- "precision": "",
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Salutation"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "mobile_no",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Mobile No.",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "mobile_no",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "oldfieldtype": "Data"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
"depends_on": "eval:!doc.organization_lead",
- "fetch_if_empty": 0,
"fieldname": "fax",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Fax",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "fax",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "oldfieldtype": "Data"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "website",
"fieldtype": "Data",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Website",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "website",
- "oldfieldtype": "Data",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "oldfieldtype": "Data"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "description": "",
- "fetch_if_empty": 0,
"fieldname": "territory",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Territory",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "territory",
"oldfieldtype": "Link",
"options": "Territory",
- "permlevel": 0,
- "print_hide": 1,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "print_hide": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
"collapsible": 1,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "more_info",
"fieldtype": "Section Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "More Information",
- "length": 0,
- "no_copy": 0,
"oldfieldtype": "Section Break",
- "options": "fa fa-file-text",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "fa fa-file-text"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "type",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Lead Type",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "type",
"oldfieldtype": "Select",
- "options": "\nClient\nChannel Partner\nConsultant",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\nClient\nChannel Partner\nConsultant"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "market_segment",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Market Segment",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "market_segment",
"oldfieldtype": "Select",
- "options": "Market Segment",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Market Segment"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "industry",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Industry",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "industry",
"oldfieldtype": "Link",
- "options": "Industry Type",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "Industry Type"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "request_type",
"fieldtype": "Select",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Request Type",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "request_type",
"oldfieldtype": "Select",
- "options": "\nProduct Enquiry\nRequest for Information\nSuggestions\nOther",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "options": "\nProduct Enquiry\nRequest for Information\nSuggestions\nOther"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "column_break3",
"fieldtype": "Column Break",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "length": 0,
- "no_copy": 0,
"oldfieldtype": "Column Break",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0,
"width": "50%"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
"fieldname": "company",
"fieldtype": "Link",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
"label": "Company",
- "length": 0,
- "no_copy": 0,
"oldfieldname": "company",
"oldfieldtype": "Link",
"options": "Company",
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 1,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "remember_last_selected_value": 1
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "unsubscribed",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Unsubscribed",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Unsubscribed"
},
{
- "allow_bulk_edit": 0,
- "allow_in_quick_entry": 0,
- "allow_on_submit": 0,
- "bold": 0,
- "collapsible": 0,
- "columns": 0,
- "fetch_if_empty": 0,
+ "default": "0",
"fieldname": "blog_subscriber",
"fieldtype": "Check",
- "hidden": 0,
- "ignore_user_permissions": 0,
- "ignore_xss_filter": 0,
- "in_filter": 0,
- "in_global_search": 0,
- "in_list_view": 0,
- "in_standard_filter": 0,
- "label": "Blog Subscriber",
- "length": 0,
- "no_copy": 0,
- "permlevel": 0,
- "print_hide": 0,
- "print_hide_if_no_value": 0,
- "read_only": 0,
- "remember_last_selected_value": 0,
- "report_hide": 0,
- "reqd": 0,
- "search_index": 0,
- "set_only_once": 0,
- "translatable": 0,
- "unique": 0
+ "label": "Blog Subscriber"
}
],
- "has_web_view": 0,
- "hide_heading": 0,
- "hide_toolbar": 0,
"icon": "fa fa-user",
"idx": 5,
"image_field": "image",
- "image_view": 0,
- "in_create": 0,
- "is_submittable": 0,
- "issingle": 0,
- "istable": 0,
- "max_attachments": 0,
- "modified": "2019-06-18 03:22:57.283628",
+ "modified": "2019-09-19 12:49:02.536647",
"modified_by": "Administrator",
"module": "CRM",
"name": "Lead",
@@ -1438,128 +374,69 @@
"owner": "Administrator",
"permissions": [
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
"permlevel": 1,
- "print": 0,
"read": 1,
"report": 1,
- "role": "All",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "All"
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales User",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
"delete": 1,
"email": 1,
"export": 1,
- "if_owner": 0,
"import": 1,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "Sales Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
"create": 1,
- "delete": 0,
"email": 1,
- "export": 0,
- "if_owner": 0,
- "import": 0,
- "permlevel": 0,
"print": 1,
"read": 1,
"report": 1,
"role": "System Manager",
- "set_user_permissions": 0,
"share": 1,
- "submit": 0,
"write": 1
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
"permlevel": 1,
- "print": 0,
"read": 1,
"report": 1,
- "role": "Sales Manager",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Sales Manager"
},
{
- "amend": 0,
- "cancel": 0,
- "create": 0,
- "delete": 0,
- "email": 0,
- "export": 0,
- "if_owner": 0,
- "import": 0,
"permlevel": 1,
- "print": 0,
"read": 1,
"report": 1,
- "role": "Sales User",
- "set_user_permissions": 0,
- "share": 0,
- "submit": 0,
- "write": 0
+ "role": "Sales User"
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Guest",
+ "share": 1
}
],
- "quick_entry": 0,
- "read_only": 0,
"search_fields": "lead_name,lead_owner,status",
"show_name_in_global_search": 1,
"sort_field": "modified",
"sort_order": "DESC",
- "title_field": "lead_name",
- "track_changes": 0,
- "track_seen": 0,
- "track_views": 0
+ "title_field": "lead_name"
}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/timezone/__init__.py b/erpnext/crm/doctype/timezone/__init__.py
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/__init__.py
diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py
new file mode 100644
index 0000000..92a8889
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/test_timezone.py
@@ -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 TestTimezone(unittest.TestCase):
+ pass
diff --git a/erpnext/crm/doctype/timezone/timezone.js b/erpnext/crm/doctype/timezone/timezone.js
new file mode 100644
index 0000000..4dc57db
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.js
@@ -0,0 +1,8 @@
+// Copyright (c) 2019, Frappe Technologies Pvt. Ltd. and contributors
+// For license information, please see license.txt
+
+frappe.ui.form.on('Timezone', {
+ // refresh: function(frm) {
+
+ // }
+});
diff --git a/erpnext/crm/doctype/timezone/timezone.json b/erpnext/crm/doctype/timezone/timezone.json
new file mode 100644
index 0000000..b998e6c
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.json
@@ -0,0 +1,61 @@
+{
+ "autoname": "field:timezone_name",
+ "creation": "2019-08-27 11:39:30.328670",
+ "doctype": "DocType",
+ "editable_grid": 1,
+ "engine": "InnoDB",
+ "field_order": [
+ "offset",
+ "timezone_name"
+ ],
+ "fields": [
+ {
+ "fieldname": "offset",
+ "fieldtype": "Int",
+ "in_list_view": 1,
+ "label": "Offset In Minutes",
+ "reqd": 1
+ },
+ {
+ "fieldname": "timezone_name",
+ "fieldtype": "Data",
+ "in_list_view": 1,
+ "label": "Name",
+ "reqd": 1,
+ "unique": 1
+ }
+ ],
+ "modified": "2019-09-03 11:59:27.729561",
+ "modified_by": "Administrator",
+ "module": "CRM",
+ "name": "Timezone",
+ "name_case": "Title 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
+ },
+ {
+ "email": 1,
+ "export": 1,
+ "print": 1,
+ "read": 1,
+ "report": 1,
+ "role": "Guest",
+ "share": 1
+ }
+ ],
+ "quick_entry": 1,
+ "sort_field": "modified",
+ "sort_order": "DESC",
+ "track_changes": 1
+}
\ No newline at end of file
diff --git a/erpnext/crm/doctype/timezone/timezone.py b/erpnext/crm/doctype/timezone/timezone.py
new file mode 100644
index 0000000..539ffa2
--- /dev/null
+++ b/erpnext/crm/doctype/timezone/timezone.py
@@ -0,0 +1,15 @@
+# -*- 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 Timezone(Document):
+ def validate(self):
+ if self.offset > 720 or self.offset < -720:
+ frappe.throw('Timezone offsets must be between -720 and +720 minutes')
+ if frappe.db.exists({'doctype':'Timezone','offset':self.offset}):
+ frappe.throw('Timezone offsets need to be unique')
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css
new file mode 100644
index 0000000..a6e6313
--- /dev/null
+++ b/erpnext/www/book-appointment/index.css
@@ -0,0 +1,38 @@
+.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;
+}
+
+.time-slot:hover {
+ background: #ddd;
+}
+
+.time-slot.unavailable {
+ background: #CBD5E0;
+ cursor: not-allowed;
+ color: #718096
+}
+
+.time-slot.unavailable .text-muted {
+ color: #718096
+}
+
+input[type="radio"] {
+ visibility: hidden;
+ display: none;
+}
+
+.time-slot.selected {
+ color: white;
+ background: #5e64ff;
+}
+
+.time-slot.selected .text-muted {
+ color: #EDF2F7 !important;
+}
diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html
new file mode 100644
index 0000000..2e03213
--- /dev/null
+++ b/erpnext/www/book-appointment/index.html
@@ -0,0 +1,67 @@
+{% 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 mb-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 mt-3 col-md">
+ </select>
+ </div>
+ </div>
+ </div>
+ <div class="row mt-3" id="timeslot-container">
+
+ </div>
+ <div class="row justify-content-center mt-3">
+ <div class="col-md-4">
+ <button class="btn btn-primary form-control" id="next-button">Next</button>
+ </div>
+ </div>
+ </div>
+</div>
+<!--Enter Details-->
+<div id="enter-details">
+ <div class="text-center mb-5">
+ <h3>Add details</h3>
+ <p class="lead">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">
+ <input class="form-control mt-3" type="text" name="customer_name" id="customer_name" placeholder="Your Name"
+ required>
+ <input class="form-control mt-3" type="tel" name="customer_number" id="customer_number"
+ placeholder="Contact Number" required>
+ <input class="form-control mt-3" type="text" name="customer_skype" id="customer_skype" placeholder="Skype"
+ required>
+ <input class="form-control mt-3"type="email" name="customer_email" id="customer_email"
+ placeholder="Email Address">
+ <textarea class="form-control mt-3" name="customer_notes" id="customer_notes" cols="30" rows="10"
+ placeholder="Notes"></textarea>
+ <div class="row mt-3 ">
+ <div class="col-md"><button class="btn btn-dark form-control" onclick="initialise_select_date()">Go back</button></div>
+ <div class="col-md"><button class="btn btn-primary form-control " onclick="submit()" id="submit-button">Submit</button></div>
+ </div>
+ </div>
+ </div>
+</div>
+</div>
+
+{% endblock %}
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js
new file mode 100644
index 0000000..345e614
--- /dev/null
+++ b/erpnext/www/book-appointment/index.js
@@ -0,0 +1,222 @@
+
+frappe.ready(() => {
+ 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
+ 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 = (await frappe.call({
+ method: 'erpnext.www.book-appointment.index.get_holiday_list',
+ args: {
+ 'holiday_list_name': window.appointment_settings.holiday_list
+ }
+ })).message;
+}
+
+function setup_timezone_selector() {
+ let timezones_element = document.getElementById('appointment-timezone');
+ let offset = new Date().getTimezoneOffset();
+ window.timezones.forEach(timezone => {
+ let opt = document.createElement('option');
+ opt.value = timezone.offset;
+ opt.innerHTML = timezone.timezone_name;
+ opt.defaultSelected = (offset == timezone.offset)
+ 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) => {
+ // Add a break after each 8 elements
+ if (index % 8 == 0) {
+ let break_element = document.createElement('div');
+ break_element.classList.add('w-100');
+ timeslot_container.appendChild(break_element);
+ }
+ // 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');
+ timeslot_div.classList.add('col-md');
+ 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) {
+ time = new Date(time);
+ let start_time_string = moment(time).format("LT");
+ let end_time = moment(time).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() {
+ // form validation here
+ get_form_data();
+ let appointment = (await frappe.call({
+ method: 'erpnext.www.book-appointment.index.create_appointment',
+ args: {
+ 'date': window.selected_date,
+ 'time': window.selected_time,
+ 'contact': window.contact
+ }
+ })).message;
+ frappe.msgprint(__('Appointment Created Successfully'));
+ let button = document.getElementById('submit-button');
+ button.disabled = true;
+ button.onclick = null
+}
+
+function get_form_data() {
+ contact = {};
+ contact.name = document.getElementById('customer_name').value;
+ contact.number = document.getElementById('customer_number').value;
+ contact.skype = document.getElementById('customer_skype').value;
+ contact.notes = document.getElementById('customer_notes').value;
+ contact.email = document.getElementById('customer_email').value;
+ window.contact = contact
+}
diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py
new file mode 100644
index 0000000..49b3ffc
--- /dev/null
+++ b/erpnext/www/book-appointment/index.py
@@ -0,0 +1,150 @@
+import frappe
+import datetime
+import json
+
+
+WEEKDAYS = ["Monday", "Tuesday", "Wednesday",
+ "Thursday", "Friday", "Saturday", "Sunday"]
+
+no_cache = 1
+
+
+@frappe.whitelist(allow_guest=True)
+def get_appointment_settings():
+ settings = frappe.get_doc('Appointment Booking Settings')
+ return settings
+
+
+@frappe.whitelist(allow_guest=True)
+def get_holiday_list(holiday_list_name):
+ holiday_list = frappe.get_doc('Holiday List', holiday_list_name)
+ return holiday_list
+
+
+@frappe.whitelist(allow_guest=True)
+def get_timezones():
+ timezones = frappe.get_list('Timezone', fields='*')
+ return timezones
+
+
+@frappe.whitelist(allow_guest=True)
+def get_appointment_slots(date, timezone):
+ timezone = int(timezone)
+ 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_ist(query_start_time, timezone)
+ query_end_time = _convert_to_ist(query_end_time, timezone)
+ now = 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 timeslots based on date
+ converted_timeslots = []
+ for timeslot in timeslots:
+ # Check if holiday
+ if _is_holiday(timeslot.date(), holiday_list):
+ converted_timeslots.append(
+ dict(time=_convert_to_tz(timeslot, timezone), availability=False))
+ continue
+ # Check availability
+ if check_availabilty(timeslot, settings) and timeslot >= now:
+ converted_timeslots.append(
+ dict(time=_convert_to_tz(timeslot, timezone), availability=True))
+ else:
+ converted_timeslots.append(
+ dict(time=_convert_to_tz(timeslot, timezone), 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, contact):
+ appointment = frappe.new_doc('Appointment')
+ format_string = '%Y-%m-%d %H:%M:%S'
+ appointment.scheduled_time = datetime.datetime.strptime(
+ date+" "+time, format_string)
+ contact = json.loads(contact)
+ appointment.customer_name = contact['name']
+ appointment.customer_phone_number = contact['number']
+ appointment.customer_skype = contact['skype']
+ appointment.customer_details = contact['notes']
+ appointment.customer_email = contact['email']
+ appointment.status = 'Open'
+ appointment.insert()
+
+
+# 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 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)
+
+def _convert_to_ist(datetime_object, timezone):
+ offset = datetime.timedelta(minutes=timezone)
+ datetime_object = datetime_object + offset
+ offset = datetime.timedelta(minutes=-330)
+ datetime_object = datetime_object - offset
+ return datetime_object
+
+def _convert_to_tz(datetime_object, timezone):
+ offset = datetime.timedelta(minutes=timezone)
+ datetime_object = datetime_object - offset
+ offset = datetime.timedelta(minutes=-330)
+ datetime_object = datetime_object + offset
+ return datetime_object
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/verify/index.html b/erpnext/www/book-appointment/verify/index.html
new file mode 100644
index 0000000..ebb65b1
--- /dev/null
+++ b/erpnext/www/book-appointment/verify/index.html
@@ -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%}
\ No newline at end of file
diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py
new file mode 100644
index 0000000..86f9515
--- /dev/null
+++ b/erpnext/www/book-appointment/verify/index.py
@@ -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:
+ print('Something not found')
+ context.success = False
+ return context
\ No newline at end of file