From 7b9e30914fdd97fa3894c09e04be74aa11ff37af Mon Sep 17 00:00:00 2001 From: pranav nachnekar Date: Wed, 28 Aug 2019 16:57:37 +0530 Subject: [PATCH 001/299] 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 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/appointment/appointment.js b/erpnext/crm/doctype/appointment/appointment.js new file mode 100644 index 00000000000..4e41047fa11 --- /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 00000000000..24cbd92bc73 --- /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 00000000000..204b066031b --- /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 00000000000..702ac7176fb --- /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 00000000000..e69de29bb2d 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 00000000000..465df2c3a64 --- /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 00000000000..ed6150a210f --- /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 00000000000..33076366c10 --- /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 00000000000..3dc3c399712 --- /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 00000000000..e69de29bb2d 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 00000000000..d26f7ced357 --- /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 00000000000..8258471eed1 --- /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 00000000000..e69de29bb2d diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py new file mode 100644 index 00000000000..92a8889cced --- /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 00000000000..4dc57db2ed4 --- /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 00000000000..9eb8ed9012b --- /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 00000000000..20e7d378f70 --- /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 00000000000..db4ef26651f --- /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 00000000000..d05c2535c19 --- /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 00000000000..95169b9bf20 --- /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 00000000000..198b12d67cd --- /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 00000000000..bdcabdc5ef2 --- /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 00000000000..688545a77da --- /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 00000000000..b627a0c9cf7 --- /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 00000000000..23c55a3fce2 --- /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/299] 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 198b12d67cd..2a8c5c916c8 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 bdcabdc5ef2..113564a7228 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 688545a77da..fa8aafac0b1 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/299] 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 00000000000..6899d82291d --- /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/299] 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 d05c2535c19..9f6c4872410 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/299] 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 db4ef26651f..da4fb259191 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 9f6c4872410..00000000000 --- 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 95169b9bf20..00000000000 --- 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 2a8c5c916c8..00000000000 --- 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 113564a7228..00000000000 --- 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 fa8aafac0b1..00000000000 --- 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 b627a0c9cf7..00000000000 --- 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 23c55a3fce2..00000000000 --- 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 00000000000..3ffe996238e --- /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 00000000000..b705f9e82d4 --- /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 00000000000..1482c511d43 --- /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 00000000000..15d5f9a49d1 --- /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/299] 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 204b066031b..cce6a1d684c 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 d54b568c34f..cf27f770c27 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 33076366c10..8f1fb14f5be 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/299] 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 1482c511d43..e1a2338bfd5 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 15d5f9a49d1..340f3adb672 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/299] 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 435607f99a1..aee16f799f7 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/299] 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 aee16f799f7..ec63420e984 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/299] 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 00000000000..e69de29bb2d 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 00000000000..d26f7ced357 --- /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 00000000000..62436b8da7d --- /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/299] 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 340f3adb672..e853a35fff7 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/299] 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 ec63420e984..8392549fd37 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 3ffe996238e..a6e6313f796 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 b705f9e82d4..b915484f549 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 e1a2338bfd5..bb21ddf273a 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/299] 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 702ac7176fb..e446712d016 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/299] 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 bb21ddf273a..5bc8af0bde6 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/299] 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 e69de29bb2d..00000000000 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 d26f7ced357..00000000000 --- 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 62436b8da7d..00000000000 --- 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 5bc8af0bde6..b2df3b43821 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 e853a35fff7..9c37fb0c990 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/299] 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 8258471eed1..94fb0c94d64 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 b915484f549..f4074270e08 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 9c37fb0c990..f4e96b47d67 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/299] 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 00000000000..e69de29bb2d 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 00000000000..d26f7ced357 --- /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 00000000000..bd764806ba9 --- /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/299] 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 8392549fd37..356cbea2cce 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 465df2c3a64..2642e6eb26a 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 6899d82291d..00000000000 --- 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/299] 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 b2df3b43821..61ea8e40d76 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/299] 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 f4e96b47d67..1b87b86a407 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/299] 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 e446712d016..96c4e4fc059 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 cf27f770c27..11820b965a5 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 d26f7ced357..b54af8dba4f 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 94fb0c94d64..8258471eed1 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/299] 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 96c4e4fc059..8487b258f23 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/299] 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 8487b258f23..c1a1c4ff460 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/299] 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 cce6a1d684c..30d10194b2c 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/299] 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 f4074270e08..43275eb2437 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 61ea8e40d76..90572fb8918 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/299] 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 11820b965a5..0150309ad0a 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/299] 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 f0da6e3d9a4..2c77023b39a 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/299] 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 0150309ad0a..2386ed76e99 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/299] 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 356cbea2cce..b2fe7b9db21 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 30d10194b2c..4c95c6e5c38 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 43275eb2437..2e0321394e7 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 90572fb8918..f9d9b6e8456 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 1b87b86a407..530445ff919 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/299] 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 2386ed76e99..6ef00703d1a 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 f9d9b6e8456..96ad66ace31 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/299] 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 4c95c6e5c38..13904116a79 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/299] 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 13904116a79..1b6ef94bfc8 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/299] 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 b2fe7b9db21..2d695f31999 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/299] 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 96ad66ace31..5302d1b626b 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/299] 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 1b6ef94bfc8..4dea04b39cb 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 6ef00703d1a..c59a2e466fd 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/299] 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 4dea04b39cb..3a588fbcd86 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/299] 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 3a588fbcd86..614a43c5904 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/299] 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 3a588fbcd86..5408b4d91ad 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 c1a1c4ff460..3c977505b53 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 8f1fb14f5be..da181ae119f 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 2c77023b39a..539ffa25476 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 5302d1b626b..8fc5e317088 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 530445ff919..6f6d4ac45cb 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/299] 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 8fc5e317088..345e6141542 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/299] 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 6f6d4ac45cb..e238bd52053 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/299] 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 8a6d0635bc7..1ffd58fa04e 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 e238bd52053..3370f2429e7 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/299] 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 1ffd58fa04e..ac2e0a8c74b 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/299] 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 3370f2429e7..d5111c8d1b2 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/299] 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 ac2e0a8c74b..6d23f2a7670 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/299] 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 6d23f2a7670..9365301e8f8 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/299] 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 c59a2e466fd..d72f577656a 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/299] 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 2d695f31999..5ea234437db 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 3c22dc71999..eb68c679ba5 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/299] 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 9365301e8f8..52711fee848 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 d5111c8d1b2..c1585aaf2ff 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 00000000000..ebb65b1f24e --- /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 00000000000..d25b50565ab --- /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/299] 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 52711fee848..dee7c7c32cd 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/299] 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 52711fee848..af2878ec67a 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/299] 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 1ca706c19b9..5d8a30fd2f8 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/299] 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 4e41047fa11..975abfcd936 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/299] 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 5ea234437db..22df5c6aa8b 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/299] 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 5d8a30fd2f8..5e0648659be 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/299] 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 c1585aaf2ff..67619fc5d54 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/299] 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 5e0648659be..1095b56ae99 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 c1585aaf2ff..67619fc5d54 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/299] 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 3c977505b53..d529d37aad0 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/299] 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 1095b56ae99..219f93111a6 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 67619fc5d54..49b3ffc2cf8 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/299] 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 219f93111a6..b259758e024 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/299] 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 219f93111a6..95c7f35fbba 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/299] 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 95c7f35fbba..260026c4953 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/299] 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 260026c4953..a495b910e8b 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 d25b50565ab..86f95153325 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/299] 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 a495b910e8b..2f140989a79 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/299] 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 2f140989a79..f32699e7867 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/299] 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 22df5c6aa8b..9dfcc571973 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 2e0321394e7..1f6dd2e0e65 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 345e6141542..6034f4eb485 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/299] 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 86f95153325..8ea96383a3a 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/299] 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 d529d37aad0..bc7fe72e901 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/299] 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 9dfcc571973..323e096b404 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 d72f577656a..4229e4b7887 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/299] 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 975abfcd936..485520f5624 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/299] 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 bc7fe72e901..d73c6ec035b 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/299] 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 bc7fe72e901..9b87c792cdf 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/299] 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 9b87c792cdf..f6385bfba2d 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/299] 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 6034f4eb485..1b7a80154ee 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/299] 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 f6385bfba2d..7e7f67c7004 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/299] 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 49b3ffc2cf8..5f51ced96e2 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/299] 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 f32699e7867..4dcb2016ace 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/299] 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 1f6dd2e0e65..e1355a77657 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 1b7a80154ee..f2496da5a63 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/299] 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 7e7f67c7004..ec16d6a582e 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/299] 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 a6e6313f796..d5202065eac 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/299] 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 ec16d6a582e..5670a7e52e1 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/299] 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 da181ae119f..6a1cf56cb76 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/299] 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 5670a7e52e1..111ab08d23c 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/299] 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 111ab08d23c..f1aa610548c 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/299] 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 f1aa610548c..d7731bec87d 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/299] 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 4229e4b7887..90f3ad9410e 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 f2496da5a63..07355e16ffb 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 5f51ced96e2..946cc1b1925 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/299] 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 d7731bec87d..16a370eb7be 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/299] 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 16a370eb7be..852e0f1e06e 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/299] 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 852e0f1e06e..b22e6e0087a 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/299] 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 e69de29bb2d..00000000000 diff --git a/erpnext/crm/doctype/timezone/test_timezone.py b/erpnext/crm/doctype/timezone/test_timezone.py deleted file mode 100644 index 92a8889cced..00000000000 --- 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 4dc57db2ed4..00000000000 --- 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 b998e6c21d8..00000000000 --- 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 539ffa25476..00000000000 --- 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 946cc1b1925..5f03e777e36 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/299] 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 5f03e777e36..a810a2b323c 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 8ea96383a3a..6eda19f925a 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/299] 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 07355e16ffb..cfacc79dc22 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 a810a2b323c..1a9afa577e4 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/299] 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 4dcb2016ace..2da4acc1f55 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 b22e6e0087a..72c2ae5ee70 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 cfacc79dc22..f0cf1d76643 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 1a9afa577e4..e279a4717b3 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/299] 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 90f3ad9410e..25a7c692686 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/299] 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 2da4acc1f55..5615ae19691 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/299] 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 5615ae19691..942960e81e0 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/299] 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 942960e81e0..714e88ded38 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/299] 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 714e88ded38..2b9d31da505 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/299] 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 485520f5624..fb78d1af949 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/299] 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 fb78d1af949..8888b569c46 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 2b9d31da505..5d1e301b6e8 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/299] 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 e279a4717b3..6f416d1b2b3 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 6eda19f925a..e8ccecd8b69 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/299] 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 5d1e301b6e8..b39de13abbe 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/299] 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 5d1e301b6e8..607584f685c 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/299] 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 d5202065eac..9398b30371d 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/299] 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 6a1cf56cb76..ef762ff0259 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/299] 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 e1355a77657..43a3f1026da 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/299] 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 323e096b404..32df8ec4295 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/299] 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 ef762ff0259..2aa51caefdd 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/299] 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 9398b30371d..30ce957e2c7 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/299] 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 43a3f1026da..10fe09ab3c0 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/299] 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 f0cf1d76643..19fc7045016 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/299] 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 f575f529092..18f47c9be03 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/299] 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 6f416d1b2b3..eb7d5b918b6 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/299] 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 18f47c9be03..bc2c838930b 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/299] 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 eb7d5b918b6..b983dde6f27 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/299] 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 19fc7045016..6bd868bbc71 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/299] 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 b983dde6f27..9b5ea57a83e 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/299] 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 bc2c838930b..95a9580dbb9 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/299] 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 95a9580dbb9..b3af99da941 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 00000000000..6c9b28bc136 --- /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/299] 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 b3af99da941..fa4b7ec401c 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 e3bc2132622b9e7bf0aff49061a0bc225a6882f2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 1 Nov 2019 22:35:08 +0530 Subject: [PATCH 124/299] feat: multiple company pos profile --- .../page/point_of_sale/point_of_sale.js | 18 +++++++++++++++--- 1 file changed, 15 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index d2c2d70dbe7..5b7f2415714 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -451,7 +451,7 @@ erpnext.pos.PointOfSale = class PointOfSale { change_pos_profile() { return new Promise((resolve) => { - const on_submit = ({ pos_profile, set_as_default }) => { + const on_submit = ({ company, pos_profile, set_as_default }) => { if (pos_profile) { this.pos_profile = pos_profile; } @@ -461,7 +461,7 @@ erpnext.pos.PointOfSale = class PointOfSale { method: "erpnext.accounts.doctype.pos_profile.pos_profile.set_default_profile", args: { 'pos_profile': pos_profile, - 'company': this.frm.doc.company + 'company': company } }).then(() => { this.on_change_pos_profile(); @@ -495,7 +495,19 @@ erpnext.pos.PointOfSale = class PointOfSale { } get_prompt_fields() { + var company_field = this.frm.doc.company; return [{ + fieldtype: 'Link', + label: __('Company'), + options: 'Company', + fieldname: 'company', + default: this.frm.doc.company, + reqd: 1, + onchange: function(e) { + company_field = this.value; + } + }, + { fieldtype: 'Link', label: __('POS Profile'), options: 'POS Profile', @@ -505,7 +517,7 @@ erpnext.pos.PointOfSale = class PointOfSale { return { query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', filters: { - company: this.frm.doc.company + company: company_field } }; } From 4d3dc87a1a2dcf4ad7c71c276c0fc4e8368944dc Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 5 Nov 2019 04:32:06 +0000 Subject: [PATCH 125/299] 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 2aa51caefdd..bb45b7222b4 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 126/299] 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 fa4b7ec401c..9e051f607a7 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 127/299] 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 9e051f607a7..5ca124bd853 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 bb45b7222b4..b8028e31033 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 128/299] 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 5ca124bd853..780e04c5ae1 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 129/299] 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 780e04c5ae1..91d1c03f7d0 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 b8028e31033..2874f3fae2c 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 9b5ea57a83e..11073131b1f 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 e8ccecd8b69..d4478ae34a8 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 130/299] 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 11073131b1f..fe30ef65c59 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 131/299] 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 fe30ef65c59..213617fedcd 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 132/299] 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 213617fedcd..366f399bc0c 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 133/299] 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 366f399bc0c..9765e5ea4d5 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 134/299] 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 9765e5ea4d5..707be6775c9 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 135/299] 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 e95c69413f5..512828b750f 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 136/299] 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 ea047155244..64ad1b5de95 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 137/299] 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 707be6775c9..1fe1987453c 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 138/299] 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 2642e6eb26a..4dd07236ca1 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 139/299] 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 1fe1987453c..a8ab22956d3 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 140/299] 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 6bd868bbc71..70ed4c2ecda 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 141/299] 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 10fe09ab3c0..9e470dafea1 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 142/299] 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 30ce957e2c7..0959d5c4cda 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 143/299] 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 0959d5c4cda..6c49fde739e 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 9e470dafea1..8ddfc2928b7 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 144/299] 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 70ed4c2ecda..457c6cf1a4b 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 145/299] 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 8ddfc2928b7..96774d5656b 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 146/299] 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 457c6cf1a4b..b91e3b08eb7 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 a8ab22956d3..163fdc01321 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 147/299] 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 25a7c692686..aafdfd960a4 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 b91e3b08eb7..433b9560140 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 163fdc01321..5b60dd5e7b7 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 148/299] 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 512828b750f..df4a1d14a7c 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 149/299] 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 eba6c7a02a5..8344c66c1f0 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 53b65ab8ed82d4398e657911ee24c4c6d70af14f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Fri, 15 Nov 2019 16:42:32 +0530 Subject: [PATCH 150/299] 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 9e74bfd2906..715839c58fd 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 4a56e404000..82e98277eea 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 5f4e2546fbc..802c0ba641d 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 f69b9a8c47081722f394d33af1b7b34fd2b358b2 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 15 Nov 2019 16:54:26 +0530 Subject: [PATCH 151/299] fix: fetch default pos profile user for the company --- .../page/point_of_sale/point_of_sale.js | 74 ++++++++++--------- 1 file changed, 41 insertions(+), 33 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 5b7f2415714..9ade4c18934 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -471,7 +471,44 @@ erpnext.pos.PointOfSale = class PointOfSale { } } - frappe.prompt(this.get_prompt_fields(), + + let me = this; + + var dialog = frappe.prompt([{ + fieldtype: 'Link', + label: __('Company'), + options: 'Company', + fieldname: 'company', + default: me.frm.doc.company, + reqd: 1, + onchange: function(e) { + me.get_default_pos_profile(this.value).then((r) => { + if (r && r.name) { + dialog.set_value('pos_profile', r.name); + } + }); + } + }, + { + fieldtype: 'Link', + label: __('POS Profile'), + options: 'POS Profile', + fieldname: 'pos_profile', + default: me.frm.doc.pos_profile, + reqd: 1, + get_query: () => { + return { + query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', + filters: { + company: dialog.get_value('company') + } + }; + } + }, { + fieldtype: 'Check', + label: __('Set as default'), + fieldname: 'set_as_default' + }], on_submit, __('Select POS Profile') ); @@ -494,38 +531,9 @@ erpnext.pos.PointOfSale = class PointOfSale { ]); } - get_prompt_fields() { - var company_field = this.frm.doc.company; - return [{ - fieldtype: 'Link', - label: __('Company'), - options: 'Company', - fieldname: 'company', - default: this.frm.doc.company, - reqd: 1, - onchange: function(e) { - company_field = this.value; - } - }, - { - fieldtype: 'Link', - label: __('POS Profile'), - options: 'POS Profile', - fieldname: 'pos_profile', - reqd: 1, - get_query: () => { - return { - query: 'erpnext.accounts.doctype.pos_profile.pos_profile.pos_profile_query', - filters: { - company: company_field - } - }; - } - }, { - fieldtype: 'Check', - label: __('Set as default'), - fieldname: 'set_as_default' - }]; + get_default_pos_profile(company) { + return frappe.xcall("erpnext.stock.get_item_details.get_pos_profile", + {'company': company}) } setup_company() { From 466702200f22ec43a40017b18439419966a471f6 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 18 Nov 2019 14:55:18 +0530 Subject: [PATCH 152/299] fix: 'NoneType' object has no attribute 'replace' in POS --- erpnext/accounts/doctype/sales_invoice/pos.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index ed45b2cc2c7..ba2378486f1 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -550,11 +550,15 @@ def make_address(args, customer): def make_email_queue(email_queue): name_list = [] + for key, data in iteritems(email_queue): name = frappe.db.get_value('Sales Invoice', {'offline_pos_name': key}, 'name') + if not name: continue + data = json.loads(data) sender = frappe.session.user print_format = "POS Invoice" if not cint(frappe.db.get_value('Print Format', 'POS Invoice', 'disabled')) else None + attachments = [frappe.attach_print('Sales Invoice', name, print_format=print_format)] make(subject=data.get('subject'), content=data.get('content'), recipients=data.get('recipients'), From 39eeac265b428f0d00724d73110771668bc72c19 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 18 Nov 2019 15:20:15 +0530 Subject: [PATCH 153/299] fix: not able to select department in instructor form --- erpnext/education/doctype/instructor/instructor.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/education/doctype/instructor/instructor.js b/erpnext/education/doctype/instructor/instructor.js index f9c7a2a13da..71e044bb70a 100644 --- a/erpnext/education/doctype/instructor/instructor.js +++ b/erpnext/education/doctype/instructor/instructor.js @@ -4,11 +4,11 @@ cur_frm.add_fetch("employee", "image", "image"); frappe.ui.form.on("Instructor", { employee: function(frm) { if(!frm.doc.employee) return; - frappe.db.get_value('Employee', {name: frm.doc.employee}, 'company', (company) => { + frappe.db.get_value('Employee', {name: frm.doc.employee}, 'company', (d) => { frm.set_query("department", function() { return { "filters": { - "company": company, + "company": d.company, } }; }); @@ -16,7 +16,7 @@ frappe.ui.form.on("Instructor", { frm.set_query("department", "instructor_log", function() { return { "filters": { - "company": company, + "company": d.company, } }; }); From 6ef057a2a3225e405435f67dd9c77b11c43eaed0 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 18 Nov 2019 15:55:32 +0530 Subject: [PATCH 154/299] 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 38f283c8d49..4e9ef0b410f 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 3dfc8911fc4..eb99192b889 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 155/299] 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 df4a1d14a7c..456f2ba2b34 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 873a3e76a3f..e94aea94b75 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 156/299] 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 456f2ba2b34..65f248e7bde 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 910dfd05dab..2ff9b02129f 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 157/299] 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 82e98277eea..9903884b883 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 158/299] 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 4dd07236ca1..99b82148d2e 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 aafdfd960a4..2c161ee0c22 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 d26f7ced357..ddf87386295 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 bd764806ba9..3cadbc95590 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 f3ecfd8e5803e976de8966014b2857bd25db11ec Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 19 Nov 2019 14:50:05 +0530 Subject: [PATCH 159/299] fix: fetch leave approvers from both department and employee master (#19611) * 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 d6b66da0814..df0f75a18c3 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 776ff2f75da07808e6a2cecba3890b6e4737440f Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 19 Nov 2019 14:51:25 +0530 Subject: [PATCH 160/299] fix: query for item group listing (#19604) --- erpnext/setup/doctype/item_group/item_group.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index 5603f17a54b..f78246fe011 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -136,6 +136,7 @@ def get_child_groups_for_list_in_html(item_group, start, limit, search): fields = ['name', 'route', 'description', 'image'], filters = dict( show_in_website = 1, + parent_item_group = item_group.name, lft = ('>', item_group.lft), rgt = ('<', item_group.rgt), ), From 2578d49b84e49121ea4864e0995733c519596357 Mon Sep 17 00:00:00 2001 From: Joseph Marie Alba <54699674+erpjosephalba@users.noreply.github.com> Date: Tue, 19 Nov 2019 17:24:09 +0800 Subject: [PATCH 161/299] Correct bug in abbr cause by missing " " separator (#19605) Only 1 letter ABBR is generated after typing in a COMPANY NAME separated by spaces. This is due to missing " " value in split method. For example: Company Name: ABC Multiple Industries Generates Abbr: A Correct Abbr should be: AMI This is caused by mission " " in split method. --- erpnext/setup/doctype/company/company.js | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/setup/doctype/company/company.js b/erpnext/setup/doctype/company/company.js index 313de677fc5..81c5f027a79 100644 --- a/erpnext/setup/doctype/company/company.js +++ b/erpnext/setup/doctype/company/company.js @@ -29,7 +29,8 @@ frappe.ui.form.on("Company", { company_name: function(frm) { if(frm.doc.__islocal) { - let parts = frm.doc.company_name.split(); + # 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; }).join(""); From e13b7698139c1d6a41b22e158803216357350d50 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Tue, 19 Nov 2019 12:04:30 +0000 Subject: [PATCH 162/299] 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 9903884b883..b97eefcf196 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 c436d933038033b71baa8a648d19c0e02e793cce Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 19 Nov 2019 18:21:53 +0530 Subject: [PATCH 163/299] fix: reset pos profile when default doesn't exists --- erpnext/selling/page/point_of_sale/point_of_sale.js | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index 9ade4c18934..b213a29ae7e 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -483,9 +483,7 @@ erpnext.pos.PointOfSale = class PointOfSale { reqd: 1, onchange: function(e) { me.get_default_pos_profile(this.value).then((r) => { - if (r && r.name) { - dialog.set_value('pos_profile', r.name); - } + dialog.set_value('pos_profile', (r && r.name)? r.name : ''); }); } }, From 353f73a153eee3dd474714233be95e8fbabe376f Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Tue, 19 Nov 2019 18:37:21 +0530 Subject: [PATCH 164/299] fix: stock qty not displayed in pos --- erpnext/accounts/doctype/sales_invoice/pos.py | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/pos.py b/erpnext/accounts/doctype/sales_invoice/pos.py index ba2378486f1..a48d2244893 100755 --- a/erpnext/accounts/doctype/sales_invoice/pos.py +++ b/erpnext/accounts/doctype/sales_invoice/pos.py @@ -357,14 +357,11 @@ def get_customer_wise_price_list(): def get_bin_data(pos_profile): itemwise_bin_data = {} - cond = "1=1" + filters = { 'actual_qty': ['>', 0] } if pos_profile.get('warehouse'): - cond = "warehouse = %(warehouse)s" + filters.update({ 'warehouse': pos_profile.get('warehouse') }) - bin_data = frappe.db.sql(""" select item_code, warehouse, actual_qty from `tabBin` - where actual_qty > 0 and {cond}""".format(cond=cond), { - 'warehouse': frappe.db.escape(pos_profile.get('warehouse')) - }, as_dict=1) + bin_data = frappe.db.get_all('Bin', fields = ['item_code', 'warehouse', 'actual_qty'], filters=filters) for bins in bin_data: if bins.item_code not in itemwise_bin_data: From a85ddf2fb472ba82c5ac6c01b24ad6b2c4c25547 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 19 Nov 2019 18:47:48 +0530 Subject: [PATCH 165/299] fix: performance issue of sales invoice while save/submit (#19598) * fix: performace issue of sales invoice while save/submit * Cached price list data, item group child data, added indexing for blanket order --- .../doctype/pricing_rule/pricing_rule.py | 105 +-- .../accounts/doctype/pricing_rule/utils.py | 115 +-- erpnext/controllers/accounts_controller.py | 60 +- erpnext/controllers/taxes_and_totals.py | 2 +- .../doctype/blanket_order/blanket_order.json | 5 +- .../blanket_order_item.json | 352 ++------ erpnext/public/js/controllers/transaction.js | 61 +- .../setup/doctype/item_group/item_group.py | 18 + erpnext/stock/doctype/bin/bin.json | 751 ++++-------------- .../stock/doctype/price_list/price_list.py | 20 + erpnext/stock/get_item_details.py | 44 +- 11 files changed, 451 insertions(+), 1082 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 17762755f42..430dce7ddbb 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -181,8 +181,9 @@ def get_serial_no_for_item(args): item_details.serial_no = get_serial_no(args) return item_details -def get_pricing_rule_for_item(args, price_list_rate=0, doc=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rules +def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): + from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, + get_applied_pricing_rules, get_pricing_rule_items) if isinstance(doc, string_types): doc = json.loads(doc) @@ -209,6 +210,55 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None): item_details, args.get('item_code')) return item_details + update_args_for_pricing_rule(args) + + pricing_rules = (get_applied_pricing_rules(args) + if for_validate and args.get("pricing_rules") else get_pricing_rules(args, doc)) + + if pricing_rules: + rules = [] + + for pricing_rule in pricing_rules: + if not pricing_rule: continue + + if isinstance(pricing_rule, string_types): + pricing_rule = frappe.get_cached_doc("Pricing Rule", pricing_rule) + pricing_rule.apply_rule_on_other_items = get_pricing_rule_items(pricing_rule) + + if pricing_rule.get('suggestion'): continue + + item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0) + item_details.price_or_product_discount = pricing_rule.get("price_or_product_discount") + + rules.append(get_pricing_rule_details(args, pricing_rule)) + + if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: + item_details.update({ + 'apply_rule_on_other_items': json.dumps(pricing_rule.apply_rule_on_other_items), + 'apply_rule_on': (frappe.scrub(pricing_rule.apply_rule_on_other) + if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) + }) + + if pricing_rule.coupon_code_based==1 and args.coupon_code==None: + return item_details + + if (not pricing_rule.validate_applied_rule and + pricing_rule.price_or_product_discount == "Price"): + apply_price_discount_pricing_rule(pricing_rule, item_details, args) + + item_details.has_pricing_rule = 1 + + item_details.pricing_rules = ','.join([d.pricing_rule for d in rules]) + + if not doc: return item_details + + elif args.get("pricing_rules"): + item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), + item_details, args.get('item_code')) + + return item_details + +def update_args_for_pricing_rule(args): if not (args.item_group and args.brand): try: args.item_group, args.brand = frappe.get_cached_value("Item", args.item_code, ["item_group", "brand"]) @@ -235,52 +285,12 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None): args.supplier_group = frappe.get_cached_value("Supplier", args.supplier, "supplier_group") args.customer = args.customer_group = args.territory = None - pricing_rules = get_pricing_rules(args, doc) - - if pricing_rules: - rules = [] - - for pricing_rule in pricing_rules: - if not pricing_rule or pricing_rule.get('suggestion'): continue - - item_details.validate_applied_rule = pricing_rule.get("validate_applied_rule", 0) - - rules.append(get_pricing_rule_details(args, pricing_rule)) - if pricing_rule.mixed_conditions or pricing_rule.apply_rule_on_other: - continue - - if pricing_rule.coupon_code_based==1 and args.coupon_code==None: - return item_details - - if (not pricing_rule.validate_applied_rule and - pricing_rule.price_or_product_discount == "Price"): - apply_price_discount_pricing_rule(pricing_rule, item_details, args) - - item_details.has_pricing_rule = 1 - - # if discount is applied on the rate and not on price list rate - # if price_list_rate: - # set_discount_amount(price_list_rate, item_details) - - item_details.pricing_rules = ','.join([d.pricing_rule for d in rules]) - - if not doc: return item_details - - for rule in rules: - doc.append('pricing_rules', rule) - - elif args.get("pricing_rules"): - item_details = remove_pricing_rule_for_item(args.get("pricing_rules"), - item_details, args.get('item_code')) - - return item_details - def get_pricing_rule_details(args, pricing_rule): return frappe._dict({ 'pricing_rule': pricing_rule.name, 'rate_or_discount': pricing_rule.rate_or_discount, 'margin_type': pricing_rule.margin_type, - 'item_code': pricing_rule.item_code or args.get("item_code"), + 'item_code': args.get("item_code"), 'child_docname': args.get('child_docname') }) @@ -327,10 +337,10 @@ def set_discount_amount(rate, item_details): item_details.rate = rate def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): - from erpnext.accounts.doctype.pricing_rule.utils import get_apply_on_and_items + from erpnext.accounts.doctype.pricing_rule.utils import get_pricing_rule_items for d in pricing_rules.split(','): if not d or not frappe.db.exists("Pricing Rule", d): continue - pricing_rule = frappe.get_doc('Pricing Rule', d) + pricing_rule = frappe.get_cached_doc('Pricing Rule', d) if pricing_rule.price_or_product_discount == 'Price': if pricing_rule.rate_or_discount == 'Discount Percentage': @@ -348,8 +358,9 @@ def remove_pricing_rule_for_item(pricing_rules, item_details, item_code=None): else pricing_rule.get('free_item')) if pricing_rule.get("mixed_conditions") or pricing_rule.get("apply_rule_on_other"): - apply_on, items = get_apply_on_and_items(pricing_rule, item_details) - item_details.apply_on = apply_on + items = get_pricing_rule_items(pricing_rule) + item_details.apply_on = (frappe.scrub(pricing_rule.apply_rule_on_other) + if pricing_rule.apply_rule_on_other else frappe.scrub(pricing_rule.get('apply_on'))) item_details.applied_on_items = ','.join(items) item_details.pricing_rules = '' diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index ef26c2e7bfd..637e503e658 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -8,6 +8,7 @@ import frappe, copy, json from frappe import throw, _ from six import string_types from frappe.utils import flt, cint, get_datetime +from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor @@ -173,10 +174,11 @@ def filter_pricing_rules(args, pricing_rules, doc=None): if (field and pricing_rules[0].get('other_' + field) != args.get(field)): return - pr_doc = frappe.get_doc('Pricing Rule', pricing_rules[0].name) + pr_doc = frappe.get_cached_doc('Pricing Rule', pricing_rules[0].name) if pricing_rules[0].mixed_conditions and doc: - stock_qty, amount = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) + stock_qty, amount, items = get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args) + pricing_rules[0].apply_rule_on_other_items = items elif pricing_rules[0].is_cumulative: items = [args.get(frappe.scrub(pr_doc.get('apply_on')))] @@ -339,17 +341,19 @@ def get_qty_and_rate_for_mixed_conditions(doc, pr_doc, args): sum_qty += data[0] sum_amt += data[1] - return sum_qty, sum_amt + return sum_qty, sum_amt, items def get_qty_and_rate_for_other_item(doc, pr_doc, pricing_rules): - for d in get_pricing_rule_items(pr_doc): - for row in doc.items: - if d == row.get(frappe.scrub(pr_doc.apply_on)): - pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"), - row.get("amount"), pricing_rules, row) + items = get_pricing_rule_items(pr_doc) - if pricing_rules and pricing_rules[0]: - return pricing_rules + for row in doc.items: + if row.get(frappe.scrub(pr_doc.apply_rule_on_other)) in items: + pricing_rules = filter_pricing_rules_for_qty_amount(row.get("stock_qty"), + row.get("amount"), pricing_rules, row) + + if pricing_rules and pricing_rules[0]: + pricing_rules[0].apply_rule_on_other_items = items + return pricing_rules def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]): sum_qty, sum_amt = [0, 0] @@ -397,31 +401,7 @@ def get_qty_amount_data_for_cumulative(pr_doc, doc, items=[]): return [sum_qty, sum_amt] -def validate_pricing_rules(doc): - validate_pricing_rule_on_transactions(doc) - - for d in doc.items: - validate_pricing_rule_on_items(doc, d) - - doc.calculate_taxes_and_totals() - -def validate_pricing_rule_on_items(doc, item_row, do_not_validate = False): - value = 0 - for pricing_rule in get_applied_pricing_rules(doc, item_row): - pr_doc = frappe.get_doc('Pricing Rule', pricing_rule) - - if pr_doc.get('apply_on') == 'Transaction': continue - - if pr_doc.get('price_or_product_discount') == 'Product': - apply_pricing_rule_for_free_items(doc, pr_doc) - else: - for field in ['discount_percentage', 'discount_amount', 'rate']: - if not pr_doc.get(field): continue - - value += pr_doc.get(field) - apply_pricing_rule(doc, pr_doc, item_row, value, do_not_validate) - -def validate_pricing_rule_on_transactions(doc): +def apply_pricing_rule_on_transaction(doc): conditions = "apply_on = 'Transaction'" values = {} @@ -453,7 +433,7 @@ def validate_pricing_rule_on_transactions(doc): elif d.price_or_product_discount == 'Product': apply_pricing_rule_for_free_items(doc, d) -def get_applied_pricing_rules(doc, item_row): +def get_applied_pricing_rules(item_row): return (item_row.get("pricing_rules").split(',') if item_row.get("pricing_rules") else []) @@ -468,70 +448,29 @@ def apply_pricing_rule_for_free_items(doc, pricing_rule): 'item_code': pricing_rule.get('free_item'), 'qty': pricing_rule.get('free_qty'), 'uom': pricing_rule.get('free_item_uom'), - 'rate': pricing_rule.get('free_item_rate'), + 'rate': pricing_rule.get('free_item_rate') or 0, 'is_free_item': 1 }) doc.set_missing_values() -def apply_pricing_rule(doc, pr_doc, item_row, value, do_not_validate=False): - apply_on, items = get_apply_on_and_items(pr_doc, item_row) - - rule_applied = {} - - for item in doc.get("items"): - if item.get(apply_on) in items: - if not item.pricing_rules: - item.pricing_rules = item_row.pricing_rules - - for field in ['discount_percentage', 'discount_amount', 'rate']: - if not pr_doc.get(field): continue - - key = (item.name, item.pricing_rules) - if not pr_doc.validate_applied_rule: - rule_applied[key] = 1 - item.set(field, value) - elif item.get(field) < value: - if not do_not_validate and item.idx == item_row.idx: - rule_applied[key] = 0 - frappe.msgprint(_("Row {0}: user has not applied rule {1} on the item {2}") - .format(item.idx, pr_doc.title, item.item_code)) - - if rule_applied and doc.get("pricing_rules"): - for d in doc.get("pricing_rules"): - key = (d.child_docname, d.pricing_rule) - if key in rule_applied: - d.rule_applied = 1 - -def get_apply_on_and_items(pr_doc, item_row): - # for mixed or other items conditions - apply_on = frappe.scrub(pr_doc.get('apply_on')) - items = (get_pricing_rule_items(pr_doc) - if pr_doc.mixed_conditions else [item_row.get(apply_on)]) - - if pr_doc.apply_rule_on_other: - apply_on = frappe.scrub(pr_doc.apply_rule_on_other) - items = [pr_doc.get(apply_on)] - - return apply_on, items - def get_pricing_rule_items(pr_doc): + apply_on_data = [] apply_on = frappe.scrub(pr_doc.get('apply_on')) pricing_rule_apply_on = apply_on_table.get(pr_doc.get('apply_on')) - return [item.get(apply_on) for item in pr_doc.get(pricing_rule_apply_on)] or [] + for d in pr_doc.get(pricing_rule_apply_on): + if apply_on == 'item_group': + get_child_item_groups(d.get(apply_on)) + else: + apply_on_data.append(d.get(apply_on)) -@frappe.whitelist() -def validate_pricing_rule_for_different_cond(doc): - if isinstance(doc, string_types): - doc = json.loads(doc) + if pr_doc.apply_rule_on_other: + apply_on = frappe.scrub(pr_doc.apply_rule_on_other) + apply_on_data.append(pr_doc.get(apply_on)) - doc = frappe.get_doc(doc) - for d in doc.get("items"): - validate_pricing_rule_on_items(doc, d, True) - - return doc + return list(set(apply_on_data)) def validate_coupon_code(coupon_name): from frappe.utils import today,getdate diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index a912ef00d15..1f8b6635958 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -5,15 +5,17 @@ from __future__ import unicode_literals import frappe, erpnext import json from frappe import _, throw -from frappe.utils import today, flt, cint, fmt_money, formatdate, getdate, add_days, add_months, get_last_day, nowdate -from erpnext.stock.get_item_details import get_conversion_factor +from frappe.utils import (today, flt, cint, fmt_money, formatdate, + getdate, add_days, add_months, get_last_day, nowdate, get_link_to_form) +from erpnext.stock.get_item_details import get_conversion_factor, get_item_details from erpnext.setup.utils import get_exchange_rate from erpnext.accounts.utils import get_fiscal_years, validate_fiscal_year, get_account_currency from erpnext.utilities.transaction_base import TransactionBase from erpnext.buying.utils import update_last_purchase_rate from erpnext.controllers.sales_and_purchase_return import validate_return from erpnext.accounts.party import get_party_account_currency, validate_party_frozen_disabled -from erpnext.accounts.doctype.pricing_rule.utils import validate_pricing_rules +from erpnext.accounts.doctype.pricing_rule.utils import (apply_pricing_rule_on_transaction, + apply_pricing_rule_for_free_items, get_applied_pricing_rules) from erpnext.exceptions import InvalidCurrency from six import text_type from erpnext.accounts.doctype.accounting_dimension.accounting_dimension import get_accounting_dimensions @@ -101,7 +103,7 @@ class AccountsController(TransactionBase): validate_regional(self) if self.doctype != 'Material Request': - validate_pricing_rules(self) + apply_pricing_rule_on_transaction(self) def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() @@ -232,7 +234,6 @@ class AccountsController(TransactionBase): def set_missing_item_details(self, for_validate=False): """set missing item values""" - from erpnext.stock.get_item_details import get_item_details from erpnext.stock.doctype.serial_no.serial_no import get_serial_nos if hasattr(self, "items"): @@ -244,7 +245,6 @@ class AccountsController(TransactionBase): document_type = "{} Item".format(self.doctype) parent_dict.update({"document_type": document_type}) - self.set('pricing_rules', []) # party_name field used for customer in quotation if self.doctype == "Quotation" and self.quotation_to == "Customer" and parent_dict.get("party_name"): parent_dict.update({"customer": parent_dict.get("party_name")}) @@ -264,7 +264,7 @@ class AccountsController(TransactionBase): if self.get("is_subcontracted"): args["is_subcontracted"] = self.is_subcontracted - ret = get_item_details(args, self, overwrite_warehouse=False) + ret = get_item_details(args, self, for_validate=True, overwrite_warehouse=False) for fieldname, value in ret.items(): if item.meta.get_field(fieldname) and value is not None: @@ -285,24 +285,42 @@ class AccountsController(TransactionBase): if self.doctype in ["Purchase Invoice", "Sales Invoice"] and item.meta.get_field('is_fixed_asset'): item.set('is_fixed_asset', ret.get('is_fixed_asset', 0)) - if ret.get("pricing_rules") and not ret.get("validate_applied_rule", 0): - # if user changed the discount percentage then set user's discount percentage ? - item.set("pricing_rules", ret.get("pricing_rules")) - item.set("discount_percentage", ret.get("discount_percentage")) - item.set("discount_amount", ret.get("discount_amount")) - if ret.get("pricing_rule_for") == "Rate": - item.set("price_list_rate", ret.get("price_list_rate")) - - if item.get("price_list_rate"): - item.rate = flt(item.price_list_rate * - (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) - - if item.get('discount_amount'): - item.rate = item.price_list_rate - item.discount_amount + if ret.get("pricing_rules"): + self.apply_pricing_rule_on_items(item, ret) if self.doctype == "Purchase Invoice": self.set_expense_account(for_validate) + def apply_pricing_rule_on_items(self, item, pricing_rule_args): + if not pricing_rule_args.get("validate_applied_rule", 0): + # if user changed the discount percentage then set user's discount percentage ? + if pricing_rule_args.get("price_or_product_discount") == 'Price': + item.set("pricing_rules", pricing_rule_args.get("pricing_rules")) + item.set("discount_percentage", pricing_rule_args.get("discount_percentage")) + item.set("discount_amount", pricing_rule_args.get("discount_amount")) + if pricing_rule_args.get("pricing_rule_for") == "Rate": + item.set("price_list_rate", pricing_rule_args.get("price_list_rate")) + + if item.get("price_list_rate"): + item.rate = flt(item.price_list_rate * + (1.0 - (flt(item.discount_percentage) / 100.0)), item.precision("rate")) + + if item.get('discount_amount'): + item.rate = item.price_list_rate - item.discount_amount + + elif pricing_rule_args.get('free_item'): + apply_pricing_rule_for_free_items(self, pricing_rule_args) + + elif pricing_rule_args.get("validate_applied_rule"): + for pricing_rule in get_applied_pricing_rules(item): + pricing_rule_doc = frappe.get_cached_doc("Pricing Rule", pricing_rule) + for field in ['discount_percentage', 'discount_amount', 'rate']: + if item.get(field) < pricing_rule_doc.get(field): + title = get_link_to_form("Pricing Rule", pricing_rule) + + frappe.msgprint(_("Row {0}: user has not applied the rule {1} on the item {2}") + .format(item.idx, frappe.bold(title), frappe.bold(item.item_code))) + def set_taxes(self): if not self.meta.get_field("taxes"): return diff --git a/erpnext/controllers/taxes_and_totals.py b/erpnext/controllers/taxes_and_totals.py index d2db9d005a7..66232d7ff1b 100644 --- a/erpnext/controllers/taxes_and_totals.py +++ b/erpnext/controllers/taxes_and_totals.py @@ -552,7 +552,7 @@ class calculate_taxes_and_totals(object): if item.price_list_rate: if item.pricing_rules and not self.doc.ignore_pricing_rule: for d in item.pricing_rules.split(','): - pricing_rule = frappe.get_doc('Pricing Rule', d) + pricing_rule = frappe.get_cached_doc('Pricing Rule', d) if (pricing_rule.margin_type == 'Amount' and pricing_rule.currency == self.doc.currency)\ or (pricing_rule.margin_type == 'Percentage'): diff --git a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json index 260e0b8a736..0330e5c85c9 100644 --- a/erpnext/manufacturing/doctype/blanket_order/blanket_order.json +++ b/erpnext/manufacturing/doctype/blanket_order/blanket_order.json @@ -89,7 +89,8 @@ "fieldtype": "Link", "label": "Company", "options": "Company", - "reqd": 1 + "reqd": 1, + "search_index": 1 }, { "fieldname": "section_break_12", @@ -129,7 +130,7 @@ } ], "is_submittable": 1, - "modified": "2019-10-16 13:38:32.302316", + "modified": "2019-11-18 19:37:37.151686", "modified_by": "Administrator", "module": "Manufacturing", "name": "Blanket Order", diff --git a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json index 099eed4aece..977ad547f55 100644 --- a/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json +++ b/erpnext/manufacturing/doctype/blanket_order_item/blanket_order_item.json @@ -1,298 +1,78 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "beta": 0, - "creation": "2018-05-24 07:20:04.255236", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "document_type": "", - "editable_grid": 1, - "engine": "InnoDB", + "creation": "2018-05-24 07:20:04.255236", + "doctype": "DocType", + "editable_grid": 1, + "engine": "InnoDB", + "field_order": [ + "item_code", + "item_name", + "column_break_3", + "qty", + "rate", + "ordered_qty", + "section_break_7", + "terms_and_conditions" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "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, - "options": "Item", - "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": "item_code", + "fieldtype": "Link", + "in_list_view": 1, + "label": "Item Code", + "options": "Item", + "reqd": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fetch_from": "item_code.item_name", - "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, - "options": "", - "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 - }, + "fetch_from": "item_code.item_name", + "fieldname": "item_name", + "fieldtype": "Data", + "label": "Item Name" + }, { - "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 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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": "Quantity", - "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": "qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Quantity" + }, { - "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": 1, - "in_standard_filter": 0, - "label": "Rate", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "rate", + "fieldtype": "Currency", + "in_list_view": 1, + "label": "Rate", + "reqd": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ordered_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": "Ordered Quantity", - "length": 0, - "no_copy": 1, - "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": "ordered_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Ordered Quantity", + "no_copy": 1, + "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_7", - "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_7", + "fieldtype": "Section Break" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "terms_and_conditions", - "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": "Terms and Conditions", - "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": "terms_and_conditions", + "fieldtype": "Text", + "label": "Terms and Conditions" } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 0, - "idx": 0, - "image_view": 0, - "in_create": 0, - "is_submittable": 0, - "issingle": 0, - "istable": 1, - "max_attachments": 0, - "modified": "2018-06-14 07:04:14.050836", - "modified_by": "Administrator", - "module": "Manufacturing", - "name": "Blanket Order Item", - "name_case": "", - "owner": "Administrator", - "permissions": [], - "quick_entry": 1, - "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 + ], + "istable": 1, + "modified": "2019-11-18 19:37:46.245878", + "modified_by": "Administrator", + "module": "Manufacturing", + "name": "Blanket Order Item", + "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/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index ca492baf5ab..5da949320a1 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -516,7 +516,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, () => me.conversion_factor(doc, cdt, cdn, true), - () => me.validate_pricing_rule(item) + () => me.remove_pricing_rule(item) ]); } } @@ -1174,7 +1174,7 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ callback: function(r) { if (!r.exc && r.message) { r.message.forEach(row_item => { - me.validate_pricing_rule(row_item); + me.remove_pricing_rule(row_item); }); me._set_values_for_item_list(r.message); me.calculate_taxes_and_totals(); @@ -1283,6 +1283,8 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ _set_values_for_item_list: function(children) { var me = this; var price_list_rate_changed = false; + var items_rule_dict = {}; + for(var i=0, l=children.length; i { + if (in_list(data.apply_rule_on_other_items, d[data.apply_rule_on])) { + for(var k in data) { + if (in_list(fields, k)) { + frappe.model.set_value(d.doctype, d.name, k, data[k]); + } + } + } + }); + } + }, + apply_price_list: function(item, reset_plc_conversion) { // We need to reset plc_conversion_rate sometimes because the call to // `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value @@ -1348,33 +1375,11 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ }); }, - validate_pricing_rule: function(item) { + remove_pricing_rule: function(item) { let me = this; const fields = ["discount_percentage", "discount_amount", "pricing_rules"]; - if (item.pricing_rules) { - frappe.call({ - method: "erpnext.accounts.doctype.pricing_rule.utils.validate_pricing_rule_for_different_cond", - args: { - doc: me.frm.doc - }, - callback: function(r) { - if (r.message) { - r.message.items.forEach(d => { - me.frm.doc.items.forEach(row => { - if(d.name == row.name) { - fields.forEach(f => { - row[f] = d[f]; - }); - } - }); - }); - - me.trigger_price_list_rate(); - } - } - }); - } else if(item.remove_free_item) { + if(item.remove_free_item) { var items = []; me.frm.doc.items.forEach(d => { diff --git a/erpnext/setup/doctype/item_group/item_group.py b/erpnext/setup/doctype/item_group/item_group.py index f78246fe011..22375ae22c3 100644 --- a/erpnext/setup/doctype/item_group/item_group.py +++ b/erpnext/setup/doctype/item_group/item_group.py @@ -39,6 +39,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): invalidate_cache_for(self) self.validate_name_with_item() self.validate_one_root() + self.delete_child_item_groups_key() def make_route(self): '''Make website route''' @@ -58,6 +59,7 @@ class ItemGroup(NestedSet, WebsiteGenerator): def on_trash(self): NestedSet.on_trash(self) WebsiteGenerator.on_trash(self) + self.delete_child_item_groups_key() def validate_name_with_item(self): if frappe.db.exists("Item", self.name): @@ -83,6 +85,9 @@ class ItemGroup(NestedSet, WebsiteGenerator): return context + def delete_child_item_groups_key(self): + frappe.cache().hdel("child_item_groups", self.name) + @frappe.whitelist(allow_guest=True) def get_product_list_for_group(product_group=None, start=0, limit=10, search=None): if product_group: @@ -168,6 +173,19 @@ def get_child_groups(item_group_name): from `tabItem Group` where lft>=%(lft)s and rgt<=%(rgt)s and show_in_website = 1""", {"lft": item_group.lft, "rgt": item_group.rgt}) +def get_child_item_groups(item_group_name): + child_item_groups = frappe.cache().hget("child_item_groups", item_group_name) + + if not child_item_groups: + item_group = frappe.get_cached_doc("Item Group", item_group_name) + + child_item_groups = [d.name for d in frappe.get_all('Item Group', + filters= {'lft': ('>=', item_group.lft),'rgt': ('>=', item_group.rgt)})] + + frappe.cache().hset("child_item_groups", item_group_name, child_item_groups) + + return child_item_groups or {} + def get_item_for_list_in_html(context): # add missing absolute link in files # user may forget it during upload diff --git a/erpnext/stock/doctype/bin/bin.json b/erpnext/stock/doctype/bin/bin.json index e17429bc0be..04d624ec0b7 100644 --- a/erpnext/stock/doctype/bin/bin.json +++ b/erpnext/stock/doctype/bin/bin.json @@ -1,599 +1,200 @@ { - "allow_copy": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, - "autoname": "MAT-BIN-.YYYY.-.#####", - "beta": 0, - "creation": "2013-01-10 16:34:25", - "custom": 0, - "docstatus": 0, - "doctype": "DocType", - "editable_grid": 0, - "engine": "InnoDB", + "autoname": "MAT-BIN-.YYYY.-.#####", + "creation": "2013-01-10 16:34:25", + "doctype": "DocType", + "engine": "InnoDB", + "field_order": [ + "warehouse", + "item_code", + "reserved_qty", + "actual_qty", + "ordered_qty", + "indented_qty", + "planned_qty", + "projected_qty", + "reserved_qty_for_production", + "reserved_qty_for_sub_contract", + "ma_rate", + "stock_uom", + "fcfs_rate", + "valuation_rate", + "stock_value" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "warehouse", - "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": 1, - "label": "Warehouse", - "length": 0, - "no_copy": 0, - "oldfieldname": "warehouse", - "oldfieldtype": "Link", - "options": "Warehouse", - "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": "warehouse", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Warehouse", + "oldfieldname": "warehouse", + "oldfieldtype": "Link", + "options": "Warehouse", + "read_only": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "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": 1, - "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": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "item_code", + "fieldtype": "Link", + "in_filter": 1, + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Item Code", + "oldfieldname": "item_code", + "oldfieldtype": "Link", + "options": "Item", + "read_only": 1, + "search_index": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "default": "0.00", - "fieldname": "reserved_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": "Reserved Quantity", - "length": 0, - "no_copy": 0, - "oldfieldname": "reserved_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 - }, + "default": "0.00", + "fieldname": "reserved_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Reserved Quantity", + "oldfieldname": "reserved_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, - "default": "0.00", - "fieldname": "actual_qty", - "fieldtype": "Float", - "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": "Actual Quantity", - "length": 0, - "no_copy": 0, - "oldfieldname": "actual_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 - }, + "default": "0.00", + "fieldname": "actual_qty", + "fieldtype": "Float", + "in_filter": 1, + "in_list_view": 1, + "label": "Actual Quantity", + "oldfieldname": "actual_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, - "default": "0.00", - "fieldname": "ordered_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": "Ordered Quantity", - "length": 0, - "no_copy": 0, - "oldfieldname": "ordered_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 - }, + "default": "0.00", + "fieldname": "ordered_qty", + "fieldtype": "Float", + "in_list_view": 1, + "label": "Ordered Quantity", + "oldfieldname": "ordered_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, - "default": "0.00", - "fieldname": "indented_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": "Requested Quantity", - "length": 0, - "no_copy": 0, - "oldfieldname": "indented_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 - }, + "default": "0.00", + "fieldname": "indented_qty", + "fieldtype": "Float", + "label": "Requested Quantity", + "oldfieldname": "indented_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": "planned_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": "Planned Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "planned_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": "planned_qty", + "fieldtype": "Float", + "label": "Planned Qty", + "oldfieldname": "planned_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": "projected_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": "Projected Qty", - "length": 0, - "no_copy": 0, - "oldfieldname": "projected_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": "projected_qty", + "fieldtype": "Float", + "label": "Projected Qty", + "oldfieldname": "projected_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": "reserved_qty_for_production", - "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": "Reserved Qty for Production", - "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": "reserved_qty_for_production", + "fieldtype": "Float", + "label": "Reserved Qty for Production", + "read_only": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "reserved_qty_for_sub_contract", - "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": "Reserved Qty for sub contract", - "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": "reserved_qty_for_sub_contract", + "fieldtype": "Float", + "label": "Reserved Qty for sub contract" + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "ma_rate", - "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": "Moving Average Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "ma_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "ma_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "Moving Average Rate", + "oldfieldname": "ma_rate", + "oldfieldtype": "Currency", + "print_hide": 1, + "read_only": 1, + "report_hide": 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": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "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", + "in_filter": 1, + "label": "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": "fcfs_rate", - "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": "FCFS Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "fcfs_rate", - "oldfieldtype": "Currency", - "permlevel": 0, - "print_hide": 1, - "print_hide_if_no_value": 0, - "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 - }, + "fieldname": "fcfs_rate", + "fieldtype": "Float", + "hidden": 1, + "label": "FCFS Rate", + "oldfieldname": "fcfs_rate", + "oldfieldtype": "Currency", + "print_hide": 1, + "read_only": 1, + "report_hide": 1 + }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "valuation_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": "Valuation Rate", - "length": 0, - "no_copy": 0, - "oldfieldname": "valuation_rate", - "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": "valuation_rate", + "fieldtype": "Float", + "label": "Valuation Rate", + "oldfieldname": "valuation_rate", + "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": "stock_value", - "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 Value", - "length": 0, - "no_copy": 0, - "oldfieldname": "stock_value", - "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": "stock_value", + "fieldtype": "Float", + "label": "Stock Value", + "oldfieldname": "stock_value", + "oldfieldtype": "Currency", + "read_only": 1 } - ], - "has_web_view": 0, - "hide_heading": 0, - "hide_toolbar": 1, - "idx": 1, - "image_view": 0, - "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2018-08-21 16:15:39.356230", - "modified_by": "Administrator", - "module": "Stock", - "name": "Bin", - "owner": "Administrator", + ], + "hide_toolbar": 1, + "idx": 1, + "in_create": 1, + "modified": "2019-11-18 18:34:59.456882", + "modified_by": "Administrator", + "module": "Stock", + "name": "Bin", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "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": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase User" + }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Stock User" } - ], - "quick_entry": 1, - "read_only": 0, - "read_only_onload": 0, - "search_fields": "item_code,warehouse", - "show_name_in_global_search": 0, - "sort_order": "ASC", - "track_changes": 0, - "track_seen": 0, - "track_views": 0 + ], + "quick_entry": 1, + "search_fields": "item_code,warehouse", + "sort_order": "ASC" } \ No newline at end of file diff --git a/erpnext/stock/doctype/price_list/price_list.py b/erpnext/stock/doctype/price_list/price_list.py index 8773b9c33f7..33713faf696 100644 --- a/erpnext/stock/doctype/price_list/price_list.py +++ b/erpnext/stock/doctype/price_list/price_list.py @@ -16,6 +16,7 @@ class PriceList(Document): def on_update(self): self.set_default_if_missing() self.update_item_price() + self.delete_price_list_details_key() def set_default_if_missing(self): if cint(self.selling): @@ -32,6 +33,8 @@ class PriceList(Document): (self.currency, cint(self.buying), cint(self.selling), self.name)) def on_trash(self): + self.delete_price_list_details_key() + def _update_default_price_list(module): b = frappe.get_doc(module + " Settings") price_list_fieldname = module.lower() + "_price_list" @@ -43,3 +46,20 @@ class PriceList(Document): for module in ["Selling", "Buying"]: _update_default_price_list(module) + + def delete_price_list_details_key(self): + frappe.cache().hdel("price_list_details", self.name) + +def get_price_list_details(price_list): + price_list_details = frappe.cache().hget("price_list_details", price_list) + + if not price_list_details: + price_list_details = frappe.get_cached_value("Price List", price_list, + ["currency", "price_not_uom_dependent", "enabled"], as_dict=1) + + if not price_list_details or not price_list_details.get("enabled"): + throw(_("Price List {0} is disabled or does not exist").format(price_list)) + + frappe.cache().hset("price_list_details", price_list, price_list_details) + + return price_list_details or {} \ No newline at end of file diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 7c2e09e4631..9f47edc7740 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -12,6 +12,7 @@ from frappe.model.meta import get_field_precision from erpnext.stock.doctype.batch.batch import get_batch_no from erpnext import get_company_currency from erpnext.stock.doctype.item.item import get_item_defaults, get_uom_conv_factor +from erpnext.stock.doctype.price_list.price_list import get_price_list_details from erpnext.setup.doctype.item_group.item_group import get_item_group_defaults from erpnext.setup.doctype.brand.brand import get_brand_defaults from erpnext.stock.doctype.item_manufacturer.item_manufacturer import get_item_manufacturer_part_no @@ -22,7 +23,7 @@ sales_doctypes = ['Quotation', 'Sales Order', 'Delivery Note', 'Sales Invoice'] purchase_doctypes = ['Material Request', 'Supplier Quotation', 'Purchase Order', 'Purchase Receipt', 'Purchase Invoice'] @frappe.whitelist() -def get_item_details(args, doc=None, overwrite_warehouse=True): +def get_item_details(args, doc=None, for_validate=False, overwrite_warehouse=True): """ args = { "item_code": "", @@ -74,7 +75,9 @@ def get_item_details(args, doc=None, overwrite_warehouse=True): if args.get(key) is None: args[key] = value - data = get_pricing_rule_for_item(args, out.price_list_rate, doc) + data = get_pricing_rule_for_item(args, out.price_list_rate, + doc, for_validate=for_validate) + out.update(data) update_stock(args, out) @@ -479,7 +482,6 @@ def get_price_list_rate(args, item_doc, out): if meta.get_field("currency") or args.get('currency'): pl_details = get_price_list_currency_and_exchange_rate(args) args.update(pl_details) - validate_price_list(args) if meta.get_field("currency"): validate_conversion_rate(args, meta) @@ -634,14 +636,6 @@ def check_packing_list(price_list_rate_name, desired_qty, item_code): return flag -def validate_price_list(args): - if args.get("price_list"): - if not frappe.db.get_value("Price List", - {"name": args.price_list, args.transaction_type: 1, "enabled": 1}): - throw(_("Price List {0} is disabled or does not exist").format(args.price_list)) - elif args.get("customer"): - throw(_("Price List not selected")) - def validate_conversion_rate(args, meta): from erpnext.controllers.accounts_controller import validate_conversion_rate @@ -905,27 +899,6 @@ def apply_price_list_on_item(args): return item_details -def get_price_list_currency(price_list): - if price_list: - result = frappe.db.get_value("Price List", {"name": price_list, - "enabled": 1}, ["name", "currency"], as_dict=True) - - if not result: - throw(_("Price List {0} is disabled or does not exist").format(price_list)) - - return result.currency - -def get_price_list_uom_dependant(price_list): - if price_list: - result = frappe.db.get_value("Price List", {"name": price_list, - "enabled": 1}, ["name", "price_not_uom_dependent"], as_dict=True) - - if not result: - throw(_("Price List {0} is disabled or does not exist").format(price_list)) - - return not result.price_not_uom_dependent - - def get_price_list_currency_and_exchange_rate(args): if not args.price_list: return {} @@ -935,8 +908,11 @@ def get_price_list_currency_and_exchange_rate(args): elif args.doctype in ['Purchase Order', 'Purchase Receipt', 'Purchase Invoice']: args.update({"exchange_rate": "for_buying"}) - price_list_currency = get_price_list_currency(args.price_list) - price_list_uom_dependant = get_price_list_uom_dependant(args.price_list) + price_list_details = get_price_list_details(args.price_list) + + price_list_currency = price_list_details.get("currency") + price_list_uom_dependant = price_list_details.get("price_list_uom_dependant") + plc_conversion_rate = args.plc_conversion_rate company_currency = get_company_currency(args.company) From c42312ea12a58ea24d81b72d93f2386273d24872 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:05:23 +0530 Subject: [PATCH 166/299] 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 3830ca03615..7b4a4c92ad1 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 248585b5a128c8711feafdde3953b4e9e9409969 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 19 Nov 2019 19:21:27 +0530 Subject: [PATCH 167/299] 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 859529204be..81fdbbefc35 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 c8e66a0f7162bed95984804c1c74cc838894ca9c Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 10:27:59 +0530 Subject: [PATCH 168/299] 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 2c161ee0c22..92343dbb135 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 2874f3fae2c..fd20ba07925 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 169/299] 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 fd20ba07925..484a5729c5f 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 170/299] 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 484a5729c5f..e817271e2a4 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 45e9dd9c519ca12c29efd058b09212f6b3ea8d1b Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Wed, 20 Nov 2019 11:40:38 +0530 Subject: [PATCH 171/299] fix(Journal Entry): default Cash Entry account not getting fetched --- erpnext/accounts/doctype/journal_entry/journal_entry.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/journal_entry/journal_entry.js b/erpnext/accounts/doctype/journal_entry/journal_entry.js index 221e3a72803..d6236cdb04f 100644 --- a/erpnext/accounts/doctype/journal_entry/journal_entry.js +++ b/erpnext/accounts/doctype/journal_entry/journal_entry.js @@ -398,7 +398,7 @@ cur_frm.cscript.voucher_type = function(doc, cdt, cdn) { method: "erpnext.accounts.doctype.journal_entry.journal_entry.get_default_bank_cash_account", args: { "account_type": (doc.voucher_type=="Bank Entry" ? - "Bank" : (doc.voucher_type=="Cash" ? "Cash" : null)), + "Bank" : (doc.voucher_type=="Cash Entry" ? "Cash" : null)), "company": doc.company }, callback: function(r) { From 682956543eb12ef8504cea3c9a1fb83c88cab782 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 20 Nov 2019 11:45:14 +0530 Subject: [PATCH 172/299] 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 92343dbb135..dbdf432dffe 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 173/299] 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 dbdf432dffe..17e754b7483 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 e817271e2a4..82acd93f90b 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 174/299] 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 b97eefcf196..ba34dff7459 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 175/299] 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 7ee4a76ca66..bd63c3d96ac 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 c4e6c42950076d9caf1e08a5699face0b6596b5b Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 21 Nov 2019 11:20:49 +0530 Subject: [PATCH 176/299] 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 049a7eba61a..69b8e3e488d 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 177/299] 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 bd63c3d96ac..a95fd52f0a4 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 178/299] 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 32fcee1abe4..16c1a32b9b5 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 046137caa23d2454c63b7aabe114ad14bb9944c2 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Fri, 22 Nov 2019 11:34:50 +0530 Subject: [PATCH 179/299] 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 5c53d26ad12..19d54a011a2 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 85b11667902..e41ad428469 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 dc3a1be0c7e..27d8233a44b 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 3d96d487a84..70a80ca184c 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 46fdc8fc10e..01492807def 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 180/299] 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 81c5f027a79..be736d2d9d1 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 181/299] 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 9e4dc12e653..07b646b0f82 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 00000000000..3a8cd43e302 --- /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 182/299] 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 845ff747d61..f62df20ae1a 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 8c0a1e56f7d..b5598f8d0b2 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 164c659fe87..7495dffec24 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 183/299] 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 bf7e833285c..9530fc9556b 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 184/299] 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 ba34dff7459..ac2c2421e53 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 185/299] 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 a95fd52f0a4..1713556754a 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 186/299] 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 6d3a28e5e24..0d37c10e9cc 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 187/299] 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 2e0fc94bc90..6f2fbe5c370 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 188/299] 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 68b8b502e5b..a74253e8722 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 189/299] 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 19d54a011a2..75107b0b6de 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 38f283c8d49..e9703dd7907 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 9415eedc5c4..d4185ea25e7 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 53fd6d394d8..a56440de3d3 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 14f3922c05f..2a42894623e 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 a25b4ce82e6..3236e726ded 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 56425a0dcb4..155597e8565 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 d0befcbcf38..3392850e963 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 5842e9edbf8..4d4fc7c4629 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 2d181b53ca4..dd602eca103 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 2f4abbcea66..410d9f1b45b 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 173b394f797..7df40fb02cd 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 0cb21d73f90..d0fae6a2272 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 190/299] 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 f0889bfa1b2..6b3f2c777cf 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 d4185ea25e7..546f3740947 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 46cde6ee812..02f39e0e7f4 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 a71212ea47b..89977e29529 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 19af81d65bf..e62d6844114 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 191/299] 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 ac2c2421e53..66ad215dfa3 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 1713556754a..2aefe3a0d37 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 192/299] 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 66ad215dfa3..b63c2e1fef5 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 193/299] 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 458a56c9e79..cca8efeca4a 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 a8e3ce4eae7..67e20b1e891 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 1ed1c4e6a432214444744774ad555c8446160396 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Mon, 25 Nov 2019 12:33:40 +0530 Subject: [PATCH 194/299] 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 4e9ef0b410f..3a241476df8 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 eb99192b889..f4eaad58dae 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 6f43d9ef8c6..d5a78d4f1f0 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 195/299] 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 2aefe3a0d37..aab5fd783a0 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 f2752bf38c20e872b044fbe93d97f09f9fdbed00 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 14:09:49 +0530 Subject: [PATCH 196/299] 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 72c2ae5ee70..50c98c59de6 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 82acd93f90b..eff8b982c9e 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 197/299] 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 eff8b982c9e..27f14b1dbd8 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 198/299] 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 aab5fd783a0..7739e3e623f 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 199/299] 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 b63c2e1fef5..2ce01aa4af3 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 7739e3e623f..b450c29a874 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 032baeac5b89794bef874828ac8cab5d0dd0edda Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Mon, 25 Nov 2019 15:15:00 +0530 Subject: [PATCH 200/299] 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 b450c29a874..003fd66579b 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 201/299] 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 003fd66579b..cef8f513b07 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 202/299] 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 91d1c03f7d0..b6962d923a5 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 50c98c59de6..0dac2bb9aeb 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 203/299] 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 eba6c7a02a5..05017845b2e 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 204/299] 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 cef8f513b07..95b5634695b 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 205/299] 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 0dac2bb9aeb..50c98c59de6 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 206/299] 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 b9591d6054b..8283fd7e6fc 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 a0faeb5fb55..63f4f977c59 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 db79d7feda4..55799544982 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 803ece7c789..060cd53ef19 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 febf315988c..f094be4c647 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 5d2696933bc..25c385fb1e5 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 089cb8014d2..2c16bbe90c5 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 c8106a6bd59..b4a740fabbf 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 738c63ca358..37ab807cb7b 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 6e78b988f66..d9c94fced7d 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 26693d208b4..f81fa683ba5 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 207/299] 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 715839c58fd..e4b5e3012f5 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 2ce01aa4af3..790b2f0804d 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 95b5634695b..ee6b429ccae 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 208/299] 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 b6962d923a5..2affba2ac40 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 209/299] 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 17e754b7483..4b26e4901bd 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 433b9560140..13c87ddbcff 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"); From cbc29989fecb37c80c4b4caaeed28dc79aaed646 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 26 Nov 2019 15:13:23 +0530 Subject: [PATCH 210/299] fix: Serial no validation against sales invoice --- .../accounts/doctype/sales_invoice/sales_invoice.py | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 70a80ca184c..9d2f133da48 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1048,9 +1048,14 @@ class SalesInvoice(SellingController): continue for serial_no in item.serial_no.split("\n"): - sales_invoice, item_code = frappe.db.get_value("Serial No", serial_no, - ["sales_invoice", "item_code"]) - if sales_invoice and item_code == item.item_code and self.name != sales_invoice: + serial_no_details = frappe.db.get_value("Serial No", serial_no, + ["sales_invoice", "item_code"], as_dict=1) + + if not serial_no_details: + continue + + if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \ + and self.name != serial_no_details.sales_invoice: sales_invoice_company = frappe.db.get_value("Sales Invoice", sales_invoice, "company") if sales_invoice_company == self.company: frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}" From defed15528f1777a09e23850ecffc75c70e60bb5 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Tue, 26 Nov 2019 16:12:29 +0530 Subject: [PATCH 211/299] fix: Validation msg --- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index 9d2f133da48..def671c19b7 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -1056,10 +1056,10 @@ class SalesInvoice(SellingController): if serial_no_details.sales_invoice and serial_no_details.item_code == item.item_code \ and self.name != serial_no_details.sales_invoice: - sales_invoice_company = frappe.db.get_value("Sales Invoice", sales_invoice, "company") + sales_invoice_company = frappe.db.get_value("Sales Invoice", serial_no_details.sales_invoice, "company") if sales_invoice_company == self.company: frappe.throw(_("Serial Number: {0} is already referenced in Sales Invoice: {1}" - .format(serial_no, sales_invoice))) + .format(serial_no, serial_no_details.sales_invoice))) def update_project(self): if self.project: From c41addec96e62a0a9415675c2bbb8a0fb6c6bfda Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 27 Nov 2019 08:49:08 +0530 Subject: [PATCH 212/299] fix: revert value out of sync feature --- erpnext/accounts/general_ledger.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 2ba319d05e5..08b43f5c69e 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -163,6 +163,7 @@ def validate_account_for_perpetual_inventory(gl_map): .format(account), StockAccountInvalidTransaction) elif account_bal != stock_bal: + continue precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) From 9d5f43f4f015de0d855dd16060968e822bbf7bd1 Mon Sep 17 00:00:00 2001 From: Marica Date: Wed, 27 Nov 2019 15:50:45 +0530 Subject: [PATCH 213/299] fix: get_batch_qty_and_serial_no() requires argument 'stock_qty' (#19694) --- erpnext/selling/sales_common.js | 2 +- erpnext/stock/doctype/packed_item/packed_item.json | 14 +++++++++++++- 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/sales_common.js b/erpnext/selling/sales_common.js index e508476576d..1c9b30b8289 100644 --- a/erpnext/selling/sales_common.js +++ b/erpnext/selling/sales_common.js @@ -309,7 +309,7 @@ erpnext.selling.SellingController = erpnext.TransactionController.extend({ child: item, args: { "batch_no": item.batch_no, - "stock_qty": item.stock_qty, + "stock_qty": item.stock_qty || item.qty, //if stock_qty field is not available fetch qty (in case of Packed Items table) "warehouse": item.warehouse, "item_code": item.item_code, "has_serial_no": has_serial_no diff --git a/erpnext/stock/doctype/packed_item/packed_item.json b/erpnext/stock/doctype/packed_item/packed_item.json index b089e759a0f..2ac5c426c03 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.json +++ b/erpnext/stock/doctype/packed_item/packed_item.json @@ -18,6 +18,7 @@ "serial_no", "column_break_11", "batch_no", + "actual_batch_qty", "section_break_13", "actual_qty", "projected_qty", @@ -189,15 +190,26 @@ "oldfieldtype": "Data", "print_hide": 1, "read_only": 1 + }, + { + "depends_on": "batch_no", + "fieldname": "actual_batch_qty", + "fieldtype": "Float", + "label": "Actual Batch Quantity", + "no_copy": 1, + "print_hide": 1, + "read_only": 1 } ], "idx": 1, "istable": 1, - "modified": "2019-08-27 18:17:37.167512", + "modified": "2019-11-26 20:09:59.400960", "modified_by": "Administrator", "module": "Stock", "name": "Packed Item", "owner": "Administrator", "permissions": [], + "sort_field": "modified", + "sort_order": "DESC", "track_changes": 1 } From 074aaa60056208c9d946be88e43a19e53db539d5 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 28 Nov 2019 08:09:47 +0530 Subject: [PATCH 214/299] fix: Path for quotation expiry method in hooks --- erpnext/hooks.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e4b5e3012f5..1fb4c2b5968 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_status" + "erpnext.selling.doctype.quotation.quotation.set_expired_status" ], "daily_long": [ "erpnext.setup.doctype.email_digest.email_digest.send", From 3dd72e238fa57eded4c4f4134e1fd4695ff41b4c Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Thu, 28 Nov 2019 16:44:05 +0530 Subject: [PATCH 215/299] fix: removed stock value and account balance out of sync validation (#19728) --- erpnext/accounts/general_ledger.py | 48 +++++++++++++++--------------- 1 file changed, 24 insertions(+), 24 deletions(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index 08b43f5c69e..feb598a2e51 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -162,34 +162,34 @@ def validate_account_for_perpetual_inventory(gl_map): frappe.throw(_("Account: {0} can only be updated via Stock Transactions") .format(account), StockAccountInvalidTransaction) - elif account_bal != stock_bal: - continue - precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), - currency=frappe.get_cached_value('Company', gl_map[0].company, "default_currency")) + # This has been comment for a temporary, will add this code again on release of immutable ledger + # elif account_bal != stock_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)) - stock_adjustment_account = frappe.db.get_value("Company",gl_map[0].company,"stock_adjustment_account") + # 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') + # 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) }] - } + # 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 - }) + # 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")]) From cf645aceae35da7befd45b0f481302ab3dd9b4c8 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 16:44:56 +0530 Subject: [PATCH 216/299] chore: Added Quick Stock Balance to Stock Module (#19727) - Also 'Stock Balance Report' button no longer primary button --- erpnext/config/stock.py | 4 ++++ .../stock/doctype/quick_stock_balance/quick_stock_balance.js | 2 +- 2 files changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/config/stock.py b/erpnext/config/stock.py index 441a3ab4ec3..e24d7b88df1 100644 --- a/erpnext/config/stock.py +++ b/erpnext/config/stock.py @@ -241,6 +241,10 @@ def get_data(): "type": "doctype", "name": "Quality Inspection Template", }, + { + "type": "doctype", + "name": "Quick Stock Balance", + }, ] }, { diff --git a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js index a6f7343388c..f261fd99790 100644 --- a/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js +++ b/erpnext/stock/doctype/quick_stock_balance/quick_stock_balance.js @@ -16,7 +16,7 @@ frappe.ui.form.on('Quick Stock Balance', { frm.add_custom_button(__('Stock Balance Report'), () => { frappe.set_route('query-report', 'Stock Balance', { 'item_code': frm.doc.item, 'warehouse': frm.doc.warehouse }); - }).addClass("btn-primary"); + }); } }, From d8469a7bfa701fff8d734e0db279ea7687b7f265 Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Thu, 28 Nov 2019 16:47:14 +0530 Subject: [PATCH 217/299] fix: handle None case for get_shipping_amount_from_rules (#19724) --- erpnext/accounts/doctype/shipping_rule/shipping_rule.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py index a20f5c08726..8c4efbebe88 100644 --- a/erpnext/accounts/doctype/shipping_rule/shipping_rule.py +++ b/erpnext/accounts/doctype/shipping_rule/shipping_rule.py @@ -70,7 +70,7 @@ class ShippingRule(Document): def get_shipping_amount_from_rules(self, value): for condition in self.get("conditions"): - if not condition.to_value or (flt(condition.from_value) <= value <= flt(condition.to_value)): + if not condition.to_value or (flt(condition.from_value) <= flt(value) <= flt(condition.to_value)): return condition.shipping_amount return 0.0 From c6e2087673832a278955c2712533395116196ec9 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 28 Nov 2019 16:36:24 +0530 Subject: [PATCH 218/299] fix: Division by zero error in Stock Entry --- erpnext/stock/doctype/stock_entry/stock_entry.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index f81fa683ba5..2b99f72565c 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -657,7 +657,7 @@ class StockEntry(StockController): item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount + (t.amount * d.basic_amount) / total_basic_amount if total_basic_amount else 0 if item_account_wise_additional_cost: for d in self.get("items"): From 6516358a7175da8bce6de9ebc5d2b9913b4a2453 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 18:20:53 +0530 Subject: [PATCH 219/299] fix: Changed type of column 'serial_no' in Stock Ledger Entry (#19704) --- .../stock_ledger_entry.json | 653 ++---------------- 1 file changed, 39 insertions(+), 614 deletions(-) diff --git a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json index 947f94853e2..c9eba71b0d0 100644 --- a/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json +++ b/erpnext/stock/doctype/stock_ledger_entry/stock_ledger_entry.json @@ -1,874 +1,299 @@ { "allow_copy": 1, - "allow_events_in_timeline": 0, - "allow_guest_to_view": 0, - "allow_import": 0, - "allow_rename": 0, "autoname": "MAT-SLE-.YYYY.-.#####", - "beta": 0, "creation": "2013-01-29 19:25:42", - "custom": 0, - "docstatus": 0, "doctype": "DocType", "document_type": "Other", - "editable_grid": 0, "engine": "InnoDB", + "field_order": [ + "item_code", + "serial_no", + "batch_no", + "warehouse", + "posting_date", + "posting_time", + "voucher_type", + "voucher_no", + "voucher_detail_no", + "actual_qty", + "incoming_rate", + "outgoing_rate", + "stock_uom", + "qty_after_transaction", + "valuation_rate", + "stock_value", + "stock_value_difference", + "stock_queue", + "project", + "company", + "fiscal_year", + "is_cancelled", + "to_rename" + ], "fields": [ { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "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": 1, "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, "print_width": "100px", "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, "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": "serial_no", - "fieldtype": "Text", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_global_search": 0, + "fieldtype": "Long Text", "in_list_view": 1, - "in_standard_filter": 0, "label": "Serial No", - "length": 0, - "no_copy": 0, - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "100px", "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": "batch_no", "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": "Batch No", - "length": 0, - "no_copy": 0, "oldfieldname": "batch_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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "warehouse", "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": 1, "label": "Warehouse", - "length": 0, - "no_copy": 0, "oldfieldname": "warehouse", "oldfieldtype": "Link", "options": "Warehouse", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "100px", "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, "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": "posting_date", "fieldtype": "Date", - "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": "Posting Date", - "length": 0, - "no_copy": 0, "oldfieldname": "posting_date", "oldfieldtype": "Date", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "100px", "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 0, - "reqd": 0, "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": "posting_time", "fieldtype": "Time", - "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": "Posting Time", - "length": 0, - "no_copy": 0, "oldfieldname": "posting_time", "oldfieldtype": "Time", - "permlevel": 0, - "print_hide": 0, - "print_hide_if_no_value": 0, "print_width": "100px", "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": "voucher_type", "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": "Voucher Type", - "length": 0, - "no_copy": 0, "oldfieldname": "voucher_type", "oldfieldtype": "Data", "options": "DocType", - "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": "voucher_no", "fieldtype": "Dynamic 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": 1, "label": "Voucher No", - "length": 0, - "no_copy": 0, "oldfieldname": "voucher_no", "oldfieldtype": "Data", "options": "voucher_type", - "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": "voucher_detail_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": "Voucher Detail No", - "length": 0, - "no_copy": 0, "oldfieldname": "voucher_detail_no", "oldfieldtype": "Data", - "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": "actual_qty", "fieldtype": "Float", - "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": "Actual Quantity", - "length": 0, - "no_copy": 0, "oldfieldname": "actual_qty", "oldfieldtype": "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": "incoming_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": "Incoming Rate", - "length": 0, - "no_copy": 0, "oldfieldname": "incoming_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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "outgoing_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": "Outgoing Rate", - "length": 0, - "no_copy": 0, "options": "Company:company:default_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, "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": "qty_after_transaction", "fieldtype": "Float", - "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": "Actual Qty After Transaction", - "length": 0, - "no_copy": 0, "oldfieldname": "bin_aqat", "oldfieldtype": "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": "valuation_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": "Valuation Rate", - "length": 0, - "no_copy": 0, "oldfieldname": "valuation_rate", "oldfieldtype": "Currency", "options": "Company:company:default_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": "stock_value", "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": "Stock Value", - "length": 0, - "no_copy": 0, "oldfieldname": "stock_value", "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "stock_value_difference", "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": "Stock Value Difference", - "length": 0, - "no_copy": 0, "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 + "read_only": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "stock_queue", "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": "Stock Queue (FIFO)", - "length": 0, - "no_copy": 0, "oldfieldname": "fcfs_stack", "oldfieldtype": "Text", - "permlevel": 0, "print_hide": 1, - "print_hide_if_no_value": 0, "read_only": 1, - "remember_last_selected_value": 0, - "report_hide": 1, - "reqd": 0, - "search_index": 0, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "report_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "fieldname": "project", "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": "Project", - "length": 0, - "no_copy": 0, - "options": "Project", - "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": "Project" }, { - "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": 1, - "in_global_search": 0, - "in_list_view": 0, - "in_standard_filter": 0, "label": "Company", - "length": 0, - "no_copy": 0, "oldfieldname": "company", "oldfieldtype": "Data", "options": "Company", - "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": "fiscal_year", "fieldtype": "Data", - "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": "Fiscal Year", - "length": 0, - "no_copy": 0, "oldfieldname": "fiscal_year", "oldfieldtype": "Data", - "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": "is_cancelled", "fieldtype": "Select", "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 Cancelled", - "length": 0, - "no_copy": 0, "options": "\nNo\nYes", - "permlevel": 0, - "print_hide": 0, - "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 + "report_hide": 1 }, { - "allow_bulk_edit": 0, - "allow_in_quick_entry": 0, - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, "default": "1", "fieldname": "to_rename", "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": "To Rename", - "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": 1, - "set_only_once": 0, - "translatable": 0, - "unique": 0 + "search_index": 1 } ], - "has_web_view": 0, - "hide_heading": 0, "hide_toolbar": 1, "icon": "fa fa-list", "idx": 1, - "image_view": 0, "in_create": 1, - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2019-01-07 07:04:37.523024", + "modified": "2019-11-27 12:17:31.522675", "modified_by": "Administrator", "module": "Stock", "name": "Stock Ledger Entry", "owner": "Administrator", "permissions": [ { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, "print": 1, "read": 1, "report": 1, - "role": "Stock User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Stock User" }, { - "amend": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, "export": 1, - "if_owner": 0, - "import": 0, - "permlevel": 0, - "print": 0, "read": 1, "report": 1, - "role": "Accounts Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "role": "Accounts Manager" } ], - "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 From b1fac1817c73598360e41661d17a05a4415a4388 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 18:23:11 +0530 Subject: [PATCH 220/299] fix: Validation for Suppliers in SO to PO (#19700) - Check if there is a Supplier against atleast one item in Sales Order - Validation message earlier was vague --- erpnext/selling/doctype/sales_order/sales_order.py | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e12b359bdf1..e97a4ee4611 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -834,6 +834,10 @@ def make_purchase_order(source_name, for_supplier=None, selected_items=[], targe for item in sales_order.items: if item.supplier and item.supplier not in suppliers: suppliers.append(item.supplier) + + if not suppliers: + frappe.throw(_("Please set a Supplier against the Items to be considered in the Purchase Order.")) + for supplier in suppliers: po =frappe.get_list("Purchase Order", filters={"sales_order":source_name, "supplier":supplier, "docstatus": ("<", "2")}) if len(po) == 0: From 32b69bf12247b0befdfd90a9838e730d1a8b6674 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 19:03:14 +0530 Subject: [PATCH 221/299] fix: UOM was not fetching in purchase invoice (#19732) (#19737) * fix: UOM was not fetching in purchase invoice * fix: Changes requested Co-authored-by: Marica --- .../purchase_invoice/purchase_invoice.js | 17 ----------------- .../doctype/asset_category/asset_category.py | 3 ++- erpnext/stock/get_item_details.py | 8 +++++++- 3 files changed, 9 insertions(+), 19 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js index e4e2c7b10f3..d7e64cf36fd 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.js @@ -330,23 +330,6 @@ erpnext.accounts.PurchaseInvoice = erpnext.buying.BuyingController.extend({ frm: cur_frm }) }, - - item_code: function(frm, cdt, cdn) { - var row = locals[cdt][cdn]; - if(row.item_code) { - frappe.call({ - method: "erpnext.assets.doctype.asset_category.asset_category.get_asset_category_account", - args: { - "item": row.item_code, - "fieldname": "fixed_asset_account", - "company": frm.doc.company - }, - callback: function(r, rt) { - frappe.model.set_value(cdt, cdn, "expense_account", r.message); - } - }) - } - } }); cur_frm.script_manager.make(erpnext.accounts.PurchaseInvoice); diff --git a/erpnext/assets/doctype/asset_category/asset_category.py b/erpnext/assets/doctype/asset_category/asset_category.py index 2a42894623e..fc08841be99 100644 --- a/erpnext/assets/doctype/asset_category/asset_category.py +++ b/erpnext/assets/doctype/asset_category/asset_category.py @@ -29,7 +29,8 @@ def get_asset_category_account(fieldname, item=None, asset=None, account=None, a account=None if not account: - asset_category, company = frappe.db.get_value("Asset", asset, ["asset_category", "company"]) + asset_details = frappe.db.get_value("Asset", asset, ["asset_category", "company"]) + asset_category, company = asset_details or [None, None] account = frappe.db.get_value("Asset Category Account", filters={"parent": asset_category, "company_name": company}, fieldname=fieldname) diff --git a/erpnext/stock/get_item_details.py b/erpnext/stock/get_item_details.py index 9f47edc7740..55f4be136b6 100644 --- a/erpnext/stock/get_item_details.py +++ b/erpnext/stock/get_item_details.py @@ -254,6 +254,12 @@ def get_basic_details(args, item, overwrite_warehouse=True): args['material_request_type'] = frappe.db.get_value('Material Request', args.get('name'), 'material_request_type', cache=True) + expense_account = None + + if args.get('doctype') == 'Purchase Invoice' and item.is_fixed_asset: + from erpnext.assets.doctype.asset_category.asset_category import get_asset_category_account + expense_account = get_asset_category_account(fieldname = "fixed_asset_account", item = args.item_code, company= args.company) + #Set the UOM to the Default Sales UOM or Default Purchase UOM if configured in the Item Master if not args.uom: if args.get('doctype') in sales_doctypes: @@ -271,7 +277,7 @@ def get_basic_details(args, item, overwrite_warehouse=True): "image": cstr(item.image).strip(), "warehouse": warehouse, "income_account": get_default_income_account(args, item_defaults, item_group_defaults, brand_defaults), - "expense_account": get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults), + "expense_account": expense_account or get_default_expense_account(args, item_defaults, item_group_defaults, brand_defaults) , "cost_center": get_default_cost_center(args, item_defaults, item_group_defaults, brand_defaults), 'has_serial_no': item.has_serial_no, 'has_batch_no': item.has_batch_no, From 208c69f196d9677710f4fa45f93ef72ec2c87030 Mon Sep 17 00:00:00 2001 From: Marica Date: Thu, 28 Nov 2019 19:39:55 +0530 Subject: [PATCH 222/299] fix: Permission issue in Stock Entry (#19739) --- erpnext/accounts/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/utils.py b/erpnext/accounts/utils.py index 94697be02f6..89c8467da26 100644 --- a/erpnext/accounts/utils.py +++ b/erpnext/accounts/utils.py @@ -569,7 +569,7 @@ def get_stock_and_account_balance(account=None, posting_date=None, company=None) warehouse_account = get_warehouse_account_map(company) - account_balance = get_balance_on(account, posting_date, in_account_currency=False) + account_balance = get_balance_on(account, posting_date, in_account_currency=False, ignore_account_permission=True) related_warehouses = [wh for wh, wh_details in warehouse_account.items() if wh_details.account == account and not wh_details.is_group] From 2b172ec4b46c5914538d9a238cc22be23711966d Mon Sep 17 00:00:00 2001 From: Suraj Shetty <13928957+surajshetty3416@users.noreply.github.com> Date: Fri, 29 Nov 2019 16:59:21 +0530 Subject: [PATCH 223/299] fix: valuation of "finished good" item in purchase receipt (#19745) * fix: Remove redundant purchase orders and unwanted condition * fix: [WIP] Purchase receipt value * fix: Add raw material cost based on transfered raw material * fix: get_qty_to_be_received * fix: Remove debugger statement * fix: Reset rm_supp_cost before setting subcontracted raw_materials * test: Fix and modify tests for backflush_based_on_stock_entry * fix: Add non stock items to Purchase Receipt from Purchase Order * fix: Ignore valuation rate check for non stock raw material * fix: Rename check all rows * fix: Remove amount from test * test: Fix item rate error * fix: handling of serial nos in backflush * fix: Add serial no. of raw materials * fix: [WIP] Handle Batch nos for purchase reciept backflushed raw material * fix: Raw material batch number selection in purchase receipt * Update test_purchase_order.py --- .../doctype/purchase_order/purchase_order.js | 3 + .../purchase_order/test_purchase_order.py | 53 ++- erpnext/controllers/buying_controller.py | 336 +++++++++++++++--- .../bom_update_tool/bom_update_tool.py | 4 +- ..._order_items_to_be_received_or_billed.json | 2 +- .../stock/report/stock_ledger/stock_ledger.py | 4 +- 6 files changed, 322 insertions(+), 80 deletions(-) diff --git a/erpnext/buying/doctype/purchase_order/purchase_order.js b/erpnext/buying/doctype/purchase_order/purchase_order.js index c5fa98da09f..7b5e5c5cca0 100644 --- a/erpnext/buying/doctype/purchase_order/purchase_order.js +++ b/erpnext/buying/doctype/purchase_order/purchase_order.js @@ -18,6 +18,7 @@ frappe.ui.form.on("Purchase Order", { return { filters: { "company": frm.doc.company, + "name": ['!=', frm.doc.supplier_warehouse], "is_group": 0 } } @@ -283,6 +284,8 @@ erpnext.buying.PurchaseOrderController = erpnext.buying.BuyingController.extend( }) } + me.dialog.get_field('sub_con_rm_items').check_all_rows() + me.dialog.show() this.dialog.set_primary_action(__('Transfer'), function() { me.values = me.dialog.get_values(); diff --git a/erpnext/buying/doctype/purchase_order/test_purchase_order.py b/erpnext/buying/doctype/purchase_order/test_purchase_order.py index 4506db64051..a0a1e8ed5c4 100644 --- a/erpnext/buying/doctype/purchase_order/test_purchase_order.py +++ b/erpnext/buying/doctype/purchase_order/test_purchase_order.py @@ -519,47 +519,62 @@ class TestPurchaseOrder(unittest.TestCase): def test_backflush_based_on_stock_entry(self): item_code = "_Test Subcontracted FG Item 1" make_subcontracted_item(item_code) + make_item('Sub Contracted Raw Material 1', { + 'is_stock_item': 1, + 'is_sub_contracted_item': 1 + }) update_backflush_based_on("Material Transferred for Subcontract") - po = create_purchase_order(item_code=item_code, qty=1, + + order_qty = 5 + po = create_purchase_order(item_code=item_code, qty=order_qty, is_subcontracted="Yes", supplier_warehouse="_Test Warehouse 1 - _TC") - make_stock_entry(target="_Test Warehouse - _TC", qty=10, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code="_Test Item Home Desktop 100", qty=10, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code = "Test Extra Item 1", qty=100, basic_rate=100) make_stock_entry(target="_Test Warehouse - _TC", item_code = "Test Extra Item 2", qty=10, basic_rate=100) + make_stock_entry(target="_Test Warehouse - _TC", + item_code = "Sub Contracted Raw Material 1", qty=10, basic_rate=100) - rm_item = [ - {"item_code":item_code,"rm_item_code":"_Test Item","item_name":"_Test Item", - "qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":100,"stock_uom":"Nos"}, + rm_items = [ + {"item_code":item_code,"rm_item_code":"Sub Contracted Raw Material 1","item_name":"_Test Item", + "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, {"item_code":item_code,"rm_item_code":"_Test Item Home Desktop 100","item_name":"_Test Item Home Desktop 100", - "qty":2,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}, + "qty":20,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, {"item_code":item_code,"rm_item_code":"Test Extra Item 1","item_name":"Test Extra Item 1", - "qty":1,"warehouse":"_Test Warehouse - _TC","rate":100,"amount":200,"stock_uom":"Nos"}] + "qty":10,"warehouse":"_Test Warehouse - _TC", "stock_uom":"Nos"}, + {'item_code': item_code, 'rm_item_code': 'Test Extra Item 2', 'stock_uom':'Nos', + 'qty': 10, 'warehouse': '_Test Warehouse - _TC', 'item_name':'Test Extra Item 2'}] - rm_item_string = json.dumps(rm_item) + rm_item_string = json.dumps(rm_items) se = frappe.get_doc(make_subcontract_transfer_entry(po.name, rm_item_string)) - se.append('items', { - 'item_code': "Test Extra Item 2", - "qty": 1, - "rate": 100, - "s_warehouse": "_Test Warehouse - _TC", - "t_warehouse": "_Test Warehouse 1 - _TC" - }) - se.set_missing_values() se.submit() pr = make_purchase_receipt(po.name) + + received_qty = 2 + # partial receipt + pr.get('items')[0].qty = received_qty pr.save() pr.submit() - se_items = sorted([d.item_code for d in se.get('items')]) - supplied_items = sorted([d.rm_item_code for d in pr.get('supplied_items')]) + transferred_items = sorted([d.item_code for d in se.get('items') if se.purchase_order == po.name]) + issued_items = sorted([d.rm_item_code for d in pr.get('supplied_items')]) + + self.assertEquals(transferred_items, issued_items) + self.assertEquals(pr.get('items')[0].rm_supp_cost, 2000) + + + transferred_rm_map = frappe._dict() + for item in rm_items: + transferred_rm_map[item.get('rm_item_code')] = item + + for item in pr.get('supplied_items'): + self.assertEqual(item.get('required_qty'), (transferred_rm_map[item.get('rm_item_code')].get('qty') / order_qty) * received_qty) - self.assertEquals(se_items, supplied_items) update_backflush_based_on("BOM") def test_advance_payment_entry_unlink_against_purchase_order(self): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index d12643af820..3ec7aff9cbb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -221,7 +221,7 @@ class BuyingController(StockController): "backflush_raw_materials_of_subcontract_based_on") if (self.doctype == 'Purchase Receipt' and backflush_raw_materials_based_on != 'BOM'): - self.update_raw_materials_supplied_based_on_stock_entries(raw_material_table) + self.update_raw_materials_supplied_based_on_stock_entries() else: for item in self.get("items"): if self.doctype in ["Purchase Receipt", "Purchase Invoice"]: @@ -241,41 +241,95 @@ class BuyingController(StockController): if self.is_subcontracted == "No" and self.get("supplied_items"): self.set('supplied_items', []) - def update_raw_materials_supplied_based_on_stock_entries(self, raw_material_table): - self.set(raw_material_table, []) - purchase_orders = [d.purchase_order for d in self.items] - if purchase_orders: - items = get_subcontracted_raw_materials_from_se(purchase_orders) - backflushed_raw_materials = get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, self.name) + def update_raw_materials_supplied_based_on_stock_entries(self): + self.set('supplied_items', []) - for d in items: - qty = d.qty - backflushed_raw_materials.get(d.item_code, 0) - rm = self.append(raw_material_table, {}) - rm.rm_item_code = d.item_code - rm.item_name = d.item_name - rm.main_item_code = d.main_item_code - rm.description = d.description - rm.stock_uom = d.stock_uom - rm.required_qty = qty - rm.consumed_qty = qty - rm.serial_no = d.serial_no - rm.batch_no = d.batch_no + purchase_orders = set([d.purchase_order for d in self.items]) - # get raw materials rate - from erpnext.stock.utils import get_incoming_rate - rm.rate = get_incoming_rate({ - "item_code": d.item_code, - "warehouse": self.supplier_warehouse, - "posting_date": self.posting_date, - "posting_time": self.posting_time, - "qty": -1 * qty, - "serial_no": rm.serial_no - }) - if not rm.rate: - rm.rate = get_valuation_rate(d.item_code, self.supplier_warehouse, - self.doctype, self.name, currency=self.company_currency, company = self.company) + # qty of raw materials backflushed (for each item per purchase order) + backflushed_raw_materials_map = get_backflushed_subcontracted_raw_materials(purchase_orders) - rm.amount = qty * flt(rm.rate) + # qty of "finished good" item yet to be received + qty_to_be_received_map = get_qty_to_be_received(purchase_orders) + + for item in self.get('items'): + # reset raw_material cost + item.rm_supp_cost = 0 + + # qty of raw materials transferred to the supplier + transferred_raw_materials = get_subcontracted_raw_materials_from_se(item.purchase_order, item.item_code) + + non_stock_items = get_non_stock_items(item.purchase_order, item.item_code) + + item_key = '{}{}'.format(item.item_code, item.purchase_order) + + fg_yet_to_be_received = qty_to_be_received_map.get(item_key) + + raw_material_data = backflushed_raw_materials_map.get(item_key, {}) + + consumed_qty = raw_material_data.get('qty', 0) + consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_batch_nos = raw_material_data.get('batch_nos', '') + + transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) + backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) + + for raw_material in transferred_raw_materials + non_stock_items: + transferred_qty = raw_material.qty + + rm_qty_to_be_consumed = transferred_qty - consumed_qty + + # backflush all remaining transferred qty in the last Purchase Receipt + if fg_yet_to_be_received == item.qty: + qty = rm_qty_to_be_consumed + else: + qty = (rm_qty_to_be_consumed / fg_yet_to_be_received) * item.qty + + if frappe.get_cached_value('UOM', raw_material.stock_uom, 'must_be_whole_number'): + qty = frappe.utils.ceil(qty) + + if qty > rm_qty_to_be_consumed: + qty = rm_qty_to_be_consumed + + if not qty: continue + + if raw_material.serial_nos: + set_serial_nos(raw_material, consumed_serial_nos, qty) + + if raw_material.batch_nos: + batches_qty = get_batches_with_qty(raw_material.rm_item_code, raw_material.main_item_code, + qty, transferred_batch_qty_map, backflushed_batch_qty_map) + for batch_data in batches_qty: + qty = batch_data['qty'] + raw_material.batch_no = batch_data['batch'] + self.append_raw_material_to_be_backflushed(item, raw_material, qty) + else: + self.append_raw_material_to_be_backflushed(item, raw_material, qty) + + def append_raw_material_to_be_backflushed(self, fg_item_doc, raw_material_data, qty): + rm = self.append('supplied_items', {}) + rm.update(raw_material_data) + + rm.required_qty = qty + rm.consumed_qty = qty + + if not raw_material_data.get('non_stock_item'): + from erpnext.stock.utils import get_incoming_rate + rm.rate = get_incoming_rate({ + "item_code": raw_material_data.rm_item_code, + "warehouse": self.supplier_warehouse, + "posting_date": self.posting_date, + "posting_time": self.posting_time, + "qty": -1 * qty, + "serial_no": rm.serial_no + }) + + if not rm.rate: + rm.rate = get_valuation_rate(raw_material_data.item_code, self.supplier_warehouse, + self.doctype, self.name, currency=self.company_currency, company=self.company) + + rm.amount = qty * flt(rm.rate) + fg_item_doc.rm_supp_cost += rm.amount def update_raw_materials_supplied_based_on_bom(self, item, raw_material_table): exploded_item = 1 @@ -387,9 +441,11 @@ class BuyingController(StockController): item_codes = list(set(item.item_code for item in self.get("items"))) if item_codes: - self._sub_contracted_items = [r[0] for r in frappe.db.sql("""select name - from `tabItem` where name in (%s) and is_sub_contracted_item=1""" % \ - (", ".join((["%s"]*len(item_codes))),), item_codes)] + items = frappe.get_all('Item', filters={ + 'name': ['in', item_codes], + 'is_sub_contracted_item': 1 + }) + self._sub_contracted_items = [item.name for item in items] return self._sub_contracted_items @@ -722,28 +778,72 @@ def get_items_from_bom(item_code, bom, exploded_item=1): return bom_items -def get_subcontracted_raw_materials_from_se(purchase_orders): - return frappe.db.sql(""" - select - sed.item_name, sed.item_code, sum(sed.qty) as qty, sed.description, - sed.stock_uom, sed.subcontracted_item as main_item_code, sed.serial_no, sed.batch_no - from `tabStock Entry` se,`tabStock Entry Detail` sed - where - se.name = sed.parent and se.docstatus=1 and se.purpose='Send to Subcontractor' - and se.purchase_order in (%s) and ifnull(sed.t_warehouse, '') != '' - group by sed.item_code, sed.t_warehouse - """ % (','.join(['%s'] * len(purchase_orders))), tuple(purchase_orders), as_dict=1) +def get_subcontracted_raw_materials_from_se(purchase_order, fg_item): + common_query = """ + SELECT + sed.item_code AS rm_item_code, + SUM(sed.qty) AS qty, + sed.description, + sed.stock_uom, + sed.subcontracted_item AS main_item_code, + {serial_no_concat_syntax} AS serial_nos, + {batch_no_concat_syntax} AS batch_nos + FROM `tabStock Entry` se,`tabStock Entry Detail` sed + WHERE + se.name = sed.parent + AND se.docstatus=1 + AND se.purpose='Send to Subcontractor' + AND se.purchase_order = %s + AND IFNULL(sed.t_warehouse, '') != '' + AND sed.subcontracted_item = %s + GROUP BY sed.item_code, sed.subcontracted_item + """ + raw_materials = frappe.db.multisql({ + 'mariadb': common_query.format( + serial_no_concat_syntax="GROUP_CONCAT(sed.serial_no)", + batch_no_concat_syntax="GROUP_CONCAT(sed.batch_no)" + ), + 'postgres': common_query.format( + serial_no_concat_syntax="STRING_AGG(sed.serial_no, ',')", + batch_no_concat_syntax="STRING_AGG(sed.batch_no, ',')" + ) + }, (purchase_order, fg_item), as_dict=1) -def get_backflushed_subcontracted_raw_materials_from_se(purchase_orders, purchase_receipt): - return frappe._dict(frappe.db.sql(""" - select - prsi.rm_item_code as item_code, sum(prsi.consumed_qty) as qty - from `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi - where - pr.name = pri.parent and pr.name = prsi.parent and pri.purchase_order in (%s) - and pri.item_code = prsi.main_item_code and pr.name != '%s' and pr.docstatus = 1 - group by prsi.rm_item_code - """ % (','.join(['%s'] * len(purchase_orders)), purchase_receipt), tuple(purchase_orders))) + return raw_materials + +def get_backflushed_subcontracted_raw_materials(purchase_orders): + common_query = """ + SELECT + CONCAT(prsi.rm_item_code, pri.purchase_order) AS item_key, + SUM(prsi.consumed_qty) AS qty, + {serial_no_concat_syntax} AS serial_nos, + {batch_no_concat_syntax} AS batch_nos + FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` prsi + WHERE + pr.name = pri.parent + AND pr.name = prsi.parent + AND pri.purchase_order IN %s + AND pri.item_code = prsi.main_item_code + AND pr.docstatus = 1 + GROUP BY prsi.rm_item_code, pri.purchase_order + """ + + backflushed_raw_materials = frappe.db.multisql({ + 'mariadb': common_query.format( + serial_no_concat_syntax="GROUP_CONCAT(prsi.serial_no)", + batch_no_concat_syntax="GROUP_CONCAT(prsi.batch_no)" + ), + 'postgres': common_query.format( + serial_no_concat_syntax="STRING_AGG(prsi.serial_no, ',')", + batch_no_concat_syntax="STRING_AGG(prsi.batch_no, ',')" + ) + }, (purchase_orders, ), as_dict=1) + + backflushed_raw_materials_map = frappe._dict() + for item in backflushed_raw_materials: + backflushed_raw_materials_map.setdefault(item.item_key, item) + + return backflushed_raw_materials_map def get_asset_item_details(asset_items): asset_items_data = {} @@ -776,3 +876,125 @@ def validate_item_type(doc, fieldname, message): error_message = _("Following item {0} is not marked as {1} item. You can enable them as {1} item from its Item master".format(items, message)) frappe.throw(error_message) + +def get_qty_to_be_received(purchase_orders): + return frappe._dict(frappe.db.sql(""" + SELECT CONCAT(poi.`item_code`, poi.`parent`) AS item_key, + SUM(poi.`qty`) - SUM(poi.`received_qty`) AS qty_to_be_received + FROM `tabPurchase Order Item` poi + WHERE + poi.`parent` in %s + GROUP BY poi.`item_code`, poi.`parent` + HAVING SUM(poi.`qty`) > SUM(poi.`received_qty`) + """, (purchase_orders))) + +def get_non_stock_items(purchase_order, fg_item_code): + return frappe.db.sql(""" + SELECT + pois.main_item_code, + pois.rm_item_code, + item.description, + pois.required_qty AS qty, + pois.rate, + 1 as non_stock_item, + pois.stock_uom + FROM `tabPurchase Order Item Supplied` pois, `tabItem` item + WHERE + pois.`rm_item_code` = item.`name` + AND item.is_stock_item = 0 + AND pois.`parent` = %s + AND pois.`main_item_code` = %s + """, (purchase_order, fg_item_code), as_dict=1) + + +def set_serial_nos(raw_material, consumed_serial_nos, qty): + serial_nos = set(get_serial_nos(raw_material.serial_nos)) - \ + set(get_serial_nos(consumed_serial_nos)) + if serial_nos and qty <= len(serial_nos): + raw_material.serial_no = '\n'.join(list(serial_nos)[0:frappe.utils.cint(qty)]) + +def get_transferred_batch_qty_map(purchase_order, fg_item): + # returns + # { + # (item_code, fg_code): { + # batch1: 10, # qty + # batch2: 16 + # }, + # } + transferred_batch_qty_map = {} + transferred_batches = frappe.db.sql(""" + SELECT + sed.batch_no, + SUM(sed.qty) AS qty, + sed.item_code + FROM `tabStock Entry` se,`tabStock Entry Detail` sed + WHERE + se.name = sed.parent + AND se.docstatus=1 + AND se.purpose='Send to Subcontractor' + AND se.purchase_order = %s + AND sed.subcontracted_item = %s + AND sed.batch_no IS NOT NULL + GROUP BY + sed.batch_no, + sed.item_code + """, (purchase_order, fg_item), as_dict=1) + + for batch_data in transferred_batches: + transferred_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) + transferred_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty + + return transferred_batch_qty_map + +def get_backflushed_batch_qty_map(purchase_order, fg_item): + # returns + # { + # (item_code, fg_code): { + # batch1: 10, # qty + # batch2: 16 + # }, + # } + backflushed_batch_qty_map = {} + backflushed_batches = frappe.db.sql(""" + SELECT + pris.batch_no, + SUM(pris.consumed_qty) AS qty, + pris.rm_item_code AS item_code + FROM `tabPurchase Receipt` pr, `tabPurchase Receipt Item` pri, `tabPurchase Receipt Item Supplied` pris + WHERE + pr.name = pri.parent + AND pri.parent = pris.parent + AND pri.purchase_order = %s + AND pri.item_code = pris.main_item_code + AND pr.docstatus = 1 + AND pris.main_item_code = %s + AND pris.batch_no IS NOT NULL + GROUP BY + pris.rm_item_code, pris.batch_no + """, (purchase_order, fg_item), as_dict=1) + + for batch_data in backflushed_batches: + backflushed_batch_qty_map.setdefault((batch_data.item_code, fg_item), {}) + backflushed_batch_qty_map[(batch_data.item_code, fg_item)][batch_data.batch_no] = batch_data.qty + + return backflushed_batch_qty_map + +def get_batches_with_qty(item_code, fg_item, required_qty, transferred_batch_qty_map, backflushed_batch_qty_map): + # Returns available batches to be backflushed based on requirements + transferred_batches = transferred_batch_qty_map.get((item_code, fg_item), {}) + backflushed_batches = backflushed_batch_qty_map.get((item_code, fg_item), {}) + + available_batches = [] + + for (batch, transferred_qty) in transferred_batches.items(): + backflushed_qty = backflushed_batches.get(batch, 0) + available_qty = transferred_qty - backflushed_qty + + if available_qty >= required_qty: + available_batches.append({'batch': batch, 'qty': required_qty}) + break + else: + available_batches.append({'batch': batch, 'qty': available_qty}) + required_qty -= available_qty + + return available_batches \ No newline at end of file diff --git a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py index 2ca4d16a07c..31a9fdb28ab 100644 --- a/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py +++ b/erpnext/manufacturing/doctype/bom_update_tool/bom_update_tool.py @@ -9,6 +9,7 @@ from frappe import _ from six import string_types from erpnext.manufacturing.doctype.bom.bom import get_boms_in_bottom_up_order from frappe.model.document import Document +import click class BOMUpdateTool(Document): def replace_bom(self): @@ -17,7 +18,8 @@ class BOMUpdateTool(Document): frappe.cache().delete_key('bom_children') bom_list = self.get_parent_boms(self.new_bom) updated_bom = [] - + with click.progressbar(bom_list) as bom_list: + pass for bom in bom_list: try: bom_obj = frappe.get_cached_doc('BOM', bom) diff --git a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json index caf7eb88634..48c0f423fd9 100644 --- a/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json +++ b/erpnext/stock/report/purchase_order_items_to_be_received_or_billed/purchase_order_items_to_be_received_or_billed.json @@ -15,7 +15,7 @@ "prepared_report": 0, "query": "SELECT\n\t`poi_pri`.`purchase_order` as \"Purchase Order:Link/Purchase Order:120\",\n\t`poi_pri`.`status` as \"Status:Data:120\",\n\t`poi_pri`.`transaction_date` as \"Date:Date:100\",\n\t`poi_pri`.`schedule_date` as \"Reqd by Date:Date:110\",\n\t`poi_pri`.`supplier` as \"Supplier:Link/Supplier:120\",\n\t`poi_pri`.`supplier_name` as \"Supplier Name::150\",\n\t`poi_pri`.`item_code` as \"Item Code:Link/Item:120\",\n\t`poi_pri`.`qty` as \"Qty:Float:100\",\n\t`poi_pri`.`base_amount` as \"Base Amount:Currency:100\",\n\t`poi_pri`.`received_qty` as \"Received Qty:Float:100\",\n\t`poi_pri`.`received_amount` as \"Received Qty Amount:Currency:100\",\n\t`poi_pri`.`qty_to_receive` as \"Qty to Receive:Float:100\",\n\t`poi_pri`.`amount_to_be_received` as \"Amount to Receive:Currency:100\",\n\t`poi_pri`.`billed_amount` as \"Billed Amount:Currency:100\",\n\t`poi_pri`.`amount_to_be_billed` as \"Amount To Be Billed:Currency:100\",\n\tSUM(`pii`.`qty`) AS \"Billed Qty:Float:100\",\n\t`poi_pri`.qty - SUM(`pii`.`qty`) AS \"Qty To Be Billed:Float:100\",\n\t`poi_pri`.`warehouse` as \"Warehouse:Link/Warehouse:150\",\n\t`poi_pri`.`item_name` as \"Item Name::150\",\n\t`poi_pri`.`description` as \"Description::200\",\n\t`poi_pri`.`brand` as \"Brand::100\",\n\t`poi_pri`.`project` as \"Project\",\n\t`poi_pri`.`company` as \"Company:Link/Company:\"\nFROM\n\t(SELECT\n\t\t`po`.`name` AS 'purchase_order',\n\t\t`po`.`status`,\n\t\t`po`.`company`,\n\t\t`poi`.`warehouse`,\n\t\t`poi`.`brand`,\n\t\t`poi`.`description`,\n\t\t`po`.`transaction_date`,\n\t\t`poi`.`schedule_date`,\n\t\t`po`.`supplier`,\n\t\t`po`.`supplier_name`,\n\t\t`poi`.`project`,\n\t\t`poi`.`item_code`,\n\t\t`poi`.`item_name`,\n\t\t`poi`.`qty`,\n\t\t`poi`.`base_amount`,\n\t\t`poi`.`received_qty`,\n\t\t(`poi`.billed_amt * ifnull(`po`.conversion_rate, 1)) as billed_amount,\n\t\t(`poi`.base_amount - (`poi`.billed_amt * ifnull(`po`.conversion_rate, 1))) as amount_to_be_billed,\n\t\t`poi`.`qty` - IFNULL(`poi`.`received_qty`, 0) AS 'qty_to_receive',\n\t\t(`poi`.`qty` - IFNULL(`poi`.`received_qty`, 0)) * `poi`.`rate` AS 'amount_to_be_received',\n\t\tSUM(`pri`.`amount`) AS 'received_amount',\n\t\t`poi`.`name` AS 'poi_name',\n\t\t`pri`.`name` AS 'pri_name'\n\tFROM\n\t\t`tabPurchase Order` po\n\t\tLEFT JOIN `tabPurchase Order Item` poi\n\t\tON `poi`.`parent` = `po`.`name`\n\t\tLEFT JOIN `tabPurchase Receipt Item` pri\n\t\tON `pri`.`purchase_order_item` = `poi`.`name`\n\t\t\tAND `pri`.`docstatus`=1\n\tWHERE\n\t\t`po`.`status` not in ('Stopped', 'Closed')\n\t\tAND `po`.`docstatus` = 1\n\t\tAND IFNULL(`poi`.`received_qty`, 0) < IFNULL(`poi`.`qty`, 0)\n\tGROUP BY `poi`.`name`\n\tORDER BY `po`.`transaction_date` ASC\n\t) poi_pri\n\tLEFT JOIN `tabPurchase Invoice Item` pii\n\tON `pii`.`po_detail` = `poi_pri`.`poi_name`\n\t\tAND `pii`.`docstatus`=1\nGROUP BY `poi_pri`.`poi_name`", "ref_doctype": "Purchase Order", - "report_name": "Purchase Order Items To Be Received or Billed1", + "report_name": "Purchase Order Items To Be Received or Billed", "report_type": "Query Report", "roles": [ { diff --git a/erpnext/stock/report/stock_ledger/stock_ledger.py b/erpnext/stock/report/stock_ledger/stock_ledger.py index db7f6ad1b9c..d757ecb293d 100644 --- a/erpnext/stock/report/stock_ledger/stock_ledger.py +++ b/erpnext/stock/report/stock_ledger/stock_ledger.py @@ -122,8 +122,8 @@ def get_item_details(items, sl_entries, include_uom): cf_field = cf_join = "" if include_uom: cf_field = ", ucd.conversion_factor" - cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom='%s'" \ - % (include_uom) + cf_join = "left join `tabUOM Conversion Detail` ucd on ucd.parent=item.name and ucd.uom=%s" \ + % frappe.db.escape(include_uom) res = frappe.db.sql(""" select From 4f95e5d092bd9abdd69baca96fa54df171a78ac7 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 29 Nov 2019 14:22:52 +0530 Subject: [PATCH 225/299] fix: show create payment request for so that are not billed --- erpnext/accounts/doctype/payment_request/payment_request.py | 4 ++-- erpnext/selling/doctype/sales_order/sales_order.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/payment_request/payment_request.py b/erpnext/accounts/doctype/payment_request/payment_request.py index eda59abf04e..6133b1ccd4b 100644 --- a/erpnext/accounts/doctype/payment_request/payment_request.py +++ b/erpnext/accounts/doctype/payment_request/payment_request.py @@ -350,13 +350,13 @@ def get_amount(ref_doc): if dt in ["Sales Order", "Purchase Order"]: grand_total = flt(ref_doc.grand_total) - flt(ref_doc.advance_paid) - if dt in ["Sales Invoice", "Purchase Invoice"]: + elif dt in ["Sales Invoice", "Purchase Invoice"]: if ref_doc.party_account_currency == ref_doc.currency: grand_total = flt(ref_doc.outstanding_amount) else: grand_total = flt(ref_doc.outstanding_amount) / ref_doc.conversion_rate - if dt == "Fees": + elif dt == "Fees": grand_total = ref_doc.outstanding_amount if grand_total > 0 : diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 85e81436d16..7dc58b582ac 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -202,7 +202,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } // payment request - if(flt(doc.per_billed)==0) { + if(flt(doc.per_billed)<100) { this.frm.add_custom_button(__('Payment Request'), () => this.make_payment_request(), __('Create')); this.frm.add_custom_button(__('Payment'), () => this.make_payment_entry(), __('Create')); } From da5e227ad6da284498dcd3c901faa8cf482ce58d Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Sun, 1 Dec 2019 10:06:31 +0530 Subject: [PATCH 226/299] fix: Post GL entry fix for asset (#19752) --- erpnext/assets/doctype/asset/asset.py | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index 40f1e1efc9b..d32f834f0f4 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -517,15 +517,18 @@ def update_maintenance_status(): asset.set_status('Out of Order') def make_post_gl_entry(): - if not is_cwip_accounting_enabled(self.asset_category): - return - assets = frappe.db.sql_list(""" select name from `tabAsset` - where ifnull(booked_fixed_asset, 0) = 0 and available_for_use_date = %s""", nowdate()) + asset_categories = frappe.db.get_all('Asset Category', fields = ['name', 'enable_cwip_accounting']) - for asset in assets: - doc = frappe.get_doc('Asset', asset) - doc.make_gl_entries() + for asset_category in asset_categories: + if cint(asset_category.enable_cwip_accounting): + assets = frappe.db.sql_list(""" select name from `tabAsset` + where asset_category = %s and ifnull(booked_fixed_asset, 0) = 0 + and available_for_use_date = %s""", (asset_category.name, nowdate())) + + for asset in assets: + doc = frappe.get_doc('Asset', asset) + doc.make_gl_entries() def get_asset_naming_series(): meta = frappe.get_meta('Asset') From 9b64e2e24cfe3427deab21c5800c0664daeae9cb Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sun, 1 Dec 2019 21:53:32 +0530 Subject: [PATCH 227/299] fix: Available stock for packing item report --- .../available_stock_for_packing_items.py | 27 ++++++++++--------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py index 32711b2fce0..056492a3274 100644 --- a/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py +++ b/erpnext/selling/report/available_stock_for_packing_items/available_stock_for_packing_items.py @@ -7,7 +7,7 @@ from frappe.utils import flt def execute(filters=None): if not filters: filters = {} - + columns = get_columns() iwq_map = get_item_warehouse_quantity_map() item_map = get_item_details() @@ -15,22 +15,23 @@ def execute(filters=None): for sbom, warehouse in iwq_map.items(): total = 0 total_qty = 0 - + for wh, item_qty in warehouse.items(): total += 1 - row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, - item_map.get(sbom).stock_uom, wh] - available_qty = item_qty - total_qty += flt(available_qty) - row += [available_qty] - - if available_qty: - data.append(row) - if (total == len(warehouse)): - row = ["", "", "Total", "", "", total_qty] + if item_map.get(sbom): + row = [sbom, item_map.get(sbom).item_name, item_map.get(sbom).description, + item_map.get(sbom).stock_uom, wh] + available_qty = item_qty + total_qty += flt(available_qty) + row += [available_qty] + + if available_qty: data.append(row) + if (total == len(warehouse)): + row = ["", "", "Total", "", "", total_qty] + data.append(row) return columns, data - + def get_columns(): columns = ["Item Code:Link/Item:100", "Item Name::100", "Description::120", \ "UOM:Link/UOM:80", "Warehouse:Link/Warehouse:100", "Quantity::100"] From 586fecfe73d165bd597eab7f88fc6031c937c164 Mon Sep 17 00:00:00 2001 From: Rucha Mahabal Date: Mon, 2 Dec 2019 16:25:29 +0530 Subject: [PATCH 228/299] fix: render_template for subject in Email Campaign (#19771) --- erpnext/crm/doctype/email_campaign/email_campaign.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/crm/doctype/email_campaign/email_campaign.py b/erpnext/crm/doctype/email_campaign/email_campaign.py index 3050d05a7c8..00a4bd1a322 100644 --- a/erpnext/crm/doctype/email_campaign/email_campaign.py +++ b/erpnext/crm/doctype/email_campaign/email_campaign.py @@ -41,7 +41,8 @@ class EmailCampaign(Document): email_campaign_exists = frappe.db.exists("Email Campaign", { "campaign_name": self.campaign_name, "recipient": self.recipient, - "status": ("in", ["In Progress", "Scheduled"]) + "status": ("in", ["In Progress", "Scheduled"]), + "name": ("!=", self.name) }) if email_campaign_exists: frappe.throw(_("The Campaign '{0}' already exists for the {1} '{2}'").format(self.campaign_name, self.email_campaign_for, self.recipient)) @@ -78,7 +79,7 @@ def send_mail(entry, email_campaign): comm = make( doctype = "Email Campaign", name = email_campaign.name, - subject = email_template.get("subject"), + subject = frappe.render_template(email_template.get("subject"), context), content = frappe.render_template(email_template.get("response"), context), sender = sender, recipients = recipient, From d6d9a3ddd77fa2ac7bcea88cd7a3848fe6c37fe8 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Dec 2019 12:52:12 +0530 Subject: [PATCH 229/299] Update employee.py --- erpnext/hr/doctype/employee/employee.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/erpnext/hr/doctype/employee/employee.py b/erpnext/hr/doctype/employee/employee.py index 703ec06f83b..2f88e1e3631 100755 --- a/erpnext/hr/doctype/employee/employee.py +++ b/erpnext/hr/doctype/employee/employee.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe -from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime +from frappe.utils import getdate, validate_email_address, today, add_years, format_datetime, cstr from frappe.model.naming import set_name_by_naming_series from frappe import throw, _, scrub from frappe.permissions import add_user_permission, remove_user_permission, \ @@ -218,8 +218,8 @@ class Employee(NestedSet): def reset_employee_emails_cache(self): prev_doc = self.get_doc_before_save() or {} - cell_number = self.get('cell_number') - prev_number = prev_doc.get('cell_number') + cell_number = cstr(self.get('cell_number')) + prev_number = cstr(prev_doc.get('cell_number')) if (cell_number != prev_number or self.get('user_id') != prev_doc.get('user_id')): frappe.cache().hdel('employees_with_number', cell_number) From 35effe9be0608c86417d4365fbe1a48cf7fce459 Mon Sep 17 00:00:00 2001 From: gavin Date: Tue, 3 Dec 2019 12:54:18 +0530 Subject: [PATCH 231/299] fix: AttributeError on new Student creation (#19787) --- erpnext/education/doctype/student/student.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/education/doctype/student/student.py b/erpnext/education/doctype/student/student.py index 9af5e22913a..76825cec1b2 100644 --- a/erpnext/education/doctype/student/student.py +++ b/erpnext/education/doctype/student/student.py @@ -40,7 +40,7 @@ class Student(Document): frappe.throw(_("Student {0} exist against student applicant {1}").format(student[0][0], self.student_applicant)) def after_insert(self): - if not frappe.get_single('Education Settings').user_creation_skip: + if not frappe.get_single('Education Settings').get('user_creation_skip'): self.create_student_user() def create_student_user(self): From 0e1ef35968e5a42fc5f698b822526d05ad0fc2a4 Mon Sep 17 00:00:00 2001 From: Marica Date: Tue, 3 Dec 2019 12:59:15 +0530 Subject: [PATCH 232/299] fix: Item qty cannot be zero in Purchase Receipt (#19780) --- erpnext/controllers/accounts_controller.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 1f8b6635958..4e0dd6f1e61 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -415,9 +415,10 @@ class AccountsController(TransactionBase): return gl_dict def validate_qty_is_not_zero(self): - for item in self.items: - if not item.qty: - frappe.throw(_("Item quantity can not be zero")) + if self.doctype != "Purchase Receipt": + for item in self.items: + if not item.qty: + frappe.throw(_("Item quantity can not be zero")) def validate_account_currency(self, account, account_currency=None): valid_currency = [self.company_currency] From 485d48c1018f3de4a2929e4b96307e187f6c8725 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 3 Dec 2019 15:12:28 +0530 Subject: [PATCH 233/299] fix: Unable to see parties with negative balance in AR/AP Summary (#19777) --- .../accounts_receivable_summary/accounts_receivable_summary.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py index 8955830e09b..b607c0f7028 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.py @@ -36,7 +36,7 @@ class AccountsReceivableSummary(ReceivablePayableReport): self.filters.report_date) or {} for party, party_dict in iteritems(self.party_total): - if party_dict.outstanding <= 0: + if party_dict.outstanding == 0: continue row = frappe._dict() From 53746636c3b4a692199ee9ae55fe1f786e52bf38 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 3 Dec 2019 16:30:09 +0530 Subject: [PATCH 234/299] fix: Party name field in trial balacne for party report (#19790) --- .../trial_balance_for_party.py | 21 +++++++++++-------- 1 file changed, 12 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py index bd2c34b3b4c..3e47906a98b 100644 --- a/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py +++ b/erpnext/accounts/report/trial_balance_for_party/trial_balance_for_party.py @@ -18,14 +18,17 @@ def execute(filters=None): return columns, data def get_data(filters, show_party_name): - party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) + if filters.get('party_type') in ('Customer', 'Supplier', 'Employee', 'Member'): + party_name_field = "{0}_name".format(frappe.scrub(filters.get('party_type'))) if filters.get('party_type') == 'Student': party_name_field = 'first_name' elif filters.get('party_type') == 'Shareholder': party_name_field = 'title' + else: + party_name_field = 'name' party_filters = {"name": filters.get("party")} if filters.get("party") else {} - parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], + parties = frappe.get_all(filters.get("party_type"), fields = ["name", party_name_field], filters = party_filters, order_by="name") company_currency = frappe.get_cached_value('Company', filters.company, "default_currency") opening_balances = get_opening_balances(filters) @@ -70,7 +73,7 @@ def get_data(filters, show_party_name): # totals for col in total_row: total_row[col] += row.get(col) - + row.update({ "currency": company_currency }) @@ -78,7 +81,7 @@ def get_data(filters, show_party_name): has_value = False if (opening_debit or opening_credit or debit or credit or closing_debit or closing_credit): has_value =True - + if cint(filters.show_zero_values) or has_value: data.append(row) @@ -94,9 +97,9 @@ def get_data(filters, show_party_name): def get_opening_balances(filters): gle = frappe.db.sql(""" - select party, sum(debit) as opening_debit, sum(credit) as opening_credit + select party, sum(debit) as opening_debit, sum(credit) as opening_credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' and (posting_date < %(from_date)s or ifnull(is_opening, 'No') = 'Yes') group by party""", { @@ -114,11 +117,11 @@ def get_opening_balances(filters): def get_balances_within_period(filters): gle = frappe.db.sql(""" - select party, sum(debit) as debit, sum(credit) as credit + select party, sum(debit) as debit, sum(credit) as credit from `tabGL Entry` - where company=%(company)s + where company=%(company)s and ifnull(party_type, '') = %(party_type)s and ifnull(party, '') != '' - and posting_date >= %(from_date)s and posting_date <= %(to_date)s + and posting_date >= %(from_date)s and posting_date <= %(to_date)s and ifnull(is_opening, 'No') = 'No' group by party""", { "company": filters.company, From 6e5363ba4850b77d3a21388f4f51f6c700992c7a Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Tue, 3 Dec 2019 16:56:48 +0530 Subject: [PATCH 235/299] feat: Receivable / payable summary based on payment terms --- .../accounts_payable_summary/accounts_payable_summary.js | 5 +++++ .../accounts_receivable_summary.js | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js index 5f0fdc9f2c7..4a9f1b0dc44 100644 --- a/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js +++ b/erpnext/accounts/report/accounts_payable_summary/accounts_payable_summary.js @@ -88,6 +88,11 @@ frappe.query_reports["Accounts Payable Summary"] = { "label": __("Supplier Group"), "fieldtype": "Link", "options": "Supplier Group" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], diff --git a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js index 0120608a8ff..d54824b6855 100644 --- a/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js +++ b/erpnext/accounts/report/accounts_receivable_summary/accounts_receivable_summary.js @@ -106,6 +106,11 @@ frappe.query_reports["Accounts Receivable Summary"] = { "label": __("Sales Person"), "fieldtype": "Link", "options": "Sales Person" + }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", } ], From aa7085e11c21469a9a1547582e2dfcb41cc8b271 Mon Sep 17 00:00:00 2001 From: sahil28297 <37302950+sahil28297@users.noreply.github.com> Date: Tue, 3 Dec 2019 17:07:26 +0530 Subject: [PATCH 236/299] fix(patch): set proper tax_type based on company and set proper account if not already present (#19788) --- .../move_item_tax_to_item_tax_template.py | 45 ++++++++++++++----- 1 file changed, 33 insertions(+), 12 deletions(-) diff --git a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py index f25b9eaf521..e47344bd92c 100644 --- a/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py +++ b/erpnext/patches/v12_0/move_item_tax_to_item_tax_template.py @@ -62,12 +62,12 @@ def execute(): ] for dt in doctypes: - for d in frappe.db.sql("""select name, parent, item_code, item_tax_rate from `tab{0} Item` + for d in frappe.db.sql("""select name, parenttype, parent, item_code, item_tax_rate from `tab{0} Item` where ifnull(item_tax_rate, '') not in ('', '{{}}') and item_tax_template is NULL""".format(dt), as_dict=1): item_tax_map = json.loads(d.item_tax_rate) item_tax_template_name = get_item_tax_template(item_tax_templates, - item_tax_map, d.item_code, d.parent) + item_tax_map, d.item_code, d.parenttype, d.parent) frappe.db.set_value(dt + " Item", d.name, "item_tax_template", item_tax_template_name) frappe.db.auto_commit_on_many_writes = False @@ -77,7 +77,7 @@ def execute(): settings.determine_address_tax_category_from = "Billing Address" settings.save() -def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=None): +def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parenttype=None, parent=None): # search for previously created item tax template by comparing tax maps for template, item_tax_template_map in iteritems(item_tax_templates): if item_tax_map == item_tax_template_map: @@ -88,23 +88,44 @@ def get_item_tax_template(item_tax_templates, item_tax_map, item_code, parent=No item_tax_template.title = make_autoname("Item Tax Template-.####") for tax_type, tax_rate in iteritems(item_tax_map): - if not frappe.db.exists("Account", tax_type): + account_details = frappe.db.get_value("Account", tax_type, ['name', 'account_type'], as_dict=1) + if account_details: + if account_details.account_type not in ('Tax', 'Chargeable', 'Income Account', 'Expense Account', 'Expenses Included In Valuation'): + frappe.db.set_value('Account', account_details.name, 'account_type', 'Chargeable') + else: parts = tax_type.strip().split(" - ") account_name = " - ".join(parts[:-1]) - company = frappe.db.get_value("Company", filters={"abbr": parts[-1]}) + company = get_company(parts[-1], parenttype, parent) parent_account = frappe.db.get_value("Account", filters={"account_type": "Tax", "root_type": "Liability", "is_group": 0, "company": company}, fieldname="parent_account") - - frappe.get_doc({ - "doctype": "Account", + filters = { "account_name": account_name, - "company": company, - "account_type": "Tax", - "parent_account": parent_account - }).insert() + "company": company, + "account_type": "Tax", + "parent_account": parent_account + } + tax_type = frappe.db.get_value("Account", filters) + if not tax_type: + account = frappe.new_doc("Account") + account.update(filters) + account.insert() + tax_type = account.name item_tax_template.append("taxes", {"tax_type": tax_type, "tax_rate": tax_rate}) item_tax_templates.setdefault(item_tax_template.title, {}) item_tax_templates[item_tax_template.title][tax_type] = tax_rate item_tax_template.save() return item_tax_template.name + +def get_company(company_abbr, parenttype=None, parent=None): + if parenttype and parent: + company = frappe.get_cached_value(parenttype, parent, 'company') + else: + company = frappe.db.get_value("Company", filters={"abbr": company_abbr}) + + if not company: + companies = frappe.get_all('Company') + if len(companies) == 1: + company = companies[0].name + + return company From 27a21f80d7a987d690de3152e38f34919dfc767e Mon Sep 17 00:00:00 2001 From: Shivam Mishra Date: Tue, 3 Dec 2019 17:26:50 +0530 Subject: [PATCH 237/299] feat: allow searching from meta fields (#19725) * feat: allow searching from meta fields * feat: remove description in query based on number of items --- erpnext/portal/product_configurator/utils.py | 24 +++++++++++++++----- 1 file changed, 18 insertions(+), 6 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 61c50e5fe01..3a373a4ab19 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -313,13 +313,25 @@ def get_items(filters=None, search=None): search_condition = '' if search: + # Default fields to search from + default_fields = {'name', 'item_name', 'description', 'item_group'} + + # Get meta search fields + meta = frappe.get_meta("Item") + meta_fields = set(meta.get_search_fields()) + + # Join the meta fields and default fields set + search_fields = default_fields.union(meta_fields) + try: + if frappe.db.count('Item', cache=True) > 50000: + search_fields.remove('description') + except KeyError: + pass + + # Build or filters for query search = '%{}%'.format(search) - or_filters = [ - ['name', 'like', search], - ['item_name', 'like', search], - ['description', 'like', search], - ['item_group', 'like', search] - ] + or_filters = [[field, 'like', search] for field in search_fields] + search_condition = get_conditions(or_filters, 'or') filter_condition = get_conditions(filters, 'and') From 929fd4ce473d038f12e57d935e09c059da4c3f9d Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Wed, 4 Dec 2019 14:10:41 +0530 Subject: [PATCH 238/299] enhancement(fixed-asset-register): add date filter (#19804) * feat: add date filter in the fixed asset register * fix: remove function from keyword argument --- .../fixed_asset_register.js | 6 ++++ .../fixed_asset_register.py | 29 ++++++++++++------- 2 files changed, 24 insertions(+), 11 deletions(-) diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js index 426caaad92d..8c737d066b9 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.js @@ -26,5 +26,11 @@ frappe.query_reports["Fixed Asset Register"] = { fieldtype: "Link", options: "Finance Book" }, + { + fieldname:"date", + label: __("Date"), + fieldtype: "Date", + default: frappe.datetime.get_today() + }, ] }; diff --git a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py index f395499ad6b..57b68b4ed24 100644 --- a/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py +++ b/erpnext/assets/report/fixed_asset_register/fixed_asset_register.py @@ -4,7 +4,7 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import cstr +from frappe.utils import cstr, today, flt def execute(filters=None): filters = frappe._dict(filters or {}) @@ -86,8 +86,8 @@ def get_columns(filters): "width": 90 }, { - "label": _("Current Value"), - "fieldname": "current_value", + "label": _("Asset Value"), + "fieldname": "asset_value", "options": "Currency", "width": 90 }, @@ -114,7 +114,7 @@ def get_data(filters): data = [] conditions = get_conditions(filters) - current_value_map = get_finance_book_value_map(filters.finance_book) + depreciation_amount_map = get_finance_book_value_map(filters.date, filters.finance_book) pr_supplier_map = get_purchase_receipt_supplier_map() pi_supplier_map = get_purchase_invoice_supplier_map() @@ -125,7 +125,9 @@ def get_data(filters): "available_for_use_date", "status", "purchase_invoice"]) for asset in assets_record: - if current_value_map.get(asset.name) is not None: + asset_value = asset.gross_purchase_amount - flt(asset.opening_accumulated_depreciation) \ + - flt(depreciation_amount_map.get(asset.name)) + if asset_value: row = { "asset_id": asset.name, "asset_name": asset.asset_name, @@ -138,19 +140,24 @@ def get_data(filters): "location": asset.location, "asset_category": asset.asset_category, "purchase_date": asset.purchase_date, - "current_value": current_value_map.get(asset.name) + "asset_value": asset_value } data.append(row) return data -def get_finance_book_value_map(finance_book=''): +def get_finance_book_value_map(date, finance_book=''): + if not date: + date = today() return frappe._dict(frappe.db.sql(''' Select - parent, value_after_depreciation - FROM `tabAsset Finance Book` + parent, SUM(depreciation_amount) + FROM `tabDepreciation Schedule` WHERE - parentfield='finance_books' - AND ifnull(finance_book, '')=%s''', cstr(finance_book))) + parentfield='schedules' + AND schedule_date<=%s + AND journal_entry IS NOT NULL + AND ifnull(finance_book, '')=%s + GROUP BY parent''', (date, cstr(finance_book)))) def get_purchase_receipt_supplier_map(): return frappe._dict(frappe.db.sql(''' Select From bf0f26b4a4d789be0d313d153efc50a9c5f9a186 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:29:54 +0530 Subject: [PATCH 239/299] fix: Service start and end date validation for deferred accounting (#19806) --- .../purchase_invoice_item.json | 13 +++++++++---- .../sales_invoice_item/sales_invoice_item.json | 13 +++++++++---- erpnext/controllers/accounts_controller.py | 13 ++++++++++++- 3 files changed, 30 insertions(+), 9 deletions(-) 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 27d8233a44b..acb0398b5c0 100644 --- a/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json +++ b/erpnext/accounts/doctype/purchase_invoice_item/purchase_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-05-22 12:43:10", "doctype": "DocType", @@ -507,7 +508,8 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -523,13 +525,15 @@ "depends_on": "enable_deferred_expense", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_expense", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "fieldname": "reference", @@ -766,7 +770,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-11-21 16:27:52.043744", + "links": [], + "modified": "2019-12-04 12:23:17.046413", "modified_by": "Administrator", "module": "Accounts", "name": "Purchase Invoice Item", diff --git a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json index 779ac4f656c..b2294e4318f 100644 --- a/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json +++ b/erpnext/accounts/doctype/sales_invoice_item/sales_invoice_item.json @@ -1,4 +1,5 @@ { + "actions": [], "autoname": "hash", "creation": "2013-06-04 11:02:19", "doctype": "DocType", @@ -484,7 +485,8 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_stop_date", "fieldtype": "Date", - "label": "Service Stop Date" + "label": "Service Stop Date", + "no_copy": 1 }, { "default": "0", @@ -500,13 +502,15 @@ "depends_on": "enable_deferred_revenue", "fieldname": "service_start_date", "fieldtype": "Date", - "label": "Service Start Date" + "label": "Service Start Date", + "no_copy": 1 }, { "depends_on": "enable_deferred_revenue", "fieldname": "service_end_date", "fieldtype": "Date", - "label": "Service End Date" + "label": "Service End Date", + "no_copy": 1 }, { "collapsible": 1, @@ -783,7 +787,8 @@ ], "idx": 1, "istable": 1, - "modified": "2019-07-16 16:36:46.527606", + "links": [], + "modified": "2019-12-04 12:22:38.517710", "modified_by": "Administrator", "module": "Accounts", "name": "Sales Invoice Item", diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 4e0dd6f1e61..75564afe59e 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -61,7 +61,6 @@ class AccountsController(TransactionBase): _('{0} is blocked so this transaction cannot proceed'.format(supplier_name)), raise_exception=1) def validate(self): - if not self.get('is_return'): self.validate_qty_is_not_zero() @@ -100,11 +99,23 @@ class AccountsController(TransactionBase): if self.is_return: self.validate_qty() + else: + self.validate_deferred_start_and_end_date() validate_regional(self) if self.doctype != 'Material Request': apply_pricing_rule_on_transaction(self) + def validate_deferred_start_and_end_date(self): + for d in self.items: + if d.get("enable_deferred_revenue") or d.get("enable_deferred_expense"): + if not (d.service_start_date and d.service_end_date): + frappe.throw(_("Row #{0}: Service Start and End Date is required for deferred accounting").format(d.idx)) + elif getdate(d.service_start_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service Start Date cannot be greater than Service End Date").format(d.idx)) + elif getdate(self.posting_date) > getdate(d.service_end_date): + frappe.throw(_("Row #{0}: Service End Date cannot be before Invoice Posting Date").format(d.idx)) + def validate_invoice_documents_schedule(self): self.validate_payment_schedule_dates() self.set_due_date() From dc248b9458e1e9906e8049b6d521328df2824205 Mon Sep 17 00:00:00 2001 From: Nabin Hait Date: Wed, 4 Dec 2019 15:30:39 +0530 Subject: [PATCH 240/299] optimize: Optimization of Receivable report filtered based on sales person (#19797) --- .../accounts_receivable.py | 42 +++++++++++++------ 1 file changed, 30 insertions(+), 12 deletions(-) diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index 14906f2c2e6..e6ff5d97349 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -60,6 +60,7 @@ class ReceivablePayableReport(object): def get_data(self): self.get_gl_entries() + self.get_sales_invoices_or_customers_based_on_sales_person() self.voucher_balance = OrderedDict() self.init_voucher_balance() # invoiced, paid, credit_note, outstanding @@ -103,12 +104,18 @@ class ReceivablePayableReport(object): def get_invoices(self, gle): if gle.voucher_type in ('Sales Invoice', 'Purchase Invoice'): - self.invoices.add(gle.voucher_no) + if self.filters.get("sales_person"): + if gle.voucher_no in self.sales_person_records.get("Sales Invoice", []) \ + or gle.party in self.sales_person_records.get("Customer", []): + self.invoices.add(gle.voucher_no) + else: + self.invoices.add(gle.voucher_no) def update_voucher_balance(self, gle): # get the row where this balance needs to be updated # if its a payment, it will return the linked invoice or will be considered as advance row = self.get_voucher_balance(gle) + if not row: return # gle_balance will be the total "debit - credit" for receivable type reports and # and vice-versa for payable type reports gle_balance = self.get_gle_balance(gle) @@ -129,8 +136,13 @@ class ReceivablePayableReport(object): row.paid -= gle_balance def get_voucher_balance(self, gle): - voucher_balance = None + if self.filters.get("sales_person"): + against_voucher = gle.against_voucher or gle.voucher_no + if not (gle.party in self.sales_person_records.get("Customer", []) or \ + against_voucher in self.sales_person_records.get("Sales Invoice", [])): + return + voucher_balance = None if gle.against_voucher: # find invoice against_voucher = gle.against_voucher @@ -512,6 +524,22 @@ class ReceivablePayableReport(object): order by posting_date, party""" .format(select_fields, conditions), values, as_dict=True) + def get_sales_invoices_or_customers_based_on_sales_person(self): + if self.filters.get("sales_person"): + lft, rgt = frappe.db.get_value("Sales Person", + self.filters.get("sales_person"), ["lft", "rgt"]) + + records = frappe.db.sql(""" + select distinct parent, parenttype + from `tabSales Team` steam + where parenttype in ('Customer', 'Sales Invoice') + and exists(select name from `tabSales Person` where lft >= %s and rgt <= %s and name = steam.sales_person) + """, (lft, rgt), as_dict=1) + + self.sales_person_records = frappe._dict() + for d in records: + self.sales_person_records.setdefault(d.parenttype, set()).add(d.parent) + def prepare_conditions(self): conditions = [""] values = [self.party_type, self.filters.report_date] @@ -564,16 +592,6 @@ class ReceivablePayableReport(object): conditions.append("party in (select name from tabCustomer where default_sales_partner=%s)") values.append(self.filters.get("sales_partner")) - if self.filters.get("sales_person"): - lft, rgt = frappe.db.get_value("Sales Person", - self.filters.get("sales_person"), ["lft", "rgt"]) - - conditions.append("""exists(select name from `tabSales Team` steam where - steam.sales_person in (select name from `tabSales Person` where lft >= {0} and rgt <= {1}) - and ((steam.parent = voucher_no and steam.parenttype = voucher_type) - or (steam.parent = against_voucher and steam.parenttype = against_voucher_type) - or (steam.parent = party and steam.parenttype = 'Customer')))""".format(lft, rgt)) - def add_supplier_filters(self, conditions, values): if self.filters.get("supplier_group"): conditions.append("""party in (select name from tabSupplier From d16ef54665702efd7f43ba5943b6d54553cfe6a7 Mon Sep 17 00:00:00 2001 From: Pranav Nachnekar Date: Wed, 4 Dec 2019 10:01:25 +0000 Subject: [PATCH 241/299] fix: query for finding lost quotation (#19801) * fix:query for finding lost quotation * Update opportunity.py --- erpnext/crm/doctype/opportunity/opportunity.py | 9 +++++---- erpnext/selling/doctype/quotation/quotation.py | 1 - 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/erpnext/crm/doctype/opportunity/opportunity.py b/erpnext/crm/doctype/opportunity/opportunity.py index 99486fa2066..4e0eebd0de9 100644 --- a/erpnext/crm/doctype/opportunity/opportunity.py +++ b/erpnext/crm/doctype/opportunity/opportunity.py @@ -130,10 +130,11 @@ class Opportunity(TransactionBase): def has_lost_quotation(self): lost_quotation = frappe.db.sql(""" - select q.name - from `tabQuotation` q, `tabQuotation Item` qi - where q.name = qi.parent and q.docstatus=1 - and qi.prevdoc_docname =%s and q.status = 'Lost' + select name + from `tabQuotation` + where docstatus=1 + and opportunity =%s + and status = 'Lost' """, self.name) if lost_quotation: if self.has_active_quotation(): diff --git a/erpnext/selling/doctype/quotation/quotation.py b/erpnext/selling/doctype/quotation/quotation.py index 790b2f0804d..488331a8102 100644 --- a/erpnext/selling/doctype/quotation/quotation.py +++ b/erpnext/selling/doctype/quotation/quotation.py @@ -67,7 +67,6 @@ class Quotation(SellingController): opportunity = self.opportunity opp = frappe.get_doc("Opportunity", opportunity) - opp.status = None opp.set_status(update=True) def declare_enquiry_lost(self, lost_reasons_list, detailed_reason=None): From dabb3033589c88dd1e575b843c4a769f009ba372 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sat, 23 Nov 2019 18:10:16 +0530 Subject: [PATCH 242/299] feat: Accounts Payable report based on payment terms --- erpnext/accounts/report/accounts_payable/accounts_payable.js | 5 +++++ .../report/accounts_receivable/accounts_receivable.py | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/report/accounts_payable/accounts_payable.js b/erpnext/accounts/report/accounts_payable/accounts_payable.js index 8eb670de510..b1f427ca7f6 100644 --- a/erpnext/accounts/report/accounts_payable/accounts_payable.js +++ b/erpnext/accounts/report/accounts_payable/accounts_payable.js @@ -100,6 +100,11 @@ frappe.query_reports["Accounts Payable"] = { "fieldtype": "Link", "options": "Supplier Group" }, + { + "fieldname":"based_on_payment_terms", + "label": __("Based On Payment Terms"), + "fieldtype": "Check", + }, { "fieldname":"tax_id", "label": __("Tax Id"), diff --git a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py index e6ff5d97349..2c53f6e9971 100755 --- a/erpnext/accounts/report/accounts_receivable/accounts_receivable.py +++ b/erpnext/accounts/report/accounts_receivable/accounts_receivable.py @@ -330,7 +330,7 @@ class ReceivablePayableReport(object): self.append_payment_term(row, d, term) def append_payment_term(self, row, d, term): - if self.filters.get("customer") and d.currency == d.party_account_currency: + if (self.filters.get("customer") or self.filters.get("supplier")) and d.currency == d.party_account_currency: invoiced = d.payment_amount else: invoiced = flt(flt(d.payment_amount) * flt(d.conversion_rate), self.currency_precision) From a40dbd03840ff4a9fa2e6d3a542f404ddcb054fe Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 6 Dec 2019 19:34:49 +0530 Subject: [PATCH 243/299] fix: Validation msg fix in GSTR-1 report --- erpnext/regional/report/gstr_1/gstr_1.js | 27 +++++++++++++++------ erpnext/regional/report/gstr_1/gstr_1.py | 31 ++++++++++++------------ 2 files changed, 35 insertions(+), 23 deletions(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 96827682807..43342bfc2ba 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -55,14 +55,25 @@ frappe.query_reports["GSTR-1"] = { report.page.add_inner_button(__("Download as Json"), function () { var filters = report.get_values(); - const args = { - cmd: 'erpnext.regional.report.gstr_1.gstr_1.get_json', - data: report.data, - report_name: report.report_name, - filters: filters - }; - - open_url_post(frappe.request.url, args); + frappe.call({ + method: 'erpnext.regional.report.gstr_1.gstr_1.get_json', + args: { + data: report.data, + report_name: report.report_name, + filters: filters + }, + callback: function(r) { + if (r.message) { + const args = { + cmd: 'erpnext.regional.report.gstr_1.gstr_1.download_json_file', + data: r.message.data, + report_name: r.message.report_name, + report_type: r.message.report_type + }; + open_url_post(frappe.request.url, args); + } + } + }) }); } } diff --git a/erpnext/regional/report/gstr_1/gstr_1.py b/erpnext/regional/report/gstr_1/gstr_1.py index 090616b0777..4f9cc7ff7a0 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.py +++ b/erpnext/regional/report/gstr_1/gstr_1.py @@ -532,16 +532,9 @@ class Gstr1Report(object): self.columns = self.invoice_columns + self.tax_columns + self.other_columns @frappe.whitelist() -def get_json(): - data = frappe._dict(frappe.local.form_dict) - - del data["cmd"] - if "csrf_token" in data: - del data["csrf_token"] - - filters = json.loads(data["filters"]) - report_data = json.loads(data["data"]) - report_name = data["report_name"] +def get_json(filters, report_name, data): + filters = json.loads(filters) + report_data = json.loads(data) gstin = get_company_gstin_number(filters["company"]) fp = "%02d%s" % (getdate(filters["to_date"]).month, getdate(filters["to_date"]).year) @@ -575,7 +568,11 @@ def get_json(): out = get_export_json(res) gst_json["exp"] = out - download_json_file(report_name, filters["type_of_business"], gst_json) + return { + 'report_name': report_name, + 'report_type': filters['type_of_business'], + 'data': gst_json + } def get_b2b_json(res, gstin): inv_type, out = {"Registered Regular": "R", "Deemed Export": "DE", "URD": "URD", "SEZ": "SEZ"}, [] @@ -722,11 +719,15 @@ def get_company_gstin_number(company): if gstin: return gstin[0]["gstin"] else: - frappe.throw(_("Please set valid GSTIN No. in Company Address")) + frappe.throw(_("Please set valid GSTIN No. in Company Address for company {0}".format( + frappe.bold(company) + ))) -def download_json_file(filename, report_type, data): +@frappe.whitelist() +def download_json_file(): ''' download json content in a file ''' - frappe.response['filename'] = frappe.scrub("{0} {1}".format(filename, report_type)) + '.json' - frappe.response['filecontent'] = json.dumps(data) + data = frappe._dict(frappe.local.form_dict) + frappe.response['filename'] = frappe.scrub("{0} {1}".format(data['report_name'], data['report_type'])) + '.json' + frappe.response['filecontent'] = data['data'] frappe.response['content_type'] = 'application/json' frappe.response['type'] = 'download' From 778d7595aa02ddd2e2df006adad5e9dcb5949ff0 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 6 Dec 2019 19:59:54 +0530 Subject: [PATCH 244/299] fix: Add missing semicolon --- erpnext/regional/report/gstr_1/gstr_1.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/regional/report/gstr_1/gstr_1.js b/erpnext/regional/report/gstr_1/gstr_1.js index 43342bfc2ba..80ef3718aa0 100644 --- a/erpnext/regional/report/gstr_1/gstr_1.js +++ b/erpnext/regional/report/gstr_1/gstr_1.js @@ -73,7 +73,7 @@ frappe.query_reports["GSTR-1"] = { open_url_post(frappe.request.url, args); } } - }) + }); }); } } From 495ba1618b93ff16c20c31b6fb8b475af9923493 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Sat, 7 Dec 2019 13:20:37 +0530 Subject: [PATCH 245/299] fix: timsheet overlap error --- erpnext/projects/doctype/timesheet/timesheet.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/erpnext/projects/doctype/timesheet/timesheet.py b/erpnext/projects/doctype/timesheet/timesheet.py index c4481c9aa0e..e90821689bd 100644 --- a/erpnext/projects/doctype/timesheet/timesheet.py +++ b/erpnext/projects/doctype/timesheet/timesheet.py @@ -188,7 +188,8 @@ class Timesheet(Document): }, as_dict=True) # check internal overlap for time_log in self.time_logs: - if not (time_log.from_time or time_log.to_time): continue + if not (time_log.from_time and time_log.to_time + and args.from_time and args.to_time): continue if (fieldname != 'workstation' or args.get(fieldname) == time_log.get(fieldname)) and \ args.idx != time_log.idx and ((args.from_time > time_log.from_time and args.from_time < time_log.to_time) or From 59cc0e5029071924f8c54e0d8ac6eefbe58c161e Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 11:28:35 +0530 Subject: [PATCH 246/299] fix: NoneType' object has no attribute '__getitem_'_ (#19860) --- erpnext/stock/doctype/packed_item/packed_item.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/packed_item/packed_item.py b/erpnext/stock/doctype/packed_item/packed_item.py index 831381c86ad..5341f298531 100644 --- a/erpnext/stock/doctype/packed_item/packed_item.py +++ b/erpnext/stock/doctype/packed_item/packed_item.py @@ -65,7 +65,7 @@ def update_packing_list_item(doc, packing_item_code, qty, main_item_row, descrip bin = get_bin_qty(packing_item_code, pi.warehouse) pi.actual_qty = flt(bin.get("actual_qty")) pi.projected_qty = flt(bin.get("projected_qty")) - if old_packed_items_map: + if old_packed_items_map and old_packed_items_map.get((packing_item_code, main_item_row.item_code)): pi.batch_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].batch_no pi.serial_no = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].serial_no pi.warehouse = old_packed_items_map.get((packing_item_code, main_item_row.item_code))[0].warehouse From 6aec9e32d4ed341b0bb7835d60307db130666719 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:03:02 +0530 Subject: [PATCH 247/299] fix: Rounding Adjustment GL entry fix (#19839) * fix: Rounding Adjustment GL entry fix * fix: Spacing in tab * fix: Comment fix --- .../accounts/doctype/purchase_invoice/purchase_invoice.py | 6 +++++- erpnext/accounts/doctype/sales_invoice/sales_invoice.py | 2 +- 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 3bb3df8dbd9..7b2061ac168 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -830,7 +830,11 @@ class PurchaseInvoice(BuyingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if self.rounding_adjustment: + # if rounding adjustment in small and conversion rate is also small then + # base_rounding_adjustment may become zero due to small precision + # eg: rounding_adjustment = 0.01 and exchange rate = 0.05 and precision of base_rounding_adjustment is 2 + # then base_rounding_adjustment becomes zero and error is thrown in GL Entry + if self.rounding_adjustment and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py index def671c19b7..24024dc3100 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.py +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.py @@ -953,7 +953,7 @@ class SalesInvoice(SellingController): ) def make_gle_for_rounding_adjustment(self, gl_entries): - if flt(self.rounding_adjustment, self.precision("rounding_adjustment")): + if flt(self.rounding_adjustment, self.precision("rounding_adjustment")) and self.base_rounding_adjustment: round_off_account, round_off_cost_center = \ get_round_off_account_and_cost_center(self.company) From 1e2be328601df1eeaeba182156426aebd13806b8 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Mon, 9 Dec 2019 13:04:58 +0530 Subject: [PATCH 248/299] fix: Consistency in button positions in Sales Order and Purchase Order (#19834) --- erpnext/selling/doctype/sales_order/sales_order.js | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/selling/doctype/sales_order/sales_order.js b/erpnext/selling/doctype/sales_order/sales_order.js index 7dc58b582ac..2dae0d8063d 100644 --- a/erpnext/selling/doctype/sales_order/sales_order.js +++ b/erpnext/selling/doctype/sales_order/sales_order.js @@ -112,7 +112,6 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( let allow_delivery = false; if (doc.docstatus==1) { - this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); if(this.frm.has_perm("submit")) { if(doc.status === 'On Hold') { @@ -136,7 +135,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( if(doc.status !== 'Closed') { if(doc.status !== 'On Hold') { - allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) + allow_delivery = this.frm.doc.items.some(item => item.delivered_by_supplier === 0 && item.qty > flt(item.delivered_qty)) && !this.frm.doc.skip_delivery_note if (this.frm.has_perm("submit")) { @@ -148,6 +147,8 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( } } + this.frm.add_custom_button(__('Pick List'), () => this.create_pick_list(), __('Create')); + // delivery note if(flt(doc.per_delivered, 6) < 100 && ["Sales", "Shopping Cart"].indexOf(doc.order_type)!==-1 && allow_delivery) { this.frm.add_custom_button(__('Delivery Note'), () => this.make_delivery_note_based_on_delivery_date(), __('Create')); @@ -361,7 +362,7 @@ erpnext.selling.SalesOrderController = erpnext.selling.SellingController.extend( }, toggle_delivery_date: function() { - this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", + this.frm.fields_dict.items.grid.toggle_reqd("delivery_date", (this.frm.doc.order_type == "Sales" && !this.frm.doc.skip_delivery_note)); }, From f40d3bd10fbb794ff226bff21cf0b0b33375d69b Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 9 Dec 2019 14:07:25 +0530 Subject: [PATCH 249/299] fix: due date before posting date for items added to cart yesterday --- erpnext/shopping_cart/cart.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/shopping_cart/cart.py b/erpnext/shopping_cart/cart.py index 1236ade45f9..813d0dd196f 100644 --- a/erpnext/shopping_cart/cart.py +++ b/erpnext/shopping_cart/cart.py @@ -66,6 +66,7 @@ def place_order(): from erpnext.selling.doctype.quotation.quotation import _make_sales_order sales_order = frappe.get_doc(_make_sales_order(quotation.name, ignore_permissions=True)) + sales_order.payment_schedule = [] if not cint(cart_settings.allow_items_not_in_stock): for item in sales_order.get("items"): From 6d497ccb4c1987397a532a28059d78b128ce62c7 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 9 Dec 2019 15:24:39 +0530 Subject: [PATCH 250/299] fix: column data not visible after manual selection of columns --- erpnext/public/js/financial_statements.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/financial_statements.js b/erpnext/public/js/financial_statements.js index 63e057c39d1..dead309a5d0 100644 --- a/erpnext/public/js/financial_statements.js +++ b/erpnext/public/js/financial_statements.js @@ -4,7 +4,7 @@ erpnext.financial_statements = { "filters": get_filters(), "formatter": function(value, row, column, data, default_formatter) { if (column.fieldname=="account") { - value = data.account_name; + value = data.account_name || value; column.link_onclick = "erpnext.financial_statements.open_general_ledger(" + JSON.stringify(data) + ")"; From f092e68a58f5bebbc798458a8872c320927374d6 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 9 Dec 2019 17:03:32 +0530 Subject: [PATCH 251/299] fix: website showing disabled items in product list --- erpnext/portal/product_configurator/utils.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 3a373a4ab19..ab4b82a688b 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -335,8 +335,10 @@ def get_items(filters=None, search=None): search_condition = get_conditions(or_filters, 'or') filter_condition = get_conditions(filters, 'and') + + where_conditions = 'disabled = 0 and ' - where_conditions = ' and '.join( + where_conditions += ' and '.join( [condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] ) From 34b3b04fb09420611155c295d5c080661e9f08f9 Mon Sep 17 00:00:00 2001 From: Saqib Date: Mon, 9 Dec 2019 19:06:14 +0530 Subject: [PATCH 252/299] fix: error message displays asset category as None (#19874) * fix: error message displays asset category as None * fix: asset gl_entries doesn't considers asset category's cwip account --- erpnext/assets/doctype/asset/asset.py | 10 ++++++++-- .../stock/doctype/purchase_receipt/purchase_receipt.py | 8 +++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/erpnext/assets/doctype/asset/asset.py b/erpnext/assets/doctype/asset/asset.py index d32f834f0f4..3e7f6833a0c 100644 --- a/erpnext/assets/doctype/asset/asset.py +++ b/erpnext/assets/doctype/asset/asset.py @@ -610,13 +610,19 @@ def get_asset_account(account_name, asset=None, asset_category=None, company=Non if asset: account = get_asset_category_account(account_name, asset=asset, asset_category = asset_category, company = company) + + if not asset and not account: + account = get_asset_category_account(account_name, asset_category = asset_category, company = company) if not account: account = frappe.get_cached_value('Company', company, account_name) if not account: - frappe.throw(_("Set {0} in asset category {1} or company {2}") - .format(account_name.replace('_', ' ').title(), asset_category, company)) + if not asset_category: + frappe.throw(_("Set {0} in company {2}").format(account_name.replace('_', ' ').title(), company)) + else: + frappe.throw(_("Set {0} in asset category {1} or company {2}") + .format(account_name.replace('_', ' ').title(), asset_category, company)) return account diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index d0fae6a2272..9b73d0f2711 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -95,7 +95,8 @@ class PurchaseReceipt(BuyingController): # 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) + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) break def validate_with_previous_doc(self): @@ -364,8 +365,9 @@ class PurchaseReceipt(BuyingController): def add_asset_gl_entries(self, item, gl_entries): arbnb_account = self.get_company_default("asset_received_but_not_billed") - # This returns company's default cwip account - cwip_account = get_asset_account("capital_work_in_progress_account", company = self.company) + # This returns category's cwip account if not then fallback to company's default cwip account + cwip_account = get_asset_account("capital_work_in_progress_account", asset_category = item.asset_category, \ + company = self.company) asset_amount = flt(item.net_amount) + flt(item.item_tax_amount/self.conversion_rate) base_asset_amount = flt(item.base_net_amount + item.item_tax_amount) From 6411a56cdcc5ea696021c178c2f9c9ed4444cf5f Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 9 Dec 2019 20:26:50 +0530 Subject: [PATCH 253/299] fix: Changed check condition and added test --- .../stock/doctype/stock_entry/stock_entry.js | 2 +- .../stock/doctype/stock_entry/stock_entry.py | 9 ++++++- .../doctype/stock_entry/test_stock_entry.py | 27 +++++++++++++++++++ 3 files changed, 36 insertions(+), 2 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.js b/erpnext/stock/doctype/stock_entry/stock_entry.js index d9c94fced7d..ca480f969d8 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.js +++ b/erpnext/stock/doctype/stock_entry/stock_entry.js @@ -536,7 +536,7 @@ frappe.ui.form.on('Stock Entry Detail', { if(r.message) { var d = locals[cdt][cdn]; $.each(r.message, function(k, v) { - d[k] = v; + frappe.model.set_value(cdt, cdn, k, v); // qty and it's subsequent fields weren't triggered }); refresh_field("items"); erpnext.stock.select_batch_and_serial_no(frm, d); diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 2b99f72565c..913656ad020 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -27,6 +27,7 @@ class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass +class TotalBasicAmountZeroError(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -649,6 +650,12 @@ class StockEntry(StockController): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + + if self.get("additional_costs") and not total_basic_amount: + #If additional costs table is populated and total basic amount is + #somehow 0, interrupt transaction. + frappe.throw(_("Total Basic Amount in Items Table cannot be 0"), TotalBasicAmountZeroError) + item_account_wise_additional_cost = {} for t in self.get("additional_costs"): @@ -657,7 +664,7 @@ class StockEntry(StockController): item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount if total_basic_amount else 0 + (t.amount * d.basic_amount) / total_basic_amount if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index eddab5d79dc..c5e67092d3d 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,6 +16,7 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError +from erpnext.stock.doctype.stock_entry.stock_entry import TotalBasicAmountZeroError from six import iteritems def get_sle(**args): @@ -790,6 +791,32 @@ class TestStockEntry(unittest.TestCase): filters={"voucher_type": "Stock Entry", "voucher_no": mr.name}, fieldname="is_opening") self.assertEqual(is_opening, "Yes") + def test_total_basic_amount_zero(self): + se = frappe.get_doc({"doctype":"Stock Entry", + "purpose":"Material Receipt", + "stock_entry_type":"Material Receipt", + "posting_date": nowdate(), + "company":"_Test Company with perpetual inventory", + "items":[ + {"item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1"} + ], + "additional_costs":[ + {"expense_account":"Miscellaneous Expenses - TCP1", + "amount":100, + "description": "miscellanous"} + ] + }) + + se.insert() + self.assertRaises(TotalBasicAmountZeroError, se.submit) + def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) se.get("items")[0].item_code = item_code or "_Test Serialized Item With Series" From d06b685fdf7f0ecd04873af741441cdf4e39ea7b Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 12:15:06 +0530 Subject: [PATCH 254/299] fix: Append expense account only if expense account exists (#19881) --- erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py index 7b2061ac168..917acba92c9 100644 --- a/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py +++ b/erpnext/accounts/doctype/purchase_invoice/purchase_invoice.py @@ -248,7 +248,7 @@ class PurchaseInvoice(BuyingController): def set_against_expense_account(self): against_accounts = [] for item in self.get("items"): - if item.expense_account not in against_accounts: + if item.expense_account and (item.expense_account not in against_accounts): against_accounts.append(item.expense_account) self.against_expense_account = ",".join(against_accounts) From 72649c207f74eb01832e4c1c53aafb10f6b0c6a4 Mon Sep 17 00:00:00 2001 From: Deepesh Garg <42651287+deepeshgarg007@users.noreply.github.com> Date: Tue, 10 Dec 2019 15:54:29 +0530 Subject: [PATCH 255/299] feat(regional): Auto state wise taxation for GST India (#19877) * feat(regional): Auto state wise taxation for GST India * fix: Update gst category on addition of GSTIN * fix: Codacy and travis fixes * fix: Travis * fix(test): Update GST category only if GSTIN field available * fix: Test Cases * fix: Do not skip accounts if place of supply is not present * fix: Auto GST taxation for SEZ Party types * fix: Automatic taxation for multi state * fix: Codacy and travis fixes * fix: Auto GST template selection in Sales Order * fix: Move inter state check and source state to tax category * fix: Remove unique check from tax template * fix: Remove unique check from tax template * fix: Address fetching logic in Sales * fix: fecth tax template on company address change * fix: fetch company gstin on address change * fix: company_gstin set value fix * fix: Mutiple fixes and code refactor * fix: Add missing semicolon * fix: Company address fetching in sales invoice * fix: Remove print statement * fix: Import functools * fix: Naming fixes and code cleanup * fix: Update patches * fix: Remove changes in patches.txt * fix: Iteritems compatibility for python 3 --- .../purchase_invoice/regional/india.js | 3 + .../purchase_taxes_and_charges_template.json | 374 +++++------------ .../doctype/sales_invoice/regional/india.js | 9 +- .../doctype/sales_invoice/sales_invoice.js | 4 +- .../sales_taxes_and_charges_template.json | 382 +++++------------- erpnext/accounts/party.py | 107 ++--- .../doctype/purchase_order/regional/india.js | 3 + erpnext/hooks.py | 6 +- erpnext/patches.txt | 3 +- .../add_export_type_field_in_party_master.py | 40 ++ erpnext/public/js/queries.js | 2 +- erpnext/public/js/utils/party.js | 44 ++ .../gstr_3b_report/test_gstr_3b_report.py | 17 +- erpnext/regional/india/__init__.py | 5 +- erpnext/regional/india/setup.py | 59 ++- erpnext/regional/india/taxes.js | 41 ++ erpnext/regional/india/utils.py | 116 +++++- .../doctype/sales_order/regional/india.js | 3 + .../doctype/sales_order/sales_order.py | 16 +- erpnext/setup/doctype/company/company.py | 25 ++ .../doctype/delivery_note/delivery_note.py | 7 +- .../doctype/delivery_note/regional/india.js | 4 + .../purchase_receipt/regional/india.js | 3 + 23 files changed, 602 insertions(+), 671 deletions(-) create mode 100644 erpnext/accounts/doctype/purchase_invoice/regional/india.js create mode 100644 erpnext/buying/doctype/purchase_order/regional/india.js create mode 100644 erpnext/patches/v12_0/add_export_type_field_in_party_master.py create mode 100644 erpnext/regional/india/taxes.js create mode 100644 erpnext/selling/doctype/sales_order/regional/india.js create mode 100644 erpnext/stock/doctype/delivery_note/regional/india.js create mode 100644 erpnext/stock/doctype/purchase_receipt/regional/india.js diff --git a/erpnext/accounts/doctype/purchase_invoice/regional/india.js b/erpnext/accounts/doctype/purchase_invoice/regional/india.js new file mode 100644 index 00000000000..81488a2c52a --- /dev/null +++ b/erpnext/accounts/doctype/purchase_invoice/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Invoice'); diff --git a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json index bc42630d474..a18fec61cf3 100644 --- a/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/purchase_taxes_and_charges_template/purchase_taxes_and_charges_template.json @@ -1,300 +1,108 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:08", - "custom": 0, - "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:08", + "description": "Standard tax template that can be applied to all Purchase Transactions. This template can contain list of tax heads and also other expense heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Consider Tax or Charge for: In this section you can specify if the tax / charge is only for valuation (not a part of total) or only for total (does not add value to the item) or for both.\n10. Add or Deduct: Whether you want to add or deduct the tax.", + "doctype": "DocType", + "document_type": "Setup", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break4", + "company", + "tax_category", + "section_break6", + "taxes" + ], "fields": [ { - "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": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Disabled", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Disabled" + }, { - "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_list_view": 0, - "in_standard_filter": 0, - "label": "", - "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, - "unique": 0 - }, + "fieldname": "column_break4", + "fieldtype": "Column Break" + }, { - "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": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "label": "Company", - "length": 0, - "no_copy": 0, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break6", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "", - "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, - "unique": 0 - }, + "fieldname": "section_break6", + "fieldtype": "Section Break" + }, { - "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_list_view": 0, - "in_standard_filter": 0, - "label": "Purchase Taxes and Charges", - "length": 0, - "no_copy": 0, - "oldfieldname": "purchase_tax_details", - "oldfieldtype": "Table", - "options": "Purchase 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, - "unique": 0 + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Purchase Taxes and Charges", + "oldfieldname": "purchase_tax_details", + "oldfieldtype": "Table", + "options": "Purchase Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:44.095798", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Purchase Taxes and Charges Template", - "owner": "wasim@webnotestech.com", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:05:26.220275", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Purchase Taxes and Charges Template", + "owner": "wasim@webnotestech.com", "permissions": [ { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Manager", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Manager" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Purchase Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Purchase Master Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 0, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 0, - "read": 1, - "report": 0, - "role": "Purchase User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 + "read": 1, + "role": "Purchase User" } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "DESC", - "track_seen": 0 + ], + "sort_order": "DESC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/doctype/sales_invoice/regional/india.js b/erpnext/accounts/doctype/sales_invoice/regional/india.js index c8305e325f6..7941a59ac36 100644 --- a/erpnext/accounts/doctype/sales_invoice/regional/india.js +++ b/erpnext/accounts/doctype/sales_invoice/regional/india.js @@ -1,3 +1,7 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Invoice'); + frappe.ui.form.on("Sales Invoice", { setup: function(frm) { frm.set_query('transporter', function() { @@ -34,5 +38,8 @@ frappe.ui.form.on("Sales Invoice", { } }, __("Make")); } - } + }, + }); + + diff --git a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js index 3c852106635..61aa0aa2ca9 100644 --- a/erpnext/accounts/doctype/sales_invoice/sales_invoice.js +++ b/erpnext/accounts/doctype/sales_invoice/sales_invoice.js @@ -697,8 +697,8 @@ frappe.ui.form.on('Sales Invoice', { if (frm.doc.company) { frappe.call({ - method:"frappe.contacts.doctype.address.address.get_default_address", - args:{ doctype:'Company',name:frm.doc.company}, + method:"erpnext.setup.doctype.company.company.get_default_company_address", + args:{name:frm.doc.company, existing_address: frm.doc.company_address}, callback: function(r){ if (r.message){ frm.set_value("company_address",r.message) diff --git a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json index 29e15d165fa..19781bdffaa 100644 --- a/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json +++ b/erpnext/accounts/doctype/sales_taxes_and_charges_template/sales_taxes_and_charges_template.json @@ -1,299 +1,119 @@ { - "allow_copy": 0, - "allow_import": 1, - "allow_rename": 1, - "autoname": "field:title", - "beta": 0, - "creation": "2013-01-10 16:34:09", - "custom": 0, - "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", - "docstatus": 0, - "doctype": "DocType", - "document_type": "Setup", - "editable_grid": 0, + "allow_import": 1, + "allow_rename": 1, + "creation": "2013-01-10 16:34:09", + "description": "Standard tax template that can be applied to all Sales Transactions. This template can contain list of tax heads and also other expense / income heads like \"Shipping\", \"Insurance\", \"Handling\" etc.\n\n#### Note\n\nThe tax rate you define here will be the standard tax rate for all **Items**. If there are **Items** that have different rates, they must be added in the **Item Tax** table in the **Item** master.\n\n#### Description of Columns\n\n1. Calculation Type: \n - This can be on **Net Total** (that is the sum of basic amount).\n - **On Previous Row Total / Amount** (for cumulative taxes or charges). If you select this option, the tax will be applied as a percentage of the previous row (in the tax table) amount or total.\n - **Actual** (as mentioned).\n2. Account Head: The Account ledger under which this tax will be booked\n3. Cost Center: If the tax / charge is an income (like shipping) or expense it needs to be booked against a Cost Center.\n4. Description: Description of the tax (that will be printed in invoices / quotes).\n5. Rate: Tax rate.\n6. Amount: Tax amount.\n7. Total: Cumulative total to this point.\n8. Enter Row: If based on \"Previous Row Total\" you can select the row number which will be taken as a base for this calculation (default is the previous row).\n9. Is this Tax included in Basic Rate?: If you check this, it means that this tax will not be shown below the item table, but will be included in the Basic Rate in your main item table. This is useful where you want give a flat price (inclusive of all taxes) price to customers.", + "doctype": "DocType", + "document_type": "Setup", + "engine": "InnoDB", + "field_order": [ + "title", + "is_default", + "disabled", + "column_break_3", + "company", + "tax_category", + "section_break_5", + "taxes" + ], "fields": [ { - "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": 1, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Title", - "length": 0, - "no_copy": 1, - "oldfieldname": "title", - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "title", + "fieldtype": "Data", + "label": "Title", + "no_copy": 1, + "oldfieldname": "title", + "oldfieldtype": "Data", + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "is_default", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 1, - "in_standard_filter": 0, - "label": "Default", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "is_default", + "fieldtype": "Check", + "in_list_view": 1, + "label": "Default" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "disabled", - "fieldtype": "Check", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 0, - "in_list_view": 0, - "in_standard_filter": 0, - "label": "Disabled", - "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, - "unique": 0 - }, + "default": "0", + "fieldname": "disabled", + "fieldtype": "Check", + "label": "Disabled" + }, { - "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_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, - "unique": 0 - }, + "fieldname": "column_break_3", + "fieldtype": "Column Break" + }, { - "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": 1, - "in_list_view": 1, - "in_standard_filter": 1, - "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": 1, - "search_index": 0, - "set_only_once": 0, - "unique": 0 - }, + "fieldname": "company", + "fieldtype": "Link", + "in_list_view": 1, + "in_standard_filter": 1, + "label": "Company", + "oldfieldname": "company", + "oldfieldtype": "Link", + "options": "Company", + "remember_last_selected_value": 1, + "reqd": 1 + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "fieldname": "section_break_5", - "fieldtype": "Section Break", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 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, - "unique": 0 - }, + "fieldname": "section_break_5", + "fieldtype": "Section Break" + }, { - "allow_on_submit": 0, - "bold": 0, - "collapsible": 0, - "columns": 0, - "description": "* Will be calculated in the transaction.", - "fieldname": "taxes", - "fieldtype": "Table", - "hidden": 0, - "ignore_user_permissions": 0, - "ignore_xss_filter": 0, - "in_filter": 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, - "unique": 0 + "description": "* Will be calculated in the transaction.", + "fieldname": "taxes", + "fieldtype": "Table", + "label": "Sales Taxes and Charges", + "oldfieldname": "other_charges", + "oldfieldtype": "Table", + "options": "Sales Taxes and Charges" + }, + { + "fieldname": "tax_category", + "fieldtype": "Link", + "label": "Tax Category", + "options": "Tax Category" } - ], - "hide_heading": 0, - "hide_toolbar": 0, - "icon": "fa fa-money", - "idx": 1, - "image_view": 0, - "in_create": 0, - - "is_submittable": 0, - "issingle": 0, - "istable": 0, - "max_attachments": 0, - "modified": "2016-11-07 05:18:41.743257", - "modified_by": "Administrator", - "module": "Accounts", - "name": "Sales Taxes and Charges Template", - "owner": "Administrator", + ], + "icon": "fa fa-money", + "idx": 1, + "modified": "2019-11-25 13:06:03.279099", + "modified_by": "Administrator", + "module": "Accounts", + "name": "Sales Taxes and Charges Template", + "owner": "Administrator", "permissions": [ { - "amend": 0, - "apply_user_permissions": 1, - "cancel": 0, - "create": 0, - "delete": 0, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales User", - "set_user_permissions": 0, - "share": 0, - "submit": 0, - "write": 0 - }, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales User" + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 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, + "print": 1, + "read": 1, + "report": 1, + "role": "Accounts Manager", + "share": 1, "write": 1 - }, + }, { - "amend": 0, - "apply_user_permissions": 0, - "cancel": 0, - "create": 1, - "delete": 1, - "email": 1, - "export": 0, - "if_owner": 0, - "import": 0, - "is_custom": 0, - "permlevel": 0, - "print": 1, - "read": 1, - "report": 1, - "role": "Sales Master Manager", - "set_user_permissions": 0, - "share": 1, - "submit": 0, + "create": 1, + "delete": 1, + "email": 1, + "print": 1, + "read": 1, + "report": 1, + "role": "Sales Master Manager", + "share": 1, "write": 1 } - ], - "quick_entry": 0, - "read_only": 0, - "read_only_onload": 0, - "sort_order": "ASC", - "track_seen": 0 + ], + "sort_field": "modified", + "sort_order": "ASC", + "track_changes": 1 } \ No newline at end of file diff --git a/erpnext/accounts/party.py b/erpnext/accounts/party.py index 59936d5116d..156f2181b87 100644 --- a/erpnext/accounts/party.py +++ b/erpnext/accounts/party.py @@ -23,7 +23,7 @@ class DuplicatePartyAccountError(frappe.ValidationError): pass @frappe.whitelist() def get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, fetch_payment_terms_template=True, - party_address=None, shipping_address=None, pos_profile=None): + party_address=None, company_address=None, shipping_address=None, pos_profile=None): if not party: return {} @@ -31,14 +31,14 @@ def get_party_details(party=None, account=None, party_type="Customer", company=N frappe.throw(_("{0}: {1} does not exists").format(party_type, party)) return _get_party_details(party, account, party_type, company, posting_date, bill_date, price_list, currency, doctype, ignore_permissions, - fetch_payment_terms_template, party_address, shipping_address, pos_profile) + fetch_payment_terms_template, party_address, company_address, shipping_address, pos_profile) def _get_party_details(party=None, account=None, party_type="Customer", company=None, posting_date=None, bill_date=None, price_list=None, currency=None, doctype=None, ignore_permissions=False, - fetch_payment_terms_template=True, party_address=None, shipping_address=None, pos_profile=None): + fetch_payment_terms_template=True, party_address=None, company_address=None,shipping_address=None, pos_profile=None): - out = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) - party = out[party_type.lower()] + party_details = frappe._dict(set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype)) + party = party_details[party_type.lower()] if not ignore_permissions and not frappe.has_permission(party_type, "read", party): frappe.throw(_("Not permitted for {0}").format(party), frappe.PermissionError) @@ -46,76 +46,81 @@ def _get_party_details(party=None, account=None, party_type="Customer", company= party = frappe.get_doc(party_type, party) currency = party.default_currency if party.get("default_currency") else get_company_currency(company) - party_address, shipping_address = set_address_details(out, party, party_type, doctype, company, party_address, shipping_address) - set_contact_details(out, party, party_type) - set_other_values(out, party, party_type) - set_price_list(out, party, party_type, price_list, pos_profile) + party_address, shipping_address = set_address_details(party_details, party, party_type, doctype, company, party_address, company_address, shipping_address) + set_contact_details(party_details, party, party_type) + set_other_values(party_details, party, party_type) + set_price_list(party_details, party, party_type, price_list, pos_profile) - out["tax_category"] = get_address_tax_category(party.get("tax_category"), + party_details["tax_category"] = get_address_tax_category(party.get("tax_category"), party_address, shipping_address if party_type != "Supplier" else party_address) - out["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, - customer_group=out.customer_group, supplier_group=out.supplier_group, tax_category=out.tax_category, - billing_address=party_address, shipping_address=shipping_address) + + if not party_details.get("taxes_and_charges"): + party_details["taxes_and_charges"] = set_taxes(party.name, party_type, posting_date, company, + customer_group=party_details.customer_group, supplier_group=party_details.supplier_group, tax_category=party_details.tax_category, + billing_address=party_address, shipping_address=shipping_address) if fetch_payment_terms_template: - out["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) + party_details["payment_terms_template"] = get_pyt_term_template(party.name, party_type, company) - if not out.get("currency"): - out["currency"] = currency + if not party_details.get("currency"): + party_details["currency"] = currency # sales team if party_type=="Customer": - out["sales_team"] = [{ + party_details["sales_team"] = [{ "sales_person": d.sales_person, "allocated_percentage": d.allocated_percentage or None } for d in party.get("sales_team")] # supplier tax withholding category if party_type == "Supplier" and party: - out["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") + party_details["supplier_tds"] = frappe.get_value(party_type, party.name, "tax_withholding_category") - return out + return party_details -def set_address_details(out, party, party_type, doctype=None, company=None, party_address=None, shipping_address=None): +def set_address_details(party_details, party, party_type, doctype=None, company=None, party_address=None, company_address=None, shipping_address=None): billing_address_field = "customer_address" if party_type == "Lead" \ else party_type.lower() + "_address" - out[billing_address_field] = party_address or get_default_address(party_type, party.name) + party_details[billing_address_field] = party_address or get_default_address(party_type, party.name) if doctype: - out.update(get_fetch_values(doctype, billing_address_field, out[billing_address_field])) + party_details.update(get_fetch_values(doctype, billing_address_field, party_details[billing_address_field])) # address display - out.address_display = get_address_display(out[billing_address_field]) + party_details.address_display = get_address_display(party_details[billing_address_field]) # shipping address if party_type in ["Customer", "Lead"]: - out.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) - out.shipping_address = get_address_display(out["shipping_address_name"]) + party_details.shipping_address_name = shipping_address or get_party_shipping_address(party_type, party.name) + party_details.shipping_address = get_address_display(party_details["shipping_address_name"]) if doctype: - out.update(get_fetch_values(doctype, 'shipping_address_name', out.shipping_address_name)) + party_details.update(get_fetch_values(doctype, 'shipping_address_name', party_details.shipping_address_name)) - if doctype and doctype in ['Delivery Note', 'Sales Invoice']: - out.update(get_company_address(company)) - if out.company_address: - out.update(get_fetch_values(doctype, 'company_address', out.company_address)) - get_regional_address_details(out, doctype, company) + if company_address: + party_details.update({'company_address': company_address}) + else: + party_details.update(get_company_address(company)) - elif doctype and doctype == "Purchase Invoice": - out.update(get_company_address(company)) - if out.company_address: - out["shipping_address"] = shipping_address or out["company_address"] - out.shipping_address_display = get_address_display(out["shipping_address"]) - out.update(get_fetch_values(doctype, 'shipping_address', out.shipping_address)) - get_regional_address_details(out, doctype, company) + if doctype and doctype in ['Delivery Note', 'Sales Invoice', 'Sales Order']: + if party_details.company_address: + party_details.update(get_fetch_values(doctype, 'company_address', party_details.company_address)) + get_regional_address_details(party_details, doctype, company) - return out.get(billing_address_field), out.shipping_address_name + elif doctype and doctype in ["Purchase Invoice", "Purchase Order", "Purchase Receipt"]: + if party_details.company_address: + party_details["shipping_address"] = shipping_address or party_details["company_address"] + party_details.shipping_address_display = get_address_display(party_details["shipping_address"]) + party_details.update(get_fetch_values(doctype, 'shipping_address', party_details.shipping_address)) + get_regional_address_details(party_details, doctype, company) + + return party_details.get(billing_address_field), party_details.shipping_address_name @erpnext.allow_regional -def get_regional_address_details(out, doctype, company): +def get_regional_address_details(party_details, doctype, company): pass -def set_contact_details(out, party, party_type): - out.contact_person = get_default_contact(party_type, party.name) +def set_contact_details(party_details, party, party_type): + party_details.contact_person = get_default_contact(party_type, party.name) - if not out.contact_person: - out.update({ + if not party_details.contact_person: + party_details.update({ "contact_person": None, "contact_display": None, "contact_email": None, @@ -125,22 +130,22 @@ def set_contact_details(out, party, party_type): "contact_department": None }) else: - out.update(get_contact_details(out.contact_person)) + party_details.update(get_contact_details(party_details.contact_person)) -def set_other_values(out, party, party_type): +def set_other_values(party_details, party, party_type): # copy if party_type=="Customer": to_copy = ["customer_name", "customer_group", "territory", "language"] else: to_copy = ["supplier_name", "supplier_group", "language"] for f in to_copy: - out[f] = party.get(f) + party_details[f] = party.get(f) # fields prepended with default in Customer doctype for f in ['currency'] \ + (['sales_partner', 'commission_rate'] if party_type=="Customer" else []): if party.get("default_" + f): - out[f] = party.get("default_" + f) + party_details[f] = party.get("default_" + f) def get_default_price_list(party): """Return default price list for party (Document object)""" @@ -155,7 +160,7 @@ def get_default_price_list(party): return None -def set_price_list(out, party, party_type, given_price_list, pos=None): +def set_price_list(party_details, party, party_type, given_price_list, pos=None): # price list price_list = get_permitted_documents('Price List') @@ -173,9 +178,9 @@ def set_price_list(out, party, party_type, given_price_list, pos=None): price_list = get_default_price_list(party) or given_price_list if price_list: - out.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) + party_details.price_list_currency = frappe.db.get_value("Price List", price_list, "currency", cache=True) - out["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list + party_details["selling_price_list" if party.doctype=="Customer" else "buying_price_list"] = price_list def set_account_and_due_date(party, account, party_type, company, posting_date, bill_date, doctype): diff --git a/erpnext/buying/doctype/purchase_order/regional/india.js b/erpnext/buying/doctype/purchase_order/regional/india.js new file mode 100644 index 00000000000..42d3995907f --- /dev/null +++ b/erpnext/buying/doctype/purchase_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Order'); \ No newline at end of file diff --git a/erpnext/hooks.py b/erpnext/hooks.py index 1fb4c2b5968..1ec79b68ef0 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -248,10 +248,10 @@ doc_events = { "on_trash": "erpnext.regional.check_deletion_permission" }, 'Address': { - 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code'] + 'validate': ['erpnext.regional.india.utils.validate_gstin_for_india', 'erpnext.regional.italy.utils.set_state_code', 'erpnext.regional.india.utils.update_gst_category'] }, - ('Sales Invoice', 'Purchase Invoice', 'Delivery Note'): { - 'validate': 'erpnext.regional.india.utils.set_place_of_supply' + ('Sales Invoice', 'Sales Order', 'Delivery Note', 'Purchase Invoice', 'Purchase Order', 'Purchase Receipt'): { + 'validate': ['erpnext.regional.india.utils.set_place_of_supply'] }, "Contact": { "on_trash": "erpnext.support.doctype.issue.issue.update_issue", diff --git a/erpnext/patches.txt b/erpnext/patches.txt index 07b646b0f82..eb686122355 100644 --- a/erpnext/patches.txt +++ b/erpnext/patches.txt @@ -646,4 +646,5 @@ 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 -erpnext.patches.v12_0.update_price_or_product_discount \ No newline at end of file +erpnext.patches.v12_0.update_price_or_product_discount +erpnext.patches.v12_0.add_export_type_field_in_party_master diff --git a/erpnext/patches/v12_0/add_export_type_field_in_party_master.py b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py new file mode 100644 index 00000000000..c565b7ecd87 --- /dev/null +++ b/erpnext/patches/v12_0/add_export_type_field_in_party_master.py @@ -0,0 +1,40 @@ +from __future__ import unicode_literals +import frappe +from erpnext.regional.india.setup import make_custom_fields + +def execute(): + + company = frappe.get_all('Company', filters = {'country': 'India'}) + if not company: + return + + make_custom_fields() + + frappe.reload_doctype('Tax Category') + frappe.reload_doctype('Sales Taxes and Charges Template') + frappe.reload_doctype('Purchase Taxes and Charges Template') + + # Create tax category with inter state field checked + tax_category = frappe.db.get_value('Tax Category', {'name': 'OUT OF STATE'}, 'name') + + if not tax_category: + inter_state_category = frappe.get_doc({ + 'doctype': 'Tax Category', + 'title': 'OUT OF STATE', + 'is_inter_state': 1 + }).insert() + + tax_category = inter_state_category.name + + for doctype in ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template'): + template = frappe.db.get_value(doctype, {'is_inter_state': 1, 'disabled': 0}, ['name']) + if template: + frappe.db.set_value(doctype, template, 'tax_category', tax_category) + + frappe.db.sql(""" + DELETE FROM `tabCustom Field` + WHERE fieldname = 'is_inter_state' + AND dt IN ('Sales Taxes and Charges Template', 'Purchase Taxes and Charges Template') + """) + + diff --git a/erpnext/public/js/queries.js b/erpnext/public/js/queries.js index 84d2113c067..560a5617da5 100644 --- a/erpnext/public/js/queries.js +++ b/erpnext/public/js/queries.js @@ -65,7 +65,7 @@ $.extend(erpnext.queries, { frappe.throw(__("Please set {0}", [__(frappe.meta.get_label(doc.doctype, frappe.dynamic_link.fieldname, doc.name))])); } - console.log(frappe.dynamic_link) + return { query: 'frappe.contacts.doctype.address.address.address_query', filters: { diff --git a/erpnext/public/js/utils/party.js b/erpnext/public/js/utils/party.js index a8d3888ba0f..99c1b8ae8f3 100644 --- a/erpnext/public/js/utils/party.js +++ b/erpnext/public/js/utils/party.js @@ -7,6 +7,21 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { if(!method) { method = "erpnext.accounts.party.get_party_details"; } + + if (args) { + if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { + if (frm.doc.company_address && (!args.company_address)) { + args.company_address = frm.doc.company_address; + } + } + + if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { + if (frm.doc.shipping_address && (!args.shipping_address)) { + args.shipping_address = frm.doc.shipping_address; + } + } + } + if(!args) { if((frm.doctype != "Purchase Order" && frm.doc.customer) || (frm.doc.party_name && in_list(['Quotation', 'Opportunity'], frm.doc.doctype))) { @@ -30,6 +45,35 @@ erpnext.utils.get_party_details = function(frm, method, args, callback) { }; } + if (in_list(['Sales Invoice', 'Sales Order', 'Delivery Note'], frm.doc.doctype)) { + if (!args) { + args = { + party: frm.doc.customer || frm.doc.party_name, + party_type: 'Customer' + } + } + if (frm.doc.company_address && (!args.company_address)) { + args.company_address = frm.doc.company_address; + } + + if (frm.doc.shipping_address_name &&(!args.shipping_address_name)) { + args.shipping_address_name = frm.doc.shipping_address_name; + } + } + + if (in_list(['Purchase Invoice', 'Purchase Order', 'Purchase Receipt'], frm.doc.doctype)) { + if (!args) { + args = { + party: frm.doc.supplier, + party_type: 'Supplier' + } + } + + if (frm.doc.shipping_address && (!args.shipping_address)) { + args.shipping_address = frm.doc.shipping_address; + } + } + if (args) { args.posting_date = frm.doc.posting_date || frm.doc.transaction_date; } diff --git a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py index fef73d9a0a8..fa6fb706e9b 100644 --- a/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py +++ b/erpnext/regional/doctype/gstr_3b_report/test_gstr_3b_report.py @@ -64,7 +64,8 @@ class TestGSTR3BReport(unittest.TestCase): self.assertEqual(output["inter_sup"]["unreg_details"][0]["iamt"], 18), self.assertEqual(output["sup_details"]["osup_nil_exmp"]["txval"], 100), self.assertEqual(output["inward_sup"]["isup_details"][0]["inter"], 250) - self.assertEqual(output["itc_elg"]["itc_avl"][4]["iamt"], 45) + self.assertEqual(output["itc_elg"]["itc_avl"][4]["samt"], 22.50) + self.assertEqual(output["itc_elg"]["itc_avl"][4]["camt"], 22.50) def make_sales_invoice(): si = create_sales_invoice(company="_Test Company GST", @@ -158,10 +159,18 @@ def create_purchase_invoices(): pi.append("taxes", { "charge_type": "On Net Total", - "account_head": "IGST - _GST", + "account_head": "CGST - _GST", "cost_center": "Main - _GST", - "description": "IGST @ 18.0", - "rate": 18 + "description": "CGST @ 9.0", + "rate": 9 + }) + + pi.append("taxes", { + "charge_type": "On Net Total", + "account_head": "SGST - _GST", + "cost_center": "Main - _GST", + "description": "SGST @ 9.0", + "rate": 9 }) pi.submit() diff --git a/erpnext/regional/india/__init__.py b/erpnext/regional/india/__init__.py index 46c874b2524..0ed98b74eef 100644 --- a/erpnext/regional/india/__init__.py +++ b/erpnext/regional/india/__init__.py @@ -1,4 +1,5 @@ from __future__ import unicode_literals +from six import iteritems states = [ '', @@ -79,4 +80,6 @@ state_numbers = { "Uttar Pradesh": "09", "Uttarakhand": "05", "West Bengal": "19", -} \ No newline at end of file +} + +number_state_mapping = {v: k for k, v in iteritems(state_numbers)} \ No newline at end of file diff --git a/erpnext/regional/india/setup.py b/erpnext/regional/india/setup.py index 756c17dc3b3..14fdba013c7 100644 --- a/erpnext/regional/india/setup.py +++ b/erpnext/regional/india/setup.py @@ -107,7 +107,12 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', - fetch_from='supplier.gst_category', fetch_if_empty=1) + fetch_from='supplier.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='supplier.export_type', + fetch_if_empty=1), ] sales_invoice_gst_category = [ @@ -116,20 +121,21 @@ def make_custom_fields(update=True): dict(fieldname='gst_category', label='GST Category', fieldtype='Select', insert_after='gst_section', print_hide=1, options='\nRegistered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', - fetch_from='customer.gst_category', fetch_if_empty=1) + fetch_from='customer.gst_category', fetch_if_empty=1), + dict(fieldname='export_type', label='Export Type', + fieldtype='Select', insert_after='gst_category', print_hide=1, + depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + options='\nWith Payment of Tax\nWithout Payment of Tax', fetch_from='customer.export_type', + fetch_if_empty=1), ] invoice_gst_fields = [ dict(fieldname='invoice_copy', label='Invoice Copy', - fieldtype='Select', insert_after='gst_category', print_hide=1, allow_on_submit=1, + fieldtype='Select', insert_after='export_type', print_hide=1, allow_on_submit=1, options='Original for Recipient\nDuplicate for Transporter\nDuplicate for Supplier\nTriplicate for Supplier'), dict(fieldname='reverse_charge', label='Reverse Charge', fieldtype='Select', insert_after='invoice_copy', print_hide=1, options='Y\nN', default='N'), - dict(fieldname='export_type', label='Export Type', - fieldtype='Select', insert_after='reverse_charge', print_hide=1, - depends_on='eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', - options='\nWith Payment of Tax\nWithout Payment of Tax'), dict(fieldname='ecommerce_gstin', label='E-commerce GSTIN', fieldtype='Data', insert_after='export_type', print_hide=1), dict(fieldname='gst_col_break', fieldtype='Column Break', insert_after='ecommerce_gstin'), @@ -142,13 +148,13 @@ def make_custom_fields(update=True): purchase_invoice_gst_fields = [ dict(fieldname='supplier_gstin', label='Supplier GSTIN', fieldtype='Data', insert_after='supplier_address', - fetch_from='supplier_address.gstin', print_hide=1), + fetch_from='supplier_address.gstin', print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='shipping_address_display', - fetch_from='shipping_address.gstin', print_hide=1), + fetch_from='shipping_address.gstin', print_hide=1, read_only=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='shipping_address', - print_hide=1, read_only=0), + print_hide=1, read_only=1), ] purchase_invoice_itc_fields = [ @@ -167,17 +173,17 @@ def make_custom_fields(update=True): sales_invoice_gst_fields = [ dict(fieldname='billing_address_gstin', label='Billing Address GSTIN', - fieldtype='Data', insert_after='customer_address', + fieldtype='Data', insert_after='customer_address', read_only=1, fetch_from='customer_address.gstin', print_hide=1), dict(fieldname='customer_gstin', label='Customer GSTIN', fieldtype='Data', insert_after='shipping_address_name', fetch_from='shipping_address_name.gstin', print_hide=1), dict(fieldname='place_of_supply', label='Place of Supply', fieldtype='Data', insert_after='customer_gstin', - print_hide=1, read_only=0), + print_hide=1, read_only=1), dict(fieldname='company_gstin', label='Company GSTIN', fieldtype='Data', insert_after='company_address', - fetch_from='company_address.gstin', print_hide=1), + fetch_from='company_address.gstin', print_hide=1, read_only=1), ] sales_invoice_shipping_fields = [ @@ -194,7 +200,11 @@ def make_custom_fields(update=True): inter_state_gst_field = [ dict(fieldname='is_inter_state', label='Is Inter State', - fieldtype='Check', insert_after='disabled', print_hide=1) + fieldtype='Check', insert_after='disabled', print_hide=1), + dict(fieldname='tax_category_column_break', fieldtype='Column Break', + insert_after='is_inter_state'), + dict(fieldname='gst_state', label='Source State', fieldtype='Select', + options='\n'.join(states), insert_after='company') ] ewaybill_fields = [ @@ -374,8 +384,7 @@ def make_custom_fields(update=True): 'Sales Invoice': sales_invoice_gst_category + invoice_gst_fields + sales_invoice_shipping_fields + sales_invoice_gst_fields + si_ewaybill_fields, 'Delivery Note': sales_invoice_gst_fields + ewaybill_fields + sales_invoice_shipping_fields, 'Sales Order': sales_invoice_gst_fields, - 'Sales Taxes and Charges Template': inter_state_gst_field, - 'Purchase Taxes and Charges Template': inter_state_gst_field, + 'Tax Category': inter_state_gst_field, 'Item': [ dict(fieldname='gst_hsn_code', label='HSN/SAC', fieldtype='Link', options='GST HSN Code', insert_after='item_group'), @@ -459,6 +468,15 @@ def make_custom_fields(update=True): 'insert_after': 'gst_transporter_id', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ], 'Customer': [ @@ -469,6 +487,15 @@ def make_custom_fields(update=True): 'insert_after': 'customer_type', 'options': 'Registered Regular\nRegistered Composition\nUnregistered\nSEZ\nOverseas\nConsumer\nDeemed Export\nUIN Holders', 'default': 'Unregistered' + }, + { + 'fieldname': 'export_type', + 'label': 'Export Type', + 'fieldtype': 'Select', + 'insert_after': 'gst_category', + 'default': 'Without Payment of Tax', + 'depends_on':'eval:in_list(["SEZ", "Overseas", "Deemed Export"], doc.gst_category)', + 'options': '\nWith Payment of Tax\nWithout Payment of Tax' } ] } diff --git a/erpnext/regional/india/taxes.js b/erpnext/regional/india/taxes.js new file mode 100644 index 00000000000..1e59032db10 --- /dev/null +++ b/erpnext/regional/india/taxes.js @@ -0,0 +1,41 @@ +erpnext.setup_auto_gst_taxation = (doctype) => { + frappe.ui.form.on(doctype, { + company_address: function(frm) { + frm.trigger('get_tax_template'); + }, + shipping_address: function(frm) { + frm.trigger('get_tax_template'); + }, + tax_category: function(frm) { + frm.trigger('get_tax_template'); + }, + get_tax_template: function(frm) { + let party_details = { + 'shipping_address': frm.doc.shipping_address || '', + 'shipping_address_name': frm.doc.shipping_address_name || '', + 'customer_address': frm.doc.customer_address || '', + 'customer': frm.doc.customer, + 'supplier': frm.doc.supplier, + 'supplier_gstin': frm.doc.supplier_gstin, + 'company_gstin': frm.doc.company_gstin, + 'tax_category': frm.doc.tax_category + }; + + frappe.call({ + method: 'erpnext.regional.india.utils.get_regional_address_details', + args: { + party_details: JSON.stringify(party_details), + doctype: frm.doc.doctype, + company: frm.doc.company, + return_taxes: 1 + }, + callback: function(r) { + if(r.message) { + frm.set_value('taxes_and_charges', r.message.taxes_and_charges); + } + } + }); + } + }); +}; + diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index aae07797a15..77bcc80abab 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -7,6 +7,8 @@ from erpnext.controllers.taxes_and_totals import get_itemised_tax, get_itemised_ from erpnext.controllers.accounts_controller import get_taxes_and_charges from erpnext.hr.utils import get_salary_assignment from erpnext.hr.doctype.salary_structure.salary_structure import make_salary_slip +from erpnext.regional.india import number_state_mapping +from six import string_types def validate_gstin_for_india(doc, method): if hasattr(doc, 'gst_state') and doc.gst_state: @@ -46,6 +48,14 @@ def validate_gstin_for_india(doc, method): frappe.throw(_("Invalid GSTIN! First 2 digits of GSTIN should match with State number {0}.") .format(doc.gst_state_number)) +def update_gst_category(doc, method): + for link in doc.links: + if link.link_doctype in ['Customer', 'Supplier']: + if doc.get('gstin'): + frappe.db.sql(""" + UPDATE `tab{0}` SET gst_category = %s WHERE name = %s AND gst_category = 'Unregistered' + """.format(link.link_doctype), ("Registered Regular", link.link_name)) #nosec + def set_gst_state_and_state_number(doc): if not doc.gst_state: if not doc.state: @@ -122,44 +132,106 @@ def test_method(): '''test function''' return 'overridden' -def get_place_of_supply(out, doctype): +def get_place_of_supply(party_details, doctype): if not frappe.get_meta('Address').has_field('gst_state'): return - if doctype in ("Sales Invoice", "Delivery Note"): - address_name = out.shipping_address_name or out.customer_address - elif doctype == "Purchase Invoice": - address_name = out.shipping_address or out.supplier_address + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): + address_name = party_details.shipping_address_name or party_details.customer_address + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + address_name = party_details.shipping_address or party_details.supplier_address if address_name: address = frappe.db.get_value("Address", address_name, ["gst_state", "gst_state_number"], as_dict=1) if address and address.gst_state and address.gst_state_number: return cstr(address.gst_state_number) + "-" + cstr(address.gst_state) -def get_regional_address_details(out, doctype, company): - out.place_of_supply = get_place_of_supply(out, doctype) +@frappe.whitelist() +def get_regional_address_details(party_details, doctype, company, return_taxes=None): - if not out.place_of_supply: return + if isinstance(party_details, string_types): + party_details = json.loads(party_details) + party_details = frappe._dict(party_details) - if doctype in ("Sales Invoice", "Delivery Note"): + party_details.place_of_supply = get_place_of_supply(party_details, doctype) + if doctype in ("Sales Invoice", "Delivery Note", "Sales Order"): master_doctype = "Sales Taxes and Charges Template" - if not out.company_gstin: - return - elif doctype == "Purchase Invoice": - master_doctype = "Purchase Taxes and Charges Template" - if not out.supplier_gstin: + + get_tax_template_for_sez(party_details, master_doctype, company, 'Customer') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.company_gstin: return - if ((doctype in ("Sales Invoice", "Delivery Note") and out.company_gstin - and out.company_gstin[:2] != out.place_of_supply[:2]) or (doctype == "Purchase Invoice" - and out.supplier_gstin and out.supplier_gstin[:2] != out.place_of_supply[:2])): - default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0}) + elif doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt"): + master_doctype = "Purchase Taxes and Charges Template" + + get_tax_template_for_sez(party_details, master_doctype, company, 'Supplier') + get_tax_template_based_on_category(master_doctype, company, party_details) + + if party_details.get('taxes_and_charges') and return_taxes: + return party_details + + if not party_details.supplier_gstin: + return + + if not party_details.place_of_supply: return + + if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin + and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", + "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): + default_tax = get_tax_template(master_doctype, company, 1, party_details.company_gstin[:2]) else: - default_tax = frappe.db.get_value(master_doctype, {"company": company, "disabled":0, "is_default": 1}) + default_tax = get_tax_template(master_doctype, company, 0, party_details.company_gstin[:2]) if not default_tax: return - out["taxes_and_charges"] = default_tax - out.taxes = get_taxes_and_charges(master_doctype, default_tax) + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + + if return_taxes: + return party_details + +def get_tax_template_based_on_category(master_doctype, company, party_details): + if not party_details.get('tax_category'): + return + + default_tax = frappe.db.get_value(master_doctype, {'company': company, 'tax_category': party_details.get('tax_category')}, + 'name') + + if default_tax: + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + +def get_tax_template(master_doctype, company, is_inter_state, state_code): + tax_categories = frappe.get_all('Tax Category', fields = ['name', 'is_inter_state', 'gst_state'], + filters = {'is_inter_state': is_inter_state}) + + default_tax = '' + + for tax_category in tax_categories: + if tax_category.gst_state == number_state_mapping[state_code] or \ + (not default_tax and not tax_category.gst_state): + default_tax = frappe.db.get_value(master_doctype, + {'disabled': 0, 'tax_category': tax_category.name}, 'name') + + return default_tax + +def get_tax_template_for_sez(party_details, master_doctype, company, party_type): + + gst_details = frappe.db.get_value(party_type, {'name': party_details.get(frappe.scrub(party_type))}, + ['gst_category', 'export_type'], as_dict=1) + + if gst_details: + if gst_details.gst_category == 'SEZ' and gst_details.export_type == 'With Payment of Tax': + default_tax = frappe.db.get_value(master_doctype, {"company": company, "is_inter_state":1, "disabled":0, + "gst_state": number_state_mapping[party_details.company_gstin[:2]]}) + + party_details["taxes_and_charges"] = default_tax + party_details.taxes = get_taxes_and_charges(master_doctype, default_tax) + def calculate_annual_eligible_hra_exemption(doc): basic_component = frappe.get_cached_value('Company', doc.company, "basic_component") @@ -555,7 +627,7 @@ def get_gst_accounts(company, account_wise=False): filters={"parent": "GST Settings", "company": company}, fields=["cgst_account", "sgst_account", "igst_account", "cess_account"]) - if not gst_settings_accounts: + if not gst_settings_accounts and not frappe.flags.in_test: frappe.throw(_("Please set GST Accounts in GST Settings")) for d in gst_settings_accounts: diff --git a/erpnext/selling/doctype/sales_order/regional/india.js b/erpnext/selling/doctype/sales_order/regional/india.js new file mode 100644 index 00000000000..c11cfcc50b7 --- /dev/null +++ b/erpnext/selling/doctype/sales_order/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Sales Order'); diff --git a/erpnext/selling/doctype/sales_order/sales_order.py b/erpnext/selling/doctype/sales_order/sales_order.py index e97a4ee4611..2112a4174b1 100755 --- a/erpnext/selling/doctype/sales_order/sales_order.py +++ b/erpnext/selling/doctype/sales_order/sales_order.py @@ -578,8 +578,12 @@ def make_delivery_note(source_name, target_doc=None, skip_item_mapping=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Delivery Note", 'company_address', target.company_address)) @@ -645,8 +649,12 @@ def make_sales_invoice(source_name, target_doc=None, ignore_permissions=False): target.run_method("set_po_nos") target.run_method("calculate_taxes_and_totals") - # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 04e8a83e7f5..2eee919b530 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -14,6 +14,8 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils.nestedset import NestedSet +import functools + class Company(NestedSet): nsm_parent_field = 'parent_company' @@ -560,3 +562,26 @@ def get_timeline_data(doctype, name): return json.loads(history) if history and '{' in history else {} return date_to_value_dict + +@frappe.whitelist() +def get_default_company_address(name, sort_key='is_primary_address', existing_address=None): + if sort_key not in ['is_shipping_address', 'is_primary_address']: + return None + + out = frappe.db.sql(""" SELECT + addr.name, addr.%s + FROM + `tabAddress` addr, `tabDynamic Link` dl + WHERE + dl.parent = addr.name and dl.link_doctype = 'Company' and + dl.link_name = %s and ifnull(addr.disabled, 0) = 0 + """ %(sort_key, '%s'), (name)) #nosec + + if existing_address: + if existing_address in [d[0] for d in out]: + return existing_address + + if out: + return sorted(out, key = functools.cmp_to_key(lambda x,y: cmp(y[1], x[1])))[0][0] + else: + return None \ No newline at end of file diff --git a/erpnext/stock/doctype/delivery_note/delivery_note.py b/erpnext/stock/doctype/delivery_note/delivery_note.py index c98dfe35fb5..39aad2e0071 100644 --- a/erpnext/stock/doctype/delivery_note/delivery_note.py +++ b/erpnext/stock/doctype/delivery_note/delivery_note.py @@ -424,7 +424,12 @@ def make_sales_invoice(source_name, target_doc=None): target.run_method("calculate_taxes_and_totals") # set company address - target.update(get_company_address(target.company)) + if source.company_address: + target.update({'company_address': source.company_address}) + else: + # set company address + target.update(get_company_address(target.company)) + if target.company_address: target.update(get_fetch_values("Sales Invoice", 'company_address', target.company_address)) diff --git a/erpnext/stock/doctype/delivery_note/regional/india.js b/erpnext/stock/doctype/delivery_note/regional/india.js new file mode 100644 index 00000000000..22f4716ea52 --- /dev/null +++ b/erpnext/stock/doctype/delivery_note/regional/india.js @@ -0,0 +1,4 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Delivery Note'); + diff --git a/erpnext/stock/doctype/purchase_receipt/regional/india.js b/erpnext/stock/doctype/purchase_receipt/regional/india.js new file mode 100644 index 00000000000..b4f1201f36c --- /dev/null +++ b/erpnext/stock/doctype/purchase_receipt/regional/india.js @@ -0,0 +1,3 @@ +{% include "erpnext/regional/india/taxes.js" %} + +erpnext.setup_auto_gst_taxation('Purchase Receipt'); \ No newline at end of file From d1e8e8652f91214d1b951f937d1a9e4037a401f1 Mon Sep 17 00:00:00 2001 From: Saqib Date: Tue, 10 Dec 2019 21:32:57 +0530 Subject: [PATCH 256/299] fix: incorrect account mapping for child companies (#19888) * fix: incorrect account mapping for child companies on adding account to parent company * Update account.py --- erpnext/accounts/doctype/account/account.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/erpnext/accounts/doctype/account/account.py b/erpnext/accounts/doctype/account/account.py index cccced8e0be..cf1748f6a7f 100644 --- a/erpnext/accounts/doctype/account/account.py +++ b/erpnext/accounts/doctype/account/account.py @@ -109,12 +109,13 @@ class Account(NestedSet): if not descendants: return parent_acc_name_map = {} - parent_acc_name = frappe.db.get_value('Account', self.parent_account, "account_name") + parent_acc_name, parent_acc_number = frappe.db.get_value('Account', self.parent_account, \ + ["account_name", "account_number"]) for d in frappe.db.get_values('Account', - {"company": ["in", descendants], "account_name": parent_acc_name}, + { "company": ["in", descendants], "account_name": parent_acc_name, + "account_number": parent_acc_number }, ["company", "name"], as_dict=True): parent_acc_name_map[d["company"]] = d["name"] - if not parent_acc_name_map: return self.create_account_for_child_company(parent_acc_name_map, descendants, parent_acc_name) From ac967d09ecf4fb5bb48ee7a4ff5f30307b959c42 Mon Sep 17 00:00:00 2001 From: rohitwaghchaure Date: Tue, 10 Dec 2019 21:34:20 +0530 Subject: [PATCH 257/299] fix: Item-wise Sales History report not working (#19890) --- .../item_wise_sales_history/item_wise_sales_history.js | 4 ++++ .../item_wise_sales_history/item_wise_sales_history.py | 8 ++++---- 2 files changed, 8 insertions(+), 4 deletions(-) diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js index ee806a78fb4..daca2e3bd0c 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.js @@ -20,11 +20,15 @@ frappe.query_reports["Item-wise Sales History"] = { }, { fieldname:"from_date", + reqd: 1, label: __("From Date"), fieldtype: "Date", + default: frappe.datetime.add_months(frappe.datetime.get_today(), -1), }, { fieldname:"to_date", + reqd: 1, + default: frappe.datetime.get_today(), label: __("To Date"), fieldtype: "Date", }, diff --git a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py index 226c34f735a..1fc3663bed7 100644 --- a/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py +++ b/erpnext/selling/report/item_wise_sales_history/item_wise_sales_history.py @@ -196,6 +196,7 @@ def get_customer_details(): def get_sales_order_details(company_list, filters): conditions = get_conditions(filters) + return frappe.db.sql(""" SELECT so_item.item_code, so_item.item_name, so_item.item_group, @@ -208,7 +209,6 @@ def get_sales_order_details(company_list, filters): `tabSales Order` so, `tabSales Order Item` so_item WHERE so.name = so_item.parent - AND so.company in (%s) - AND so.docstatus = 1 - {0} - """.format(conditions), company_list, as_dict=1) #nosec + AND so.company in ({0}) + AND so.docstatus = 1 {1} + """.format(','.join(["%s"] * len(company_list)), conditions), tuple(company_list), as_dict=1) From caae8c57bcd8b8dfafbf94741b9f8d5123614a8c Mon Sep 17 00:00:00 2001 From: marination Date: Wed, 11 Dec 2019 12:10:05 +0530 Subject: [PATCH 258/299] fix: Disable Rounded Total always showing field default value --- erpnext/public/js/controllers/buying.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/public/js/controllers/buying.js b/erpnext/public/js/controllers/buying.js index 02c30587f67..926227b24c8 100644 --- a/erpnext/public/js/controllers/buying.js +++ b/erpnext/public/js/controllers/buying.js @@ -30,7 +30,7 @@ erpnext.buying.BuyingController = erpnext.TransactionController.extend({ && frappe.meta.has_field(this.frm.doc.doctype, "disable_rounded_total")) { var df = frappe.meta.get_docfield(this.frm.doc.doctype, "disable_rounded_total"); - var disable = df.default || cint(frappe.sys_defaults.disable_rounded_total); + var disable = cint(df.default) || cint(frappe.sys_defaults.disable_rounded_total); this.frm.set_value("disable_rounded_total", disable); } From 0a28387c70c4a2d6c7ff9fe457da9766874c8707 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 11 Dec 2019 12:49:43 +0530 Subject: [PATCH 259/299] fix: set project --- erpnext/projects/doctype/project/project.js | 43 ++++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/erpnext/projects/doctype/project/project.js b/erpnext/projects/doctype/project/project.js index 25c97d1fb84..3570a0f2be4 100644 --- a/erpnext/projects/doctype/project/project.js +++ b/erpnext/projects/doctype/project/project.js @@ -4,20 +4,16 @@ frappe.ui.form.on("Project", { setup(frm) { frm.make_methods = { 'Timesheet': () => { - let doctype = 'Timesheet'; - frappe.model.with_doctype(doctype, () => { - let new_doc = frappe.model.get_new_doc(doctype); - - // add a new row and set the project - let time_log = frappe.model.get_new_doc('Timesheet Detail'); - time_log.project = frm.doc.name; - time_log.parent = new_doc.name; - time_log.parentfield = 'time_logs'; - time_log.parenttype = 'Timesheet'; - new_doc.time_logs = [time_log]; - - frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); - }); + open_form(frm, "Timesheet", "Timesheet Detail", "time_logs"); + }, + 'Purchase Order': () => { + open_form(frm, "Purchase Order", "Purchase Order Item", "items"); + }, + 'Purchase Receipt': () => { + open_form(frm, "Purchase Receipt", "Purchase Receipt Item", "items"); + }, + 'Purchase Invoice': () => { + open_form(frm, "Purchase Invoice", "Purchase Invoice Item", "items"); }, }; }, @@ -80,7 +76,7 @@ frappe.ui.form.on("Project", { frm.events.set_status(frm, 'Cancelled'); }, __('Set Status')); } - + if (frappe.model.can_read("Task")) { frm.add_custom_button(__("Gantt Chart"), function () { frappe.route_options = { @@ -123,3 +119,20 @@ frappe.ui.form.on("Project", { }, }); + +function open_form(frm, doctype, child_doctype, parentfield) { + frappe.model.with_doctype(doctype, () => { + let new_doc = frappe.model.get_new_doc(doctype); + + // add a new row and set the project + let new_child_doc = frappe.model.get_new_doc(child_doctype); + new_child_doc.project = frm.doc.name; + new_child_doc.parent = new_doc.name; + new_child_doc.parentfield = parentfield; + new_child_doc.parenttype = doctype; + new_doc[parentfield] = [new_child_doc]; + + frappe.ui.form.make_quick_entry(doctype, null, null, new_doc); + }); + +} \ No newline at end of file From f23b5ed23b8dd1c3687f34109051407c57d94629 Mon Sep 17 00:00:00 2001 From: Himanshu Warekar Date: Wed, 11 Dec 2019 13:10:31 +0530 Subject: [PATCH 260/299] fix: rename labels --- .../quality_action_resolution/quality_action_resolution.json | 2 +- .../quality_procedure_process/quality_procedure_process.json | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json b/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json index 74370cc3efe..a4e6aed86a0 100644 --- a/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json +++ b/erpnext/quality_management/doctype/quality_action_resolution/quality_action_resolution.json @@ -13,7 +13,7 @@ "fieldname": "problem", "fieldtype": "Long Text", "in_list_view": 1, - "label": "Problem" + "label": "Review" }, { "fieldname": "sb_00", diff --git a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json index f5c0fbc2523..0a67fa505ee 100644 --- a/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json +++ b/erpnext/quality_management/doctype/quality_procedure_process/quality_procedure_process.json @@ -18,7 +18,7 @@ "fieldname": "procedure", "fieldtype": "Link", "in_list_view": 1, - "label": "Procedure", + "label": "Child Procedure", "options": "Quality Procedure" } ], From b69cb8080c5e05f64b83f0315a57c9b8ebacea7f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 11 Dec 2019 13:33:23 +0530 Subject: [PATCH 261/299] fix: not able to cancel the landed cost voucher --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 9b73d0f2711..8e34a8a4797 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -611,7 +611,7 @@ def make_stock_entry(source_name,target_doc=None): def get_item_account_wise_additional_cost(purchase_document): landed_cost_voucher = frappe.get_value("Landed Cost Purchase Receipt", - {"receipt_document": purchase_document}, "parent") + {"receipt_document": purchase_document, "docstatus": 1}, "parent") if not landed_cost_voucher: return From dfe629aff7ee99ef341d531d5b4664b7c8b5da6f Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 11 Dec 2019 15:18:59 +0530 Subject: [PATCH 262/299] fix: change book-appointment route --- erpnext/crm/doctype/appointment/appointment.py | 2 +- .../www/{book-appointment => book_appointment}/index.css | 0 .../www/{book-appointment => book_appointment}/index.html | 0 .../www/{book-appointment => book_appointment}/index.js | 8 ++++---- .../www/{book-appointment => book_appointment}/index.py | 0 .../verify/index.html | 0 .../verify/index.py | 0 7 files changed, 5 insertions(+), 5 deletions(-) rename erpnext/www/{book-appointment => book_appointment}/index.css (100%) rename erpnext/www/{book-appointment => book_appointment}/index.html (100%) rename erpnext/www/{book-appointment => book_appointment}/index.js (96%) rename erpnext/www/{book-appointment => book_appointment}/index.py (100%) rename erpnext/www/{book-appointment => book_appointment}/verify/index.html (100%) rename erpnext/www/{book-appointment => book_appointment}/verify/index.py (100%) diff --git a/erpnext/crm/doctype/appointment/appointment.py b/erpnext/crm/doctype/appointment/appointment.py index 2affba2ac40..b6c4c4707de 100644 --- a/erpnext/crm/doctype/appointment/appointment.py +++ b/erpnext/crm/doctype/appointment/appointment.py @@ -171,7 +171,7 @@ class Appointment(Document): self.save(ignore_permissions=True) def _get_verify_url(self): - verify_route = '/book-appointment/verify' + verify_route = '/book_appointment/verify' params = { 'email': self.customer_email, 'appointment': self.name diff --git a/erpnext/www/book-appointment/index.css b/erpnext/www/book_appointment/index.css similarity index 100% rename from erpnext/www/book-appointment/index.css rename to erpnext/www/book_appointment/index.css diff --git a/erpnext/www/book-appointment/index.html b/erpnext/www/book_appointment/index.html similarity index 100% rename from erpnext/www/book-appointment/index.html rename to erpnext/www/book_appointment/index.html diff --git a/erpnext/www/book-appointment/index.js b/erpnext/www/book_appointment/index.js similarity index 96% rename from erpnext/www/book-appointment/index.js rename to erpnext/www/book_appointment/index.js index 13c87ddbcff..c8dd5013d5c 100644 --- a/erpnext/www/book-appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -15,10 +15,10 @@ async function initialise_select_date() { async function get_global_variables() { // Using await through this file instead of then. window.appointment_settings = (await frappe.call({ - method: 'erpnext.www.book-appointment.index.get_appointment_settings' + 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; } @@ -79,7 +79,7 @@ function on_date_or_timezone_select() { async function get_time_slots(date, timezone) { let slots = (await frappe.call({ - method: 'erpnext.www.book-appointment.index.get_appointment_slots', + method: 'erpnext.www.book_appointment.index.get_appointment_slots', args: { date: date, timezone: timezone @@ -201,7 +201,7 @@ async function submit() { } let contact = get_form_data(); let appointment = frappe.call({ - method: 'erpnext.www.book-appointment.index.create_appointment', + method: 'erpnext.www.book_appointment.index.create_appointment', args: { 'date': window.selected_date, 'time': window.selected_time, diff --git a/erpnext/www/book-appointment/index.py b/erpnext/www/book_appointment/index.py similarity index 100% rename from erpnext/www/book-appointment/index.py rename to erpnext/www/book_appointment/index.py diff --git a/erpnext/www/book-appointment/verify/index.html b/erpnext/www/book_appointment/verify/index.html similarity index 100% rename from erpnext/www/book-appointment/verify/index.html rename to erpnext/www/book_appointment/verify/index.html diff --git a/erpnext/www/book-appointment/verify/index.py b/erpnext/www/book_appointment/verify/index.py similarity index 100% rename from erpnext/www/book-appointment/verify/index.py rename to erpnext/www/book_appointment/verify/index.py From 6f36691c64a8d3883a6e21750f3ed2d4f7bae915 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 16:08:31 +0530 Subject: [PATCH 263/299] fix: handle scenario with no condition --- erpnext/portal/product_configurator/utils.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index ab4b82a688b..3ed68362ed2 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -335,12 +335,14 @@ def get_items(filters=None, search=None): search_condition = get_conditions(or_filters, 'or') filter_condition = get_conditions(filters, 'and') - - where_conditions = 'disabled = 0 and ' - where_conditions += ' and '.join( + where_conditions = ' and '.join( [condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] ) + if where_conditions: + where_conditions += ' and disabled = 0' + else: + where_conditions += 'disabled = 0' left_joins = [] for f in filters: From af10f659d9eb574ada5ba3b2e52436beb282f32a Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 17:43:04 +0530 Subject: [PATCH 264/299] fix: enable address without checkout feature * fix add address form country link field --- .../templates/includes/cart/cart_address.html | 39 ++++++++++++------- erpnext/templates/pages/cart.html | 2 - 2 files changed, 25 insertions(+), 16 deletions(-) diff --git a/erpnext/templates/includes/cart/cart_address.html b/erpnext/templates/includes/cart/cart_address.html index fe53f34dba5..f7f35483208 100644 --- a/erpnext/templates/includes/cart/cart_address.html +++ b/erpnext/templates/includes/cart/cart_address.html @@ -64,16 +64,6 @@ frappe.ready(() => { fieldtype: 'Data', reqd: 1 }, - { - label: __('Address Type'), - fieldname: 'address_type', - fieldtype: 'Select', - options: [ - 'Billing', - 'Shipping' - ], - reqd: 1 - }, { label: __('Address Line 1'), fieldname: 'address_line1', @@ -96,16 +86,37 @@ frappe.ready(() => { fieldname: 'state', fieldtype: 'Data' }, + { + label: __('Country'), + fieldname: 'country', + fieldtype: 'Link', + options: 'Country', + reqd: 1 + }, + { + fieldname: "column_break0", + fieldtype: "Column Break", + width: "50%" + }, + { + label: __('Address Type'), + fieldname: 'address_type', + fieldtype: 'Select', + options: [ + 'Billing', + 'Shipping' + ], + reqd: 1 + }, { label: __('Pin Code'), fieldname: 'pincode', fieldtype: 'Data' }, { - label: __('Country'), - fieldname: 'country', - fieldtype: 'Link', - reqd: 1 + fieldname: "phone", + fieldtype: "Data", + label: "Phone" }, ], primary_action_label: __('Save'), diff --git a/erpnext/templates/pages/cart.html b/erpnext/templates/pages/cart.html index b301fc0665a..912702e386d 100644 --- a/erpnext/templates/pages/cart.html +++ b/erpnext/templates/pages/cart.html @@ -83,12 +83,10 @@
{% endif %} - {% if cart_settings.enable_checkout %}
{% include "templates/includes/cart/cart_address.html" %}
{% endif %} - {% endif %}
From 1de3040ecbd088c7f467a9b762a9fbbe9a1eff81 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Wed, 11 Dec 2019 18:11:48 +0530 Subject: [PATCH 265/299] fix: additional notes from Quotations not saved in SO --- .../sales_order_item/sales_order_item.json | 17 +++++++++++++++-- 1 file changed, 15 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/doctype/sales_order_item/sales_order_item.json b/erpnext/selling/doctype/sales_order_item/sales_order_item.json index 3fd1e6461e1..317f284eff3 100644 --- a/erpnext/selling/doctype/sales_order_item/sales_order_item.json +++ b/erpnext/selling/doctype/sales_order_item/sales_order_item.json @@ -76,10 +76,12 @@ "ordered_qty", "planned_qty", "column_break_69", - "delivered_qty", "work_order_qty", + "delivered_qty", "produced_qty", "returned_qty", + "shopping_cart_section", + "additional_notes", "section_break_63", "page_break", "item_tax_rate", @@ -741,11 +743,22 @@ "fieldname": "image_section", "fieldtype": "Section Break", "label": "Image" + }, + { + "collapsible": 1, + "fieldname": "shopping_cart_section", + "fieldtype": "Section Break", + "label": "Shopping Cart" + }, + { + "fieldname": "additional_notes", + "fieldtype": "Text", + "label": "Additional Notes" } ], "idx": 1, "istable": 1, - "modified": "2019-10-10 08:46:26.244823", + "modified": "2019-12-11 18:06:26.238169", "modified_by": "Administrator", "module": "Selling", "name": "Sales Order Item", From 234de128361b87c992501f225217f93cdbe9787b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 12 Dec 2019 11:20:31 +0530 Subject: [PATCH 266/299] fix: add init files for book-appointments --- erpnext/www/book_appointment/__init__.py | 0 erpnext/www/book_appointment/index.html | 2 +- erpnext/www/book_appointment/verify/__init__.py | 0 3 files changed, 1 insertion(+), 1 deletion(-) create mode 100644 erpnext/www/book_appointment/__init__.py create mode 100644 erpnext/www/book_appointment/verify/__init__.py diff --git a/erpnext/www/book_appointment/__init__.py b/erpnext/www/book_appointment/__init__.py new file mode 100644 index 00000000000..e69de29bb2d diff --git a/erpnext/www/book_appointment/index.html b/erpnext/www/book_appointment/index.html index 96774d5656b..f242f43ad54 100644 --- a/erpnext/www/book_appointment/index.html +++ b/erpnext/www/book_appointment/index.html @@ -4,7 +4,7 @@ {% block script %} - + {% endblock %} {% block page_content %} diff --git a/erpnext/www/book_appointment/verify/__init__.py b/erpnext/www/book_appointment/verify/__init__.py new file mode 100644 index 00000000000..e69de29bb2d From 9cc484650b0c10d8d9ea3460bb6e119d71fd1215 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Thu, 12 Dec 2019 12:10:25 +0530 Subject: [PATCH 267/299] fix: not able to submit the landed cost voucher --- erpnext/stock/doctype/purchase_receipt/purchase_receipt.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py index 8e34a8a4797..060175f9045 100644 --- a/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py +++ b/erpnext/stock/doctype/purchase_receipt/purchase_receipt.py @@ -245,7 +245,7 @@ class PurchaseReceipt(BuyingController): negative_expense_to_be_booked += flt(d.item_tax_amount) # Amount added through landed-cost-voucher - if landed_cost_entries: + if d.landed_cost_voucher_amount and landed_cost_entries: for account, amount in iteritems(landed_cost_entries[(d.item_code, d.name)]): gl_entries.append(self.get_gl_dict({ "account": account, @@ -622,8 +622,7 @@ def get_item_account_wise_additional_cost(purchase_document): based_on_field = frappe.scrub(landed_cost_voucher_doc.distribute_charges_based_on) for item in landed_cost_voucher_doc.items: - if item.receipt_document == purchase_document: - total_item_cost += item.get(based_on_field) + total_item_cost += item.get(based_on_field) for item in landed_cost_voucher_doc.items: if item.receipt_document == purchase_document: From e03871f9def831b31b05ab3c78c9d38f667fc3d2 Mon Sep 17 00:00:00 2001 From: prssanna Date: Thu, 12 Dec 2019 12:22:03 +0530 Subject: [PATCH 268/299] fix: empty fname and fcontent of uploaded file --- .../doctype/bank_transaction/bank_transaction_upload.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py index deedafdfb5d..33ae45439e7 100644 --- a/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py +++ b/erpnext/accounts/doctype/bank_transaction/bank_transaction_upload.py @@ -15,8 +15,8 @@ def upload_bank_statement(): with open(frappe.uploaded_file, "rb") as upfile: fcontent = upfile.read() else: - from frappe.utils.file_manager import get_uploaded_content - fname, fcontent = get_uploaded_content() + fcontent = frappe.local.uploaded_file + fname = frappe.local.uploaded_filename if frappe.safe_encode(fname).lower().endswith("csv".encode('utf-8')): from frappe.utils.csvutils import read_csv_content From 0a527b9f9a34e52bcbae33c053f4f84b6766db27 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 12 Dec 2019 14:55:57 +0530 Subject: [PATCH 269/299] fix: Get regional address details fix --- erpnext/regional/india/utils.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/erpnext/regional/india/utils.py b/erpnext/regional/india/utils.py index 77bcc80abab..0f9156a6b4c 100644 --- a/erpnext/regional/india/utils.py +++ b/erpnext/regional/india/utils.py @@ -179,6 +179,8 @@ def get_regional_address_details(party_details, doctype, company, return_taxes=N if not party_details.place_of_supply: return + if not party_details.company_gstin: return + if ((doctype in ("Sales Invoice", "Delivery Note", "Sales Order") and party_details.company_gstin and party_details.company_gstin[:2] != party_details.place_of_supply[:2]) or (doctype in ("Purchase Invoice", "Purchase Order", "Purchase Receipt") and party_details.supplier_gstin and party_details.supplier_gstin[:2] != party_details.place_of_supply[:2])): From 94d8b99ef9aead49b814a12a9606495ae01bacb1 Mon Sep 17 00:00:00 2001 From: marination Date: Thu, 12 Dec 2019 16:34:41 +0530 Subject: [PATCH 270/299] fix: Distribute charges based on quantity if Total Basic Amount is Zero. --- .../stock/doctype/stock_entry/stock_entry.py | 12 +++--- .../doctype/stock_entry/test_stock_entry.py | 39 +++++++++++++------ 2 files changed, 35 insertions(+), 16 deletions(-) diff --git a/erpnext/stock/doctype/stock_entry/stock_entry.py b/erpnext/stock/doctype/stock_entry/stock_entry.py index 913656ad020..00d27ef232b 100644 --- a/erpnext/stock/doctype/stock_entry/stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/stock_entry.py @@ -27,7 +27,6 @@ class IncorrectValuationRateError(frappe.ValidationError): pass class DuplicateEntryForWorkOrderError(frappe.ValidationError): pass class OperationsNotCompleteError(frappe.ValidationError): pass class MaxSampleAlreadyRetainedError(frappe.ValidationError): pass -class TotalBasicAmountZeroError(frappe.ValidationError): pass from erpnext.controllers.stock_controller import StockController @@ -650,11 +649,11 @@ class StockEntry(StockController): gl_entries = super(StockEntry, self).get_gl_entries(warehouse_account) total_basic_amount = sum([flt(t.basic_amount) for t in self.get("items") if t.t_warehouse]) + divide_based_on = total_basic_amount if self.get("additional_costs") and not total_basic_amount: - #If additional costs table is populated and total basic amount is - #somehow 0, interrupt transaction. - frappe.throw(_("Total Basic Amount in Items Table cannot be 0"), TotalBasicAmountZeroError) + # if total_basic_amount is 0, distribute additional charges based on qty + divide_based_on = sum(item.qty for item in list(self.get("items"))) item_account_wise_additional_cost = {} @@ -663,8 +662,11 @@ class StockEntry(StockController): if d.t_warehouse: item_account_wise_additional_cost.setdefault((d.item_code, d.name), {}) item_account_wise_additional_cost[(d.item_code, d.name)].setdefault(t.expense_account, 0.0) + + multiply_based_on = d.basic_amount if total_basic_amount else d.qty + item_account_wise_additional_cost[(d.item_code, d.name)][t.expense_account] += \ - (t.amount * d.basic_amount) / total_basic_amount + (t.amount * multiply_based_on) / divide_based_on if item_account_wise_additional_cost: for d in self.get("items"): diff --git a/erpnext/stock/doctype/stock_entry/test_stock_entry.py b/erpnext/stock/doctype/stock_entry/test_stock_entry.py index c5e67092d3d..ee5f2370987 100644 --- a/erpnext/stock/doctype/stock_entry/test_stock_entry.py +++ b/erpnext/stock/doctype/stock_entry/test_stock_entry.py @@ -16,7 +16,6 @@ from erpnext.stock.doctype.stock_entry.stock_entry_utils import make_stock_entry from erpnext.accounts.doctype.account.test_account import get_inventory_account from erpnext.stock.doctype.stock_entry.stock_entry import move_sample_to_retention_warehouse, make_stock_in_entry from erpnext.stock.doctype.stock_reconciliation.stock_reconciliation import OpeningEntryAccountError -from erpnext.stock.doctype.stock_entry.stock_entry import TotalBasicAmountZeroError from six import iteritems def get_sle(**args): @@ -798,14 +797,26 @@ class TestStockEntry(unittest.TestCase): "posting_date": nowdate(), "company":"_Test Company with perpetual inventory", "items":[ - {"item_code":"Basil Leaves", - "description":"Basil Leaves", - "qty": 1, - "basic_rate": 0, - "uom":"Nos", - "t_warehouse": "Stores - TCP1", - "allow_zero_valuation_rate": 1, - "cost_center": "Main - TCP1"} + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 1, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, + { + "item_code":"Basil Leaves", + "description":"Basil Leaves", + "qty": 2, + "basic_rate": 0, + "uom":"Nos", + "t_warehouse": "Stores - TCP1", + "allow_zero_valuation_rate": 1, + "cost_center": "Main - TCP1" + }, ], "additional_costs":[ {"expense_account":"Miscellaneous Expenses - TCP1", @@ -813,9 +824,15 @@ class TestStockEntry(unittest.TestCase): "description": "miscellanous"} ] }) - se.insert() - self.assertRaises(TotalBasicAmountZeroError, se.submit) + se.submit() + + self.check_gl_entries("Stock Entry", se.name, + sorted([ + ["Stock Adjustment - TCP1", 100.0, 0.0], + ["Miscellaneous Expenses - TCP1", 0.0, 100.0] + ]) + ) def make_serialized_item(item_code=None, serial_no=None, target_warehouse=None): se = frappe.copy_doc(test_records[0]) From 9097c7e11c46d5284e67e39462d9ecb2dad658b4 Mon Sep 17 00:00:00 2001 From: Ben Knowles Date: Thu, 12 Dec 2019 11:30:17 -0600 Subject: [PATCH 271/299] fix: task validation error when adding tasks to projects Related to PR #19919 --- erpnext/projects/doctype/task/task.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/projects/doctype/task/task.py b/erpnext/projects/doctype/task/task.py index 7083d694f80..45f26814a65 100755 --- a/erpnext/projects/doctype/task/task.py +++ b/erpnext/projects/doctype/task/task.py @@ -47,11 +47,11 @@ class Task(NestedSet): if not self.project or frappe.flags.in_test: return - expected_end_date = getdate(frappe.db.get_value("Project", self.project, "expected_end_date")) + expected_end_date = frappe.db.get_value("Project", self.project, "expected_end_date") if expected_end_date: - validate_project_dates(expected_end_date, self, "exp_start_date", "exp_end_date", "Expected") - validate_project_dates(expected_end_date, self, "act_start_date", "act_end_date", "Actual") + validate_project_dates(getdate(expected_end_date), self, "exp_start_date", "exp_end_date", "Expected") + validate_project_dates(getdate(expected_end_date), self, "act_start_date", "act_end_date", "Actual") def validate_status(self): if self.status!=self.get_db_value("status") and self.status == "Completed": @@ -278,4 +278,4 @@ def validate_project_dates(project_end_date, task, task_start, task_end, actual_ frappe.throw(_("Task's {0} Start Date cannot be after Project's End Date.").format(actual_or_expected_date)) if task.get(task_end) and date_diff(project_end_date, getdate(task.get(task_end))) < 0: - frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) \ No newline at end of file + frappe.throw(_("Task's {0} End Date cannot be after Project's End Date.").format(actual_or_expected_date)) From 9ec5cb2570c146dc19a4b3de2dd287a109c5cc2a Mon Sep 17 00:00:00 2001 From: marination Date: Fri, 13 Dec 2019 13:12:10 +0530 Subject: [PATCH 272/299] fix: Removed 'manufacturers' table from Item Master --- erpnext/stock/doctype/item/item.json | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/erpnext/stock/doctype/item/item.json b/erpnext/stock/doctype/item/item.json index a2aab3f69ee..af8e13288a9 100644 --- a/erpnext/stock/doctype/item/item.json +++ b/erpnext/stock/doctype/item/item.json @@ -135,8 +135,7 @@ "publish_in_hub", "hub_category_to_publish", "hub_warehouse", - "synced_with_hub", - "manufacturers" + "synced_with_hub" ], "fields": [ { @@ -1016,12 +1015,6 @@ "label": "Synced With Hub", "read_only": 1 }, - { - "fieldname": "manufacturers", - "fieldtype": "Table", - "label": "Manufacturers", - "options": "Item Manufacturer" - }, { "depends_on": "eval:!doc.__islocal", "fieldname": "over_delivery_receipt_allowance", @@ -1049,7 +1042,7 @@ "idx": 2, "image_field": "image", "max_attachments": 1, - "modified": "2019-10-09 17:05:59.576119", + "modified": "2019-12-13 12:15:56.197246", "modified_by": "Administrator", "module": "Stock", "name": "Item", From b7329eac196772bcd8897c1138e6e1d2ada11711 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Fri, 13 Dec 2019 13:47:15 +0530 Subject: [PATCH 273/299] fix: Price rule filtering fix --- erpnext/accounts/doctype/pricing_rule/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 637e503e658..7af6748254f 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -284,7 +284,7 @@ def filter_pricing_rules_for_qty_amount(qty, rate, pricing_rules, args=None): status = True # if user has created item price against the transaction UOM - if rule.get("uom") == args.get("uom"): + if args and rule.get("uom") == args.get("uom"): conversion_factor = 1.0 if status and (flt(rate) >= (flt(rule.min_amt) * conversion_factor) From 980793bde09ef8e3954cd7fdea5d10f7fa1e89b0 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Fri, 13 Dec 2019 15:09:51 +0530 Subject: [PATCH 274/299] fix: gl entries doesn't filter based on debit precision --- erpnext/accounts/general_ledger.py | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/erpnext/accounts/general_ledger.py b/erpnext/accounts/general_ledger.py index feb598a2e51..bb1b7e392dc 100644 --- a/erpnext/accounts/general_ledger.py +++ b/erpnext/accounts/general_ledger.py @@ -90,8 +90,12 @@ def merge_similar_entries(gl_map): else: merged_gl_map.append(entry) + company = gl_map[0].company if gl_map else erpnext.get_default_company() + company_currency = erpnext.get_company_currency(company) + precision = get_field_precision(frappe.get_meta("GL Entry").get_field("debit"), company_currency) + # filter zero debit and credit entries - merged_gl_map = filter(lambda x: flt(x.debit, 9)!=0 or flt(x.credit, 9)!=0, merged_gl_map) + merged_gl_map = filter(lambda x: flt(x.debit, precision)!=0 or flt(x.credit, precision)!=0, merged_gl_map) merged_gl_map = list(merged_gl_map) return merged_gl_map From 6a8ff1bebe953ac0f3bb912c8a12943f8603b314 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Sat, 14 Dec 2019 21:25:30 +0530 Subject: [PATCH 275/299] fix: Add missing import --- erpnext/patches/v12_0/set_gst_category.py | 2 ++ erpnext/setup/doctype/company/company.py | 1 + 2 files changed, 3 insertions(+) diff --git a/erpnext/patches/v12_0/set_gst_category.py b/erpnext/patches/v12_0/set_gst_category.py index 54bc5b3c74f..55bbdee7edf 100644 --- a/erpnext/patches/v12_0/set_gst_category.py +++ b/erpnext/patches/v12_0/set_gst_category.py @@ -7,6 +7,8 @@ def execute(): if not company: return + frappe.reload_doc('accounts', 'doctype', 'Tax Category') + make_custom_fields() for doctype in ['Sales Invoice', 'Purchase Invoice']: diff --git a/erpnext/setup/doctype/company/company.py b/erpnext/setup/doctype/company/company.py index 2eee919b530..ff3515485c4 100644 --- a/erpnext/setup/doctype/company/company.py +++ b/erpnext/setup/doctype/company/company.py @@ -14,6 +14,7 @@ from frappe.model.document import Document from frappe.contacts.address_and_contact import load_address_and_contact from frappe.utils.nestedset import NestedSet +from past.builtins import cmp import functools class Company(NestedSet): From 5cd8c7c722aa36e151315b8308a61dff8de8c1bb Mon Sep 17 00:00:00 2001 From: Suraj Shetty Date: Thu, 12 Dec 2019 13:06:17 +0530 Subject: [PATCH 276/299] fix: Financial statement report - Hidden column should note be considered in the report - Remove hardcoded currency formatting - Remove duplicate letterhead in the report (print_template already adds one) - Remove extra quotes from Total Amount text --- .../accounts/report/financial_statements.html | 34 +++++++++++-------- .../accounts/report/financial_statements.py | 4 +-- 2 files changed, 21 insertions(+), 17 deletions(-) diff --git a/erpnext/accounts/report/financial_statements.html b/erpnext/accounts/report/financial_statements.html index 4081723bf0f..50947ecf5ef 100644 --- a/erpnext/accounts/report/financial_statements.html +++ b/erpnext/accounts/report/financial_statements.html @@ -1,5 +1,6 @@ {% var report_columns = report.get_columns_for_print(); + report_columns = report_columns.filter(col => !col.hidden); if (report_columns.length > 8) { frappe.throw(__("Too many columns. Export the report and print it using a spreadsheet application.")); @@ -15,34 +16,35 @@ height: 37px; } -{% var letterhead= filters.letter_head || (frappe.get_doc(":Company", filters.company) && frappe.get_doc(":Company", filters.company).default_letter_head) %} -{% if(letterhead) { %} -
- {%= frappe.boot.letter_heads[letterhead].header %} -
-{% } %} +

{%= __(report.report_name) %}

{%= filters.company %}

+ {% if 'cost_center' in filters %}

{%= filters.cost_center %}

{% endif %} +

{%= filters.fiscal_year %}

-
{%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %}
+
+ {%= __("Currency") %} : {%= filters.presentation_currency || erpnext.get_currency(filters.company) %} +
{% if (filters.from_date) { %} -

{%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %}

+
+ {%= frappe.datetime.str_to_user(filters.from_date) %} - {%= frappe.datetime.str_to_user(filters.to_date) %} +
{% } %}
- - {% for(var i=2, l=report_columns.length; i + {% for (let i=1, l=report_columns.length; i{%= report_columns[i].label %} {% } %} - {% for(var j=0, k=data.length-1; j {%= row.account_name %} - {% for(var i=2, l=report_columns.length; i - {% var fieldname = report_columns[i].fieldname; %} + {% const fieldname = report_columns[i].fieldname; %} {% if (!is_null(row[fieldname])) { %} - {%= format_currency(row[fieldname], filters.presentation_currency) %} + {%= frappe.format(row[fieldname], report_columns[i], {}, row) %} {% } %} {% } %} @@ -64,4 +66,6 @@ {% } %}
-

Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %}

+

+ Printed On {%= frappe.datetime.str_to_user(frappe.datetime.get_datetime_as_string()) %} +

diff --git a/erpnext/accounts/report/financial_statements.py b/erpnext/accounts/report/financial_statements.py index 3c8de6026a6..40d5682726d 100644 --- a/erpnext/accounts/report/financial_statements.py +++ b/erpnext/accounts/report/financial_statements.py @@ -264,8 +264,8 @@ def filter_out_zero_value_rows(data, parent_children_map, show_zero_values=False def add_total_row(out, root_type, balance_must_be, period_list, company_currency): total_row = { - "account_name": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", - "account": "'" + _("Total {0} ({1})").format(_(root_type), _(balance_must_be)) + "'", + "account_name": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), + "account": _("Total {0} ({1})").format(_(root_type), _(balance_must_be)), "currency": company_currency } From 4e8e466a98a84f18c0e40d63820d849ebbe90561 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 13 Dec 2019 16:18:38 +0530 Subject: [PATCH 277/299] fix: pricing rule not working for production discount --- .../doctype/pricing_rule/pricing_rule.json | 9 ++- .../doctype/pricing_rule/pricing_rule.py | 12 ++-- .../accounts/doctype/pricing_rule/utils.py | 57 +++++++++++++------ erpnext/controllers/accounts_controller.py | 4 +- erpnext/controllers/buying_controller.py | 8 +-- erpnext/public/js/controllers/transaction.js | 18 ++++++ 6 files changed, 76 insertions(+), 32 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index 971d308368a..f73fb10d320 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -1,4 +1,5 @@ { + "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:title", @@ -439,19 +440,20 @@ }, { "default": "0", - "depends_on": "eval:!doc.mixed_conditions", + "depends_on": "eval:!doc.mixed_conditions && doc.price_or_product_discount == 'Price'", "fieldname": "same_item", "fieldtype": "Check", "label": "Same Item" }, { - "depends_on": "eval:!doc.same_item || doc.mixed_conditions", + "depends_on": "eval:(!doc.same_item || doc.apply_on == 'Transaction') || doc.mixed_conditions", "fieldname": "free_item", "fieldtype": "Link", "label": "Free Item", "options": "Item" }, { + "default": "0", "fieldname": "free_qty", "fieldtype": "Float", "label": "Qty" @@ -554,7 +556,8 @@ ], "icon": "fa fa-gift", "idx": 1, - "modified": "2019-10-15 12:39:40.399792", + "links": [], + "modified": "2019-12-13 15:48:48.331495", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 430dce7ddbb..a37026412f1 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -183,7 +183,7 @@ def get_serial_no_for_item(args): def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=False): from erpnext.accounts.doctype.pricing_rule.utils import (get_pricing_rules, - get_applied_pricing_rules, get_pricing_rule_items) + get_applied_pricing_rules, get_pricing_rule_items, get_product_discount_rule) if isinstance(doc, string_types): doc = json.loads(doc) @@ -242,9 +242,11 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.coupon_code_based==1 and args.coupon_code==None: return item_details - if (not pricing_rule.validate_applied_rule and - pricing_rule.price_or_product_discount == "Price"): - apply_price_discount_pricing_rule(pricing_rule, item_details, args) + if not pricing_rule.validate_applied_rule: + if pricing_rule.price_or_product_discount == "Price": + apply_price_discount_rule(pricing_rule, item_details, args) + else: + get_product_discount_rule(pricing_rule, item_details) item_details.has_pricing_rule = 1 @@ -294,7 +296,7 @@ def get_pricing_rule_details(args, pricing_rule): 'child_docname': args.get('child_docname') }) -def apply_price_discount_pricing_rule(pricing_rule, item_details, args): +def apply_price_discount_rule(pricing_rule, item_details, args): item_details.pricing_rule_for = pricing_rule.rate_or_discount if ((pricing_rule.margin_type == 'Amount' and pricing_rule.currency == args.currency) diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index 7af6748254f..c8afc768da1 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe, copy, json from frappe import throw, _ from six import string_types -from frappe.utils import flt, cint, get_datetime +from frappe.utils import flt, cint, get_datetime, get_link_to_form from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor @@ -408,7 +408,8 @@ def apply_pricing_rule_on_transaction(doc): conditions = get_other_conditions(conditions, values, doc) pricing_rules = frappe.db.sql(""" Select `tabPricing Rule`.* from `tabPricing Rule` - where {conditions} """.format(conditions = conditions), values, as_dict=1) + where {conditions} and `tabPricing Rule`.disable = 0 + """.format(conditions = conditions), values, as_dict=1) if pricing_rules: pricing_rules = filter_pricing_rules_for_qty_amount(doc.total_qty, @@ -420,39 +421,59 @@ def apply_pricing_rule_on_transaction(doc): doc.set('apply_discount_on', d.apply_discount_on) for field in ['additional_discount_percentage', 'discount_amount']: - if not d.get(field): continue - pr_field = ('discount_percentage' if field == 'additional_discount_percentage' else field) + if not d.get(pr_field): continue + if d.validate_applied_rule and doc.get(field) < d.get(pr_field): frappe.msgprint(_("User has not applied rule on the invoice {0}") .format(doc.name)) else: doc.set(field, d.get(pr_field)) + + doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': - apply_pricing_rule_for_free_items(doc, d) + item_details = frappe._dict() + get_product_discount_rule(d, item_details) + apply_pricing_rule_for_free_items(doc, item_details.free_item_data) + doc.set_missing_values() def get_applied_pricing_rules(item_row): return (item_row.get("pricing_rules").split(',') if item_row.get("pricing_rules") else []) -def apply_pricing_rule_for_free_items(doc, pricing_rule): - if pricing_rule.get('free_item'): +def get_product_discount_rule(pricing_rule, item_details): + free_item = (pricing_rule.free_item + if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code) + + if not free_item: + frappe.throw(_("Free item not set in the pricing rule {0}") + .format(get_link_to_form("Pricing Rule", pricing_rule.name))) + + item_details.free_item_data = { + 'item_code': free_item, + 'qty': pricing_rule.free_qty or 1, + 'rate': pricing_rule.free_item_rate or 0, + 'price_list_rate': pricing_rule.free_item_rate or 0, + 'is_free_item': 1 + } + + item_data = frappe.get_cached_value('Item', free_item, ['item_name', + 'description', 'stock_uom'], as_dict=1) + + item_details.free_item_data.update(item_data) + item_details.free_item_data['uom'] = pricing_rule.free_item_uom or item_data.stock_uom + item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, + item_details.free_item_data['uom']).get("conversion_factor", 1) + +def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): + if pricing_rule_args.get('item_code'): items = [d.item_code for d in doc.items - if d.item_code == (d.item_code - if pricing_rule.get('same_item') else pricing_rule.get('free_item')) and d.is_free_item] + if d.item_code == (pricing_rule_args.get("item_code")) and d.is_free_item] if not items: - doc.append('items', { - 'item_code': pricing_rule.get('free_item'), - 'qty': pricing_rule.get('free_qty'), - 'uom': pricing_rule.get('free_item_uom'), - 'rate': pricing_rule.get('free_item_rate') or 0, - 'is_free_item': 1 - }) - - doc.set_missing_values() + doc.append('items', pricing_rule_args) def get_pricing_rule_items(pr_doc): apply_on_data = [] diff --git a/erpnext/controllers/accounts_controller.py b/erpnext/controllers/accounts_controller.py index 75564afe59e..6150516ac8c 100644 --- a/erpnext/controllers/accounts_controller.py +++ b/erpnext/controllers/accounts_controller.py @@ -319,8 +319,8 @@ class AccountsController(TransactionBase): if item.get('discount_amount'): item.rate = item.price_list_rate - item.discount_amount - elif pricing_rule_args.get('free_item'): - apply_pricing_rule_for_free_items(self, pricing_rule_args) + elif pricing_rule_args.get('free_item_data'): + apply_pricing_rule_for_free_items(self, pricing_rule_args.get('free_item_data')) elif pricing_rule_args.get("validate_applied_rule"): for pricing_rule in get_applied_pricing_rules(item): diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3ec7aff9cbb..17fba8ec4ff 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -735,10 +735,6 @@ class BuyingController(StockController): if not self.get("items"): return - earliest_schedule_date = min([d.schedule_date for d in self.get("items")]) - if earliest_schedule_date: - self.schedule_date = earliest_schedule_date - if self.schedule_date: for d in self.get('items'): if not d.schedule_date: @@ -750,6 +746,10 @@ class BuyingController(StockController): else: frappe.throw(_("Please enter Reqd by Date")) + earliest_schedule_date = min([d.schedule_date for d in self.get("items")]) + if earliest_schedule_date: + self.schedule_date = earliest_schedule_date + def validate_items(self): # validate items to see if they have is_purchase_item or is_subcontracted_item enabled if self.doctype=="Material Request": return diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 5da949320a1..877a243967c 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -1305,6 +1305,10 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ me.remove_pricing_rule(frappe.get_doc(d.doctype, d.name)); } + if (d.free_item_data) { + me.apply_product_discount(d.free_item_data); + } + if (d.apply_rule_on_other_items) { items_rule_dict[d.name] = d; } @@ -1334,6 +1338,20 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ } }, + apply_product_discount: function(free_item_data) { + const items = this.frm.doc.items.filter(d => (d.item_code == free_item_data.item_code + && d.is_free_item)) || []; + + if (!items.length) { + let row_to_modify = frappe.model.add_child(this.frm.doc, + this.frm.doc.doctype + ' Item', 'items'); + + for (let key in free_item_data) { + row_to_modify[key] = free_item_data[key]; + } + } + }, + apply_price_list: function(item, reset_plc_conversion) { // We need to reset plc_conversion_rate sometimes because the call to // `erpnext.stock.get_item_details.apply_price_list` is sensitive to its value From 2b8df06f8e5174acc546c9e2bfa5cb513611adf0 Mon Sep 17 00:00:00 2001 From: marination Date: Mon, 16 Dec 2019 12:08:41 +0530 Subject: [PATCH 278/299] fix: Removed validation from non existent manufacturers table --- erpnext/stock/doctype/item/item.py | 8 -------- 1 file changed, 8 deletions(-) diff --git a/erpnext/stock/doctype/item/item.py b/erpnext/stock/doctype/item/item.py index 7495dffec24..efcede1708d 100644 --- a/erpnext/stock/doctype/item/item.py +++ b/erpnext/stock/doctype/item/item.py @@ -125,7 +125,6 @@ class Item(WebsiteGenerator): self.validate_auto_reorder_enabled_in_stock_settings() self.cant_change() self.update_show_in_website() - self.validate_manufacturer() if not self.get("__islocal"): self.old_item_group = frappe.db.get_value(self.doctype, self.name, "item_group") @@ -145,13 +144,6 @@ class Item(WebsiteGenerator): if cint(frappe.db.get_single_value('Stock Settings', 'clean_description_html')): self.description = clean_html(self.description) - def validate_manufacturer(self): - list_man = [(x.manufacturer, x.manufacturer_part_no) for x in self.get('manufacturers')] - set_man = set(list_man) - - if len(list_man) != len(set_man): - frappe.throw(_("Duplicate entry in Manufacturers table")) - def validate_customer_provided_part(self): if self.is_customer_provided_item: if self.is_purchase_item: From 821166c6285737db63a8d2d6c1a4041e2656fb7e Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 12:29:07 +0530 Subject: [PATCH 279/299] fix: schedule date --- .../accounts/doctype/pricing_rule/pricing_rule.py | 2 +- erpnext/accounts/doctype/pricing_rule/utils.py | 14 ++++++++++---- erpnext/controllers/buying_controller.py | 8 ++++---- 3 files changed, 15 insertions(+), 9 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index a37026412f1..03641028e6f 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -246,7 +246,7 @@ def get_pricing_rule_for_item(args, price_list_rate=0, doc=None, for_validate=Fa if pricing_rule.price_or_product_discount == "Price": apply_price_discount_rule(pricing_rule, item_details, args) else: - get_product_discount_rule(pricing_rule, item_details) + get_product_discount_rule(pricing_rule, item_details, doc) item_details.has_pricing_rule = 1 diff --git a/erpnext/accounts/doctype/pricing_rule/utils.py b/erpnext/accounts/doctype/pricing_rule/utils.py index c8afc768da1..87f68225fe5 100644 --- a/erpnext/accounts/doctype/pricing_rule/utils.py +++ b/erpnext/accounts/doctype/pricing_rule/utils.py @@ -7,7 +7,7 @@ from __future__ import unicode_literals import frappe, copy, json from frappe import throw, _ from six import string_types -from frappe.utils import flt, cint, get_datetime, get_link_to_form +from frappe.utils import flt, cint, get_datetime, get_link_to_form, today from erpnext.setup.doctype.item_group.item_group import get_child_item_groups from erpnext.stock.doctype.warehouse.warehouse import get_child_warehouses from erpnext.stock.get_item_details import get_conversion_factor @@ -434,8 +434,8 @@ def apply_pricing_rule_on_transaction(doc): doc.calculate_taxes_and_totals() elif d.price_or_product_discount == 'Product': - item_details = frappe._dict() - get_product_discount_rule(d, item_details) + item_details = frappe._dict({'parenttype': doc.doctype}) + get_product_discount_rule(d, item_details, doc) apply_pricing_rule_for_free_items(doc, item_details.free_item_data) doc.set_missing_values() @@ -443,7 +443,7 @@ def get_applied_pricing_rules(item_row): return (item_row.get("pricing_rules").split(',') if item_row.get("pricing_rules") else []) -def get_product_discount_rule(pricing_rule, item_details): +def get_product_discount_rule(pricing_rule, item_details, doc=None): free_item = (pricing_rule.free_item if not pricing_rule.same_item or pricing_rule.apply_on == 'Transaction' else item_details.item_code) @@ -467,6 +467,12 @@ def get_product_discount_rule(pricing_rule, item_details): item_details.free_item_data['conversion_factor'] = get_conversion_factor(free_item, item_details.free_item_data['uom']).get("conversion_factor", 1) + if item_details.get("parenttype") == 'Purchase Order': + item_details.free_item_data['schedule_date'] = doc.schedule_date if doc else today() + + if item_details.get("parenttype") == 'Sales Order': + item_details.free_item_data['delivery_date'] = doc.delivery_date if doc else today() + def apply_pricing_rule_for_free_items(doc, pricing_rule_args, set_missing_values=False): if pricing_rule_args.get('item_code'): items = [d.item_code for d in doc.items diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 17fba8ec4ff..3ec7aff9cbb 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -735,6 +735,10 @@ class BuyingController(StockController): if not self.get("items"): return + earliest_schedule_date = min([d.schedule_date for d in self.get("items")]) + if earliest_schedule_date: + self.schedule_date = earliest_schedule_date + if self.schedule_date: for d in self.get('items'): if not d.schedule_date: @@ -746,10 +750,6 @@ class BuyingController(StockController): else: frappe.throw(_("Please enter Reqd by Date")) - earliest_schedule_date = min([d.schedule_date for d in self.get("items")]) - if earliest_schedule_date: - self.schedule_date = earliest_schedule_date - def validate_items(self): # validate items to see if they have is_purchase_item or is_subcontracted_item enabled if self.doctype=="Material Request": return From b8f9fd023bf1d053a6c2e992ea5bae4849269bd5 Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 16 Dec 2019 14:49:59 +0530 Subject: [PATCH 280/299] fix: display serial no selection on adding items to cart --- erpnext/selling/page/point_of_sale/point_of_sale.js | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/selling/page/point_of_sale/point_of_sale.js b/erpnext/selling/page/point_of_sale/point_of_sale.js index b213a29ae7e..33fbc229b6e 100644 --- a/erpnext/selling/page/point_of_sale/point_of_sale.js +++ b/erpnext/selling/page/point_of_sale/point_of_sale.js @@ -286,14 +286,14 @@ erpnext.pos.PointOfSale = class PointOfSale { if (in_list(['serial_no', 'batch_no'], field)) { args[field] = value; } - + // add to cur_frm const item = this.frm.add_child('items', args); frappe.flags.hide_serial_batch_dialog = true; frappe.run_serially([ () => { - this.frm.script_manager.trigger('item_code', item.doctype, item.name) + return this.frm.script_manager.trigger('item_code', item.doctype, item.name) .then(() => { this.frm.script_manager.trigger('qty', item.doctype, item.name) .then(() => { From f8df3c7af26d26fab0396fa4250921f89e5fd1cf Mon Sep 17 00:00:00 2001 From: thefalconx33 Date: Mon, 16 Dec 2019 15:03:27 +0530 Subject: [PATCH 281/299] fix: review changes --- erpnext/portal/product_configurator/utils.py | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/erpnext/portal/product_configurator/utils.py b/erpnext/portal/product_configurator/utils.py index 3ed68362ed2..336b9319c1d 100644 --- a/erpnext/portal/product_configurator/utils.py +++ b/erpnext/portal/product_configurator/utils.py @@ -302,6 +302,8 @@ def get_items(filters=None, search=None): if isinstance(filters, dict): filters = [['Item', fieldname, '=', value] for fieldname, value in filters.items()] + enabled_items_filter = get_conditions({ 'disabled': 0 }, 'and') + show_in_website_condition = '' if products_settings.hide_variants: show_in_website_condition = get_conditions({'show_in_website': 1 }, 'and') @@ -337,12 +339,9 @@ def get_items(filters=None, search=None): filter_condition = get_conditions(filters, 'and') where_conditions = ' and '.join( - [condition for condition in [show_in_website_condition, search_condition, filter_condition] if condition] + [condition for condition in [enabled_items_filter, show_in_website_condition, \ + search_condition, filter_condition] if condition] ) - if where_conditions: - where_conditions += ' and disabled = 0' - else: - where_conditions += 'disabled = 0' left_joins = [] for f in filters: From 666fba94e268f2e7e1ceced3748d01edaed7f847 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 16:18:33 +0530 Subject: [PATCH 282/299] fix: incorrect children boms fetched --- erpnext/manufacturing/doctype/bom/bom.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/manufacturing/doctype/bom/bom.py b/erpnext/manufacturing/doctype/bom/bom.py index 55799544982..723445ad528 100644 --- a/erpnext/manufacturing/doctype/bom/bom.py +++ b/erpnext/manufacturing/doctype/bom/bom.py @@ -65,6 +65,7 @@ class BOM(WebsiteGenerator): context.parents = [{'name': 'boms', 'title': _('All BOMs') }] def on_update(self): + frappe.cache().hdel('bom_children', self.name) self.check_recursion() self.update_stock_qty() self.update_exploded_items() From c76c5e699b42eec98e6cdcdce722c474afa218e8 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Mon, 16 Dec 2019 16:53:00 +0530 Subject: [PATCH 283/299] fix: now allow to over production against work order --- erpnext/manufacturing/doctype/work_order/work_order.js | 2 ++ erpnext/manufacturing/doctype/work_order/work_order.py | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.js b/erpnext/manufacturing/doctype/work_order/work_order.js index 107c79b89bc..1951c271548 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.js +++ b/erpnext/manufacturing/doctype/work_order/work_order.js @@ -581,6 +581,8 @@ erpnext.work_order = { description: __('Max: {0}', [max]), default: max }, data => { + max += (max * (frm.doc.__onload.overproduction_percentage || 0.0)) / 100; + if (data.qty > max) { frappe.msgprint(__('Quantity must not be more than {0}', [max])); reject(); diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 2c16bbe90c5..767d99eda52 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -37,7 +37,7 @@ class WorkOrder(Document): ms = frappe.get_doc("Manufacturing Settings") self.set_onload("material_consumption", ms.material_consumption) self.set_onload("backflush_raw_materials_based_on", ms.backflush_raw_materials_based_on) - + self.set_onload("overproduction_percentage", ms.overproduction_percentage_for_work_order) def validate(self): self.validate_production_item() From 92ecdbe0c81036815a9ec9afd16e8646820f86aa Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Tue, 17 Dec 2019 18:13:54 +0530 Subject: [PATCH 284/299] fix: not able to make work order from BOM --- erpnext/manufacturing/doctype/work_order/work_order.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/erpnext/manufacturing/doctype/work_order/work_order.py b/erpnext/manufacturing/doctype/work_order/work_order.py index 767d99eda52..1cb69012083 100644 --- a/erpnext/manufacturing/doctype/work_order/work_order.py +++ b/erpnext/manufacturing/doctype/work_order/work_order.py @@ -619,8 +619,9 @@ def make_work_order(item, qty=0, project=None): wo_doc = frappe.new_doc("Work Order") wo_doc.production_item = item wo_doc.update(item_details) - if qty > 0: - wo_doc.qty = qty + + if flt(qty) > 0: + wo_doc.qty = flt(qty) wo_doc.get_items_and_operations_from_bom() return wo_doc From b76a04b4706c74c536c0787c5c51c14f657d9891 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Mon, 16 Dec 2019 16:56:57 +0530 Subject: [PATCH 285/299] fix: compensatory leave request creation --- .../compensatory_leave_request.js | 2 +- .../compensatory_leave_request.py | 73 ++++---- .../test_compensatory_leave_request.py | 157 ++++++++++++++---- 3 files changed, 167 insertions(+), 65 deletions(-) diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.js b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.js index 1baa1e04e32..57838423d71 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.js +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.js @@ -19,4 +19,4 @@ frappe.ui.form.on('Compensatory Leave Request', { frm.set_df_property('half_day_date', 'reqd', false); } } -}); +}); \ No newline at end of file diff --git a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py index bc4a1b4034b..7a9727f18c4 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/compensatory_leave_request.py @@ -5,9 +5,10 @@ from __future__ import unicode_literals import frappe from frappe import _ -from frappe.utils import date_diff, add_days, getdate +from frappe.utils import date_diff, add_days, getdate, cint from frappe.model.document import Document -from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, get_holidays_for_employee +from erpnext.hr.utils import validate_dates, validate_overlap, get_leave_period, \ + get_holidays_for_employee, create_additional_leave_ledger_entry class CompensatoryLeaveRequest(Document): @@ -25,16 +26,14 @@ class CompensatoryLeaveRequest(Document): frappe.throw(_("Leave Type is madatory")) def validate_attendance(self): - query = """select attendance_date, status - from `tabAttendance` where - attendance_date between %(work_from_date)s and %(work_end_date)s - and docstatus=1 and status = 'Present' and employee=%(employee)s""" + attendance = frappe.get_all('Attendance', + filters={ + 'attendance_date': ['between', (self.work_from_date, self.work_end_date)], + 'status': 'Present', + 'docstatus': 1, + 'employee': self.employee + }, fields=['attendance_date', 'status']) - attendance = frappe.db.sql(query, { - "work_from_date": self.work_from_date, - "work_end_date": self.work_end_date, - "employee": self.employee - }, as_dict=True) if len(attendance) < date_diff(self.work_end_date, self.work_from_date) + 1: frappe.throw(_("You are not present all day(s) between compensatory leave request days")) @@ -50,13 +49,19 @@ class CompensatoryLeaveRequest(Document): date_difference -= 0.5 leave_period = get_leave_period(self.work_from_date, self.work_end_date, company) if leave_period: - leave_allocation = self.exists_allocation_for_period(leave_period) + leave_allocation = self.get_existing_allocation_for_period(leave_period) if leave_allocation: leave_allocation.new_leaves_allocated += date_difference - leave_allocation.submit() + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) + + # generate additional ledger entry for the new compensatory leaves off + create_additional_leave_ledger_entry(leave_allocation, date_difference, add_days(self.work_end_date, 1)) + else: leave_allocation = self.create_leave_allocation(leave_period, date_difference) - self.db_set("leave_allocation", leave_allocation.name) + self.leave_allocation=leave_allocation.name else: frappe.throw(_("There is no leave period in between {0} and {1}").format(self.work_from_date, self.work_end_date)) @@ -68,11 +73,16 @@ class CompensatoryLeaveRequest(Document): leave_allocation = frappe.get_doc("Leave Allocation", self.leave_allocation) if leave_allocation: leave_allocation.new_leaves_allocated -= date_difference - if leave_allocation.total_leaves_allocated - date_difference <= 0: - leave_allocation.total_leaves_allocated = 0 - leave_allocation.submit() + if leave_allocation.new_leaves_allocated - date_difference <= 0: + leave_allocation.new_leaves_allocated = 0 + leave_allocation.validate() + leave_allocation.db_set("new_leaves_allocated", leave_allocation.total_leaves_allocated) + leave_allocation.db_set("total_leaves_allocated", leave_allocation.total_leaves_allocated) - def exists_allocation_for_period(self, leave_period): + # create reverse entry on cancelation + create_additional_leave_ledger_entry(leave_allocation, date_difference * -1, add_days(self.work_end_date, 1)) + + def get_existing_allocation_for_period(self, leave_period): leave_allocation = frappe.db.sql(""" select name from `tabLeave Allocation` @@ -95,17 +105,18 @@ class CompensatoryLeaveRequest(Document): def create_leave_allocation(self, leave_period, date_difference): is_carry_forward = frappe.db.get_value("Leave Type", self.leave_type, "is_carry_forward") - allocation = frappe.new_doc("Leave Allocation") - allocation.employee = self.employee - allocation.employee_name = self.employee_name - allocation.leave_type = self.leave_type - allocation.from_date = add_days(self.work_end_date, 1) - allocation.to_date = leave_period[0].to_date - allocation.new_leaves_allocated = date_difference - allocation.total_leaves_allocated = date_difference - allocation.description = self.reason - if is_carry_forward == 1: - allocation.carry_forward = True - allocation.save(ignore_permissions = True) + allocation = frappe.get_doc(dict( + doctype="Leave Allocation", + employee=self.employee, + employee_name=self.employee_name, + leave_type=self.leave_type, + from_date=add_days(self.work_end_date, 1), + to_date=leave_period[0].to_date, + carry_forward=cint(is_carry_forward), + new_leaves_allocated=date_difference, + total_leaves_allocated=date_difference, + description=self.reason + )) + allocation.insert(ignore_permissions=True) allocation.submit() - return allocation + return allocation \ No newline at end of file diff --git a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py index f2ca1f4f5f0..1615ab30f1d 100644 --- a/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py +++ b/erpnext/hr/doctype/compensatory_leave_request/test_compensatory_leave_request.py @@ -5,37 +5,128 @@ from __future__ import unicode_literals import frappe import unittest +from frappe.utils import today, add_months, add_days +from erpnext.hr.doctype.attendance_request.test_attendance_request import get_employee +from erpnext.hr.doctype.leave_period.test_leave_period import create_leave_period +from erpnext.hr.doctype.leave_application.leave_application import get_leave_balance_on -# class TestCompensatoryLeaveRequest(unittest.TestCase): -# def get_compensatory_leave_request(self): -# return frappe.get_doc('Compensatory Leave Request', dict( -# employee = employee, -# work_from_date = today, -# work_to_date = today, -# reason = 'test' -# )).insert() -# -# def test_creation_of_leave_allocation(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request(today) -# -# before = get_leave_balance(employee, compensatory_leave_request.leave_type) -# -# compensatory_leave_request.submit() -# -# self.assertEqual(get_leave_balance(employee, compensatory_leave_request.leave_type), before + 1) -# -# def test_max_compensatory_leave(self): -# employee = get_employee() -# today = get_today() -# -# compensatory_leave_request = self.get_compensatory_leave_request() -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 0) -# -# self.assertRaises(MaxLeavesLimitCrossed, compensatory_leave_request.submit) -# -# frappe.db.set_value('Leave Type', compensatory_leave_request.leave_type, 'max_leaves_allowed', 10) -# +class TestCompensatoryLeaveRequest(unittest.TestCase): + def setUp(self): + frappe.db.sql(''' delete from `tabCompensatory Leave Request`''') + frappe.db.sql(''' delete from `tabLeave Ledger Entry`''') + frappe.db.sql(''' delete from `tabLeave Allocation`''') + frappe.db.sql(''' delete from `tabAttendance` where attendance_date in {0} '''.format((today(), add_days(today(), -1)))) #nosec + create_leave_period(add_months(today(), -3), add_months(today(), 3), "_Test Company") + create_holiday_list() + + employee = get_employee() + employee.holiday_list = "_Test Compensatory Leave" + employee.save() + + def test_leave_balance_on_submit(self): + ''' check creation of leave allocation on submission of compensatory leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + + before = get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, today()) + compensatory_leave_request.submit() + + self.assertEqual(get_leave_balance_on(employee.name, compensatory_leave_request.leave_type, add_days(today(), 1)), before + 1) + + def test_leave_allocation_update_on_submit(self): + employee = get_employee() + mark_attendance(employee, date=add_days(today(), -1)) + compensatory_leave_request = get_compensatory_leave_request(employee.name, leave_date=add_days(today(), -1)) + compensatory_leave_request.submit() + + # leave allocation creation on submit + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 1) + + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + # leave allocation updates on submission of second compensatory leave request + leaves_allocated = frappe.db.get_value('Leave Allocation', { + 'name': compensatory_leave_request.leave_allocation + }, ['total_leaves_allocated']) + self.assertEqual(leaves_allocated, 2) + + def test_creation_of_leave_ledger_entry_on_submit(self): + ''' check creation of leave ledger entry on submission of leave request ''' + employee = get_employee() + mark_attendance(employee) + compensatory_leave_request = get_compensatory_leave_request(employee.name) + compensatory_leave_request.submit() + + filters = dict(transaction_name=compensatory_leave_request.leave_allocation) + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters) + + self.assertEquals(len(leave_ledger_entry), 1) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, 1) + + # check reverse leave ledger entry on cancellation + compensatory_leave_request.cancel() + leave_ledger_entry = frappe.get_all('Leave Ledger Entry', fields='*', filters=filters, order_by = 'creation desc') + + self.assertEquals(len(leave_ledger_entry), 2) + self.assertEquals(leave_ledger_entry[0].employee, compensatory_leave_request.employee) + self.assertEquals(leave_ledger_entry[0].leave_type, compensatory_leave_request.leave_type) + self.assertEquals(leave_ledger_entry[0].leaves, -1) + +def get_compensatory_leave_request(employee, leave_date=today()): + prev_comp_leave_req = frappe.db.get_value('Compensatory Leave Request', + dict(leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + employee=employee), 'name') + if prev_comp_leave_req: + return frappe.get_doc('Compensatory Leave Request', prev_comp_leave_req) + + return frappe.get_doc(dict( + doctype='Compensatory Leave Request', + employee=employee, + leave_type='Compensatory Off', + work_from_date=leave_date, + work_end_date=leave_date, + reason='test' + )).insert() + +def mark_attendance(employee, date=today(), status='Present'): + if not frappe.db.exists(dict(doctype='Attendance', employee=employee.name, attendance_date=date, status='Present')): + attendance = frappe.get_doc({ + "doctype": "Attendance", + "employee": employee.name, + "attendance_date": date, + "status": status + }) + attendance.save() + attendance.submit() + +def create_holiday_list(): + if frappe.db.exists("Holiday List", "_Test Compensatory Leave"): + return + + holiday_list = frappe.get_doc({ + "doctype": "Holiday List", + "from_date": add_months(today(), -3), + "to_date": add_months(today(), 3), + "holidays": [ + { + "description": "Test Holiday", + "holiday_date": today() + }, + { + "description": "Test Holiday 1", + "holiday_date": add_days(today(), -1) + } + ], + "holiday_list_name": "_Test Compensatory Leave" + }) + holiday_list.save() \ No newline at end of file From 86600ac8b9015598035dbb6e888ff101d9a3aa75 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Tue, 17 Dec 2019 12:25:25 +0530 Subject: [PATCH 286/299] fix: allow creation of additional leave ledger entry --- .../leave_allocation/leave_allocation.py | 12 ++++++---- .../leave_application/leave_application.py | 23 ++++++++++--------- .../doctype/leave_period/test_leave_period.py | 12 ++++++++-- erpnext/hr/utils.py | 11 +++++---- 4 files changed, 36 insertions(+), 22 deletions(-) diff --git a/erpnext/hr/doctype/leave_allocation/leave_allocation.py b/erpnext/hr/doctype/leave_allocation/leave_allocation.py index 874ae7a1bc2..d13bb4577cd 100755 --- a/erpnext/hr/doctype/leave_allocation/leave_allocation.py +++ b/erpnext/hr/doctype/leave_allocation/leave_allocation.py @@ -69,10 +69,14 @@ class LeaveAllocation(Document): def validate_allocation_overlap(self): leave_allocation = frappe.db.sql(""" - select name from `tabLeave Allocation` - where employee=%s and leave_type=%s and docstatus=1 - and to_date >= %s and from_date <= %s""", - (self.employee, self.leave_type, self.from_date, self.to_date)) + SELECT + name + FROM `tabLeave Allocation` + WHERE + employee=%s AND leave_type=%s + AND name <> %s AND docstatus=1 + AND to_date >= %s AND from_date <= %s""", + (self.employee, self.leave_type, self.name, self.from_date, self.to_date)) if leave_allocation: frappe.msgprint(_("{0} already allocated for Employee {1} for period {2} to {3}") diff --git a/erpnext/hr/doctype/leave_application/leave_application.py b/erpnext/hr/doctype/leave_application/leave_application.py index 0e6630541c4..65fcbf7a999 100755 --- a/erpnext/hr/doctype/leave_application/leave_application.py +++ b/erpnext/hr/doctype/leave_application/leave_application.py @@ -549,10 +549,10 @@ def get_leaves_for_period(employee, leave_type, from_date, to_date): leave_days += leave_entry.leaves elif inclusive_period and leave_entry.transaction_type == 'Leave Allocation' \ - and not skip_expiry_leaves(leave_entry, to_date): + and leave_entry.is_expired and not skip_expiry_leaves(leave_entry, to_date): leave_days += leave_entry.leaves - else: + elif leave_entry.transaction_type == 'Leave Application': if leave_entry.from_date < getdate(from_date): leave_entry.from_date = from_date if leave_entry.to_date > getdate(to_date): @@ -579,14 +579,15 @@ def skip_expiry_leaves(leave_entry, date): def get_leave_entries(employee, leave_type, from_date, to_date): ''' Returns leave entries between from_date and to_date ''' return frappe.db.sql(""" - select employee, leave_type, from_date, to_date, leaves, transaction_type, is_carry_forward, transaction_name - from `tabLeave Ledger Entry` - where employee=%(employee)s and leave_type=%(leave_type)s - and docstatus=1 - and leaves<0 - and (from_date between %(from_date)s and %(to_date)s - or to_date between %(from_date)s and %(to_date)s - or (from_date < %(from_date)s and to_date > %(to_date)s)) + SELECT + employee, leave_type, from_date, to_date, leaves, transaction_name, transaction_type, + is_carry_forward, is_expired + FROM `tabLeave Ledger Entry` + WHERE employee=%(employee)s AND leave_type=%(leave_type)s + AND docstatus=1 AND leaves<0 + AND (from_date between %(from_date)s AND %(to_date)s + OR to_date between %(from_date)s AND %(to_date)s + OR (from_date < %(from_date)s AND to_date > %(to_date)s)) """, { "from_date": from_date, "to_date": to_date, @@ -773,4 +774,4 @@ def get_leave_approver(employee): leave_approver = frappe.db.get_value('Department Approver', {'parent': department, 'parentfield': 'leave_approvers', 'idx': 1}, 'approver') - return leave_approver + return leave_approver \ No newline at end of file diff --git a/erpnext/hr/doctype/leave_period/test_leave_period.py b/erpnext/hr/doctype/leave_period/test_leave_period.py index 850a08dd536..1762cf917a2 100644 --- a/erpnext/hr/doctype/leave_period/test_leave_period.py +++ b/erpnext/hr/doctype/leave_period/test_leave_period.py @@ -43,10 +43,18 @@ class TestLeavePeriod(unittest.TestCase): leave_period.grant_leave_allocation(employee=employee_doc_name) self.assertEqual(get_leave_balance_on(employee_doc_name, leave_type, today()), 20) -def create_leave_period(from_date, to_date): +def create_leave_period(from_date, to_date, company=None): + leave_period = frappe.db.get_value('Leave Period', + dict(company=company or erpnext.get_default_company(), + from_date=from_date, + to_date=to_date, + is_active=1), 'name') + if leave_period: + return frappe.get_doc("Leave Period", leave_period) + leave_period = frappe.get_doc({ "doctype": "Leave Period", - "company": erpnext.get_default_company(), + "company": company or erpnext.get_default_company(), "from_date": from_date, "to_date": to_date, "is_active": 1 diff --git a/erpnext/hr/utils.py b/erpnext/hr/utils.py index 1464a779368..9b0ca4e9a1a 100644 --- a/erpnext/hr/utils.py +++ b/erpnext/hr/utils.py @@ -321,11 +321,11 @@ def allocate_earned_leaves(): if new_allocation == allocation.total_leaves_allocated: continue allocation.db_set("total_leaves_allocated", new_allocation, update_modified=False) - create_earned_leave_ledger_entry(allocation, earned_leaves, today) + create_additional_leave_ledger_entry(allocation, earned_leaves, today) -def create_earned_leave_ledger_entry(allocation, earned_leaves, date): - ''' Create leave ledger entry based on the earned leave frequency ''' - allocation.new_leaves_allocated = earned_leaves +def create_additional_leave_ledger_entry(allocation, leaves, date): + ''' Create leave ledger entry for leave types ''' + allocation.new_leaves_allocated = leaves allocation.from_date = date allocation.unused_leaves = 0 allocation.create_leave_ledger_entry() @@ -389,6 +389,7 @@ def get_sal_slip_total_benefit_given(employee, payroll_period, component=False): def get_holidays_for_employee(employee, start_date, end_date): holiday_list = get_holiday_list_for_employee(employee) + holidays = frappe.db.sql_list('''select holiday_date from `tabHoliday` where parent=%(holiday_list)s @@ -437,4 +438,4 @@ def get_previous_claimed_amount(employee, payroll_period, non_pro_rata=False, co }, as_dict=True) if sum_of_claimed_amount and flt(sum_of_claimed_amount[0].total_amount) > 0: total_claimed_amount = sum_of_claimed_amount[0].total_amount - return total_claimed_amount + return total_claimed_amount \ No newline at end of file From 200ceb5352c986e391e390b968aa2e1192708284 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 12:30:58 +0530 Subject: [PATCH 287/299] use open_mapped_doc instead of create_new_doc --- erpnext/selling/doctype/customer/customer.js | 12 ++-- erpnext/selling/doctype/customer/customer.py | 59 ++++++++++++++++++++ 2 files changed, 65 insertions(+), 6 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.js b/erpnext/selling/doctype/customer/customer.js index cca8efeca4a..68f75a03495 100644 --- a/erpnext/selling/doctype/customer/customer.js +++ b/erpnext/selling/doctype/customer/customer.js @@ -5,13 +5,13 @@ frappe.ui.form.on("Customer", { setup: function(frm) { frm.make_methods = { - 'Quotation': () => erpnext.utils.create_new_doc('Quotation', { - 'quotation_to': frm.doc.doctype, - 'party_name': frm.doc.name + 'Quotation': () => frappe.model.open_mapped_doc({ + method: 'erpnext.selling.doctype.customer.customer.make_quotation', + frm: cur_frm }), - 'Opportunity': () => erpnext.utils.create_new_doc('Opportunity', { - 'opportunity_from': frm.doc.doctype, - 'party_name': frm.doc.name + 'Opportunity': () => frappe.model.open_mapped_doc({ + method: 'erpnext.selling.doctype.customer.customer.make_opportunity', + frm: cur_frm }) } diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 67e20b1e891..9cd41be22a9 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -12,6 +12,7 @@ from erpnext.utilities.transaction_base import TransactionBase from erpnext.accounts.party import validate_party_accounts, get_dashboard_info, get_timeline_data # keep this from frappe.contacts.address_and_contact import load_address_and_contact, delete_contact_and_address from frappe.model.rename_doc import update_linked_doctypes +from frappe.model.mapper import get_mapped_doc class Customer(TransactionBase): def get_feed(self): @@ -204,6 +205,64 @@ class Customer(TransactionBase): else: frappe.msgprint(_("Multiple Loyalty Program found for the Customer. Please select manually.")) +@frappe.whitelist() +def make_quotation(source_name, target_doc=None): + + def set_missing_values(source, target): + _set_missing_values(source,target) + + target_doc = get_mapped_doc("Customer", source_name, + {"Customer": { + "doctype": "Quotation", + "field_map": { + "name":"party_name" + } + }}, target_doc, set_missing_values) + + target_doc.quotation_to = "Customer" + target_doc.run_method("set_missing_values") + target_doc.run_method("set_other_charges") + target_doc.run_method("calculate_taxes_and_totals") + + target_doc.selling_price_list = frappe.get_doc("Customer",source_name).default_price_list + return target_doc + +@frappe.whitelist() +def make_opportunity(source_name, target_doc=None): + def set_missing_values(source, target): + _set_missing_values(source,target) + + target_doc = get_mapped_doc("Customer", source_name, + {"Customer": { + "doctype": "Opportunity", + "field_map": { + "name": "party_name", + "doctype": "opportunity_from", + } + }}, target_doc, set_missing_values + ) + + return target_doc + +def _set_missing_values(source, target): + address = frappe.get_all('Dynamic Link', { + 'link_doctype': source.doctype, + 'link_name': source.name, + 'parenttype': 'Address', + }, ['parent'], limit=1) + + contact = frappe.get_all('Dynamic Link', { + 'link_doctype': source.doctype, + 'link_name': source.name, + 'parenttype': 'Contact', + }, ['parent'], limit=1) + + if address: + target.customer_address = address[0].parent + + if contact: + target.contact_person = contact[0].parent + @frappe.whitelist() def get_loyalty_programs(doc): ''' returns applicable loyalty programs for a customer ''' From b3addff99e892e3c5456a43bb9d09d20fc68ec91 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Dec 2019 15:29:28 +0530 Subject: [PATCH 288/299] v12_3_0 change log --- erpnext/change_log/v12/v12_3_0.md | 33 +++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) create mode 100644 erpnext/change_log/v12/v12_3_0.md diff --git a/erpnext/change_log/v12/v12_3_0.md b/erpnext/change_log/v12/v12_3_0.md new file mode 100644 index 00000000000..6ac71df564f --- /dev/null +++ b/erpnext/change_log/v12/v12_3_0.md @@ -0,0 +1,33 @@ +# Version 12.3.0 Release Notes + +### Accounting + +1. Statewise GST taxation for India + - Added GST state in the tax category + - Added tax category in the address, sales/purchase tax template + - Based on the address system will fetch the tax template +2. Accounts Payable report based on payment terms +3. Trial Balance Report with filter "Party Name" +4. Fixed asset register report with date filters + +### CRM + +1. Appointment Scheduling + - Configure the appointment slots using Appointment Booking Settings + - Users can book the appointment through the portal based on slot availability + +### HR + +1. Refactored Employee Attendance Tool +2. Set allocated amount in employee advance as per total amount + +### Fixes + +1. Stock entry decimal issue while creating the GL entries +2. Item wise stock balance report +3. Valuation of subcontracting finished good item +4. Not able to create Instructor, Student entries +5. Pricing rule for a product discount +6. POS for serialized items +7. Not able to cancel share transfer entry +8. Ledger entries for compensatory off were not getting created From 6e4147561247a83a03232a34f95566971d62b7f0 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:02:16 +0530 Subject: [PATCH 289/299] fix : only set price list if it exists for customer --- erpnext/selling/doctype/customer/customer.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 9cd41be22a9..7394bd98e46 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -224,7 +224,10 @@ def make_quotation(source_name, target_doc=None): target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") - target_doc.selling_price_list = frappe.get_doc("Customer",source_name).default_price_list + price_list = frappe.get_doc("Customer", source_name, "default_price_list") + if price_list: + target_doc.selling_price_list = price_list + return target_doc @frappe.whitelist() From 4d7862ef4c140afe230323248f8395cfa5c580e9 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:27:16 +0530 Subject: [PATCH 290/299] fix: defualt timezone not getting selected --- erpnext/www/book_appointment/index.js | 9 ++------- erpnext/www/book_appointment/index.py | 14 ++------------ 2 files changed, 4 insertions(+), 19 deletions(-) diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index c8dd5013d5c..44f52ff6690 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -24,20 +24,15 @@ async function get_global_variables() { } function setup_timezone_selector() { - /** - * window.timezones is a dictionary with the following structure - * { IANA name: Pretty name} - * For example : { Asia/Kolkata : "India Time - Asia/Kolkata"} - */ let timezones_element = document.getElementById('appointment-timezone'); let offset = new Date().getTimezoneOffset(); - Object.keys(window.timezones).forEach((timezone) => { + window.timezones.forEach((timezone) => { let opt = document.createElement('option'); opt.value = timezone; if (timezone == moment.tz.guess()) { opt.selected = true; } - opt.innerHTML = window.timezones[timezone] + opt.innerHTML = timezone timezones_element.appendChild(opt) }); } diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 5b60dd5e7b7..3382dabf512 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -25,18 +25,8 @@ def get_appointment_settings(): @frappe.whitelist(allow_guest=True) def get_timezones(): - from babel.dates import get_timezone, get_timezone_name, Locale - from frappe.utils.momentjs import get_all_timezones - - translated_dict = {} - locale = Locale.parse(frappe.local.lang, sep="-") - - for tz in get_all_timezones(): - timezone_name = get_timezone_name(get_timezone(tz), locale=locale, width='short') - if timezone_name: - translated_dict[tz] = timezone_name + ' - ' + tz - - return translated_dict + import pytz + return pytz.all_timezones @frappe.whitelist(allow_guest=True) def get_appointment_slots(date, timezone): From 5b4050a4ff3c31f8374cbae4baa538197522662b Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Wed, 18 Dec 2019 16:30:54 +0530 Subject: [PATCH 291/299] add link to appointment booking in sidebar --- erpnext/hooks.py | 1 + 1 file changed, 1 insertion(+) diff --git a/erpnext/hooks.py b/erpnext/hooks.py index e4b5e3012f5..eb1543188a9 100644 --- a/erpnext/hooks.py +++ b/erpnext/hooks.py @@ -182,6 +182,7 @@ standard_portal_menu_items = [ {"title": _("Admission"), "route": "/admissions", "reference_doctype": "Student Admission", "role": "Student"}, {"title": _("Certification"), "route": "/certification", "reference_doctype": "Certification Application", "role": "Non Profit Portal User"}, {"title": _("Material Request"), "route": "/material-requests", "reference_doctype": "Material Request", "role": "Customer"}, + {"title": _("Appointment Booking"), "route": "/book_appointment"}, ] default_roles = [ From fecf5a9a1593f23ce469eaac13f05c5eae1e5a64 Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Wed, 18 Dec 2019 17:48:39 +0530 Subject: [PATCH 292/299] fix: Pricing Rule Discount for Product --- erpnext/accounts/doctype/pricing_rule/pricing_rule.json | 9 +++------ erpnext/accounts/doctype/pricing_rule/pricing_rule.py | 3 +++ erpnext/public/js/controllers/transaction.js | 3 +++ 3 files changed, 9 insertions(+), 6 deletions(-) diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json index f73fb10d320..29d83783d07 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.json +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.json @@ -1,5 +1,4 @@ { - "actions": [], "allow_import": 1, "allow_rename": 1, "autoname": "field:title", @@ -390,8 +389,7 @@ "fieldname": "rate_or_discount", "fieldtype": "Select", "label": "Rate or Discount", - "options": "\nRate\nDiscount Percentage\nDiscount Amount", - "reqd": 1 + "options": "\nRate\nDiscount Percentage\nDiscount Amount" }, { "default": "Grand Total", @@ -440,7 +438,7 @@ }, { "default": "0", - "depends_on": "eval:!doc.mixed_conditions && doc.price_or_product_discount == 'Price'", + "depends_on": "eval:!doc.mixed_conditions && doc.apply_on != 'Transaction'", "fieldname": "same_item", "fieldtype": "Check", "label": "Same Item" @@ -556,8 +554,7 @@ ], "icon": "fa fa-gift", "idx": 1, - "links": [], - "modified": "2019-12-13 15:48:48.331495", + "modified": "2019-12-18 17:29:22.957077", "modified_by": "Administrator", "module": "Accounts", "name": "Pricing Rule", diff --git a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py index 03641028e6f..5fd933b3d98 100644 --- a/erpnext/accounts/doctype/pricing_rule/pricing_rule.py +++ b/erpnext/accounts/doctype/pricing_rule/pricing_rule.py @@ -48,6 +48,9 @@ class PricingRule(Document): if tocheck and not self.get(tocheck): throw(_("{0} is required").format(self.meta.get_label(tocheck)), frappe.MandatoryError) + if self.price_or_product_discount == 'Price' and not self.rate_or_discount: + throw(_("Rate or Discount is required for the price discount."), frappe.MandatoryError) + def validate_applicable_for_selling_or_buying(self): if not self.selling and not self.buying: throw(_("Atleast one of the Selling or Buying must be selected")) diff --git a/erpnext/public/js/controllers/transaction.js b/erpnext/public/js/controllers/transaction.js index 877a243967c..c735f840769 100644 --- a/erpnext/public/js/controllers/transaction.js +++ b/erpnext/public/js/controllers/transaction.js @@ -500,6 +500,9 @@ erpnext.TransactionController = erpnext.taxes_and_totals.extend({ () => { var d = locals[cdt][cdn]; me.add_taxes_from_item_tax_template(d.item_tax_rate); + if (d.free_item_data) { + me.apply_product_discount(d.free_item_data); + } }, () => me.frm.script_manager.trigger("price_list_rate", cdt, cdn), () => me.toggle_conversion_factor(item), From 5cc0e08a4116bbac7ef623491339935e090ef315 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 19 Dec 2019 11:07:43 +0530 Subject: [PATCH 293/299] fix: Use get_value instead of get_doc and formatting --- erpnext/selling/doctype/customer/customer.py | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/erpnext/selling/doctype/customer/customer.py b/erpnext/selling/doctype/customer/customer.py index 7394bd98e46..6ee02190bad 100644 --- a/erpnext/selling/doctype/customer/customer.py +++ b/erpnext/selling/doctype/customer/customer.py @@ -209,9 +209,9 @@ class Customer(TransactionBase): def make_quotation(source_name, target_doc=None): def set_missing_values(source, target): - _set_missing_values(source,target) + _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, + target_doc = get_mapped_doc("Customer", source_name, {"Customer": { "doctype": "Quotation", "field_map": { @@ -224,7 +224,7 @@ def make_quotation(source_name, target_doc=None): target_doc.run_method("set_other_charges") target_doc.run_method("calculate_taxes_and_totals") - price_list = frappe.get_doc("Customer", source_name, "default_price_list") + price_list = frappe.get_value("Customer", source_name, "default_price_list") if price_list: target_doc.selling_price_list = price_list @@ -233,17 +233,16 @@ def make_quotation(source_name, target_doc=None): @frappe.whitelist() def make_opportunity(source_name, target_doc=None): def set_missing_values(source, target): - _set_missing_values(source,target) + _set_missing_values(source, target) - target_doc = get_mapped_doc("Customer", source_name, + target_doc = get_mapped_doc("Customer", source_name, {"Customer": { "doctype": "Opportunity", "field_map": { "name": "party_name", "doctype": "opportunity_from", } - }}, target_doc, set_missing_values - ) + }}, target_doc, set_missing_values) return target_doc From 0c8e46fdea127c67557582a74cac63bca9755450 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 19 Dec 2019 12:25:29 +0530 Subject: [PATCH 294/299] fix: remove timezones in js --- erpnext/www/book_appointment/index.js | 2 +- erpnext/www/book_appointment/index.py | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index 44f52ff6690..b13f18893c1 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -109,7 +109,7 @@ function get_timeslot_div_layout(timeslot) { timeslot_div.classList.add('unavailable') } timeslot_div.innerHTML = get_slot_layout(start_time); - timeslot_div.id = timeslot.time.substr(11, 20); + timeslot_div.id = timeslot.time.substring(11, 19); timeslot_div.addEventListener('click', select_time); return timeslot_div } diff --git a/erpnext/www/book_appointment/index.py b/erpnext/www/book_appointment/index.py index 3382dabf512..7bfac89f308 100644 --- a/erpnext/www/book_appointment/index.py +++ b/erpnext/www/book_appointment/index.py @@ -80,7 +80,7 @@ def get_available_slots_between(query_start_time, query_end_time, settings): @frappe.whitelist(allow_guest=True) def create_appointment(date, time, tz, contact): - format_string = '%Y-%m-%d %H:%M:%S%z' + format_string = '%Y-%m-%d %H:%M:%S' 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) From 220a208f4e77578ef5197e5d26f0da446ee25840 Mon Sep 17 00:00:00 2001 From: 0Pranav Date: Thu, 19 Dec 2019 12:27:32 +0530 Subject: [PATCH 295/299] fix: default timezone selection --- erpnext/www/book_appointment/index.js | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/erpnext/www/book_appointment/index.js b/erpnext/www/book_appointment/index.js index b13f18893c1..3c296980688 100644 --- a/erpnext/www/book_appointment/index.js +++ b/erpnext/www/book_appointment/index.js @@ -25,15 +25,15 @@ async function get_global_variables() { function setup_timezone_selector() { let timezones_element = document.getElementById('appointment-timezone'); - let offset = new Date().getTimezoneOffset(); + let local_timezone = moment.tz.guess() window.timezones.forEach((timezone) => { let opt = document.createElement('option'); opt.value = timezone; - if (timezone == moment.tz.guess()) { + if (timezone == local_timezone) { opt.selected = true; } - opt.innerHTML = timezone - timezones_element.appendChild(opt) + opt.innerHTML = timezone; + timezones_element.appendChild(opt); }); } From 55bf951ff57b0c6b41ce950e21c631e47eed7e01 Mon Sep 17 00:00:00 2001 From: deepeshgarg007 Date: Thu, 19 Dec 2019 20:57:40 +0530 Subject: [PATCH 296/299] fix: Tax amount not visible for some invoices --- erpnext/regional/report/gstr_2/gstr_2.py | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/erpnext/regional/report/gstr_2/gstr_2.py b/erpnext/regional/report/gstr_2/gstr_2.py index a3622690078..f326fe07cac 100644 --- a/erpnext/regional/report/gstr_2/gstr_2.py +++ b/erpnext/regional/report/gstr_2/gstr_2.py @@ -44,12 +44,16 @@ class Gstr2Report(Gstr1Report): for inv, items_based_on_rate in self.items_based_on_tax_rate.items(): invoice_details = self.invoices.get(inv) for rate, items in items_based_on_rate.items(): - row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) - tax_amount = taxable_value * rate / 100 - if inv in self.igst_invoices: - row += [tax_amount, 0, 0] + if inv not in self.igst_invoices: + rate = rate / 2 + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [0, tax_amount, tax_amount] else: - row += [0, tax_amount / 2, tax_amount / 2] + row, taxable_value = self.get_row_data_for_invoice(inv, invoice_details, rate, items) + tax_amount = taxable_value * rate / 100 + row += [tax_amount, 0, 0] + row += [ self.invoice_cess.get(inv), From 39436c6d38812078530a35c35ab28d0ba46c588f Mon Sep 17 00:00:00 2001 From: Rohit Waghchaure Date: Fri, 20 Dec 2019 12:56:01 +0530 Subject: [PATCH 297/299] fix: incorrect consumed qty for partial purchase receipt in subcontracting --- erpnext/controllers/buying_controller.py | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/erpnext/controllers/buying_controller.py b/erpnext/controllers/buying_controller.py index 3ec7aff9cbb..75b896bb134 100644 --- a/erpnext/controllers/buying_controller.py +++ b/erpnext/controllers/buying_controller.py @@ -265,16 +265,17 @@ class BuyingController(StockController): fg_yet_to_be_received = qty_to_be_received_map.get(item_key) - raw_material_data = backflushed_raw_materials_map.get(item_key, {}) - - consumed_qty = raw_material_data.get('qty', 0) - consumed_serial_nos = raw_material_data.get('serial_nos', '') - consumed_batch_nos = raw_material_data.get('batch_nos', '') - transferred_batch_qty_map = get_transferred_batch_qty_map(item.purchase_order, item.item_code) backflushed_batch_qty_map = get_backflushed_batch_qty_map(item.purchase_order, item.item_code) for raw_material in transferred_raw_materials + non_stock_items: + rm_item_key = '{}{}'.format(raw_material.rm_item_code, item.purchase_order) + raw_material_data = backflushed_raw_materials_map.get(rm_item_key, {}) + + consumed_qty = raw_material_data.get('qty', 0) + consumed_serial_nos = raw_material_data.get('serial_nos', '') + consumed_batch_nos = raw_material_data.get('batch_nos', '') + transferred_qty = raw_material.qty rm_qty_to_be_consumed = transferred_qty - consumed_qty From 328d5920bdb7e6b325f847c4837bf7a539bf7ba9 Mon Sep 17 00:00:00 2001 From: Mangesh-Khairnar Date: Fri, 20 Dec 2019 15:26:49 +0530 Subject: [PATCH 298/299] fix(expense-claim): update status (#20033) * fix(expense-claim): update status * fix(expense-claim): compare using grandtotal precision --- erpnext/hr/doctype/expense_claim/expense_claim.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/erpnext/hr/doctype/expense_claim/expense_claim.py b/erpnext/hr/doctype/expense_claim/expense_claim.py index 59391505fa0..ab9a2b22102 100644 --- a/erpnext/hr/doctype/expense_claim/expense_claim.py +++ b/erpnext/hr/doctype/expense_claim/expense_claim.py @@ -43,9 +43,9 @@ class ExpenseClaim(AccountsController): }[cstr(self.docstatus or 0)] paid_amount = flt(self.total_amount_reimbursed) + flt(self.total_advance_amount) - precision = self.precision("total_sanctioned_amount") + precision = self.precision("grand_total") if (self.is_paid or (flt(self.total_sanctioned_amount) > 0 - and flt(self.total_sanctioned_amount, precision) == flt(paid_amount, precision))) \ + and flt(self.grand_total, precision) == flt(paid_amount, precision))) \ and self.docstatus == 1 and self.approval_status == 'Approved': self.status = "Paid" elif flt(self.total_sanctioned_amount) > 0 and self.docstatus == 1 and self.approval_status == 'Approved': From 67d25028b259cc71fe28ddc0cb34aae6c44d5011 Mon Sep 17 00:00:00 2001 From: Sahil Khan Date: Fri, 20 Dec 2019 16:15:12 +0550 Subject: [PATCH 299/299] bumped to version 12.3.0 --- erpnext/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/erpnext/__init__.py b/erpnext/__init__.py index f702dc90b46..41ee03971f8 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.2.2' +__version__ = '12.3.0' def get_default_company(user=None): '''Get default company for user'''