From 7b9e30914fdd97fa3894c09e04be74aa11ff37af Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Wed, 28 Aug 2019 16:57:37 +0530 Subject: [PATCH 001/210] Added doctypes and portal pages --- erpnext/crm/doctype/appointment/__init__.py | 0 .../crm/doctype/appointment/appointment.js | 8 ++ .../crm/doctype/appointment/appointment.json | 91 +++++++++++++++++++ .../crm/doctype/appointment/appointment.py | 10 ++ .../doctype/appointment/test_appointment.py | 10 ++ .../appointment_booking_settings/__init__.py | 0 .../appointment_booking_settings.js | 18 ++++ .../appointment_booking_settings.json | 72 +++++++++++++++ .../appointment_booking_settings.py | 10 ++ .../test_appointment_booking_settings.py | 10 ++ .../doctype/availability_of_slots/__init__.py | 0 .../availability_of_slots.json | 46 ++++++++++ .../availability_of_slots.py | 10 ++ erpnext/crm/doctype/timezone/__init__.py | 0 erpnext/crm/doctype/timezone/test_timezone.py | 10 ++ erpnext/crm/doctype/timezone/timezone.js | 8 ++ erpnext/crm/doctype/timezone/timezone.json | 52 +++++++++++ erpnext/crm/doctype/timezone/timezone.py | 10 ++ erpnext/www/book-appointment/1.html | 31 +++++++ erpnext/www/book-appointment/1.js | 14 +++ erpnext/www/book-appointment/1.py | 17 ++++ erpnext/www/book-appointment/2.html | 85 +++++++++++++++++ erpnext/www/book-appointment/2.js | 27 ++++++ erpnext/www/book-appointment/2.py | 28 ++++++ erpnext/www/book-appointment/3.html | 22 +++++ erpnext/www/book-appointment/3.js | 11 +++ 26 files changed, 600 insertions(+) create mode 100644 erpnext/crm/doctype/appointment/__init__.py create mode 100644 erpnext/crm/doctype/appointment/appointment.js create mode 100644 erpnext/crm/doctype/appointment/appointment.json create mode 100644 erpnext/crm/doctype/appointment/appointment.py create mode 100644 erpnext/crm/doctype/appointment/test_appointment.py create mode 100644 erpnext/crm/doctype/appointment_booking_settings/__init__.py create mode 100644 erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js create mode 100644 erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json create mode 100644 erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py create mode 100644 erpnext/crm/doctype/appointment_booking_settings/test_appointment_booking_settings.py create mode 100644 erpnext/crm/doctype/availability_of_slots/__init__.py create mode 100644 erpnext/crm/doctype/availability_of_slots/availability_of_slots.json create mode 100644 erpnext/crm/doctype/availability_of_slots/availability_of_slots.py create mode 100644 erpnext/crm/doctype/timezone/__init__.py create mode 100644 erpnext/crm/doctype/timezone/test_timezone.py create mode 100644 erpnext/crm/doctype/timezone/timezone.js create mode 100644 erpnext/crm/doctype/timezone/timezone.json create mode 100644 erpnext/crm/doctype/timezone/timezone.py create mode 100644 erpnext/www/book-appointment/1.html create mode 100644 erpnext/www/book-appointment/1.js create mode 100644 erpnext/www/book-appointment/1.py create mode 100644 erpnext/www/book-appointment/2.html create mode 100644 erpnext/www/book-appointment/2.js create mode 100644 erpnext/www/book-appointment/2.py create mode 100644 erpnext/www/book-appointment/3.html create mode 100644 erpnext/www/book-appointment/3.js diff --git a/erpnext/crm/doctype/appointment/__init__.py b/erpnext/crm/doctype/appointment/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js new file mode 100644 index 0000000000..4e41047fa1 --- /dev/null +++ b/erpnext/crm/doctype/appointment/appointment.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('Appointment', { + // refresh: function(frm) { + + // } +}); diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json new file mode 100644 index 0000000000..24cbd92bc7 --- /dev/null +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -0,0 +1,91 @@ +{ + "autoname": "format:APMT-{appointment_date}-{####}", + "creation": "2019-08-27 10:48:27.926283", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "from_time", + "to_time", + "appointment_date", + "customer_details_section", + "customer_name", + "customer_phone_number", + "customer_skype", + "customer_details" + ], + "fields": [ + { + "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 Tme", + "reqd": 1 + }, + { + "fieldname": "appointment_date", + "fieldtype": "Date", + "in_list_view": 1, + "label": "Date ", + "reqd": 1 + }, + { + "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" + } + ], + "modified": "2019-08-27 12:43:30.143937", + "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 + } + ], + "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 0000000000..204b066031 --- /dev/null +++ b/erpnext/crm/doctype/appointment/appointment.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 Appointment(Document): + pass diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py new file mode 100644 index 0000000000..702ac7176f --- /dev/null +++ b/erpnext/crm/doctype/appointment/test_appointment.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 TestAppointment(unittest.TestCase): + pass 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 0000000000..e69de29bb2 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 0000000000..465df2c3a6 --- /dev/null +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -0,0 +1,18 @@ +// 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); + console.log(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"`)) + } + }) +} +// function check_times(frm, cdt, cdn) { + // let d = locals[cdt][cdn]; +// +// } \ 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 0000000000..ed6150a210 --- /dev/null +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -0,0 +1,72 @@ +{ + "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", + "email_reminders", + "appointment_duration" + ], + "fields": [ + { + "fieldname": "availability_of_slots", + "fieldtype": "Table", + "label": "Availability Of Slots", + "options": "Availability Of Slots", + "reqd": 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": "0", + "fieldname": "email_reminders", + "fieldtype": "Check", + "label": "Email Reminders" + }, + { + "default": "60", + "fieldname": "appointment_duration", + "fieldtype": "Int", + "label": "Appointment Duration", + "reqd": 1 + } + ], + "issingle": 1, + "modified": "2019-08-27 17:32:46.208951", + "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 + } + ], + "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 0000000000..33076366c1 --- /dev/null +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.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 AppointmentBookingSettings(Document): + pass 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 0000000000..3dc3c39971 --- /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 0000000000..e69de29bb2 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 0000000000..d26f7ced35 --- /dev/null +++ b/erpnext/crm/doctype/availability_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/availability_of_slots/availability_of_slots.py b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py new file mode 100644 index 0000000000..8258471eed --- /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/timezone/__init__.py b/erpnext/crm/doctype/timezone/__init__.py new file mode 100644 index 0000000000..e69de29bb2 diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py new file mode 100644 index 0000000000..92a8889cce --- /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 0000000000..4dc57db2ed --- /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 0000000000..9eb8ed9012 --- /dev/null +++ b/erpnext/crm/doctype/timezone/timezone.json @@ -0,0 +1,52 @@ +{ + "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-08-27 11:39:30.328670", + "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 + } + ], + "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 0000000000..20e7d378f7 --- /dev/null +++ b/erpnext/crm/doctype/timezone/timezone.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 Timezone(Document): + pass diff --git a/erpnext/www/book-appointment/1.html b/erpnext/www/book-appointment/1.html new file mode 100644 index 0000000000..db4ef26651 --- /dev/null +++ b/erpnext/www/book-appointment/1.html @@ -0,0 +1,31 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Book Appointment") }}{% endblock %} + +{% block page_content %} +
+ +
+

Book an appointment

+

Select the date and your timezone

+
+
+
+
+ + +
+ +
+
+
+ +{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/1.js b/erpnext/www/book-appointment/1.js new file mode 100644 index 0000000000..d05c2535c1 --- /dev/null +++ b/erpnext/www/book-appointment/1.js @@ -0,0 +1,14 @@ + +let holidays = []; +{% if holidays %} + holidays = {{holidays}} +{% endif %} + +function next() { + let date = document.getElementsByName('appointment-date')[0].value; + if(holidays.includes(date)){ + frappe.throw("That day is a holiday") + } + let tz = document.getElementsByName('appointment-timezone')[0].value; + window.location = `/book-appointment/2?date=${date}&tz=${tz}`; +} \ No newline at end of file diff --git a/erpnext/www/book-appointment/1.py b/erpnext/www/book-appointment/1.py new file mode 100644 index 0000000000..95169b9bf2 --- /dev/null +++ b/erpnext/www/book-appointment/1.py @@ -0,0 +1,17 @@ +import frappe + +def get_context(context): + settings = frappe.get_doc('Appointment Booking Settings') + holiday_list = frappe.get_doc('Holiday List',settings.holiday_list) + holidays = [] + for holiday in holiday_list.holidays: + print(str(holiday.holiday_date)) + holidays.append(str(holiday.holiday_date)) + context.holidays = holidays + context.from_date = holiday_list.from_date + context.to_date = holiday_list.to_date + timezones = frappe.get_all('Timezone',fields=["timezone_name","offset"]) + context.timezones = timezones + + return context + diff --git a/erpnext/www/book-appointment/2.html b/erpnext/www/book-appointment/2.html new file mode 100644 index 0000000000..198b12d67c --- /dev/null +++ b/erpnext/www/book-appointment/2.html @@ -0,0 +1,85 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Book Appointment") }}{% endblock %} + +{% block page_content %} + +
+
+ {% if is_holiday %} +

This day is a holiday

+ {% else %} +

Pick A Time Slot

+

Selected date is {{ date }}

+
+ + +
+
+
12 pm to 1 am
+
1 am to 2 am
+
2 am to 3 am
+
3 am to 4 am
+
4 am to 5 am
+
5 am to 6 am
+
6 am to 7 am
+
7 am to 8 am
+
+
+
8 am to 9 am
+
9 am to 10 am
+
10 am to 11 am
+
11 am to 12 am
+
12 am to 1 pm
+
1 pm to 2 pm
+
2 pm to 3 pm
+
3 pm to 4pm
+
+
+
4pm to 5pm
+
5 pm to 6 pm
+
6 pm to 7 pm
+
7 pm to 8 pm
+
8 pm to 9 pm
+
9 pm to 10 pm
+
10 pm to 11 pm
+
11 pm to 12 pm
+
+
+
+ +
+
+ {% endif %} +
+
+ +{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/2.js b/erpnext/www/book-appointment/2.js new file mode 100644 index 0000000000..bdcabdc5ef --- /dev/null +++ b/erpnext/www/book-appointment/2.js @@ -0,0 +1,27 @@ +let time_slot_divs = document.getElementsByClassName('time-slot'); + +function get_available_slots() { + frappe.db +} + +function select_time() { + if (this.classList.contains("unavailable")) { + return + } + console.log(this.id) + var selected = document.getElementsByClassName('selected')[0]; + selected.classList.remove('selected'); + this.classList.add('selected'); +} + +for (var i = 0; i < time_slot_divs.length; i++) { + time_slot_divs[i].addEventListener('click', select_time); +} + +function next() { + let urlParams = new URLSearchParams(window.location.search); + let date = urlParams.get("date"); + let tz = urlParams.get("tz"); + let time_slot = document.querySelector(".selected").id; + window.location.href = `/book-appointment/3?date=${date}&tz=${tz}&time=${time_slot}`; +} \ No newline at end of file diff --git a/erpnext/www/book-appointment/2.py b/erpnext/www/book-appointment/2.py new file mode 100644 index 0000000000..688545a77d --- /dev/null +++ b/erpnext/www/book-appointment/2.py @@ -0,0 +1,28 @@ +import frappe +import datetime + + +def get_context(context): + context.date = frappe.form_dict['date'] + settings = frappe.get_doc('Appointment Booking Settings') + holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) + if(is_holiday(context.date,holiday_list)): + context.is_holiday = True + return context + get_time_slots(context.date,settings) + # time_slots = get_time_slots(date) + return context + +def is_holiday(date,holiday_list): + for holiday in holiday_list.holidays: + if holiday.holiday_date.isoformat() == date: + print('matched') + return True + return False + + + +def _deltatime_to_time(deltatime): + return (datetime.datetime.min + deltatime).time() + +weekdays = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"] \ No newline at end of file diff --git a/erpnext/www/book-appointment/3.html b/erpnext/www/book-appointment/3.html new file mode 100644 index 0000000000..b627a0c9cf --- /dev/null +++ b/erpnext/www/book-appointment/3.html @@ -0,0 +1,22 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Book Appointment") }}{% endblock %} + +{% block page_content %} +
+ +
+

Add details

+

Selected date is {{ date }} at {{ time }}

+
+
+
+ + + + + +
+
+
+{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/3.js b/erpnext/www/book-appointment/3.js new file mode 100644 index 0000000000..23c55a3fce --- /dev/null +++ b/erpnext/www/book-appointment/3.js @@ -0,0 +1,11 @@ +function submit(){ + let params = new URLSearchParams(window.location.search); + const date = params.get('date'); + const time = params.get('time'); + const tz = params.get('tz'); + const customer_name = document.getElementById('customer_name').value; + const customer_number = document.getElementById('customer_number').value; + const customer_skype = document.getElementById('customer_skype').value; + const customer_notes = document.getElementById('customer_notes').value; + console.log({date,time,tz,customer_name,customer_number,customer_skype,customer_notes}); +} \ No newline at end of file From dbd72ea89d0a985dea8ef661d36eb23f2f2abcde Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Thu, 29 Aug 2019 16:56:19 +0530 Subject: [PATCH 002/210] Added time generation --- erpnext/www/book-appointment/2.html | 31 +--------- erpnext/www/book-appointment/2.js | 8 ++- erpnext/www/book-appointment/2.py | 87 +++++++++++++++++++++++++---- 3 files changed, 85 insertions(+), 41 deletions(-) diff --git a/erpnext/www/book-appointment/2.html b/erpnext/www/book-appointment/2.html index 198b12d67c..2a8c5c916c 100644 --- a/erpnext/www/book-appointment/2.html +++ b/erpnext/www/book-appointment/2.html @@ -42,34 +42,9 @@
-
12 pm to 1 am
-
1 am to 2 am
-
2 am to 3 am
-
3 am to 4 am
-
4 am to 5 am
-
5 am to 6 am
-
6 am to 7 am
-
7 am to 8 am
-
-
-
8 am to 9 am
-
9 am to 10 am
-
10 am to 11 am
-
11 am to 12 am
-
12 am to 1 pm
-
1 pm to 2 pm
-
2 pm to 3 pm
-
3 pm to 4pm
-
-
-
4pm to 5pm
-
5 pm to 6 pm
-
6 pm to 7 pm
-
7 pm to 8 pm
-
8 pm to 9 pm
-
9 pm to 10 pm
-
10 pm to 11 pm
-
11 pm to 12 pm
+ {% for timeslot in timeslots %} +
{{ timeslot.time.time().strftime('%H : %M') }}
+ {% endfor %}
diff --git a/erpnext/www/book-appointment/2.js b/erpnext/www/book-appointment/2.js index bdcabdc5ef..113564a722 100644 --- a/erpnext/www/book-appointment/2.js +++ b/erpnext/www/book-appointment/2.js @@ -9,8 +9,12 @@ function select_time() { return } console.log(this.id) - var selected = document.getElementsByClassName('selected')[0]; - selected.classList.remove('selected'); + try{ + selected_element = document.getElementsByClassName('selected')[0] + }catch(e){ + this.classList.add('selected') + } + selected_element.classList.remove('selected'); this.classList.add('selected'); } diff --git a/erpnext/www/book-appointment/2.py b/erpnext/www/book-appointment/2.py index 688545a77d..fa8aafac0b 100644 --- a/erpnext/www/book-appointment/2.py +++ b/erpnext/www/book-appointment/2.py @@ -3,26 +3,91 @@ import datetime def get_context(context): - context.date = frappe.form_dict['date'] + # Get query parameters + date = frappe.form_dict['date'] + tz = frappe.form_dict['tz'] + tz = int(tz) + # Database queries settings = frappe.get_doc('Appointment Booking Settings') holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) - if(is_holiday(context.date,holiday_list)): - context.is_holiday = True - return context - get_time_slots(context.date,settings) - # time_slots = get_time_slots(date) + # Format datetimes + format_string = '%Y-%m-%d %H:%M:%S' + start_time = datetime.datetime.strptime(date+' 00:00:00', format_string) + end_time = datetime.datetime.strptime(date+' 23:59:59', format_string) + # Convert to ist + start_time = _convert_to_ist(start_time, tz) + end_time = _convert_to_ist(end_time, tz) + timeslots = get_available_slots_between(start_time, end_time, settings) + converted_timeslots = [] + print('Appointments') + print(frappe.get_list('Appointment',fields=['from_time'])) + for timeslot in timeslots: + if timeslot > end_time or timeslot < start_time: + pass + else: + if frappe.db.count('Appointment',{'from_time':start_time.time()}) < settings.number_of_agents: + converted_timeslots.append(dict(time=_convert_to_tz(timeslot, tz), unavailable=False)) + else: + converted_timeslots.append(dict(time=_convert_to_tz(timeslot, tz),unavailable=True)) + + context.timeslots = converted_timeslots + context.date = date return context -def is_holiday(date,holiday_list): +def _is_holiday(date, holiday_list): for holiday in holiday_list.holidays: if holiday.holiday_date.isoformat() == date: - print('matched') return True return False +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 + +def get_available_slots_between(start_time_parameter, end_time_parameter, settings): + records = get_records(start_time_parameter, end_time_parameter, settings) + timeslots = [] + appointment_duration = datetime.timedelta( + minutes=settings.appointment_duration) + for record in records: + if record.day_of_week == weekdays[start_time_parameter.weekday()]: + current_time = _deltatime_to_datetime( + start_time_parameter, record.from_time) + end_time = _deltatime_to_datetime( + start_time_parameter, record.to_time) + elif record.day_of_week == weekdays[end_time_parameter.weekday()]: + current_time = _deltatime_to_datetime( + end_time_parameter, record.from_time) + end_time = _deltatime_to_datetime( + end_time_parameter, record.to_time) + while current_time + appointment_duration <= end_time: + timeslots.append(current_time) + current_time += appointment_duration + return timeslots -def _deltatime_to_time(deltatime): - return (datetime.datetime.min + deltatime).time() +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 -weekdays = ["Monday","Tuesday","Wednesday","Thursday","Friday","Saturday","Sunday"] \ No newline at end of file + +def _deltatime_to_datetime(date, deltatime): + time = (datetime.datetime.min + deltatime).time() + return datetime.datetime.combine(date.date(), time) + + +weekdays = ["Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday", "Sunday"] From 17906d5599d33c57033f68c060d24afb7eca505c Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Fri, 30 Aug 2019 10:49:07 +0530 Subject: [PATCH 003/210] Added polyfill for datepicker for Safari and IE support --- erpnext/public/js/date_polyfill.js | 1 + 1 file changed, 1 insertion(+) create mode 100644 erpnext/public/js/date_polyfill.js diff --git a/erpnext/public/js/date_polyfill.js b/erpnext/public/js/date_polyfill.js new file mode 100644 index 0000000000..6899d82291 --- /dev/null +++ b/erpnext/public/js/date_polyfill.js @@ -0,0 +1 @@ +(function(a,b){'object'==typeof exports&&'undefined'!=typeof module?b():'function'==typeof define&&define.amd?define(b):b()})(this,function(){'use strict';(function(a){if(a&&'undefined'!=typeof window){var b=document.createElement('style');return b.setAttribute('type','text/css'),b.innerHTML=a,document.head.appendChild(b),a}})('date-input-polyfill {\n background: #fff;\n color: #000;\n text-shadow: none;\n border: 0;\n padding: 0;\n height: auto;\n width: auto;\n line-height: normal;\n border-radius: 0;\n font-family: sans-serif;\n font-size: 14px;\n position: absolute !important;\n text-align: center;\n box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 0 12px 17px 2px rgba(0, 0, 0, 0.14), 0 5px 22px 4px rgba(0, 0, 0, 0.12);\n cursor: default;\n z-index: 1; }\n date-input-polyfill[data-open="false"] {\n display: none; }\n date-input-polyfill[data-open="true"] {\n display: block; }\n date-input-polyfill select, date-input-polyfill table, date-input-polyfill th, date-input-polyfill td {\n background: #fff;\n color: #000;\n text-shadow: none;\n border: 0;\n padding: 0;\n height: auto;\n width: auto;\n line-height: normal;\n border-radius: 0;\n font-family: sans-serif;\n font-size: 14px;\n box-shadow: none; }\n date-input-polyfill select, date-input-polyfill button {\n border: 0;\n border-bottom: 1px solid #E0E0E0;\n height: 24px;\n vertical-align: top; }\n date-input-polyfill select {\n width: 50%; }\n date-input-polyfill select:first-of-type {\n border-right: 1px solid #E0E0E0;\n width: 30%; }\n date-input-polyfill button {\n padding: 0;\n width: 20%;\n background: #E0E0E0; }\n date-input-polyfill table {\n border-collapse: collapse; }\n date-input-polyfill th, date-input-polyfill td {\n width: 32px;\n padding: 4px;\n text-align: center; }\n date-input-polyfill td[data-day] {\n cursor: pointer; }\n date-input-polyfill td[data-day]:hover {\n background: #E0E0E0; }\n date-input-polyfill [data-selected] {\n font-weight: bold;\n background: #D8EAF6; }\n\ninput[data-has-picker]::-ms-clear {\n display: none; }\n');var a=function(a,b){if(!(a instanceof b))throw new TypeError('Cannot call a class as a function')},b=function(){function a(a,b){for(var c,d=0;d'],b=0,d=this.input.localeText.days.length;b'+this.input.localeText.days[b]+'');this.daysHead.innerHTML=a.join(''),c.createRangeSelect(this.month,0,11,this.input.localeText.months,this.date.getMonth()),this.today.textContent=this.input.localeText.today}},{key:'refreshDaysMatrix',value:function(){this.refreshLocale();for(var a=this.date.getFullYear(),b=this.date.getMonth(),d=new Date(a,b,1).getDay(),e=new Date(this.date.getFullYear(),b+1,0).getDate(),f=c.absoluteDate(this.input.element.valueAsDate)||!1,g=f&&a===f.getFullYear()&&b===f.getMonth(),h=[],j=0;j')+'\n \n '),j+1<=d){h.push('');continue}var i=j+1-d,k=g&&f.getDate()===i;h.push('\n '+i+'\n ')}this.days.innerHTML=h.join('')}},{key:'pingInput',value:function(){var a,b;try{a=new Event('input'),b=new Event('change')}catch(c){a=document.createEvent('KeyboardEvent'),a.initEvent('input',!0,!1),b=document.createEvent('KeyboardEvent'),b.initEvent('change',!0,!1)}this.input.element.dispatchEvent(a),this.input.element.dispatchEvent(b)}}],[{key:'createRangeSelect',value:function(a,b,c,d,e){a.innerHTML='';for(var f,g=b;g<=c;++g){f=document.createElement('option'),a.appendChild(f);var h=d?d[g-b]:g;f.text=h,f.value=g,g===e&&(f.selected='selected')}return a}},{key:'absoluteDate',value:function(a){return a&&new Date(a.getTime()+1e3*(60*a.getTimezoneOffset()))}}]),c}();c.instance=null;var d={"en_en-US":{days:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],months:['January','February','March','April','May','June','July','August','September','October','November','December'],today:'Today',format:'M/D/Y'},"en-GB":{days:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],months:['January','February','March','April','May','June','July','August','September','October','November','December'],today:'Today',format:'D/M/Y'},"zh_zh-CN":{days:['\u661F\u671F\u5929','\u661F\u671F\u4E00','\u661F\u671F\u4E8C','\u661F\u671F\u4E09','\u661F\u671F\u56DB','\u661F\u671F\u4E94','\u661F\u671F\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"zh-Hans_zh-Hans-CN":{days:['\u5468\u65E5','\u5468\u4E00','\u5468\u4E8C','\u5468\u4E09','\u5468\u56DB','\u5468\u4E94','\u5468\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"zh-Hant_zh-Hant-TW":{days:['\u9031\u65E5','\u9031\u4E00','\u9031\u4E8C','\u9031\u4E09','\u9031\u56DB','\u9031\u4E94','\u9031\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"de_de-DE":{days:['So','Mo','Di','Mi','Do','Fr','Sa'],months:['Januar','Februar','M\xE4rz','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'],today:'Heute',format:'D.M.Y'},"da_da-DA":{days:['S\xF8n','Man','Tirs','Ons','Tors','Fre','L\xF8r'],months:['Januar','Februar','Marts','April','Maj','Juni','Juli','August','September','Oktober','November','December'],today:'I dag',format:'D/M/Y'},es:{days:['Dom','Lun','Mar','Mi\xE9','Jue','Vie','S\xE1b'],months:['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],today:'Hoy',format:'D/M/Y'},hi:{days:['\u0930\u0935\u093F','\u0938\u094B\u092E','\u092E\u0902\u0917\u0932','\u092C\u0941\u0927','\u0917\u0941\u0930\u0941','\u0936\u0941\u0915\u094D\u0930','\u0936\u0928\u093F'],months:['\u091C\u0928\u0935\u0930\u0940','\u092B\u0930\u0935\u0930\u0940','\u092E\u093E\u0930\u094D\u091A','\u0905\u092A\u094D\u0930\u0947\u0932','\u092E\u0948','\u091C\u0942\u0928','\u091C\u0942\u0932\u093E\u0908','\u0905\u0917\u0938\u094D\u0924','\u0938\u093F\u0924\u092E\u094D\u092C\u0930','\u0906\u0915\u094D\u091F\u094B\u092C\u0930','\u0928\u0935\u092E\u094D\u092C\u0930','\u0926\u093F\u0938\u092E\u094D\u092C\u0930'],today:'\u0906\u091C',format:'D/M/Y'},pt:{days:['Dom','Seg','Ter','Qua','Qui','Sex','S\xE1b'],months:['Janeiro','Fevereiro','Mar\xE7o','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],today:'Hoje',format:'D/M/Y'},ja:{days:['\u65E5','\u6708','\u706B','\u6C34','\u6728','\u91D1','\u571F'],months:['1\u6708','2\u6708','3\u6708','4\u6708','5\u6708','6\u6708','7\u6708','8\u6708','9\u6708','10\u6708','11\u6708','12\u6708'],today:'\u4ECA\u65E5',format:'Y/M/D'},"nl_nl-NL_nl-BE":{days:['Zondag','Maandag','Dinsdag','Woensdag','Donderdag','Vrijdag','Zaterdag'],months:['Januari','Februari','Maart','April','Mei','Juni','Juli','Augustus','September','Oktober','November','December'],today:'Vandaag',format:'D/M/Y'},"tr_tr-TR":{days:['Pzr','Pzt','Sal','\xC7r\u015F','Pr\u015F','Cum','Cmt'],months:['Ocak','\u015Eubat','Mart','Nisan','May\u0131s','Haziran','Temmuz','A\u011Fustos','Eyl\xFCl','Ekim','Kas\u0131m','Aral\u0131k'],today:'Bug\xFCn',format:'D/M/Y'},"fr_fr-FR":{days:['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'],months:['Janvier','F\xE9vrier','Mars','Avril','Mai','Juin','Juillet','Ao\xFBt','Septembre','Octobre','Novembre','D\xE9cembre'],today:'Auj.',format:'D/M/Y'},"uk_uk-UA":{days:['\u041D\u0434','\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431'],months:['\u0421\u0456\u0447\u0435\u043D\u044C','\u041B\u044E\u0442\u0438\u0439','\u0411\u0435\u0440\u0435\u0437\u0435\u043D\u044C','\u041A\u0432\u0456\u0442\u0435\u043D\u044C','\u0422\u0440\u0430\u0432\u0435\u043D\u044C','\u0427\u0435\u0440\u0432\u0435\u043D\u044C','\u041B\u0438\u043F\u0435\u043D\u044C','\u0421\u0435\u0440\u043F\u0435\u043D\u044C','\u0412\u0435\u0440\u0435\u0441\u0435\u043D\u044C','\u0416\u043E\u0432\u0442\u0435\u043D\u044C','\u041B\u0438\u0441\u0442\u043E\u043F\u0430\u0434','\u0413\u0440\u0443\u0434\u0435\u043D\u044C'],today:'\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456',format:'D.M.Y'},it:{days:['Dom','Lun','Mar','Mer','Gio','Ven','Sab'],months:['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','ottobre','Novembre','Dicembre'],today:'Oggi',format:'D/M/Y'},pl:{days:['Nie','Pon','Wto','\u015Aro','Czw','Pt','Sob'],months:['Stycze\u0144','Luty','Marzec','Kwiecie\u0144','Maj','Czerwiec','Lipiec','Sierpie\u0144','Wrzesie\u0144','Pa\u017Adziernik','Listopad','Grudzie\u0144'],today:'Dzisiaj',format:'D.M.Y'},cs:{days:['Po','\xDAt','St','\u010Ct','P\xE1','So','Ne'],months:['Leden','\xDAnor','B\u0159ezen','Duben','Kv\u011Bten','\u010Cerven','\u010Cervenec','Srpen','Z\xE1\u0159\xED','\u0158\xEDjen','Listopad','Prosinec'],today:'Dnes',format:'D.M.Y'},ru:{days:['\u0412\u0441','\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431'],months:['\u042F\u043D\u0432\u0430\u0440\u044C','\u0424\u0435\u0432\u0440\u0430\u043B\u044C','\u041C\u0430\u0440\u0442','\u0410\u043F\u0440\u0435\u043B\u044C','\u041C\u0430\u0439','\u0418\u044E\u043D\u044C','\u0418\u044E\u043B\u044C','\u0410\u0432\u0433\u0443\u0441\u0442','\u0421\u0435\u043D\u0442\u044F\u0431\u0440\u044C','\u041E\u043A\u0442\u044F\u0431\u0440\u044C','\u041D\u043E\u044F\u0431\u0440\u044C','\u0414\u0435\u043A\u0430\u0431\u0440\u044C'],today:'\u0421\u0435\u0433\u043E\u0434\u043D\u044F',format:'D.M.Y'}},e=function(){function e(b){var d=this;a(this,e),this.element=b,this.element.setAttribute('data-has-picker','');for(var f=this.element,g='';f.parentNode&&(g=f.getAttribute('lang'),!g);)f=f.parentNode;this.locale=g||'en',this.localeText=this.getLocaleText(),Object.defineProperties(this.element,{value:{get:function(){return d.element.polyfillValue},set:function(a){if(!/^\d{4}-\d{2}-\d{2}$/.test(a))return d.element.polyfillValue='',d.element.setAttribute('value',''),!1;d.element.polyfillValue=a;var b=a.split('-');d.element.setAttribute('value',d.localeText.format.replace('Y',b[0]).replace('M',b[1]).replace('D',b[2]))}},valueAsDate:{get:function(){return d.element.polyfillValue?new Date(d.element.polyfillValue):null},set:function(a){d.element.value=a.toISOString().slice(0,10)}},valueAsNumber:{get:function(){return d.element.value?d.element.valueAsDate.getTime():NaN},set:function(a){d.element.valueAsDate=new Date(a)}}}),this.element.value=this.element.getAttribute('value');var h=function(){c.instance.attachTo(d)};this.element.addEventListener('focus',h),this.element.addEventListener('mousedown',h),this.element.addEventListener('mouseup',h),this.element.addEventListener('keydown',function(a){var b=new Date;switch(a.keyCode){case 27:c.instance.hide();break;case 38:d.element.valueAsDate&&(b.setDate(d.element.valueAsDate.getDate()+1),d.element.valueAsDate=b,c.instance.pingInput());break;case 40:d.element.valueAsDate&&(b.setDate(d.element.valueAsDate.getDate()-1),d.element.valueAsDate=b,c.instance.pingInput());break;default:}c.instance.sync()})}return b(e,[{key:'getLocaleText',value:function(){var a=this.locale.toLowerCase();for(var b in d){var c=b.split('_').map(function(a){return a.toLowerCase()});if(!!~c.indexOf(a))return d[b]}for(var e in d){var f=e.split('_').map(function(a){return a.toLowerCase()});if(!!~f.indexOf(a.substr(0,2)))return d[e]}return this.locale='en',this.getLocaleText()}}],[{key:'supportsDateInput',value:function(){var a=document.createElement('input');a.setAttribute('type','date');var b='not-a-date';return a.setAttribute('value',b),document.currentScript&&!document.currentScript.hasAttribute('data-nodep-date-input-polyfill-debug')&&a.value!==b}},{key:'addPickerToDateInputs',value:function(){var a=document.querySelectorAll('input[type="date"]:not([data-has-picker]):not([readonly])'),b=a.length;if(!b)return!1;for(var c=0;c Date: Fri, 30 Aug 2019 10:49:43 +0530 Subject: [PATCH 004/210] Better date validation --- erpnext/www/book-appointment/1.js | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/www/book-appointment/1.js b/erpnext/www/book-appointment/1.js index d05c2535c1..9f6c487241 100644 --- a/erpnext/www/book-appointment/1.js +++ b/erpnext/www/book-appointment/1.js @@ -1,4 +1,5 @@ +{% include 'erpnext/public/js/date_polyfill.js' %} let holidays = []; {% if holidays %} holidays = {{holidays}} @@ -9,6 +10,16 @@ function next() { if(holidays.includes(date)){ frappe.throw("That day is a holiday") } + if(date === ""){ + frappe.throw("Please select a date") + } let tz = document.getElementsByName('appointment-timezone')[0].value; window.location = `/book-appointment/2?date=${date}&tz=${tz}`; +} + +function ondatechange(){ + let date = document.getElementById('appointment-date') + if(holidays.includes(date.value)){ + frappe.throw("That day is a holiday") + } } \ No newline at end of file From 828fea6d66fc15eec19c27ed4bcc65f24154f913 Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Fri, 30 Aug 2019 10:49:57 +0530 Subject: [PATCH 005/210] formatting and date validation --- erpnext/www/book-appointment/1.html | 9 ++++++++- 1 file changed, 8 insertions(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/1.html b/erpnext/www/book-appointment/1.html index db4ef26651..da4fb25919 100644 --- a/erpnext/www/book-appointment/1.html +++ b/erpnext/www/book-appointment/1.html @@ -12,7 +12,14 @@
- + - -
- -
-
-
- -{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/1.js b/erpnext/www/book-appointment/1.js deleted file mode 100644 index 9f6c487241..0000000000 --- a/erpnext/www/book-appointment/1.js +++ /dev/null @@ -1,25 +0,0 @@ - -{% include 'erpnext/public/js/date_polyfill.js' %} -let holidays = []; -{% if holidays %} - holidays = {{holidays}} -{% endif %} - -function next() { - let date = document.getElementsByName('appointment-date')[0].value; - if(holidays.includes(date)){ - frappe.throw("That day is a holiday") - } - if(date === ""){ - frappe.throw("Please select a date") - } - let tz = document.getElementsByName('appointment-timezone')[0].value; - window.location = `/book-appointment/2?date=${date}&tz=${tz}`; -} - -function ondatechange(){ - let date = document.getElementById('appointment-date') - if(holidays.includes(date.value)){ - frappe.throw("That day is a holiday") - } -} \ No newline at end of file diff --git a/erpnext/www/book-appointment/1.py b/erpnext/www/book-appointment/1.py deleted file mode 100644 index 95169b9bf2..0000000000 --- a/erpnext/www/book-appointment/1.py +++ /dev/null @@ -1,17 +0,0 @@ -import frappe - -def get_context(context): - settings = frappe.get_doc('Appointment Booking Settings') - holiday_list = frappe.get_doc('Holiday List',settings.holiday_list) - holidays = [] - for holiday in holiday_list.holidays: - print(str(holiday.holiday_date)) - holidays.append(str(holiday.holiday_date)) - context.holidays = holidays - context.from_date = holiday_list.from_date - context.to_date = holiday_list.to_date - timezones = frappe.get_all('Timezone',fields=["timezone_name","offset"]) - context.timezones = timezones - - return context - diff --git a/erpnext/www/book-appointment/2.html b/erpnext/www/book-appointment/2.html deleted file mode 100644 index 2a8c5c916c..0000000000 --- a/erpnext/www/book-appointment/2.html +++ /dev/null @@ -1,60 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %}{{ _("Book Appointment") }}{% endblock %} - -{% block page_content %} - -
-
- {% if is_holiday %} -

This day is a holiday

- {% else %} -

Pick A Time Slot

-

Selected date is {{ date }}

-
- - -
-
- {% for timeslot in timeslots %} -
{{ timeslot.time.time().strftime('%H : %M') }}
- {% endfor %} -
-
-
- -
-
- {% endif %} -
-
- -{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/2.js b/erpnext/www/book-appointment/2.js deleted file mode 100644 index 113564a722..0000000000 --- a/erpnext/www/book-appointment/2.js +++ /dev/null @@ -1,31 +0,0 @@ -let time_slot_divs = document.getElementsByClassName('time-slot'); - -function get_available_slots() { - frappe.db -} - -function select_time() { - if (this.classList.contains("unavailable")) { - return - } - console.log(this.id) - try{ - selected_element = document.getElementsByClassName('selected')[0] - }catch(e){ - this.classList.add('selected') - } - selected_element.classList.remove('selected'); - this.classList.add('selected'); -} - -for (var i = 0; i < time_slot_divs.length; i++) { - time_slot_divs[i].addEventListener('click', select_time); -} - -function next() { - let urlParams = new URLSearchParams(window.location.search); - let date = urlParams.get("date"); - let tz = urlParams.get("tz"); - let time_slot = document.querySelector(".selected").id; - window.location.href = `/book-appointment/3?date=${date}&tz=${tz}&time=${time_slot}`; -} \ No newline at end of file diff --git a/erpnext/www/book-appointment/2.py b/erpnext/www/book-appointment/2.py deleted file mode 100644 index fa8aafac0b..0000000000 --- a/erpnext/www/book-appointment/2.py +++ /dev/null @@ -1,93 +0,0 @@ -import frappe -import datetime - - -def get_context(context): - # Get query parameters - date = frappe.form_dict['date'] - tz = frappe.form_dict['tz'] - tz = int(tz) - # Database queries - settings = frappe.get_doc('Appointment Booking Settings') - holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) - # Format datetimes - format_string = '%Y-%m-%d %H:%M:%S' - start_time = datetime.datetime.strptime(date+' 00:00:00', format_string) - end_time = datetime.datetime.strptime(date+' 23:59:59', format_string) - # Convert to ist - start_time = _convert_to_ist(start_time, tz) - end_time = _convert_to_ist(end_time, tz) - timeslots = get_available_slots_between(start_time, end_time, settings) - converted_timeslots = [] - print('Appointments') - print(frappe.get_list('Appointment',fields=['from_time'])) - for timeslot in timeslots: - if timeslot > end_time or timeslot < start_time: - pass - else: - if frappe.db.count('Appointment',{'from_time':start_time.time()}) < settings.number_of_agents: - converted_timeslots.append(dict(time=_convert_to_tz(timeslot, tz), unavailable=False)) - else: - converted_timeslots.append(dict(time=_convert_to_tz(timeslot, tz),unavailable=True)) - - context.timeslots = converted_timeslots - context.date = date - return context - -def _is_holiday(date, holiday_list): - for holiday in holiday_list.holidays: - if holiday.holiday_date.isoformat() == date: - return True - return False - -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 - -def get_available_slots_between(start_time_parameter, end_time_parameter, settings): - records = get_records(start_time_parameter, end_time_parameter, settings) - timeslots = [] - appointment_duration = datetime.timedelta( - minutes=settings.appointment_duration) - for record in records: - if record.day_of_week == weekdays[start_time_parameter.weekday()]: - current_time = _deltatime_to_datetime( - start_time_parameter, record.from_time) - end_time = _deltatime_to_datetime( - start_time_parameter, record.to_time) - elif record.day_of_week == weekdays[end_time_parameter.weekday()]: - current_time = _deltatime_to_datetime( - end_time_parameter, record.from_time) - end_time = _deltatime_to_datetime( - end_time_parameter, record.to_time) - while current_time + appointment_duration <= end_time: - timeslots.append(current_time) - current_time += appointment_duration - return timeslots - - -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) - - -weekdays = ["Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday", "Sunday"] diff --git a/erpnext/www/book-appointment/3.html b/erpnext/www/book-appointment/3.html deleted file mode 100644 index b627a0c9cf..0000000000 --- a/erpnext/www/book-appointment/3.html +++ /dev/null @@ -1,22 +0,0 @@ -{% extends "templates/web.html" %} - -{% block title %}{{ _("Book Appointment") }}{% endblock %} - -{% block page_content %} -
- -
-

Add details

-

Selected date is {{ date }} at {{ time }}

-
-
-
- - - - - -
-
-
-{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/3.js b/erpnext/www/book-appointment/3.js deleted file mode 100644 index 23c55a3fce..0000000000 --- a/erpnext/www/book-appointment/3.js +++ /dev/null @@ -1,11 +0,0 @@ -function submit(){ - let params = new URLSearchParams(window.location.search); - const date = params.get('date'); - const time = params.get('time'); - const tz = params.get('tz'); - const customer_name = document.getElementById('customer_name').value; - const customer_number = document.getElementById('customer_number').value; - const customer_skype = document.getElementById('customer_skype').value; - const customer_notes = document.getElementById('customer_notes').value; - console.log({date,time,tz,customer_name,customer_number,customer_skype,customer_notes}); -} \ 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 0000000000..3ffe996238 --- /dev/null +++ b/erpnext/www/book-appointment/index.css @@ -0,0 +1,25 @@ +.time-slot { + margin: 0 0; + border: 0.5px solid #cccccc; + min-height: 100px; +} + +.time-slot:hover { + background: #ddd; +} + +.time-slot.unavailable { + background: #bbb; + + color: #777777 +} + +input[type="radio"] { + visibility: hidden; + display: none; +} + +.time-slot.selected { + color: white; + background: #5e64ff; +} \ No newline at end of file diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html new file mode 100644 index 0000000000..b705f9e82d --- /dev/null +++ b/erpnext/www/book-appointment/index.html @@ -0,0 +1,70 @@ +{% extends "templates/web.html" %} + +{% block title %}{{ _("Book Appointment") }}{% endblock %} + +{% block page_content %} +
+ +
+
+

Book an appointment

+

Select the date and your timezone

+
+
+
+
+ + +
+ +
+
+
+ + +
+
+

Pick A Time Slot

+

Selected date is Date Span

+
+
+
+ +
+
+
+ +
+
+
+
+ + +
+
+

Add details

+

Selected date is Date Span at time

+
+
+
+ + + + + +
+
+
+
+ +{% 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 0000000000..1482c511d4 --- /dev/null +++ b/erpnext/www/book-appointment/index.js @@ -0,0 +1,146 @@ + +frappe.ready(() => { + initialise_select_date() +}) +var holiday_list = []; + +function navigator(page_no) { + let select_date_div = document.getElementById('select-date'); + select_date_div.style.display = 'none'; + let select_time_div = document.getElementById('select-time'); + select_time_div.style.display = 'none'; + let contact_details_div = document.getElementById('enter-details'); + contact_details_div.style.display = 'none'; + let page; + switch (page_no) { + case 1: page = select_date_div; break; + case 2: page = select_time_div; break; + case 3: page = contact_details_div; break; + } + page.style.display = 'block' +} + +// Page 1 +async function initialise_select_date() { + navigator(1); + let timezones, settings; + settings = (await frappe.call({ + method: 'erpnext.www.book-appointment.index.get_appointment_settings' + })).message + timezones = (await frappe.call({ + method: 'erpnext.www.book-appointment.index.get_timezones' + })).message; + holiday_list = (await frappe.call({ + method: 'erpnext.www.book-appointment.index.get_holiday_list', + args: { + 'holiday_list_name': settings.holiday_list + } + })).message; + let date_picker = document.getElementById('appointment-date'); + date_picker.max = holiday_list.to_date; + date_picker.min = holiday_list.from_date; + date_picker.value = (new Date()).toISOString().substr(0, 10); + let timezones_element = document.getElementById('appointment-timezone'); + var offset = new Date().getTimezoneOffset(); + timezones.forEach(timezone => { + var opt = document.createElement('option'); + opt.value = timezone.offset; + opt.innerHTML = timezone.timezone_name; + opt.defaultSelected = (offset == timezone.offset) + timezones_element.appendChild(opt) + }); +} + +function validate_date() { + let date_picker = document.getElementById('appointment-date'); + if (date_picker.value === '') { + frappe.throw('Please select a date') + } +} + +// Page 2 +async function navigate_to_time_select() { + navigator(2); + timezone = document.getElementById('appointment-timezone').value + date = document.getElementById('appointment-date').value; + var date_spans = document.getElementsByClassName('date-span'); + for (var i = 0; i < date_spans.length; i++) date_spans[i].innerHTML = date; + // date_span.addEventListener('click',initialise_select_date) + // date_span.style.color = '#5e64ff'; + // date_span.style.textDecoration = 'underline'; + // date_span.style.cursor = 'pointer'; + var slots = (await frappe.call({ + method: 'erpnext.www.book-appointment.index.get_appointment_slots', + args: { + date: date, + timezone: timezone + } + })).message; + let timeslot_container = document.getElementById('timeslot-container'); + console.log(slots) + if (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); + } + for (let i = 0; i < slots.length; i++) { + const slot = slots[i]; + var timeslot_div = document.createElement('div'); + timeslot_div.classList.add('time-slot'); + timeslot_div.classList.add('col-md'); + if (!slot.availability) { + timeslot_div.classList.add('unavailable') + } + timeslot_div.innerHTML = slot.time.substr(11, 20); + timeslot_div.id = slot.time.substr(11, 20); + timeslot_container.appendChild(timeslot_div); + } + set_default_timeslot() + let time_slot_divs = document.getElementsByClassName('time-slot'); + for (var i = 0; i < time_slot_divs.length; i++) { + time_slot_divs[i].addEventListener('click', select_time); + } +} + +function select_time() { + if (this.classList.contains("unavailable")) { + return + } + try { + selected_element = document.getElementsByClassName('selected')[0] + } catch (e) { + this.classList.add("selected") + } + selected_element.classList.remove("selected"); + this.classList.add("selected"); +} + +function set_default_timeslot() { + let timeslots = document.getElementsByClassName('time-slot') + for (let i = 0; i < timeslots.length; i++) { + const timeslot = timeslots[i]; + if (!timeslot.classList.contains('unavailable')) { + timeslot.classList.add("selected"); + break; + } + } +} + +function initialise_enter_details() { + navigator(3); + let time_div = document.getElementsByClassName('selected')[0]; + let time_span = document.getElementsByClassName('time-span')[0]; + time_span.innerHTML = time_div.id +} + +function submit() { + var date = document.getElementById('appointment-date').value; + var time = document.getElementsByClassName('selected')[0].id; + 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; + console.log({ date, time, contact }); +} diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py new file mode 100644 index 0000000000..15d5f9a49d --- /dev/null +++ b/erpnext/www/book-appointment/index.py @@ -0,0 +1,123 @@ +import frappe +import datetime + +@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) + # 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): + 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.frappe.get_doc('Appointment') + appointment.scheduled_time = date + +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}) Date: Tue, 3 Sep 2019 12:58:12 +0530 Subject: [PATCH 010/210] A --- .../crm/doctype/appointment/appointment.py | 9 +++++-- .../appointment_booking_settings.json | 2 +- .../appointment_booking_settings.py | 24 +++++++++++++++++-- 3 files changed, 30 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 204b066031..cce6a1d684 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -3,8 +3,13 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe from frappe.model.document import Document class Appointment(Document): - pass + def validate(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') + diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index d54b568c34..cf27f770c2 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -48,7 +48,7 @@ } ], "issingle": 1, - "modified": "2019-09-01 10:20:06.935115", + "modified": "2019-09-03 12:27:09.763730", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 33076366c1..8f1fb14f5b 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -3,8 +3,28 @@ # For license information, please see license.txt from __future__ import unicode_literals -# import frappe +import frappe +from frappe import _ +import datetime from frappe.model.document import Document class AppointmentBookingSettings(Document): - pass + 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')) + From c5b2a5866904c8426e6e5eb314b1033a9a94e86d Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Tue, 3 Sep 2019 14:16:47 +0530 Subject: [PATCH 011/210] Added submit fucntionality --- erpnext/www/book-appointment/index.js | 13 +++++++++++-- erpnext/www/book-appointment/index.py | 17 ++++++++++++----- 2 files changed, 23 insertions(+), 7 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 1482c511d4..e1a2338bfd 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -80,7 +80,7 @@ async function navigate_to_time_select() { console.log(slots) if (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); } @@ -134,7 +134,7 @@ function initialise_enter_details() { time_span.innerHTML = time_div.id } -function submit() { +async function submit() { var date = document.getElementById('appointment-date').value; var time = document.getElementsByClassName('selected')[0].id; contact = {}; @@ -143,4 +143,13 @@ function submit() { contact.skype = document.getElementById('customer_skype').value; contact.notes = document.getElementById('customer_notes').value; console.log({ date, time, contact }); + let abc = (await frappe.call({ + method: 'erpnext.www.book-appointment.index.create_appointment', + args: { + 'date': date, + 'time': time, + 'contact': contact + } + })).message; + console.log(abc) } diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 15d5f9a49d..340f3adb67 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -1,5 +1,6 @@ import frappe import datetime +import json @frappe.whitelist(allow_guest=True) def get_appointment_settings(): @@ -68,10 +69,18 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date,time,contact): - - appointment = frappe.frappe.get_doc('Appointment') - appointment.scheduled_time = date + 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_no = contact['number'] + appointment.customer_skype = contact['skype'] + appointment.customer_details = contact['notes'] + appointment.insert() + +# Helper Functions def filter_timeslots(date,timeslots): filtered_timeslots = [] for timeslot in timeslots: @@ -82,8 +91,6 @@ def filter_timeslots(date,timeslots): def check_availabilty(timeslot,settings): return frappe.db.count('Appointment',{'scheduled_time':timeslot}) Date: Tue, 3 Sep 2019 14:16:56 +0530 Subject: [PATCH 012/210] changed Autoname --- erpnext/crm/doctype/appointment/appointment.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 435607f99a..aee16f799f 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -1,5 +1,5 @@ { - "autoname": "format:APMT-{appointment_date}-{####}", + "autoname": "format:APMT-{scheduled_time}-{####}", "creation": "2019-08-27 10:48:27.926283", "doctype": "DocType", "editable_grid": 1, @@ -48,7 +48,7 @@ "reqd": 1 } ], - "modified": "2019-09-01 10:19:50.711989", + "modified": "2019-09-03 14:07:16.837591", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From 217aadba7e40169ac27e72ac38eb811d1df0e1d5 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 14:43:41 +0530 Subject: [PATCH 013/210] Better autoname --- erpnext/crm/doctype/appointment/appointment.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index aee16f799f..ec63420e98 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -1,5 +1,5 @@ { - "autoname": "format:APMT-{scheduled_time}-{####}", + "autoname": "format:APMT-{customer_name}-{####}", "creation": "2019-08-27 10:48:27.926283", "doctype": "DocType", "editable_grid": 1, @@ -48,7 +48,7 @@ "reqd": 1 } ], - "modified": "2019-09-03 14:07:16.837591", + "modified": "2019-09-09 12:23:33.611408", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From 48e43e2421dda3ae29d115af6b2d326062730f99 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 14:43:55 +0530 Subject: [PATCH 014/210] build fix --- .../doctype/availabilty_of_slots/__init__.py | 0 .../availability_of_slots.json | 46 +++++++++++++++++++ .../availabilty_of_slots.py | 10 ++++ 3 files changed, 56 insertions(+) create mode 100644 erpnext/crm/doctype/availabilty_of_slots/__init__.py create mode 100644 erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json create mode 100644 erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py 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 0000000000..e69de29bb2 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 0000000000..d26f7ced35 --- /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 0000000000..62436b8da7 --- /dev/null +++ b/erpnext/crm/doctype/availabilty_of_slots/availabilty_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 AvailabiltyOfSlots(Document): + pass \ No newline at end of file From 63dbacd7c034e9b8bc94d283e4509cdfdea054fe Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 15:19:57 +0530 Subject: [PATCH 015/210] Disabled caching --- erpnext/www/book-appointment/index.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 340f3adb67..e853a35fff 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -2,6 +2,8 @@ import frappe import datetime import json +no_cache = 1 + @frappe.whitelist(allow_guest=True) def get_appointment_settings(): settings = frappe.get_doc('Appointment Booking Settings') From 10711dd09daeddee84375b8d7663943daba73271 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 15:41:20 +0530 Subject: [PATCH 016/210] Refactor UI --- .../crm/doctype/appointment/appointment.json | 8 +- erpnext/www/book-appointment/index.css | 23 ++- erpnext/www/book-appointment/index.html | 85 ++++---- erpnext/www/book-appointment/index.js | 181 +++++++++++------- 4 files changed, 171 insertions(+), 126 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index ec63420e98..8392549fd3 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -28,12 +28,14 @@ { "fieldname": "customer_phone_number", "fieldtype": "Data", - "label": "Phone Number" + "label": "Phone Number", + "reqd": 1 }, { "fieldname": "customer_skype", "fieldtype": "Data", - "label": "Skype ID" + "label": "Skype ID", + "reqd": 1 }, { "fieldname": "customer_details", @@ -48,7 +50,7 @@ "reqd": 1 } ], - "modified": "2019-09-09 12:23:33.611408", + "modified": "2019-09-09 15:40:21.881421", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index 3ffe996238..a6e6313f79 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -1,7 +1,12 @@ .time-slot { - margin: 0 0; + margin-bottom: 2em; + margin-left: 0.5em; + margin-right: 0.5em; + border-radius: 0.4em; + cursor: pointer; border: 0.5px solid #cccccc; - min-height: 100px; + min-height: 75px; + padding: 0.5em 1em; } .time-slot:hover { @@ -9,9 +14,13 @@ } .time-slot.unavailable { - background: #bbb; + background: #CBD5E0; + cursor: not-allowed; + color: #718096 +} - color: #777777 +.time-slot.unavailable .text-muted { + color: #718096 } input[type="radio"] { @@ -22,4 +31,8 @@ input[type="radio"] { .time-slot.selected { color: white; background: #5e64ff; -} \ No newline at end of file +} + +.time-slot.selected .text-muted { + color: #EDF2F7 !important; +} diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index b705f9e82d..b915484f54 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -2,69 +2,60 @@ {% block title %}{{ _("Book Appointment") }}{% endblock %} +{% block script %} + + +{% endblock %} + {% block page_content %}
-
+

Book an appointment

-

Select the date and your timezone

+

Select the date and your timezone

-
-
- - + -
- -
-
-
- - -
-
-

Pick A Time Slot

-

Selected date is Date Span

-
-
-
- -
-
-
-
-
- - -
-
-

Add details

-

Selected date is Date Span at time

+
+
-
- - - - - +
+
+ +
+
+

Add details

+

Selected date is at +

+
+
+
+ + + + + +
+
+
+
{% endblock %} \ No newline at end of file diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index e1a2338bfd..bb21ddf273 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -2,47 +2,35 @@ frappe.ready(() => { initialise_select_date() }) -var holiday_list = []; +window.holiday_list = []; -function navigator(page_no) { - let select_date_div = document.getElementById('select-date'); - select_date_div.style.display = 'none'; - let select_time_div = document.getElementById('select-time'); - select_time_div.style.display = 'none'; - let contact_details_div = document.getElementById('enter-details'); - contact_details_div.style.display = 'none'; - let page; - switch (page_no) { - case 1: page = select_date_div; break; - case 2: page = select_time_div; break; - case 3: page = contact_details_div; break; - } - page.style.display = 'block' +async function initialise_select_date() { + document.getElementById('enter-details').style.display = 'none'; + await get_global_variables(); + setup_date_picker(); + setup_timezone_selector(); + hide_next_button(); } -// Page 1 -async function initialise_select_date() { - navigator(1); - let timezones, settings; - settings = (await frappe.call({ +async function get_global_variables() { + window.appointment_settings = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_appointment_settings' })).message - timezones = (await frappe.call({ + window.timezones = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_timezones' })).message; - holiday_list = (await frappe.call({ + window.holiday_list = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_holiday_list', args: { - 'holiday_list_name': settings.holiday_list + 'holiday_list_name': window.appointment_settings.holiday_list } })).message; - let date_picker = document.getElementById('appointment-date'); - date_picker.max = holiday_list.to_date; - date_picker.min = holiday_list.from_date; - date_picker.value = (new Date()).toISOString().substr(0, 10); +} + +function setup_timezone_selector() { let timezones_element = document.getElementById('appointment-timezone'); var offset = new Date().getTimezoneOffset(); - timezones.forEach(timezone => { + window.timezones.forEach(timezone => { var opt = document.createElement('option'); opt.value = timezone.offset; opt.innerHTML = timezone.timezone_name; @@ -51,56 +39,90 @@ async function initialise_select_date() { }); } -function validate_date() { +function setup_date_picker() { let date_picker = document.getElementById('appointment-date'); - if (date_picker.value === '') { - frappe.throw('Please select a date') - } + let today = new Date(); + date_picker.min = today.toISOString().substr(0, 10); + date_picker.max = window.holiday_list.to_date; } -// Page 2 -async function navigate_to_time_select() { - navigator(2); - timezone = document.getElementById('appointment-timezone').value - date = document.getElementById('appointment-date').value; - var date_spans = document.getElementsByClassName('date-span'); - for (var i = 0; i < date_spans.length; i++) date_spans[i].innerHTML = date; - // date_span.addEventListener('click',initialise_select_date) - // date_span.style.color = '#5e64ff'; - // date_span.style.textDecoration = 'underline'; - // date_span.style.cursor = 'pointer'; - var slots = (await frappe.call({ +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); +} + +async function get_time_slots(date, timezone) { + debugger + let slots = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_appointment_slots', args: { date: date, timezone: timezone } })).message; - let timeslot_container = document.getElementById('timeslot-container'); - console.log(slots) - if (slots.length <= 0) { - let message_div = document.createElement('p'); + 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 } - for (let i = 0; i < slots.length; i++) { - const slot = slots[i]; + window.slots.forEach(slot => { + let start_time = new Date(slot.time) var timeslot_div = document.createElement('div'); timeslot_div.classList.add('time-slot'); timeslot_div.classList.add('col-md'); if (!slot.availability) { timeslot_div.classList.add('unavailable') } - timeslot_div.innerHTML = slot.time.substr(11, 20); + timeslot_div.innerHTML = get_slot_layout(start_time); timeslot_div.id = slot.time.substr(11, 20); + timeslot_div.addEventListener('click', select_time); timeslot_container.appendChild(timeslot_div); + }); + set_default_timeslot(); + show_next_button(); +} + +function clear_time_slots() { + let timeslot_container = document.getElementById('timeslot-container'); + while (timeslot_container.firstChild) { + timeslot_container.removeChild(timeslot_container.firstChild) } - set_default_timeslot() - let time_slot_divs = document.getElementsByClassName('time-slot'); - for (var i = 0; i < time_slot_divs.length; i++) { - time_slot_divs[i].addEventListener('click', select_time); - } +} + +function get_slot_layout(time) { + time = new Date(time) + let start_time_string = moment(time).format("LT"); + let end_time = moment(time).add('1','hours'); + let end_time_string = end_time.format("LT"); + return `${start_time_string}
to ${end_time_string}`; } function select_time() { @@ -110,8 +132,10 @@ function select_time() { try { selected_element = document.getElementsByClassName('selected')[0] } catch (e) { + debugger this.classList.add("selected") } + window.selected_time = this.id selected_element.classList.remove("selected"); this.classList.add("selected"); } @@ -127,23 +151,23 @@ function set_default_timeslot() { } } -function initialise_enter_details() { - navigator(3); - let time_div = document.getElementsByClassName('selected')[0]; - let time_span = document.getElementsByClassName('time-span')[0]; - time_span.innerHTML = time_div.id +function setup_details_page(){ + let page1 = document.getElementById('select-date-time'); + let page2 = document.getElementById('enter-details'); + page1.style.display = 'none'; + page2.style.display = 'block'; + + let date_container = document.getElementsByClassName('date-span')[0]; + let time_container = document.getElementsByClassName('time-span')[0]; + + date_container.innerHTML = new Date(window.selected_date).toLocaleDateString(); + time_container.innerHTML = moment(window.selected_time,"HH:mm:ss").format("LT"); } async function submit() { - var date = document.getElementById('appointment-date').value; - var time = document.getElementsByClassName('selected')[0].id; - 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; - console.log({ date, time, contact }); - let abc = (await frappe.call({ + // form validation here + form_validation(); + let appointment = (await frappe.call({ method: 'erpnext.www.book-appointment.index.create_appointment', args: { 'date': date, @@ -151,5 +175,20 @@ async function submit() { 'contact': contact } })).message; - console.log(abc) + frappe.msgprint(__('Appointment Created Successfully')); + let button = document.getElementById('submit-button'); + button.disabled = true; + button.onclick = () => { console.log('This should never have happened') } } + +function form_validation(){ + var date = window.selected_date; + var time = document.getElementsByClassName('selected')[0].id; + 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; + window.contact = contact + console.log({ date, time, contact }); +} From 5945144c08e00966a4cdbeb662e29be9d0952c0b Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 16:35:48 +0530 Subject: [PATCH 017/210] Added tests --- .../doctype/appointment/test_appointment.py | 41 ++++++++++++++++++- 1 file changed, 39 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 702ac7176f..e446712d01 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -3,8 +3,45 @@ # See license.txt from __future__ import unicode_literals -# import frappe +import frappe import unittest +import datetime + + +def create_appointments(number): + for i in range(1, number): + frappe.get_doc({ + 'doctype': 'Appointment', + 'scheduled_time': datetime.datetime.min, + 'customer_name': 'Test Customer'+str(i), + 'customer_phone_number': '8088', + 'customer_skype': 'test'+str(i), + }) + class TestAppointment(unittest.TestCase): - pass + def setUp(self): + settings = frappe.get_doc('Appointment Booking Settings') + create_appointments(settings.number_of_agents) + frappe.get_doc({ + 'doctype': 'Appointment', + 'scheduled_time': datetime.datetime.min, + 'customer_name': 'Extra Customer', + 'customer_phone_number': '8088', + 'customer_skype': 'extra_customer', + }) + + def tearDown(self): + delete_appointments() + + def delete_appointments(self): + doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) + for doc in doc_list: + doc.delete() + + def test_number_of_appointments(self): + settings = frappe.get_doc('Appointment Booking Settings') + self.assertLessEqual(frappe.db.count('Apoointment', + filters={'scheduled_time': datetime.datetime.min, 'customer_name':}), + settings.number_of_agents, + "Number of appointments exceed number of agents") From 20c7c290fa0c5564d5c4b226203152948db3b458 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 16:36:01 +0530 Subject: [PATCH 018/210] Formatting --- erpnext/www/book-appointment/index.js | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index bb21ddf273..5bc8af0bde 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -156,10 +156,8 @@ function setup_details_page(){ let page2 = document.getElementById('enter-details'); page1.style.display = 'none'; page2.style.display = 'block'; - let date_container = document.getElementsByClassName('date-span')[0]; let time_container = document.getElementsByClassName('time-span')[0]; - date_container.innerHTML = new Date(window.selected_date).toLocaleDateString(); time_container.innerHTML = moment(window.selected_time,"HH:mm:ss").format("LT"); } From db21f86b260f7ef68a06adfa736eee522f734431 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 17:01:40 +0530 Subject: [PATCH 019/210] Removed unneccessary doctype --- .../doctype/availabilty_of_slots/__init__.py | 0 .../availability_of_slots.json | 46 ------------------- .../availabilty_of_slots.py | 10 ---- erpnext/www/book-appointment/index.js | 9 ++-- erpnext/www/book-appointment/index.py | 2 +- 5 files changed, 6 insertions(+), 61 deletions(-) delete mode 100644 erpnext/crm/doctype/availabilty_of_slots/__init__.py delete mode 100644 erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json delete mode 100644 erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py diff --git a/erpnext/crm/doctype/availabilty_of_slots/__init__.py b/erpnext/crm/doctype/availabilty_of_slots/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json deleted file mode 100644 index d26f7ced35..0000000000 --- a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json +++ /dev/null @@ -1,46 +0,0 @@ -{ - "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 deleted file mode 100644 index 62436b8da7..0000000000 --- a/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- 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 \ No newline at end of file diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 5bc8af0bde..b2df3b4382 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -165,12 +165,13 @@ function setup_details_page(){ async function submit() { // form validation here form_validation(); + debugger; let appointment = (await frappe.call({ method: 'erpnext.www.book-appointment.index.create_appointment', args: { - 'date': date, - 'time': time, - 'contact': contact + 'date': window.selected_date, + 'time': window.selected_time, + 'contact': window.contact } })).message; frappe.msgprint(__('Appointment Created Successfully')); @@ -181,7 +182,7 @@ async function submit() { function form_validation(){ var date = window.selected_date; - var time = document.getElementsByClassName('selected')[0].id; + var time = window.selected_time; contact = {}; contact.name = document.getElementById('customer_name').value; contact.number = document.getElementById('customer_number').value; diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index e853a35fff..9c37fb0c99 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -76,7 +76,7 @@ def create_appointment(date,time,contact): appointment.scheduled_time = datetime.datetime.strptime(date+" "+time,format_string) contact = json.loads(contact) appointment.customer_name = contact['name'] - appointment.customer_phone_no = contact['number'] + appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] appointment.insert() From 110f4ea0c9b3b9121697f008026c6da668230311 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 9 Sep 2019 17:04:25 +0530 Subject: [PATCH 020/210] Formatting --- .../availability_of_slots.py | 3 +- erpnext/www/book-appointment/index.html | 15 ++-- erpnext/www/book-appointment/index.py | 72 ++++++++++++------- 3 files changed, 57 insertions(+), 33 deletions(-) diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py index 8258471eed..94fb0c94d6 100644 --- a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py +++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py @@ -6,5 +6,6 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document + class AvailabilityOfSlots(Document): - pass + pass diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index b915484f54..f4074270e0 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -3,8 +3,8 @@ {% block title %}{{ _("Book Appointment") }}{% endblock %} {% block script %} - - + + {% endblock %} {% block page_content %} @@ -18,15 +18,16 @@
- - +
-
- +
+
diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 9c37fb0c99..f4e96b47d6 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -4,50 +4,62 @@ import json 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) + 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='*') + timezones = frappe.get_list('Timezone', fields='*') return timezones + @frappe.whitelist(allow_guest=True) -def get_appointment_slots(date,timezone): +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) + 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) # 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) - + 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)) + 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): - converted_timeslots.append(dict(time=_convert_to_tz(timeslot,timezone),availability=True)) + if check_availabilty(timeslot, settings): + 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) + 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 = [] @@ -59,7 +71,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings): query_start_time, record.from_time) end_time = _deltatime_to_datetime( query_start_time, record.to_time) - else : + else: current_time = _deltatime_to_datetime( query_end_time, record.from_time) end_time = _deltatime_to_datetime( @@ -69,11 +81,13 @@ def get_available_slots_between(query_start_time, query_end_time, settings): current_time += appointment_duration return timeslots -@frappe.whitelist(allow_guest=True) -def create_appointment(date,time,contact): + +@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) + 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'] @@ -83,15 +97,17 @@ def create_appointment(date,time,contact): # Helper Functions -def filter_timeslots(date,timeslots): +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}) Date: Mon, 9 Sep 2019 17:09:03 +0530 Subject: [PATCH 021/210] added doctype --- .../doctype/availabilty_of_slots/__init__.py | 0 .../availability_of_slots.json | 46 +++++++++++++++++++ .../availabilty_of_slots.py | 11 +++++ 3 files changed, 57 insertions(+) create mode 100644 erpnext/crm/doctype/availabilty_of_slots/__init__.py create mode 100644 erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json create mode 100644 erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py 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 0000000000..e69de29bb2 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 0000000000..d26f7ced35 --- /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 0000000000..bd764806ba --- /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 From 5c211d8abfb764ad52f53accad8ffe15b0a4893d Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 10 Sep 2019 11:52:55 +0530 Subject: [PATCH 022/210] fixed codacy --- erpnext/crm/doctype/appointment/appointment.json | 10 +++++++++- .../appointment_booking_settings.js | 13 ++++--------- erpnext/public/js/date_polyfill.js | 1 - 3 files changed, 13 insertions(+), 11 deletions(-) delete mode 100644 erpnext/public/js/date_polyfill.js diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 8392549fd3..356cbea2cc 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -6,6 +6,7 @@ "engine": "InnoDB", "field_order": [ "scheduled_time", + "status", "customer_details_section", "customer_name", "customer_phone_number", @@ -48,9 +49,16 @@ "in_list_view": 1, "label": "Scheduled Time", "reqd": 1 + }, + { + "fieldname": "status", + "fieldtype": "Select", + "label": "Status", + "options": "Open\nClosed", + "reqd": 1 } ], - "modified": "2019-09-09 15:40:21.881421", + "modified": "2019-09-10 11:17:20.200603", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index 465df2c3a6..2642e6eb26 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -1,18 +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) +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); - console.log(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"`)) + frappe.throw(__(`In row ${i + 1} of Availability Of Slots : "To Time" must be later than "From Time"`)); } - }) -} -// function check_times(frm, cdt, cdn) { - // let d = locals[cdt][cdn]; -// -// } \ No newline at end of file + }); +} \ No newline at end of file diff --git a/erpnext/public/js/date_polyfill.js b/erpnext/public/js/date_polyfill.js deleted file mode 100644 index 6899d82291..0000000000 --- a/erpnext/public/js/date_polyfill.js +++ /dev/null @@ -1 +0,0 @@ -(function(a,b){'object'==typeof exports&&'undefined'!=typeof module?b():'function'==typeof define&&define.amd?define(b):b()})(this,function(){'use strict';(function(a){if(a&&'undefined'!=typeof window){var b=document.createElement('style');return b.setAttribute('type','text/css'),b.innerHTML=a,document.head.appendChild(b),a}})('date-input-polyfill {\n background: #fff;\n color: #000;\n text-shadow: none;\n border: 0;\n padding: 0;\n height: auto;\n width: auto;\n line-height: normal;\n border-radius: 0;\n font-family: sans-serif;\n font-size: 14px;\n position: absolute !important;\n text-align: center;\n box-shadow: 0 7px 8px -4px rgba(0, 0, 0, 0.2), 0 12px 17px 2px rgba(0, 0, 0, 0.14), 0 5px 22px 4px rgba(0, 0, 0, 0.12);\n cursor: default;\n z-index: 1; }\n date-input-polyfill[data-open="false"] {\n display: none; }\n date-input-polyfill[data-open="true"] {\n display: block; }\n date-input-polyfill select, date-input-polyfill table, date-input-polyfill th, date-input-polyfill td {\n background: #fff;\n color: #000;\n text-shadow: none;\n border: 0;\n padding: 0;\n height: auto;\n width: auto;\n line-height: normal;\n border-radius: 0;\n font-family: sans-serif;\n font-size: 14px;\n box-shadow: none; }\n date-input-polyfill select, date-input-polyfill button {\n border: 0;\n border-bottom: 1px solid #E0E0E0;\n height: 24px;\n vertical-align: top; }\n date-input-polyfill select {\n width: 50%; }\n date-input-polyfill select:first-of-type {\n border-right: 1px solid #E0E0E0;\n width: 30%; }\n date-input-polyfill button {\n padding: 0;\n width: 20%;\n background: #E0E0E0; }\n date-input-polyfill table {\n border-collapse: collapse; }\n date-input-polyfill th, date-input-polyfill td {\n width: 32px;\n padding: 4px;\n text-align: center; }\n date-input-polyfill td[data-day] {\n cursor: pointer; }\n date-input-polyfill td[data-day]:hover {\n background: #E0E0E0; }\n date-input-polyfill [data-selected] {\n font-weight: bold;\n background: #D8EAF6; }\n\ninput[data-has-picker]::-ms-clear {\n display: none; }\n');var a=function(a,b){if(!(a instanceof b))throw new TypeError('Cannot call a class as a function')},b=function(){function a(a,b){for(var c,d=0;d'],b=0,d=this.input.localeText.days.length;b'+this.input.localeText.days[b]+'');this.daysHead.innerHTML=a.join(''),c.createRangeSelect(this.month,0,11,this.input.localeText.months,this.date.getMonth()),this.today.textContent=this.input.localeText.today}},{key:'refreshDaysMatrix',value:function(){this.refreshLocale();for(var a=this.date.getFullYear(),b=this.date.getMonth(),d=new Date(a,b,1).getDay(),e=new Date(this.date.getFullYear(),b+1,0).getDate(),f=c.absoluteDate(this.input.element.valueAsDate)||!1,g=f&&a===f.getFullYear()&&b===f.getMonth(),h=[],j=0;j')+'\n \n '),j+1<=d){h.push('');continue}var i=j+1-d,k=g&&f.getDate()===i;h.push('\n '+i+'\n ')}this.days.innerHTML=h.join('')}},{key:'pingInput',value:function(){var a,b;try{a=new Event('input'),b=new Event('change')}catch(c){a=document.createEvent('KeyboardEvent'),a.initEvent('input',!0,!1),b=document.createEvent('KeyboardEvent'),b.initEvent('change',!0,!1)}this.input.element.dispatchEvent(a),this.input.element.dispatchEvent(b)}}],[{key:'createRangeSelect',value:function(a,b,c,d,e){a.innerHTML='';for(var f,g=b;g<=c;++g){f=document.createElement('option'),a.appendChild(f);var h=d?d[g-b]:g;f.text=h,f.value=g,g===e&&(f.selected='selected')}return a}},{key:'absoluteDate',value:function(a){return a&&new Date(a.getTime()+1e3*(60*a.getTimezoneOffset()))}}]),c}();c.instance=null;var d={"en_en-US":{days:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],months:['January','February','March','April','May','June','July','August','September','October','November','December'],today:'Today',format:'M/D/Y'},"en-GB":{days:['Sun','Mon','Tue','Wed','Thu','Fri','Sat'],months:['January','February','March','April','May','June','July','August','September','October','November','December'],today:'Today',format:'D/M/Y'},"zh_zh-CN":{days:['\u661F\u671F\u5929','\u661F\u671F\u4E00','\u661F\u671F\u4E8C','\u661F\u671F\u4E09','\u661F\u671F\u56DB','\u661F\u671F\u4E94','\u661F\u671F\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"zh-Hans_zh-Hans-CN":{days:['\u5468\u65E5','\u5468\u4E00','\u5468\u4E8C','\u5468\u4E09','\u5468\u56DB','\u5468\u4E94','\u5468\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"zh-Hant_zh-Hant-TW":{days:['\u9031\u65E5','\u9031\u4E00','\u9031\u4E8C','\u9031\u4E09','\u9031\u56DB','\u9031\u4E94','\u9031\u516D'],months:['\u4E00\u6708','\u4E8C\u6708','\u4E09\u6708','\u56DB\u6708','\u4E94\u6708','\u516D\u6708','\u4E03\u6708','\u516B\u6708','\u4E5D\u6708','\u5341\u6708','\u5341\u4E00\u6708','\u5341\u4E8C\u6708'],today:'\u4ECA\u5929',format:'Y/M/D'},"de_de-DE":{days:['So','Mo','Di','Mi','Do','Fr','Sa'],months:['Januar','Februar','M\xE4rz','April','Mai','Juni','Juli','August','September','Oktober','November','Dezember'],today:'Heute',format:'D.M.Y'},"da_da-DA":{days:['S\xF8n','Man','Tirs','Ons','Tors','Fre','L\xF8r'],months:['Januar','Februar','Marts','April','Maj','Juni','Juli','August','September','Oktober','November','December'],today:'I dag',format:'D/M/Y'},es:{days:['Dom','Lun','Mar','Mi\xE9','Jue','Vie','S\xE1b'],months:['Enero','Febrero','Marzo','Abril','Mayo','Junio','Julio','Agosto','Septiembre','Octubre','Noviembre','Diciembre'],today:'Hoy',format:'D/M/Y'},hi:{days:['\u0930\u0935\u093F','\u0938\u094B\u092E','\u092E\u0902\u0917\u0932','\u092C\u0941\u0927','\u0917\u0941\u0930\u0941','\u0936\u0941\u0915\u094D\u0930','\u0936\u0928\u093F'],months:['\u091C\u0928\u0935\u0930\u0940','\u092B\u0930\u0935\u0930\u0940','\u092E\u093E\u0930\u094D\u091A','\u0905\u092A\u094D\u0930\u0947\u0932','\u092E\u0948','\u091C\u0942\u0928','\u091C\u0942\u0932\u093E\u0908','\u0905\u0917\u0938\u094D\u0924','\u0938\u093F\u0924\u092E\u094D\u092C\u0930','\u0906\u0915\u094D\u091F\u094B\u092C\u0930','\u0928\u0935\u092E\u094D\u092C\u0930','\u0926\u093F\u0938\u092E\u094D\u092C\u0930'],today:'\u0906\u091C',format:'D/M/Y'},pt:{days:['Dom','Seg','Ter','Qua','Qui','Sex','S\xE1b'],months:['Janeiro','Fevereiro','Mar\xE7o','Abril','Maio','Junho','Julho','Agosto','Setembro','Outubro','Novembro','Dezembro'],today:'Hoje',format:'D/M/Y'},ja:{days:['\u65E5','\u6708','\u706B','\u6C34','\u6728','\u91D1','\u571F'],months:['1\u6708','2\u6708','3\u6708','4\u6708','5\u6708','6\u6708','7\u6708','8\u6708','9\u6708','10\u6708','11\u6708','12\u6708'],today:'\u4ECA\u65E5',format:'Y/M/D'},"nl_nl-NL_nl-BE":{days:['Zondag','Maandag','Dinsdag','Woensdag','Donderdag','Vrijdag','Zaterdag'],months:['Januari','Februari','Maart','April','Mei','Juni','Juli','Augustus','September','Oktober','November','December'],today:'Vandaag',format:'D/M/Y'},"tr_tr-TR":{days:['Pzr','Pzt','Sal','\xC7r\u015F','Pr\u015F','Cum','Cmt'],months:['Ocak','\u015Eubat','Mart','Nisan','May\u0131s','Haziran','Temmuz','A\u011Fustos','Eyl\xFCl','Ekim','Kas\u0131m','Aral\u0131k'],today:'Bug\xFCn',format:'D/M/Y'},"fr_fr-FR":{days:['Dim','Lun','Mar','Mer','Jeu','Ven','Sam'],months:['Janvier','F\xE9vrier','Mars','Avril','Mai','Juin','Juillet','Ao\xFBt','Septembre','Octobre','Novembre','D\xE9cembre'],today:'Auj.',format:'D/M/Y'},"uk_uk-UA":{days:['\u041D\u0434','\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431'],months:['\u0421\u0456\u0447\u0435\u043D\u044C','\u041B\u044E\u0442\u0438\u0439','\u0411\u0435\u0440\u0435\u0437\u0435\u043D\u044C','\u041A\u0432\u0456\u0442\u0435\u043D\u044C','\u0422\u0440\u0430\u0432\u0435\u043D\u044C','\u0427\u0435\u0440\u0432\u0435\u043D\u044C','\u041B\u0438\u043F\u0435\u043D\u044C','\u0421\u0435\u0440\u043F\u0435\u043D\u044C','\u0412\u0435\u0440\u0435\u0441\u0435\u043D\u044C','\u0416\u043E\u0432\u0442\u0435\u043D\u044C','\u041B\u0438\u0441\u0442\u043E\u043F\u0430\u0434','\u0413\u0440\u0443\u0434\u0435\u043D\u044C'],today:'\u0421\u044C\u043E\u0433\u043E\u0434\u043D\u0456',format:'D.M.Y'},it:{days:['Dom','Lun','Mar','Mer','Gio','Ven','Sab'],months:['Gennaio','Febbraio','Marzo','Aprile','Maggio','Giugno','Luglio','Agosto','Settembre','ottobre','Novembre','Dicembre'],today:'Oggi',format:'D/M/Y'},pl:{days:['Nie','Pon','Wto','\u015Aro','Czw','Pt','Sob'],months:['Stycze\u0144','Luty','Marzec','Kwiecie\u0144','Maj','Czerwiec','Lipiec','Sierpie\u0144','Wrzesie\u0144','Pa\u017Adziernik','Listopad','Grudzie\u0144'],today:'Dzisiaj',format:'D.M.Y'},cs:{days:['Po','\xDAt','St','\u010Ct','P\xE1','So','Ne'],months:['Leden','\xDAnor','B\u0159ezen','Duben','Kv\u011Bten','\u010Cerven','\u010Cervenec','Srpen','Z\xE1\u0159\xED','\u0158\xEDjen','Listopad','Prosinec'],today:'Dnes',format:'D.M.Y'},ru:{days:['\u0412\u0441','\u041F\u043D','\u0412\u0442','\u0421\u0440','\u0427\u0442','\u041F\u0442','\u0421\u0431'],months:['\u042F\u043D\u0432\u0430\u0440\u044C','\u0424\u0435\u0432\u0440\u0430\u043B\u044C','\u041C\u0430\u0440\u0442','\u0410\u043F\u0440\u0435\u043B\u044C','\u041C\u0430\u0439','\u0418\u044E\u043D\u044C','\u0418\u044E\u043B\u044C','\u0410\u0432\u0433\u0443\u0441\u0442','\u0421\u0435\u043D\u0442\u044F\u0431\u0440\u044C','\u041E\u043A\u0442\u044F\u0431\u0440\u044C','\u041D\u043E\u044F\u0431\u0440\u044C','\u0414\u0435\u043A\u0430\u0431\u0440\u044C'],today:'\u0421\u0435\u0433\u043E\u0434\u043D\u044F',format:'D.M.Y'}},e=function(){function e(b){var d=this;a(this,e),this.element=b,this.element.setAttribute('data-has-picker','');for(var f=this.element,g='';f.parentNode&&(g=f.getAttribute('lang'),!g);)f=f.parentNode;this.locale=g||'en',this.localeText=this.getLocaleText(),Object.defineProperties(this.element,{value:{get:function(){return d.element.polyfillValue},set:function(a){if(!/^\d{4}-\d{2}-\d{2}$/.test(a))return d.element.polyfillValue='',d.element.setAttribute('value',''),!1;d.element.polyfillValue=a;var b=a.split('-');d.element.setAttribute('value',d.localeText.format.replace('Y',b[0]).replace('M',b[1]).replace('D',b[2]))}},valueAsDate:{get:function(){return d.element.polyfillValue?new Date(d.element.polyfillValue):null},set:function(a){d.element.value=a.toISOString().slice(0,10)}},valueAsNumber:{get:function(){return d.element.value?d.element.valueAsDate.getTime():NaN},set:function(a){d.element.valueAsDate=new Date(a)}}}),this.element.value=this.element.getAttribute('value');var h=function(){c.instance.attachTo(d)};this.element.addEventListener('focus',h),this.element.addEventListener('mousedown',h),this.element.addEventListener('mouseup',h),this.element.addEventListener('keydown',function(a){var b=new Date;switch(a.keyCode){case 27:c.instance.hide();break;case 38:d.element.valueAsDate&&(b.setDate(d.element.valueAsDate.getDate()+1),d.element.valueAsDate=b,c.instance.pingInput());break;case 40:d.element.valueAsDate&&(b.setDate(d.element.valueAsDate.getDate()-1),d.element.valueAsDate=b,c.instance.pingInput());break;default:}c.instance.sync()})}return b(e,[{key:'getLocaleText',value:function(){var a=this.locale.toLowerCase();for(var b in d){var c=b.split('_').map(function(a){return a.toLowerCase()});if(!!~c.indexOf(a))return d[b]}for(var e in d){var f=e.split('_').map(function(a){return a.toLowerCase()});if(!!~f.indexOf(a.substr(0,2)))return d[e]}return this.locale='en',this.getLocaleText()}}],[{key:'supportsDateInput',value:function(){var a=document.createElement('input');a.setAttribute('type','date');var b='not-a-date';return a.setAttribute('value',b),document.currentScript&&!document.currentScript.hasAttribute('data-nodep-date-input-polyfill-debug')&&a.value!==b}},{key:'addPickerToDateInputs',value:function(){var a=document.querySelectorAll('input[type="date"]:not([data-has-picker]):not([readonly])'),b=a.length;if(!b)return!1;for(var c=0;c Date: Tue, 10 Sep 2019 13:12:07 +0530 Subject: [PATCH 023/210] UI Fixes Only 8 time slots will appear in a row Date is more readable on the contact details page --- erpnext/www/book-appointment/index.js | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index b2df3b4382..61ea8e40d7 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -93,7 +93,13 @@ async function update_time_slots(selected_date, selected_timezone) { timeslot_container.appendChild(message_div); return } - window.slots.forEach(slot => { + window.slots.forEach((slot,index) => { + debugger + if(index%8==0){ + let break_element = document.createElement('div'); + break_element.classList.add('w-100'); + timeslot_container.appendChild(break_element); + } let start_time = new Date(slot.time) var timeslot_div = document.createElement('div'); timeslot_div.classList.add('time-slot'); @@ -120,7 +126,7 @@ function clear_time_slots() { function get_slot_layout(time) { time = new Date(time) let start_time_string = moment(time).format("LT"); - let end_time = moment(time).add('1','hours'); + let end_time = moment(time).add(window.appointment_settings.appointment_duration,'minutes'); let end_time_string = end_time.format("LT"); return `${start_time_string}
to ${end_time_string}`; } @@ -158,7 +164,7 @@ function setup_details_page(){ page2.style.display = 'block'; let date_container = document.getElementsByClassName('date-span')[0]; let time_container = document.getElementsByClassName('time-span')[0]; - date_container.innerHTML = new Date(window.selected_date).toLocaleDateString(); + date_container.innerHTML = moment(window.selected_date).format("MMM Do YYYY"); time_container.innerHTML = moment(window.selected_time,"HH:mm:ss").format("LT"); } From 6f486f371919962958644b892bfaffe614cf407e Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 10 Sep 2019 13:12:28 +0530 Subject: [PATCH 024/210] Addded status to appointment creation --- erpnext/www/book-appointment/index.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index f4e96b47d6..1b87b86a40 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -93,6 +93,7 @@ def create_appointment(date, time, contact): appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] + appointment.status = 'Open' appointment.insert() From c4950a028136e8e68661488250ce41c8f3a73305 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 10 Sep 2019 15:10:51 +0530 Subject: [PATCH 025/210] Added doctype availabitlity of slots added --- erpnext/crm/doctype/appointment/test_appointment.py | 12 ++++++------ .../appointment_booking_settings.json | 2 +- .../availability_of_slots/availability_of_slots.json | 8 ++++---- .../availability_of_slots/availability_of_slots.py | 3 +-- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index e446712d01..96c4e4fc05 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -34,14 +34,14 @@ class TestAppointment(unittest.TestCase): def tearDown(self): delete_appointments() - def delete_appointments(self): - doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) - for doc in doc_list: - doc.delete() + def delete_appointments(self): + doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) + for doc in doc_list: + doc.delete() def test_number_of_appointments(self): settings = frappe.get_doc('Appointment Booking Settings') - self.assertLessEqual(frappe.db.count('Apoointment', - filters={'scheduled_time': datetime.datetime.min, 'customer_name':}), + self.assertFalse(frappe.db.exists('Apoointment', + filters={'scheduled_time': datetime.datetime.min, 'customer_name':'Extra Customer'}), settings.number_of_agents, "Number of appointments exceed number of agents") diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index cf27f770c2..11820b965a 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -48,7 +48,7 @@ } ], "issingle": 1, - "modified": "2019-09-03 12:27:09.763730", + "modified": "2019-09-10 15:02:39.969131", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json index d26f7ced35..b54af8dba4 100644 --- a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json +++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.json @@ -1,5 +1,5 @@ { - "creation": "2019-08-27 10:52:54.204677", + "creation": "2019-09-10 15:02:05.779434", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -21,7 +21,7 @@ "fieldname": "from_time", "fieldtype": "Time", "in_list_view": 1, - "label": "From Time ", + "label": "From Time", "reqd": 1 }, { @@ -33,10 +33,10 @@ } ], "istable": 1, - "modified": "2019-08-27 10:52:54.204677", + "modified": "2019-09-10 15:05:20.406855", "modified_by": "Administrator", "module": "CRM", - "name": "Availabilty Of Slots", + "name": "Availability Of Slots", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py index 94fb0c94d6..8258471eed 100644 --- a/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py +++ b/erpnext/crm/doctype/availability_of_slots/availability_of_slots.py @@ -6,6 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document - class AvailabilityOfSlots(Document): - pass + pass From 2d7370a525622ee02345b65f15a312555d2fe0ab Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 10 Sep 2019 16:46:17 +0530 Subject: [PATCH 026/210] Moved delete_appointment --- erpnext/crm/doctype/appointment/test_appointment.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 96c4e4fc05..8487b258f2 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -18,6 +18,11 @@ def create_appointments(number): 'customer_skype': 'test'+str(i), }) +def delete_appointments(): + doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) + for doc in doc_list: + doc.delete() + class TestAppointment(unittest.TestCase): def setUp(self): @@ -34,14 +39,9 @@ class TestAppointment(unittest.TestCase): def tearDown(self): delete_appointments() - def delete_appointments(self): - doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) - for doc in doc_list: - doc.delete() - def test_number_of_appointments(self): settings = frappe.get_doc('Appointment Booking Settings') self.assertFalse(frappe.db.exists('Apoointment', - filters={'scheduled_time': datetime.datetime.min, 'customer_name':'Extra Customer'}), + filters={'scheduled_time': datetime.datetime.min, 'customer_name':'Extra Cu'}), settings.number_of_agents, "Number of appointments exceed number of agents") From 5038d6a6db28f9112f0ee743ccf0c44ec394ff57 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 11 Sep 2019 10:31:04 +0530 Subject: [PATCH 027/210] Removed appointment tests TODO: Write better tests after adding lead and calender event generation --- .../doctype/appointment/test_appointment.py | 21 +------------------ 1 file changed, 1 insertion(+), 20 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 8487b258f2..c1a1c4ff46 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -25,23 +25,4 @@ def delete_appointments(): class TestAppointment(unittest.TestCase): - def setUp(self): - settings = frappe.get_doc('Appointment Booking Settings') - create_appointments(settings.number_of_agents) - frappe.get_doc({ - 'doctype': 'Appointment', - 'scheduled_time': datetime.datetime.min, - 'customer_name': 'Extra Customer', - 'customer_phone_number': '8088', - 'customer_skype': 'extra_customer', - }) - - def tearDown(self): - delete_appointments() - - def test_number_of_appointments(self): - settings = frappe.get_doc('Appointment Booking Settings') - self.assertFalse(frappe.db.exists('Apoointment', - filters={'scheduled_time': datetime.datetime.min, 'customer_name':'Extra Cu'}), - settings.number_of_agents, - "Number of appointments exceed number of agents") + pass From 0cc837eac5f66be7042a18241de58c1747d50867 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 11 Sep 2019 14:12:30 +0530 Subject: [PATCH 028/210] Create event for the appointment TODO: Add lead and employee to this --- erpnext/crm/doctype/appointment/appointment.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index cce6a1d684..30d10194b2 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -3,6 +3,7 @@ # For license information, please see license.txt from __future__ import unicode_literals +from datetime import timedelta import frappe from frappe.model.document import Document @@ -12,4 +13,14 @@ class Appointment(Document): 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') + + def after_insert(self): + appointment_event = frappe.new_doc('Event') + appointment_event.subject = 'Appointment with ' + self.customer_name + appointment_event.starts_on = self.scheduled_time + appointment_event.status = 'Open' + appointment_event.type = 'Private' + settings = frappe.get_doc('Appointment Booking Settings') + appointment_event.ends_on = self.scheduled_time + timedelta(minutes=settings.appointment_duration) + appointment_event.insert() From a322b159ab9da2e60dbf260b8bd578c20fdd3612 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 11 Sep 2019 14:25:26 +0530 Subject: [PATCH 029/210] Added back button from details page --- erpnext/www/book-appointment/index.html | 7 +++++-- erpnext/www/book-appointment/index.js | 2 ++ 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index f4074270e0..43275eb243 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -13,7 +13,7 @@

Book an appointment

-

Select the date and your timezone

+

Select the date and your timezone

@@ -53,7 +53,10 @@ required> - +
+
+
+
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 61ea8e40d7..90572fb891 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -69,6 +69,8 @@ function on_date_or_timezone_select() { 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) { From e543fc483fea6b09ae72a1f84cdaf783122cc721 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 11 Sep 2019 14:59:13 +0530 Subject: [PATCH 030/210] Removed email reminders As it will be handled by calender event in the future --- .../appointment_booking_settings.json | 12 +++--------- 1 file changed, 3 insertions(+), 9 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 11820b965a..0150309ad0 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -7,7 +7,6 @@ "availability_of_slots", "number_of_agents", "holiday_list", - "email_reminders", "appointment_duration" ], "fields": [ @@ -19,6 +18,7 @@ "reqd": 1 }, { + "default": "1", "fieldname": "number_of_agents", "fieldtype": "Int", "in_list_view": 1, @@ -33,22 +33,16 @@ "options": "Holiday List", "reqd": 1 }, - { - "default": "0", - "fieldname": "email_reminders", - "fieldtype": "Check", - "label": "Email Reminders" - }, { "default": "60", "fieldname": "appointment_duration", "fieldtype": "Int", - "label": "Appointment Duration", + "label": "Appointment Duration (In Minutes)", "reqd": 1 } ], "issingle": 1, - "modified": "2019-09-10 15:02:39.969131", + "modified": "2019-09-11 14:44:33.471834", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", From 249cdd92e0d430984a63e54504bfa6f21f0d87f5 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 11 Sep 2019 14:59:25 +0530 Subject: [PATCH 031/210] Added uniqueness check for offset --- erpnext/crm/doctype/timezone/timezone.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/erpnext/crm/doctype/timezone/timezone.py b/erpnext/crm/doctype/timezone/timezone.py index f0da6e3d9a..2c77023b39 100644 --- a/erpnext/crm/doctype/timezone/timezone.py +++ b/erpnext/crm/doctype/timezone/timezone.py @@ -12,3 +12,6 @@ class Timezone(Document): 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 From 8051ca1859f247aaaeba758a038997fb858cf538 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 12 Sep 2019 10:47:45 +0530 Subject: [PATCH 032/210] Limit advance booking of appointments --- .../appointment_booking_settings.json | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 0150309ad0..2386ed76e9 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -7,7 +7,9 @@ "availability_of_slots", "number_of_agents", "holiday_list", - "appointment_duration" + "appointment_duration", + "email_reminders", + "advance_booking_days" ], "fields": [ { @@ -39,10 +41,23 @@ "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 } ], "issingle": 1, - "modified": "2019-09-11 14:44:33.471834", + "modified": "2019-09-12 10:47:20.274330", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", From a2dbd391b3bffad6b0c87b82565231051ee93f96 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 12 Sep 2019 10:48:26 +0530 Subject: [PATCH 033/210] Add lead and calender event to appointments --- .../crm/doctype/appointment/appointment.json | 20 +++++++++++++-- .../crm/doctype/appointment/appointment.py | 3 ++- erpnext/www/book-appointment/index.html | 4 ++- erpnext/www/book-appointment/index.js | 25 +++++++++++++++---- erpnext/www/book-appointment/index.py | 8 ++++++ 5 files changed, 51 insertions(+), 9 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 356cbea2cc..b2fe7b9db2 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -11,7 +11,9 @@ "customer_name", "customer_phone_number", "customer_skype", - "customer_details" + "customer_details", + "lead", + "calender_event" ], "fields": [ { @@ -56,9 +58,23 @@ "label": "Status", "options": "Open\nClosed", "reqd": 1 + }, + { + "fieldname": "lead", + "fieldtype": "Link", + "label": "Lead", + "options": "Lead", + "reqd": 1 + }, + { + "fieldname": "calender_event", + "fieldtype": "Link", + "label": "Calender Event", + "options": "Event", + "reqd": 1 } ], - "modified": "2019-09-10 11:17:20.200603", + "modified": "2019-09-12 10:42:47.841841", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 30d10194b2..4c95c6e5c3 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -14,7 +14,7 @@ class Appointment(Document): if(number_of_appointments_in_same_slot>=settings.number_of_agents): frappe.throw('Time slot is not available') - def after_insert(self): + def before_insert(self): appointment_event = frappe.new_doc('Event') appointment_event.subject = 'Appointment with ' + self.customer_name appointment_event.starts_on = self.scheduled_time @@ -23,4 +23,5 @@ class Appointment(Document): settings = frappe.get_doc('Appointment Booking Settings') appointment_event.ends_on = self.scheduled_time + timedelta(minutes=settings.appointment_duration) appointment_event.insert() + self.calender_event = appointment_event.name diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 43275eb243..2e0321394e 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -51,10 +51,12 @@ placeholder="Contact Number" required> +
-
+
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 90572fb891..f9d9b6e845 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -5,7 +5,7 @@ frappe.ready(() => { window.holiday_list = []; async function initialise_select_date() { - document.getElementById('enter-details').style.display = 'none'; + navigate_to_page(1); await get_global_variables(); setup_date_picker(); setup_timezone_selector(); @@ -115,7 +115,6 @@ async function update_time_slots(selected_date, selected_timezone) { timeslot_container.appendChild(timeslot_div); }); set_default_timeslot(); - show_next_button(); } function clear_time_slots() { @@ -146,6 +145,7 @@ function select_time() { window.selected_time = this.id selected_element.classList.remove("selected"); this.classList.add("selected"); + show_next_button(); } function set_default_timeslot() { @@ -159,11 +159,25 @@ function set_default_timeslot() { } } -function setup_details_page(){ +function navigate_to_page(page_number){ let page1 = document.getElementById('select-date-time'); let page2 = document.getElementById('enter-details'); - page1.style.display = 'none'; - page2.style.display = 'block'; + 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: + console.log("That's not a valid page") + } +} + +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"); @@ -196,6 +210,7 @@ function form_validation(){ 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 console.log({ date, time, contact }); } diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 1b87b86a40..530445ff91 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -94,8 +94,16 @@ def create_appointment(date, time, contact): appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] appointment.status = 'Open' + appointment.lead = find_lead_by_email(contact['email']).name appointment.insert() +def find_lead_by_email(email): + if frappe.db.exists({ + 'doctype':'Lead', + 'email_id':email + }): + return frappe.get_list('Lead',filters={'email_id':email})[0] + frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') # Helper Functions def filter_timeslots(date, timeslots): From 469247bf73c94458ef730d68b6f13fff961bd253 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 12 Sep 2019 11:15:42 +0530 Subject: [PATCH 034/210] Change max date of datepicker to number of days in future as specified by the settings --- .../appointment_booking_settings.json | 2 +- erpnext/www/book-appointment/index.js | 3 ++- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 2386ed76e9..6ef00703d1 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -57,7 +57,7 @@ } ], "issingle": 1, - "modified": "2019-09-12 10:47:20.274330", + "modified": "2019-09-12 10:52:25.931931", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index f9d9b6e845..96ad66ace3 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -43,7 +43,8 @@ function setup_date_picker() { let date_picker = document.getElementById('appointment-date'); let today = new Date(); date_picker.min = today.toISOString().substr(0, 10); - date_picker.max = window.holiday_list.to_date; + today.setDate(today.getDate() + window.appointment_settings.advance_booking_days); + date_picker.max = today.toISOString().substr(0,10); } function hide_next_button(){ From 1564f1476c75323c84ec19e5d52b27f99220d923 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 12 Sep 2019 14:24:28 +0530 Subject: [PATCH 035/210] Added customer to calender event --- erpnext/crm/doctype/appointment/appointment.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 4c95c6e5c3..13904116a7 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -22,6 +22,12 @@ class Appointment(Document): appointment_event.type = 'Private' settings = frappe.get_doc('Appointment Booking Settings') appointment_event.ends_on = self.scheduled_time + timedelta(minutes=settings.appointment_duration) + event_participants = [] + event_participant_customer = frappe.new_doc('Event Participants') + event_participant_customer.reference_doctype = 'Lead' + event_participant_customer.reference_docname = self.lead + event_participants.append(event_participant_customer) + appointment_event.event_participants = event_participants appointment_event.insert() self.calender_event = appointment_event.name From a3b8c77af133f137fae0df50663f7b515947a07b Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 12 Sep 2019 15:19:22 +0530 Subject: [PATCH 036/210] Fixed leads --- .../crm/doctype/appointment/appointment.py | 23 ++++++++----------- 1 file changed, 10 insertions(+), 13 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 13904116a7..1b6ef94bfc 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -16,18 +16,15 @@ class Appointment(Document): def before_insert(self): appointment_event = frappe.new_doc('Event') - appointment_event.subject = 'Appointment with ' + self.customer_name - appointment_event.starts_on = self.scheduled_time - appointment_event.status = 'Open' - appointment_event.type = 'Private' - settings = frappe.get_doc('Appointment Booking Settings') - appointment_event.ends_on = self.scheduled_time + timedelta(minutes=settings.appointment_duration) - event_participants = [] - event_participant_customer = frappe.new_doc('Event Participants') - event_participant_customer.reference_doctype = 'Lead' - event_participant_customer.reference_docname = self.lead - event_participants.append(event_participant_customer) - appointment_event.event_participants = event_participants - appointment_event.insert() + appointment_event = frappe.get_doc({ + "doctype": "Event", + "subject": ' '.join(['Appointment with', self.customer_name]), + "starts_on": self.scheduled_time, + "status": "Open", + "type": "Private", + "event_participants": [dict(reference_doctype="Lead", reference_docname=self.lead)] + }) + + appointment_event.insert(ignore_permissions=True) self.calender_event = appointment_event.name From cf045d86b07dae9dcd23ab53327574038b5e84fa Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 13 Sep 2019 15:55:54 +0530 Subject: [PATCH 037/210] fixed typo --- erpnext/crm/doctype/appointment/appointment.json | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index b2fe7b9db2..2d695f3199 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -13,7 +13,7 @@ "customer_skype", "customer_details", "lead", - "calender_event" + "calendar_event" ], "fields": [ { @@ -67,14 +67,13 @@ "reqd": 1 }, { - "fieldname": "calender_event", + "fieldname": "calendar_event", "fieldtype": "Link", - "label": "Calender Event", - "options": "Event", - "reqd": 1 + "label": "Calendar Event", + "options": "Event" } ], - "modified": "2019-09-12 10:42:47.841841", + "modified": "2019-09-13 15:25:49.362246", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From d88f850d0fa0100eae8ce5ca6cbb8740aed153b2 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 13 Sep 2019 15:56:47 +0530 Subject: [PATCH 038/210] removed debugger --- erpnext/www/book-appointment/index.js | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 96ad66ace3..5302d1b626 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -75,7 +75,6 @@ function on_date_or_timezone_select() { } async function get_time_slots(date, timezone) { - debugger let slots = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_appointment_slots', args: { @@ -97,7 +96,6 @@ async function update_time_slots(selected_date, selected_timezone) { return } window.slots.forEach((slot,index) => { - debugger if(index%8==0){ let break_element = document.createElement('div'); break_element.classList.add('w-100'); @@ -140,7 +138,6 @@ function select_time() { try { selected_element = document.getElementsByClassName('selected')[0] } catch (e) { - debugger this.classList.add("selected") } window.selected_time = this.id @@ -188,7 +185,6 @@ function setup_details_page(){ async function submit() { // form validation here form_validation(); - debugger; let appointment = (await frappe.call({ method: 'erpnext.www.book-appointment.index.create_appointment', args: { From 1cd762e9d0587cab4649ab1c55d7b9ad4b24a67a Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 13 Sep 2019 15:56:54 +0530 Subject: [PATCH 039/210] Added ajuto assignment --- .../crm/doctype/appointment/appointment.py | 67 ++++++++++++++++--- .../appointment_booking_settings.json | 12 +++- 2 files changed, 67 insertions(+), 12 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 1b6ef94bfc..4dea04b39c 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -3,28 +3,75 @@ # For license information, please see license.txt from __future__ import unicode_literals +from collections import Counter from datetime import timedelta import frappe from frappe.model.document import Document +from frappe.desk.form.assign_to import add as add_assignemnt + + +def _get_agents_sorted_by_asc_workload(): + appointments = frappe.db.get_list('Appointment', fields='*') + # Handle case where no appointments are created + appointment_counter = Counter() + if not appointments: + return frappe.get_doc('Appointment Booking Settings').agent_list + for appointment in appointments: + if appointment._assign == '[]' or not appointment._assign: + continue + appointment_counter[appointment._assign] += 1 + sorted_agent_list = appointment_counter.most_common() + sorted_agent_list.reverse() + return sorted_agent_list + +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): + return frappe.get_list('Employee', fields='*',filters={'user_id':user}) class Appointment(Document): def validate(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): + if(number_of_appointments_in_same_slot >= settings.number_of_agents): frappe.throw('Time slot is not available') - + def before_insert(self): appointment_event = frappe.new_doc('Event') appointment_event = frappe.get_doc({ - "doctype": "Event", - "subject": ' '.join(['Appointment with', self.customer_name]), - "starts_on": self.scheduled_time, - "status": "Open", - "type": "Private", - "event_participants": [dict(reference_doctype="Lead", reference_docname=self.lead)] + 'doctype': 'Event', + 'subject': ' '.join(['Appointment with', self.customer_name]), + 'starts_on': self.scheduled_time, + 'status': 'Open', + 'type': 'Private', + 'event_participants': [dict(reference_doctype="Lead", reference_docname=self.lead)] }) - appointment_event.insert(ignore_permissions=True) - self.calender_event = appointment_event.name + self.calendar_event = appointment_event.name + def after_insert(self): + available_agents = _get_agents_sorted_by_asc_workload() + for agent in available_agents: + if(_check_agent_availability(agent, self.scheduled_time)): + agent = agent[0] + agent = frappe.json.loads(agent)[0] + add_assignemnt({ + 'doctype':self.doctype, + 'name':self.name, + 'assign_to':agent + }) + employee = _get_employee_from_user(agent) + if employee: + print(employee) + calendar_event = frappe.get_doc('Event', self.calendar_event) + calendar_event.append('event_participants', dict( + reference_doctype='Employee', + reference_docname=employee[0].name)) + print(calendar_event) + calendar_event.save() + break \ 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 index 6ef00703d1..c59a2e466f 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -9,7 +9,8 @@ "holiday_list", "appointment_duration", "email_reminders", - "advance_booking_days" + "advance_booking_days", + "agent_list" ], "fields": [ { @@ -54,10 +55,17 @@ "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-12 10:52:25.931931", + "modified": "2019-09-13 11:31:26.654516", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", From 018f0d3bbd044cb61cd6cc5805ef0169d070b42b Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 13 Sep 2019 16:25:26 +0530 Subject: [PATCH 040/210] Fixed issue: agents weren't looked up in settings --- .../crm/doctype/appointment/appointment.py | 73 ++++++++++++------- 1 file changed, 48 insertions(+), 25 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 4dea04b39c..3a588fbcd8 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -3,37 +3,15 @@ # For license information, please see license.txt from __future__ import unicode_literals + from collections import Counter from datetime import timedelta + import frappe from frappe.model.document import Document from frappe.desk.form.assign_to import add as add_assignemnt -def _get_agents_sorted_by_asc_workload(): - appointments = frappe.db.get_list('Appointment', fields='*') - # Handle case where no appointments are created - appointment_counter = Counter() - if not appointments: - return frappe.get_doc('Appointment Booking Settings').agent_list - for appointment in appointments: - if appointment._assign == '[]' or not appointment._assign: - continue - appointment_counter[appointment._assign] += 1 - sorted_agent_list = appointment_counter.most_common() - sorted_agent_list.reverse() - return sorted_agent_list - -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): - return frappe.get_list('Employee', fields='*',filters={'user_id':user}) - class Appointment(Document): def validate(self): number_of_appointments_in_same_slot = frappe.db.count('Appointment',filters={'scheduled_time':self.scheduled_time}) @@ -74,4 +52,49 @@ class Appointment(Document): reference_docname=employee[0].name)) print(calendar_event) calendar_event.save() - break \ No newline at end of file + break + + +def _get_agents_sorted_by_asc_workload(): + 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) + print(assigned_to) + if appointment._assign == '[]' or not appointment._assign: + continue + if assigned_to[0] in agent_list: + 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): + return frappe.get_list('Employee', fields='*',filters={'user_id':user}) \ No newline at end of file From a8752db012ffa9222b6e9a18c152bcc776a5be10 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Mon, 16 Sep 2019 20:02:20 +0530 Subject: [PATCH 041/210] Typo and styling fixes Co-Authored-By: Shivam Mishra --- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 3a588fbcd8..614a43c590 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -89,7 +89,7 @@ def _get_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}) + appointments_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 @@ -97,4 +97,4 @@ def _check_agent_availability(agent_email,scheduled_time): def _get_employee_from_user(user): - return frappe.get_list('Employee', fields='*',filters={'user_id':user}) \ No newline at end of file + return frappe.get_list('Employee', fields='*',filters={'user_id':user}) From 91a564989f883293aeeca7479d9f5eaa0a02bc65 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 17 Sep 2019 16:58:41 +0530 Subject: [PATCH 042/210] Styling and PR review changes --- .../crm/doctype/appointment/appointment.py | 33 +++---- .../doctype/appointment/test_appointment.py | 14 +-- .../appointment_booking_settings.py | 13 ++- erpnext/crm/doctype/timezone/timezone.py | 6 +- erpnext/www/book-appointment/index.js | 87 ++++++++++--------- erpnext/www/book-appointment/index.py | 18 ++-- 6 files changed, 79 insertions(+), 92 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 3a588fbcd8..5408b4d91a 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -14,22 +14,21 @@ from frappe.desk.form.assign_to import add as add_assignemnt class Appointment(Document): def validate(self): - number_of_appointments_in_same_slot = frappe.db.count('Appointment',filters={'scheduled_time':self.scheduled_time}) + 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') def before_insert(self): - appointment_event = frappe.new_doc('Event') appointment_event = frappe.get_doc({ 'doctype': 'Event', 'subject': ' '.join(['Appointment with', self.customer_name]), 'starts_on': self.scheduled_time, 'status': 'Open', 'type': 'Private', - 'event_participants': [dict(reference_doctype="Lead", reference_docname=self.lead)] + 'event_participants': [dict(reference_doctype = "Lead", reference_docname = self.lead)] }) - appointment_event.insert(ignore_permissions=True) + appointment_event.insert(ignore_permissions = True) self.calendar_event = appointment_event.name def after_insert(self): @@ -37,7 +36,6 @@ class Appointment(Document): for agent in available_agents: if(_check_agent_availability(agent, self.scheduled_time)): agent = agent[0] - agent = frappe.json.loads(agent)[0] add_assignemnt({ 'doctype':self.doctype, 'name':self.name, @@ -45,33 +43,25 @@ class Appointment(Document): }) employee = _get_employee_from_user(agent) if employee: - print(employee) calendar_event = frappe.get_doc('Event', self.calendar_event) calendar_event.append('event_participants', dict( - reference_doctype='Employee', - reference_docname=employee[0].name)) - print(calendar_event) + reference_doctype= 'Employee', + reference_docname= employee.name)) calendar_event.save() break - def _get_agents_sorted_by_asc_workload(): appointments = frappe.db.get_list('Appointment', fields='*') - agent_list = _get_agent_list_as_strings() - + 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) - print(assigned_to) - if appointment._assign == '[]' or not appointment._assign: + if not assigned_to: continue if assigned_to[0] in agent_list: appointment_counter[assigned_to[0]] += 1 - sorted_agent_list = appointment_counter.most_common() sorted_agent_list.reverse() @@ -81,15 +71,13 @@ def _get_agents_sorted_by_asc_workload(): 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}) + 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 @@ -97,4 +85,7 @@ def _check_agent_availability(agent_email,scheduled_time): def _get_employee_from_user(user): - return frappe.get_list('Employee', fields='*',filters={'user_id':user}) \ No newline at end of file + 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 index c1a1c4ff46..3c977505b5 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,20 +8,8 @@ import unittest import datetime -def create_appointments(number): - for i in range(1, number): - frappe.get_doc({ - 'doctype': 'Appointment', - 'scheduled_time': datetime.datetime.min, - 'customer_name': 'Test Customer'+str(i), - 'customer_phone_number': '8088', - 'customer_skype': 'test'+str(i), - }) - def delete_appointments(): - doc_list = frappe.get_list('Appointment',filters={'scheduled_time':datetime.datetime.min,'customer_phone_number':'8088'}) - for doc in doc_list: - doc.delete() + pass class TestAppointment(unittest.TestCase): diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 8f1fb14f5b..da181ae119 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -14,17 +14,22 @@ class AppointmentBookingSettings(Document): 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) + 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): + + 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): + + 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/timezone/timezone.py b/erpnext/crm/doctype/timezone/timezone.py index 2c77023b39..539ffa2547 100644 --- a/erpnext/crm/doctype/timezone/timezone.py +++ b/erpnext/crm/doctype/timezone/timezone.py @@ -10,8 +10,6 @@ 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') + 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 + frappe.throw('Timezone offsets need to be unique') \ No newline at end of file diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 5302d1b626..8fc5e31708 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -13,6 +13,7 @@ async function initialise_select_date() { } async function get_global_variables() { + // Using await window.appointment_settings = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_appointment_settings' })).message @@ -29,9 +30,9 @@ async function get_global_variables() { function setup_timezone_selector() { let timezones_element = document.getElementById('appointment-timezone'); - var offset = new Date().getTimezoneOffset(); + let offset = new Date().getTimezoneOffset(); window.timezones.forEach(timezone => { - var opt = document.createElement('option'); + let opt = document.createElement('option'); opt.value = timezone.offset; opt.innerHTML = timezone.timezone_name; opt.defaultSelected = (offset == timezone.offset) @@ -44,16 +45,16 @@ function setup_date_picker() { 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); + date_picker.max = today.toISOString().substr(0, 10); } -function hide_next_button(){ +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")}; + next_button.onclick = () => frappe.msgprint("Please select a date and time"); } -function show_next_button(){ +function show_next_button() { let next_button = document.getElementById('next-button'); next_button.disabled = false; next_button.onclick = setup_details_page; @@ -95,28 +96,36 @@ async function update_time_slots(selected_date, selected_timezone) { timeslot_container.appendChild(message_div); return } - window.slots.forEach((slot,index) => { - if(index%8==0){ + 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); } - let start_time = new Date(slot.time) - var timeslot_div = document.createElement('div'); - timeslot_div.classList.add('time-slot'); - timeslot_div.classList.add('col-md'); - if (!slot.availability) { - timeslot_div.classList.add('unavailable') - } - timeslot_div.innerHTML = get_slot_layout(start_time); - timeslot_div.id = slot.time.substr(11, 20); - timeslot_div.addEventListener('click', select_time); + // 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) @@ -126,23 +135,24 @@ function clear_time_slots() { 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 = moment(time).add(window.appointment_settings.appointment_duration, 'minutes'); let end_time_string = end_time.format("LT"); return `${start_time_string}
to ${end_time_string}`; } function select_time() { - if (this.classList.contains("unavailable")) { + if (this.classList.contains('unavailable')) { return } - try { - selected_element = document.getElementsByClassName('selected')[0] - } catch (e) { - this.classList.add("selected") + let selected_element = document.getElementsByClassName('selected'); + if (!(selected_element.length > 0)){ + this.classList.add('selected') + return } + selected_element = selected_element[0] window.selected_time = this.id - selected_element.classList.remove("selected"); - this.classList.add("selected"); + selected_element.classList.remove('selected'); + this.classList.add('selected'); show_next_button(); } @@ -151,17 +161,17 @@ function set_default_timeslot() { for (let i = 0; i < timeslots.length; i++) { const timeslot = timeslots[i]; if (!timeslot.classList.contains('unavailable')) { - timeslot.classList.add("selected"); + timeslot.classList.add('selected'); break; } } } -function navigate_to_page(page_number){ +function navigate_to_page(page_number) { let page1 = document.getElementById('select-date-time'); let page2 = document.getElementById('enter-details'); - switch(page_number){ - case 1: + switch (page_number) { + case 1: page1.style.display = 'block'; page2.style.display = 'none'; break; @@ -170,21 +180,21 @@ function navigate_to_page(page_number){ page2.style.display = 'block'; break; default: - console.log("That's not a valid page") + break; } } -function setup_details_page(){ +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"); + time_container.innerHTML = moment(window.selected_time, "HH:mm:ss").format("LT"); } async function submit() { // form validation here - form_validation(); + get_form_data(); let appointment = (await frappe.call({ method: 'erpnext.www.book-appointment.index.create_appointment', args: { @@ -196,12 +206,10 @@ async function submit() { frappe.msgprint(__('Appointment Created Successfully')); let button = document.getElementById('submit-button'); button.disabled = true; - button.onclick = () => { console.log('This should never have happened') } -} + button.onclick = null +} -function form_validation(){ - var date = window.selected_date; - var time = window.selected_time; +function get_form_data() { contact = {}; contact.name = document.getElementById('customer_name').value; contact.number = document.getElementById('customer_number').value; @@ -209,5 +217,4 @@ function form_validation(){ contact.notes = document.getElementById('customer_notes').value; contact.email = document.getElementById('customer_email').value; window.contact = contact - console.log({ date, time, contact }); } diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 530445ff91..6f6d4ac45c 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -2,6 +2,10 @@ import frappe import datetime import json + +WEEKDAYS = ["Monday", "Tuesday", "Wednesday", + "Thursday", "Friday", "Saturday", "Sunday"] + no_cache = 1 @@ -98,11 +102,9 @@ def create_appointment(date, time, contact): appointment.insert() def find_lead_by_email(email): - if frappe.db.exists({ - 'doctype':'Lead', - 'email_id':email - }): - return frappe.get_list('Lead',filters={'email_id':email})[0] + lead_list = frappe.get_list('Lead',filters={'email_id':email})[0] + if lead_list: + return lead_list frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') # Helper Functions @@ -156,8 +158,4 @@ def _convert_to_tz(datetime_object, timezone): datetime_object = datetime_object - offset offset = datetime.timedelta(minutes=-330) datetime_object = datetime_object + offset - return datetime_object - - -WEEKDAYS = ["Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday", "Sunday"] + return datetime_object \ No newline at end of file From 7323bfdad7bd02b42400ce8c9b924f0c244685c8 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 18 Sep 2019 14:33:10 +0530 Subject: [PATCH 043/210] Styling and bug fixes --- erpnext/www/book-appointment/index.js | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 8fc5e31708..345e614154 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -128,12 +128,12 @@ 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) + timeslot_container.removeChild(timeslot_container.firstChild); } } function get_slot_layout(time) { - time = new Date(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"); @@ -142,15 +142,16 @@ function get_slot_layout(time) { function select_time() { if (this.classList.contains('unavailable')) { - return + return; } let selected_element = document.getElementsByClassName('selected'); if (!(selected_element.length > 0)){ - this.classList.add('selected') - return + this.classList.add('selected'); + show_next_button(); + return; } selected_element = selected_element[0] - window.selected_time = this.id + window.selected_time = this.id; selected_element.classList.remove('selected'); this.classList.add('selected'); show_next_button(); @@ -158,6 +159,7 @@ function select_time() { 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')) { From 81449ece54aacd7cbe1d62c9a37f6719b6ac3b28 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 18 Sep 2019 14:33:40 +0530 Subject: [PATCH 044/210] fix:Linking lead --- erpnext/www/book-appointment/index.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 6f6d4ac45c..e238bd5205 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -102,9 +102,9 @@ def create_appointment(date, time, contact): appointment.insert() def find_lead_by_email(email): - lead_list = frappe.get_list('Lead',filters={'email_id':email})[0] + lead_list = frappe.get_list('Lead',filters={'email_id':email},ignore_permissions=True) if lead_list: - return lead_list + return lead_list[0] frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') # Helper Functions From 7d476a3e353dcb1ca711208ee111dbe5b80b00b2 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 18 Sep 2019 15:33:31 +0530 Subject: [PATCH 045/210] Moved lead assignment to the controller --- erpnext/crm/doctype/appointment/appointment.py | 7 +++++++ erpnext/www/book-appointment/index.py | 7 +------ 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 8a6d0635bc..1ffd58fa04 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -20,6 +20,7 @@ class Appointment(Document): frappe.throw('Time slot is not available') def before_insert(self): + self.lead = _find_lead_by_email(self.lead).name appointment_event = frappe.get_doc({ 'doctype': 'Event', 'subject': ' '.join(['Appointment with', self.customer_name]), @@ -67,6 +68,12 @@ def _get_agents_sorted_by_asc_workload(): return sorted_agent_list +def _find_lead_by_email(email): + lead_list = frappe.get_list('Lead',filters={'email_id':email},ignore_permissions=True) + if lead_list: + return lead_list[0] + frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') + def _get_agent_list_as_strings(): agent_list_as_strings = [] diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index e238bd5205..3370f2429e 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -97,15 +97,10 @@ def create_appointment(date, time, contact): appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] + appointment.lead = contact['email'] appointment.status = 'Open' - appointment.lead = find_lead_by_email(contact['email']).name appointment.insert() -def find_lead_by_email(email): - lead_list = frappe.get_list('Lead',filters={'email_id':email},ignore_permissions=True) - if lead_list: - return lead_list[0] - frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') # Helper Functions def filter_timeslots(date, timeslots): From ec1dae023cf9de6513452f70712b7393d7348a79 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 18 Sep 2019 16:13:29 +0530 Subject: [PATCH 046/210] styling --- erpnext/crm/doctype/appointment/appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 1ffd58fa04..ac2e0a8c74 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -29,7 +29,7 @@ class Appointment(Document): 'type': 'Private', 'event_participants': [dict(reference_doctype = "Lead", reference_docname = self.lead)] }) - appointment_event.insert(ignore_permissions = True) + appointment_event.insert(ignore_permissions=True) self.calendar_event = appointment_event.name def after_insert(self): From ba99945359a41130744e9dbd36824897654a8918 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 19 Sep 2019 11:21:05 +0530 Subject: [PATCH 047/210] Prevent booking of appointments for past times --- erpnext/www/book-appointment/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 3370f2429e..d5111c8d1b 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -37,6 +37,7 @@ def get_appointment_slots(date, timezone): 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) @@ -52,7 +53,7 @@ def get_appointment_slots(date, timezone): dict(time=_convert_to_tz(timeslot, timezone), availability=False)) continue # Check availability - if check_availabilty(timeslot, settings): + if check_availabilty(timeslot, settings) and timeslot >= now: converted_timeslots.append( dict(time=_convert_to_tz(timeslot, timezone), availability=True)) else: From 5bf52ebed66e5d95dc401df324d027d806280904 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 19 Sep 2019 11:47:54 +0530 Subject: [PATCH 048/210] limit assigment load to appointment day --- erpnext/crm/doctype/appointment/appointment.py | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index ac2e0a8c74..6d23f2a767 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -21,6 +21,9 @@ class Appointment(Document): def before_insert(self): self.lead = _find_lead_by_email(self.lead).name + + + def after_insert(self): appointment_event = frappe.get_doc({ 'doctype': 'Event', 'subject': ' '.join(['Appointment with', self.customer_name]), @@ -31,9 +34,7 @@ class Appointment(Document): }) appointment_event.insert(ignore_permissions=True) self.calendar_event = appointment_event.name - - def after_insert(self): - available_agents = _get_agents_sorted_by_asc_workload() + 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] @@ -51,7 +52,7 @@ class Appointment(Document): calendar_event.save() break -def _get_agents_sorted_by_asc_workload(): +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: @@ -61,7 +62,7 @@ def _get_agents_sorted_by_asc_workload(): assigned_to = frappe.parse_json(appointment._assign) if not assigned_to: continue - if assigned_to[0] in agent_list: + 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() @@ -69,7 +70,7 @@ def _get_agents_sorted_by_asc_workload(): return sorted_agent_list def _find_lead_by_email(email): - lead_list = frappe.get_list('Lead',filters={'email_id':email},ignore_permissions=True) + lead_list = frappe.get_list('Lead', filters={'email_id':email}, ignore_permissions=True) if lead_list: return lead_list[0] frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') @@ -92,7 +93,7 @@ def _check_agent_availability(agent_email,scheduled_time): def _get_employee_from_user(user): - employee_docname = frappe.db.exists({'doctype':'Employee','user_id':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 frappe.get_doc('Employee', employee_docname[0][0]) # frappe.db.exists returns a tuple of a tuple return None From 4109f88c04f2ac1240d381da248c3735ff96fd14 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 19 Sep 2019 12:08:10 +0530 Subject: [PATCH 049/210] Linked send_reminder in calendar event to Appointment Booking Settings --- erpnext/crm/doctype/appointment/appointment.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 6d23f2a767..9365301e8f 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -30,6 +30,7 @@ class Appointment(Document): 'starts_on': self.scheduled_time, 'status': 'Open', 'type': 'Private', + 'send_reminder': frappe.db.get_single_value('Appointment Booking Settings','email_reminders'), 'event_participants': [dict(reference_doctype = "Lead", reference_docname = self.lead)] }) appointment_event.insert(ignore_permissions=True) From ca2509423ab809127441b6efb3a66bbde7d41837 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 19 Sep 2019 12:36:51 +0530 Subject: [PATCH 050/210] Added permissions for HR manager --- .../appointment_booking_settings.json | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index c59a2e466f..d72f577656 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -65,7 +65,7 @@ } ], "issingle": 1, - "modified": "2019-09-13 11:31:26.654516", + "modified": "2019-09-19 12:36:34.011724", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -87,6 +87,15 @@ "read": 1, "role": "Guest", "share": 1 + }, + { + "create": 1, + "email": 1, + "print": 1, + "read": 1, + "role": "HR Manager", + "share": 1, + "write": 1 } ], "quick_entry": 1, From 5324234bd00357e4f0f0be8016f4f0dc9ae708a7 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 20 Sep 2019 10:08:26 +0530 Subject: [PATCH 051/210] Removed required lead --- .../crm/doctype/appointment/appointment.json | 5 +- erpnext/crm/doctype/lead/lead.json | 1319 ++--------------- 2 files changed, 100 insertions(+), 1224 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 2d695f3199..5ea234437d 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -63,8 +63,7 @@ "fieldname": "lead", "fieldtype": "Link", "label": "Lead", - "options": "Lead", - "reqd": 1 + "options": "Lead" }, { "fieldname": "calendar_event", @@ -73,7 +72,7 @@ "options": "Event" } ], - "modified": "2019-09-13 15:25:49.362246", + "modified": "2019-09-19 16:00:54.390581", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", diff --git a/erpnext/crm/doctype/lead/lead.json b/erpnext/crm/doctype/lead/lead.json index 3c22dc7199..eb68c679ba 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 From df1a5a9633646945351cac034283f6409c9d4958 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 20 Sep 2019 10:08:48 +0530 Subject: [PATCH 052/210] Added flow for verifying emails --- .../crm/doctype/appointment/appointment.py | 95 +++++++++++++------ erpnext/www/book-appointment/index.py | 7 -- .../www/book-appointment/verify/index.html | 18 ++++ erpnext/www/book-appointment/verify/index.py | 14 +++ 4 files changed, 96 insertions(+), 38 deletions(-) create mode 100644 erpnext/www/book-appointment/verify/index.html create mode 100644 erpnext/www/book-appointment/verify/index.py diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 9365301e8f..52711fee84 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -13,28 +13,56 @@ from frappe.desk.form.assign_to import add as add_assignemnt class Appointment(Document): - def validate(self): + email='' + + def find_lead_by_email(self,email): + lead_list = frappe.get_list('Lead', filters = {'email_id':email}, ignore_permissions = True) + if lead_list: + return lead_list[0].name + self.email = email + 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') - - def before_insert(self): - self.lead = _find_lead_by_email(self.lead).name - + # Link lead + self.lead = self.find_lead_by_email(self.lead) def after_insert(self): - appointment_event = frappe.get_doc({ - 'doctype': 'Event', - 'subject': ' '.join(['Appointment with', self.customer_name]), - 'starts_on': self.scheduled_time, - 'status': 'Open', - 'type': 'Private', - 'send_reminder': frappe.db.get_single_value('Appointment Booking Settings','email_reminders'), - 'event_participants': [dict(reference_doctype = "Lead", reference_docname = self.lead)] + # Auto assign + self.auto_assign() + # Check if lead was found + if(self.lead): + # Create Calendar event + self.create_calendar_event() + else: + # Send email to confirm + # frappe.sendmail(recipients=[self.email],message='https:/',subject="") + frappe.msgprint("Please check your email to confirm the appointment") + + def set_verified(self,email): + # Create new lead + self.create_lead(email) + # Create calender event + self.create_calendar_event() + self.save( ignore_permissions=True ) + frappe.db.commit() + + def create_lead(self,email): + lead = frappe.get_doc({ + 'doctype':'Lead', + 'lead_name':self.customer_name, + 'email_id':email, + 'notes':self.customer_details, + 'phone':self.customer_phone_number, }) - appointment_event.insert(ignore_permissions=True) - self.calendar_event = appointment_event.name + print(lead.insert( ignore_permissions=True )) + # Link lead + self.lead = lead.name + + def auto_assign(self): 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)): @@ -44,14 +72,26 @@ class Appointment(Document): 'name':self.name, 'assign_to':agent }) - employee = _get_employee_from_user(agent) - if employee: - calendar_event = frappe.get_doc('Event', self.calendar_event) - calendar_event.append('event_participants', dict( - reference_doctype= 'Employee', - reference_docname= employee.name)) - calendar_event.save() - break + break + + def create_calendar_event(self): + 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='*') @@ -70,13 +110,6 @@ def _get_agents_sorted_by_asc_workload(date): return sorted_agent_list -def _find_lead_by_email(email): - lead_list = frappe.get_list('Lead', filters={'email_id':email}, ignore_permissions=True) - if lead_list: - return lead_list[0] - frappe.throw('Email ID not associated with any Lead. Please make sure to use the email address you got this mail on') - - def _get_agent_list_as_strings(): agent_list_as_strings = [] agent_list = frappe.get_doc('Appointment Booking Settings').agent_list @@ -97,4 +130,4 @@ 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 + return None \ No newline at end of file diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index d5111c8d1b..c1585aaf2f 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -111,18 +111,15 @@ def filter_timeslots(date, timeslots): 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: @@ -130,17 +127,14 @@ def _get_records(start_time, end_time, settings): 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 @@ -148,7 +142,6 @@ def _convert_to_ist(datetime_object, timezone): 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 diff --git a/erpnext/www/book-appointment/verify/index.html b/erpnext/www/book-appointment/verify/index.html new file mode 100644 index 0000000000..ebb65b1f24 --- /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 %} +
+ Your email has been verified and your appointment has been scheduled +
+ {% else %} +
+ Verification failed please check the link +
+ {% 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 0000000000..d25b50565a --- /dev/null +++ b/erpnext/www/book-appointment/verify/index.py @@ -0,0 +1,14 @@ +import frappe +@frappe.whitelist(allow_guest=True) +def get_context(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 From fa4a2a53e8029f35b0194d6f5186478090796607 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 20 Sep 2019 10:41:59 +0530 Subject: [PATCH 053/210] Added email --- erpnext/crm/doctype/appointment/appointment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 52711fee84..dee7c7c32c 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -39,7 +39,8 @@ class Appointment(Document): self.create_calendar_event() else: # Send email to confirm - # frappe.sendmail(recipients=[self.email],message='https:/',subject="") + verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,"&appoitnment=",self.name])) + frappe.sendmail(recipients=[self.email],message=verify_url',self.email,"&appoitnment=",self.name),subject="") frappe.msgprint("Please check your email to confirm the appointment") def set_verified(self,email): From 73420e462f821eeacb33423017a8f8715439788a Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 20 Sep 2019 10:41:59 +0530 Subject: [PATCH 054/210] Added email --- erpnext/crm/doctype/appointment/appointment.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 52711fee84..af2878ec67 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -39,7 +39,8 @@ class Appointment(Document): self.create_calendar_event() else: # Send email to confirm - # frappe.sendmail(recipients=[self.email],message='https:/',subject="") + verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,"&appoitnment=",self.name]) + frappe.sendmail(recipients=[self.email],message=verify_url',self.email,"&appoitnment=",self.name),subject="") frappe.msgprint("Please check your email to confirm the appointment") def set_verified(self,email): From 9c0f46233639768a5fd393943ff4ab8540c72692 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 20 Sep 2019 10:51:56 +0530 Subject: [PATCH 055/210] Fixed Syntax errors --- erpnext/crm/doctype/appointment/appointment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 1ca706c19b..5d8a30fd2f 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -40,7 +40,9 @@ class Appointment(Document): else: # Send email to confirm verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,"&appoitnment=",self.name]) - frappe.sendmail(recipients=[self.email],message=verify_url,self.email,"&appoitnment=",self.name),subject="") + frappe.sendmail(recipients=[self.email], + message=verify_url, + subject="") frappe.msgprint("Please check your email to confirm the appointment") def set_verified(self,email): From 6b0fea16b64806bdfcc1e5f391ce8fd0a5d82fab Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 11:26:18 +0530 Subject: [PATCH 056/210] Added buttons to linked docs --- erpnext/crm/doctype/appointment/appointment.js | 15 ++++++++++++--- 1 file changed, 12 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js index 4e41047fa1..975abfcd93 100644 --- a/erpnext/crm/doctype/appointment/appointment.js +++ b/erpnext/crm/doctype/appointment/appointment.js @@ -2,7 +2,16 @@ // For license information, please see license.txt frappe.ui.form.on('Appointment', { - // refresh: function(frm) { - - // } + 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) + }) + } + } }); From 0800031c0d712d9d24e34703beedc8defe8660db Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 11:26:46 +0530 Subject: [PATCH 057/210] Addee email to appointment doctyoe and asthetic changes --- .../crm/doctype/appointment/appointment.json | 27 +++++++++++++++++-- 1 file changed, 25 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 5ea234437d..22df5c6aa8 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -11,8 +11,12 @@ "customer_name", "customer_phone_number", "customer_skype", + "customer_email", + "col_br_2", "customer_details", + "linked_docs_section", "lead", + "col_br_3", "calendar_event" ], "fields": [ @@ -56,7 +60,7 @@ "fieldname": "status", "fieldtype": "Select", "label": "Status", - "options": "Open\nClosed", + "options": "Open\nUnverified\nClosed", "reqd": 1 }, { @@ -70,9 +74,28 @@ "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-19 16:00:54.390581", + "modified": "2019-09-23 10:57:04.876506", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From f8cc86bfedb9a0e8ec1945e56c320e44d27f4cbb Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 11:28:05 +0530 Subject: [PATCH 058/210] Moved email from class variable to doctype Formatting Made methods which link other doctypes idempotent --- .../crm/doctype/appointment/appointment.py | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5d8a30fd2f..5e0648659b 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -13,10 +13,9 @@ from frappe.desk.form.assign_to import add as add_assignemnt class Appointment(Document): - email='' - def find_lead_by_email(self,email): - lead_list = frappe.get_list('Lead', filters = {'email_id':email}, ignore_permissions = True) + def find_lead_by_email(self): + lead_list = frappe.get_list('Lead', filters = {'email_id':self.email}, ignore_permissions = True) if lead_list: return lead_list[0].name self.email = email @@ -28,7 +27,7 @@ class Appointment(Document): 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(self.lead) + self.lead = self.find_lead_by_email() def after_insert(self): # Auto assign @@ -38,22 +37,35 @@ class Appointment(Document): # Create Calendar event self.create_calendar_event() else: + # Set status to unverified + self.status = 'Unverified' # Send email to confirm - verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,"&appoitnment=",self.name]) + verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,'&appoitnment=',self.name]) + message = ''.join(['Please click the following link to confirm your appointment:']+verify_url) frappe.sendmail(recipients=[self.email], - message=verify_url, - subject="") - frappe.msgprint("Please check your email to confirm the appointment") + message=message, + subject=_('Appointment Confirmation')) + frappe.msgprint('Please check your email to confirm the appointment') + + def on_update(): + # Sync Calednar + cal_event = frappe.get_doc('Event,self.calendar_event def set_verified(self,email): + if not email == self.email: + frappe.throw('Email verification failed.') # Create new lead - self.create_lead(email) + self.create_lead() # Create calender event + self.status = 'Open' self.create_calendar_event() - self.save( ignore_permissions=True ) + self.save(ignore_permissions=True) frappe.db.commit() def create_lead(self,email): + # Return if already linked + if self.lead: + return lead = frappe.get_doc({ 'doctype':'Lead', 'lead_name':self.customer_name, @@ -61,11 +73,13 @@ class Appointment(Document): 'notes':self.customer_details, 'phone':self.customer_phone_number, }) - print(lead.insert( ignore_permissions=True )) + 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)): @@ -78,6 +92,8 @@ class Appointment(Document): break def create_calendar_event(self): + if self.appointment: + return appointment_event = frappe.get_doc({ 'doctype': 'Event', 'subject': ' '.join(['Appointment with', self.customer_name]), @@ -85,7 +101,7 @@ class Appointment(Document): '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)] + 'event_participants': [dict(reference_doctype = 'Lead', reference_docname = self.lead)] }) employee = _get_employee_from_user(self._assign) if employee: @@ -110,7 +126,6 @@ def _get_agents_sorted_by_asc_workload(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(): @@ -120,7 +135,6 @@ def _get_agent_list_as_strings(): 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: @@ -128,7 +142,6 @@ def _check_agent_availability(agent_email,scheduled_time): return False return True - def _get_employee_from_user(user): employee_docname = frappe.db.exists({'doctype':'Employee', 'user_id':user}) if employee_docname: From d9ab09ab2b169873f17b49ddd8994462bf39f990 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 11:28:17 +0530 Subject: [PATCH 059/210] Moved email to appoitnmetn doctype --- erpnext/www/book-appointment/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index c1585aaf2f..67619fc5d5 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -98,7 +98,7 @@ def create_appointment(date, time, contact): appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] - appointment.lead = contact['email'] + appointment.email = contact['email'] appointment.status = 'Open' appointment.insert() From dcfc849946f5b1f0700b13f40bee008e333d0ba4 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 11:28:17 +0530 Subject: [PATCH 060/210] Moved email to appoitnmetn doctype --- erpnext/crm/doctype/appointment/appointment.py | 4 +++- erpnext/www/book-appointment/index.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5e0648659b..1095b56ae9 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -49,7 +49,9 @@ class Appointment(Document): def on_update(): # Sync Calednar - cal_event = frappe.get_doc('Event,self.calendar_event + cal_event = frappe.get_doc('Event',self.calendar_event) + cal_event.starts_on = self.scheduled_time + cal_event.save() def set_verified(self,email): if not email == self.email: diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index c1585aaf2f..67619fc5d5 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -98,7 +98,7 @@ def create_appointment(date, time, contact): appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] - appointment.lead = contact['email'] + appointment.email = contact['email'] appointment.status = 'Open' appointment.insert() From 7b7962d28c7f53731f0aa7c0e21c9f4b23aa59e3 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 13:05:18 +0530 Subject: [PATCH 061/210] Added test cases --- .../doctype/appointment/test_appointment.py | 44 +++++++++++++++++-- 1 file changed, 40 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 3c977505b5..d529d37aad 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -7,10 +7,46 @@ 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 delete_appointments(): - pass - +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): - pass + 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 From b6b27d9256be2c3d72522c6baf647fcac1fd0bfd Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 14:16:13 +0530 Subject: [PATCH 062/210] Corrected moving to doctype for email --- erpnext/crm/doctype/appointment/appointment.py | 12 ++++++------ erpnext/www/book-appointment/index.py | 2 +- 2 files changed, 7 insertions(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 1095b56ae9..219f93111a 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -8,6 +8,7 @@ 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 @@ -15,10 +16,9 @@ from frappe.desk.form.assign_to import add as add_assignemnt class Appointment(Document): def find_lead_by_email(self): - lead_list = frappe.get_list('Lead', filters = {'email_id':self.email}, ignore_permissions = True) + lead_list = frappe.get_list('Lead', filters = {'email_id':self.customer_email}, ignore_permissions = True) if lead_list: return lead_list[0].name - self.email = email return None def before_insert(self): @@ -40,9 +40,9 @@ class Appointment(Document): # Set status to unverified self.status = 'Unverified' # Send email to confirm - verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.email,'&appoitnment=',self.name]) - message = ''.join(['Please click the following link to confirm your appointment:']+verify_url) - frappe.sendmail(recipients=[self.email], + verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.customer_email,'&appoitnment=',self.name]) + 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') @@ -54,7 +54,7 @@ class Appointment(Document): cal_event.save() def set_verified(self,email): - if not email == self.email: + if not email == self.customer_email: frappe.throw('Email verification failed.') # Create new lead self.create_lead() diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 67619fc5d5..49b3ffc2cf 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -98,7 +98,7 @@ def create_appointment(date, time, contact): appointment.customer_phone_number = contact['number'] appointment.customer_skype = contact['skype'] appointment.customer_details = contact['notes'] - appointment.email = contact['email'] + appointment.customer_email = contact['email'] appointment.status = 'Open' appointment.insert() From e40b1001104614275b6e622cbbee0cbcd753b3d4 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 14:23:04 +0530 Subject: [PATCH 063/210] Fixed update method --- erpnext/crm/doctype/appointment/appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 219f93111a..b259758e02 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -47,7 +47,7 @@ class Appointment(Document): subject=_('Appointment Confirmation')) frappe.msgprint('Please check your email to confirm the appointment') - def on_update(): + def on_update(self): # Sync Calednar cal_event = frappe.get_doc('Event',self.calendar_event) cal_event.starts_on = self.scheduled_time From 3eccb84eaa4240c84049447440041b5c5992bb41 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 14:23:04 +0530 Subject: [PATCH 064/210] Fixed update method --- erpnext/crm/doctype/appointment/appointment.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 219f93111a..95c7f35fbb 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -47,8 +47,10 @@ class Appointment(Document): subject=_('Appointment Confirmation')) frappe.msgprint('Please check your email to confirm the appointment') - def on_update(): + 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() From a35e34b5f0f36323fd0941f5eb2070a5c4510622 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 14:38:22 +0530 Subject: [PATCH 065/210] FIxed typos and create_lead method --- erpnext/crm/doctype/appointment/appointment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 95c7f35fbb..260026c495 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -40,7 +40,7 @@ class Appointment(Document): # Set status to unverified self.status = 'Unverified' # Send email to confirm - verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.customer_email,'&appoitnment=',self.name]) + verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.customer_email,'&appointment=',self.name]) message = ''.join(['Please click the following link to confirm your appointment:',verify_url]) frappe.sendmail(recipients=[self.customer_email], message=message, @@ -66,14 +66,14 @@ class Appointment(Document): self.save(ignore_permissions=True) frappe.db.commit() - def create_lead(self,email): + 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':email, + 'email_id':self.customer_email, 'notes':self.customer_details, 'phone':self.customer_phone_number, }) @@ -96,7 +96,7 @@ class Appointment(Document): break def create_calendar_event(self): - if self.appointment: + if self.calendar_event: return appointment_event = frappe.get_doc({ 'doctype': 'Event', From 8b744b2d03eb88b4674503836c3fa2da66674de6 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 15:55:35 +0530 Subject: [PATCH 066/210] Added request verification and url encoding --- erpnext/crm/doctype/appointment/appointment.py | 18 ++++++++++++++++-- erpnext/www/book-appointment/verify/index.py | 6 ++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 260026c495..a495b910e8 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -4,6 +4,7 @@ from __future__ import unicode_literals +import urllib from collections import Counter from datetime import timedelta @@ -11,6 +12,8 @@ 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): @@ -40,13 +43,23 @@ class Appointment(Document): # Set status to unverified self.status = 'Unverified' # Send email to confirm - verify_url = ''.join([frappe.utils.get_url(),'/book-appointment/verify?email=',self.customer_email,'&appointment=',self.name]) + 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: @@ -60,8 +73,9 @@ class Appointment(Document): frappe.throw('Email verification failed.') # Create new lead self.create_lead() - # Create calender event + # Remove unverified status self.status = 'Open' + # Create calender event self.create_calendar_event() self.save(ignore_permissions=True) frappe.db.commit() diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py index d25b50565a..86f9515332 100644 --- a/erpnext/www/book-appointment/verify/index.py +++ b/erpnext/www/book-appointment/verify/index.py @@ -1,8 +1,14 @@ 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) From 8393ebbbca6c262c635a5504c4f52ddea7604ad3 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 23 Sep 2019 17:14:31 +0530 Subject: [PATCH 067/210] Fixed missing permission in update --- erpnext/crm/doctype/appointment/appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index a495b910e8..2f140989a7 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -66,7 +66,7 @@ class Appointment(Document): return cal_event = frappe.get_doc('Event',self.calendar_event) cal_event.starts_on = self.scheduled_time - cal_event.save() + cal_event.save(ignore_permissions=True) def set_verified(self,email): if not email == self.customer_email: From 558d44e519d64b59f341802acd193db447244421 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 24 Sep 2019 11:33:57 +0530 Subject: [PATCH 068/210] Removed auto-assignment for unverified appointments --- erpnext/crm/doctype/appointment/appointment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2f140989a7..f32699e786 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -33,12 +33,10 @@ class Appointment(Document): 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() + self.auto_assign() else: # Set status to unverified self.status = 'Unverified' @@ -77,6 +75,7 @@ class Appointment(Document): self.status = 'Open' # Create calender event self.create_calendar_event() + self.auto_assign() self.save(ignore_permissions=True) frappe.db.commit() From c9cf5aebeaa159580b1ed6e35c1155de9602063d Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 24 Sep 2019 12:08:37 +0530 Subject: [PATCH 069/210] Changed required values, add clientside validation --- erpnext/crm/doctype/appointment/appointment.json | 8 +++----- erpnext/www/book-appointment/index.html | 14 ++++++-------- erpnext/www/book-appointment/index.js | 7 ++++++- 3 files changed, 15 insertions(+), 14 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 22df5c6aa8..9dfcc57197 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -35,14 +35,12 @@ { "fieldname": "customer_phone_number", "fieldtype": "Data", - "label": "Phone Number", - "reqd": 1 + "label": "Phone Number" }, { "fieldname": "customer_skype", "fieldtype": "Data", - "label": "Skype ID", - "reqd": 1 + "label": "Skype ID" }, { "fieldname": "customer_details", @@ -95,7 +93,7 @@ "fieldtype": "Column Break" } ], - "modified": "2019-09-23 10:57:04.876506", + "modified": "2019-09-24 11:44:21.228104", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 2e0321394e..1f6dd2e0e6 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -45,16 +45,14 @@
- - - - +
+ + + + +
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 345e614154..6034f4eb48 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -195,7 +195,11 @@ function setup_details_page() { } async function submit() { - // form validation here + let form = document.querySelector('#customer-form'); + if(!form.checkValidity()){ + form.reportValidity(); + return; + } get_form_data(); let appointment = (await frappe.call({ method: 'erpnext.www.book-appointment.index.create_appointment', @@ -212,6 +216,7 @@ async function submit() { } function get_form_data() { + contact = {}; contact.name = document.getElementById('customer_name').value; contact.number = document.getElementById('customer_number').value; From d45c12b38265e8b2bb28becfac5c247793713cac Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 24 Sep 2019 16:07:02 +0530 Subject: [PATCH 070/210] Formatting --- erpnext/www/book-appointment/verify/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py index 86f9515332..8ea96383a3 100644 --- a/erpnext/www/book-appointment/verify/index.py +++ b/erpnext/www/book-appointment/verify/index.py @@ -5,7 +5,7 @@ def get_context(context): if not verify_request(): context.success = False return context - + email = frappe.form_dict['email'] appointment_name = frappe.form_dict['appointment'] From 9f86022c2b68268a7bd69d7f1c5f3ca099048a32 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Tue, 24 Sep 2019 16:07:41 +0530 Subject: [PATCH 071/210] fix: Error in test setUp --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index d529d37aad..bc7fe72e90 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -35,7 +35,7 @@ def create_test_appointments(): return test_appointment class TestAppointment(unittest.TestCase): - test_appointment,test_lead = None + test_appointment = test_lead = None def setUp(self): test_lead = create_test_lead() test_appointment = test_create_test_appointments() From 291e1617935e4551dcc9f32b9981d8188e50d13f Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 25 Sep 2019 13:11:04 +0530 Subject: [PATCH 072/210] Added permissions for sales user --- .../crm/doctype/appointment/appointment.json | 25 ++++++++++++++++++- .../appointment_booking_settings.json | 12 ++++++++- 2 files changed, 35 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 9dfcc57197..323e096b40 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -93,7 +93,7 @@ "fieldtype": "Column Break" } ], - "modified": "2019-09-24 11:44:21.228104", + "modified": "2019-09-25 13:08:46.368307", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", @@ -121,6 +121,29 @@ "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, diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index d72f577656..4229e4b788 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -65,7 +65,7 @@ } ], "issingle": 1, - "modified": "2019-09-19 12:36:34.011724", + "modified": "2019-09-25 13:08:28.328561", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", @@ -96,6 +96,16 @@ "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, From fd46bf261624a8e0ade9a54698ac0924446fe8a4 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 25 Sep 2019 16:01:48 +0530 Subject: [PATCH 073/210] fix codacy --- erpnext/crm/doctype/appointment/appointment.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js index 975abfcd93..485520f562 100644 --- a/erpnext/crm/doctype/appointment/appointment.js +++ b/erpnext/crm/doctype/appointment/appointment.js @@ -5,13 +5,13 @@ 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) - }) + 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) - }) + frappe.set_route("Form","Event",frm.doc.calendar_event); + }); } } }); From 250bae260380e5fba2bce41e0f1664157cdf8d72 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 30 Sep 2019 12:40:25 +0530 Subject: [PATCH 074/210] fix:appointment tests exist check --- erpnext/crm/doctype/appointment/test_appointment.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index bc7fe72e90..d73c6ec035 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -19,7 +19,10 @@ def create_test_lead(): return test_lead def create_test_appointments(): - if frappe.db.exists('Appointment',filters={'email':'test@example.com'}): + if frappe.db.exists({ + 'doctype':'Appointment', + 'email':'test@example.com' + }): return test_appointment = frappe.get_doc({ 'doctype':'Appointment', From 7f4bc64d22a78db2b20d1d945a4555194951c8fa Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 30 Sep 2019 12:40:25 +0530 Subject: [PATCH 075/210] fix:appointment tests exist check --- erpnext/crm/doctype/appointment/test_appointment.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index bc7fe72e90..9b87c792cd 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,7 +8,7 @@ import unittest import datetime def create_test_lead(): - if frappe.db.exists('Lead',filters={'lead_name':'Test Lead'}): + if frappe.db.exists({'doctype:''Lead','lead_name':'Test Lead'}): return test_lead = frappe.get_doc({ 'doctype':'Lead', @@ -19,7 +19,10 @@ def create_test_lead(): return test_lead def create_test_appointments(): - if frappe.db.exists('Appointment',filters={'email':'test@example.com'}): + if frappe.db.exists({ + 'doctype':'Appointment', + 'email':'test@example.com' + }): return test_appointment = frappe.get_doc({ 'doctype':'Appointment', From 2ea9b3e6f20a502643e231853350fbd04372cac0 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Mon, 30 Sep 2019 15:35:38 +0530 Subject: [PATCH 076/210] fix:test appointments --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 9b87c792cd..f6385bfba2 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,7 +8,7 @@ import unittest import datetime def create_test_lead(): - if frappe.db.exists({'doctype:''Lead','lead_name':'Test Lead'}): + if frappe.db.exists({'doctype':'Lead','lead_name':'Test Lead'}): return test_lead = frappe.get_doc({ 'doctype':'Lead', From c6da5fb38e72a4dc999642e7083c2e26414cb8b1 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 11:56:23 +0530 Subject: [PATCH 077/210] fix:guess timezone using moment --- erpnext/www/book-appointment/index.js | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 6034f4eb48..1b7a80154e 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -33,9 +33,11 @@ function setup_timezone_selector() { 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) + opt.value = timezone; + if(timezone == moment.tz.guess()){ + opt.selected = true; + } + opt.innerHTML = timezone; timezones_element.appendChild(opt) }); } From 1dcedb5054203c6e6a81302bfbbaa2758193cefb Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 11:56:54 +0530 Subject: [PATCH 078/210] fix: empty leads and appointment in test --- erpnext/crm/doctype/appointment/test_appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index f6385bfba2..7e7f67c700 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -9,7 +9,7 @@ import datetime def create_test_lead(): if frappe.db.exists({'doctype':'Lead','lead_name':'Test Lead'}): - return + return frappe.get_doc('Lead','Test Lead') test_lead = frappe.get_doc({ 'doctype':'Lead', 'lead_name':'Test Lead', @@ -23,7 +23,7 @@ def create_test_appointments(): 'doctype':'Appointment', 'email':'test@example.com' }): - return + return frappe.get_doc('Appointment','Test Appointment') test_appointment = frappe.get_doc({ 'doctype':'Appointment', 'email':'test@example.com', From 93670fedda62a3a91517e5c80deb66f1728bc0b2 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 11:58:02 +0530 Subject: [PATCH 079/210] timezone manipulation using pytz --- erpnext/www/book-appointment/index.py | 41 +++++++++++++-------------- 1 file changed, 19 insertions(+), 22 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 49b3ffc2cf..5f51ced96e 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -1,6 +1,7 @@ import frappe import datetime import json +import pytz WEEKDAYS = ["Monday", "Tuesday", "Wednesday", @@ -24,20 +25,26 @@ def get_holiday_list(holiday_list_name): @frappe.whitelist(allow_guest=True) def get_timezones(): timezones = frappe.get_list('Timezone', fields='*') - return timezones + return pytz.all_timezones @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): - timezone = int(timezone) + import pytz + guest_timezone = pytz.timezone(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) + local_timezone = frappe.utils.get_time_zone() + local_timezone = pytz.timezone(local_timezone) + query_start_time = guest_timezone.localize(query_start_time) + query_end_time = guest_timezone.localize(query_end_time) + query_start_time = query_start_time.astimezone(local_timezone) + query_end_time = query_end_time.astimezone(local_timezone) now = datetime.datetime.now() + # now = local_timezone.localize(now) # Database queries settings = frappe.get_doc('Appointment Booking Settings') holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) @@ -47,18 +54,22 @@ def get_appointment_slots(date, timezone): # Filter timeslots based on date converted_timeslots = [] for timeslot in timeslots: + timeslot = local_timezone.localize(timeslot) + print(timeslot) + timeslot = timeslot.astimezone(guest_timezone) + timeslot = timeslot.replace(tzinfo=None) # Check if holiday if _is_holiday(timeslot.date(), holiday_list): converted_timeslots.append( - dict(time=_convert_to_tz(timeslot, timezone), availability=False)) + dict(time=timeslot, 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)) + dict(time=timeslot, availability=True)) else: converted_timeslots.append( - dict(time=_convert_to_tz(timeslot, timezone), availability=False)) + dict(time=timeslot, availability=False)) date_required = datetime.datetime.strptime( date + ' 00:00:00', format_string).date() converted_timeslots = filter_timeslots(date_required, converted_timeslots) @@ -133,18 +144,4 @@ def _deltatime_to_datetime(date, deltatime): 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 + return (date_time-midnight) \ No newline at end of file From c5420bb4535bfc9006f9ed035441bc76b852a2dc Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 12:06:43 +0530 Subject: [PATCH 080/210] fix: remove validation for repeated days --- erpnext/crm/doctype/appointment/appointment.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index f32699e786..4dcb2016ac 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -58,7 +58,7 @@ class Appointment(Document): return get_url(verify_route + '?' + get_signed_params(params)) - def on_update(self): + def on_change(self): # Sync Calednar if not self.calendar_event: return @@ -66,6 +66,12 @@ class Appointment(Document): cal_event.starts_on = self.scheduled_time cal_event.save(ignore_permissions=True) + def on_trash(self): + # Delete calendar event + cal_event = frappe.get_doc('Event',self.calendar_event) + if cal_event: + cal_event.delete() + # Delete task? def set_verified(self,email): if not email == self.customer_email: frappe.throw('Email verification failed.') From 4856645b6d4c38db3c9edd5e6cd15410c43abd5f Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 12:45:42 +0530 Subject: [PATCH 081/210] fix:styling for time-slot --- erpnext/www/book-appointment/index.html | 2 +- erpnext/www/book-appointment/index.js | 7 ------- 2 files changed, 1 insertion(+), 8 deletions(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 1f6dd2e0e6..e1355a7765 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -26,7 +26,7 @@
-
+
diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 1b7a80154e..f2496da5a6 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -99,12 +99,6 @@ async function update_time_slots(selected_date, selected_timezone) { 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); @@ -116,7 +110,6 @@ 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') } From 76cbb9132f6126d22e4d5f9375d2364988395c4f Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 12:50:55 +0530 Subject: [PATCH 082/210] fix: more test errors --- erpnext/crm/doctype/appointment/test_appointment.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 7e7f67c700..ec16d6a582 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -8,8 +8,9 @@ import unittest import datetime def create_test_lead(): - if frappe.db.exists({'doctype':'Lead','lead_name':'Test Lead'}): - return frappe.get_doc('Lead','Test Lead') + test_lead = frappe.db.exists({'doctype':'Lead','lead_name':'Test Lead'}) + if test_lead: + return frappe.get_doc('Lead',test_lead) test_lead = frappe.get_doc({ 'doctype':'Lead', 'lead_name':'Test Lead', @@ -19,11 +20,9 @@ def create_test_lead(): return test_lead def create_test_appointments(): - if frappe.db.exists({ - 'doctype':'Appointment', - 'email':'test@example.com' - }): - return frappe.get_doc('Appointment','Test Appointment') + test_appointment = frappe.db.exists({ 'doctype':'Appointment', 'email':'test@example.com' }) + if test_appointment: + return frappe.get_doc('Appointment',test_appointment) test_appointment = frappe.get_doc({ 'doctype':'Appointment', 'email':'test@example.com', From 59c543570a411b2d647a88f3aceb74a69250ca00 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 13:18:13 +0530 Subject: [PATCH 083/210] feat: made timeslots into flex --- erpnext/www/book-appointment/index.css | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index a6e6313f79..d5202065ea 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -7,6 +7,12 @@ border: 0.5px solid #cccccc; min-height: 75px; padding: 0.5em 1em; + +} + +#timeslot-container{ + display: grid; + grid-template-columns: repeat(6, 1fr); } .time-slot:hover { From 1dccc039b720aa4ff82c0a45794fc695899861b8 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 13:32:46 +0530 Subject: [PATCH 084/210] fix:add tear down to tests --- erpnext/crm/doctype/appointment/test_appointment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index ec16d6a582..5670a7e52e 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -40,10 +40,11 @@ class TestAppointment(unittest.TestCase): test_appointment = test_lead = None def setUp(self): test_lead = create_test_lead() - test_appointment = test_create_test_appointments() + test_appointment = create_test_appointments() def tearDown(self): - pass + test_appointment.delete() + test_lead.delete() def test_calendar_event_created(self): cal_event = frappe.get_doc('Event',test_appointment.calendar_event) From 8640a01f8535df351ba59ffdbf2991cf73daf7dd Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 13:32:57 +0530 Subject: [PATCH 085/210] remove duplicate day validation --- .../appointment_booking_settings.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index da181ae119..6a1cf56cb7 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -26,10 +26,4 @@ class AppointmentBookingSettings(Document): 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')) - + frappe.throw('The difference between from time and To Time must be a multiple of Appointment ') \ No newline at end of file From 42cf5f279f5b03f856b4ec35fbe386dcac9a7938 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 14:26:21 +0530 Subject: [PATCH 086/210] fix:added class variables to test --- erpnext/crm/doctype/appointment/test_appointment.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 5670a7e52e..111ab08d23 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -39,12 +39,12 @@ def create_test_appointments(): class TestAppointment(unittest.TestCase): test_appointment = test_lead = None def setUp(self): - test_lead = create_test_lead() - test_appointment = create_test_appointments() + self.test_lead = create_test_lead() + self.test_appointment = create_test_appointments() def tearDown(self): - test_appointment.delete() - test_lead.delete() + self.test_appointment.delete() + self.test_lead.delete() def test_calendar_event_created(self): cal_event = frappe.get_doc('Event',test_appointment.calendar_event) From 43331564b42614e823ad660c198e2b734ad26557 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 15:29:09 +0530 Subject: [PATCH 087/210] fix:class variable in tests --- erpnext/crm/doctype/appointment/test_appointment.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 111ab08d23..f1aa610548 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -44,11 +44,10 @@ class TestAppointment(unittest.TestCase): def tearDown(self): self.test_appointment.delete() - self.test_lead.delete() 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) + 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.lead) From 72aac09d62d92db73a6810be43ac74c99f6ecfa3 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 16:09:50 +0530 Subject: [PATCH 088/210] fix:remove tearDown from test --- erpnext/crm/doctype/appointment/test_appointment.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index f1aa610548..d7731bec87 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -42,9 +42,6 @@ class TestAppointment(unittest.TestCase): self.test_lead = create_test_lead() self.test_appointment = create_test_appointments() - def tearDown(self): - self.test_appointment.delete() - 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) From afe52e8e09008c5a212cd21508c1e9f33ac73bda Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 16:35:08 +0530 Subject: [PATCH 089/210] feat: add check for toggling the route --- .../appointment_booking_settings.json | 10 +++++++++- erpnext/www/book-appointment/index.js | 18 ++++++++++++++---- erpnext/www/book-appointment/index.py | 5 +++++ 3 files changed, 28 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 4229e4b788..90f3ad9410 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -4,6 +4,7 @@ "editable_grid": 1, "engine": "InnoDB", "field_order": [ + "enable_scheduling", "availability_of_slots", "number_of_agents", "holiday_list", @@ -62,10 +63,17 @@ "label": "Agents", "options": "Assignment Rule User", "reqd": 1 + }, + { + "default": "0", + "fieldname": "enable_scheduling", + "fieldtype": "Check", + "label": "Enable Appointment Scheduling", + "reqd": 1 } ], "issingle": 1, - "modified": "2019-09-25 13:08:28.328561", + "modified": "2019-10-03 14:52:33.076253", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index f2496da5a6..07355e16ff 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -1,7 +1,17 @@ - -frappe.ready(() => { - initialise_select_date() +frappe.ready(async () => { + debugger + let isSchedulingEnabled = await frappe.call({ + method:'erpnext.www.book-appointment.index.is_enabled' + }) + isSchedulingEnabled = isSchedulingEnabled.message + if (!isSchedulingEnabled) { + frappe.show_alert("This feature is not enabled"); + window.location.replace('/'); + return; + } + initialise_select_date(); }) + window.holiday_list = []; async function initialise_select_date() { @@ -16,7 +26,7 @@ async function get_global_variables() { // Using await window.appointment_settings = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_appointment_settings' - })).message + })).message; window.timezones = (await frappe.call({ method: 'erpnext.www.book-appointment.index.get_timezones' })).message; diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 5f51ced96e..946cc1b192 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -15,6 +15,11 @@ def get_appointment_settings(): settings = frappe.get_doc('Appointment Booking Settings') return settings +@frappe.whitelist(allow_guest=True) +def is_enabled(): + enable_scheduling = frappe.db.get_single_value('Appointment Booking Settings','enable_scheduling') + return enable_scheduling + @frappe.whitelist(allow_guest=True) def get_holiday_list(holiday_list_name): From bec88bc52a0eb410353b68c10c946c58360dabf2 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 16:58:38 +0530 Subject: [PATCH 090/210] fix: exists return tuple not string --- erpnext/crm/doctype/appointment/test_appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index d7731bec87..16a370eb7b 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -10,7 +10,7 @@ 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) + return frappe.get_doc('Lead',test_lead[0][0]) test_lead = frappe.get_doc({ 'doctype':'Lead', 'lead_name':'Test Lead', @@ -22,7 +22,7 @@ def create_test_lead(): def create_test_appointments(): test_appointment = frappe.db.exists({ 'doctype':'Appointment', 'email':'test@example.com' }) if test_appointment: - return frappe.get_doc('Appointment',test_appointment) + return frappe.get_doc('Appointment',test_appointment[0][0]) test_appointment = frappe.get_doc({ 'doctype':'Appointment', 'email':'test@example.com', From d40c020e0e461ef4a165966ede267e6452c033eb Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 17:43:31 +0530 Subject: [PATCH 091/210] fix:variable names --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 16a370eb7b..852e0f1e06 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -47,5 +47,5 @@ class TestAppointment(unittest.TestCase): self.assertEqual(cal_event.starts_on ,self.test_appointment.scheduled_time) def test_lead_linked(self): - lead = frappe.get_doc('Lead',self.lead) + lead = frappe.get_doc('Lead',self.test_lead) self.assertIsNotNone(lead) \ No newline at end of file From a1d39cab21bef14cd71e316808fabce7a39a234c Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Thu, 3 Oct 2019 18:26:02 +0530 Subject: [PATCH 092/210] fix: travis --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 852e0f1e06..b22e6e0087 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -47,5 +47,5 @@ class TestAppointment(unittest.TestCase): self.assertEqual(cal_event.starts_on ,self.test_appointment.scheduled_time) def test_lead_linked(self): - lead = frappe.get_doc('Lead',self.test_lead) + lead = frappe.get_doc('Lead',self.test_lead.name) self.assertIsNotNone(lead) \ No newline at end of file From 22189ec9e83f35232604a937738af42ef948e27a Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 11:07:04 +0530 Subject: [PATCH 093/210] remove unnecessary doctype 'Timezone' --- erpnext/crm/doctype/timezone/__init__.py | 0 erpnext/crm/doctype/timezone/test_timezone.py | 10 --- erpnext/crm/doctype/timezone/timezone.js | 8 --- erpnext/crm/doctype/timezone/timezone.json | 61 ------------------- erpnext/crm/doctype/timezone/timezone.py | 15 ----- erpnext/www/book-appointment/index.py | 1 - 6 files changed, 95 deletions(-) delete mode 100644 erpnext/crm/doctype/timezone/__init__.py delete mode 100644 erpnext/crm/doctype/timezone/test_timezone.py delete mode 100644 erpnext/crm/doctype/timezone/timezone.js delete mode 100644 erpnext/crm/doctype/timezone/timezone.json delete mode 100644 erpnext/crm/doctype/timezone/timezone.py diff --git a/erpnext/crm/doctype/timezone/__init__.py b/erpnext/crm/doctype/timezone/__init__.py deleted file mode 100644 index e69de29bb2..0000000000 diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py deleted file mode 100644 index 92a8889cce..0000000000 --- a/erpnext/crm/doctype/timezone/test_timezone.py +++ /dev/null @@ -1,10 +0,0 @@ -# -*- 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 deleted file mode 100644 index 4dc57db2ed..0000000000 --- a/erpnext/crm/doctype/timezone/timezone.js +++ /dev/null @@ -1,8 +0,0 @@ -// 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 deleted file mode 100644 index b998e6c21d..0000000000 --- a/erpnext/crm/doctype/timezone/timezone.json +++ /dev/null @@ -1,61 +0,0 @@ -{ - "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 deleted file mode 100644 index 539ffa2547..0000000000 --- a/erpnext/crm/doctype/timezone/timezone.py +++ /dev/null @@ -1,15 +0,0 @@ -# -*- 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.py b/erpnext/www/book-appointment/index.py index 946cc1b192..5f03e777e3 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -29,7 +29,6 @@ def get_holiday_list(holiday_list_name): @frappe.whitelist(allow_guest=True) def get_timezones(): - timezones = frappe.get_list('Timezone', fields='*') return pytz.all_timezones From faf39ecef46b9b4764fcee41f86faf01933c29b8 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 11:12:50 +0530 Subject: [PATCH 094/210] fix:removed print statements --- erpnext/www/book-appointment/index.py | 1 - erpnext/www/book-appointment/verify/index.py | 1 - 2 files changed, 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 5f03e777e3..a810a2b323 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -59,7 +59,6 @@ def get_appointment_slots(date, timezone): converted_timeslots = [] for timeslot in timeslots: timeslot = local_timezone.localize(timeslot) - print(timeslot) timeslot = timeslot.astimezone(guest_timezone) timeslot = timeslot.replace(tzinfo=None) # Check if holiday diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py index 8ea96383a3..6eda19f925 100644 --- a/erpnext/www/book-appointment/verify/index.py +++ b/erpnext/www/book-appointment/verify/index.py @@ -15,6 +15,5 @@ def get_context(context): context.success = True return context else: - print('Something not found') context.success = False return context \ No newline at end of file From 9e36a9ee043d8c6c24adb2c23d04aed4b8355d7c Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 11:28:29 +0530 Subject: [PATCH 095/210] fix: move enable check to serverside --- erpnext/www/book-appointment/index.js | 10 ---------- erpnext/www/book-appointment/index.py | 6 ++++++ 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 07355e16ff..cfacc79dc2 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -1,14 +1,4 @@ frappe.ready(async () => { - debugger - let isSchedulingEnabled = await frappe.call({ - method:'erpnext.www.book-appointment.index.is_enabled' - }) - isSchedulingEnabled = isSchedulingEnabled.message - if (!isSchedulingEnabled) { - frappe.show_alert("This feature is not enabled"); - window.location.replace('/'); - return; - } initialise_select_date(); }) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index a810a2b323..1a9afa577e 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -9,6 +9,12 @@ WEEKDAYS = ["Monday", "Tuesday", "Wednesday", 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: + raise frappe.DoesNotExistError @frappe.whitelist(allow_guest=True) def get_appointment_settings(): From 25148d0de50937cead2ad27ce53e675335d091cd Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 11:32:39 +0530 Subject: [PATCH 096/210] fix:readability --- .../crm/doctype/appointment/appointment.py | 287 +++++++++--------- .../doctype/appointment/test_appointment.py | 45 +-- erpnext/www/book-appointment/index.js | 7 +- erpnext/www/book-appointment/index.py | 16 +- 4 files changed, 192 insertions(+), 163 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 4dcb2016ac..2da4acc1f5 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -13,160 +13,173 @@ 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 +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 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 after_insert(self): - if(self.lead): - # Create Calendar event - self.create_calendar_event() - self.auto_assign() - 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 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 get_verify_url(self): - verify_route = '/book-appointment/verify' + def after_insert(self): + if(self.lead): + # Create Calendar event + self.create_calendar_event() + self.auto_assign() + 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') - params = { - 'email':self.customer_email, - 'appointment':self.name - } + def get_verify_url(self): + verify_route = '/book-appointment/verify' - return get_url(verify_route + '?' + get_signed_params(params)) + params = { + 'email': self.customer_email, + 'appointment': self.name + } - def on_change(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) + return get_url(verify_route + '?' + get_signed_params(params)) - def on_trash(self): - # Delete calendar event - cal_event = frappe.get_doc('Event',self.calendar_event) - if cal_event: - cal_event.delete() - # Delete task? - 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.auto_assign() - self.save(ignore_permissions=True) - frappe.db.commit() + def on_change(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 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 on_trash(self): + # Delete calendar event + cal_event = frappe.get_doc('Event', self.calendar_event) + if cal_event: + cal_event.delete() + # Delete task? - 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 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.auto_assign() + 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 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 + 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 + 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 _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 + 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 diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index b22e6e0087..72c2ae5ee7 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -7,45 +7,52 @@ import frappe import unittest import datetime + def create_test_lead(): - test_lead = frappe.db.exists({'doctype':'Lead','lead_name':'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]) + 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' + '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', 'email':'test@example.com' }) + test_appointment = frappe.db.exists( + {'doctype': 'Appointment', 'email': 'test@example.com'}) if test_appointment: - return frappe.get_doc('Appointment',test_appointment[0][0]) + 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() + '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) + 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) \ No newline at end of file + lead = frappe.get_doc('Lead', self.test_lead.name) + self.assertIsNotNone(lead) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index cfacc79dc2..f0cf1d7664 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -34,7 +34,7 @@ function setup_timezone_selector() { window.timezones.forEach(timezone => { let opt = document.createElement('option'); opt.value = timezone; - if(timezone == moment.tz.guess()){ + if (timezone == moment.tz.guess()) { opt.selected = true; } opt.innerHTML = timezone; @@ -140,7 +140,7 @@ function select_time() { return; } let selected_element = document.getElementsByClassName('selected'); - if (!(selected_element.length > 0)){ + if (!(selected_element.length > 0)) { this.classList.add('selected'); show_next_button(); return; @@ -191,7 +191,7 @@ function setup_details_page() { async function submit() { let form = document.querySelector('#customer-form'); - if(!form.checkValidity()){ + if (!form.checkValidity()) { form.reportValidity(); return; } @@ -211,7 +211,6 @@ async function submit() { } function get_form_data() { - contact = {}; contact.name = document.getElementById('customer_name').value; contact.number = document.getElementById('customer_number').value; diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 1a9afa577e..e279a4717b 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -9,21 +9,26 @@ WEEKDAYS = ["Monday", "Tuesday", "Wednesday", no_cache = 1 + def get_context(context): - is_enabled = frappe.db.get_single_value('Appointment Booking Settings','enable_scheduling') + is_enabled = frappe.db.get_single_value( + 'Appointment Booking Settings', 'enable_scheduling') if is_enabled: return context else: raise frappe.DoesNotExistError + @frappe.whitelist(allow_guest=True) def get_appointment_settings(): settings = frappe.get_doc('Appointment Booking Settings') return settings + @frappe.whitelist(allow_guest=True) def is_enabled(): - enable_scheduling = frappe.db.get_single_value('Appointment Booking Settings','enable_scheduling') + enable_scheduling = frappe.db.get_single_value( + 'Appointment Booking Settings', 'enable_scheduling') return enable_scheduling @@ -131,15 +136,18 @@ def filter_timeslots(date, timeslots): 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: @@ -147,10 +155,12 @@ def _get_records(start_time, end_time, settings): 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) \ No newline at end of file + return (date_time-midnight) From c1bc0f9dfb8ed4e83b16d9e6b8cde29e53146796 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 11:36:53 +0530 Subject: [PATCH 097/210] fix: added sections for settings --- .../appointment_booking_settings.json | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 90f3ad9410..25a7c69268 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -5,13 +5,15 @@ "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", - "agent_list" + "advance_booking_days" ], "fields": [ { @@ -70,10 +72,20 @@ "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" } ], "issingle": 1, - "modified": "2019-10-03 14:52:33.076253", + "modified": "2019-10-04 11:36:20.839075", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", From bfe18d6085d4b64e66e003ad7837c1c1341fdc08 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 14:41:54 +0530 Subject: [PATCH 098/210] feat:assign appointments from opportunity --- .../crm/doctype/appointment/appointment.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2da4acc1f5..5615ae1969 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -106,6 +106,16 @@ class Appointment(Document): self.lead = lead.name def auto_assign(self): + # If the latest opportunity is assigned to someone + # Assign the appointment to the same + existing_assignee = self.get_assignee_from_latest_opportunity() + if existing_assignee: + 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( @@ -120,6 +130,25 @@ class Appointment(Document): }) 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, + }, + order_by='creation desc') + 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 From 0082b780759460c0ae635d95fec28e043e8ed316 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 15:36:36 +0530 Subject: [PATCH 099/210] fix:incosistent tabs and spaces --- .../crm/doctype/appointment/appointment.py | 21 ++++++++++--------- 1 file changed, 11 insertions(+), 10 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5615ae1969..942960e81e 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -89,6 +89,7 @@ class Appointment(Document): self.auto_assign() self.save(ignore_permissions=True) frappe.db.commit() + this.wrapper.find('.filter-edit-area').after(this.get_clear_button()) def create_lead(self): # Return if already linked @@ -106,16 +107,16 @@ class Appointment(Document): self.lead = lead.name def auto_assign(self): - # If the latest opportunity is assigned to someone - # Assign the appointment to the same - existing_assignee = self.get_assignee_from_latest_opportunity() - if existing_assignee: - add_assignemnt({ - 'doctype':self.doctype - 'name':self.name - 'assign_to':existing_assignee - }) - return + # If the latest opportunity is assigned to someone + # Assign the appointment to the same + existing_assignee = self.get_assignee_from_latest_opportunity() + if existing_assignee: + 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( From 911e034d1c3ee22c3999e7dd8d8badb2267c565c Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 15:50:02 +0530 Subject: [PATCH 100/210] fix: syntax error --- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 942960e81e..714e88ded3 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -112,8 +112,8 @@ class Appointment(Document): existing_assignee = self.get_assignee_from_latest_opportunity() if existing_assignee: add_assignemnt({ - 'doctype': self.doctype - 'name': self.name + 'doctype': self.doctype, + 'name': self.name, 'assign_to': existing_assignee }) return From e18388ade3973c8a5ecae6f48470ff5cc830c116 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Fri, 4 Oct 2019 16:32:32 +0530 Subject: [PATCH 101/210] fix:add exception for no opportunity --- erpnext/crm/doctype/appointment/appointment.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 714e88ded3..2b9d31da50 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -142,6 +142,8 @@ class Appointment(Document): 'party_name': self.lead, }, order_by='creation desc') + if not opporutnities: + return None latest_opportunity = frappe.get_doc( 'Opportunity', opporutnities[0].name) assignee = latest_opportunity._assign From 5e4ec8557443027268414c8bb4b5906304613e4a Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 9 Oct 2019 08:23:54 +0000 Subject: [PATCH 102/210] remove:unnecessary translation Co-Authored-By: Shivam Mishra --- erpnext/crm/doctype/appointment/appointment.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js index 485520f562..fb78d1af94 100644 --- a/erpnext/crm/doctype/appointment/appointment.js +++ b/erpnext/crm/doctype/appointment/appointment.js @@ -4,7 +4,7 @@ frappe.ui.form.on('Appointment', { refresh: function(frm) { if(frm.doc.lead){ - frm.add_custom_button(__(frm.doc.lead),()=>{ + frm.add_custom_button(frm.doc.lead,()=>{ frappe.set_route("Form","Lead",frm.doc.lead); }); } From 96930e25f3b768b3926eb6f4cf0c8950b395a8ed Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 9 Oct 2019 08:31:37 +0000 Subject: [PATCH 103/210] fix: readability Co-Authored-By: Shivam Mishra --- erpnext/crm/doctype/appointment/appointment.js | 4 ++-- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js index fb78d1af94..8888b569c4 100644 --- a/erpnext/crm/doctype/appointment/appointment.js +++ b/erpnext/crm/doctype/appointment/appointment.js @@ -5,12 +5,12 @@ 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); + 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); + frappe.set_route("Form", "Event", frm.doc.calendar_event); }); } } diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2b9d31da50..5d1e301b6e 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -35,7 +35,7 @@ class Appointment(Document): self.lead = self.find_lead_by_email() def after_insert(self): - if(self.lead): + if self.lead: # Create Calendar event self.create_calendar_event() self.auto_assign() @@ -63,7 +63,7 @@ class Appointment(Document): return get_url(verify_route + '?' + get_signed_params(params)) def on_change(self): - # Sync Calednar + # Sync Calendar if not self.calendar_event: return cal_event = frappe.get_doc('Event', self.calendar_event) From e434e8e2e24205ff83ce4888b3b9f3f426fad54a Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 9 Oct 2019 14:08:01 +0530 Subject: [PATCH 104/210] fix: formatting --- erpnext/www/book-appointment/index.py | 1 - erpnext/www/book-appointment/verify/index.py | 1 + 2 files changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index e279a4717b..6f416d1b2b 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -59,7 +59,6 @@ def get_appointment_slots(date, timezone): query_start_time = query_start_time.astimezone(local_timezone) query_end_time = query_end_time.astimezone(local_timezone) now = datetime.datetime.now() - # now = local_timezone.localize(now) # Database queries settings = frappe.get_doc('Appointment Booking Settings') holiday_list = frappe.get_doc('Holiday List', settings.holiday_list) diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py index 6eda19f925..e8ccecd8b6 100644 --- a/erpnext/www/book-appointment/verify/index.py +++ b/erpnext/www/book-appointment/verify/index.py @@ -1,4 +1,5 @@ import frappe + from frappe.utils.verified_command import verify_request @frappe.whitelist(allow_guest=True) def get_context(context): From 604febb398f6f27c4caf6202d9ea557f9582ac54 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 9 Oct 2019 14:09:47 +0530 Subject: [PATCH 105/210] fix: set_verified method contained js --- erpnext/crm/doctype/appointment/appointment.py | 1 - 1 file changed, 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5d1e301b6e..b39de13abb 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -89,7 +89,6 @@ class Appointment(Document): self.auto_assign() self.save(ignore_permissions=True) frappe.db.commit() - this.wrapper.find('.filter-edit-area').after(this.get_clear_button()) def create_lead(self): # Return if already linked From 50e66d81de0dff46d4cd1baf9317df76b2ae5f63 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 9 Oct 2019 08:43:18 +0000 Subject: [PATCH 106/210] fix: use get_single_value Co-Authored-By: Shivam Mishra --- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5d1e301b6e..607584f685 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -28,8 +28,8 @@ class Appointment(Document): 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): + number_of_agents = frappe.db.get_single_value('Appointment Booking Settings', 'number_of_agents') + if(number_of_appointments_in_same_slot >= number_of_agents): frappe.throw('Time slot is not available') # Link lead self.lead = self.find_lead_by_email() From 2c9959468821add2b895a197e971f98f4213a3f9 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 9 Oct 2019 15:22:57 +0530 Subject: [PATCH 107/210] remove: styles for non existant radio --- erpnext/www/book-appointment/index.css | 6 ------ 1 file changed, 6 deletions(-) diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index d5202065ea..9398b30371 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -7,7 +7,6 @@ border: 0.5px solid #cccccc; min-height: 75px; padding: 0.5em 1em; - } #timeslot-container{ @@ -29,11 +28,6 @@ color: #718096 } -input[type="radio"] { - visibility: hidden; - display: none; -} - .time-slot.selected { color: white; background: #5e64ff; From aa918e852832efcdda3bd124779bbc8a14b42247 Mon Sep 17 00:00:00 2001 From: Pranav Nachanekar Date: Wed, 9 Oct 2019 15:49:48 +0530 Subject: [PATCH 108/210] moved validations to sepeate functions --- .../appointment_booking_settings.py | 41 +++++++++++-------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 6a1cf56cb7..ef762ff025 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -8,22 +8,31 @@ 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" + min_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 + def validate(self): + self.validate_availability_of_slots() - 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 ') \ No newline at end of file + def validate_availability_of_slots(self): + for record in self.availability_of_slots: + from_time = datetime.datetime.strptime( + min_date+record.from_time, format_string) + to_time = datetime.datetime.strptime( + min_date+record.to_time, format_string) + timedelta = to_time-from_time + self.from_time_is_later_than_to_time(from_time, to_time) + self.duration_is_divisible(from_time, to_time) + + def from_time_is_later_than_to_time(self, from_time, to_time): + if from_time > to_time: + err_msg = 'From Time cannot be later than To Time for '+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 Appointmen')) From 29c7d5fc63ec0cd5552fcb793c1f636a3dd98f30 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 15 Oct 2019 16:43:18 +0530 Subject: [PATCH 109/210] fix:margins --- erpnext/www/book-appointment/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index e1355a7765..43a3f1026d 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -11,7 +11,7 @@
-
+

Book an appointment

Select the date and your timezone

From 3d73a4f944d519c7c2470152a20b2bbd326fd178 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 15 Oct 2019 16:43:40 +0530 Subject: [PATCH 110/210] fix:readability for user --- erpnext/crm/doctype/appointment/appointment.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.json b/erpnext/crm/doctype/appointment/appointment.json index 323e096b40..32df8ec429 100644 --- a/erpnext/crm/doctype/appointment/appointment.json +++ b/erpnext/crm/doctype/appointment/appointment.json @@ -86,14 +86,14 @@ { "fieldname": "linked_docs_section", "fieldtype": "Section Break", - "label": "Linked Docs" + "label": "Linked Documents" }, { "fieldname": "col_br_3", "fieldtype": "Column Break" } ], - "modified": "2019-09-25 13:08:46.368307", + "modified": "2019-10-14 15:23:54.630731", "modified_by": "Administrator", "module": "CRM", "name": "Appointment", From 2f9ef85614322565f65335cee9fa4b7a66f505e1 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 15 Oct 2019 16:44:28 +0530 Subject: [PATCH 111/210] fix:typo --- .../appointment_booking_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index ef762ff025..2aa51caefd 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -19,9 +19,9 @@ class AppointmentBookingSettings(Document): def validate_availability_of_slots(self): for record in self.availability_of_slots: from_time = datetime.datetime.strptime( - min_date+record.from_time, format_string) + self.min_date+record.from_time, self.format_string) to_time = datetime.datetime.strptime( - min_date+record.to_time, format_string) + self.min_date+record.to_time, self.format_string) timedelta = to_time-from_time self.from_time_is_later_than_to_time(from_time, to_time) self.duration_is_divisible(from_time, to_time) @@ -35,4 +35,4 @@ class AppointmentBookingSettings(Document): 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 Appointmen')) + _('The difference between from time and To Time must be a multiple of Appointment')) From 7c27436d210fba9e9dc2c48fdca3e6004132024a Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 15 Oct 2019 16:45:24 +0530 Subject: [PATCH 112/210] fix:visibilty for forms --- erpnext/www/book-appointment/index.css | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index 9398b30371..30ce957e2c 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -1,4 +1,6 @@ .time-slot { + flex-grow: 1; + flex : 0 0 calc(16.66% - 20px); margin-bottom: 2em; margin-left: 0.5em; margin-right: 0.5em; @@ -9,9 +11,16 @@ padding: 0.5em 1em; } +#customer-form{ + border-color: black; +} +#customer-form ::placeholder{ + color: #ddd; +} #timeslot-container{ - display: grid; - grid-template-columns: repeat(6, 1fr); + display: flex; + flex-wrap: wrap; + justify-content: center; } .time-slot:hover { From ad013264eb92704e96dad3de71e80c818654a729 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 15 Oct 2019 16:45:37 +0530 Subject: [PATCH 113/210] fix:margins --- erpnext/www/book-appointment/index.html | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 43a3f1026d..10fe09ab3c 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -37,8 +37,8 @@
-
-
+
+

Add details

Selected date is at

@@ -46,10 +46,11 @@
- + - + +
From d1c530c564e639bc8bac58f170c54e72a73381aa Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 31 Oct 2019 15:36:33 +0530 Subject: [PATCH 114/210] fix: merge settings into one call --- erpnext/www/book-appointment/index.js | 10 +++------- 1 file changed, 3 insertions(+), 7 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index f0cf1d7664..19fc704501 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -20,12 +20,7 @@ async function get_global_variables() { 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; + window.holiday_list = window.appointment_settings.holiday_list; } function setup_timezone_selector() { @@ -201,7 +196,8 @@ async function submit() { args: { 'date': window.selected_date, 'time': window.selected_time, - 'contact': window.contact + 'contact': window.contact, + 'tz':window.selected_timezone } })).message; frappe.msgprint(__('Appointment Created Successfully')); From 60093d98b07a465ccd29352895f13dd0eec8e07e Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 31 Oct 2019 15:37:57 +0530 Subject: [PATCH 115/210] auto assign before creating event --- erpnext/crm/doctype/appointment/appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index f575f52909..18f47c9be0 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -85,8 +85,8 @@ class Appointment(Document): # Remove unverified status self.status = 'Open' # Create calender event - self.create_calendar_event() self.auto_assign() + self.create_calendar_event() self.save(ignore_permissions=True) frappe.db.commit() From e494144c965a314b77cbdd1a67e7436daa16a4f4 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 31 Oct 2019 15:38:39 +0530 Subject: [PATCH 116/210] merge settings fetch, add helpers --- erpnext/www/book-appointment/index.py | 66 ++++++++++++++------------- 1 file changed, 34 insertions(+), 32 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 6f416d1b2b..eb7d5b918b 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -18,47 +18,31 @@ def get_context(context): else: raise frappe.DoesNotExistError - @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 is_enabled(): - enable_scheduling = frappe.db.get_single_value( - 'Appointment Booking Settings', 'enable_scheduling') - return enable_scheduling - - -@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(): return pytz.all_timezones - @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): import pytz guest_timezone = pytz.timezone(timezone) + local_timezone = pytz.timezone(frappe.utils.get_time_zone()) 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) - local_timezone = frappe.utils.get_time_zone() - local_timezone = pytz.timezone(local_timezone) - query_start_time = guest_timezone.localize(query_start_time) - query_end_time = guest_timezone.localize(query_end_time) - query_start_time = query_start_time.astimezone(local_timezone) - query_end_time = query_end_time.astimezone(local_timezone) - now = datetime.datetime.now() + + query_start_time = convert_to_system_timzone(timezone,query_start_time) + query_end_time = convert_to_system_timzone(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) @@ -68,9 +52,9 @@ def get_appointment_slots(date, timezone): # Filter timeslots based on date converted_timeslots = [] for timeslot in timeslots: - timeslot = local_timezone.localize(timeslot) - timeslot = timeslot.astimezone(guest_timezone) - timeslot = timeslot.replace(tzinfo=None) + print("Unconverted Timeslot:{0}".format(timeslot)) + timeslot = convert_to_guest_timezone(timezone,timeslot) + print("Converted Timeslot:{0}".format(timeslot)) # Check if holiday if _is_holiday(timeslot.date(), holiday_list): converted_timeslots.append( @@ -112,11 +96,16 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) -def create_appointment(date, time, contact): +def create_appointment(date, time, tz, contact): + import pytz appointment = frappe.new_doc('Appointment') - format_string = '%Y-%m-%d %H:%M:%S' - appointment.scheduled_time = datetime.datetime.strptime( + format_string = '%Y-%m-%d %H:%M:%S%z' + scheduled_time = datetime.datetime.strptime( date+" "+time, format_string) + scheduled_time = scheduled_time.replace(tzinfo=None) + scheduled_time = convert_to_system_timzone(tz,scheduled_time) + scheduled_time= scheduled_time.replace(tzinfo=None) + appointment.scheduled_time = scheduled_time contact = json.loads(contact) appointment.customer_name = contact['name'] appointment.customer_phone_number = contact['number'] @@ -126,7 +115,6 @@ def create_appointment(date, time, contact): appointment.status = 'Open' appointment.insert() - # Helper Functions def filter_timeslots(date, timeslots): filtered_timeslots = [] @@ -135,11 +123,25 @@ def filter_timeslots(date, timeslots): filtered_timeslots.append(timeslot) return filtered_timeslots +def convert_to_guest_timezone(guest_tz,datetimeobject): + import pytz + 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_timzone(guest_tz,datetimeobject): + import pytz + 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: @@ -162,4 +164,4 @@ def _deltatime_to_datetime(date, deltatime): def _datetime_to_deltatime(date_time): midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) - return (date_time-midnight) + return (date_time-midnight) \ No newline at end of file From 4701bc8bfcf73889a72086ca3b20ad7f89e29afc Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 09:36:29 +0530 Subject: [PATCH 117/210] Add ignore permissions for opportunity --- erpnext/crm/doctype/appointment/appointment.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 18f47c9be0..bc2c838930 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -140,11 +140,11 @@ class Appointment(Document): 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) + latest_opportunity = frappe.get_doc('Opportunity', opporutnities[0].name ) assignee = latest_opportunity._assign if not assignee: return None From 957c9f5ff036d28916522fe30c27942133a509f8 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 09:36:45 +0530 Subject: [PATCH 118/210] fix:comments --- erpnext/www/book-appointment/index.py | 9 ++------- 1 file changed, 2 insertions(+), 7 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index eb7d5b918b..b983dde6f2 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -31,14 +31,12 @@ def get_timezones(): @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): import pytz - guest_timezone = pytz.timezone(timezone) - local_timezone = pytz.timezone(frappe.utils.get_time_zone()) + # 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_timzone(timezone,query_start_time) query_end_time = convert_to_system_timzone(timezone,query_end_time) now = convert_to_guest_timezone(timezone,datetime.datetime.now()) @@ -49,12 +47,10 @@ def get_appointment_slots(date, timezone): timeslots = get_available_slots_between( query_start_time, query_end_time, settings) - # Filter timeslots based on date + # Filter and convert timeslots converted_timeslots = [] for timeslot in timeslots: - print("Unconverted Timeslot:{0}".format(timeslot)) timeslot = convert_to_guest_timezone(timezone,timeslot) - print("Converted Timeslot:{0}".format(timeslot)) # Check if holiday if _is_holiday(timeslot.date(), holiday_list): converted_timeslots.append( @@ -72,7 +68,6 @@ def get_appointment_slots(date, timezone): 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 = [] From 6de68c8671d4fa691cd17a815ea0ab0f3adb08aa Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 09:51:32 +0530 Subject: [PATCH 119/210] avoid repetition on get_form date --- erpnext/www/book-appointment/index.js | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 19fc704501..6bd868bbc7 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -208,10 +208,7 @@ async function submit() { 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; + let inputs = ['name', 'skype', 'number', 'notes', 'email']; + inputs.forEach((id) => contact[id] = document.getElementById(`customer_${id}`).value) window.contact = contact } From 36098727601daa973cb90e3efa8b1fdb39cbbda7 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 12:06:42 +0530 Subject: [PATCH 120/210] rename function --- erpnext/www/book-appointment/index.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index b983dde6f2..9b5ea57a83 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -37,8 +37,8 @@ def get_appointment_slots(date, timezone): date + ' 00:00:00', format_string) query_end_time = datetime.datetime.strptime( date + ' 23:59:59', format_string) - query_start_time = convert_to_system_timzone(timezone,query_start_time) - query_end_time = convert_to_system_timzone(timezone,query_end_time) + 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 @@ -50,19 +50,19 @@ def get_appointment_slots(date, timezone): # Filter and convert timeslots converted_timeslots = [] for timeslot in timeslots: - timeslot = convert_to_guest_timezone(timezone,timeslot) + converted_timeslot = convert_to_guest_timezone(timezone,timeslot) # Check if holiday - if _is_holiday(timeslot.date(), holiday_list): + if _is_holiday(converted_timeslot.date(), holiday_list): converted_timeslots.append( - dict(time=timeslot, availability=False)) + dict(time=converted_timeslot, availability=False)) continue # Check availability - if check_availabilty(timeslot, settings) and timeslot >= now: + if check_availabilty(timeslot, settings) and converted_timeslot >= now: converted_timeslots.append( - dict(time=timeslot, availability=True)) + dict(time=converted_timeslot, availability=True)) else: converted_timeslots.append( - dict(time=timeslot, availability=False)) + 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) @@ -98,7 +98,7 @@ def create_appointment(date, time, tz, contact): scheduled_time = datetime.datetime.strptime( date+" "+time, format_string) scheduled_time = scheduled_time.replace(tzinfo=None) - scheduled_time = convert_to_system_timzone(tz,scheduled_time) + scheduled_time = convert_to_system_timezone(tz,scheduled_time) scheduled_time= scheduled_time.replace(tzinfo=None) appointment.scheduled_time = scheduled_time contact = json.loads(contact) @@ -126,7 +126,7 @@ def convert_to_guest_timezone(guest_tz,datetimeobject): datetimeobject = datetimeobject.astimezone(guest_tz) return datetimeobject -def convert_to_system_timzone(guest_tz,datetimeobject): +def convert_to_system_timezone(guest_tz,datetimeobject): import pytz guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) From 54f33f4e5d913a97163a48fb6ffbbef90078dd94 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 12:14:21 +0530 Subject: [PATCH 121/210] move utility functions --- .../crm/doctype/appointment/appointment.py | 38 ++++++++----------- 1 file changed, 16 insertions(+), 22 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index bc2c838930..95a9580dbb 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -11,7 +11,6 @@ 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 @@ -37,13 +36,13 @@ class Appointment(Document): def after_insert(self): if self.lead: # Create Calendar event - self.create_calendar_event() self.auto_assign() + self.create_calendar_event() else: # Set status to unverified self.status = 'Unverified' # Send email to confirm - verify_url = self.get_verify_url() + 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], @@ -52,15 +51,6 @@ class Appointment(Document): 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_change(self): # Sync Calendar @@ -70,18 +60,12 @@ class Appointment(Document): cal_event.starts_on = self.scheduled_time cal_event.save(ignore_permissions=True) - def on_trash(self): - # Delete calendar event - cal_event = frappe.get_doc('Event', self.calendar_event) - if cal_event: - cal_event.delete() - # Delete task? def set_verified(self, email): if not email == self.customer_email: frappe.throw('Email verification failed.') # Create new lead - self.create_lead() + self.create_lead_and_link() # Remove unverified status self.status = 'Open' # Create calender event @@ -90,7 +74,7 @@ class Appointment(Document): self.save(ignore_permissions=True) frappe.db.commit() - def create_lead(self): + def create_lead_and_link(self): # Return if already linked if self.lead: return @@ -106,10 +90,11 @@ class Appointment(Document): self.lead = lead.name def auto_assign(self): - # If the latest opportunity is assigned to someone - # Assign the appointment to the same + 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, @@ -171,6 +156,14 @@ class Appointment(Document): 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): @@ -214,3 +207,4 @@ def _get_employee_from_user(user): # frappe.db.exists returns a tuple of a tuple return frappe.get_doc('Employee', employee_docname[0][0]) return None + From 97f65762130a5439172dc204ec1ea0027d477d0d Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 12:36:06 +0530 Subject: [PATCH 122/210] prettify confirmation email --- erpnext/crm/doctype/appointment/appointment.py | 9 ++++++++- erpnext/templates/emails/confirm_appointment.html | 10 ++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) create mode 100644 erpnext/templates/emails/confirm_appointment.html diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 95a9580dbb..b3af99da94 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -43,10 +43,17 @@ class Appointment(Document): self.status = 'Unverified' # Send email to confirm verify_url = self._get_verify_url() + template = 'confirm_appointment' + args = { + "link":verify_url, + "site_url":frappe.utils.get_url(), + "full_name":self.customer_name, + } message = ''.join( ['Please click the following link to confirm your appointment:', verify_url]) frappe.sendmail(recipients=[self.customer_email], - message=message, + template=template, + args=args, subject=_('Appointment Confirmation')) frappe.msgprint( 'Please check your email to confirm the appointment') diff --git a/erpnext/templates/emails/confirm_appointment.html b/erpnext/templates/emails/confirm_appointment.html new file mode 100644 index 0000000000..6c9b28bc13 --- /dev/null +++ b/erpnext/templates/emails/confirm_appointment.html @@ -0,0 +1,10 @@ +

{{_("Dear")}} {{ full_name }}{% if last_name %} {{ last_name}}{% endif %},

+

{{_("A new appointment has been created for you with {0}").format(site_url)}}.

+

{{_("Click on the link below to verify your email and confirm the appointment")}}.

+ +

+ {{ _("Verify Email") }} +

+ +
+

{{_("You can also copy-paste this link in your browser")}} {{ link }}

From e573bd90740c93917377f012f51ad2e2e90124ca Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 1 Nov 2019 12:47:11 +0530 Subject: [PATCH 123/210] remove unnecessary variable --- erpnext/crm/doctype/appointment/appointment.py | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index b3af99da94..fa4b7ec401 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -49,8 +49,6 @@ class Appointment(Document): "site_url":frappe.utils.get_url(), "full_name":self.customer_name, } - message = ''.join( - ['Please click the following link to confirm your appointment:', verify_url]) frappe.sendmail(recipients=[self.customer_email], template=template, args=args, From 4d3dc87a1a2dcf4ad7c71c276c0fc4e8368944dc Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 5 Nov 2019 04:32:06 +0000 Subject: [PATCH 124/210] Apply suggestions from code review Co-Authored-By: Shivam Mishra --- .../appointment_booking_settings.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 2aa51caefd..bb45b7222b 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -23,12 +23,12 @@ class AppointmentBookingSettings(Document): to_time = datetime.datetime.strptime( self.min_date+record.to_time, self.format_string) timedelta = to_time-from_time - self.from_time_is_later_than_to_time(from_time, to_time) + self.validate_from_and_to_time(from_time, to_time) self.duration_is_divisible(from_time, to_time) - def from_time_is_later_than_to_time(self, from_time, to_time): + def validate_from_and_to_time(self, from_time, to_time): if from_time > to_time: - err_msg = 'From Time cannot be later than To Time for '+record.day_of_week + err_msg = _(''From Time cannot be later than To Time for {0}'').format(record.day_of_week) frappe.throw(_(err_msg)) def duration_is_divisible(self, from_time, to_time): From d1ee962d4b7e94518f1dc897660d064c6eac4169 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 5 Nov 2019 14:53:36 +0530 Subject: [PATCH 125/210] seperate function for sending confirmation --- .../crm/doctype/appointment/appointment.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index fa4b7ec401..9e051f607a 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -42,20 +42,21 @@ class Appointment(Document): # Set status to unverified self.status = 'Unverified' # Send email to confirm - 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')) - frappe.msgprint( - 'Please check your email to confirm the appointment') + def send_confirmation_email() + 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')) + frappe.msgprint( + 'Please check your email to confirm the appointment') def on_change(self): # Sync Calendar From 6f1d2eeffd81f280fb26a17c34399db84715b3ea Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 6 Nov 2019 11:57:37 +0530 Subject: [PATCH 126/210] changes to suggestions made by shivam --- erpnext/crm/doctype/appointment/appointment.py | 3 ++- .../appointment_booking_settings.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 9e051f607a..5ca124bd85 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -42,8 +42,9 @@ class Appointment(Document): # Set status to unverified self.status = 'Unverified' # Send email to confirm + self.send_confirmation_email() - def send_confirmation_email() + def send_confirmation_email(self): verify_url = self._get_verify_url() template = 'confirm_appointment' args = { diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index bb45b7222b..b8028e3103 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -28,7 +28,7 @@ class AppointmentBookingSettings(Document): def validate_from_and_to_time(self, from_time, to_time): if from_time > to_time: - err_msg = _(''From Time cannot be later than To Time for {0}'').format(record.day_of_week) + err_msg = _('From Time cannot be later than To Time for {0}').format(record.day_of_week) frappe.throw(_(err_msg)) def duration_is_divisible(self, from_time, to_time): From fce8f36bb2ba3541895a67714ec508b45e48d487 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 12:37:28 +0530 Subject: [PATCH 127/210] don't change lead if assigned --- erpnext/crm/doctype/appointment/appointment.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 5ca124bd85..780e04c5ae 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -31,7 +31,8 @@ class Appointment(Document): if(number_of_appointments_in_same_slot >= number_of_agents): frappe.throw('Time slot is not available') # Link lead - self.lead = self.find_lead_by_email() + if not self.lead: + self.lead = self.find_lead_by_email() def after_insert(self): if self.lead: @@ -56,8 +57,9 @@ class Appointment(Document): template=template, args=args, subject=_('Appointment Confirmation')) - frappe.msgprint( - 'Please check your email to confirm the appointment') + if frappe.session.user == "Guest": + frappe.msgprint( + 'Please check your email to confirm the appointment') def on_change(self): # Sync Calendar From 75db6f70735ab930d1dbab03d7b19317028b2653 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 12:47:00 +0530 Subject: [PATCH 128/210] convert indentation to tabs --- .../crm/doctype/appointment/appointment.py | 357 +++++++++--------- .../appointment_booking_settings.py | 44 +-- erpnext/www/book-appointment/index.py | 226 +++++------ erpnext/www/book-appointment/verify/index.py | 26 +- 4 files changed, 328 insertions(+), 325 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 780e04c5ae..91d1c03f7d 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -17,203 +17,206 @@ 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 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(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 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(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 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') + 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 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 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 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 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 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 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 + 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 + 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 + 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 + 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 diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index b8028e3103..2874f3fae2 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -10,29 +10,29 @@ from frappe.model.document import Document class AppointmentBookingSettings(Document): - min_date = '01/01/1970 ' - format_string = "%d/%m/%Y %H:%M:%S" + min_date = '01/01/1970 ' + format_string = "%d/%m/%Y %H:%M:%S" - def validate(self): - self.validate_availability_of_slots() + def validate(self): + self.validate_availability_of_slots() - 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_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 = _('From Time cannot be later than To Time for {0}').format(record.day_of_week) - frappe.throw(_(err_msg)) + def validate_from_and_to_time(self, from_time, to_time): + if from_time > to_time: + err_msg = _('From Time cannot be later than To Time 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')) + 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')) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 9b5ea57a83..11073131b1 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -5,158 +5,158 @@ import pytz WEEKDAYS = ["Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday", "Sunday"] + "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: - raise frappe.DoesNotExistError + is_enabled = frappe.db.get_single_value( + 'Appointment Booking Settings', 'enable_scheduling') + if is_enabled: + return context + else: + raise frappe.DoesNotExistError @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 + 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(): - return pytz.all_timezones + return pytz.all_timezones @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): - import pytz - # 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()) + import pytz + # 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) + # 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 + # 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 + 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): - import pytz - appointment = frappe.new_doc('Appointment') - format_string = '%Y-%m-%d %H:%M:%S%z' - scheduled_time = datetime.datetime.strptime( - date+" "+time, format_string) - scheduled_time = scheduled_time.replace(tzinfo=None) - scheduled_time = convert_to_system_timezone(tz,scheduled_time) - scheduled_time= scheduled_time.replace(tzinfo=None) - appointment.scheduled_time = scheduled_time - 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() + import pytz + appointment = frappe.new_doc('Appointment') + format_string = '%Y-%m-%d %H:%M:%S%z' + scheduled_time = datetime.datetime.strptime( + date+" "+time, format_string) + scheduled_time = scheduled_time.replace(tzinfo=None) + scheduled_time = convert_to_system_timezone(tz,scheduled_time) + scheduled_time= scheduled_time.replace(tzinfo=None) + appointment.scheduled_time = scheduled_time + 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 + 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): - import pytz - 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 + import pytz + 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): - import pytz - 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 + import pytz + 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 + 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 + 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 + 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) + 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) \ No newline at end of file + midnight = datetime.datetime.combine(date_time.date(), datetime.time.min) + return (date_time-midnight) \ No newline at end of file diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book-appointment/verify/index.py index e8ccecd8b6..d4478ae34a 100644 --- a/erpnext/www/book-appointment/verify/index.py +++ b/erpnext/www/book-appointment/verify/index.py @@ -3,18 +3,18 @@ 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 + if not verify_request(): + context.success = False + return context - email = frappe.form_dict['email'] - appointment_name = frappe.form_dict['appointment'] + 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 \ No newline at end of file + 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 \ No newline at end of file From 51208b3f0b848f1de06646d2c2647c09e081381f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 12:54:48 +0530 Subject: [PATCH 129/210] fix:formatting --- erpnext/www/book-appointment/index.py | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 11073131b1..fe30ef65c5 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -37,9 +37,9 @@ def get_appointment_slots(date, timezone): 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()) + 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') @@ -50,7 +50,7 @@ def get_appointment_slots(date, timezone): # Filter and convert timeslots converted_timeslots = [] for timeslot in timeslots: - converted_timeslot = convert_to_guest_timezone(timezone,timeslot) + converted_timeslot = convert_to_guest_timezone(timezone, timeslot) # Check if holiday if _is_holiday(converted_timeslot.date(), holiday_list): converted_timeslots.append( @@ -98,15 +98,15 @@ def create_appointment(date, time, tz, contact): scheduled_time = datetime.datetime.strptime( date+" "+time, format_string) scheduled_time = scheduled_time.replace(tzinfo=None) - scheduled_time = convert_to_system_timezone(tz,scheduled_time) - scheduled_time= scheduled_time.replace(tzinfo=None) + scheduled_time = convert_to_system_timezone(tz, scheduled_time) + scheduled_time = scheduled_time.replace(tzinfo=None) appointment.scheduled_time = scheduled_time 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.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() From 151853b887a7ab43075b5ffdd83e139c8bf6228e Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 12:55:43 +0530 Subject: [PATCH 130/210] remove unneccessary imports --- erpnext/www/book-appointment/index.py | 4 ---- 1 file changed, 4 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index fe30ef65c5..213617fedc 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -30,7 +30,6 @@ def get_timezones(): @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): - import pytz # Convert query to local timezones format_string = '%Y-%m-%d %H:%M:%S' query_start_time = datetime.datetime.strptime( @@ -92,7 +91,6 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - import pytz appointment = frappe.new_doc('Appointment') format_string = '%Y-%m-%d %H:%M:%S%z' scheduled_time = datetime.datetime.strptime( @@ -119,7 +117,6 @@ def filter_timeslots(date, timeslots): return filtered_timeslots def convert_to_guest_timezone(guest_tz,datetimeobject): - import pytz guest_tz = pytz.timezone(guest_tz) local_timezone = pytz.timezone(frappe.utils.get_time_zone()) datetimeobject = local_timezone.localize(datetimeobject) @@ -127,7 +124,6 @@ def convert_to_guest_timezone(guest_tz,datetimeobject): return datetimeobject def convert_to_system_timezone(guest_tz,datetimeobject): - import pytz guest_tz = pytz.timezone(guest_tz) datetimeobject = guest_tz.localize(datetimeobject) system_tz = pytz.timezone(frappe.utils.get_time_zone()) From 76b20a5fa4927a1821f511d921c6faaff6690eef Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 13:24:59 +0530 Subject: [PATCH 131/210] crack some one liners --- erpnext/www/book-appointment/index.py | 42 +++++++++------------------ 1 file changed, 14 insertions(+), 28 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 213617fedc..366f399bc0 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -4,15 +4,13 @@ import json import pytz -WEEKDAYS = ["Monday", "Tuesday", "Wednesday", - "Thursday", "Friday", "Saturday", "Sunday"] +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') + is_enabled = frappe.db.get_single_value('Appointment Booking Settings', 'enable_scheduling') if is_enabled: return context else: @@ -32,10 +30,8 @@ def get_timezones(): 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 = 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()) @@ -43,8 +39,7 @@ def get_appointment_slots(date, timezone): # 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) + timeslots = get_available_slots_between(query_start_time, query_end_time, settings) # Filter and convert timeslots converted_timeslots = [] @@ -52,18 +47,14 @@ def get_appointment_slots(date, timezone): 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)) + 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)) + 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.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 @@ -74,15 +65,11 @@ def get_available_slots_between(query_start_time, query_end_time, settings): 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) + 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) + 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 @@ -93,8 +80,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings): def create_appointment(date, time, tz, contact): appointment = frappe.new_doc('Appointment') format_string = '%Y-%m-%d %H:%M:%S%z' - scheduled_time = datetime.datetime.strptime( - date+" "+time, format_string) + scheduled_time = datetime.datetime.strptime(date+" "+time, format_string) scheduled_time = scheduled_time.replace(tzinfo=None) scheduled_time = convert_to_system_timezone(tz, scheduled_time) scheduled_time = scheduled_time.replace(tzinfo=None) From 0671ea8137f2b8bae1a9f54606635a3f7bd470f5 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 13:31:56 +0530 Subject: [PATCH 132/210] use frappe.Redirect instead of DoesNotExistError --- erpnext/www/book-appointment/index.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 366f399bc0..9765e5ea4d 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -14,7 +14,8 @@ def get_context(context): if is_enabled: return context else: - raise frappe.DoesNotExistError + frappe.local.flags.redirect_location = '/404' + raise frappe.Redirect @frappe.whitelist(allow_guest=True) def get_appointment_settings(): From 83100c9c847ef000c8e071ccdab4c1cf1ab675bc Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 7 Nov 2019 13:37:11 +0530 Subject: [PATCH 133/210] Add comemnts for tz conversions --- erpnext/www/book-appointment/index.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 9765e5ea4d..707be6775c 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -79,13 +79,15 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - appointment = frappe.new_doc('Appointment') format_string = '%Y-%m-%d %H:%M:%S%z' - scheduled_time = datetime.datetime.strptime(date+" "+time, format_string) + 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.scheduled_time = scheduled_time + appointment = frappe.new_doc('Appointment') contact = json.loads(contact) appointment.customer_name = contact.get('name',None) appointment.customer_phone_number = contact.get('number', None) From 929676fceb90d2d016f459e4ad88d2079606c597 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 12 Nov 2019 12:00:30 +0530 Subject: [PATCH 134/210] fix: Validation logic code cleanup --- .../doctype/share_transfer/share_transfer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index e95c69413f..512828b750 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -82,19 +82,19 @@ class ShareTransfer(Document): def basic_validations(self): if self.transfer_type == 'Purchase': self.to_shareholder = '' - if self.from_shareholder is None or self.from_shareholder is '': + if not self.from_shareholder: frappe.throw(_('The field From Shareholder cannot be blank')) - if self.from_folio_no is None or self.from_folio_no is '': + if not self.from_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) - if self.asset_account is None: + if not self.asset_account: frappe.throw(_('The field Asset Account cannot be blank')) elif (self.transfer_type == 'Issue'): self.from_shareholder = '' - if self.to_shareholder is None or self.to_shareholder == '': + if not self.to_shareholder: frappe.throw(_('The field To Shareholder cannot be blank')) - if self.to_folio_no is None or self.to_folio_no is '': + if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) - if self.asset_account is None: + if not self.asset_account: frappe.throw(_('The field Asset Account cannot be blank')) else: if self.from_shareholder is None or self.to_shareholder is None: From f75ea952e3aeb203b5c6569fc5e9604dee236a77 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 11:03:53 +0530 Subject: [PATCH 135/210] Added expired status to quotation --- .../selling/doctype/quotation/quotation.json | 4532 ++++------------- 1 file changed, 1025 insertions(+), 3507 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.json b/erpnext/selling/doctype/quotation/quotation.json index ea04715524..64ad1b5de9 100644 --- a/erpnext/selling/doctype/quotation/quotation.json +++ b/erpnext/selling/doctype/quotation/quotation.json @@ -1,3509 +1,1027 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 1, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2013-05-24 19:29:08", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "Document", - "editable_grid": 1, - "fields": [ - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_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": "", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "{customer_name}", - "fieldname": "title", - "fieldtype": "Data", - "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": "Title", - "length": 0, - "no_copy": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "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": "SAL-QTN-.YYYY.-", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 1, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Customer", - "fetch_if_empty": 0, - "fieldname": "quotation_to", - "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": 1, - "label": "Quotation To", - "length": 0, - "no_copy": 0, - "oldfieldname": "quotation_to", - "oldfieldtype": "Select", - "options": "DocType", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fetch_if_empty": 0, - "fieldname": "party_name", - "fieldtype": "Dynamic Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 1, - "label": "Party", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer", - "oldfieldtype": "Link", - "options": "quotation_to", - "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": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fetch_from": "", - "fieldname": "customer_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Customer Name", - "length": 0, - "no_copy": 0, - "options": "", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break1", - "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, - "fieldname": "amended_from", - "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 1, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Amended From", - "length": 0, - "no_copy": 1, - "oldfieldname": "amended_from", - "oldfieldtype": "Data", - "options": "Quotation", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "150px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "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": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 1, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, - "width": "150px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Today", - "fieldname": "transaction_date", - "fieldtype": "Date", - "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": "Date", - "length": 0, - "no_copy": 1, - "oldfieldname": "transaction_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": 1, - "search_index": 1, - "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, - "fieldname": "valid_till", - "fieldtype": "Date", - "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": "Valid Till", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Sales", - "fieldname": "order_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": 1, - "label": "Order Type", - "length": 0, - "no_copy": 0, - "oldfieldname": "order_type", - "oldfieldtype": "Select", - "options": "\nSales\nMaintenance\nShopping Cart", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "party_name", - "fieldname": "contact_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": "Address and Contact", - "length": 0, - "no_copy": 0, - "options": "fa fa-bullhorn", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "customer_address", - "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": "Customer Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_display", - "fieldtype": "Small Text", - "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", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_address", - "oldfieldtype": "Small Text", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "", - "fieldname": "contact_person", - "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": "Contact Person", - "length": 0, - "no_copy": 0, - "oldfieldname": "contact_person", - "oldfieldtype": "Link", - "options": "Contact", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_display", - "fieldtype": "Small Text", - "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": "Contact", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_mobile", - "fieldtype": "Small Text", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "contact_email", - "fieldtype": "Data", - "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": "Contact Email", - "length": 0, - "no_copy": 0, - "options": "Email", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", - "fieldname": "col_break98", - "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, - "fieldname": "shipping_address_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": "Shipping Address", - "length": 0, - "no_copy": 0, - "options": "Address", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_address", - "fieldtype": "Small Text", - "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": "Shipping Address", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", - "description": "", - "fieldname": "customer_group", - "fieldtype": "Link", - "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": "Customer Group", - "length": 0, - "no_copy": 0, - "oldfieldname": "customer_group", - "oldfieldtype": "Link", - "options": "Customer Group", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "currency_and_price_list", - "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": "Currency and Price List", - "length": 0, - "no_copy": 0, - "options": "fa fa-tag", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "currency", - "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": "Currency", - "length": 0, - "no_copy": 0, - "oldfieldname": "currency", - "oldfieldtype": "Select", - "options": "Currency", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "description": "Rate at which customer's currency is converted to company's base currency", - "fieldname": "conversion_rate", - "fieldtype": "Float", - "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": "Exchange Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "conversion_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "precision": "9", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "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, - "width": "50%" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "selling_price_list", - "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": "Price List", - "length": 0, - "no_copy": 0, - "oldfieldname": "price_list_name", - "oldfieldtype": "Select", - "options": "Price List", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "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, - "fieldname": "price_list_currency", - "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": "Price List Currency", - "length": 0, - "no_copy": 0, - "options": "Currency", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Rate at which Price list currency is converted to company's base currency", - "fieldname": "plc_conversion_rate", - "fieldtype": "Float", - "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": "Price List Exchange Rate", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "9", - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ignore_pricing_rule", - "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": "Ignore Pricing Rule", - "length": 0, - "no_copy": 1, - "permlevel": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items_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": "", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-shopping-cart", - "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 - }, - { - "allow_bulk_edit": 1, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "items", - "fieldtype": "Table", - "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": "Items", - "length": 0, - "no_copy": 0, - "oldfieldname": "quotation_details", - "oldfieldtype": "Table", - "options": "Quotation Item", - "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": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, - "width": "40px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pricing_rule_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": "Pricing Rules", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "pricing_rules", - "fieldtype": "Table", - "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": "Pricing Rule Detail", - "length": 0, - "no_copy": 0, - "options": "Pricing Rule Detail", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "sec_break23", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_qty", - "fieldtype": "Float", - "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": "Total Quantity", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_total", - "fieldtype": "Currency", - "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": "Total (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_net_total", - "fieldtype": "Currency", - "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": "Net Total (Company Currency)", - "length": 0, - "no_copy": 0, - "oldfieldname": "net_total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "100px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_28", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total", - "fieldtype": "Currency", - "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": "Total", - "length": 0, - "no_copy": 0, - "options": "currency", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "net_total", - "fieldtype": "Currency", - "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": "Net Total", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_net_weight", - "fieldtype": "Float", - "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": "Total Net Weight", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes_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": "Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-money", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tax_category", - "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": "Tax Category", - "length": 0, - "no_copy": 0, - "options": "Tax Category", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_34", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "shipping_rule", - "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": "Shipping Rule", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Button", - "options": "Shipping Rule", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_36", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes_and_charges", - "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": "Sales Taxes and Charges Template", - "length": 0, - "no_copy": 0, - "oldfieldname": "charge", - "oldfieldtype": "Link", - "options": "Sales Taxes and Charges Template", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "taxes", - "fieldtype": "Table", - "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": "Sales Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "other_charges", - "oldfieldtype": "Table", - "options": "Sales Taxes and Charges", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "sec_tax_breakup", - "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": "Tax Breakup", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "other_charges_calculation", - "fieldtype": "Text", - "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": "Taxes and Charges Calculation", - "length": 0, - "no_copy": 1, - "oldfieldtype": "HTML", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_39", - "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, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_total_taxes_and_charges", - "fieldtype": "Currency", - "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": "Total Taxes and Charges (Company Currency)", - "length": 0, - "no_copy": 0, - "oldfieldname": "other_charges_total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_42", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "total_taxes_and_charges", - "fieldtype": "Currency", - "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": "Total Taxes and Charges", - "length": 0, - "no_copy": 0, - "options": "currency", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "discount_amount", - "columns": 0, - "fieldname": "section_break_44", - "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": "Additional Discount and Coupon Code", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "coupon_code", - "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": "Coupon Code", - "length": 0, - "no_copy": 0, - "options": "Coupon Code", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_if_empty": 0, - "fieldname": "referral_sales_partner", - "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": "Referral Sales Partner", - "length": 0, - "no_copy": 0, - "options": "Sales Partner", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Grand Total", - "fieldname": "apply_discount_on", - "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": "Apply Additional Discount On", - "length": 0, - "no_copy": 0, - "options": "\nGrand Total\nNet Total", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_discount_amount", - "fieldtype": "Currency", - "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": "Additional Discount Amount (Company Currency)", - "length": 0, - "no_copy": 0, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_46", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "additional_discount_percentage", - "fieldtype": "Float", - "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": "Additional Discount Percentage", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "discount_amount", - "fieldtype": "Currency", - "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": "Additional Discount Amount", - "length": 0, - "no_copy": 0, - "options": "currency", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "totals", - "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, - "oldfieldtype": "Section Break", - "options": "fa fa-money", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_grand_total", - "fieldtype": "Currency", - "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": "Grand Total (Company Currency)", - "length": 0, - "no_copy": 0, - "oldfieldname": "grand_total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rounding_adjustment", - "fieldtype": "Currency", - "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": "Rounding Adjustment (Company Currency)", - "length": 0, - "no_copy": 1, - "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "In Words will be visible once you save the Quotation.", - "fieldname": "base_in_words", - "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": "In Words (Company Currency)", - "length": 0, - "no_copy": 0, - "oldfieldname": "in_words", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "base_rounded_total", - "fieldtype": "Currency", - "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": "Rounded Total (Company Currency)", - "length": 0, - "no_copy": 0, - "oldfieldname": "rounded_total", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 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, - "width": "50%" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "grand_total", - "fieldtype": "Currency", - "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": "Grand Total", - "length": 0, - "no_copy": 0, - "oldfieldname": "grand_total_export", - "oldfieldtype": "Currency", - "options": "currency", - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rounding_adjustment", - "fieldtype": "Currency", - "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": "Rounding Adjustment", - "length": 0, - "no_copy": 1, - "options": "currency", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 1, - "collapsible": 0, - "columns": 0, - "fieldname": "rounded_total", - "fieldtype": "Currency", - "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": "Rounded Total", - "length": 0, - "no_copy": 0, - "oldfieldname": "rounded_total_export", - "oldfieldtype": "Currency", - "options": "currency", - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "in_words", - "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": "In Words", - "length": 0, - "no_copy": 0, - "oldfieldname": "in_words_export", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 1, - "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, - "width": "200px" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "collapsible_depends_on": "", - "columns": 0, - "depends_on": "", - "fieldname": "payment_schedule_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": "Payment Terms", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_terms_template", - "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": "Payment Terms Template", - "length": 0, - "no_copy": 0, - "options": "Payment Terms Template", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "payment_schedule", - "fieldtype": "Table", - "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": "Payment Schedule", - "length": 0, - "no_copy": 1, - "options": "Payment Schedule", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "collapsible_depends_on": "terms", - "columns": 0, - "fieldname": "terms_section_break", - "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": "Terms and Conditions", - "length": 0, - "no_copy": 0, - "oldfieldtype": "Section Break", - "options": "fa fa-legal", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "tc_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": "Terms", - "length": 0, - "no_copy": 0, - "oldfieldname": "tc_name", - "oldfieldtype": "Link", - "options": "Terms and Conditions", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "terms", - "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": "Term Details", - "length": 0, - "no_copy": 0, - "oldfieldname": "terms", - "oldfieldtype": "Text Editor", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 0, - "fieldname": "print_settings", - "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": "Print Settings", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "letter_head", - "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": "Letter Head", - "length": 0, - "no_copy": 0, - "oldfieldname": "letter_head", - "oldfieldtype": "Select", - "options": "Letter Head", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "group_same_items", - "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": "Group same items", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_73", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "select_print_heading", - "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": "Print Heading", - "length": 0, - "no_copy": 1, - "oldfieldname": "select_print_heading", - "oldfieldtype": "Link", - "options": "Print Heading", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "language", - "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": "Print Language", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "subscription_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": "Auto Repeat Section", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "auto_repeat", - "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": "Auto Repeat", - "length": 0, - "no_copy": 1, - "options": "Auto Repeat", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval: doc.auto_repeat", - "fieldname": "update_auto_repeat_reference", - "fieldtype": "Button", - "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": "Update Auto Repeat Reference", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 1, - "columns": 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": 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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "campaign", - "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", - "length": 0, - "no_copy": 0, - "oldfieldname": "campaign", - "oldfieldtype": "Link", - "options": "Campaign", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "depends_on": "eval:doc.status===\"Lost\"", - "fieldname": "order_lost_reason", - "fieldtype": "Small Text", - "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": "Detailed Reason", - "length": 0, - "no_copy": 1, - "oldfieldname": "order_lost_reason", - "oldfieldtype": "Small Text", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break4", - "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": 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, - "width": "50%" - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "Draft", - "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": 0, - "label": "Status", - "length": 0, - "no_copy": 1, - "oldfieldname": "status", - "oldfieldtype": "Select", - "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "enq_det", - "fieldtype": "Text", - "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": "Opportunity Item", - "length": 0, - "no_copy": 0, - "oldfieldname": "enq_det", - "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "supplier_quotation", - "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": "Supplier Quotation", - "length": 0, - "no_copy": 0, - "options": "Supplier Quotation", - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "opportunity", - "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": "Opportunity", - "length": 0, - "no_copy": 0, - "options": "Opportunity", - "permlevel": 0, - "precision": "", - "print_hide": 1, - "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 - }, - { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 1, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "lost_reasons", - "fieldtype": "Table MultiSelect", - "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": "Lost Reasons", - "length": 0, - "no_copy": 0, - "options": "Lost Reason Detail", - "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 - } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-shopping-cart", - "idx": 82, - "image_view": 0, - "in_create": 0, - "is_submittable": 1, - "issingle": 0, - "istable": 0, - "max_attachments": 1, - "menu_index": 0, - "modified": "2019-10-14 01:00:21.545591", - "modified_by": "Administrator", - "module": "Selling", - "name": "Quotation", - "owner": "Administrator", - "permissions": [ - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "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": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "match": "", - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "match": "", - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "Sales Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "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": 1, - "write": 1 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Maintenance Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "match": "", - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "Maintenance Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, - { - "amend": 1, - "cancel": 1, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Maintenance User", - "set_user_permissions": 0, - "share": 1, - "submit": 1, - "write": 1 - }, - { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "match": "", - "permlevel": 1, - "print": 0, - "read": 1, - "report": 1, - "role": "Maintenance User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 1, - "search_fields": "status,transaction_date,party_name,order_type", - "show_name_in_global_search": 1, - "sort_field": "modified", - "sort_order": "DESC", - "timeline_field": "party_name", - "title_field": "title", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "allow_import": 1, + "autoname": "naming_series:", + "creation": "2013-05-24 19:29:08", + "doctype": "DocType", + "document_type": "Document", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "customer_section", + "title", + "naming_series", + "quotation_to", + "party_name", + "customer_name", + "column_break1", + "amended_from", + "company", + "transaction_date", + "valid_till", + "order_type", + "contact_section", + "customer_address", + "address_display", + "contact_person", + "contact_display", + "contact_mobile", + "contact_email", + "col_break98", + "shipping_address_name", + "shipping_address", + "customer_group", + "territory", + "currency_and_price_list", + "currency", + "conversion_rate", + "column_break2", + "selling_price_list", + "price_list_currency", + "plc_conversion_rate", + "ignore_pricing_rule", + "items_section", + "items", + "pricing_rule_details", + "pricing_rules", + "sec_break23", + "total_qty", + "base_total", + "base_net_total", + "column_break_28", + "total", + "net_total", + "total_net_weight", + "taxes_section", + "tax_category", + "column_break_34", + "shipping_rule", + "section_break_36", + "taxes_and_charges", + "taxes", + "sec_tax_breakup", + "other_charges_calculation", + "section_break_39", + "base_total_taxes_and_charges", + "column_break_42", + "total_taxes_and_charges", + "section_break_44", + "coupon_code", + "referral_sales_partner", + "apply_discount_on", + "base_discount_amount", + "column_break_46", + "additional_discount_percentage", + "discount_amount", + "totals", + "base_grand_total", + "base_rounding_adjustment", + "base_in_words", + "base_rounded_total", + "column_break3", + "grand_total", + "rounding_adjustment", + "rounded_total", + "in_words", + "payment_schedule_section", + "payment_terms_template", + "payment_schedule", + "terms_section_break", + "tc_name", + "terms", + "print_settings", + "letter_head", + "group_same_items", + "column_break_73", + "select_print_heading", + "language", + "subscription_section", + "auto_repeat", + "update_auto_repeat_reference", + "more_info", + "campaign", + "source", + "order_lost_reason", + "column_break4", + "status", + "enq_det", + "supplier_quotation", + "opportunity", + "lost_reasons" + ], + "fields": [ + { + "fieldname": "customer_section", + "fieldtype": "Section Break", + "options": "fa fa-user" + }, + { + "allow_on_submit": 1, + "default": "{customer_name}", + "fieldname": "title", + "fieldtype": "Data", + "hidden": 1, + "label": "Title", + "no_copy": 1, + "print_hide": 1 + }, + { + "fieldname": "naming_series", + "fieldtype": "Select", + "label": "Series", + "no_copy": 1, + "oldfieldname": "naming_series", + "oldfieldtype": "Select", + "options": "SAL-QTN-.YYYY.-", + "print_hide": 1, + "reqd": 1, + "set_only_once": 1 + }, + { + "default": "Customer", + "fieldname": "quotation_to", + "fieldtype": "Link", + "in_standard_filter": 1, + "label": "Quotation To", + "oldfieldname": "quotation_to", + "oldfieldtype": "Select", + "options": "DocType", + "print_hide": 1, + "reqd": 1 + }, + { + "bold": 1, + "fieldname": "party_name", + "fieldtype": "Dynamic Link", + "in_global_search": 1, + "in_standard_filter": 1, + "label": "Party", + "oldfieldname": "customer", + "oldfieldtype": "Link", + "options": "quotation_to", + "print_hide": 1, + "search_index": 1 + }, + { + "bold": 1, + "fieldname": "customer_name", + "fieldtype": "Data", + "hidden": 1, + "in_global_search": 1, + "label": "Customer Name", + "read_only": 1 + }, + { + "fieldname": "column_break1", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "amended_from", + "fieldtype": "Link", + "ignore_user_permissions": 1, + "label": "Amended From", + "no_copy": 1, + "oldfieldname": "amended_from", + "oldfieldtype": "Data", + "options": "Quotation", + "print_hide": 1, + "read_only": 1, + "width": "150px" + }, + { + "fieldname": "company", + "fieldtype": "Link", + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "print_hide": 1, + "remember_last_selected_value": 1, + "reqd": 1, + "width": "150px" + }, + { + "default": "Today", + "fieldname": "transaction_date", + "fieldtype": "Date", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Date", + "no_copy": 1, + "oldfieldname": "transaction_date", + "oldfieldtype": "Date", + "reqd": 1, + "search_index": 1, + "width": "100px" + }, + { + "fieldname": "valid_till", + "fieldtype": "Date", + "label": "Valid Till" + }, + { + "default": "Sales", + "fieldname": "order_type", + "fieldtype": "Select", + "in_standard_filter": 1, + "label": "Order Type", + "oldfieldname": "order_type", + "oldfieldtype": "Select", + "options": "\nSales\nMaintenance\nShopping Cart", + "print_hide": 1, + "reqd": 1 + }, + { + "collapsible": 1, + "depends_on": "party_name", + "fieldname": "contact_section", + "fieldtype": "Section Break", + "label": "Address and Contact", + "options": "fa fa-bullhorn" + }, + { + "fieldname": "customer_address", + "fieldtype": "Link", + "label": "Customer Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "address_display", + "fieldtype": "Small Text", + "label": "Address", + "oldfieldname": "customer_address", + "oldfieldtype": "Small Text", + "read_only": 1 + }, + { + "fieldname": "contact_person", + "fieldtype": "Link", + "label": "Contact Person", + "oldfieldname": "contact_person", + "oldfieldtype": "Link", + "options": "Contact", + "print_hide": 1 + }, + { + "fieldname": "contact_display", + "fieldtype": "Small Text", + "in_global_search": 1, + "label": "Contact", + "read_only": 1 + }, + { + "fieldname": "contact_mobile", + "fieldtype": "Small Text", + "label": "Mobile No", + "read_only": 1 + }, + { + "fieldname": "contact_email", + "fieldtype": "Data", + "hidden": 1, + "label": "Contact Email", + "options": "Email", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "fieldname": "col_break98", + "fieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "shipping_address_name", + "fieldtype": "Link", + "label": "Shipping Address", + "options": "Address", + "print_hide": 1 + }, + { + "fieldname": "shipping_address", + "fieldtype": "Small Text", + "label": "Shipping Address", + "print_hide": 1, + "read_only": 1 + }, + { + "depends_on": "eval:doc.quotaion_to=='Customer' && doc.party_name", + "fieldname": "customer_group", + "fieldtype": "Link", + "hidden": 1, + "label": "Customer Group", + "oldfieldname": "customer_group", + "oldfieldtype": "Link", + "options": "Customer Group", + "print_hide": 1 + }, + { + "fieldname": "territory", + "fieldtype": "Link", + "label": "Territory", + "options": "Territory", + "print_hide": 1 + }, + { + "collapsible": 1, + "fieldname": "currency_and_price_list", + "fieldtype": "Section Break", + "label": "Currency and Price List", + "options": "fa fa-tag" + }, + { + "fieldname": "currency", + "fieldtype": "Link", + "label": "Currency", + "oldfieldname": "currency", + "oldfieldtype": "Select", + "options": "Currency", + "print_hide": 1, + "reqd": 1, + "width": "100px" + }, + { + "description": "Rate at which customer's currency is converted to company's base currency", + "fieldname": "conversion_rate", + "fieldtype": "Float", + "label": "Exchange Rate", + "oldfieldname": "conversion_rate", + "oldfieldtype": "Currency", + "precision": "9", + "print_hide": 1, + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "column_break2", + "fieldtype": "Column Break", + "width": "50%" + }, + { + "fieldname": "selling_price_list", + "fieldtype": "Link", + "label": "Price List", + "oldfieldname": "price_list_name", + "oldfieldtype": "Select", + "options": "Price List", + "print_hide": 1, + "reqd": 1, + "width": "100px" + }, + { + "fieldname": "price_list_currency", + "fieldtype": "Link", + "label": "Price List Currency", + "options": "Currency", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "description": "Rate at which Price list currency is converted to company's base currency", + "fieldname": "plc_conversion_rate", + "fieldtype": "Float", + "label": "Price List Exchange Rate", + "precision": "9", + "print_hide": 1, + "reqd": 1 + }, + { + "default": "0", + "fieldname": "ignore_pricing_rule", + "fieldtype": "Check", + "label": "Ignore Pricing Rule", + "no_copy": 1, + "permlevel": 1, + "print_hide": 1 + }, + { + "fieldname": "items_section", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-shopping-cart" + }, + { + "allow_bulk_edit": 1, + "fieldname": "items", + "fieldtype": "Table", + "label": "Items", + "oldfieldname": "quotation_details", + "oldfieldtype": "Table", + "options": "Quotation Item", + "reqd": 1, + "width": "40px" + }, + { + "fieldname": "pricing_rule_details", + "fieldtype": "Section Break", + "label": "Pricing Rules" + }, + { + "fieldname": "pricing_rules", + "fieldtype": "Table", + "label": "Pricing Rule Detail", + "options": "Pricing Rule Detail", + "read_only": 1 + }, + { + "fieldname": "sec_break23", + "fieldtype": "Section Break" + }, + { + "fieldname": "total_qty", + "fieldtype": "Float", + "label": "Total Quantity", + "read_only": 1 + }, + { + "fieldname": "base_total", + "fieldtype": "Currency", + "label": "Total (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "base_net_total", + "fieldtype": "Currency", + "label": "Net Total (Company Currency)", + "oldfieldname": "net_total", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1, + "width": "100px" + }, + { + "fieldname": "column_break_28", + "fieldtype": "Column Break" + }, + { + "fieldname": "total", + "fieldtype": "Currency", + "label": "Total", + "options": "currency", + "read_only": 1 + }, + { + "fieldname": "net_total", + "fieldtype": "Currency", + "label": "Net Total", + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "total_net_weight", + "fieldtype": "Float", + "label": "Total Net Weight", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "taxes_section", + "fieldtype": "Section Break", + "label": "Taxes and Charges", + "oldfieldtype": "Section Break", + "options": "fa fa-money" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category", + "print_hide": 1 + }, + { + "fieldname": "column_break_34", + "fieldtype": "Column Break" + }, + { + "fieldname": "shipping_rule", + "fieldtype": "Link", + "label": "Shipping Rule", + "oldfieldtype": "Button", + "options": "Shipping Rule", + "print_hide": 1 + }, + { + "fieldname": "section_break_36", + "fieldtype": "Section Break" + }, + { + "fieldname": "taxes_and_charges", + "fieldtype": "Link", + "label": "Sales Taxes and Charges Template", + "oldfieldname": "charge", + "oldfieldtype": "Link", + "options": "Sales Taxes and Charges Template", + "print_hide": 1 + }, + { + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Sales Taxes and Charges", + "oldfieldname": "other_charges", + "oldfieldtype": "Table", + "options": "Sales Taxes and Charges" + }, + { + "collapsible": 1, + "fieldname": "sec_tax_breakup", + "fieldtype": "Section Break", + "label": "Tax Breakup" + }, + { + "fieldname": "other_charges_calculation", + "fieldtype": "Text", + "label": "Taxes and Charges Calculation", + "no_copy": 1, + "oldfieldtype": "HTML", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "section_break_39", + "fieldtype": "Section Break" + }, + { + "fieldname": "base_total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges (Company Currency)", + "oldfieldname": "other_charges_total", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_42", + "fieldtype": "Column Break" + }, + { + "fieldname": "total_taxes_and_charges", + "fieldtype": "Currency", + "label": "Total Taxes and Charges", + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "discount_amount", + "fieldname": "section_break_44", + "fieldtype": "Section Break", + "label": "Additional Discount and Coupon Code" + }, + { + "fieldname": "coupon_code", + "fieldtype": "Link", + "label": "Coupon Code", + "options": "Coupon Code" + }, + { + "fieldname": "referral_sales_partner", + "fieldtype": "Link", + "label": "Referral Sales Partner", + "options": "Sales Partner" + }, + { + "default": "Grand Total", + "fieldname": "apply_discount_on", + "fieldtype": "Select", + "label": "Apply Additional Discount On", + "options": "\nGrand Total\nNet Total", + "print_hide": 1 + }, + { + "fieldname": "base_discount_amount", + "fieldtype": "Currency", + "label": "Additional Discount Amount (Company Currency)", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "column_break_46", + "fieldtype": "Column Break" + }, + { + "fieldname": "additional_discount_percentage", + "fieldtype": "Float", + "label": "Additional Discount Percentage", + "print_hide": 1 + }, + { + "fieldname": "discount_amount", + "fieldtype": "Currency", + "label": "Additional Discount Amount", + "options": "currency", + "print_hide": 1 + }, + { + "fieldname": "totals", + "fieldtype": "Section Break", + "oldfieldtype": "Section Break", + "options": "fa fa-money", + "print_hide": 1 + }, + { + "fieldname": "base_grand_total", + "fieldtype": "Currency", + "label": "Grand Total (Company Currency)", + "oldfieldname": "grand_total", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "base_rounding_adjustment", + "fieldtype": "Currency", + "label": "Rounding Adjustment (Company Currency)", + "no_copy": 1, + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1 + }, + { + "description": "In Words will be visible once you save the Quotation.", + "fieldname": "base_in_words", + "fieldtype": "Data", + "label": "In Words (Company Currency)", + "oldfieldname": "in_words", + "oldfieldtype": "Data", + "print_hide": 1, + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "base_rounded_total", + "fieldtype": "Currency", + "label": "Rounded Total (Company Currency)", + "oldfieldname": "rounded_total", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "print_hide": 1, + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "column_break3", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_hide": 1, + "width": "50%" + }, + { + "fieldname": "grand_total", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Grand Total", + "oldfieldname": "grand_total_export", + "oldfieldtype": "Currency", + "options": "currency", + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "rounding_adjustment", + "fieldtype": "Currency", + "label": "Rounding Adjustment", + "no_copy": 1, + "options": "currency", + "print_hide": 1, + "read_only": 1 + }, + { + "bold": 1, + "fieldname": "rounded_total", + "fieldtype": "Currency", + "label": "Rounded Total", + "oldfieldname": "rounded_total_export", + "oldfieldtype": "Currency", + "options": "currency", + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "in_words", + "fieldtype": "Data", + "label": "In Words", + "oldfieldname": "in_words_export", + "oldfieldtype": "Data", + "print_hide": 1, + "read_only": 1, + "width": "200px" + }, + { + "fieldname": "payment_schedule_section", + "fieldtype": "Section Break", + "label": "Payment Terms" + }, + { + "fieldname": "payment_terms_template", + "fieldtype": "Link", + "label": "Payment Terms Template", + "options": "Payment Terms Template", + "print_hide": 1 + }, + { + "fieldname": "payment_schedule", + "fieldtype": "Table", + "label": "Payment Schedule", + "no_copy": 1, + "options": "Payment Schedule", + "print_hide": 1 + }, + { + "collapsible": 1, + "collapsible_depends_on": "terms", + "fieldname": "terms_section_break", + "fieldtype": "Section Break", + "label": "Terms and Conditions", + "oldfieldtype": "Section Break", + "options": "fa fa-legal" + }, + { + "fieldname": "tc_name", + "fieldtype": "Link", + "label": "Terms", + "oldfieldname": "tc_name", + "oldfieldtype": "Link", + "options": "Terms and Conditions", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "terms", + "fieldtype": "Text Editor", + "label": "Term Details", + "oldfieldname": "terms", + "oldfieldtype": "Text Editor" + }, + { + "collapsible": 1, + "fieldname": "print_settings", + "fieldtype": "Section Break", + "label": "Print Settings" + }, + { + "allow_on_submit": 1, + "fieldname": "letter_head", + "fieldtype": "Link", + "label": "Letter Head", + "oldfieldname": "letter_head", + "oldfieldtype": "Select", + "options": "Letter Head", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "default": "0", + "fieldname": "group_same_items", + "fieldtype": "Check", + "label": "Group same items", + "print_hide": 1 + }, + { + "fieldname": "column_break_73", + "fieldtype": "Column Break" + }, + { + "allow_on_submit": 1, + "fieldname": "select_print_heading", + "fieldtype": "Link", + "label": "Print Heading", + "no_copy": 1, + "oldfieldname": "select_print_heading", + "oldfieldtype": "Link", + "options": "Print Heading", + "print_hide": 1, + "report_hide": 1 + }, + { + "fieldname": "language", + "fieldtype": "Data", + "label": "Print Language", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "subscription_section", + "fieldtype": "Section Break", + "label": "Auto Repeat Section" + }, + { + "fieldname": "auto_repeat", + "fieldtype": "Link", + "label": "Auto Repeat", + "no_copy": 1, + "options": "Auto Repeat", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval: doc.auto_repeat", + "fieldname": "update_auto_repeat_reference", + "fieldtype": "Button", + "label": "Update Auto Repeat Reference" + }, + { + "collapsible": 1, + "fieldname": "more_info", + "fieldtype": "Section Break", + "label": "More Information", + "oldfieldtype": "Section Break", + "options": "fa fa-file-text", + "print_hide": 1 + }, + { + "fieldname": "campaign", + "fieldtype": "Link", + "label": "Campaign", + "oldfieldname": "campaign", + "oldfieldtype": "Link", + "options": "Campaign", + "print_hide": 1 + }, + { + "fieldname": "source", + "fieldtype": "Link", + "label": "Source", + "oldfieldname": "source", + "oldfieldtype": "Select", + "options": "Lead Source", + "print_hide": 1 + }, + { + "allow_on_submit": 1, + "depends_on": "eval:doc.status===\"Lost\"", + "fieldname": "order_lost_reason", + "fieldtype": "Small Text", + "label": "Detailed Reason", + "no_copy": 1, + "oldfieldname": "order_lost_reason", + "oldfieldtype": "Small Text", + "print_hide": 1 + }, + { + "fieldname": "column_break4", + "fieldtype": "Column Break", + "oldfieldtype": "Column Break", + "print_hide": 1, + "width": "50%" + }, + { + "default": "Draft", + "fieldname": "status", + "fieldtype": "Select", + "in_list_view": 1, + "label": "Status", + "no_copy": 1, + "oldfieldname": "status", + "oldfieldtype": "Select", + "options": "Draft\nOpen\nReplied\nOrdered\nLost\nCancelled\nExpired", + "print_hide": 1, + "read_only": 1, + "reqd": 1 + }, + { + "fieldname": "enq_det", + "fieldtype": "Text", + "hidden": 1, + "label": "Opportunity Item", + "oldfieldname": "enq_det", + "oldfieldtype": "Text", + "print_hide": 1, + "read_only": 1 + }, + { + "fieldname": "supplier_quotation", + "fieldtype": "Link", + "label": "Supplier Quotation", + "options": "Supplier Quotation" + }, + { + "fieldname": "opportunity", + "fieldtype": "Link", + "label": "Opportunity", + "options": "Opportunity", + "print_hide": 1, + "read_only": 1 + }, + { + "allow_on_submit": 1, + "fieldname": "lost_reasons", + "fieldtype": "Table MultiSelect", + "label": "Lost Reasons", + "options": "Lost Reason Detail", + "read_only": 1 } + ], + "icon": "fa fa-shopping-cart", + "idx": 82, + "is_submittable": 1, + "max_attachments": 1, + "modified": "2019-11-12 13:19:11.895715", + "modified_by": "Administrator", + "module": "Selling", + "name": "Quotation", + "owner": "Administrator", + "permissions": [ + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "import": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance Manager", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Maintenance Manager" + }, + { + "amend": 1, + "cancel": 1, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Maintenance User", + "share": 1, + "submit": 1, + "write": 1 + }, + { + "permlevel": 1, + "read": 1, + "report": 1, + "role": "Maintenance User" + } + ], + "search_fields": "status,transaction_date,party_name,order_type", + "show_name_in_global_search": 1, + "sort_field": "modified", + "sort_order": "DESC", + "timeline_field": "party_name", + "title_field": "title" +} \ No newline at end of file From db64c69dace07d36753e31537ec45bb7abb8e668 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 11:12:38 +0530 Subject: [PATCH 136/210] fix: reference before assignement error --- erpnext/www/book-appointment/index.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 707be6775c..1fe1987453 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -86,8 +86,8 @@ def create_appointment(date, time, tz, contact): scheduled_time = convert_to_system_timezone(tz, scheduled_time) scheduled_time = scheduled_time.replace(tzinfo=None) # Create a appointment document from form - appointment.scheduled_time = scheduled_time 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) From cce000a6d09fc860ce6d720e45a84a7325e4e4b8 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 11:48:37 +0530 Subject: [PATCH 137/210] remove: commented code --- .../appointment_booking_settings.js | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index 2642e6eb26..4dd07236ca 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -1,6 +1,3 @@ -// 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) { From f25e2a29f7888d01cc0fefde3c240e74e54094bf Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 12:01:36 +0530 Subject: [PATCH 138/210] fix:formatting --- erpnext/www/book-appointment/index.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 1fe1987453..a8ab22956d 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -82,14 +82,14 @@ 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 = scheduled_time.replace(tzinfo = None) scheduled_time = convert_to_system_timezone(tz, scheduled_time) - scheduled_time = scheduled_time.replace(tzinfo=None) + 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_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) @@ -105,7 +105,7 @@ def filter_timeslots(date, timeslots): filtered_timeslots.append(timeslot) return filtered_timeslots -def convert_to_guest_timezone(guest_tz,datetimeobject): +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) From a92f060740f5ffaa22347dc54318efb9aa4b43b2 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 12:13:42 +0530 Subject: [PATCH 139/210] multiple fixes in index.js --- erpnext/www/book-appointment/index.js | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 6bd868bbc7..70ed4c2ecd 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -185,30 +185,30 @@ function setup_details_page() { } 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; } - get_form_data(); + let contact = 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, + 'contact': contact, 'tz':window.selected_timezone } })).message; frappe.msgprint(__('Appointment Created Successfully')); - let button = document.getElementById('submit-button'); - button.disabled = true; - button.onclick = null } function get_form_data() { contact = {}; let inputs = ['name', 'skype', 'number', 'notes', 'email']; inputs.forEach((id) => contact[id] = document.getElementById(`customer_${id}`).value) - window.contact = contact + return contact } From c72e1f812dea12bd25b2a43087ee60796e8dc79b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 12:59:05 +0530 Subject: [PATCH 140/210] adjust padding for appointment booking --- erpnext/www/book-appointment/index.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 10fe09ab3c..9e470dafea 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -21,7 +21,7 @@
@@ -40,7 +40,7 @@

Add details

-

Selected date is at +

Selected date is at

From 67f191df4edecd43de1a7d4904792fe088e8aae2 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 14:14:10 +0530 Subject: [PATCH 141/210] padding fixes for timeslot divs --- erpnext/www/book-appointment/index.css | 2 -- 1 file changed, 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index 30ce957e2c..0959d5c4cd 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -1,6 +1,4 @@ .time-slot { - flex-grow: 1; - flex : 0 0 calc(16.66% - 20px); margin-bottom: 2em; margin-left: 0.5em; margin-right: 0.5em; From b1e9fb9e144e91e4f37a4eaff136496ff776d209 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 15:32:56 +0530 Subject: [PATCH 142/210] fix: buttons on page of appointment scheduling --- erpnext/www/book-appointment/index.css | 8 ++++++++ erpnext/www/book-appointment/index.html | 6 +++--- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book-appointment/index.css index 0959d5c4cd..6c49fde739 100644 --- a/erpnext/www/book-appointment/index.css +++ b/erpnext/www/book-appointment/index.css @@ -9,6 +9,14 @@ padding: 0.5em 1em; } +@media (max-width: 768px) { + #submit-button-area { + display: grid; + grid-template-areas: + "submit" + "back"; + } +} #customer-form{ border-color: black; } diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 9e470dafea..8ddfc2928b 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -54,9 +54,9 @@ -
-
-
+
+
+
From 6e6954cab8179af978b0650fd90f1f6cfdd84c7b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 16:00:59 +0530 Subject: [PATCH 143/210] timezone aware datetime --- erpnext/www/book-appointment/index.js | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 70ed4c2ecd..457c6cf1a4 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -123,9 +123,10 @@ function clear_time_slots() { } function get_slot_layout(time) { + let timezone = document.getElementById("appointment-timezone").value; 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 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 `${start_time_string}
to ${end_time_string}`; } From c31808f5b2f87c19bc366a68a8c8b575477773a3 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 13 Nov 2019 16:47:51 +0530 Subject: [PATCH 144/210] fix margins --- erpnext/www/book-appointment/index.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book-appointment/index.html index 8ddfc2928b..96774d5656 100644 --- a/erpnext/www/book-appointment/index.html +++ b/erpnext/www/book-appointment/index.html @@ -30,7 +30,7 @@
-
+
From 793ba8fc06ac5fec09b5c7a52cb73bd44b3f903b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 14 Nov 2019 11:25:49 +0530 Subject: [PATCH 145/210] pretty timezone names --- erpnext/www/book-appointment/index.js | 13 +++++++++---- erpnext/www/book-appointment/index.py | 13 ++++++++++++- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 457c6cf1a4..b91e3b08eb 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -13,26 +13,31 @@ async function initialise_select_date() { } async function get_global_variables() { - // Using await + // 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' + 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(); - window.timezones.forEach(timezone => { + Object.keys(window.timezones).forEach((timezone) => { let opt = document.createElement('option'); opt.value = timezone; if (timezone == moment.tz.guess()) { opt.selected = true; } - opt.innerHTML = timezone; + opt.innerHTML = window.timezones[timezone] timezones_element.appendChild(opt) }); } diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index a8ab22956d..163fdc0132 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -25,7 +25,18 @@ def get_appointment_settings(): @frappe.whitelist(allow_guest=True) def get_timezones(): - return pytz.all_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): From 511780a4d4d6c1a03601f167635ef66ba4cbcb2f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 14 Nov 2019 12:47:08 +0530 Subject: [PATCH 146/210] feat: configurable redirect on success --- .../appointment_booking_settings.json | 22 +++++++++++++++---- erpnext/www/book-appointment/index.js | 22 ++++++++++++++++--- erpnext/www/book-appointment/index.py | 1 + 3 files changed, 38 insertions(+), 7 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 25a7c69268..aafdfd960a 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -13,7 +13,9 @@ "appointment_details_section", "appointment_duration", "email_reminders", - "advance_booking_days" + "advance_booking_days", + "success_details", + "success_redirect_url" ], "fields": [ { @@ -28,7 +30,7 @@ "fieldname": "number_of_agents", "fieldtype": "Int", "in_list_view": 1, - "label": "No. Of Agents", + "label": "Number of Concurrent Appointments", "reqd": 1 }, { @@ -48,9 +50,10 @@ }, { "default": "0", + "description": "Notify customer and agent via email on the day of the appointment.", "fieldname": "email_reminders", "fieldtype": "Check", - "label": "Email Reminders" + "label": "Notify Via Email" }, { "default": "7", @@ -82,10 +85,21 @@ "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-10-04 11:36:20.839075", + "modified": "2019-11-14 12:17:08.721683", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index b91e3b08eb..433b956014 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -200,16 +200,32 @@ async function submit() { return; } let contact = get_form_data(); - let appointment = (await frappe.call({ + 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;},2) + }, + error: (err)=>{ + frappe.show_alert("Something went wrong please try again"); + button.disabled = false; } - })).message; - frappe.msgprint(__('Appointment Created Successfully')); + }); } function get_form_data() { diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book-appointment/index.py index 163fdc0132..5b60dd5e7b 100644 --- a/erpnext/www/book-appointment/index.py +++ b/erpnext/www/book-appointment/index.py @@ -107,6 +107,7 @@ def create_appointment(date, time, tz, contact): appointment.customer_email = contact.get('email', None) appointment.status = 'Open' appointment.insert() + return appointment # Helper Functions def filter_timeslots(date, timeslots): From 57bd1308eba1c7ced40121546b189215a286a210 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 15 Nov 2019 08:25:48 +0530 Subject: [PATCH 147/210] fix: Validation messages code cleanup --- .../doctype/share_transfer/share_transfer.py | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index 512828b750..df4a1d14a7 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -97,17 +97,17 @@ class ShareTransfer(Document): if not self.asset_account: frappe.throw(_('The field Asset Account cannot be blank')) else: - if self.from_shareholder is None or self.to_shareholder is None: + if not self.from_shareholder or not self.to_shareholder: frappe.throw(_('The fields From Shareholder and To Shareholder cannot be blank')) - if self.to_folio_no is None or self.to_folio_no is '': + if not self.to_folio_no: self.to_folio_no = self.autoname_folio(self.to_shareholder) - if self.equity_or_liability_account is None: + if not self.equity_or_liability_account: frappe.throw(_('The field Equity/Liability Account cannot be blank')) if self.from_shareholder == self.to_shareholder: frappe.throw(_('The seller and the buyer cannot be the same')) if self.no_of_shares != self.to_no - self.from_no + 1: frappe.throw(_('The number of shares and the share numbers are inconsistent')) - if self.amount is None: + if not self.amount: self.amount = self.rate * self.no_of_shares if self.amount != self.rate * self.no_of_shares: frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated')) @@ -190,9 +190,9 @@ class ShareTransfer(Document): doc = frappe.get_doc('Shareholder', self.get(shareholder)) if doc.company != self.company: frappe.throw(_('The shareholder does not belong to this company')) - if doc.folio_no is '' or doc.folio_no is None: + if not doc.folio_no: doc.folio_no = self.from_folio_no \ - if (shareholder == 'from_shareholder') else self.to_folio_no; + if (shareholder == 'from_shareholder') else self.to_folio_no doc.save() else: if doc.folio_no and doc.folio_no != (self.from_folio_no if (shareholder == 'from_shareholder') else self.to_folio_no): From 18fda5a57173fb4204a0e69f1dc06e90eb7dab6b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 15 Nov 2019 11:58:21 +0530 Subject: [PATCH 148/210] add appointment list to module page --- erpnext/config/crm.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index eba6c7a02a..8344c66c1f 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -46,6 +46,11 @@ def get_data(): "name": "Contract", "description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"), }, + { + "type": "doctype", + "name": "Appointment", + "description" : _("Helps you manage appointments with your leads"), + }, ] }, { From 28a7ce9a5027b47b2718d5a4264955ec57432910 Mon Sep 17 00:00:00 2001 From: Anurag Mishra Date: Mon, 28 Oct 2019 11:23:14 +0530 Subject: [PATCH 149/210] fix: On Specific case if no item code in name --- erpnext/manufacturing/doctype/bom/bom.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 225ae29429..c15b52ea38 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -39,9 +39,11 @@ class BOM(WebsiteGenerator): names = [d[-1][1:] for d in filter(lambda x: len(x) > 1 and x[-1], names)] # split by (-) if cancelled - names = [cint(name.split('-')[-1]) for name in names] - - idx = max(names) + 1 + if names: + names = [cint(name.split('-')[-1]) for name in names] + idx = max(names) + 1 + else: + idx = 1 else: idx = 1 From 53b65ab8ed82d4398e657911ee24c4c6d70af14f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 15 Nov 2019 16:42:32 +0530 Subject: [PATCH 150/210] Add status expired to doctype quotation --- erpnext/hooks.py | 3 ++- erpnext/selling/doctype/quotation/quotation.py | 8 ++++++++ erpnext/selling/doctype/quotation/quotation_list.js | 8 +++----- 3 files changed, 13 insertions(+), 6 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 9e74bfd290..715839c58f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -301,7 +301,8 @@ scheduler_events = { "erpnext.quality_management.doctype.quality_review.quality_review.review", "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", - "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status" + "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", + "erpnext.selling.doctype.quotation.set_expired" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 4a56e40400..82e98277ee 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -185,6 +185,14 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist +def set_expired_status(): + quotations = frappe.get_all("Quotation") + for quotation in quotations: + quotation = frappe.get_doc("Quotation",quotation.name) + if quotation.valid_till and getdate(quotation.valid_till) < getdate(nowdate()): + frappe.db.set(quotation,'status','Expired') + frappe.db.commit() + @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): return _make_sales_invoice(source_name, target_doc) diff --git a/erpnext/selling/doctype/quotation/quotation_list.js b/erpnext/selling/doctype/quotation/quotation_list.js index 5f4e2546fb..802c0ba641 100644 --- a/erpnext/selling/doctype/quotation/quotation_list.js +++ b/erpnext/selling/doctype/quotation/quotation_list.js @@ -14,15 +14,13 @@ frappe.listview_settings['Quotation'] = { get_indicator: function(doc) { if(doc.status==="Open") { - if (doc.valid_till && doc.valid_till < frappe.datetime.nowdate()) { - return [__("Expired"), "darkgrey", "valid_till,<," + frappe.datetime.nowdate()]; - } else { - return [__("Open"), "orange", "status,=,Open"]; - } + return [__("Open"), "orange", "status,=,Open"]; } else if(doc.status==="Ordered") { return [__("Ordered"), "green", "status,=,Ordered"]; } else if(doc.status==="Lost") { return [__("Lost"), "darkgrey", "status,=,Lost"]; + } else if(doc.status==="Expired") { + return [__("Expired"), "darkgrey", "status,=,Expired"]; } } }; From 6ef057a2a3225e405435f67dd9c77b11c43eaed0 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Nov 2019 15:55:32 +0530 Subject: [PATCH 151/210] fix: Prefilled JV via Account Balance and Stock Value mismatch error message - Make JV button will route to Journal Entry and add rows in child table --- erpnext/accounts/general_ledger.py | 31 ++++++++++++++++------- erpnext/public/js/controllers/accounts.js | 18 ++++++++++++- 2 files changed, 39 insertions(+), 10 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 38f283c8d4..4e9ef0b410 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,16 +163,29 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: - error_reason = _("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and it's linked warehouses.").format( - account_bal, stock_bal, frappe.bold(account)) - error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(stock_bal - account_bal)) - button_text = _("Make Adjustment Entry") + diff = flt(stock_bal - account_bal) + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( + stock_bal, account_bal, frappe.bold(account)) + error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) + stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") - frappe.throw("""{0}

{1}

-
- -
""".format(error_reason, error_resolution, button_text), - StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync')) + db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') + db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') + + journal_entry_args = { + 'accounts':[ + {'account': account, db_or_cr_warehouse_account : abs(diff)}, + {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] + } + + frappe.msgprint(msg="""{0}

{1}

""".format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': 'Make JV', + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': journal_entry_args + }) def validate_cwip_accounts(gl_map): cwip_enabled = cint(frappe.get_cached_value("Company", diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 3dfc8911fc..eb99192b88 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -64,7 +64,7 @@ frappe.ui.form.on(cur_frm.doctype, { } }) } - } + } }); frappe.ui.form.on('Sales Invoice Payment', { @@ -356,3 +356,19 @@ cur_frm.pformat.taxes= function(doc){ } return out; } + +erpnext.route_to_adjustment_jv = (args) => { + frappe.model.with_doctype('Journal Entry', () => { + // route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch + let journal_entry = frappe.model.get_new_doc('Journal Entry'); + + args.accounts.forEach((je_account) => { + let child_row = frappe.model.add_child(journal_entry, "accounts"); + child_row.account = je_account.account; + child_row.debit_in_account_currency = je_account.debit_in_account_currency; + child_row.credit_in_account_currency = je_account.credit_in_account_currency; + child_row.party_type = "" ; + }); + frappe.set_route('Form','Journal Entry', journal_entry.name); + }); +} \ No newline at end of file From 9c1c4ef3dd684cddb42bc792ed0d841677bb2b7d Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 18 Nov 2019 17:52:19 +0530 Subject: [PATCH 152/210] refactor: Share transfer cancellation and code cleanup --- .../doctype/share_transfer/share_transfer.py | 234 +++--- .../doctype/shareholder/shareholder.json | 694 ++++-------------- 2 files changed, 247 insertions(+), 681 deletions(-) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index df4a1d14a7..456f2ba2b3 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -13,9 +13,9 @@ from frappe.utils import nowdate class ShareDontExists(ValidationError): pass class ShareTransfer(Document): - def before_submit(self): + def on_submit(self): if self.transfer_type == 'Issue': - shareholder = self.get_shareholder_doc(self.company) + shareholder = self.get_company_shareholder() shareholder.append('share_balance', { 'share_type': self.share_type, 'from_no': self.from_no, @@ -28,7 +28,7 @@ class ShareTransfer(Document): }) shareholder.save() - doc = frappe.get_doc('Shareholder', self.to_shareholder) + doc = self.get_shareholder_doc(self.to_shareholder) doc.append('share_balance', { 'share_type': self.share_type, 'from_no': self.from_no, @@ -41,11 +41,11 @@ class ShareTransfer(Document): elif self.transfer_type == 'Purchase': self.remove_shares(self.from_shareholder) - self.remove_shares(self.get_shareholder_doc(self.company).name) + self.remove_shares(self.get_company_shareholder().name) elif self.transfer_type == 'Transfer': self.remove_shares(self.from_shareholder) - doc = frappe.get_doc('Shareholder', self.to_shareholder) + doc = self.get_shareholder_doc(self.to_shareholder) doc.append('share_balance', { 'share_type': self.share_type, 'from_no': self.from_no, @@ -56,26 +56,65 @@ class ShareTransfer(Document): }) doc.save() + def on_cancel(self): + if self.transfer_type == 'Issue': + compnay_shareholder = self.get_company_shareholder() + self.remove_shares(compnay_shareholder.name) + self.remove_shares(self.to_shareholder) + + elif self.transfer_type == 'Purchase': + compnay_shareholder = self.get_company_shareholder() + from_shareholder = self.get_shareholder_doc(self.from_shareholder) + + from_shareholder.append('share_balance', { + 'share_type': self.share_type, + 'from_no': self.from_no, + 'to_no': self.to_no, + 'rate': self.rate, + 'amount': self.amount, + 'no_of_shares': self.no_of_shares + }) + + from_shareholder.save() + + compnay_shareholder.append('share_balance', { + 'share_type': self.share_type, + 'from_no': self.from_no, + 'to_no': self.to_no, + 'rate': self.rate, + 'amount': self.amount, + 'no_of_shares': self.no_of_shares + }) + + compnay_shareholder.save() + + elif self.transfer_type == 'Transfer': + self.remove_shares(self.to_shareholder) + from_shareholder = self.get_shareholder_doc(self.from_shareholder) + from_shareholder.append('share_balance', { + 'share_type': self.share_type, + 'from_no': self.from_no, + 'to_no': self.to_no, + 'rate': self.rate, + 'amount': self.amount, + 'no_of_shares': self.no_of_shares + }) + from_shareholder.save() + def validate(self): + self.get_company_shareholder() self.basic_validations() self.folio_no_validation() + if self.transfer_type == 'Issue': - if not self.get_shareholder_doc(self.company): - shareholder = frappe.get_doc({ - 'doctype': 'Shareholder', - 'title': self.company, - 'company': self.company, - 'is_company': 1 - }) - shareholder.insert() - # validate share doesnt exist in company - ret_val = self.share_exists(self.get_shareholder_doc(self.company).name) - if ret_val != False: + # validate share doesn't exist in company + ret_val = self.share_exists(self.get_company_shareholder().name) + if ret_val: frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError) else: # validate share exists with from_shareholder ret_val = self.share_exists(self.from_shareholder) - if ret_val != True: + if not ret_val: frappe.throw(_("The shares don't exist with the {0}") .format(self.from_shareholder), ShareDontExists) @@ -113,81 +152,24 @@ class ShareTransfer(Document): frappe.throw(_('There are inconsistencies between the rate, no of shares and the amount calculated')) def share_exists(self, shareholder): - # return True if exits, - # False if completely doesn't exist, - # 'partially exists' if partailly doesn't exist - ret_val = self.recursive_share_check(shareholder, self.share_type, - query = { - 'from_no': self.from_no, - 'to_no': self.to_no - } - ) - if all(boolean == True for boolean in ret_val): - return True - elif True in ret_val: - return 'partially exists' - else: - return False - - def recursive_share_check(self, shareholder, share_type, query): - # query = {'from_no': share_starting_no, 'to_no': share_ending_no} - # Recursive check if a given part of shares is held by the shareholder - # return a list containing True and False - # Eg. [True, False, True] - # All True implies its completely inside - # All False implies its completely outside - # A mix implies its partially inside/outside - does_share_exist = [] - doc = frappe.get_doc('Shareholder', shareholder) + doc = self.get_shareholder_doc(shareholder) for entry in doc.share_balance: - if entry.share_type != share_type or \ - entry.from_no > query['to_no'] or \ - entry.to_no < query['from_no']: + if entry.share_type != self.share_type or \ + entry.from_no > self.to_no or \ + entry.to_no < self.from_no: continue # since query lies outside bounds - elif entry.from_no <= query['from_no'] and entry.to_no >= query['to_no']: - return [True] # absolute truth! - elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']: - # split and check - does_share_exist.extend(self.recursive_share_check(shareholder, - share_type, - { - 'from_no': query['from_no'], - 'to_no': entry.from_no - 1 - } - )) - does_share_exist.append(True) - does_share_exist.extend(self.recursive_share_check(shareholder, - share_type, - { - 'from_no': entry.to_no + 1, - 'to_no': query['to_no'] - } - )) - elif query['from_no'] <= entry.from_no <= query['to_no'] and entry.to_no >= query['to_no']: - does_share_exist.extend(self.recursive_share_check(shareholder, - share_type, - { - 'from_no': query['from_no'], - 'to_no': entry.from_no - 1 - } - )) - elif query['from_no'] <= entry.to_no <= query['to_no'] and entry.from_no <= query['from_no']: - does_share_exist.extend(self.recursive_share_check(shareholder, - share_type, - { - 'from_no': entry.to_no + 1, - 'to_no': query['to_no'] - } - )) + elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside + return True # absolute truth! + elif (entry.from_no <= self.from_no <= self.to_no) or entry.from_no <= self.to_no and entry.to_no: + return True - does_share_exist.append(False) - return does_share_exist + return False def folio_no_validation(self): shareholders = ['from_shareholder', 'to_shareholder'] shareholders = [shareholder for shareholder in shareholders if self.get(shareholder) is not ''] for shareholder in shareholders: - doc = frappe.get_doc('Shareholder', self.get(shareholder)) + doc = self.get_shareholder_doc(self.get(shareholder)) if doc.company != self.company: frappe.throw(_('The shareholder does not belong to this company')) if not doc.folio_no: @@ -200,24 +182,14 @@ class ShareTransfer(Document): def autoname_folio(self, shareholder, is_company=False): if is_company: - doc = self.get_shareholder_doc(shareholder) + doc = self.get_company_shareholder() else: - doc = frappe.get_doc('Shareholder' , shareholder) + doc = self.get_shareholder_doc(shareholder) doc.folio_no = make_autoname('FN.#####') doc.save() return doc.folio_no def remove_shares(self, shareholder): - self.iterative_share_removal(shareholder, self.share_type, - { - 'from_no': self.from_no, - 'to_no' : self.to_no - }, - rate = self.rate, - amount = self.amount - ) - - def iterative_share_removal(self, shareholder, share_type, query, rate, amount): # query = {'from_no': share_starting_no, 'to_no': share_ending_no} # Shares exist for sure # Iterate over all entries and modify entry if in entry @@ -227,31 +199,31 @@ class ShareTransfer(Document): for entry in current_entries: # use spaceage logic here - if entry.share_type != share_type or \ - entry.from_no > query['to_no'] or \ - entry.to_no < query['from_no']: + if entry.share_type != self.share_type or \ + entry.from_no > self.to_no or \ + entry.to_no < self.from_no: new_entries.append(entry) continue # since query lies outside bounds - elif entry.from_no <= query['from_no'] and entry.to_no >= query['to_no']: + elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #split - if entry.from_no == query['from_no']: - if entry.to_no == query['to_no']: + if entry.from_no == self.from_no: + if entry.to_no == self.to_no: pass #nothing to append else: - new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) else: - if entry.to_no == query['to_no']: - new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) + if entry.to_no == self.to_no: + new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) else: - new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) - new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) - elif entry.from_no >= query['from_no'] and entry.to_no <= query['to_no']: + new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) + new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + elif entry.from_no >= self.from_no and entry.to_no <= self.to_no: # split and check pass #nothing to append - elif query['from_no'] <= entry.from_no <= query['to_no'] and entry.to_no >= query['to_no']: - new_entries.append(self.return_share_balance_entry(query['to_no']+1, entry.to_no, entry.rate)) - elif query['from_no'] <= entry.to_no <= query['to_no'] and entry.from_no <= query['from_no']: - new_entries.append(self.return_share_balance_entry(entry.from_no, query['from_no']-1, entry.rate)) + elif self.from_no <= entry.from_no <= self.to_no and entry.to_no >= self.to_no: + new_entries.append(self.return_share_balance_entry(self.to_no+1, entry.to_no, entry.rate)) + elif self.from_no <= entry.to_no <= self.to_no and entry.from_no <= self.from_no: + new_entries.append(self.return_share_balance_entry(entry.from_no, self.from_no-1, entry.rate)) else: new_entries.append(entry) @@ -272,16 +244,34 @@ class ShareTransfer(Document): } def get_shareholder_doc(self, shareholder): - # Get Shareholder doc based on the Shareholder title - doc = frappe.get_list('Shareholder', - filters = [ - ('Shareholder', 'title', '=', shareholder) - ] - ) - if len(doc) == 1: - return frappe.get_doc('Shareholder', doc[0]['name']) - else: #It will necessarily by 0 indicating it doesn't exist - return False + # Get Shareholder doc based on the Shareholder name + if shareholder: + query_filters = {'name': shareholder} + + name = frappe.db.get_value('Shareholder', {'name': shareholder}, 'name') + + return frappe.get_doc('Shareholder', name) + + def get_company_shareholder(self): + # Get company doc or create one if not present + company_shareholder = frappe.db.get_value('Shareholder', + { + 'company': self.company, + 'is_company': 1 + }, 'name') + + if company_shareholder: + return frappe.get_doc('Shareholder', company_shareholder) + else: + shareholder = frappe.get_doc({ + 'doctype': 'Shareholder', + 'title': self.company, + 'company': self.company, + 'is_company': 1 + }) + shareholder.insert() + + return shareholder @frappe.whitelist() def make_jv_entry( company, account, amount, payment_account,\ diff --git a/erpnext/accounts/doctype/shareholder/shareholder.json b/erpnext/accounts/doctype/shareholder/shareholder.json index 873a3e76a3..e94aea94b7 100644 --- a/erpnext/accounts/doctype/shareholder/shareholder.json +++ b/erpnext/accounts/doctype/shareholder/shareholder.json @@ -1,587 +1,163 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "naming_series:", - "beta": 0, - "creation": "2017-12-25 16:50:53.878430", - "custom": 0, - "description": "", - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "autoname": "naming_series:", + "creation": "2017-12-25 16:50:53.878430", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "title", + "column_break_2", + "naming_series", + "section_break_2", + "folio_no", + "column_break_4", + "company", + "is_company", + "address_contacts", + "address_html", + "column_break_9", + "contact_html", + "section_break_3", + "share_balance", + "contact_list" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "title", - "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": "Title", - "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 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_2", - "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 - }, + "fieldname": "column_break_2", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "", - "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": "", - "length": 0, - "no_copy": 0, - "options": "ACC-SH-.YYYY.-", - "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 - }, + "fieldname": "naming_series", + "fieldtype": "Select", + "options": "ACC-SH-.YYYY.-" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_2", - "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, - "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 - }, + "fieldname": "section_break_2", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "folio_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": "Folio no.", - "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, + "fieldname": "folio_no", + "fieldtype": "Data", + "label": "Folio no.", + "read_only": 1, "unique": 1 - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_4", - "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 - }, + "fieldname": "column_break_4", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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": 1, - "in_standard_filter": 0, - "label": "Company", - "length": 0, - "no_copy": 0, - "options": "Company", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Company", + "options": "Company", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_company", - "fieldtype": "Check", - "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": "Is Company", - "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 - }, + "default": "0", + "fieldname": "is_company", + "fieldtype": "Check", + "hidden": 1, + "label": "Is Company", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "address_contacts", - "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 and Contacts", - "length": 0, - "no_copy": 0, - "options": "fa fa-map-marker", - "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 - }, + "fieldname": "address_contacts", + "fieldtype": "Section Break", + "label": "Address and Contacts", + "options": "fa fa-map-marker" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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, - "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 - }, + "fieldname": "address_html", + "fieldtype": "HTML", + "label": "Address HTML", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "column_break_9", - "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 - }, + "fieldname": "column_break_9", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 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, - "options": "", - "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 - }, + "fieldname": "contact_html", + "fieldtype": "HTML", + "label": "Contact HTML", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_3", - "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": "Share Balance", - "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 - }, + "fieldname": "section_break_3", + "fieldtype": "Section Break", + "label": "Share Balance" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "share_balance", - "fieldtype": "Table", - "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": "Share Balance", - "length": 0, - "no_copy": 0, - "options": "Share Balance", - "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 - }, + "fieldname": "share_balance", + "fieldtype": "Table", + "label": "Share Balance", + "options": "Share Balance", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "Hidden list maintaining the list of contacts linked to Shareholder", - "fieldname": "contact_list", - "fieldtype": "Code", - "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": "Contact List", - "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 + "description": "Hidden list maintaining the list of contacts linked to Shareholder", + "fieldname": "contact_list", + "fieldtype": "Code", + "hidden": 1, + "label": "Contact List", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-09-18 14:14:24.953014", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Shareholder", - "name_case": "Title Case", - "owner": "Administrator", + ], + "modified": "2019-11-17 23:24:11.395882", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Shareholder", + "name_case": "Title Case", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "System Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "System Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Accounts User", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "export": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts User", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "folio_no", - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "title_field": "title", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "search_fields": "folio_no", + "sort_field": "modified", + "sort_order": "DESC", + "title_field": "title", + "track_changes": 1 } \ No newline at end of file From 0debcf9f2f6d4c8b4b7706b466ff815770575933 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 18 Nov 2019 22:12:29 +0530 Subject: [PATCH 153/210] fix: Share existing condition logic --- .../doctype/share_transfer/share_transfer.py | 14 +- .../share_transfer/test_share_transfer.py | 154 +++++++++--------- 2 files changed, 85 insertions(+), 83 deletions(-) diff --git a/erpnext/accounts/doctype/share_transfer/share_transfer.py b/erpnext/accounts/doctype/share_transfer/share_transfer.py index 456f2ba2b3..65f248e7bd 100644 --- a/erpnext/accounts/doctype/share_transfer/share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/share_transfer.py @@ -109,12 +109,12 @@ class ShareTransfer(Document): if self.transfer_type == 'Issue': # validate share doesn't exist in company ret_val = self.share_exists(self.get_company_shareholder().name) - if ret_val: + if ret_val in ('Complete', 'Partial'): frappe.throw(_('The shares already exist'), frappe.DuplicateEntryError) else: # validate share exists with from_shareholder ret_val = self.share_exists(self.from_shareholder) - if not ret_val: + if ret_val in ('Outside', 'Partial'): frappe.throw(_("The shares don't exist with the {0}") .format(self.from_shareholder), ShareDontExists) @@ -159,11 +159,13 @@ class ShareTransfer(Document): entry.to_no < self.from_no: continue # since query lies outside bounds elif entry.from_no <= self.from_no and entry.to_no >= self.to_no: #both inside - return True # absolute truth! - elif (entry.from_no <= self.from_no <= self.to_no) or entry.from_no <= self.to_no and entry.to_no: - return True + return 'Complete' # absolute truth! + elif entry.from_no <= self.from_no <= self.to_no: + return 'Partial' + elif entry.from_no <= self.to_no <= entry.to_no: + return 'Partial' - return False + return 'Outside' def folio_no_validation(self): shareholders = ['from_shareholder', 'to_shareholder'] diff --git a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py index 910dfd05da..2ff9b02129 100644 --- a/erpnext/accounts/doctype/share_transfer/test_share_transfer.py +++ b/erpnext/accounts/doctype/share_transfer/test_share_transfer.py @@ -15,73 +15,73 @@ class TestShareTransfer(unittest.TestCase): frappe.db.sql("delete from `tabShare Balance`") share_transfers = [ { - "doctype" : "Share Transfer", - "transfer_type" : "Issue", - "date" : "2018-01-01", - "to_shareholder" : "SH-00001", - "share_type" : "Equity", - "from_no" : 1, - "to_no" : 500, - "no_of_shares" : 500, - "rate" : 10, - "company" : "_Test Company", - "asset_account" : "Cash - _TC", + "doctype": "Share Transfer", + "transfer_type": "Issue", + "date": "2018-01-01", + "to_shareholder": "SH-00001", + "share_type": "Equity", + "from_no": 1, + "to_no": 500, + "no_of_shares": 500, + "rate": 10, + "company": "_Test Company", + "asset_account": "Cash - _TC", "equity_or_liability_account": "Creditors - _TC" }, { - "doctype" : "Share Transfer", - "transfer_type" : "Transfer", - "date" : "2018-01-02", - "from_shareholder" : "SH-00001", - "to_shareholder" : "SH-00002", - "share_type" : "Equity", - "from_no" : 101, - "to_no" : 200, - "no_of_shares" : 100, - "rate" : 15, - "company" : "_Test Company", + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-02", + "from_shareholder": "SH-00001", + "to_shareholder": "SH-00002", + "share_type": "Equity", + "from_no": 101, + "to_no": 200, + "no_of_shares": 100, + "rate": 15, + "company": "_Test Company", "equity_or_liability_account": "Creditors - _TC" }, { - "doctype" : "Share Transfer", - "transfer_type" : "Transfer", - "date" : "2018-01-03", - "from_shareholder" : "SH-00001", - "to_shareholder" : "SH-00003", - "share_type" : "Equity", - "from_no" : 201, - "to_no" : 500, - "no_of_shares" : 300, - "rate" : 20, - "company" : "_Test Company", + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-03", + "from_shareholder": "SH-00001", + "to_shareholder": "SH-00003", + "share_type": "Equity", + "from_no": 201, + "to_no": 500, + "no_of_shares": 300, + "rate": 20, + "company": "_Test Company", "equity_or_liability_account": "Creditors - _TC" }, { - "doctype" : "Share Transfer", - "transfer_type" : "Transfer", - "date" : "2018-01-04", - "from_shareholder" : "SH-00003", - "to_shareholder" : "SH-00002", - "share_type" : "Equity", - "from_no" : 201, - "to_no" : 400, - "no_of_shares" : 200, - "rate" : 15, - "company" : "_Test Company", + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-04", + "from_shareholder": "SH-00003", + "to_shareholder": "SH-00002", + "share_type": "Equity", + "from_no": 201, + "to_no": 400, + "no_of_shares": 200, + "rate": 15, + "company": "_Test Company", "equity_or_liability_account": "Creditors - _TC" }, { - "doctype" : "Share Transfer", - "transfer_type" : "Purchase", - "date" : "2018-01-05", - "from_shareholder" : "SH-00003", - "share_type" : "Equity", - "from_no" : 401, - "to_no" : 500, - "no_of_shares" : 100, - "rate" : 25, - "company" : "_Test Company", - "asset_account" : "Cash - _TC", + "doctype": "Share Transfer", + "transfer_type": "Purchase", + "date": "2018-01-05", + "from_shareholder": "SH-00003", + "share_type": "Equity", + "from_no": 401, + "to_no": 500, + "no_of_shares": 100, + "rate": 25, + "company": "_Test Company", + "asset_account": "Cash - _TC", "equity_or_liability_account": "Creditors - _TC" } ] @@ -91,33 +91,33 @@ class TestShareTransfer(unittest.TestCase): def test_invalid_share_transfer(self): doc = frappe.get_doc({ - "doctype" : "Share Transfer", - "transfer_type" : "Transfer", - "date" : "2018-01-05", - "from_shareholder" : "SH-00003", - "to_shareholder" : "SH-00002", - "share_type" : "Equity", - "from_no" : 1, - "to_no" : 100, - "no_of_shares" : 100, - "rate" : 15, - "company" : "_Test Company", + "doctype": "Share Transfer", + "transfer_type": "Transfer", + "date": "2018-01-05", + "from_shareholder": "SH-00003", + "to_shareholder": "SH-00002", + "share_type": "Equity", + "from_no": 1, + "to_no": 100, + "no_of_shares": 100, + "rate": 15, + "company": "_Test Company", "equity_or_liability_account": "Creditors - _TC" }) self.assertRaises(ShareDontExists, doc.insert) doc = frappe.get_doc({ - "doctype" : "Share Transfer", - "transfer_type" : "Purchase", - "date" : "2018-01-02", - "from_shareholder" : "SH-00001", - "share_type" : "Equity", - "from_no" : 1, - "to_no" : 200, - "no_of_shares" : 200, - "rate" : 15, - "company" : "_Test Company", - "asset_account" : "Cash - _TC", + "doctype": "Share Transfer", + "transfer_type": "Purchase", + "date": "2018-01-02", + "from_shareholder": "SH-00001", + "share_type": "Equity", + "from_no": 1, + "to_no": 200, + "no_of_shares": 200, + "rate": 15, + "company": "_Test Company", + "asset_account": "Cash - _TC", "equity_or_liability_account": "Creditors - _TC" }) self.assertRaises(ShareDontExists, doc.insert) From b9460ed22c96b1d35186aea9555cf95f7b383987 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 19 Nov 2019 10:46:07 +0530 Subject: [PATCH 154/210] switched ORM methods for single SQL query --- erpnext/selling/doctype/quotation/quotation.py | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 82e98277ee..9903884b88 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -186,12 +186,10 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist def set_expired_status(): - quotations = frappe.get_all("Quotation") - for quotation in quotations: - quotation = frappe.get_doc("Quotation",quotation.name) - if quotation.valid_till and getdate(quotation.valid_till) < getdate(nowdate()): - frappe.db.set(quotation,'status','Expired') - frappe.db.commit() + from datetime import date + DATE_FORMAT = "%Y%m%d" # For converting python date to SQL comparable date + today = date.today().strftime(DATE_FORMAT) + frappe.db.sql("UPDATE tabQuotation SET status = 'Expired' WHERE valid_till < " + today) @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): From 539ea2cefbe4e38e073ae4d549611624ed292f70 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 19 Nov 2019 10:56:58 +0530 Subject: [PATCH 155/210] Rename doctype `Appointment Booking Slots` --- .../appointment_booking_settings.js | 2 +- .../appointment_booking_settings.json | 4 ++-- .../__init__.py | 0 .../appointment_booking_slots.json} | 6 +++--- .../appointment_booking_slots.py} | 5 ++--- 5 files changed, 8 insertions(+), 9 deletions(-) rename erpnext/crm/doctype/{availabilty_of_slots => appointment_booking_slots}/__init__.py (100%) rename erpnext/crm/doctype/{availabilty_of_slots/availability_of_slots.json => appointment_booking_slots/appointment_booking_slots.json} (86%) rename erpnext/crm/doctype/{availabilty_of_slots/availabilty_of_slots.py => appointment_booking_slots/appointment_booking_slots.py} (83%) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js index 4dd07236ca..99b82148d2 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.js @@ -4,7 +4,7 @@ function check_times(frm) { 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"`)); + frappe.throw(__(`In row ${i + 1} of Appointment Booking 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 index aafdfd960a..2c161ee0c2 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -22,7 +22,7 @@ "fieldname": "availability_of_slots", "fieldtype": "Table", "label": "Availability Of Slots", - "options": "Availability Of Slots", + "options": "Appointment Booking Slots", "reqd": 1 }, { @@ -99,7 +99,7 @@ } ], "issingle": 1, - "modified": "2019-11-14 12:17:08.721683", + "modified": "2019-11-19 10:53:26.935061", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/crm/doctype/availabilty_of_slots/__init__.py b/erpnext/crm/doctype/appointment_booking_slots/__init__.py similarity index 100% rename from erpnext/crm/doctype/availabilty_of_slots/__init__.py rename to erpnext/crm/doctype/appointment_booking_slots/__init__.py diff --git a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json b/erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.json similarity index 86% rename from erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json rename to erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.json index d26f7ced35..ddf8738629 100644 --- a/erpnext/crm/doctype/availabilty_of_slots/availability_of_slots.json +++ b/erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.json @@ -1,5 +1,5 @@ { - "creation": "2019-08-27 10:52:54.204677", + "creation": "2019-11-19 10:49:49.494927", "doctype": "DocType", "editable_grid": 1, "engine": "InnoDB", @@ -33,10 +33,10 @@ } ], "istable": 1, - "modified": "2019-08-27 10:52:54.204677", + "modified": "2019-11-19 10:49:49.494927", "modified_by": "Administrator", "module": "CRM", - "name": "Availabilty Of Slots", + "name": "Appointment Booking Slots", "owner": "Administrator", "permissions": [], "quick_entry": 1, diff --git a/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py b/erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.py similarity index 83% rename from erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py rename to erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.py index bd764806ba..3cadbc9559 100644 --- a/erpnext/crm/doctype/availabilty_of_slots/availabilty_of_slots.py +++ b/erpnext/crm/doctype/appointment_booking_slots/appointment_booking_slots.py @@ -6,6 +6,5 @@ from __future__ import unicode_literals # import frappe from frappe.model.document import Document - -class AvailabiltyOfSlots(Document): - pass +class AppointmentBookingSlots(Document): + pass From a99897841536fb34508dc549a4a917ab22ee6b6a Mon Sep 17 00:00:00 2001 From: RJPvT <48353029+RJPvT@users.noreply.github.com> Date: Tue, 19 Nov 2019 10:24:38 +0100 Subject: [PATCH 156/210] fix: pending on review date (#19609) * fix: On Specific case if no item code in name * fix: pending on review date --- erpnext/projects/doctype/task/task.py | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 54fce8d6db..7083d694f8 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -7,7 +7,7 @@ import json import frappe from frappe import _, throw -from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate +from frappe.utils import add_days, cstr, date_diff, get_link_to_form, getdate, today from frappe.utils.nestedset import NestedSet from frappe.desk.form.assign_to import close_all_assignments, clear from frappe.utils import date_diff @@ -212,8 +212,11 @@ def set_multiple_status(names, status): task.save() def set_tasks_as_overdue(): - tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Completed']]}) + tasks = frappe.get_all("Task", filters={'status':['not in',['Cancelled', 'Closed']]}) for task in tasks: + if frappe.db.get_value("Task", task.name, "status") in 'Pending Review': + if getdate(frappe.db.get_value("Task", task.name, "review_date")) < getdate(today()): + continue frappe.get_doc("Task", task.name).update_status() @frappe.whitelist() From 3f854fce2eb6e9c15d6e5f84ad2d5d14b8e7882c Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 19 Nov 2019 15:07:30 +0530 Subject: [PATCH 157/210] feat: fetch leave approver from both employee and department approvers (#19613) * fix: fetch leave approvers from both department and employee master * fix: creaate a set of approvers --- .../doctype/department_approver/department_approver.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/hr/doctype/department_approver/department_approver.py b/erpnext/hr/doctype/department_approver/department_approver.py index d6b66da081..df0f75a18c 100644 --- a/erpnext/hr/doctype/department_approver/department_approver.py +++ b/erpnext/hr/doctype/department_approver/department_approver.py @@ -20,10 +20,6 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): department_details = {} department_list = [] employee = frappe.get_value("Employee", filters.get("employee"), ["department", "leave_approver"], as_dict=True) - if employee.leave_approver: - approver = frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name']) - approvers.append(approver) - return approvers employee_department = filters.get("department") or employee.department if employee_department: @@ -34,6 +30,9 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and disabled=0 order by lft desc""", (department_details.lft, department_details.rgt), as_list=True) + if filters.get("doctype") == "Leave Application" and employee.leave_approver: + approvers.append(frappe.db.get_value("User", employee.leave_approver, ['name', 'first_name', 'last_name'])) + if filters.get("doctype") == "Leave Application": parentfield = "leave_approvers" else: @@ -47,4 +46,4 @@ def get_approvers(doctype, txt, searchfield, start, page_len, filters): and approver.parentfield = %s and approver.approver=user.name""",(d, "%" + txt + "%", parentfield), as_list=True) - return approvers + return set(tuple(approver) for approver in approvers) From e13b7698139c1d6a41b22e158803216357350d50 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 19 Nov 2019 12:04:30 +0000 Subject: [PATCH 158/210] use `nowdate` instead of `date.today()` Co-Authored-By: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> --- erpnext/selling/doctype/quotation/quotation.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 9903884b88..b97eefcf19 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -189,7 +189,8 @@ def set_expired_status(): from datetime import date DATE_FORMAT = "%Y%m%d" # For converting python date to SQL comparable date today = date.today().strftime(DATE_FORMAT) - frappe.db.sql("UPDATE tabQuotation SET status = 'Expired' WHERE valid_till < " + today) + frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' + WHERE status != 'Expired' AND 'valid_till < %s""" , (nowdate())) @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): From 9db9edca2c785e3a4ed784e1a3f3c38e102b662b Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 19 Nov 2019 18:44:32 +0530 Subject: [PATCH 159/210] fix(expense claim): fetch outstanding documents based on party account type --- .../purchase_invoice/purchase_invoice.py | 16 ++++++------ erpnext/accounts/utils.py | 12 ++++++--- .../hr/doctype/expense_claim/expense_claim.py | 26 ------------------- 3 files changed, 16 insertions(+), 38 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5c53d26ad1..ba7ad37c8d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -357,7 +357,7 @@ class PurchaseInvoice(BuyingController): return if not gl_entries: gl_entries = self.get_gl_entries() - + if gl_entries: update_outstanding = "No" if (cint(self.is_paid) or self.write_off_account) else "Yes" @@ -504,7 +504,7 @@ class PurchaseInvoice(BuyingController): asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) - + if not item.is_fixed_asset: amount = flt(item.base_net_amount, item.precision("base_net_amount")) else: @@ -517,7 +517,7 @@ class PurchaseInvoice(BuyingController): "cost_center": item.cost_center, "project": item.project }, account_currency, item=item)) - + # If asset is bought through this document and not linked to PR if self.update_stock and item.landed_cost_voucher_amount: expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") @@ -539,9 +539,9 @@ class PurchaseInvoice(BuyingController): "debit": flt(item.landed_cost_voucher_amount), "project": item.project }, item=item)) - + # update gross amount of asset bought through this document - assets = frappe.db.get_all('Asset', + assets = frappe.db.get_all('Asset', filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } ) for asset in assets: @@ -633,7 +633,7 @@ class PurchaseInvoice(BuyingController): if asset_eiiav_currency == self.company_currency else item.item_tax_amount / self.conversion_rate) }, item=item)) - + # When update stock is checked # Assets are bought through this document then it will be linked to this document if self.update_stock: @@ -655,9 +655,9 @@ class PurchaseInvoice(BuyingController): "debit": flt(item.landed_cost_voucher_amount), "project": item.project }, item=item)) - + # update gross amount of assets bought through this document - assets = frappe.db.get_all('Asset', + assets = frappe.db.get_all('Asset', filters={ 'purchase_invoice': self.name, 'item_code': item.item_code } ) for asset in assets: diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 382a89b310..94697be02f 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -630,7 +630,7 @@ def get_held_invoices(party_type, party): 'select name from `tabPurchase Invoice` where release_date IS NOT NULL and release_date > CURDATE()', as_dict=1 ) - held_invoices = [d['name'] for d in held_invoices] + held_invoices = set([d['name'] for d in held_invoices]) return held_invoices @@ -639,14 +639,19 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters outstanding_invoices = [] precision = frappe.get_precision("Sales Invoice", "outstanding_amount") or 2 - if erpnext.get_party_account_type(party_type) == 'Receivable': + if account: + root_type = frappe.get_cached_value("Account", account, "root_type") + party_account_type = "Receivable" if root_type == "Asset" else "Payable" + else: + party_account_type = erpnext.get_party_account_type(party_type) + + if party_account_type == 'Receivable': dr_or_cr = "debit_in_account_currency - credit_in_account_currency" payment_dr_or_cr = "credit_in_account_currency - debit_in_account_currency" else: dr_or_cr = "credit_in_account_currency - debit_in_account_currency" payment_dr_or_cr = "debit_in_account_currency - credit_in_account_currency" - invoice = 'Sales Invoice' if erpnext.get_party_account_type(party_type) == 'Receivable' else 'Purchase Invoice' held_invoices = get_held_invoices(party_type, party) invoice_list = frappe.db.sql(""" @@ -665,7 +670,6 @@ def get_outstanding_invoices(party_type, party, account, condition=None, filters group by voucher_type, voucher_no order by posting_date, name""".format( dr_or_cr=dr_or_cr, - invoice = invoice, condition=condition or "" ), { "party_type": party_type, diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index f0036277c8..59391505fa 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -140,32 +140,6 @@ class ExpenseClaim(AccountsController): "against": ",".join([d.default_account for d in self.expenses]), "party_type": "Employee", "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }) - ) - - gl_entry.append( - self.get_gl_dict({ - "account": data.advance_account, - "debit": data.allocated_amount, - "debit_in_account_currency": data.allocated_amount, - "against": self.payable_account, - "party_type": "Employee", - "party": self.employee, - "against_voucher_type": self.doctype, - "against_voucher": self.name - }) - ) - - gl_entry.append( - self.get_gl_dict({ - "account": self.payable_account, - "credit": data.allocated_amount, - "credit_in_account_currency": data.allocated_amount, - "against": data.advance_account, - "party_type": "Employee", - "party": self.employee, "against_voucher_type": "Employee Advance", "against_voucher": data.employee_advance }) From c8e66a0f7162bed95984804c1c74cc838894ca9c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 10:27:59 +0530 Subject: [PATCH 160/210] Infer number_of_agents from agent_list in apppointment booking settings --- .../appointment_booking_settings.json | 4 +++- .../appointment_booking_settings.py | 6 ++++++ 2 files changed, 9 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 2c161ee0c2..92343dbb13 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -29,8 +29,10 @@ "default": "1", "fieldname": "number_of_agents", "fieldtype": "Int", + "hidden": 1, "in_list_view": 1, "label": "Number of Concurrent Appointments", + "read_only": 1, "reqd": 1 }, { @@ -99,7 +101,7 @@ } ], "issingle": 1, - "modified": "2019-11-19 10:53:26.935061", + "modified": "2019-11-20 10:23:37.393363", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 2874f3fae2..fd20ba0792 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -16,6 +16,12 @@ class AppointmentBookingSettings(Document): def validate(self): self.validate_availability_of_slots() + def save(self): + self.infer_number_of_agents() + + def infer_number_of_agents(): + self.number_of_agents = len(self.agent_list) + def validate_availability_of_slots(self): for record in self.availability_of_slots: from_time = datetime.datetime.strptime( From dbde140e46ecc5aaae611f5355e8482023e3b80c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 10:30:41 +0530 Subject: [PATCH 161/210] fix: save method of Appointment Booking Setting --- .../appointment_booking_settings.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index fd20ba0792..484a5729c5 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -18,8 +18,9 @@ class AppointmentBookingSettings(Document): def save(self): self.infer_number_of_agents() + super().save() - def infer_number_of_agents(): + def infer_number_of_agents(self): self.number_of_agents = len(self.agent_list) def validate_availability_of_slots(self): From fe2147a496e6d1117099394fa8e4a73035ae8cab Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 11:37:49 +0530 Subject: [PATCH 162/210] fix travis --- .../appointment_booking_settings.py | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 484a5729c5..e817271e2a 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -17,11 +17,8 @@ class AppointmentBookingSettings(Document): self.validate_availability_of_slots() def save(self): - self.infer_number_of_agents() - super().save() - - def infer_number_of_agents(self): self.number_of_agents = len(self.agent_list) + super().save() def validate_availability_of_slots(self): for record in self.availability_of_slots: From 682956543eb12ef8504cea3c9a1fb83c88cab782 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 11:45:14 +0530 Subject: [PATCH 163/210] fix travis --- .../appointment_booking_settings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 92343dbb13..dbdf432dff 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -101,7 +101,7 @@ } ], "issingle": 1, - "modified": "2019-11-20 10:23:37.393363", + "modified": "2019-11-20 11:44:59.629254", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", From ae90ea9547d934ae6ee596c72bacb5cde41731d0 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 15:24:33 +0530 Subject: [PATCH 164/210] fix:travis errors --- .../appointment_booking_settings.json | 2 +- .../appointment_booking_settings.py | 1 + 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index dbdf432dff..17e754b748 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -101,7 +101,7 @@ } ], "issingle": 1, - "modified": "2019-11-20 11:44:59.629254", + "modified": "2019-11-20 15:17:55.617364", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index e817271e2a..82acd93f90 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -10,6 +10,7 @@ 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" From 5717a265b7a68486ce41fb0920698b834af3f648 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 15:31:13 +0530 Subject: [PATCH 165/210] remove: unused imports --- erpnext/selling/doctype/quotation/quotation.py | 3 --- 1 file changed, 3 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index b97eefcf19..ba34dff745 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -186,9 +186,6 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist def set_expired_status(): - from datetime import date - DATE_FORMAT = "%Y%m%d" # For converting python date to SQL comparable date - today = date.today().strftime(DATE_FORMAT) frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' WHERE status != 'Expired' AND 'valid_till < %s""" , (nowdate())) From 4a28144941bb1def31ae8442070769f953442f9f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 15:53:19 +0530 Subject: [PATCH 166/210] add tests --- .../doctype/quotation/test_quotation.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 7ee4a76ca6..bd63c3d96a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -201,6 +201,27 @@ class TestQuotation(unittest.TestCase): sec_qo = make_quotation(item_list=qo_item2, do_not_submit=True) sec_qo.submit() + def test_expired_quotations(self): + import datetime + from erpnext.selling.doctype.quotation.quotation import set_expired_status + test_item = make_item("_Test Paraglider", + {"is_stock_item":1}) + + quotation_item = [ + { + "item_code": test_item.item_code, + "warehouse":"", + "qty": 1, + "rate": 500 + } + ] + yesterday = getdate(nowdate()) + datetime.timedelta(days=-1) + expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday,do_not_submit=True) + set_expired_status() + + self.assertEqual(expired_quotation.status,"Expired") + + test_records = frappe.get_test_records('Quotation') def get_quotation_dict(party_name=None, item_code=None): @@ -258,3 +279,5 @@ def make_quotation(**args): qo.submit() return qo + + From eed30c6d8c3a4855e6d580b66bcf85a922647a9f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:05:23 +0530 Subject: [PATCH 167/210] fix: not able to select item in sales order --- erpnext/controllers/queries.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/queries.py b/erpnext/controllers/queries.py index 3830ca0361..7b4a4c92ad 100644 --- a/erpnext/controllers/queries.py +++ b/erpnext/controllers/queries.py @@ -159,8 +159,12 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals if "description" in searchfields: searchfields.remove("description") - columns = [field for field in searchfields if not field in ["name", "item_group", "description"]] - columns = ", ".join(columns) + columns = '' + extra_searchfields = [field for field in searchfields + if not field in ["name", "item_group", "description"]] + + if extra_searchfields: + columns = ", " + ", ".join(extra_searchfields) searchfields = searchfields + [field for field in[searchfield or "name", "item_code", "item_group", "item_name"] if not field in searchfields] @@ -176,7 +180,7 @@ def item_query(doctype, txt, searchfield, start, page_len, filters, as_dict=Fals concat(substr(tabItem.item_name, 1, 40), "..."), item_name) as item_name, tabItem.item_group, if(length(tabItem.description) > 40, \ - concat(substr(tabItem.description, 1, 40), "..."), description) as description, + concat(substr(tabItem.description, 1, 40), "..."), description) as description {columns} from tabItem where tabItem.docstatus < 2 From a8318480744dac43e724a63cc0cd356198e872b1 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:21:27 +0530 Subject: [PATCH 168/210] fix: code cleanup --- erpnext/controllers/sales_and_purchase_return.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/controllers/sales_and_purchase_return.py b/erpnext/controllers/sales_and_purchase_return.py index 859529204b..81fdbbefc3 100644 --- a/erpnext/controllers/sales_and_purchase_return.py +++ b/erpnext/controllers/sales_and_purchase_return.py @@ -72,7 +72,7 @@ def validate_returned_items(doc): items_returned = False for d in doc.get("items"): - if d.item_code and (flt(d.qty) < 0 or d.get('received_qty') < 0): + if d.item_code and (flt(d.qty) < 0 or flt(d.get('received_qty')) < 0): if d.item_code not in valid_items: frappe.throw(_("Row # {0}: Returned Item {1} does not exists in {2} {3}") .format(d.idx, d.item_code, doc.doctype, doc.return_against)) From c4e6c42950076d9caf1e08a5699face0b6596b5b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 Nov 2019 11:20:49 +0530 Subject: [PATCH 169/210] fix: e-invoice issue --- erpnext/regional/italy/e-invoice.xml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/regional/italy/e-invoice.xml b/erpnext/regional/italy/e-invoice.xml index 049a7eba61..69b8e3e488 100644 --- a/erpnext/regional/italy/e-invoice.xml +++ b/erpnext/regional/italy/e-invoice.xml @@ -205,7 +205,9 @@ {%- endif %} {{ format_float(data.taxable_amount, item_meta.get_field("tax_amount").precision) }} {{ format_float(data.tax_amount, item_meta.get_field("tax_amount").precision) }} - {{ doc.vat_collectability.split("-")[0] }} + {%- if data.vat_collectability %} + {{ doc.vat_collectability.split("-")[0] }} + {%- endif %} {%- if data.tax_exemption_law %} {{ data.tax_exemption_law }} {%- endif %} From f5112905dcc806ddde0ef5f94c80e7a44fd0e7a9 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 21 Nov 2019 13:19:44 +0530 Subject: [PATCH 170/210] import make_item method in tests --- erpnext/selling/doctype/quotation/test_quotation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index bd63c3d96a..a95fd52f0a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -204,6 +204,7 @@ class TestQuotation(unittest.TestCase): def test_expired_quotations(self): import datetime from erpnext.selling.doctype.quotation.quotation import set_expired_status + from erpnext.stock.doctype.item.test_item import make_item test_item = make_item("_Test Paraglider", {"is_stock_item":1}) From 1a92eb14ed512a9600a5dc9b69ae52801e5a2ec2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Thu, 21 Nov 2019 17:58:18 +0530 Subject: [PATCH 171/210] fix: Mark attendance from employee attendance tool (#19627) --- .../employee_attendance_tool.py | 28 +++++++++++++------ 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py index 32fcee1abe..16c1a32b9b 100644 --- a/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py +++ b/erpnext/hr/doctype/employee_attendance_tool/employee_attendance_tool.py @@ -6,6 +6,7 @@ from __future__ import unicode_literals import frappe import json from frappe.model.document import Document +from frappe.utils import getdate class EmployeeAttendanceTool(Document): @@ -43,17 +44,26 @@ def get_employees(date, department = None, branch = None, company = None): @frappe.whitelist() def mark_employee_attendance(employee_list, status, date, leave_type=None, company=None): + employee_list = json.loads(employee_list) for employee in employee_list: - attendance = frappe.new_doc("Attendance") - attendance.employee = employee['employee'] - attendance.employee_name = employee['employee_name'] - attendance.attendance_date = date - attendance.status = status + if status == "On Leave" and leave_type: - attendance.leave_type = leave_type - if company: - attendance.company = company + leave_type = leave_type else: - attendance.company = frappe.db.get_value("Employee", employee['employee'], "Company") + leave_type = None + + if not company: + company = frappe.db.get_value("Employee", employee['employee'], "Company") + + attendance=frappe.get_doc(dict( + doctype='Attendance', + employee=employee.get('employee'), + employee_name=employee.get('employee_name'), + attendance_date=getdate(date), + status=status, + leave_type=leave_type, + company=company + )) + attendance.insert() attendance.submit() From 150c44b350f85bf7d566a07b3eb99dda205403ff Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 11:08:35 +0530 Subject: [PATCH 172/210] fix: asset movement ux fixes (#19641) --- erpnext/assets/doctype/asset/asset.js | 117 +++++------------- erpnext/assets/doctype/asset/asset.py | 4 +- erpnext/assets/doctype/asset/asset_list.js | 1 + .../doctype/asset_movement/asset_movement.js | 2 +- .../asset_movement/asset_movement.json | 6 +- 5 files changed, 38 insertions(+), 92 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f0889bfa1b..6b3f2c777c 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -42,6 +42,24 @@ frappe.ui.form.on('Asset', { }, setup: function(frm) { + frm.make_methods = { + 'Asset Movement': () => { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, + args:{ + "assets": [{ name: cur_frm.doc.name }] + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, + } + frm.set_query("purchase_receipt", (doc) => { return { query: "erpnext.controllers.queries.get_purchase_receipts", @@ -487,92 +505,19 @@ erpnext.asset.restore_asset = function(frm) { }) }; -erpnext.asset.transfer_asset = function(frm) { - var dialog = new frappe.ui.Dialog({ - title: __("Transfer Asset"), - fields: [ - { - "label": __("Target Location"), - "fieldname": "target_location", - "fieldtype": "Link", - "options": "Location", - "get_query": function () { - return { - filters: [ - ["Location", "is_group", "=", 0] - ] - } - }, - "reqd": 1 - }, - { - "label": __("Select Serial No"), - "fieldname": "serial_nos", - "fieldtype": "Link", - "options": "Serial No", - "get_query": function () { - return { - filters: { - 'asset': frm.doc.name - } - } - }, - "onchange": function() { - let val = this.get_value(); - if (val) { - let serial_nos = dialog.get_value("serial_no") || val; - if (serial_nos) { - serial_nos = serial_nos.split('\n'); - serial_nos.push(val); - - const unique_sn = serial_nos.filter(function(elem, index, self) { - return index === self.indexOf(elem); - }); - - dialog.set_value("serial_no", unique_sn.join('\n')); - dialog.set_value("serial_nos", ""); - } - } - } - }, - { - "label": __("Serial No"), - "fieldname": "serial_no", - "read_only": 1, - "fieldtype": "Small Text" - }, - { - "label": __("Date"), - "fieldname": "transfer_date", - "fieldtype": "Datetime", - "reqd": 1, - "default": frappe.datetime.now_datetime() +erpnext.asset.transfer_asset = function() { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, + args:{ + "assets": [{ name: cur_frm.doc.name }], + "purpose": "Transfer" + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); } - ] + } }); - - dialog.set_primary_action(__("Transfer"), function() { - var args = dialog.get_values(); - if(!args) return; - dialog.hide(); - return frappe.call({ - type: "GET", - method: "erpnext.assets.doctype.asset.asset.transfer_asset", - args: { - args: { - "asset": frm.doc.name, - "transaction_date": args.transfer_date, - "source_location": frm.doc.location, - "target_location": args.target_location, - "serial_no": args.serial_no, - "company": frm.doc.company - } - }, - freeze: true, - callback: function(r) { - cur_frm.reload_doc(); - } - }) - }); - dialog.show(); }; diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9415eedc5c..8b6bc40cf0 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -646,7 +646,7 @@ def make_journal_entry(asset_name): return je @frappe.whitelist() -def make_asset_movement(assets): +def make_asset_movement(assets, purpose=None): import json from six import string_types @@ -657,7 +657,7 @@ def make_asset_movement(assets): frappe.throw(_('Atleast one asset has to be selected.')) asset_movement = frappe.new_doc("Asset Movement") - asset_movement.quantity = len(assets) + asset_movement.purpose = purpose prev_reference_docname = '' for asset in assets: diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 46cde6ee81..02f39e0e7f 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -37,6 +37,7 @@ frappe.listview_settings['Asset'] = { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, args:{ "assets": assets }, diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index a71212ea47..89977e2952 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -132,7 +132,7 @@ frappe.ui.form.on('Asset Movement Item', { if(asset_doc.location) frappe.model.set_value(cdt, cdn, 'source_location', asset_doc.location); if(asset_doc.custodian) frappe.model.set_value(cdt, cdn, 'from_employee', asset_doc.custodian); }).catch((err) => { - console.log(err); + console.log(err); // eslint-disable-line }); } } diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index 19af81d65b..e62d684411 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -54,7 +54,7 @@ { "fieldname": "reference_doctype", "fieldtype": "Link", - "label": "Reference DocType", + "label": "Reference Document", "no_copy": 1, "options": "DocType", "reqd": 1 @@ -62,7 +62,7 @@ { "fieldname": "reference_name", "fieldtype": "Dynamic Link", - "label": "Reference Name", + "label": "Reference Document Name", "no_copy": 1, "options": "reference_doctype", "reqd": 1 @@ -93,7 +93,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-13 15:37:48.870147", + "modified": "2019-11-21 14:35:51.880332", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", From 046137caa23d2454c63b7aabe114ad14bb9944c2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Nov 2019 11:34:50 +0530 Subject: [PATCH 173/210] fix: Multiple fixes related to landed cost accounting (#19657) --- .../purchase_invoice/purchase_invoice.py | 10 ++++++--- .../purchase_invoice/test_purchase_invoice.py | 22 +------------------ .../purchase_invoice_item.json | 4 ++-- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../landed_cost_voucher.json | 6 ++--- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 5c53d26ad1..19d54a011a 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -452,6 +452,10 @@ class PurchaseInvoice(BuyingController): fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + valuation_tax_accounts = [d.account_head for d in self.get("taxes") + if d.category in ('Valuation', 'Total and Valuation') + and flt(d.base_tax_amount_after_discount_amount)] + for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -551,10 +555,10 @@ class PurchaseInvoice(BuyingController): if self.auto_accounting_for_stock and self.is_opening == "No" and \ item.item_code in stock_items and item.item_tax_amount: # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - if item.purchase_receipt: + if item.purchase_receipt and valuation_tax_accounts: negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no=%s and account=%s""", - (item.purchase_receipt, self.expenses_included_in_valuation)) + where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""", + (item.purchase_receipt, valuation_tax_accounts)) if not negative_expense_booked_in_pr: gl_entries.append( diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 85b1166790..e41ad42846 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -204,7 +204,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.insert() pi.submit() - self.check_gle_for_pi_against_pr(pi.name) + self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit @@ -225,26 +225,6 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - def check_gle_for_pi_against_pr(self, pi): - gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit - from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account""", pi, as_dict=1) - - self.assertTrue(gl_entries) - - expected_values = dict((d[0], d) for d in [ - ["Creditors - TCP1", 0, 720], - ["Stock Received But Not Billed - TCP1", 750.0, 0], - ["_Test Account Shipping Charges - TCP1", 100.0, 100.0], - ["_Test Account VAT - TCP1", 120.0, 0], - ["_Test Account Customs Duty - TCP1", 0, 150] - ]) - - for i, gle in enumerate(gl_entries): - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index dc3a1be0c7..27d8233a44 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -117,6 +117,7 @@ }, { "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -192,7 +193,6 @@ "fieldtype": "Column Break" }, { - "fetch_from": "item_code.stock_uom", "fieldname": "uom", "fieldtype": "Link", "label": "UOM", @@ -766,7 +766,7 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-03 13:43:23.782877", + "modified": "2019-11-21 16:27:52.043744", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3d96d487a8..70a80ca184 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -135,7 +135,7 @@ class SalesInvoice(SellingController): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: validate_loyalty_points(self, self.loyalty_points) - + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 46fdc8fc10..01492807de 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -8,11 +8,11 @@ "naming_series", "company", "purchase_receipts", - "sec_break1", - "taxes", "purchase_receipt_items", "get_items_from_purchase_receipts", "items", + "sec_break1", + "taxes", "section_break_9", "total_taxes_and_charges", "col_break1", @@ -123,7 +123,7 @@ ], "icon": "icon-usd", "is_submittable": 1, - "modified": "2019-10-09 13:39:36.082777", + "modified": "2019-11-21 15:34:10.846093", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", From 7af153da50fbf6ab87d407f4ae9b2077873efc1c Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Nov 2019 11:35:14 +0530 Subject: [PATCH 174/210] fix: Multiple fixes related to landed cost accounting (#19656) --- .../purchase_invoice/purchase_invoice.py | 10 ++++++--- .../purchase_invoice/test_purchase_invoice.py | 22 +------------------ .../purchase_invoice_item.json | 4 ++-- .../doctype/sales_invoice/sales_invoice.py | 2 +- .../landed_cost_voucher.json | 6 ++--- 5 files changed, 14 insertions(+), 30 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index ba7ad37c8d..c0023560ff 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -452,6 +452,10 @@ class PurchaseInvoice(BuyingController): fields = ["voucher_detail_no", "stock_value_difference"], filters={'voucher_no': self.name}): voucher_wise_stock_value.setdefault(d.voucher_detail_no, d.stock_value_difference) + valuation_tax_accounts = [d.account_head for d in self.get("taxes") + if d.category in ('Valuation', 'Total and Valuation') + and flt(d.base_tax_amount_after_discount_amount)] + for item in self.get("items"): if flt(item.base_net_amount): account_currency = get_account_currency(item.expense_account) @@ -551,10 +555,10 @@ class PurchaseInvoice(BuyingController): if self.auto_accounting_for_stock and self.is_opening == "No" and \ item.item_code in stock_items and item.item_tax_amount: # Post reverse entry for Stock-Received-But-Not-Billed if it is booked in Purchase Receipt - if item.purchase_receipt: + if item.purchase_receipt and valuation_tax_accounts: negative_expense_booked_in_pr = frappe.db.sql("""select name from `tabGL Entry` - where voucher_type='Purchase Receipt' and voucher_no=%s and account=%s""", - (item.purchase_receipt, self.expenses_included_in_valuation)) + where voucher_type='Purchase Receipt' and voucher_no=%s and account in %s""", + (item.purchase_receipt, valuation_tax_accounts)) if not negative_expense_booked_in_pr: gl_entries.append( diff --git a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py index 85b1166790..e41ad42846 100644 --- a/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/test_purchase_invoice.py @@ -204,7 +204,7 @@ class TestPurchaseInvoice(unittest.TestCase): pi.insert() pi.submit() - self.check_gle_for_pi_against_pr(pi.name) + self.check_gle_for_pi(pi.name) def check_gle_for_pi(self, pi): gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit @@ -225,26 +225,6 @@ class TestPurchaseInvoice(unittest.TestCase): self.assertEqual(expected_values[gle.account][1], gle.debit) self.assertEqual(expected_values[gle.account][2], gle.credit) - def check_gle_for_pi_against_pr(self, pi): - gl_entries = frappe.db.sql("""select account, sum(debit) as debit, sum(credit) as credit - from `tabGL Entry` where voucher_type='Purchase Invoice' and voucher_no=%s - group by account""", pi, as_dict=1) - - self.assertTrue(gl_entries) - - expected_values = dict((d[0], d) for d in [ - ["Creditors - TCP1", 0, 720], - ["Stock Received But Not Billed - TCP1", 750.0, 0], - ["_Test Account Shipping Charges - TCP1", 100.0, 100.0], - ["_Test Account VAT - TCP1", 120.0, 0], - ["_Test Account Customs Duty - TCP1", 0, 150] - ]) - - for i, gle in enumerate(gl_entries): - self.assertEqual(expected_values[gle.account][0], gle.account) - self.assertEqual(expected_values[gle.account][1], gle.debit) - self.assertEqual(expected_values[gle.account][2], gle.credit) - def test_purchase_invoice_change_naming_series(self): pi = frappe.copy_doc(test_records[1]) pi.insert() diff --git a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json index dc3a1be0c7..27d8233a44 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -117,6 +117,7 @@ }, { "fetch_from": "item_code.item_name", + "fetch_if_empty": 1, "fieldname": "item_name", "fieldtype": "Data", "in_global_search": 1, @@ -192,7 +193,6 @@ "fieldtype": "Column Break" }, { - "fetch_from": "item_code.stock_uom", "fieldname": "uom", "fieldtype": "Link", "label": "UOM", @@ -766,7 +766,7 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-03 13:43:23.782877", + "modified": "2019-11-21 16:27:52.043744", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 3d96d487a8..70a80ca184 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -135,7 +135,7 @@ class SalesInvoice(SellingController): if self.redeem_loyalty_points and self.loyalty_program and self.loyalty_points: validate_loyalty_points(self, self.loyalty_points) - + def validate_fixed_asset(self): for d in self.get("items"): if d.is_fixed_asset and d.meta.get_field("asset") and d.asset: diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json index 46fdc8fc10..01492807de 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.json @@ -8,11 +8,11 @@ "naming_series", "company", "purchase_receipts", - "sec_break1", - "taxes", "purchase_receipt_items", "get_items_from_purchase_receipts", "items", + "sec_break1", + "taxes", "section_break_9", "total_taxes_and_charges", "col_break1", @@ -123,7 +123,7 @@ ], "icon": "icon-usd", "is_submittable": 1, - "modified": "2019-10-09 13:39:36.082777", + "modified": "2019-11-21 15:34:10.846093", "modified_by": "Administrator", "module": "Stock", "name": "Landed Cost Voucher", From 49cd19d917b4f2f84a82bb4c510270449132aee9 Mon Sep 17 00:00:00 2001 From: Ben Knowles Date: Fri, 22 Nov 2019 00:06:02 -0600 Subject: [PATCH 175/210] fix: update syntax error in company.js (#19661) --- erpnext/setup/doctype/company/company.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 81c5f027a7..be736d2d9d 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -29,7 +29,7 @@ frappe.ui.form.on("Company", { company_name: function(frm) { if(frm.doc.__islocal) { - # add missing " " arg in split method + // add missing " " arg in split method let parts = frm.doc.company_name.split(" "); let abbr = $.map(parts, function (p) { return p? p.substr(0, 1) : null; From 24cde55e286d289b2315b96241c864f76602a2c2 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Fri, 22 Nov 2019 11:57:42 +0530 Subject: [PATCH 176/210] fix: Patch for updating price or product discount field (#19642) * fix: Patch for updating price or product discount field * fix: Update pactch * Update update_price_or_product_discount.py --- erpnext/patches.txt | 3 ++- erpnext/patches/v12_0/update_price_or_product_discount.py | 8 ++++++++ 2 files changed, 10 insertions(+), 1 deletion(-) create mode 100644 erpnext/patches/v12_0/update_price_or_product_discount.py diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 9e4dc12e65..07b646b0f8 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -645,4 +645,5 @@ erpnext.patches.v12_0.replace_accounting_with_accounts_in_home_settings erpnext.patches.v12_0.set_payment_entry_status erpnext.patches.v12_0.update_owner_fields_in_acc_dimension_custom_fields erpnext.patches.v12_0.set_default_for_add_taxes_from_item_tax_template -erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger \ No newline at end of file +erpnext.patches.v12_0.remove_denied_leaves_from_leave_ledger +erpnext.patches.v12_0.update_price_or_product_discount \ No newline at end of file diff --git a/erpnext/patches/v12_0/update_price_or_product_discount.py b/erpnext/patches/v12_0/update_price_or_product_discount.py new file mode 100644 index 0000000000..3a8cd43e30 --- /dev/null +++ b/erpnext/patches/v12_0/update_price_or_product_discount.py @@ -0,0 +1,8 @@ +from __future__ import unicode_literals +import frappe + +def execute(): + frappe.reload_doc("accounts", "doctype", "pricing_rule") + + frappe.db.sql(""" UPDATE `tabPricing Rule` SET price_or_product_discount = 'Price' + WHERE ifnull(price_or_product_discount,'') = '' """) From 290253fdd03d7e76df5ecd18fbe7c0f81e27af30 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 12:12:29 +0530 Subject: [PATCH 177/210] fix: last purchase rate greater than selling price (#19617) --- .../buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/buying/utils.py | 4 ++-- erpnext/stock/doctype/item/item.py | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 845ff747d6..f62df20ae1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -313,7 +313,7 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor= last_purchase_details = get_last_purchase_details(item_code, name) if last_purchase_details: - last_purchase_rate = (last_purchase_details['base_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate + last_purchase_rate = (last_purchase_details['base_net_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate return last_purchase_rate else: item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate") diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 8c0a1e56f7..b5598f8d0b 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -24,12 +24,12 @@ def update_last_purchase_rate(doc, is_submit): last_purchase_rate = None if last_purchase_details and \ (last_purchase_details.purchase_date > this_purchase_date): - last_purchase_rate = last_purchase_details['base_rate'] + last_purchase_rate = last_purchase_details['base_net_rate'] elif is_submit == 1: # even if this transaction is the latest one, it should be submitted # for it to be considered for latest purchase rate if flt(d.conversion_factor): - last_purchase_rate = flt(d.base_rate) / flt(d.conversion_factor) + last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor) # Check if item code is present # Conversion factor should not be mandatory for non itemized items elif d.item_code: diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 164c659fe8..7495dffec2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -645,7 +645,7 @@ class Item(WebsiteGenerator): json.dumps(item_wise_tax_detail), update_modified=False) def set_last_purchase_rate(self, new_name): - last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) + last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0) frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) def recalculate_bin_qty(self, new_name): @@ -942,7 +942,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): last_purchase_order = frappe.db.sql("""\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, - po_item.discount_percentage, po_item.base_rate + po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate from `tabPurchase Order` po, `tabPurchase Order Item` po_item where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and po.name = po_item.parent @@ -953,7 +953,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): last_purchase_receipt = frappe.db.sql("""\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, - pr_item.base_rate + pr_item.base_rate, pr_item.base_net_rate from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and pr.name = pr_item.parent @@ -984,6 +984,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): out = frappe._dict({ "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "base_net_rate": flt(last_purchase.net_rate) / conversion_factor, "discount_percentage": flt(last_purchase.discount_percentage), "purchase_date": purchase_date }) @@ -992,7 +993,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): out.update({ "price_list_rate": out.base_price_list_rate / conversion_rate, "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate + "base_rate": out.base_rate, + "base_net_rate": out.base_net_rate }) return out From fa4299931455bccdf096329eb1f95a8af77e83d3 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Tue, 19 Nov 2019 14:19:52 +0530 Subject: [PATCH 178/210] fix: last purchase rate greater than selling price --- .../buying/doctype/purchase_order/purchase_order.py | 2 +- erpnext/buying/utils.py | 4 ++-- erpnext/stock/doctype/item/item.py | 10 ++++++---- 3 files changed, 9 insertions(+), 7 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.py b/erpnext/buying/doctype/purchase_order/purchase_order.py index 845ff747d6..f62df20ae1 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/purchase_order.py @@ -313,7 +313,7 @@ def item_last_purchase_rate(name, conversion_rate, item_code, conversion_factor= last_purchase_details = get_last_purchase_details(item_code, name) if last_purchase_details: - last_purchase_rate = (last_purchase_details['base_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate + last_purchase_rate = (last_purchase_details['base_net_rate'] * (flt(conversion_factor) or 1.0)) / conversion_rate return last_purchase_rate else: item_last_purchase_rate = frappe.get_cached_value("Item", item_code, "last_purchase_rate") diff --git a/erpnext/buying/utils.py b/erpnext/buying/utils.py index 8c0a1e56f7..b5598f8d0b 100644 --- a/erpnext/buying/utils.py +++ b/erpnext/buying/utils.py @@ -24,12 +24,12 @@ def update_last_purchase_rate(doc, is_submit): last_purchase_rate = None if last_purchase_details and \ (last_purchase_details.purchase_date > this_purchase_date): - last_purchase_rate = last_purchase_details['base_rate'] + last_purchase_rate = last_purchase_details['base_net_rate'] elif is_submit == 1: # even if this transaction is the latest one, it should be submitted # for it to be considered for latest purchase rate if flt(d.conversion_factor): - last_purchase_rate = flt(d.base_rate) / flt(d.conversion_factor) + last_purchase_rate = flt(d.base_net_rate) / flt(d.conversion_factor) # Check if item code is present # Conversion factor should not be mandatory for non itemized items elif d.item_code: diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 164c659fe8..7495dffec2 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -645,7 +645,7 @@ class Item(WebsiteGenerator): json.dumps(item_wise_tax_detail), update_modified=False) def set_last_purchase_rate(self, new_name): - last_purchase_rate = get_last_purchase_details(new_name).get("base_rate", 0) + last_purchase_rate = get_last_purchase_details(new_name).get("base_net_rate", 0) frappe.db.set_value("Item", new_name, "last_purchase_rate", last_purchase_rate) def recalculate_bin_qty(self, new_name): @@ -942,7 +942,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): last_purchase_order = frappe.db.sql("""\ select po.name, po.transaction_date, po.conversion_rate, po_item.conversion_factor, po_item.base_price_list_rate, - po_item.discount_percentage, po_item.base_rate + po_item.discount_percentage, po_item.base_rate, po_item.base_net_rate from `tabPurchase Order` po, `tabPurchase Order Item` po_item where po.docstatus = 1 and po_item.item_code = %s and po.name != %s and po.name = po_item.parent @@ -953,7 +953,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): last_purchase_receipt = frappe.db.sql("""\ select pr.name, pr.posting_date, pr.posting_time, pr.conversion_rate, pr_item.conversion_factor, pr_item.base_price_list_rate, pr_item.discount_percentage, - pr_item.base_rate + pr_item.base_rate, pr_item.base_net_rate from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pr_item where pr.docstatus = 1 and pr_item.item_code = %s and pr.name != %s and pr.name = pr_item.parent @@ -984,6 +984,7 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): out = frappe._dict({ "base_price_list_rate": flt(last_purchase.base_price_list_rate) / conversion_factor, "base_rate": flt(last_purchase.base_rate) / conversion_factor, + "base_net_rate": flt(last_purchase.net_rate) / conversion_factor, "discount_percentage": flt(last_purchase.discount_percentage), "purchase_date": purchase_date }) @@ -992,7 +993,8 @@ def get_last_purchase_details(item_code, doc_name=None, conversion_rate=1.0): out.update({ "price_list_rate": out.base_price_list_rate / conversion_rate, "rate": out.base_rate / conversion_rate, - "base_rate": out.base_rate + "base_rate": out.base_rate, + "base_net_rate": out.base_net_rate }) return out From b10526dd8676f8c3c45d8e3ff84f279a16fb23e2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 22 Nov 2019 12:28:33 +0530 Subject: [PATCH 179/210] fix: consider taxes in the grand total (#19631) --- erpnext/accounts/doctype/payment_entry/payment_entry.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_entry/payment_entry.py b/erpnext/accounts/doctype/payment_entry/payment_entry.py index bf7e833285..9530fc9556 100644 --- a/erpnext/accounts/doctype/payment_entry/payment_entry.py +++ b/erpnext/accounts/doctype/payment_entry/payment_entry.py @@ -931,9 +931,9 @@ def get_payment_entry(dt, dn, party_amount=None, bank_account=None, bank_amount= grand_total = doc.rounded_total or doc.grand_total outstanding_amount = doc.outstanding_amount elif dt in ("Expense Claim"): - grand_total = doc.total_sanctioned_amount - outstanding_amount = doc.total_sanctioned_amount \ - - doc.total_amount_reimbursed - flt(doc.total_advance_amount) + grand_total = doc.total_sanctioned_amount + doc.total_taxes_and_charges + outstanding_amount = doc.grand_total \ + - doc.total_amount_reimbursed elif dt == "Employee Advance": grand_total = doc.advance_amount outstanding_amount = flt(doc.advance_amount) - flt(doc.paid_amount) From b3354198f129275d330b9619f4be9610b2875587 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 22 Nov 2019 12:38:43 +0530 Subject: [PATCH 180/210] Fix sql query in set_expired_status --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ba34dff745..ac2c2421e5 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -187,7 +187,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def set_expired_status(): frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' - WHERE status != 'Expired' AND 'valid_till < %s""" , (nowdate())) + WHERE status != 'Expired' AND 'valid_till' < %s""", (nowdate()) ) @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): From 6c368e2dfb8593fe630fd1b26476f33223fe7e3f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 22 Nov 2019 13:22:12 +0530 Subject: [PATCH 181/210] submit quotation in test --- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index a95fd52f0a..1713556754 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -217,7 +217,7 @@ class TestQuotation(unittest.TestCase): } ] yesterday = getdate(nowdate()) + datetime.timedelta(days=-1) - expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday,do_not_submit=True) + expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday) set_expired_status() self.assertEqual(expired_quotation.status,"Expired") From 763660b2e483731c7fb2ce4b738d22d975fb6ea0 Mon Sep 17 00:00:00 2001 From: Anurag Mishra <32095923+Anurag810@users.noreply.github.com> Date: Fri, 22 Nov 2019 13:32:25 +0530 Subject: [PATCH 182/210] fix: set allocated amount in employee advance as per total amount (#19626) --- .../hr/doctype/expense_claim/expense_claim.js | 20 ++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.js b/erpnext/hr/doctype/expense_claim/expense_claim.js index 6d3a28e5e2..0d37c10e9c 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.js +++ b/erpnext/hr/doctype/expense_claim/expense_claim.js @@ -208,6 +208,24 @@ frappe.ui.form.on("Expense Claim", { frm.refresh_fields(); }, + grand_total: function(frm) { + frm.trigger("update_employee_advance_claimed_amount"); + }, + + update_employee_advance_claimed_amount: function(frm) { + let amount_to_be_allocated = frm.doc.grand_total; + $.each(frm.doc.advances || [], function(i, advance){ + if (amount_to_be_allocated >= advance.unclaimed_amount){ + frm.doc.advances[i].allocated_amount = frm.doc.advances[i].unclaimed_amount; + amount_to_be_allocated -= advance.allocated_amount; + } else{ + frm.doc.advances[i].allocated_amount = amount_to_be_allocated; + amount_to_be_allocated = 0; + } + frm.refresh_field("advances"); + }); + }, + make_payment_entry: function(frm) { var method = "erpnext.accounts.doctype.payment_entry.payment_entry.get_payment_entry"; if(frm.doc.__onload && frm.doc.__onload.make_payment_via_journal_entry) { @@ -300,7 +318,7 @@ frappe.ui.form.on("Expense Claim", { row.advance_account = d.advance_account; row.advance_paid = d.paid_amount; row.unclaimed_amount = flt(d.paid_amount) - flt(d.claimed_amount); - row.allocated_amount = flt(d.paid_amount) - flt(d.claimed_amount); + row.allocated_amount = 0; }); refresh_field("advances"); } From 7ca472780ba01a1b1c6bcdad9cc64bfa151d2931 Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 22 Nov 2019 14:37:38 +0530 Subject: [PATCH 183/210] fix: Get Current Stock button not working in Purchase Receipt (#19645) - Field visible in grid view as well for better feedback --- .../purchase_receipt_item_supplied.json | 665 ++++-------------- 1 file changed, 148 insertions(+), 517 deletions(-) diff --git a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json index 2e0fc94bc9..6f2fbe5c37 100644 --- a/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json +++ b/erpnext/buying/doctype/purchase_receipt_item_supplied/purchase_receipt_item_supplied.json @@ -1,537 +1,168 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2013-02-22 01:27:42", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2013-02-22 01:27:42", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "main_item_code", + "rm_item_code", + "description", + "batch_no", + "serial_no", + "col_break1", + "required_qty", + "consumed_qty", + "stock_uom", + "rate", + "amount", + "conversion_factor", + "current_stock", + "reference_name", + "bom_detail_no" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "main_item_code", - "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": "Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "main_item_code", - "oldfieldtype": "Data", - "options": "Item", - "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 - }, + "fieldname": "main_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "oldfieldname": "main_item_code", + "oldfieldtype": "Data", + "options": "Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rm_item_code", - "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": "Raw Material Item Code", - "length": 0, - "no_copy": 0, - "oldfieldname": "rm_item_code", - "oldfieldtype": "Data", - "options": "Item", - "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 - }, + "fieldname": "rm_item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Raw Material Item Code", + "oldfieldname": "rm_item_code", + "oldfieldtype": "Data", + "options": "Item", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "description", - "fieldtype": "Text Editor", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 1, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Description", - "length": 0, - "no_copy": 0, - "oldfieldname": "description", - "oldfieldtype": "Data", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "print_width": "300px", - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, + "fieldname": "description", + "fieldtype": "Text Editor", + "in_global_search": 1, + "in_list_view": 1, + "label": "Description", + "oldfieldname": "description", + "oldfieldtype": "Data", + "print_width": "300px", + "read_only": 1, "width": "300px" - }, + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "batch_no", - "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": "Batch No", - "length": 0, - "no_copy": 1, - "options": "Batch", - "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 - }, + "fieldname": "batch_no", + "fieldtype": "Link", + "label": "Batch No", + "no_copy": 1, + "options": "Batch" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "serial_no", - "fieldtype": "Text", - "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": "Serial No", - "length": 0, - "no_copy": 1, - "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 - }, + "fieldname": "serial_no", + "fieldtype": "Text", + "label": "Serial No", + "no_copy": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "col_break1", - "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 - }, + "fieldname": "col_break1", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "required_qty", - "fieldtype": "Float", - "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": "Required Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "required_qty", - "oldfieldtype": "Currency", - "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 - }, + "fieldname": "required_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Required Qty", + "oldfieldname": "required_qty", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "consumed_qty", - "fieldtype": "Float", - "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": "Consumed Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "consumed_qty", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "consumed_qty", + "fieldtype": "Float", + "label": "Consumed Qty", + "oldfieldname": "consumed_qty", + "oldfieldtype": "Currency", + "read_only": 1, + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "stock_uom", - "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": "Stock Uom", - "length": 0, - "no_copy": 0, - "oldfieldname": "stock_uom", - "oldfieldtype": "Data", - "options": "UOM", - "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 - }, + "fieldname": "stock_uom", + "fieldtype": "Link", + "label": "Stock Uom", + "oldfieldname": "stock_uom", + "oldfieldtype": "Data", + "options": "UOM", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "rate", - "fieldtype": "Currency", - "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": "Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "rate", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "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 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "label": "Rate", + "oldfieldname": "rate", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "amount", - "fieldtype": "Currency", - "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": "Amount", - "length": 0, - "no_copy": 0, - "oldfieldname": "amount", - "oldfieldtype": "Currency", - "options": "Company:company:default_currency", - "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 - }, + "fieldname": "amount", + "fieldtype": "Currency", + "label": "Amount", + "oldfieldname": "amount", + "oldfieldtype": "Currency", + "options": "Company:company:default_currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "conversion_factor", - "fieldtype": "Float", - "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": "Conversion Factor", - "length": 0, - "no_copy": 0, - "oldfieldname": "conversion_factor", - "oldfieldtype": "Currency", - "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 - }, + "fieldname": "conversion_factor", + "fieldtype": "Float", + "hidden": 1, + "label": "Conversion Factor", + "oldfieldname": "conversion_factor", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "current_stock", - "fieldtype": "Float", - "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": "Current Stock", - "length": 0, - "no_copy": 0, - "oldfieldname": "current_stock", - "oldfieldtype": "Currency", - "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 - }, + "fieldname": "current_stock", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Current Stock", + "oldfieldname": "current_stock", + "oldfieldtype": "Currency", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reference_name", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Reference Name", - "length": 0, - "no_copy": 0, - "oldfieldname": "reference_name", - "oldfieldtype": "Data", - "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 - }, + "fieldname": "reference_name", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "Reference Name", + "oldfieldname": "reference_name", + "oldfieldtype": "Data", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "bom_detail_no", - "fieldtype": "Data", - "hidden": 1, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "BOM Detail No", - "length": 0, - "no_copy": 0, - "oldfieldname": "bom_detail_no", - "oldfieldtype": "Data", - "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 + "fieldname": "bom_detail_no", + "fieldtype": "Data", + "hidden": 1, + "in_list_view": 1, + "label": "BOM Detail No", + "oldfieldname": "bom_detail_no", + "oldfieldtype": "Data", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2019-01-07 16:51:59.536291", - "modified_by": "Administrator", - "module": "Buying", - "name": "Purchase Receipt Item Supplied", - "owner": "wasim@webnotestech.com", - "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, - "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 1, - "track_seen": 0, - "track_views": 0 + ], + "idx": 1, + "istable": 1, + "modified": "2019-11-21 16:25:29.909112", + "modified_by": "Administrator", + "module": "Buying", + "name": "Purchase Receipt Item Supplied", + "owner": "wasim@webnotestech.com", + "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file From b5c296da9edd486d095b35dee56747a4c5b0284b Mon Sep 17 00:00:00 2001 From: Marica Date: Fri, 22 Nov 2019 14:38:58 +0530 Subject: [PATCH 184/210] fix: Validation Error message on Prepared Report. (#19639) Give the user the reason why he has to use filters. --- erpnext/stock/report/stock_balance/stock_balance.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/report/stock_balance/stock_balance.py b/erpnext/stock/report/stock_balance/stock_balance.py index 68b8b502e5..a74253e872 100644 --- a/erpnext/stock/report/stock_balance/stock_balance.py +++ b/erpnext/stock/report/stock_balance/stock_balance.py @@ -292,7 +292,7 @@ def validate_filters(filters): if not (filters.get("item_code") or filters.get("warehouse")): sle_count = flt(frappe.db.sql("""select count(name) from `tabStock Ledger Entry`""")[0][0]) if sle_count > 500000: - frappe.throw(_("Please set filter based on Item or Warehouse")) + frappe.throw(_("Please set filter based on Item or Warehouse due to a large amount of entries.")) def get_variants_attributes(): '''Return all item variant attributes.''' From 1919af2ff19b74c80735914acb4d3d3b233e6796 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 16:32:34 +0530 Subject: [PATCH 185/210] Fixed Asset Refactor Review fixes (#19666) * fix: fixed asset item creation ux fixes * fix: auto creation of asset ux fixes * fix: [LCV] incorrect condition when checking assets linked with PR * fix: bulk update assets * refac: remove company level cwip enabling * cwip can be enabled only on category level * fix: #19649 --- .../purchase_invoice/purchase_invoice.py | 7 +- erpnext/accounts/general_ledger.py | 6 +- erpnext/assets/doctype/asset/asset.py | 18 +- erpnext/assets/doctype/asset/test_asset.py | 1004 ++++++++--------- .../doctype/asset_category/asset_category.py | 10 - .../asset_value_adjustment.json | 5 +- .../asset_value_adjustment.py | 9 +- erpnext/controllers/buying_controller.py | 13 +- .../set_cwip_and_delete_asset_settings.py | 18 +- erpnext/setup/doctype/company/company.json | 9 +- erpnext/stock/doctype/item/item.js | 9 +- .../landed_cost_voucher.py | 7 +- .../purchase_receipt/purchase_receipt.py | 14 +- 13 files changed, 562 insertions(+), 567 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 19d54a011a..75107b0b6d 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -237,7 +237,7 @@ class PurchaseInvoice(BuyingController): item.expense_account = warehouse_account[item.warehouse]["account"] else: item.expense_account = stock_not_billed_account - elif item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, asset_category): + elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) elif item.is_fixed_asset and item.pr_detail: @@ -408,7 +408,7 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if item.item_code and item.is_fixed_asset: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if is_cwip_accounting_enabled(self.company, asset_category): + if is_cwip_accounting_enabled(asset_category): return 1 return 0 @@ -504,8 +504,7 @@ class PurchaseInvoice(BuyingController): "credit": flt(item.rm_supp_cost) }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item)) - elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, - asset_category)): + elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 38f283c8d4..e9703dd790 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -175,11 +175,7 @@ def validate_account_for_perpetual_inventory(gl_map): StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync')) def validate_cwip_accounts(gl_map): - cwip_enabled = cint(frappe.get_cached_value("Company", - gl_map[0].company, "enable_cwip_accounting")) - - if not cwip_enabled: - cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) + cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) if cwip_enabled and gl_map[0].voucher_type == "Journal Entry": cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 9415eedc5c..d4185ea25e 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -31,8 +31,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company, - self.asset_category): + if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): self.make_gl_entries() def before_cancel(self): @@ -99,7 +98,7 @@ class Asset(AccountsController): if not flt(self.gross_purchase_amount): frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) - if is_cwip_accounting_enabled(self.company, self.asset_category): + if is_cwip_accounting_enabled(self.asset_category): if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}"). format(self.item_code)) @@ -295,7 +294,9 @@ class Asset(AccountsController): .format(row.idx)) if not row.depreciation_start_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + if not self.available_for_use_date: + frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + row.depreciation_start_date = self.available_for_use_date if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 @@ -514,7 +515,7 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.company, self.asset_category): + if not is_cwip_accounting_enabled(self.asset_category): return assets = frappe.db.sql_list(""" select name from `tabAsset` @@ -683,12 +684,7 @@ def make_asset_movement(assets): if asset_movement.get('assets'): return asset_movement.as_dict() -def is_cwip_accounting_enabled(company, asset_category=None): - enable_cwip_in_company = cint(frappe.db.get_value("Company", company, "enable_cwip_accounting")) - - if enable_cwip_in_company or not asset_category: - return enable_cwip_in_company - +def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 53fd6d394d..a56440de3d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -69,508 +69,508 @@ class TestAsset(unittest.TestCase): self.assertFalse(frappe.db.get_value("GL Entry", {"voucher_type": "Purchase Invoice", "voucher_no": pi.name})) - # def test_is_fixed_asset_set(self): - # asset = create_asset(is_existing_asset = 1) - # doc = frappe.new_doc('Purchase Invoice') - # doc.supplier = '_Test Supplier' - # doc.append('items', { - # 'item_code': 'Macbook Pro', - # 'qty': 1, - # 'asset': asset.name - # }) - - # doc.set_missing_values() - # self.assertEquals(doc.items[0].is_fixed_asset, 1) - - - # def test_schedule_for_straight_line_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save() - - # self.assertEqual(asset.status, "Draft") - # expected_schedules = [ - # ["2030-12-31", 30000.00, 30000.00], - # ["2031-12-31", 30000.00, 60000.00], - # ["2032-12-31", 30000.00, 90000.00] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_straight_line_method_for_existing_asset(self): - # create_asset(is_existing_asset=1) - # asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - # asset.calculate_depreciation = 1 - # asset.number_of_depreciations_booked = 1 - # asset.opening_accumulated_depreciation = 40000 - # asset.available_for_use_date = "2030-06-06" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - # asset.save() - # expected_schedules = [ - # ["2030-12-31", 14246.58, 54246.58], - # ["2031-12-31", 25000.00, 79246.58], - # ["2032-06-06", 10753.42, 90000.00] - # ] - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_double_declining_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Double Declining Balance", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": '2030-12-31' - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - # asset.save() - - # expected_schedules = [ - # ['2030-12-31', 66667.00, 66667.00], - # ['2031-12-31', 22222.11, 88889.11], - # ['2032-12-31', 1110.89, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_double_declining_method_for_existing_asset(self): - # create_asset(is_existing_asset = 1) - # asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - # asset.calculate_depreciation = 1 - # asset.is_existing_asset = 1 - # asset.number_of_depreciations_booked = 1 - # asset.opening_accumulated_depreciation = 50000 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2029-11-30' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Double Declining Balance", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - - # expected_schedules = [ - # ["2030-12-31", 33333.50, 83333.50], - # ["2031-12-31", 6666.50, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_prorated_straight_line_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.purchase_date = '2030-01-30' - # asset.is_existing_asset = 0 - # asset.available_for_use_date = "2030-01-30" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - - # asset.insert() - # asset.save() - - # expected_schedules = [ - # ["2030-12-31", 27534.25, 27534.25], - # ["2031-12-31", 30000.0, 57534.25], - # ["2032-12-31", 30000.0, 87534.25], - # ["2033-01-30", 2465.75, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_depreciation(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.purchase_date = '2020-01-30' - # asset.available_for_use_date = "2020-01-30" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # asset.load_from_db() - # self.assertEqual(asset.status, "Submitted") - - # frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") - # post_depreciation_entries(date="2021-01-01") - # asset.load_from_db() - - # # check depreciation entry series - # self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), - # ("_Test Depreciations - _TC", 30000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where against_voucher_type='Asset' and against_voucher = %s - # order by account""", asset.name) - - # self.assertEqual(gle, expected_gle) - # self.assertEqual(asset.get("value_after_depreciation"), 0) - - # def test_depreciation_entry_for_wdv_without_pro_rata(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=8000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 1000, - # "depreciation_method": "Written Down Value", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save(ignore_permissions=True) - - # self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) - - # expected_schedules = [ - # ["2030-12-31", 4000.00, 4000.00], - # ["2031-12-31", 2000.00, 6000.00], - # ["2032-12-31", 1000.00, 7000.0], - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_pro_rata_depreciation_entry_for_wdv(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=8000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-06-06' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 1000, - # "depreciation_method": "Written Down Value", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save(ignore_permissions=True) - - # self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) - - # expected_schedules = [ - # ["2030-12-31", 2279.45, 2279.45], - # ["2031-12-31", 2860.28, 5139.73], - # ["2032-12-31", 1430.14, 6569.87], - # ["2033-06-06", 430.13, 7000.0], - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_depreciation_entry_cancellation(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # post_depreciation_entries(date="2021-01-01") - - # asset.load_from_db() - - # # cancel depreciation entry - # depr_entry = asset.get("schedules")[0].journal_entry - # self.assertTrue(depr_entry) - # frappe.get_doc("Journal Entry", depr_entry).cancel() - - # asset.load_from_db() - # depr_entry = asset.get("schedules")[0].journal_entry - # self.assertFalse(depr_entry) - - # def test_scrap_asset(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = nowdate() - # asset.purchase_date = nowdate() - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": nowdate() - # }) - # asset.insert() - # asset.submit() - - # post_depreciation_entries(date=add_months(nowdate(), 10)) - - # scrap_asset(asset.name) - - # asset.load_from_db() - # self.assertEqual(asset.status, "Scrapped") - # self.assertTrue(asset.journal_entry_for_scrap) - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), - # ("_Test Fixed Asset - _TC", 0.0, 100000.0), - # ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Journal Entry' and voucher_no = %s - # order by account""", asset.journal_entry_for_scrap) - # self.assertEqual(gle, expected_gle) - - # restore_asset(asset.name) - - # asset.load_from_db() - # self.assertFalse(asset.journal_entry_for_scrap) - # self.assertEqual(asset.status, "Partially Depreciated") - - # def test_asset_sale(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # post_depreciation_entries(date="2021-01-01") - - # si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") - # si.customer = "_Test Customer" - # si.due_date = nowdate() - # si.get("items")[0].rate = 25000 - # si.insert() - # si.submit() - - # self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), - # ("_Test Fixed Asset - _TC", 0.0, 100000.0), - # ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), - # ("Debtors - _TC", 25000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Sales Invoice' and voucher_no = %s - # order by account""", si.name) - - # self.assertEqual(gle, expected_gle) - - # si.cancel() - # self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") - - # def test_asset_expected_value_after_useful_life(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-06-06" - # }) - # asset.insert() - # accumulated_depreciation_after_full_schedule = \ - # max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) - - # asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - - # flt(accumulated_depreciation_after_full_schedule)) - - # self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) - - # def test_cwip_accounting(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=5000, do_not_submit=True, location="Test Location") - - # pr.set('taxes', [{ - # 'category': 'Total', - # 'add_deduct_tax': 'Add', - # 'charge_type': 'On Net Total', - # 'account_head': '_Test Account Service Tax - _TC', - # 'description': '_Test Account Service Tax', - # 'cost_center': 'Main - _TC', - # 'rate': 5.0 - # }, { - # 'category': 'Valuation and Total', - # 'add_deduct_tax': 'Add', - # 'charge_type': 'On Net Total', - # 'account_head': '_Test Account Shipping Charges - _TC', - # 'description': '_Test Account Shipping Charges', - # 'cost_center': 'Main - _TC', - # 'rate': 5.0 - # }]) - - # pr.submit() - - # expected_gle = ( - # ("Asset Received But Not Billed - _TC", 0.0, 5250.0), - # ("CWIP Account - _TC", 5250.0, 0.0) - # ) - - # pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Purchase Receipt' and voucher_no = %s - # order by account""", pr.name) - - # self.assertEqual(pr_gle, expected_gle) - - # pi = make_invoice(pr.name) - # pi.submit() - - # expected_gle = ( - # ("_Test Account Service Tax - _TC", 250.0, 0.0), - # ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - # ("Asset Received But Not Billed - _TC", 5250.0, 0.0), - # ("Creditors - _TC", 0.0, 5500.0), - # ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), - # ) - - # pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Purchase Invoice' and voucher_no = %s - # order by account""", pi.name) - - # self.assertEqual(pi_gle, expected_gle) - - # asset = frappe.db.get_value('Asset', - # {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - - # asset_doc = frappe.get_doc('Asset', asset) - - # month_end_date = get_last_day(nowdate()) - # asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - # self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) - - # asset_doc.append("finance_books", { - # "expected_value_after_useful_life": 200, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": month_end_date - # }) - # asset_doc.submit() - - # expected_gle = ( - # ("_Test Fixed Asset - _TC", 5250.0, 0.0), - # ("CWIP Account - _TC", 0.0, 5250.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Asset' and voucher_no = %s - # order by account""", asset_doc.name) - - - # self.assertEqual(gle, expected_gle) - - # def test_expense_head(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=2, rate=200000.0, location="Test Location") - - # doc = make_invoice(pr.name) - - # self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + def test_is_fixed_asset_set(self): + asset = create_asset(is_existing_asset = 1) + doc = frappe.new_doc('Purchase Invoice') + doc.supplier = '_Test Supplier' + doc.append('items', { + 'item_code': 'Macbook Pro', + 'qty': 1, + 'asset': asset.name + }) + + doc.set_missing_values() + self.assertEquals(doc.items[0].is_fixed_asset, 1) + + + def test_schedule_for_straight_line_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save() + + self.assertEqual(asset.status, "Draft") + expected_schedules = [ + ["2030-12-31", 30000.00, 30000.00], + ["2031-12-31", 30000.00, 60000.00], + ["2032-12-31", 30000.00, 90000.00] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_straight_line_method_for_existing_asset(self): + create_asset(is_existing_asset=1) + asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) + asset.calculate_depreciation = 1 + asset.number_of_depreciations_booked = 1 + asset.opening_accumulated_depreciation = 40000 + asset.available_for_use_date = "2030-06-06" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + asset.save() + expected_schedules = [ + ["2030-12-31", 14246.58, 54246.58], + ["2031-12-31", 25000.00, 79246.58], + ["2032-06-06", 10753.42, 90000.00] + ] + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Double Declining Balance", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": '2030-12-31' + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + asset.save() + + expected_schedules = [ + ['2030-12-31', 66667.00, 66667.00], + ['2031-12-31', 22222.11, 88889.11], + ['2032-12-31', 1110.89, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method_for_existing_asset(self): + create_asset(is_existing_asset = 1) + asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) + asset.calculate_depreciation = 1 + asset.is_existing_asset = 1 + asset.number_of_depreciations_booked = 1 + asset.opening_accumulated_depreciation = 50000 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2029-11-30' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Double Declining Balance", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + + expected_schedules = [ + ["2030-12-31", 33333.50, 83333.50], + ["2031-12-31", 6666.50, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_prorated_straight_line_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.purchase_date = '2030-01-30' + asset.is_existing_asset = 0 + asset.available_for_use_date = "2030-01-30" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + + asset.insert() + asset.save() + + expected_schedules = [ + ["2030-12-31", 27534.25, 27534.25], + ["2031-12-31", 30000.0, 57534.25], + ["2032-12-31", 30000.0, 87534.25], + ["2033-01-30", 2465.75, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_depreciation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.purchase_date = '2020-01-30' + asset.available_for_use_date = "2020-01-30" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + asset.load_from_db() + self.assertEqual(asset.status, "Submitted") + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date="2021-01-01") + asset.load_from_db() + + # check depreciation entry series + self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), + ("_Test Depreciations - _TC", 30000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where against_voucher_type='Asset' and against_voucher = %s + order by account""", asset.name) + + self.assertEqual(gle, expected_gle) + self.assertEqual(asset.get("value_after_depreciation"), 0) + + def test_depreciation_entry_for_wdv_without_pro_rata(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=8000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 1000, + "depreciation_method": "Written Down Value", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save(ignore_permissions=True) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 4000.00, 4000.00], + ["2031-12-31", 2000.00, 6000.00], + ["2032-12-31", 1000.00, 7000.0], + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_pro_rata_depreciation_entry_for_wdv(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=8000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-06-06' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 1000, + "depreciation_method": "Written Down Value", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save(ignore_permissions=True) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 2279.45, 2279.45], + ["2031-12-31", 2860.28, 5139.73], + ["2032-12-31", 1430.14, 6569.87], + ["2033-06-06", 430.13, 7000.0], + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_depreciation_entry_cancellation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + post_depreciation_entries(date="2021-01-01") + + asset.load_from_db() + + # cancel depreciation entry + depr_entry = asset.get("schedules")[0].journal_entry + self.assertTrue(depr_entry) + frappe.get_doc("Journal Entry", depr_entry).cancel() + + asset.load_from_db() + depr_entry = asset.get("schedules")[0].journal_entry + self.assertFalse(depr_entry) + + def test_scrap_asset(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = nowdate() + asset.purchase_date = nowdate() + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": nowdate() + }) + asset.insert() + asset.submit() + + post_depreciation_entries(date=add_months(nowdate(), 10)) + + scrap_asset(asset.name) + + asset.load_from_db() + self.assertEqual(asset.status, "Scrapped") + self.assertTrue(asset.journal_entry_for_scrap) + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Journal Entry' and voucher_no = %s + order by account""", asset.journal_entry_for_scrap) + self.assertEqual(gle, expected_gle) + + restore_asset(asset.name) + + asset.load_from_db() + self.assertFalse(asset.journal_entry_for_scrap) + self.assertEqual(asset.status, "Partially Depreciated") + + def test_asset_sale(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + post_depreciation_entries(date="2021-01-01") + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), + ("Debtors - _TC", 25000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", si.name) + + self.assertEqual(gle, expected_gle) + + si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + + def test_asset_expected_value_after_useful_life(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-06-06" + }) + asset.insert() + accumulated_depreciation_after_full_schedule = \ + max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) + + asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - + flt(accumulated_depreciation_after_full_schedule)) + + self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) + + def test_cwip_accounting(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=5000, do_not_submit=True, location="Test Location") + + pr.set('taxes', [{ + 'category': 'Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Service Tax - _TC', + 'description': '_Test Account Service Tax', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }, { + 'category': 'Valuation and Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'description': '_Test Account Shipping Charges', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }]) + + pr.submit() + + expected_gle = ( + ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("CWIP Account - _TC", 5250.0, 0.0) + ) + + pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no = %s + order by account""", pr.name) + + self.assertEqual(pr_gle, expected_gle) + + pi = make_invoice(pr.name) + pi.submit() + + expected_gle = ( + ("_Test Account Service Tax - _TC", 250.0, 0.0), + ("_Test Account Shipping Charges - _TC", 250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Creditors - _TC", 0.0, 5500.0), + ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), + ) + + pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no = %s + order by account""", pi.name) + + self.assertEqual(pi_gle, expected_gle) + + asset = frappe.db.get_value('Asset', + {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + + asset_doc = frappe.get_doc('Asset', asset) + + month_end_date = get_last_day(nowdate()) + asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) + + asset_doc.append("finance_books", { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + asset_doc.submit() + + expected_gle = ( + ("_Test Fixed Asset - _TC", 5250.0, 0.0), + ("CWIP Account - _TC", 0.0, 5250.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Asset' and voucher_no = %s + order by account""", asset_doc.name) + + + self.assertEqual(gle, expected_gle) + + def test_expense_head(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=2, rate=200000.0, location="Test Location") + + doc = make_invoice(pr.name) + + self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 14f3922c05..2a42894623 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -11,7 +11,6 @@ from frappe.model.document import Document class AssetCategory(Document): def validate(self): self.validate_finance_books() - self.validate_enable_cwip_accounting() def validate_finance_books(self): for d in self.finance_books: @@ -19,15 +18,6 @@ class AssetCategory(Document): if cint(d.get(frappe.scrub(field)))<1: frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) - def validate_enable_cwip_accounting(self): - if self.enable_cwip_accounting : - for d in self.accounts: - cwip = frappe.db.get_value("Company",d.company_name,"enable_cwip_accounting") - if cwip: - frappe.throw(_ - ("CWIP is enabled globally in Company {1}. To enable it in Asset Category, first disable it in {1} ").format( - frappe.bold(d.idx), frappe.bold(d.company_name))) - @frappe.whitelist() def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): if item and frappe.db.get_value("Item", item, "is_fixed_asset"): diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json index a25b4ce82e..3236e726de 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json @@ -60,7 +60,8 @@ { "fieldname": "date", "fieldtype": "Date", - "label": "Date" + "label": "Date", + "reqd": 1 }, { "fieldname": "current_asset_value", @@ -110,7 +111,7 @@ } ], "is_submittable": 1, - "modified": "2019-05-26 09:46:23.613412", + "modified": "2019-11-22 14:09:25.800375", "modified_by": "Administrator", "module": "Assets", "name": "Asset Value Adjustment", diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 56425a0dcb..155597e856 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,12 +5,13 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, getdate, cint, date_diff +from frappe.utils import flt, getdate, cint, date_diff, formatdate from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from frappe.model.document import Document class AssetValueAdjustment(Document): def validate(self): + self.validate_date() self.set_difference_amount() self.set_current_asset_value() @@ -23,6 +24,12 @@ class AssetValueAdjustment(Document): frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) self.reschedule_depreciations(self.current_asset_value) + + def validate_date(self): + asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date') + if getdate(self.date) < getdate(asset_purchase_date): + frappe.throw(_("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.") + .format(formatdate(asset_purchase_date)), title="Incorrect Date") def set_difference_amount(self): self.difference_amount = flt(self.current_asset_value - self.new_asset_value) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index d0befcbcf3..3392850e96 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -577,6 +577,7 @@ class BuyingController(StockController): def auto_make_assets(self, asset_items): items_data = get_asset_item_details(asset_items) + messages = [] for d in self.items: if d.is_fixed_asset: @@ -589,12 +590,16 @@ class BuyingController(StockController): for qty in range(cint(d.qty)): self.make_asset(d) is_plural = 's' if cint(d.qty) != 1 else '' - frappe.msgprint(_('{0} Asset{2} Created for {1}').format(cint(d.qty), d.item_code, is_plural)) + messages.append(_('{0} Asset{2} Created for {1}').format(cint(d.qty), d.item_code, is_plural)) else: - frappe.throw(_("Asset Naming Series is mandatory for the auto creation for item {0}").format(d.item_code)) + frappe.throw(_("Row {1}: Asset Naming Series is mandatory for the auto creation for item {0}") + .format(d.item_code, d.idx)) else: - frappe.msgprint(_("Assets not created. You will have to create asset manually.")) - + messages.append(_("Assets not created for {0}. You will have to create asset manually.") + .format(d.item_code)) + + for message in messages: + frappe.msgprint(message, title="Success") def make_asset(self, row): if not row.asset_location: diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py index 5842e9edbf..4d4fc7c462 100644 --- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -7,15 +7,11 @@ def execute(): '''Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field in Company, delete Asset Settings ''' - if frappe.db.exists("DocType","Asset Settings"): - frappe.reload_doctype("Company") - cwip_value = frappe.db.get_single_value("Asset Settings","disable_cwip_accounting") + if frappe.db.exists("DocType", "Asset Settings"): + frappe.reload_doctype("Asset Category") + cwip_value = frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting") + + frappe.db.sql("""UPDATE `tabAsset Category` SET enable_cwip_accounting = %s""", cint(cwip_value)) - companies = [x['name'] for x in frappe.get_all("Company", "name")] - for company in companies: - enable_cwip_accounting = cint(not cint(cwip_value)) - frappe.db.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting) - - frappe.db.sql( - """ DELETE FROM `tabSingles` where doctype = 'Asset Settings' """) - frappe.delete_doc_if_exists("DocType","Asset Settings") \ No newline at end of file + frappe.db.sql("""DELETE FROM `tabSingles` where doctype = 'Asset Settings'""") + frappe.delete_doc_if_exists("DocType", "Asset Settings") \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 2d181b53ca..dd602eca10 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -72,7 +72,6 @@ "stock_received_but_not_billed", "expenses_included_in_valuation", "fixed_asset_depreciation_settings", - "enable_cwip_accounting", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", @@ -721,18 +720,12 @@ "fieldtype": "Link", "label": "Default Buying Terms", "options": "Terms and Conditions" - }, - { - "default": "0", - "fieldname": "enable_cwip_accounting", - "fieldtype": "Check", - "label": "Enable Capital Work in Progress Accounting" } ], "icon": "fa fa-building", "idx": 1, "image_field": "company_logo", - "modified": "2019-10-09 14:42:04.440974", + "modified": "2019-11-22 13:04:47.470768", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 2f4abbcea6..410d9f1b45 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -49,7 +49,7 @@ frappe.ui.form.on("Item", { if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } - + if (frm.doc.is_fixed_asset) { frm.trigger('is_fixed_asset'); frm.trigger('auto_create_assets'); @@ -140,6 +140,7 @@ frappe.ui.form.on("Item", { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); + frm.toggle_reqd(['asset_category'], frm.doc.is_fixed_asset); frm.toggle_display(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); frm.call({ @@ -150,6 +151,8 @@ frappe.ui.form.on("Item", { frm.trigger("set_asset_naming_series"); } }); + + frm.trigger('auto_create_assets'); }, set_asset_naming_series: function(frm) { @@ -159,8 +162,8 @@ frappe.ui.form.on("Item", { }, auto_create_assets: function(frm) { - frm.toggle_reqd(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets); - frm.toggle_display(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets); + frm.toggle_reqd(['asset_naming_series'], frm.doc.auto_create_assets); + frm.toggle_display(['asset_naming_series'], frm.doc.auto_create_assets); }, page_name: frappe.utils.warn_page_name_change, diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 173b394f79..7df40fb02c 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -138,8 +138,8 @@ class LandedCostVoucher(Document): if item.is_fixed_asset: receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \ else 'purchase_receipt' - docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document }, - fields=['name', 'docstatus']) + docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, + 'item_code': item.item_code }, fields=['name', 'docstatus']) if not docs or len(docs) != item.qty: frappe.throw(_('There are not enough asset created or linked to {0}. \ Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty)) @@ -148,8 +148,7 @@ class LandedCostVoucher(Document): if d.docstatus == 1: frappe.throw(_('{2} {0} has submitted Assets.\ Remove Item {1} from table to continue.').format( - item.receipt_document, item.item_code, item.receipt_document_type) - ) + item.receipt_document, item.item_code, item.receipt_document_type)) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0cb21d73f9..d0fae6a227 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -82,11 +82,21 @@ class PurchaseReceipt(BuyingController): self.validate_with_previous_doc() self.validate_uom_is_integer("uom", ["qty", "received_qty"]) self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_cwip_accounts() self.check_on_hold_or_closed_status() if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + + def validate_cwip_accounts(self): + for item in self.get('items'): + if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): + # check cwip accounts before making auto assets + # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account + arbnb_account = self.get_company_default("asset_received_but_not_billed") + cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + break def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc({ @@ -343,7 +353,7 @@ class PurchaseReceipt(BuyingController): def get_asset_gl_entry(self, gl_entries): for item in self.get("items"): if item.is_fixed_asset: - if is_cwip_accounting_enabled(self.company, item.asset_category): + if is_cwip_accounting_enabled(item.asset_category): self.add_asset_gl_entries(item, gl_entries) if flt(item.landed_cost_voucher_amount): self.add_lcv_gl_entries(item, gl_entries) @@ -386,7 +396,7 @@ class PurchaseReceipt(BuyingController): def add_lcv_gl_entries(self, item, gl_entries): expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") - if not is_cwip_accounting_enabled(self.company, item.asset_category): + if not is_cwip_accounting_enabled(item.asset_category): asset_account = get_asset_category_account(asset_category=item.asset_category, \ fieldname='fixed_asset_account', company=self.company) else: From f37a46edea1ef8ce9e0041241c9dc95b6130e124 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 16:32:50 +0530 Subject: [PATCH 186/210] Fixed Asset Refactor Review fixes (#19665) * fix: fixed asset item creation ux fixes * fix: auto creation of asset ux fixes * fix: [LCV] incorrect condition when checking assets linked with PR * fix: bulk update assets * refac: remove company level cwip enabling * cwip can be enabled only on category level * fix: #19649 --- .../purchase_invoice/purchase_invoice.py | 7 +- erpnext/accounts/general_ledger.py | 6 +- erpnext/assets/doctype/asset/asset.py | 18 +- erpnext/assets/doctype/asset/test_asset.py | 1004 ++++++++--------- .../doctype/asset_category/asset_category.py | 10 - .../asset_value_adjustment.json | 5 +- .../asset_value_adjustment.py | 9 +- erpnext/controllers/buying_controller.py | 13 +- .../set_cwip_and_delete_asset_settings.py | 18 +- erpnext/setup/doctype/company/company.json | 9 +- erpnext/stock/doctype/item/item.js | 9 +- .../landed_cost_voucher.py | 7 +- .../purchase_receipt/purchase_receipt.py | 14 +- 13 files changed, 562 insertions(+), 567 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index c0023560ff..3bb3df8dbd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -237,7 +237,7 @@ class PurchaseInvoice(BuyingController): item.expense_account = warehouse_account[item.warehouse]["account"] else: item.expense_account = stock_not_billed_account - elif item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, asset_category): + elif item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category): item.expense_account = get_asset_category_account('fixed_asset_account', item=item.item_code, company = self.company) elif item.is_fixed_asset and item.pr_detail: @@ -408,7 +408,7 @@ class PurchaseInvoice(BuyingController): for item in self.get("items"): if item.item_code and item.is_fixed_asset: asset_category = frappe.get_cached_value("Item", item.item_code, "asset_category") - if is_cwip_accounting_enabled(self.company, asset_category): + if is_cwip_accounting_enabled(asset_category): return 1 return 0 @@ -504,8 +504,7 @@ class PurchaseInvoice(BuyingController): "credit": flt(item.rm_supp_cost) }, warehouse_account[self.supplier_warehouse]["account_currency"], item=item)) - elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(self.company, - asset_category)): + elif not item.is_fixed_asset or (item.is_fixed_asset and not is_cwip_accounting_enabled(asset_category)): expense_account = (item.expense_account if (not item.enable_deferred_expense or self.is_return) else item.deferred_expense_account) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 38f283c8d4..e9703dd790 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -175,11 +175,7 @@ def validate_account_for_perpetual_inventory(gl_map): StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync')) def validate_cwip_accounts(gl_map): - cwip_enabled = cint(frappe.get_cached_value("Company", - gl_map[0].company, "enable_cwip_accounting")) - - if not cwip_enabled: - cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) + cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) if cwip_enabled and gl_map[0].voucher_type == "Journal Entry": cwip_accounts = [d[0] for d in frappe.db.sql("""select name from tabAccount diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 8b6bc40cf0..546f374094 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -31,8 +31,7 @@ class Asset(AccountsController): self.validate_in_use_date() self.set_status() self.make_asset_movement() - if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.company, - self.asset_category): + if not self.booked_fixed_asset and is_cwip_accounting_enabled(self.asset_category): self.make_gl_entries() def before_cancel(self): @@ -99,7 +98,7 @@ class Asset(AccountsController): if not flt(self.gross_purchase_amount): frappe.throw(_("Gross Purchase Amount is mandatory"), frappe.MandatoryError) - if is_cwip_accounting_enabled(self.company, self.asset_category): + if is_cwip_accounting_enabled(self.asset_category): if not self.is_existing_asset and not (self.purchase_receipt or self.purchase_invoice): frappe.throw(_("Please create purchase receipt or purchase invoice for the item {0}"). format(self.item_code)) @@ -295,7 +294,9 @@ class Asset(AccountsController): .format(row.idx)) if not row.depreciation_start_date: - frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + if not self.available_for_use_date: + frappe.throw(_("Row {0}: Depreciation Start Date is required").format(row.idx)) + row.depreciation_start_date = self.available_for_use_date if not self.is_existing_asset: self.opening_accumulated_depreciation = 0 @@ -514,7 +515,7 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.company, self.asset_category): + if not is_cwip_accounting_enabled(self.asset_category): return assets = frappe.db.sql_list(""" select name from `tabAsset` @@ -683,12 +684,7 @@ def make_asset_movement(assets, purpose=None): if asset_movement.get('assets'): return asset_movement.as_dict() -def is_cwip_accounting_enabled(company, asset_category=None): - enable_cwip_in_company = cint(frappe.db.get_value("Company", company, "enable_cwip_accounting")) - - if enable_cwip_in_company or not asset_category: - return enable_cwip_in_company - +def is_cwip_accounting_enabled(asset_category): return cint(frappe.db.get_value("Asset Category", asset_category, "enable_cwip_accounting")) def get_pro_rata_amt(row, depreciation_amount, from_date, to_date): diff --git a/erpnext/assets/doctype/asset/test_asset.py b/erpnext/assets/doctype/asset/test_asset.py index 53fd6d394d..a56440de3d 100644 --- a/erpnext/assets/doctype/asset/test_asset.py +++ b/erpnext/assets/doctype/asset/test_asset.py @@ -69,508 +69,508 @@ class TestAsset(unittest.TestCase): self.assertFalse(frappe.db.get_value("GL Entry", {"voucher_type": "Purchase Invoice", "voucher_no": pi.name})) - # def test_is_fixed_asset_set(self): - # asset = create_asset(is_existing_asset = 1) - # doc = frappe.new_doc('Purchase Invoice') - # doc.supplier = '_Test Supplier' - # doc.append('items', { - # 'item_code': 'Macbook Pro', - # 'qty': 1, - # 'asset': asset.name - # }) - - # doc.set_missing_values() - # self.assertEquals(doc.items[0].is_fixed_asset, 1) - - - # def test_schedule_for_straight_line_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save() - - # self.assertEqual(asset.status, "Draft") - # expected_schedules = [ - # ["2030-12-31", 30000.00, 30000.00], - # ["2031-12-31", 30000.00, 60000.00], - # ["2032-12-31", 30000.00, 90000.00] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_straight_line_method_for_existing_asset(self): - # create_asset(is_existing_asset=1) - # asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - # asset.calculate_depreciation = 1 - # asset.number_of_depreciations_booked = 1 - # asset.opening_accumulated_depreciation = 40000 - # asset.available_for_use_date = "2030-06-06" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - # asset.save() - # expected_schedules = [ - # ["2030-12-31", 14246.58, 54246.58], - # ["2031-12-31", 25000.00, 79246.58], - # ["2032-06-06", 10753.42, 90000.00] - # ] - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_double_declining_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Double Declining Balance", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": '2030-12-31' - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - # asset.save() - - # expected_schedules = [ - # ['2030-12-31', 66667.00, 66667.00], - # ['2031-12-31', 22222.11, 88889.11], - # ['2032-12-31', 1110.89, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_double_declining_method_for_existing_asset(self): - # create_asset(is_existing_asset = 1) - # asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) - # asset.calculate_depreciation = 1 - # asset.is_existing_asset = 1 - # asset.number_of_depreciations_booked = 1 - # asset.opening_accumulated_depreciation = 50000 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2029-11-30' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Double Declining Balance", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.insert() - # self.assertEqual(asset.status, "Draft") - - # expected_schedules = [ - # ["2030-12-31", 33333.50, 83333.50], - # ["2031-12-31", 6666.50, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_schedule_for_prorated_straight_line_method(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.purchase_date = '2030-01-30' - # asset.is_existing_asset = 0 - # asset.available_for_use_date = "2030-01-30" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - - # asset.insert() - # asset.save() - - # expected_schedules = [ - # ["2030-12-31", 27534.25, 27534.25], - # ["2031-12-31", 30000.0, 57534.25], - # ["2032-12-31", 30000.0, 87534.25], - # ["2033-01-30", 2465.75, 90000.0] - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_depreciation(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.purchase_date = '2020-01-30' - # asset.available_for_use_date = "2020-01-30" - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # asset.load_from_db() - # self.assertEqual(asset.status, "Submitted") - - # frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") - # post_depreciation_entries(date="2021-01-01") - # asset.load_from_db() - - # # check depreciation entry series - # self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), - # ("_Test Depreciations - _TC", 30000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where against_voucher_type='Asset' and against_voucher = %s - # order by account""", asset.name) - - # self.assertEqual(gle, expected_gle) - # self.assertEqual(asset.get("value_after_depreciation"), 0) - - # def test_depreciation_entry_for_wdv_without_pro_rata(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=8000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-01-01' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 1000, - # "depreciation_method": "Written Down Value", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save(ignore_permissions=True) - - # self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) - - # expected_schedules = [ - # ["2030-12-31", 4000.00, 4000.00], - # ["2031-12-31", 2000.00, 6000.00], - # ["2032-12-31", 1000.00, 7000.0], - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_pro_rata_depreciation_entry_for_wdv(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=8000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2030-06-06' - # asset.purchase_date = '2030-01-01' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 1000, - # "depreciation_method": "Written Down Value", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 12, - # "depreciation_start_date": "2030-12-31" - # }) - # asset.save(ignore_permissions=True) - - # self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) - - # expected_schedules = [ - # ["2030-12-31", 2279.45, 2279.45], - # ["2031-12-31", 2860.28, 5139.73], - # ["2032-12-31", 1430.14, 6569.87], - # ["2033-06-06", 430.13, 7000.0], - # ] - - # schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] - # for d in asset.get("schedules")] - - # self.assertEqual(schedules, expected_schedules) - - # def test_depreciation_entry_cancellation(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # post_depreciation_entries(date="2021-01-01") - - # asset.load_from_db() - - # # cancel depreciation entry - # depr_entry = asset.get("schedules")[0].journal_entry - # self.assertTrue(depr_entry) - # frappe.get_doc("Journal Entry", depr_entry).cancel() - - # asset.load_from_db() - # depr_entry = asset.get("schedules")[0].journal_entry - # self.assertFalse(depr_entry) - - # def test_scrap_asset(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = nowdate() - # asset.purchase_date = nowdate() - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": nowdate() - # }) - # asset.insert() - # asset.submit() - - # post_depreciation_entries(date=add_months(nowdate(), 10)) - - # scrap_asset(asset.name) - - # asset.load_from_db() - # self.assertEqual(asset.status, "Scrapped") - # self.assertTrue(asset.journal_entry_for_scrap) - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), - # ("_Test Fixed Asset - _TC", 0.0, 100000.0), - # ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Journal Entry' and voucher_no = %s - # order by account""", asset.journal_entry_for_scrap) - # self.assertEqual(gle, expected_gle) - - # restore_asset(asset.name) - - # asset.load_from_db() - # self.assertFalse(asset.journal_entry_for_scrap) - # self.assertEqual(asset.status, "Partially Depreciated") - - # def test_asset_sale(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-12-31" - # }) - # asset.insert() - # asset.submit() - # post_depreciation_entries(date="2021-01-01") - - # si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") - # si.customer = "_Test Customer" - # si.due_date = nowdate() - # si.get("items")[0].rate = 25000 - # si.insert() - # si.submit() - - # self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") - - # expected_gle = ( - # ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), - # ("_Test Fixed Asset - _TC", 0.0, 100000.0), - # ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), - # ("Debtors - _TC", 25000.0, 0.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Sales Invoice' and voucher_no = %s - # order by account""", si.name) - - # self.assertEqual(gle, expected_gle) - - # si.cancel() - # self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") - - # def test_asset_expected_value_after_useful_life(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=100000.0, location="Test Location") - - # asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') - # asset = frappe.get_doc('Asset', asset_name) - # asset.calculate_depreciation = 1 - # asset.available_for_use_date = '2020-06-06' - # asset.purchase_date = '2020-06-06' - # asset.append("finance_books", { - # "expected_value_after_useful_life": 10000, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": "2020-06-06" - # }) - # asset.insert() - # accumulated_depreciation_after_full_schedule = \ - # max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) - - # asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - - # flt(accumulated_depreciation_after_full_schedule)) - - # self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) - - # def test_cwip_accounting(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=1, rate=5000, do_not_submit=True, location="Test Location") - - # pr.set('taxes', [{ - # 'category': 'Total', - # 'add_deduct_tax': 'Add', - # 'charge_type': 'On Net Total', - # 'account_head': '_Test Account Service Tax - _TC', - # 'description': '_Test Account Service Tax', - # 'cost_center': 'Main - _TC', - # 'rate': 5.0 - # }, { - # 'category': 'Valuation and Total', - # 'add_deduct_tax': 'Add', - # 'charge_type': 'On Net Total', - # 'account_head': '_Test Account Shipping Charges - _TC', - # 'description': '_Test Account Shipping Charges', - # 'cost_center': 'Main - _TC', - # 'rate': 5.0 - # }]) - - # pr.submit() - - # expected_gle = ( - # ("Asset Received But Not Billed - _TC", 0.0, 5250.0), - # ("CWIP Account - _TC", 5250.0, 0.0) - # ) - - # pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Purchase Receipt' and voucher_no = %s - # order by account""", pr.name) - - # self.assertEqual(pr_gle, expected_gle) - - # pi = make_invoice(pr.name) - # pi.submit() - - # expected_gle = ( - # ("_Test Account Service Tax - _TC", 250.0, 0.0), - # ("_Test Account Shipping Charges - _TC", 250.0, 0.0), - # ("Asset Received But Not Billed - _TC", 5250.0, 0.0), - # ("Creditors - _TC", 0.0, 5500.0), - # ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), - # ) - - # pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Purchase Invoice' and voucher_no = %s - # order by account""", pi.name) - - # self.assertEqual(pi_gle, expected_gle) - - # asset = frappe.db.get_value('Asset', - # {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') - - # asset_doc = frappe.get_doc('Asset', asset) - - # month_end_date = get_last_day(nowdate()) - # asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) - # self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) - - # asset_doc.append("finance_books", { - # "expected_value_after_useful_life": 200, - # "depreciation_method": "Straight Line", - # "total_number_of_depreciations": 3, - # "frequency_of_depreciation": 10, - # "depreciation_start_date": month_end_date - # }) - # asset_doc.submit() - - # expected_gle = ( - # ("_Test Fixed Asset - _TC", 5250.0, 0.0), - # ("CWIP Account - _TC", 0.0, 5250.0) - # ) - - # gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` - # where voucher_type='Asset' and voucher_no = %s - # order by account""", asset_doc.name) - - - # self.assertEqual(gle, expected_gle) - - # def test_expense_head(self): - # pr = make_purchase_receipt(item_code="Macbook Pro", - # qty=2, rate=200000.0, location="Test Location") - - # doc = make_invoice(pr.name) - - # self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) + def test_is_fixed_asset_set(self): + asset = create_asset(is_existing_asset = 1) + doc = frappe.new_doc('Purchase Invoice') + doc.supplier = '_Test Supplier' + doc.append('items', { + 'item_code': 'Macbook Pro', + 'qty': 1, + 'asset': asset.name + }) + + doc.set_missing_values() + self.assertEquals(doc.items[0].is_fixed_asset, 1) + + + def test_schedule_for_straight_line_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save() + + self.assertEqual(asset.status, "Draft") + expected_schedules = [ + ["2030-12-31", 30000.00, 30000.00], + ["2031-12-31", 30000.00, 60000.00], + ["2032-12-31", 30000.00, 90000.00] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_straight_line_method_for_existing_asset(self): + create_asset(is_existing_asset=1) + asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) + asset.calculate_depreciation = 1 + asset.number_of_depreciations_booked = 1 + asset.opening_accumulated_depreciation = 40000 + asset.available_for_use_date = "2030-06-06" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + asset.save() + expected_schedules = [ + ["2030-12-31", 14246.58, 54246.58], + ["2031-12-31", 25000.00, 79246.58], + ["2032-06-06", 10753.42, 90000.00] + ] + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Double Declining Balance", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": '2030-12-31' + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + asset.save() + + expected_schedules = [ + ['2030-12-31', 66667.00, 66667.00], + ['2031-12-31', 22222.11, 88889.11], + ['2032-12-31', 1110.89, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_double_declining_method_for_existing_asset(self): + create_asset(is_existing_asset = 1) + asset = frappe.get_doc("Asset", {"asset_name": "Macbook Pro 1"}) + asset.calculate_depreciation = 1 + asset.is_existing_asset = 1 + asset.number_of_depreciations_booked = 1 + asset.opening_accumulated_depreciation = 50000 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2029-11-30' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Double Declining Balance", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.insert() + self.assertEqual(asset.status, "Draft") + + expected_schedules = [ + ["2030-12-31", 33333.50, 83333.50], + ["2031-12-31", 6666.50, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), d.depreciation_amount, d.accumulated_depreciation_amount] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_schedule_for_prorated_straight_line_method(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.purchase_date = '2030-01-30' + asset.is_existing_asset = 0 + asset.available_for_use_date = "2030-01-30" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + + asset.insert() + asset.save() + + expected_schedules = [ + ["2030-12-31", 27534.25, 27534.25], + ["2031-12-31", 30000.0, 57534.25], + ["2032-12-31", 30000.0, 87534.25], + ["2033-01-30", 2465.75, 90000.0] + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_depreciation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.purchase_date = '2020-01-30' + asset.available_for_use_date = "2020-01-30" + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + asset.load_from_db() + self.assertEqual(asset.status, "Submitted") + + frappe.db.set_value("Company", "_Test Company", "series_for_depreciation_entry", "DEPR-") + post_depreciation_entries(date="2021-01-01") + asset.load_from_db() + + # check depreciation entry series + self.assertEqual(asset.get("schedules")[0].journal_entry[:4], "DEPR") + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 0.0, 30000.0), + ("_Test Depreciations - _TC", 30000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where against_voucher_type='Asset' and against_voucher = %s + order by account""", asset.name) + + self.assertEqual(gle, expected_gle) + self.assertEqual(asset.get("value_after_depreciation"), 0) + + def test_depreciation_entry_for_wdv_without_pro_rata(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=8000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-01-01' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 1000, + "depreciation_method": "Written Down Value", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save(ignore_permissions=True) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 4000.00, 4000.00], + ["2031-12-31", 2000.00, 6000.00], + ["2032-12-31", 1000.00, 7000.0], + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_pro_rata_depreciation_entry_for_wdv(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=8000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2030-06-06' + asset.purchase_date = '2030-01-01' + asset.append("finance_books", { + "expected_value_after_useful_life": 1000, + "depreciation_method": "Written Down Value", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 12, + "depreciation_start_date": "2030-12-31" + }) + asset.save(ignore_permissions=True) + + self.assertEqual(asset.finance_books[0].rate_of_depreciation, 50.0) + + expected_schedules = [ + ["2030-12-31", 2279.45, 2279.45], + ["2031-12-31", 2860.28, 5139.73], + ["2032-12-31", 1430.14, 6569.87], + ["2033-06-06", 430.13, 7000.0], + ] + + schedules = [[cstr(d.schedule_date), flt(d.depreciation_amount, 2), flt(d.accumulated_depreciation_amount, 2)] + for d in asset.get("schedules")] + + self.assertEqual(schedules, expected_schedules) + + def test_depreciation_entry_cancellation(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + post_depreciation_entries(date="2021-01-01") + + asset.load_from_db() + + # cancel depreciation entry + depr_entry = asset.get("schedules")[0].journal_entry + self.assertTrue(depr_entry) + frappe.get_doc("Journal Entry", depr_entry).cancel() + + asset.load_from_db() + depr_entry = asset.get("schedules")[0].journal_entry + self.assertFalse(depr_entry) + + def test_scrap_asset(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = nowdate() + asset.purchase_date = nowdate() + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": nowdate() + }) + asset.insert() + asset.submit() + + post_depreciation_entries(date=add_months(nowdate(), 10)) + + scrap_asset(asset.name) + + asset.load_from_db() + self.assertEqual(asset.status, "Scrapped") + self.assertTrue(asset.journal_entry_for_scrap) + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 30000.0, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 70000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Journal Entry' and voucher_no = %s + order by account""", asset.journal_entry_for_scrap) + self.assertEqual(gle, expected_gle) + + restore_asset(asset.name) + + asset.load_from_db() + self.assertFalse(asset.journal_entry_for_scrap) + self.assertEqual(asset.status, "Partially Depreciated") + + def test_asset_sale(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-12-31" + }) + asset.insert() + asset.submit() + post_depreciation_entries(date="2021-01-01") + + si = make_sales_invoice(asset=asset.name, item_code="Macbook Pro", company="_Test Company") + si.customer = "_Test Customer" + si.due_date = nowdate() + si.get("items")[0].rate = 25000 + si.insert() + si.submit() + + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Sold") + + expected_gle = ( + ("_Test Accumulated Depreciations - _TC", 20392.16, 0.0), + ("_Test Fixed Asset - _TC", 0.0, 100000.0), + ("_Test Gain/Loss on Asset Disposal - _TC", 54607.84, 0.0), + ("Debtors - _TC", 25000.0, 0.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Sales Invoice' and voucher_no = %s + order by account""", si.name) + + self.assertEqual(gle, expected_gle) + + si.cancel() + self.assertEqual(frappe.db.get_value("Asset", asset.name, "status"), "Partially Depreciated") + + def test_asset_expected_value_after_useful_life(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=100000.0, location="Test Location") + + asset_name = frappe.db.get_value("Asset", {"purchase_receipt": pr.name}, 'name') + asset = frappe.get_doc('Asset', asset_name) + asset.calculate_depreciation = 1 + asset.available_for_use_date = '2020-06-06' + asset.purchase_date = '2020-06-06' + asset.append("finance_books", { + "expected_value_after_useful_life": 10000, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": "2020-06-06" + }) + asset.insert() + accumulated_depreciation_after_full_schedule = \ + max([d.accumulated_depreciation_amount for d in asset.get("schedules")]) + + asset_value_after_full_schedule = (flt(asset.gross_purchase_amount) - + flt(accumulated_depreciation_after_full_schedule)) + + self.assertTrue(asset.finance_books[0].expected_value_after_useful_life >= asset_value_after_full_schedule) + + def test_cwip_accounting(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=1, rate=5000, do_not_submit=True, location="Test Location") + + pr.set('taxes', [{ + 'category': 'Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Service Tax - _TC', + 'description': '_Test Account Service Tax', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }, { + 'category': 'Valuation and Total', + 'add_deduct_tax': 'Add', + 'charge_type': 'On Net Total', + 'account_head': '_Test Account Shipping Charges - _TC', + 'description': '_Test Account Shipping Charges', + 'cost_center': 'Main - _TC', + 'rate': 5.0 + }]) + + pr.submit() + + expected_gle = ( + ("Asset Received But Not Billed - _TC", 0.0, 5250.0), + ("CWIP Account - _TC", 5250.0, 0.0) + ) + + pr_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Receipt' and voucher_no = %s + order by account""", pr.name) + + self.assertEqual(pr_gle, expected_gle) + + pi = make_invoice(pr.name) + pi.submit() + + expected_gle = ( + ("_Test Account Service Tax - _TC", 250.0, 0.0), + ("_Test Account Shipping Charges - _TC", 250.0, 0.0), + ("Asset Received But Not Billed - _TC", 5250.0, 0.0), + ("Creditors - _TC", 0.0, 5500.0), + ("Expenses Included In Asset Valuation - _TC", 0.0, 250.0), + ) + + pi_gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Purchase Invoice' and voucher_no = %s + order by account""", pi.name) + + self.assertEqual(pi_gle, expected_gle) + + asset = frappe.db.get_value('Asset', + {'purchase_receipt': pr.name, 'docstatus': 0}, 'name') + + asset_doc = frappe.get_doc('Asset', asset) + + month_end_date = get_last_day(nowdate()) + asset_doc.available_for_use_date = nowdate() if nowdate() != month_end_date else add_days(nowdate(), -15) + self.assertEqual(asset_doc.gross_purchase_amount, 5250.0) + + asset_doc.append("finance_books", { + "expected_value_after_useful_life": 200, + "depreciation_method": "Straight Line", + "total_number_of_depreciations": 3, + "frequency_of_depreciation": 10, + "depreciation_start_date": month_end_date + }) + asset_doc.submit() + + expected_gle = ( + ("_Test Fixed Asset - _TC", 5250.0, 0.0), + ("CWIP Account - _TC", 0.0, 5250.0) + ) + + gle = frappe.db.sql("""select account, debit, credit from `tabGL Entry` + where voucher_type='Asset' and voucher_no = %s + order by account""", asset_doc.name) + + + self.assertEqual(gle, expected_gle) + + def test_expense_head(self): + pr = make_purchase_receipt(item_code="Macbook Pro", + qty=2, rate=200000.0, location="Test Location") + + doc = make_invoice(pr.name) + + self.assertEquals('Asset Received But Not Billed - _TC', doc.items[0].expense_account) def create_asset_data(): if not frappe.db.exists("Asset Category", "Computers"): diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 14f3922c05..2a42894623 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -11,7 +11,6 @@ from frappe.model.document import Document class AssetCategory(Document): def validate(self): self.validate_finance_books() - self.validate_enable_cwip_accounting() def validate_finance_books(self): for d in self.finance_books: @@ -19,15 +18,6 @@ class AssetCategory(Document): if cint(d.get(frappe.scrub(field)))<1: frappe.throw(_("Row {0}: {1} must be greater than 0").format(d.idx, field), frappe.MandatoryError) - def validate_enable_cwip_accounting(self): - if self.enable_cwip_accounting : - for d in self.accounts: - cwip = frappe.db.get_value("Company",d.company_name,"enable_cwip_accounting") - if cwip: - frappe.throw(_ - ("CWIP is enabled globally in Company {1}. To enable it in Asset Category, first disable it in {1} ").format( - frappe.bold(d.idx), frappe.bold(d.company_name))) - @frappe.whitelist() def get_asset_category_account(fieldname, item=None, asset=None, account=None, asset_category = None, company = None): if item and frappe.db.get_value("Item", item, "is_fixed_asset"): diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json index a25b4ce82e..3236e726de 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.json @@ -60,7 +60,8 @@ { "fieldname": "date", "fieldtype": "Date", - "label": "Date" + "label": "Date", + "reqd": 1 }, { "fieldname": "current_asset_value", @@ -110,7 +111,7 @@ } ], "is_submittable": 1, - "modified": "2019-05-26 09:46:23.613412", + "modified": "2019-11-22 14:09:25.800375", "modified_by": "Administrator", "module": "Assets", "name": "Asset Value Adjustment", diff --git a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py index 56425a0dcb..155597e856 100644 --- a/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py +++ b/erpnext/assets/doctype/asset_value_adjustment/asset_value_adjustment.py @@ -5,12 +5,13 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import flt, getdate, cint, date_diff +from frappe.utils import flt, getdate, cint, date_diff, formatdate from erpnext.assets.doctype.asset.depreciation import get_depreciation_accounts from frappe.model.document import Document class AssetValueAdjustment(Document): def validate(self): + self.validate_date() self.set_difference_amount() self.set_current_asset_value() @@ -23,6 +24,12 @@ class AssetValueAdjustment(Document): frappe.throw(_("Cancel the journal entry {0} first").format(self.journal_entry)) self.reschedule_depreciations(self.current_asset_value) + + def validate_date(self): + asset_purchase_date = frappe.db.get_value('Asset', self.asset, 'purchase_date') + if getdate(self.date) < getdate(asset_purchase_date): + frappe.throw(_("Asset Value Adjustment cannot be posted before Asset's purchase date {0}.") + .format(formatdate(asset_purchase_date)), title="Incorrect Date") def set_difference_amount(self): self.difference_amount = flt(self.current_asset_value - self.new_asset_value) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index d0befcbcf3..3392850e96 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -577,6 +577,7 @@ class BuyingController(StockController): def auto_make_assets(self, asset_items): items_data = get_asset_item_details(asset_items) + messages = [] for d in self.items: if d.is_fixed_asset: @@ -589,12 +590,16 @@ class BuyingController(StockController): for qty in range(cint(d.qty)): self.make_asset(d) is_plural = 's' if cint(d.qty) != 1 else '' - frappe.msgprint(_('{0} Asset{2} Created for {1}').format(cint(d.qty), d.item_code, is_plural)) + messages.append(_('{0} Asset{2} Created for {1}').format(cint(d.qty), d.item_code, is_plural)) else: - frappe.throw(_("Asset Naming Series is mandatory for the auto creation for item {0}").format(d.item_code)) + frappe.throw(_("Row {1}: Asset Naming Series is mandatory for the auto creation for item {0}") + .format(d.item_code, d.idx)) else: - frappe.msgprint(_("Assets not created. You will have to create asset manually.")) - + messages.append(_("Assets not created for {0}. You will have to create asset manually.") + .format(d.item_code)) + + for message in messages: + frappe.msgprint(message, title="Success") def make_asset(self, row): if not row.asset_location: diff --git a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py index 5842e9edbf..4d4fc7c462 100644 --- a/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py +++ b/erpnext/patches/v12_0/set_cwip_and_delete_asset_settings.py @@ -7,15 +7,11 @@ def execute(): '''Get 'Disable CWIP Accounting value' from Asset Settings, set it in 'Enable Capital Work in Progress Accounting' field in Company, delete Asset Settings ''' - if frappe.db.exists("DocType","Asset Settings"): - frappe.reload_doctype("Company") - cwip_value = frappe.db.get_single_value("Asset Settings","disable_cwip_accounting") + if frappe.db.exists("DocType", "Asset Settings"): + frappe.reload_doctype("Asset Category") + cwip_value = frappe.db.get_single_value("Asset Settings", "disable_cwip_accounting") + + frappe.db.sql("""UPDATE `tabAsset Category` SET enable_cwip_accounting = %s""", cint(cwip_value)) - companies = [x['name'] for x in frappe.get_all("Company", "name")] - for company in companies: - enable_cwip_accounting = cint(not cint(cwip_value)) - frappe.db.set_value("Company", company, "enable_cwip_accounting", enable_cwip_accounting) - - frappe.db.sql( - """ DELETE FROM `tabSingles` where doctype = 'Asset Settings' """) - frappe.delete_doc_if_exists("DocType","Asset Settings") \ No newline at end of file + frappe.db.sql("""DELETE FROM `tabSingles` where doctype = 'Asset Settings'""") + frappe.delete_doc_if_exists("DocType", "Asset Settings") \ No newline at end of file diff --git a/erpnext/setup/doctype/company/company.json b/erpnext/setup/doctype/company/company.json index 2d181b53ca..dd602eca10 100644 --- a/erpnext/setup/doctype/company/company.json +++ b/erpnext/setup/doctype/company/company.json @@ -72,7 +72,6 @@ "stock_received_but_not_billed", "expenses_included_in_valuation", "fixed_asset_depreciation_settings", - "enable_cwip_accounting", "accumulated_depreciation_account", "depreciation_expense_account", "series_for_depreciation_entry", @@ -721,18 +720,12 @@ "fieldtype": "Link", "label": "Default Buying Terms", "options": "Terms and Conditions" - }, - { - "default": "0", - "fieldname": "enable_cwip_accounting", - "fieldtype": "Check", - "label": "Enable Capital Work in Progress Accounting" } ], "icon": "fa fa-building", "idx": 1, "image_field": "company_logo", - "modified": "2019-10-09 14:42:04.440974", + "modified": "2019-11-22 13:04:47.470768", "modified_by": "Administrator", "module": "Setup", "name": "Company", diff --git a/erpnext/stock/doctype/item/item.js b/erpnext/stock/doctype/item/item.js index 2f4abbcea6..410d9f1b45 100644 --- a/erpnext/stock/doctype/item/item.js +++ b/erpnext/stock/doctype/item/item.js @@ -49,7 +49,7 @@ frappe.ui.form.on("Item", { if (!frm.doc.is_fixed_asset) { erpnext.item.make_dashboard(frm); } - + if (frm.doc.is_fixed_asset) { frm.trigger('is_fixed_asset'); frm.trigger('auto_create_assets'); @@ -140,6 +140,7 @@ frappe.ui.form.on("Item", { // set serial no to false & toggles its visibility frm.set_value('has_serial_no', 0); frm.toggle_enable(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); + frm.toggle_reqd(['asset_category'], frm.doc.is_fixed_asset); frm.toggle_display(['has_serial_no', 'serial_no_series'], !frm.doc.is_fixed_asset); frm.call({ @@ -150,6 +151,8 @@ frappe.ui.form.on("Item", { frm.trigger("set_asset_naming_series"); } }); + + frm.trigger('auto_create_assets'); }, set_asset_naming_series: function(frm) { @@ -159,8 +162,8 @@ frappe.ui.form.on("Item", { }, auto_create_assets: function(frm) { - frm.toggle_reqd(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets); - frm.toggle_display(['asset_category', 'asset_naming_series'], frm.doc.auto_create_assets); + frm.toggle_reqd(['asset_naming_series'], frm.doc.auto_create_assets); + frm.toggle_display(['asset_naming_series'], frm.doc.auto_create_assets); }, page_name: frappe.utils.warn_page_name_change, diff --git a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py index 173b394f79..7df40fb02c 100644 --- a/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py +++ b/erpnext/stock/doctype/landed_cost_voucher/landed_cost_voucher.py @@ -138,8 +138,8 @@ class LandedCostVoucher(Document): if item.is_fixed_asset: receipt_document_type = 'purchase_invoice' if item.receipt_document_type == 'Purchase Invoice' \ else 'purchase_receipt' - docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document }, - fields=['name', 'docstatus']) + docs = frappe.db.get_all('Asset', filters={ receipt_document_type: item.receipt_document, + 'item_code': item.item_code }, fields=['name', 'docstatus']) if not docs or len(docs) != item.qty: frappe.throw(_('There are not enough asset created or linked to {0}. \ Please create or link {1} Assets with respective document.').format(item.receipt_document, item.qty)) @@ -148,8 +148,7 @@ class LandedCostVoucher(Document): if d.docstatus == 1: frappe.throw(_('{2} {0} has submitted Assets.\ Remove Item {1} from table to continue.').format( - item.receipt_document, item.item_code, item.receipt_document_type) - ) + item.receipt_document, item.item_code, item.receipt_document_type)) def update_rate_in_serial_no_for_non_asset_items(self, receipt_document): for item in receipt_document.get("items"): diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 0cb21d73f9..d0fae6a227 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -82,11 +82,21 @@ class PurchaseReceipt(BuyingController): self.validate_with_previous_doc() self.validate_uom_is_integer("uom", ["qty", "received_qty"]) self.validate_uom_is_integer("stock_uom", "stock_qty") + self.validate_cwip_accounts() self.check_on_hold_or_closed_status() if getdate(self.posting_date) > getdate(nowdate()): throw(_("Posting Date cannot be future date")) + + def validate_cwip_accounts(self): + for item in self.get('items'): + if item.is_fixed_asset and is_cwip_accounting_enabled(item.asset_category): + # check cwip accounts before making auto assets + # Improves UX by not giving messages of "Assets Created" before throwing error of not finding arbnb account + arbnb_account = self.get_company_default("asset_received_but_not_billed") + cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + break def validate_with_previous_doc(self): super(PurchaseReceipt, self).validate_with_previous_doc({ @@ -343,7 +353,7 @@ class PurchaseReceipt(BuyingController): def get_asset_gl_entry(self, gl_entries): for item in self.get("items"): if item.is_fixed_asset: - if is_cwip_accounting_enabled(self.company, item.asset_category): + if is_cwip_accounting_enabled(item.asset_category): self.add_asset_gl_entries(item, gl_entries) if flt(item.landed_cost_voucher_amount): self.add_lcv_gl_entries(item, gl_entries) @@ -386,7 +396,7 @@ class PurchaseReceipt(BuyingController): def add_lcv_gl_entries(self, item, gl_entries): expenses_included_in_asset_valuation = self.get_company_default("expenses_included_in_asset_valuation") - if not is_cwip_accounting_enabled(self.company, item.asset_category): + if not is_cwip_accounting_enabled(item.asset_category): asset_account = get_asset_category_account(asset_category=item.asset_category, \ fieldname='fixed_asset_account', company=self.company) else: From c9203a1bee60b953f60f7511c4ca64c3bd0eddf1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Fri, 22 Nov 2019 16:35:15 +0530 Subject: [PATCH 187/210] fix: asset movement ux fixes (#19637) --- erpnext/assets/doctype/asset/asset.js | 117 +++++------------- erpnext/assets/doctype/asset/asset.py | 4 +- erpnext/assets/doctype/asset/asset_list.js | 1 + .../doctype/asset_movement/asset_movement.js | 2 +- .../asset_movement/asset_movement.json | 6 +- 5 files changed, 38 insertions(+), 92 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.js b/erpnext/assets/doctype/asset/asset.js index f0889bfa1b..6b3f2c777c 100644 --- a/erpnext/assets/doctype/asset/asset.js +++ b/erpnext/assets/doctype/asset/asset.js @@ -42,6 +42,24 @@ frappe.ui.form.on('Asset', { }, setup: function(frm) { + frm.make_methods = { + 'Asset Movement': () => { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, + args:{ + "assets": [{ name: cur_frm.doc.name }] + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, + } + frm.set_query("purchase_receipt", (doc) => { return { query: "erpnext.controllers.queries.get_purchase_receipts", @@ -487,92 +505,19 @@ erpnext.asset.restore_asset = function(frm) { }) }; -erpnext.asset.transfer_asset = function(frm) { - var dialog = new frappe.ui.Dialog({ - title: __("Transfer Asset"), - fields: [ - { - "label": __("Target Location"), - "fieldname": "target_location", - "fieldtype": "Link", - "options": "Location", - "get_query": function () { - return { - filters: [ - ["Location", "is_group", "=", 0] - ] - } - }, - "reqd": 1 - }, - { - "label": __("Select Serial No"), - "fieldname": "serial_nos", - "fieldtype": "Link", - "options": "Serial No", - "get_query": function () { - return { - filters: { - 'asset': frm.doc.name - } - } - }, - "onchange": function() { - let val = this.get_value(); - if (val) { - let serial_nos = dialog.get_value("serial_no") || val; - if (serial_nos) { - serial_nos = serial_nos.split('\n'); - serial_nos.push(val); - - const unique_sn = serial_nos.filter(function(elem, index, self) { - return index === self.indexOf(elem); - }); - - dialog.set_value("serial_no", unique_sn.join('\n')); - dialog.set_value("serial_nos", ""); - } - } - } - }, - { - "label": __("Serial No"), - "fieldname": "serial_no", - "read_only": 1, - "fieldtype": "Small Text" - }, - { - "label": __("Date"), - "fieldname": "transfer_date", - "fieldtype": "Datetime", - "reqd": 1, - "default": frappe.datetime.now_datetime() +erpnext.asset.transfer_asset = function() { + frappe.call({ + method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, + args:{ + "assets": [{ name: cur_frm.doc.name }], + "purpose": "Transfer" + }, + callback: function (r) { + if (r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); } - ] + } }); - - dialog.set_primary_action(__("Transfer"), function() { - var args = dialog.get_values(); - if(!args) return; - dialog.hide(); - return frappe.call({ - type: "GET", - method: "erpnext.assets.doctype.asset.asset.transfer_asset", - args: { - args: { - "asset": frm.doc.name, - "transaction_date": args.transfer_date, - "source_location": frm.doc.location, - "target_location": args.target_location, - "serial_no": args.serial_no, - "company": frm.doc.company - } - }, - freeze: true, - callback: function(r) { - cur_frm.reload_doc(); - } - }) - }); - dialog.show(); }; diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d4185ea25e..546f374094 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -647,7 +647,7 @@ def make_journal_entry(asset_name): return je @frappe.whitelist() -def make_asset_movement(assets): +def make_asset_movement(assets, purpose=None): import json from six import string_types @@ -658,7 +658,7 @@ def make_asset_movement(assets): frappe.throw(_('Atleast one asset has to be selected.')) asset_movement = frappe.new_doc("Asset Movement") - asset_movement.quantity = len(assets) + asset_movement.purpose = purpose prev_reference_docname = '' for asset in assets: diff --git a/erpnext/assets/doctype/asset/asset_list.js b/erpnext/assets/doctype/asset/asset_list.js index 46cde6ee81..02f39e0e7f 100644 --- a/erpnext/assets/doctype/asset/asset_list.js +++ b/erpnext/assets/doctype/asset/asset_list.js @@ -37,6 +37,7 @@ frappe.listview_settings['Asset'] = { const assets = me.get_checked_items(); frappe.call({ method: "erpnext.assets.doctype.asset.asset.make_asset_movement", + freeze: true, args:{ "assets": assets }, diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index a71212ea47..89977e2952 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -132,7 +132,7 @@ frappe.ui.form.on('Asset Movement Item', { if(asset_doc.location) frappe.model.set_value(cdt, cdn, 'source_location', asset_doc.location); if(asset_doc.custodian) frappe.model.set_value(cdt, cdn, 'from_employee', asset_doc.custodian); }).catch((err) => { - console.log(err); + console.log(err); // eslint-disable-line }); } } diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index 19af81d65b..e62d684411 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -54,7 +54,7 @@ { "fieldname": "reference_doctype", "fieldtype": "Link", - "label": "Reference DocType", + "label": "Reference Document", "no_copy": 1, "options": "DocType", "reqd": 1 @@ -62,7 +62,7 @@ { "fieldname": "reference_name", "fieldtype": "Dynamic Link", - "label": "Reference Name", + "label": "Reference Document Name", "no_copy": 1, "options": "reference_doctype", "reqd": 1 @@ -93,7 +93,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-13 15:37:48.870147", + "modified": "2019-11-21 14:35:51.880332", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", From eefc492ff48738515eb9753ec6e0a5cd8970b203 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 10:51:27 +0530 Subject: [PATCH 188/210] call commit after sql query for schedular job --- erpnext/selling/doctype/quotation/quotation.py | 3 ++- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 2 files changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index ac2c2421e5..66ad215dfa 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -187,7 +187,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def set_expired_status(): frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' - WHERE status != 'Expired' AND 'valid_till' < %s""", (nowdate()) ) + WHERE status != 'Expired' AND 'valid_till' < %s""", (nowdate())) + frappe.db.commit() @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 1713556754..2aefe3a0d3 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -216,7 +216,7 @@ class TestQuotation(unittest.TestCase): "rate": 500 } ] - yesterday = getdate(nowdate()) + datetime.timedelta(days=-1) + yesterday = getdate(nowdate()) - datetime.timedelta(days=1) expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday) set_expired_status() From a077795581a389c58dafbf36b082ba02df19fd0c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 12:01:00 +0530 Subject: [PATCH 189/210] fix tests --- erpnext/selling/doctype/quotation/quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 66ad215dfa..b63c2e1fef 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -187,7 +187,7 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): def set_expired_status(): frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' - WHERE status != 'Expired' AND 'valid_till' < %s""", (nowdate())) + WHERE 'valid_till' < %s""", (nowdate())) frappe.db.commit() @frappe.whitelist() From 04e3a506e4dccfccaeae4fbfbdc575bccc5e3458 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Mon, 25 Nov 2019 06:34:00 +0000 Subject: [PATCH 190/210] fix: Primary address not being fetched for customer (#19667) * fix: priamry address not being fetched * add doctype to filter for customer_primary_address * remove get_customer_primary_address_method --- erpnext/selling/doctype/customer/customer.js | 4 ++-- erpnext/selling/doctype/customer/customer.py | 12 ------------ 2 files changed, 2 insertions(+), 14 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index 458a56c9e7..cca8efeca4 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -49,9 +49,9 @@ frappe.ui.form.on("Customer", { }) frm.set_query('customer_primary_address', function(doc) { return { - query: "erpnext.selling.doctype.customer.customer.get_customer_primary_address", filters: { - 'customer': doc.name + 'link_doctype': 'Customer', + 'link_name': doc.name } } }) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index a8e3ce4eae..67e20b1e89 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -397,15 +397,3 @@ def get_customer_primary_contact(doctype, txt, searchfield, start, page_len, fil 'customer': customer, 'txt': '%%%s%%' % txt }) - -def get_customer_primary_address(doctype, txt, searchfield, start, page_len, filters): - customer = frappe.db.escape(filters.get('customer')) - return frappe.db.sql(""" - select `tabAddress`.name from `tabAddress`, `tabDynamic Link` - where `tabAddress`.name = `tabDynamic Link`.parent and `tabDynamic Link`.link_name = %(customer)s - and `tabDynamic Link`.link_doctype = 'Customer' - and `tabAddress`.name like %(txt)s - """, { - 'customer': customer, - 'txt': '%%%s%%' % txt - }) From cd3976f7d24a703c80858237e9f51569b9323bea Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 25 Nov 2019 12:24:34 +0530 Subject: [PATCH 191/210] Asset cancellation fix (#19671) * fix: remove asset movement mandatory fields * fix: label for reference doctype --- erpnext/assets/doctype/asset/asset.py | 25 +++------ .../doctype/asset_movement/asset_movement.js | 51 +++---------------- .../asset_movement/asset_movement.json | 15 +++--- .../doctype/asset_movement/asset_movement.py | 2 +- erpnext/controllers/buying_controller.py | 13 +++-- 5 files changed, 32 insertions(+), 74 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 546f374094..56341ed1b1 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -125,12 +125,14 @@ class Asset(AccountsController): frappe.throw(_("Available-for-use Date should be after purchase date")) def cancel_auto_gen_movement(self): - reference_docname = self.purchase_invoice or self.purchase_receipt - movement = frappe.db.get_all('Asset Movement', filters={ 'reference_name': reference_docname, 'docstatus': 1 }) - if len(movement) > 1: + movements = frappe.db.sql( + """SELECT asm.name, asm.docstatus + FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item + WHERE asm_item.parent=asm.name and asm_item.asset=%s and asm.docstatus=1""", self.name, as_dict=1) + if len(movements) > 1: frappe.throw(_('Asset has multiple Asset Movement Entries which has to be \ cancelled manually to cancel this asset.')) - movement = frappe.get_doc('Asset Movement', movement[0].get('name')) + movement = frappe.get_doc('Asset Movement', movements[0].get('name')) movement.flags.ignore_validate = True movement.cancel() @@ -658,23 +660,10 @@ def make_asset_movement(assets, purpose=None): frappe.throw(_('Atleast one asset has to be selected.')) asset_movement = frappe.new_doc("Asset Movement") - asset_movement.purpose = purpose - prev_reference_docname = '' - + asset_movement.quantity = len(assets) for asset in assets: asset = frappe.get_doc('Asset', asset.get('name')) - # get PR/PI linked with asset - reference_docname = asset.get('purchase_receipt') if asset.get('purchase_receipt') \ - else asset.get('purchase_invoice') - # checks if all the assets are linked with a single PR/PI - if prev_reference_docname == '': - prev_reference_docname = reference_docname - elif prev_reference_docname != reference_docname: - frappe.throw(_('Assets selected should belong to same reference document.')) - asset_movement.company = asset.get('company') - asset_movement.reference_doctype = 'Purchase Receipt' if asset.get('purchase_receipt') else 'Purchase Invoice' - asset_movement.reference_name = prev_reference_docname asset_movement.append("assets", { 'asset': asset.get('name'), 'source_location': asset.get('location'), diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.js b/erpnext/assets/doctype/asset_movement/asset_movement.js index 89977e2952..06d8879091 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.js +++ b/erpnext/assets/doctype/asset_movement/asset_movement.js @@ -31,6 +31,13 @@ frappe.ui.form.on('Asset Movement', { name: ["in", ["Purchase Receipt", "Purchase Invoice"]] } }; + }), + frm.set_query("asset", "assets", () => { + return { + filters: { + status: ["not in", ["Draft"]] + } + } }) }, @@ -76,50 +83,6 @@ frappe.ui.form.on('Asset Movement', { }); }); frm.refresh_field('assets'); - }, - - reference_name: function(frm) { - if (frm.doc.reference_name && frm.doc.reference_doctype) { - const reference_doctype = frm.doc.reference_doctype === 'Purchase Invoice' ? 'purchase_invoice' : 'purchase_receipt'; - // On selection of reference name, - // sets query to display assets linked to that reference doc - frm.set_query('asset', 'assets', function() { - return { - filters: { - [reference_doctype] : frm.doc.reference_name - } - }; - }); - - // fetches linked asset & adds to the assets table - frappe.db.get_list('Asset', { - fields: ['name', 'location', 'custodian'], - filters: { - [reference_doctype] : frm.doc.reference_name - } - }).then((docs) => { - if (docs.length == 0) { - frappe.msgprint(frappe._(`Please select ${frm.doc.reference_doctype} which has assets.`)); - frm.doc.reference_name = ''; - frm.refresh_field('reference_name'); - return; - } - frm.doc.assets = []; - docs.forEach(doc => { - frm.add_child('assets', { - asset: doc.name, - source_location: doc.location, - from_employee: doc.custodian - }); - frm.refresh_field('assets'); - }) - }).catch((err) => { - console.log(err); // eslint-disable-line - }); - } else { - // if reference is deleted then remove query - frm.set_query('asset', 'assets', () => ({ filters: {} })); - } } }); diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.json b/erpnext/assets/doctype/asset_movement/asset_movement.json index e62d684411..3472ab5d7d 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.json +++ b/erpnext/assets/doctype/asset_movement/asset_movement.json @@ -9,12 +9,12 @@ "purpose", "column_break_4", "transaction_date", + "section_break_10", + "assets", "reference", "reference_doctype", "column_break_9", "reference_name", - "section_break_10", - "assets", "amended_from" ], "fields": [ @@ -47,6 +47,7 @@ "fieldtype": "Column Break" }, { + "collapsible": 1, "fieldname": "reference", "fieldtype": "Section Break", "label": "Reference" @@ -54,18 +55,16 @@ { "fieldname": "reference_doctype", "fieldtype": "Link", - "label": "Reference Document", + "label": "Reference Document Type", "no_copy": 1, - "options": "DocType", - "reqd": 1 + "options": "DocType" }, { "fieldname": "reference_name", "fieldtype": "Dynamic Link", "label": "Reference Document Name", "no_copy": 1, - "options": "reference_doctype", - "reqd": 1 + "options": "reference_doctype" }, { "fieldname": "amended_from", @@ -93,7 +92,7 @@ } ], "is_submittable": 1, - "modified": "2019-11-21 14:35:51.880332", + "modified": "2019-11-23 13:28:47.256935", "modified_by": "Administrator", "module": "Assets", "name": "Asset Movement", diff --git a/erpnext/assets/doctype/asset_movement/asset_movement.py b/erpnext/assets/doctype/asset_movement/asset_movement.py index 714845dfac..4e1822b2ce 100644 --- a/erpnext/assets/doctype/asset_movement/asset_movement.py +++ b/erpnext/assets/doctype/asset_movement/asset_movement.py @@ -22,7 +22,7 @@ class AssetMovement(Document): if company != self.company: frappe.throw(_("Asset {0} does not belong to company {1}").format(d.asset, self.company)) - if not(d.source_location or d.target_location or d.from_employee or d.to_employee): + if not (d.source_location or d.target_location or d.from_employee or d.to_employee): frappe.throw(_("Either location or employee must be required")) def validate_location(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3392850e96..d12643af82 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -641,7 +641,10 @@ class BuyingController(StockController): asset = frappe.get_doc('Asset', asset.name) if delete_asset and is_auto_create_enabled: # need to delete movements to delete assets otherwise throws link exists error - movements = frappe.db.get_all('Asset Movement', filters={ 'reference_name': self.name }) + movements = frappe.db.sql( + """SELECT asm.name + FROM `tabAsset Movement` asm, `tabAsset Movement Item` asm_item + WHERE asm_item.parent=asm.name and asm_item.asset=%s""", asset.name, as_dict=1) for movement in movements: frappe.delete_doc('Asset Movement', movement.name, force=1) frappe.delete_doc("Asset", asset.name, force=1) @@ -652,8 +655,12 @@ class BuyingController(StockController): asset.purchase_date = self.posting_date asset.supplier = self.supplier elif self.docstatus == 2: - asset.set(field, None) - asset.supplier = None + if asset.docstatus == 0: + asset.set(field, None) + asset.supplier = None + if asset.docstatus == 1 and delete_asset: + frappe.throw(_('Cannot cancel this document as it is linked with submitted asset {0}.\ + Please cancel the it to continue.').format(asset.name)) asset.flags.ignore_validate_update_after_submit = True asset.flags.ignore_mandatory = True From 1ed1c4e6a432214444744774ad555c8446160396 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 12:33:40 +0530 Subject: [PATCH 192/210] fix: make journal entry to sync stock and account balance --- erpnext/accounts/general_ledger.py | 7 +++++-- erpnext/public/js/controllers/accounts.js | 16 ---------------- erpnext/public/js/utils.js | 16 ++++++++++++++++ 3 files changed, 21 insertions(+), 18 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 4e9ef0b410..3a241476df 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,7 +163,10 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: - diff = flt(stock_bal - account_bal) + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) + + diff = flt(stock_bal - account_bal, precision) error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( stock_bal, account_bal, frappe.bold(account)) error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) @@ -182,7 +185,7 @@ def validate_account_for_perpetual_inventory(gl_map): raise_exception=StockValueAndAccountBalanceOutOfSync, title=_('Values Out Of Sync'), primary_action={ - 'label': 'Make JV', + 'label': 'Make Journal Entry', 'client_action': 'erpnext.route_to_adjustment_jv', 'args': journal_entry_args }) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index eb99192b88..f4eaad58da 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -355,20 +355,4 @@ cur_frm.pformat.taxes= function(doc){ out += '
'; } return out; -} - -erpnext.route_to_adjustment_jv = (args) => { - frappe.model.with_doctype('Journal Entry', () => { - // route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch - let journal_entry = frappe.model.get_new_doc('Journal Entry'); - - args.accounts.forEach((je_account) => { - let child_row = frappe.model.add_child(journal_entry, "accounts"); - child_row.account = je_account.account; - child_row.debit_in_account_currency = je_account.debit_in_account_currency; - child_row.credit_in_account_currency = je_account.credit_in_account_currency; - child_row.party_type = "" ; - }); - frappe.set_route('Form','Journal Entry', journal_entry.name); - }); } \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 6f43d9ef8c..d5a78d4f1f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -74,6 +74,22 @@ $.extend(erpnext, { ); }); }, + + route_to_adjustment_jv: (args) => { + frappe.model.with_doctype('Journal Entry', () => { + // route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch + let journal_entry = frappe.model.get_new_doc('Journal Entry'); + + args.accounts.forEach((je_account) => { + let child_row = frappe.model.add_child(journal_entry, "accounts"); + child_row.account = je_account.account; + child_row.debit_in_account_currency = je_account.debit_in_account_currency; + child_row.credit_in_account_currency = je_account.credit_in_account_currency; + child_row.party_type = "" ; + }); + frappe.set_route('Form','Journal Entry', journal_entry.name); + }); + } }); From a9ff7df2e6b81a5f824c566c4649db2d43517ba8 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 12:55:27 +0530 Subject: [PATCH 193/210] add sql query to set valid_till --- erpnext/selling/doctype/quotation/test_quotation.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 2aefe3a0d3..aab5fd783a 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -217,7 +217,13 @@ class TestQuotation(unittest.TestCase): } ] yesterday = getdate(nowdate()) - datetime.timedelta(days=1) - expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday) + expired_quotation = make_quotation(item_list=quotation_item) + # Manually set valid till date to bypass validation + frappe.db.sql(""" + UPDATE tabQuotation + SET valid_till = %s + WHERE name = %s + """,(yesterday,expired_quotation.name)) set_expired_status() self.assertEqual(expired_quotation.status,"Expired") From 35e8d1e1d7381bcbd92a50f2798c0fdb9dae4fc4 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 14:02:51 +0530 Subject: [PATCH 194/210] Stock acc bal sync msg (#19676) * fix: Prefilled JV via Account Balance and Stock Value mismatch error message - Make JV button will route to Journal Entry and add rows in child table * fix: make journal entry to sync stock and account balance * fix: translated action label --- erpnext/accounts/general_ledger.py | 34 +++++++++++++++++------ erpnext/public/js/controllers/accounts.js | 4 +-- erpnext/public/js/utils.js | 16 +++++++++++ 3 files changed, 43 insertions(+), 11 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index e9703dd790..2ba319d05e 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,16 +163,32 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: - error_reason = _("Account Balance ({0}) and Stock Value ({1}) is out of sync for account {2} and it's linked warehouses.").format( - account_bal, stock_bal, frappe.bold(account)) - error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(stock_bal - account_bal)) - button_text = _("Make Adjustment Entry") + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), + currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) - frappe.throw("""{0}

{1}

-
- -
""".format(error_reason, error_resolution, button_text), - StockValueAndAccountBalanceOutOfSync, title=_('Account Balance Out Of Sync')) + diff = flt(stock_bal - account_bal, precision) + error_reason = _("Stock Value ({0}) and Account Balance ({1}) are out of sync for account {2} and it's linked warehouses.").format( + stock_bal, account_bal, frappe.bold(account)) + error_resolution = _("Please create adjustment Journal Entry for amount {0} ").format(frappe.bold(diff)) + stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") + + db_or_cr_warehouse_account =('credit_in_account_currency' if diff < 0 else 'debit_in_account_currency') + db_or_cr_stock_adjustment_account = ('debit_in_account_currency' if diff < 0 else 'credit_in_account_currency') + + journal_entry_args = { + 'accounts':[ + {'account': account, db_or_cr_warehouse_account : abs(diff)}, + {'account': stock_adjustment_account, db_or_cr_stock_adjustment_account : abs(diff) }] + } + + frappe.msgprint(msg="""{0}

{1}

""".format(error_reason, error_resolution), + raise_exception=StockValueAndAccountBalanceOutOfSync, + title=_('Values Out Of Sync'), + primary_action={ + 'label': _('Make Journal Entry'), + 'client_action': 'erpnext.route_to_adjustment_jv', + 'args': journal_entry_args + }) def validate_cwip_accounts(gl_map): cwip_enabled = any([cint(ac.enable_cwip_accounting) for ac in frappe.db.get_all("Asset Category","enable_cwip_accounting")]) diff --git a/erpnext/public/js/controllers/accounts.js b/erpnext/public/js/controllers/accounts.js index 3dfc8911fc..f4eaad58da 100644 --- a/erpnext/public/js/controllers/accounts.js +++ b/erpnext/public/js/controllers/accounts.js @@ -64,7 +64,7 @@ frappe.ui.form.on(cur_frm.doctype, { } }) } - } + } }); frappe.ui.form.on('Sales Invoice Payment', { @@ -355,4 +355,4 @@ cur_frm.pformat.taxes= function(doc){ out += '
'; } return out; -} +} \ No newline at end of file diff --git a/erpnext/public/js/utils.js b/erpnext/public/js/utils.js index 6f43d9ef8c..d5a78d4f1f 100755 --- a/erpnext/public/js/utils.js +++ b/erpnext/public/js/utils.js @@ -74,6 +74,22 @@ $.extend(erpnext, { ); }); }, + + route_to_adjustment_jv: (args) => { + frappe.model.with_doctype('Journal Entry', () => { + // route to adjustment Journal Entry to handle Account Balance and Stock Value mismatch + let journal_entry = frappe.model.get_new_doc('Journal Entry'); + + args.accounts.forEach((je_account) => { + let child_row = frappe.model.add_child(journal_entry, "accounts"); + child_row.account = je_account.account; + child_row.debit_in_account_currency = je_account.debit_in_account_currency; + child_row.credit_in_account_currency = je_account.credit_in_account_currency; + child_row.party_type = "" ; + }); + frappe.set_route('Form','Journal Entry', journal_entry.name); + }); + } }); From f2752bf38c20e872b044fbe93d97f09f9fdbed00 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:09:49 +0530 Subject: [PATCH 195/210] fix: tests for python2 --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- .../appointment_booking_settings.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 72c2ae5ee7..50c98c59de 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -23,7 +23,7 @@ def create_test_lead(): def create_test_appointments(): test_appointment = frappe.db.exists( - {'doctype': 'Appointment', 'email': 'test@example.com'}) + {'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({ diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index 82acd93f90..eff8b982c9 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -19,7 +19,7 @@ class AppointmentBookingSettings(Document): def save(self): self.number_of_agents = len(self.agent_list) - super().save() + super(AppointmentBookingSettings,self).save() def validate_availability_of_slots(self): for record in self.availability_of_slots: From 3ec5eabaf64b97f41ba8ebf98e52784c2038f215 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:11:31 +0530 Subject: [PATCH 196/210] formatting --- .../appointment_booking_settings.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py index eff8b982c9..27f14b1dbd 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.py @@ -19,7 +19,7 @@ class AppointmentBookingSettings(Document): def save(self): self.number_of_agents = len(self.agent_list) - super(AppointmentBookingSettings,self).save() + super(AppointmentBookingSettings, self).save() def validate_availability_of_slots(self): for record in self.availability_of_slots: From e0c9f3c282d535b726cdf21d3a04ec1e6fd22a33 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:14:51 +0530 Subject: [PATCH 197/210] fix valid date --- erpnext/selling/doctype/quotation/test_quotation.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index aab5fd783a..7739e3e623 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -219,11 +219,8 @@ class TestQuotation(unittest.TestCase): yesterday = getdate(nowdate()) - datetime.timedelta(days=1) expired_quotation = make_quotation(item_list=quotation_item) # Manually set valid till date to bypass validation - frappe.db.sql(""" - UPDATE tabQuotation - SET valid_till = %s - WHERE name = %s - """,(yesterday,expired_quotation.name)) + expired_quotation.valid_till = yesterday + expired_quotation.save() set_expired_status() self.assertEqual(expired_quotation.status,"Expired") From 754c43f6c3afb918f4fa60b971994a31ef97f782 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:27:43 +0530 Subject: [PATCH 198/210] fix set_expired_status method --- erpnext/selling/doctype/quotation/quotation.py | 4 ++-- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index b63c2e1fef..2ce01aa4af 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -186,8 +186,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist def set_expired_status(): - frappe.db.sql("""UPDATE `tabQuotation` SET status = 'Expired' - WHERE 'valid_till' < %s""", (nowdate())) + frappe.db.sql("""UPDATE `tabQuotation` SET `status` = 'Expired' + WHERE `status` != "Expired" AND `valid_till` < %s""", (nowdate())) frappe.db.commit() @frappe.whitelist() diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 7739e3e623..b450c29a87 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -218,9 +218,9 @@ class TestQuotation(unittest.TestCase): ] yesterday = getdate(nowdate()) - datetime.timedelta(days=1) expired_quotation = make_quotation(item_list=quotation_item) - # Manually set valid till date to bypass validation expired_quotation.valid_till = yesterday expired_quotation.save() + # Call schedular method set_expired_status() self.assertEqual(expired_quotation.status,"Expired") From c856cb85d9e443f00ee306428ab69e4b8aa88049 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 15:08:24 +0530 Subject: [PATCH 199/210] log: Change log for v12.2.0 --- erpnext/change_log/v12/v12_2_0.md | 14 ++++++++++++++ 1 file changed, 14 insertions(+) create mode 100644 erpnext/change_log/v12/v12_2_0.md diff --git a/erpnext/change_log/v12/v12_2_0.md b/erpnext/change_log/v12/v12_2_0.md new file mode 100644 index 0000000000..0ec0eeca3d --- /dev/null +++ b/erpnext/change_log/v12/v12_2_0.md @@ -0,0 +1,14 @@ +# Version 12.2.0 Release Notes + +### Accounting + +1. Fixed Asset + - "Enable CWIP" options moved to Asset Category from Asset Settings + - Removed Asset link from Purchase Receipt Item table + - Enhanced Asset master + - Asset Movement now handles movement of multiple assets + - Introduced monthly depreciation +2. GL Entries for Landed Cost Voucher now posted directly against individual Charges account +3. Optimization of BOM Update Tool +4. Syncing of Stock and Account balance is enforced, in case of perpetual inventory +5. Rendered email template in Email Campaign From 4d12f8acab13add3bff3bdf30f8d640d60e16dfc Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 15:31:23 +0550 Subject: [PATCH 200/210] bumped to version 12.2.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index d031bc5bb1..f40b957563 100644 --- a/erpnext/__init__.py +++ b/erpnext/__init__.py @@ -5,7 +5,7 @@ import frappe from erpnext.hooks import regional_overrides from frappe.utils import getdate -__version__ = '12.1.8' +__version__ = '12.2.0' def get_default_company(user=None): '''Get default company for user''' From 032baeac5b89794bef874828ac8cab5d0dd0edda Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 15:15:00 +0530 Subject: [PATCH 201/210] don't submit quotation --- erpnext/selling/doctype/quotation/test_quotation.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index b450c29a87..003fd66579 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -217,12 +217,11 @@ class TestQuotation(unittest.TestCase): } ] yesterday = getdate(nowdate()) - datetime.timedelta(days=1) - expired_quotation = make_quotation(item_list=quotation_item) + expired_quotation = make_quotation(item_list=quotation_item,do_not_submit=True) expired_quotation.valid_till = yesterday expired_quotation.save() - # Call schedular method + expired_quotation.submit() set_expired_status() - self.assertEqual(expired_quotation.status,"Expired") From cf3a2f657942abb5dae80725b6660a069ed5706c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 16:26:04 +0530 Subject: [PATCH 202/210] set transaction date to yesterday --- erpnext/selling/doctype/quotation/test_quotation.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 003fd66579..cef8f513b0 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -217,7 +217,7 @@ class TestQuotation(unittest.TestCase): } ] yesterday = getdate(nowdate()) - datetime.timedelta(days=1) - expired_quotation = make_quotation(item_list=quotation_item,do_not_submit=True) + expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday,do_not_submit=True) expired_quotation.valid_till = yesterday expired_quotation.save() expired_quotation.submit() From f9dec5201fe9fe789dd26ca91498f2b92414967c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 16:42:07 +0530 Subject: [PATCH 203/210] fix:tests --- erpnext/crm/doctype/appointment/appointment.py | 2 +- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 91d1c03f7d..b6962d923a 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -28,7 +28,7 @@ class Appointment(Document): 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(number_of_appointments_in_same_slot >= number_of_agents): + if (number_of_appointments_in_same_slot >= number_of_agents): frappe.throw('Time slot is not available') # Link lead if not self.lead: diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 50c98c59de..0dac2bb9ae 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -24,7 +24,7 @@ def create_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: + if test_appointment[0][0]: return frappe.get_doc('Appointment', test_appointment[0][0]) test_appointment = frappe.get_doc({ 'doctype': 'Appointment', From d63ad3bb5f9917940ad8c0855feae92e22438afe Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Mon, 25 Nov 2019 11:20:47 +0000 Subject: [PATCH 204/210] fix: add email group and newsletter links to CRM module view (#19679) * fix: add email group and newsletter links to CRM module view * chore: move email group to bottom --- erpnext/config/crm.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/erpnext/config/crm.py b/erpnext/config/crm.py index eba6c7a02a..05017845b2 100644 --- a/erpnext/config/crm.py +++ b/erpnext/config/crm.py @@ -46,6 +46,11 @@ def get_data(): "name": "Contract", "description": _("Helps you keep tracks of Contracts based on Supplier, Customer and Employee"), }, + { + "type": "doctype", + "name": "Newsletter", + "label": _("Newsletter"), + } ] }, { @@ -165,6 +170,11 @@ def get_data(): "type": "doctype", "name": "SMS Settings", "description": _("Setup SMS gateway settings") + }, + { + "type": "doctype", + "label": _("Email Group"), + "name": "Email Group", } ] }, From 565d3efcdfe51a968e2fcbffe8d8e148ee444c23 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 17:11:12 +0530 Subject: [PATCH 205/210] fetch updated document in test_quotation --- erpnext/selling/doctype/quotation/test_quotation.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index cef8f513b0..95b5634695 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -222,6 +222,7 @@ class TestQuotation(unittest.TestCase): expired_quotation.save() expired_quotation.submit() set_expired_status() + expired_quotation = frappe.get_doc("Quotation",expired_quotation.name) self.assertEqual(expired_quotation.status,"Expired") From b84e56ebb55794ce749a362e360e4d338879141f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 17:32:02 +0530 Subject: [PATCH 206/210] fix:travis tests --- erpnext/crm/doctype/appointment/test_appointment.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/crm/doctype/appointment/test_appointment.py b/erpnext/crm/doctype/appointment/test_appointment.py index 0dac2bb9ae..50c98c59de 100644 --- a/erpnext/crm/doctype/appointment/test_appointment.py +++ b/erpnext/crm/doctype/appointment/test_appointment.py @@ -24,7 +24,7 @@ def create_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[0][0]: + if test_appointment: return frappe.get_doc('Appointment', test_appointment[0][0]) test_appointment = frappe.get_doc({ 'doctype': 'Appointment', From 9326fb78f296628fd8f05b64455630056b020f8b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 Nov 2019 16:35:47 +0530 Subject: [PATCH 207/210] fix: BOM UX --- erpnext/manufacturing/doctype/bom/bom.js | 57 +- erpnext/manufacturing/doctype/bom/bom.json | 74 +- erpnext/manufacturing/doctype/bom/bom.py | 21 +- .../doctype/bom/bom_dashboard.py | 8 +- .../doctype/bom_item/bom_item.json | 910 ++---------------- .../production_plan/production_plan.py | 1 - .../doctype/work_order/work_order.py | 16 + erpnext/patches/v11_0/rename_bom_wo_fields.py | 7 - .../quality_inspection/quality_inspection.py | 35 + .../stock/doctype/stock_entry/stock_entry.js | 7 +- .../stock/doctype/stock_entry/stock_entry.py | 16 + 11 files changed, 234 insertions(+), 918 deletions(-) diff --git a/erpnext/manufacturing/doctype/bom/bom.js b/erpnext/manufacturing/doctype/bom/bom.js index b9591d6054..8283fd7e6f 100644 --- a/erpnext/manufacturing/doctype/bom/bom.js +++ b/erpnext/manufacturing/doctype/bom/bom.js @@ -5,6 +5,12 @@ frappe.provide("erpnext.bom"); frappe.ui.form.on("BOM", { setup: function(frm) { + frm.custom_make_buttons = { + 'BOM': 'Duplicate BOM', + 'Work Order': 'Work Order', + 'Quality Inspection': 'Quality Inspection' + }; + frm.set_query("bom_no", "items", function() { return { filters: { @@ -85,9 +91,21 @@ frappe.ui.form.on("BOM", { } if(frm.doc.docstatus!=0) { - frm.add_custom_button(__("Duplicate"), function() { + frm.add_custom_button(__("Duplicate BOM"), function() { frm.copy_doc(); - }); + }, __("Create")); + + frm.add_custom_button(__("Work Order"), function() { + frm.trigger("make_work_order"); + }, __("Create")); + + if (frm.doc.inspection_required) { + frm.add_custom_button(__("Quality Inspection"), function() { + frm.trigger("make_quality_inspection"); + }, __("Create")); + } + + frm.page.set_inner_btn_group_as_primary(__('Create')); } if(frm.doc.items && frm.doc.allow_alternative_item) { @@ -109,6 +127,41 @@ frappe.ui.form.on("BOM", { } }, + make_work_order: function(frm) { + const fields = [{ + fieldtype: 'Float', + label: __('Qty To Manufacture'), + fieldname: 'qty', + reqd: 1, + default: 1 + }]; + + frappe.prompt(fields, data => { + frappe.call({ + method: "erpnext.manufacturing.doctype.work_order.work_order.make_work_order", + args: { + item: frm.doc.item, + qty: data.qty || 0.0, + project: frm.doc.project + }, + freeze: true, + callback: function(r) { + if(r.message) { + var doc = frappe.model.sync(r.message)[0]; + frappe.set_route("Form", doc.doctype, doc.name); + } + } + }); + }, __("Enter Value"), __("Create")); + }, + + make_quality_inspection: function(frm) { + frappe.model.open_mapped_doc({ + method: "erpnext.stock.doctype.quality_inspection.quality_inspection.make_quality_inspection", + frm: frm + }) + }, + update_cost: function(frm) { return frappe.call({ doc: frm.doc, diff --git a/erpnext/manufacturing/doctype/bom/bom.json b/erpnext/manufacturing/doctype/bom/bom.json index a0faeb5fb5..63f4f977c5 100644 --- a/erpnext/manufacturing/doctype/bom/bom.json +++ b/erpnext/manufacturing/doctype/bom/bom.json @@ -3,33 +3,36 @@ "creation": "2013-01-22 15:11:38", "doctype": "DocType", "document_type": "Setup", + "engine": "InnoDB", "field_order": [ "item", - "item_name", - "image", - "uom", "quantity", + "set_rate_of_sub_assembly_item_based_on_bom", "cb0", "is_active", "is_default", - "with_operations", - "inspection_required", "allow_alternative_item", - "allow_same_item_multiple_times", - "set_rate_of_sub_assembly_item_based_on_bom", - "quality_inspection_template", + "image", + "item_name", + "uom", "currency_detail", "company", - "transfer_material_against", + "project", "conversion_rate", "column_break_12", "currency", "rm_cost_as_per", "buying_price_list", - "operations_section", + "section_break_21", + "with_operations", + "column_break_23", + "transfer_material_against", "routing", + "operations_section", "operations", "materials_section", + "inspection_required", + "quality_inspection_template", "items", "scrap_section", "scrap_items", @@ -41,14 +44,9 @@ "base_operating_cost", "base_raw_material_cost", "base_scrap_material_cost", - "total_cost_of_bom", - "total_cost", "column_break_26", + "total_cost", "base_total_cost", - "more_info_section", - "project", - "amended_from", - "col_break23", "section_break_25", "description", "column_break_27", @@ -57,12 +55,14 @@ "website_section", "show_in_website", "route", + "column_break_52", "website_image", "thumbnail", "sb_web_spec", - "web_long_description", "show_items", - "show_operations" + "show_operations", + "web_long_description", + "amended_from" ], "fields": [ { @@ -152,7 +152,7 @@ "default": "0", "fieldname": "inspection_required", "fieldtype": "Check", - "label": "Inspection Required" + "label": "Quality Inspection Required" }, { "default": "0", @@ -160,12 +160,6 @@ "fieldtype": "Check", "label": "Allow Alternative Item" }, - { - "default": "0", - "fieldname": "allow_same_item_multiple_times", - "fieldtype": "Check", - "label": "Allow Same Item Multiple Times" - }, { "allow_on_submit": 1, "default": "1", @@ -193,6 +187,7 @@ "reqd": 1 }, { + "default": "Work Order", "fieldname": "transfer_material_against", "fieldtype": "Select", "label": "Transfer Material Against", @@ -235,10 +230,10 @@ { "fieldname": "operations_section", "fieldtype": "Section Break", - "label": "Operations", "oldfieldtype": "Section Break" }, { + "depends_on": "with_operations", "fieldname": "routing", "fieldtype": "Link", "label": "Routing", @@ -335,10 +330,6 @@ "options": "Company:company:default_currency", "read_only": 1 }, - { - "fieldname": "total_cost_of_bom", - "fieldtype": "Section Break" - }, { "fieldname": "total_cost", "fieldtype": "Currency", @@ -359,10 +350,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "more_info_section", - "fieldtype": "Section Break" - }, { "fieldname": "project", "fieldtype": "Link", @@ -381,10 +368,6 @@ "print_hide": 1, "read_only": 1 }, - { - "fieldname": "col_break23", - "fieldtype": "Column Break" - }, { "fieldname": "section_break_25", "fieldtype": "Section Break" @@ -481,13 +464,26 @@ "fieldname": "show_operations", "fieldtype": "Check", "label": "Show Operations" + }, + { + "fieldname": "section_break_21", + "fieldtype": "Section Break", + "label": "Operations" + }, + { + "fieldname": "column_break_23", + "fieldtype": "Column Break" + }, + { + "fieldname": "column_break_52", + "fieldtype": "Column Break" } ], "icon": "fa fa-sitemap", "idx": 1, "image_field": "image", "is_submittable": 1, - "modified": "2019-07-30 17:00:09.665068", + "modified": "2019-11-22 14:35:12.142150", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM", diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index db79d7feda..5579954498 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -96,6 +96,7 @@ class BOM(WebsiteGenerator): def get_routing(self): if self.routing: + self.set("operations", []) for d in frappe.get_all("BOM Operation", fields = ["*"], filters = {'parenttype': 'Routing', 'parent': self.routing}): child = self.append('operations', d) @@ -289,7 +290,7 @@ class BOM(WebsiteGenerator): if not valuation_rate: valuation_rate = frappe.db.get_value("Item", args['item_code'], "valuation_rate") - return valuation_rate + return flt(valuation_rate) def manage_default_bom(self): """ Uncheck others if current one is selected as default or @@ -362,15 +363,9 @@ class BOM(WebsiteGenerator): def validate_materials(self): """ Validate raw material entries """ - def get_duplicates(lst): - seen = set() - seen_add = seen.add - for item in lst: - if item.item_code in seen or seen_add(item.item_code): - yield item - if not self.get('items'): frappe.throw(_("Raw Materials cannot be blank.")) + check_list = [] for m in self.get('items'): if m.bom_no: @@ -379,16 +374,6 @@ class BOM(WebsiteGenerator): frappe.throw(_("Quantity required for Item {0} in row {1}").format(m.item_code, m.idx)) check_list.append(m) - if not self.allow_same_item_multiple_times: - duplicate_items = list(get_duplicates(check_list)) - if duplicate_items: - li = [] - for i in duplicate_items: - li.append("{0} on row {1}".format(i.item_code, i.idx)) - duplicate_list = '
' + '
'.join(li) - - frappe.throw(_("Same item has been entered multiple times. {0}").format(duplicate_list)) - def check_recursion(self, bom_list=[]): """ Check whether recursion occurs in any bom""" bom_list = self.traverse_tree() diff --git a/erpnext/manufacturing/doctype/bom/bom_dashboard.py b/erpnext/manufacturing/doctype/bom/bom_dashboard.py index 803ece7c78..060cd53ef1 100644 --- a/erpnext/manufacturing/doctype/bom/bom_dashboard.py +++ b/erpnext/manufacturing/doctype/bom/bom_dashboard.py @@ -17,11 +17,13 @@ def get_data(): }, { 'label': _('Manufacture'), - 'items': ['BOM', 'Work Order', 'Job Card', 'Production Plan'] + 'items': ['BOM', 'Work Order', 'Job Card'] }, { - 'label': _('Purchase'), + 'label': _('Subcontract'), 'items': ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] } - ] + ], + 'disable_create_buttons': ["Item", "Purchase Order", "Purchase Receipt", + "Purchase Invoice", "Job Card", "Stock Entry"] } diff --git a/erpnext/manufacturing/doctype/bom_item/bom_item.json b/erpnext/manufacturing/doctype/bom_item/bom_item.json index febf315988..f094be4c64 100644 --- a/erpnext/manufacturing/doctype/bom_item/bom_item.json +++ b/erpnext/manufacturing/doctype/bom_item/bom_item.json @@ -1,1053 +1,273 @@ { - "allow_copy": 0, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, "creation": "2013-02-22 01:27:49", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Setup", "editable_grid": 1, + "field_order": [ + "item_code", + "item_name", + "operation", + "column_break_3", + "bom_no", + "source_warehouse", + "allow_alternative_item", + "section_break_5", + "description", + "col_break1", + "image", + "image_view", + "quantity_and_rate", + "qty", + "uom", + "col_break2", + "stock_qty", + "stock_uom", + "conversion_factor", + "rate_amount_section", + "rate", + "base_rate", + "column_break_21", + "amount", + "base_amount", + "section_break_18", + "scrap", + "qty_consumed_per_unit", + "section_break_27", + "include_item_in_manufacturing", + "original_item" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 3, "fieldname": "item_code", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, "in_list_view": 1, - "in_standard_filter": 0, "label": "Item Code", - "length": 0, - "no_copy": 0, "oldfieldname": "item_code", "oldfieldtype": "Link", "options": "Item", - "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": 3, "fieldname": "item_name", "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": "Item Name", - "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": "Item Name" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "operation", "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": "Item operation", - "length": 0, - "no_copy": 0, - "options": "Operation", - "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": "Operation" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "column_break_3", - "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": 0, - "collapsible": 0, - "columns": 0, "fieldname": "bom_no", "fieldtype": "Link", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, "in_filter": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "BOM No", - "length": 0, - "no_copy": 0, "oldfieldname": "bom_no", "oldfieldtype": "Link", "options": "BOM", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, "search_index": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "source_warehouse", "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 Warehouse", - "length": 0, - "no_copy": 0, - "options": "Warehouse", - "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": "Warehouse" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, "collapsible": 1, - "columns": 0, "fieldname": "section_break_5", "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": "Description", - "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": "Description" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "description", "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": "Item Description", - "length": 0, - "no_copy": 0, "oldfieldname": "description", "oldfieldtype": "Text", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "250px", - "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": "250px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "col_break1", - "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, "fieldname": "image", "fieldtype": "Attach", "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, "fieldname": "image_view", "fieldtype": "Image", - "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": "Image View", - "length": 0, - "no_copy": 0, - "options": "image", - "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": "image" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "quantity_and_rate", "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": "Quantity and Rate", - "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": "Quantity and Rate" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 2, "fieldname": "qty", "fieldtype": "Float", - "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": "Qty", - "length": 0, - "no_copy": 0, "oldfieldname": "qty", "oldfieldtype": "Currency", - "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": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "fieldname": "uom", "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": "UOM", - "length": 0, - "no_copy": 0, "options": "UOM", - "permlevel": 0, - "precision": "", - "print_hide": 0, - "print_hide_if_no_value": 0, - "read_only": 0, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "col_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, "fieldname": "stock_qty", "fieldtype": "Float", - "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": "Stock Qty", - "length": 0, - "no_copy": 0, "oldfieldname": "stock_qty", "oldfieldtype": "Currency", - "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, "fieldname": "stock_uom", "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": "Stock UOM", - "length": 0, - "no_copy": 0, "oldfieldname": "stock_uom", "oldfieldtype": "Data", "options": "UOM", - "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, "fieldname": "conversion_factor", "fieldtype": "Float", - "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": "Conversion Factor", - "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": "Conversion Factor" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "rate_amount_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": "Rate & Amount", - "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": "Rate & Amount" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "", "fieldname": "rate", "fieldtype": "Currency", - "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": "Rate", - "length": 0, - "no_copy": 0, "options": "currency", - "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": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "reqd": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "base_rate", "fieldtype": "Currency", - "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": "Basic Rate (Company Currency)", - "length": 0, - "no_copy": 0, "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", "print_hide": 1, - "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, "fieldname": "column_break_21", - "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": 0, - "collapsible": 0, - "columns": 0, "fieldname": "amount", "fieldtype": "Currency", - "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": "Amount", - "length": 0, - "no_copy": 0, "oldfieldname": "amount_as_per_mar", "oldfieldtype": "Currency", "options": "currency", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "150px", "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0, "width": "150px" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "base_amount", "fieldtype": "Currency", - "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": "Amount (Company Currency)", - "length": 0, - "no_copy": 0, "options": "Company:company:default_currency", - "permlevel": 0, - "precision": "", "print_hide": 1, - "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, "fieldname": "section_break_18", - "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, - "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": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, "columns": 1, "fieldname": "scrap", "fieldtype": "Float", - "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": "Scrap %", - "length": 0, - "no_copy": 0, "oldfieldname": "scrap", "oldfieldtype": "Currency", - "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, "fieldname": "qty_consumed_per_unit", "fieldtype": "Float", "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": "Qty Consumed Per Unit", - "length": 0, - "no_copy": 0, "oldfieldname": "qty_consumed_per_unit", "oldfieldtype": "Float", - "permlevel": 0, "print_hide": 1, - "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, "fieldname": "section_break_27", - "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, - "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": "Section Break" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fieldname": "allow_alternative_item", "fieldtype": "Check", - "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": "Allow Alternative Item", - "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 + "label": "Allow Alternative Item" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, + "default": "0", "fetch_from": "item_code.include_item_in_manufacturing", "fieldname": "include_item_in_manufacturing", "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": "Include Item In Manufacturing", - "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": "Include Item In Manufacturing" }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "original_item", "fieldtype": "Link", "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": "Original Item", - "length": 0, - "no_copy": 0, "options": "Item", - "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 } ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, "idx": 1, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, "istable": 1, - "max_attachments": 0, - "modified": "2019-02-21 19:19:54.872459", + "modified": "2019-11-22 11:38:52.087303", "modified_by": "Administrator", "module": "Manufacturing", "name": "BOM Item", "owner": "Administrator", "permissions": [], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "show_name_in_global_search": 0, "sort_field": "modified", - "sort_order": "DESC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + "sort_order": "DESC" } \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/production_plan/production_plan.py b/erpnext/manufacturing/doctype/production_plan/production_plan.py index 5d2696933b..25c385fb1e 100644 --- a/erpnext/manufacturing/doctype/production_plan/production_plan.py +++ b/erpnext/manufacturing/doctype/production_plan/production_plan.py @@ -529,7 +529,6 @@ def get_material_request_items(row, sales_order, required_qty = ceil(required_qty) if required_qty > 0: - print(row) return { 'item_code': row.item_code, 'item_name': row.item_name, diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 089cb8014d..2c16bbe90c 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -609,6 +609,22 @@ def get_item_details(item, project = None): return res +@frappe.whitelist() +def make_work_order(item, qty=0, project=None): + if not frappe.has_permission("Work Order", "write"): + frappe.throw(_("Not permitted"), frappe.PermissionError) + + item_details = get_item_details(item, project) + + wo_doc = frappe.new_doc("Work Order") + wo_doc.production_item = item + wo_doc.update(item_details) + if qty > 0: + wo_doc.qty = qty + wo_doc.get_items_and_operations_from_bom() + + return wo_doc + @frappe.whitelist() def check_if_scrap_warehouse_mandatory(bom_no): res = {"set_scrap_wh_mandatory": False } diff --git a/erpnext/patches/v11_0/rename_bom_wo_fields.py b/erpnext/patches/v11_0/rename_bom_wo_fields.py index c8106a6bd5..b4a740fabb 100644 --- a/erpnext/patches/v11_0/rename_bom_wo_fields.py +++ b/erpnext/patches/v11_0/rename_bom_wo_fields.py @@ -15,13 +15,6 @@ def execute(): rename_field(doctype, "allow_transfer_for_manufacture", "include_item_in_manufacturing") - if frappe.db.has_column('BOM', 'allow_same_item_multiple_times'): - frappe.db.sql(""" UPDATE tabBOM - SET - allow_same_item_multiple_times = 0 - WHERE - trim(coalesce(allow_same_item_multiple_times, '')) = '' """) - for doctype in ['BOM', 'Work Order']: frappe.reload_doc('manufacturing', 'doctype', frappe.scrub(doctype)) diff --git a/erpnext/stock/doctype/quality_inspection/quality_inspection.py b/erpnext/stock/doctype/quality_inspection/quality_inspection.py index 738c63ca35..37ab807cb7 100644 --- a/erpnext/stock/doctype/quality_inspection/quality_inspection.py +++ b/erpnext/stock/doctype/quality_inspection/quality_inspection.py @@ -6,6 +6,7 @@ import frappe from frappe.model.document import Document from erpnext.stock.doctype.quality_inspection_template.quality_inspection_template \ import get_template_details +from frappe.model.mapper import get_mapped_doc class QualityInspection(Document): def validate(self): @@ -84,3 +85,37 @@ def item_query(doctype, txt, searchfield, start, page_len, filters): parent=filters.get('parent'), cond = cond, mcond = mcond, start = start, page_len = page_len, qi_condition = qi_condition), {'parent': filters.get('parent'), 'txt': "%%%s%%" % txt}) + +def quality_inspection_query(doctype, txt, searchfield, start, page_len, filters): + return frappe.get_all('Quality Inspection', + limit_start=start, + limit_page_length=page_len, + filters = { + 'docstatus': 1, + 'name': ('like', '%%%s%%' % txt), + 'item_code': filters.get("item_code"), + 'reference_name': ('in', [filters.get("reference_name", ''), '']) + }, as_list=1) + +@frappe.whitelist() +def make_quality_inspection(source_name, target_doc=None): + def postprocess(source, doc): + doc.inspected_by = frappe.session.user + doc.get_quality_inspection_template() + + doc = get_mapped_doc("BOM", source_name, { + 'BOM': { + "doctype": "Quality Inspection", + "validation": { + "docstatus": ["=", 1] + }, + "field_map": { + "name": "bom_no", + "item": "item_code", + "stock_uom": "uom", + "stock_qty": "qty" + }, + } + }, target_doc, postprocess) + + return doc \ No newline at end of file diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index 6e78b988f6..d9c94fced7 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -102,11 +102,12 @@ frappe.ui.form.on('Stock Entry', { frm.set_query("quality_inspection", "items", function(doc, cdt, cdn) { var d = locals[cdt][cdn]; + return { + query:"erpnext.stock.doctype.quality_inspection.quality_inspection.quality_inspection_query", filters: { - docstatus: 1, - item_code: d.item_code, - reference_name: doc.name + 'item_code': d.item_code, + 'reference_name': doc.name } } }); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 26693d208b..f81fa683ba 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -91,6 +91,7 @@ class StockEntry(StockController): self.update_cost_in_project() self.validate_reserved_serial_no_consumption() self.update_transferred_qty() + self.update_quality_inspection() if self.work_order and self.purpose == "Manufacture": self.update_so_in_serial_number() @@ -108,6 +109,7 @@ class StockEntry(StockController): self.make_gl_entries_on_cancel() self.update_cost_in_project() self.update_transferred_qty() + self.update_quality_inspection() def set_job_card_data(self): if self.job_card and not self.work_order: @@ -1285,6 +1287,20 @@ class StockEntry(StockController): self._update_percent_field_in_targets(args, update_modified=True) + def update_quality_inspection(self): + if self.inspection_required: + reference_type = reference_name = '' + if self.docstatus == 1: + reference_name = self.name + reference_type = 'Stock Entry' + + for d in self.items: + if d.quality_inspection: + frappe.db.set_value("Quality Inspection", d.quality_inspection, { + 'reference_type': reference_type, + 'reference_name': reference_name + }) + @frappe.whitelist() def move_sample_to_retention_warehouse(company, items): if isinstance(items, string_types): From 53a66ee3865b351549f9b2c5f54d1df83e285065 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Mon, 25 Nov 2019 21:58:15 +0530 Subject: [PATCH 208/210] fix: Method name in hooks, test case code clean up --- erpnext/hooks.py | 2 +- erpnext/selling/doctype/quotation/quotation.py | 3 +-- .../selling/doctype/quotation/test_quotation.py | 17 +++++++---------- 3 files changed, 9 insertions(+), 13 deletions(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 715839c58f..e4b5e3012f 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -302,7 +302,7 @@ scheduler_events = { "erpnext.support.doctype.service_level_agreement.service_level_agreement.check_agreement_status", "erpnext.crm.doctype.email_campaign.email_campaign.send_email_to_leads_or_contacts", "erpnext.crm.doctype.email_campaign.email_campaign.set_email_campaign_status", - "erpnext.selling.doctype.quotation.set_expired" + "erpnext.selling.doctype.quotation.set_expired_status" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 2ce01aa4af..790b2f0804 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -186,9 +186,8 @@ def _make_sales_order(source_name, target_doc=None, ignore_permissions=False): return doclist def set_expired_status(): - frappe.db.sql("""UPDATE `tabQuotation` SET `status` = 'Expired' + frappe.db.sql("""UPDATE `tabQuotation` SET `status` = 'Expired' WHERE `status` != "Expired" AND `valid_till` < %s""", (nowdate())) - frappe.db.commit() @frappe.whitelist() def make_sales_invoice(source_name, target_doc=None): diff --git a/erpnext/selling/doctype/quotation/test_quotation.py b/erpnext/selling/doctype/quotation/test_quotation.py index 95b5634695..ee6b429cca 100644 --- a/erpnext/selling/doctype/quotation/test_quotation.py +++ b/erpnext/selling/doctype/quotation/test_quotation.py @@ -201,29 +201,26 @@ class TestQuotation(unittest.TestCase): sec_qo = make_quotation(item_list=qo_item2, do_not_submit=True) sec_qo.submit() - def test_expired_quotations(self): - import datetime + def test_quotation_expiry(self): from erpnext.selling.doctype.quotation.quotation import set_expired_status - from erpnext.stock.doctype.item.test_item import make_item - test_item = make_item("_Test Paraglider", - {"is_stock_item":1}) quotation_item = [ { - "item_code": test_item.item_code, + "item_code": "_Test Item", "warehouse":"", "qty": 1, "rate": 500 } ] - yesterday = getdate(nowdate()) - datetime.timedelta(days=1) - expired_quotation = make_quotation(item_list=quotation_item,transaction_date=yesterday,do_not_submit=True) + + yesterday = add_days(nowdate(), -1) + expired_quotation = make_quotation(item_list=quotation_item, transaction_date=yesterday, do_not_submit=True) expired_quotation.valid_till = yesterday expired_quotation.save() expired_quotation.submit() set_expired_status() - expired_quotation = frappe.get_doc("Quotation",expired_quotation.name) - self.assertEqual(expired_quotation.status,"Expired") + expired_quotation.reload() + self.assertEqual(expired_quotation.status, "Expired") test_records = frappe.get_test_records('Quotation') From 2515022377ba47109a563b3bb23a8bacbd93f7ce Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 26 Nov 2019 10:55:28 +0530 Subject: [PATCH 209/210] add condition for zero appointment slots --- erpnext/crm/doctype/appointment/appointment.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index b6962d923a..2affba2ac4 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -28,8 +28,9 @@ class Appointment(Document): 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 (number_of_appointments_in_same_slot >= number_of_agents): - frappe.throw('Time slot is not available') + 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() From fb1e87710b42821f983abb70659e6ac1a5f79d34 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Tue, 26 Nov 2019 12:14:41 +0530 Subject: [PATCH 210/210] Tweaks to success redirect - 5 seconds wait before redirect - Edited description for URL in settings --- .../appointment_booking_settings.json | 4 ++-- erpnext/www/book-appointment/index.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json index 17e754b748..4b26e4901b 100644 --- a/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json +++ b/erpnext/crm/doctype/appointment_booking_settings/appointment_booking_settings.json @@ -94,14 +94,14 @@ "label": "Success Settings" }, { - "description": "Leave blank for home.\nThis is relative to site URL, for example \"/about\" will redirect to \"https://yoursitename.com/about\"", + "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-20 15:17:55.617364", + "modified": "2019-11-26 12:14:17.669366", "modified_by": "Administrator", "module": "CRM", "name": "Appointment Booking Settings", diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book-appointment/index.js index 433b956014..13c87ddbcf 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book-appointment/index.js @@ -219,7 +219,7 @@ async function submit() { if (window.appointment_settings.success_redirect_url){ redirect_url += window.appointment_settings.success_redirect_url; } - window.location.href = redirect_url;},2) + window.location.href = redirect_url;},5000) }, error: (err)=>{ frappe.show_alert("Something went wrong please try again");